Normalize thinking option IDs and update display logic
- Remove synthetic "default" thinking option ID, use concrete option IDs from providers - Normalize thinking option IDs to undefined when "default" or empty string - Update app UI to display "Model default" label for fallback thinking option - Fix Codex thinking option round-trip through app-server turn context - Add test for thinking option ID persistence across Codex sessions - Update test helpers to use actual default thinking option IDs - Update OpenCode label consistency to match app display logic
This commit is contained in:
@@ -71,12 +71,16 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
|
|||||||
: agent.model ?? "default";
|
: agent.model ?? "default";
|
||||||
|
|
||||||
const thinkingOptions = selectedModel?.thinkingOptions ?? null;
|
const thinkingOptions = selectedModel?.thinkingOptions ?? null;
|
||||||
|
const explicitThinkingId =
|
||||||
|
agent.thinkingOptionId && agent.thinkingOptionId !== "default"
|
||||||
|
? agent.thinkingOptionId
|
||||||
|
: null;
|
||||||
const selectedThinkingId =
|
const selectedThinkingId =
|
||||||
agent.thinkingOptionId ??
|
explicitThinkingId ?? selectedModel?.defaultThinkingOptionId ?? null;
|
||||||
selectedModel?.defaultThinkingOptionId ??
|
|
||||||
"default";
|
|
||||||
const selectedThinking = thinkingOptions?.find((o) => o.id === selectedThinkingId) ?? null;
|
const selectedThinking = thinkingOptions?.find((o) => o.id === selectedThinkingId) ?? null;
|
||||||
const displayThinking = selectedThinking?.label ?? selectedThinkingId ?? "default";
|
const displayThinking =
|
||||||
|
selectedThinking?.label ??
|
||||||
|
(selectedThinkingId === "default" ? "Model default" : selectedThinkingId ?? "auto");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
@@ -201,7 +205,7 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void client
|
void client
|
||||||
.setAgentThinkingOption(agentId, opt.id === "default" ? null : opt.id)
|
.setAgentThinkingOption(agentId, opt.id)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.warn("[AgentStatusBar] setAgentThinkingOption failed", error);
|
console.warn("[AgentStatusBar] setAgentThinkingOption failed", error);
|
||||||
});
|
});
|
||||||
@@ -289,22 +293,22 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
|
|||||||
accessibilityLabel="Select thinking option"
|
accessibilityLabel="Select thinking option"
|
||||||
testID="agent-preferences-thinking"
|
testID="agent-preferences-thinking"
|
||||||
>
|
>
|
||||||
<Text style={styles.sheetSelectText}>{displayThinking}</Text>
|
<Text style={styles.sheetSelectText}>{displayThinking}</Text>
|
||||||
<ChevronDown size={16} color={theme.colors.foregroundMuted} />
|
<ChevronDown size={16} color={theme.colors.foregroundMuted} />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent side="top" align="start">
|
<DropdownMenuContent side="top" align="start">
|
||||||
{thinkingOptions.map((opt) => {
|
{thinkingOptions.map((opt) => {
|
||||||
const isActive = opt.id === selectedThinkingId;
|
const isActive = opt.id === selectedThinkingId;
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
key={opt.id}
|
key={opt.id}
|
||||||
selected={isActive}
|
selected={isActive}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
if (!client) {
|
if (!client) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void client
|
void client
|
||||||
.setAgentThinkingOption(agentId, opt.id === "default" ? null : opt.id)
|
.setAgentThinkingOption(agentId, opt.id)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.warn("[AgentStatusBar] setAgentThinkingOption failed", error);
|
console.warn("[AgentStatusBar] setAgentThinkingOption failed", error);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -90,6 +90,49 @@ async function waitForFileToContainText(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readRolloutTurnContextEfforts(rolloutPath: string): string[] {
|
||||||
|
const lines = readFileSync(rolloutPath, "utf8")
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => line.length > 0);
|
||||||
|
|
||||||
|
const efforts: string[] = [];
|
||||||
|
for (const line of lines) {
|
||||||
|
let parsed: {
|
||||||
|
type?: string;
|
||||||
|
payload?: {
|
||||||
|
effort?: string;
|
||||||
|
reasoning_effort?: string;
|
||||||
|
collaboration_mode?: { settings?: { reasoning_effort?: string } };
|
||||||
|
};
|
||||||
|
} | null = null;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(line) as {
|
||||||
|
type?: string;
|
||||||
|
payload?: {
|
||||||
|
effort?: string;
|
||||||
|
reasoning_effort?: string;
|
||||||
|
collaboration_mode?: { settings?: { reasoning_effort?: string } };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed?.type !== "turn_context") continue;
|
||||||
|
const effort =
|
||||||
|
parsed.payload?.effort ??
|
||||||
|
parsed.payload?.reasoning_effort ??
|
||||||
|
parsed.payload?.collaboration_mode?.settings?.reasoning_effort ??
|
||||||
|
null;
|
||||||
|
if (typeof effort === "string" && effort.length > 0) {
|
||||||
|
efforts.push(effort);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return efforts;
|
||||||
|
}
|
||||||
|
|
||||||
describe("Codex app-server provider (integration)", () => {
|
describe("Codex app-server provider (integration)", () => {
|
||||||
const logger = createTestLogger();
|
const logger = createTestLogger();
|
||||||
|
|
||||||
@@ -117,6 +160,28 @@ describe("Codex app-server provider (integration)", () => {
|
|||||||
expect(models.some((model) => model.id.includes("gpt-5.1-codex"))).toBe(true);
|
expect(models.some((model) => model.id.includes("gpt-5.1-codex"))).toBe(true);
|
||||||
}, 30000);
|
}, 30000);
|
||||||
|
|
||||||
|
test.runIf(isCodexInstalled())(
|
||||||
|
"listModels exposes concrete thinking options (no synthetic default id)",
|
||||||
|
async () => {
|
||||||
|
const client = new CodexAppServerAgentClient(logger);
|
||||||
|
const models = await client.listModels();
|
||||||
|
|
||||||
|
for (const model of models) {
|
||||||
|
const options = model.thinkingOptions ?? [];
|
||||||
|
for (const option of options) {
|
||||||
|
expect(option.id).not.toBe("default");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.length > 0) {
|
||||||
|
const defaultThinkingId = model.defaultThinkingOptionId;
|
||||||
|
expect(typeof defaultThinkingId).toBe("string");
|
||||||
|
expect(options.some((option) => option.id === defaultThinkingId)).toBe(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
30000
|
||||||
|
);
|
||||||
|
|
||||||
test.runIf(isCodexInstalled())("accepts image prompt blocks without request validation errors", async () => {
|
test.runIf(isCodexInstalled())("accepts image prompt blocks without request validation errors", async () => {
|
||||||
const cleanup = useTempCodexSessionDir();
|
const cleanup = useTempCodexSessionDir();
|
||||||
const cwd = tmpCwd("codex-image-prompt-");
|
const cwd = tmpCwd("codex-image-prompt-");
|
||||||
@@ -170,6 +235,78 @@ describe("Codex app-server provider (integration)", () => {
|
|||||||
}
|
}
|
||||||
}, 120000);
|
}, 120000);
|
||||||
|
|
||||||
|
test.runIf(isCodexInstalled())(
|
||||||
|
"thinking option changes round-trip through Codex app-server turn context",
|
||||||
|
async () => {
|
||||||
|
const cleanup = useTempCodexSessionDir();
|
||||||
|
const cwd = tmpCwd("codex-thinking-roundtrip-");
|
||||||
|
let session: Awaited<ReturnType<CodexAppServerAgentClient["createSession"]>> | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = new CodexAppServerAgentClient(logger);
|
||||||
|
const models = await client.listModels();
|
||||||
|
const modelWithThinking = models.find((m) => (m.thinkingOptions?.length ?? 0) > 1);
|
||||||
|
if (!modelWithThinking) {
|
||||||
|
throw new Error("No Codex model with at least two non-default thinking options");
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultThinkingId = modelWithThinking.defaultThinkingOptionId ?? null;
|
||||||
|
const thinkingIds = (modelWithThinking.thinkingOptions ?? []).map((opt) => opt.id);
|
||||||
|
if (thinkingIds.length < 2) {
|
||||||
|
throw new Error("No Codex model with at least two non-default thinking options");
|
||||||
|
}
|
||||||
|
const initialThinkingId = defaultThinkingId ?? thinkingIds[0]!;
|
||||||
|
const switchedThinkingId =
|
||||||
|
thinkingIds.find((id) => id !== initialThinkingId) ?? thinkingIds[0]!;
|
||||||
|
|
||||||
|
session = await client.createSession({
|
||||||
|
provider: "codex",
|
||||||
|
cwd,
|
||||||
|
modeId: "auto",
|
||||||
|
model: modelWithThinking.id,
|
||||||
|
thinkingOptionId: initialThinkingId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await session.run("Reply with exactly OK.");
|
||||||
|
await session.setThinkingOption?.(switchedThinkingId);
|
||||||
|
await session.run("Reply with exactly OK.");
|
||||||
|
|
||||||
|
const internal = session as unknown as {
|
||||||
|
client?: {
|
||||||
|
request: (method: string, params: unknown) => Promise<unknown>;
|
||||||
|
};
|
||||||
|
currentThreadId?: string | null;
|
||||||
|
};
|
||||||
|
const threadId = internal.currentThreadId;
|
||||||
|
const codexClient = internal.client;
|
||||||
|
if (!threadId || !codexClient) {
|
||||||
|
throw new Error("Codex session did not initialize app-server client/thread");
|
||||||
|
}
|
||||||
|
|
||||||
|
const threadRead = (await codexClient.request("thread/read", {
|
||||||
|
threadId,
|
||||||
|
includeTurns: true,
|
||||||
|
})) as { thread?: { path?: string } };
|
||||||
|
const rolloutPath = threadRead.thread?.path;
|
||||||
|
if (!rolloutPath) {
|
||||||
|
throw new Error("Codex app-server did not return rollout path");
|
||||||
|
}
|
||||||
|
|
||||||
|
const efforts = readRolloutTurnContextEfforts(rolloutPath);
|
||||||
|
const initialIndex = efforts.lastIndexOf(initialThinkingId);
|
||||||
|
const switchedIndex = efforts.lastIndexOf(switchedThinkingId);
|
||||||
|
|
||||||
|
expect(initialIndex).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(switchedIndex).toBeGreaterThan(initialIndex);
|
||||||
|
} finally {
|
||||||
|
await session?.close().catch(() => undefined);
|
||||||
|
cleanup();
|
||||||
|
rmSync(cwd, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
120000
|
||||||
|
);
|
||||||
|
|
||||||
test.runIf(isCodexInstalled())("round-trips a stdio MCP tool call", async () => {
|
test.runIf(isCodexInstalled())("round-trips a stdio MCP tool call", async () => {
|
||||||
const cleanup = useTempCodexSessionDir();
|
const cleanup = useTempCodexSessionDir();
|
||||||
const cwd = tmpCwd("codex-mcp-roundtrip-");
|
const cwd = tmpCwd("codex-mcp-roundtrip-");
|
||||||
|
|||||||
@@ -101,6 +101,19 @@ function validateCodexMode(modeId: string): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeCodexThinkingOptionId(
|
||||||
|
thinkingOptionId: string | null | undefined
|
||||||
|
): string | undefined {
|
||||||
|
if (typeof thinkingOptionId !== "string") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const normalized = thinkingOptionId.trim();
|
||||||
|
if (!normalized || normalized === "default") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
function resolveCodexBinary(): string {
|
function resolveCodexBinary(): string {
|
||||||
try {
|
try {
|
||||||
const codexPath = execSync("which codex", { encoding: "utf8" }).trim();
|
const codexPath = execSync("which codex", { encoding: "utf8" }).trim();
|
||||||
@@ -1162,6 +1175,7 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
validateCodexMode(config.modeId);
|
validateCodexMode(config.modeId);
|
||||||
this.currentMode = config.modeId;
|
this.currentMode = config.modeId;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
this.config.thinkingOptionId = normalizeCodexThinkingOptionId(this.config.thinkingOptionId);
|
||||||
|
|
||||||
if (this.resumeHandle?.sessionId) {
|
if (this.resumeHandle?.sessionId) {
|
||||||
this.currentThreadId = this.resumeHandle.sessionId;
|
this.currentThreadId = this.resumeHandle.sessionId;
|
||||||
@@ -1277,7 +1291,7 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
.join("\n\n");
|
.join("\n\n");
|
||||||
if (developerInstructions) settings.developer_instructions = developerInstructions;
|
if (developerInstructions) settings.developer_instructions = developerInstructions;
|
||||||
if (this.config.model) settings.model = this.config.model;
|
if (this.config.model) settings.model = this.config.model;
|
||||||
const thinkingOptionId = this.config.thinkingOptionId;
|
const thinkingOptionId = normalizeCodexThinkingOptionId(this.config.thinkingOptionId);
|
||||||
if (thinkingOptionId) settings.reasoning_effort = thinkingOptionId;
|
if (thinkingOptionId) settings.reasoning_effort = thinkingOptionId;
|
||||||
return { mode: match.mode ?? "code", settings, name: match.name };
|
return { mode: match.mode ?? "code", settings, name: match.name };
|
||||||
}
|
}
|
||||||
@@ -1430,7 +1444,7 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
if (this.config.model) {
|
if (this.config.model) {
|
||||||
params.model = this.config.model;
|
params.model = this.config.model;
|
||||||
}
|
}
|
||||||
const thinkingOptionId = this.config.thinkingOptionId;
|
const thinkingOptionId = normalizeCodexThinkingOptionId(this.config.thinkingOptionId);
|
||||||
if (thinkingOptionId) {
|
if (thinkingOptionId) {
|
||||||
params.effort = thinkingOptionId;
|
params.effort = thinkingOptionId;
|
||||||
}
|
}
|
||||||
@@ -1528,7 +1542,7 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setThinkingOption(thinkingOptionId: string | null): Promise<void> {
|
async setThinkingOption(thinkingOptionId: string | null): Promise<void> {
|
||||||
this.config.thinkingOptionId = thinkingOptionId ?? undefined;
|
this.config.thinkingOptionId = normalizeCodexThinkingOptionId(thinkingOptionId);
|
||||||
this.resolvedCollaborationMode = this.resolveCollaborationMode(this.currentMode);
|
this.resolvedCollaborationMode = this.resolveCollaborationMode(this.currentMode);
|
||||||
this.cachedRuntimeInfo = null;
|
this.cachedRuntimeInfo = null;
|
||||||
}
|
}
|
||||||
@@ -1599,7 +1613,7 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
|
|
||||||
describePersistence(): { provider: typeof CODEX_PROVIDER; sessionId: string; nativeHandle: string; metadata: Record<string, unknown> } | null {
|
describePersistence(): { provider: typeof CODEX_PROVIDER; sessionId: string; nativeHandle: string; metadata: Record<string, unknown> } | null {
|
||||||
if (!this.currentThreadId) return null;
|
if (!this.currentThreadId) return null;
|
||||||
const thinkingOptionId = this.config.thinkingOptionId ?? null;
|
const thinkingOptionId = normalizeCodexThinkingOptionId(this.config.thinkingOptionId) ?? null;
|
||||||
return {
|
return {
|
||||||
provider: CODEX_PROVIDER,
|
provider: CODEX_PROVIDER,
|
||||||
sessionId: this.currentThreadId,
|
sessionId: this.currentThreadId,
|
||||||
@@ -2123,37 +2137,58 @@ export class CodexAppServerAgentClient implements AgentClient {
|
|||||||
|
|
||||||
const response = (await client.request("model/list", {})) as { data?: Array<any> };
|
const response = (await client.request("model/list", {})) as { data?: Array<any> };
|
||||||
const models = Array.isArray(response?.data) ? response.data : [];
|
const models = Array.isArray(response?.data) ? response.data : [];
|
||||||
return models.map((model) => ({
|
return models.map((model) => {
|
||||||
provider: CODEX_PROVIDER,
|
const defaultReasoningEffort = normalizeCodexThinkingOptionId(
|
||||||
id: model.id,
|
typeof model.defaultReasoningEffort === "string"
|
||||||
label: model.displayName,
|
? model.defaultReasoningEffort
|
||||||
description: model.description,
|
: null
|
||||||
isDefault: model.isDefault,
|
);
|
||||||
thinkingOptions: [
|
|
||||||
{
|
const thinkingById = new Map<string, { id: string; label: string; description?: string }>();
|
||||||
id: "default",
|
if (Array.isArray(model.supportedReasoningEfforts)) {
|
||||||
label: "Default",
|
for (const entry of model.supportedReasoningEfforts) {
|
||||||
description: typeof model.defaultReasoningEffort === "string"
|
const id = normalizeCodexThinkingOptionId(
|
||||||
? `Use model default (${model.defaultReasoningEffort})`
|
typeof entry?.reasoningEffort === "string" ? entry.reasoningEffort : null
|
||||||
: "Use model default",
|
);
|
||||||
isDefault: true,
|
if (!id) continue;
|
||||||
|
const description =
|
||||||
|
typeof entry?.description === "string" && entry.description.trim().length > 0
|
||||||
|
? entry.description
|
||||||
|
: undefined;
|
||||||
|
thinkingById.set(id, { id, label: id, description });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultReasoningEffort && !thinkingById.has(defaultReasoningEffort)) {
|
||||||
|
thinkingById.set(defaultReasoningEffort, {
|
||||||
|
id: defaultReasoningEffort,
|
||||||
|
label: defaultReasoningEffort,
|
||||||
|
description: "Model default reasoning effort",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const thinkingOptions = Array.from(thinkingById.values()).map((option) => ({
|
||||||
|
...option,
|
||||||
|
isDefault: option.id === defaultReasoningEffort,
|
||||||
|
}));
|
||||||
|
const defaultThinkingOptionId =
|
||||||
|
defaultReasoningEffort ?? thinkingOptions.find((option) => option.isDefault)?.id ?? thinkingOptions[0]?.id;
|
||||||
|
|
||||||
|
return {
|
||||||
|
provider: CODEX_PROVIDER,
|
||||||
|
id: model.id,
|
||||||
|
label: model.displayName,
|
||||||
|
description: model.description,
|
||||||
|
isDefault: model.isDefault,
|
||||||
|
thinkingOptions: thinkingOptions.length > 0 ? thinkingOptions : undefined,
|
||||||
|
defaultThinkingOptionId,
|
||||||
|
metadata: {
|
||||||
|
model: model.model,
|
||||||
|
defaultReasoningEffort: model.defaultReasoningEffort,
|
||||||
|
supportedReasoningEfforts: model.supportedReasoningEfforts,
|
||||||
},
|
},
|
||||||
...(Array.isArray(model.supportedReasoningEfforts)
|
};
|
||||||
? model.supportedReasoningEfforts.map((entry: any) => ({
|
});
|
||||||
id: entry.reasoningEffort,
|
|
||||||
label: entry.reasoningEffort,
|
|
||||||
description: entry.description,
|
|
||||||
isDefault: entry.reasoningEffort === model.defaultReasoningEffort,
|
|
||||||
}))
|
|
||||||
: []),
|
|
||||||
],
|
|
||||||
defaultThinkingOptionId: "default",
|
|
||||||
metadata: {
|
|
||||||
model: model.model,
|
|
||||||
defaultReasoningEffort: model.defaultReasoningEffort,
|
|
||||||
supportedReasoningEfforts: model.supportedReasoningEfforts,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
} finally {
|
} finally {
|
||||||
await client.dispose();
|
await client.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ export class OpenCodeAgentClient implements AgentClient {
|
|||||||
for (const [modelId, model] of Object.entries(provider.models)) {
|
for (const [modelId, model] of Object.entries(provider.models)) {
|
||||||
const rawVariants = model.variants ? Object.keys(model.variants) : [];
|
const rawVariants = model.variants ? Object.keys(model.variants) : [];
|
||||||
const thinkingOptions = [
|
const thinkingOptions = [
|
||||||
{ id: "default", label: "Default", isDefault: true },
|
{ id: "default", label: "Model default", isDefault: true },
|
||||||
...rawVariants.map((id) => ({ id, label: id })),
|
...rawVariants.map((id) => ({ id, label: id })),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -184,8 +184,10 @@ describe("daemon E2E", () => {
|
|||||||
if (!modelWithOptions) {
|
if (!modelWithOptions) {
|
||||||
throw new Error("No Codex model with thinkingOptions returned");
|
throw new Error("No Codex model with thinkingOptions returned");
|
||||||
}
|
}
|
||||||
|
const defaultThinkingId = modelWithOptions.defaultThinkingOptionId ?? "default";
|
||||||
const nonDefault =
|
const nonDefault =
|
||||||
modelWithOptions.thinkingOptions?.find((o) => o.id !== "default")?.id ??
|
modelWithOptions.thinkingOptions?.find((o) => o.id !== defaultThinkingId)?.id ??
|
||||||
|
modelWithOptions.thinkingOptions?.[0]?.id ??
|
||||||
null;
|
null;
|
||||||
if (!nonDefault) {
|
if (!nonDefault) {
|
||||||
throw new Error("No non-default Codex thinking option found");
|
throw new Error("No non-default Codex thinking option found");
|
||||||
@@ -240,8 +242,10 @@ describe("daemon E2E", () => {
|
|||||||
model: modelWithThinkingOptions.id,
|
model: modelWithThinkingOptions.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const defaultThinkingId = modelWithThinkingOptions.defaultThinkingOptionId ?? "default";
|
||||||
const thinkingId =
|
const thinkingId =
|
||||||
modelWithThinkingOptions.thinkingOptions?.find((o) => o.id !== "default")?.id ??
|
modelWithThinkingOptions.thinkingOptions?.find((o) => o.id !== defaultThinkingId)?.id ??
|
||||||
|
modelWithThinkingOptions.thinkingOptions?.[0]?.id ??
|
||||||
null;
|
null;
|
||||||
if (!thinkingId) {
|
if (!thinkingId) {
|
||||||
throw new Error("No non-default OpenCode thinking option found");
|
throw new Error("No non-default OpenCode thinking option found");
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
createDaemonTestContext,
|
createDaemonTestContext,
|
||||||
type DaemonTestContext,
|
type DaemonTestContext,
|
||||||
} from "./test-utils/index.js";
|
} from "./test-utils/index.js";
|
||||||
import { getFullAccessConfig, type AgentProvider } from "./daemon-e2e/agent-configs.js";
|
|
||||||
import { OpenAITTS } from "./speech/providers/openai/tts.js";
|
import { OpenAITTS } from "./speech/providers/openai/tts.js";
|
||||||
import { OpenAISTT } from "./speech/providers/openai/stt.js";
|
import { OpenAISTT } from "./speech/providers/openai/stt.js";
|
||||||
import { STTManager } from "./agent/stt-manager.js";
|
import { STTManager } from "./agent/stt-manager.js";
|
||||||
@@ -18,6 +17,37 @@ const openaiApiKey = process.env.OPENAI_API_KEY ?? null;
|
|||||||
const shouldRun = process.env.PASEO_VOICE_ROUNDTRIP_E2E === "1" && Boolean(openaiApiKey);
|
const shouldRun = process.env.PASEO_VOICE_ROUNDTRIP_E2E === "1" && Boolean(openaiApiKey);
|
||||||
const speechTest = shouldRun ? test : test.skip;
|
const speechTest = shouldRun ? test : test.skip;
|
||||||
|
|
||||||
|
type VoiceRoundtripProvider = "claude" | "codex" | "opencode";
|
||||||
|
|
||||||
|
function getVoiceRoundtripConfig(provider: VoiceRoundtripProvider): {
|
||||||
|
provider: VoiceRoundtripProvider;
|
||||||
|
model: string;
|
||||||
|
modeId: string;
|
||||||
|
thinkingOptionId?: string;
|
||||||
|
} {
|
||||||
|
switch (provider) {
|
||||||
|
case "claude":
|
||||||
|
return {
|
||||||
|
provider: "claude",
|
||||||
|
model: "haiku",
|
||||||
|
modeId: "bypassPermissions",
|
||||||
|
};
|
||||||
|
case "codex":
|
||||||
|
return {
|
||||||
|
provider: "codex",
|
||||||
|
model: "gpt-5.1-codex-mini",
|
||||||
|
modeId: "full-access",
|
||||||
|
thinkingOptionId: "low",
|
||||||
|
};
|
||||||
|
case "opencode":
|
||||||
|
return {
|
||||||
|
provider: "opencode",
|
||||||
|
model: "opencode/gpt-5-nano",
|
||||||
|
modeId: "default",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function waitForSignal<T>(
|
function waitForSignal<T>(
|
||||||
timeoutMs: number,
|
timeoutMs: number,
|
||||||
setup: (
|
setup: (
|
||||||
@@ -98,7 +128,7 @@ describe("voice roundtrip e2e", () => {
|
|||||||
await ctx.cleanup();
|
await ctx.cleanup();
|
||||||
}, 60000);
|
}, 60000);
|
||||||
|
|
||||||
for (const targetProvider of ["claude", "codex"] as const satisfies AgentProvider[]) {
|
for (const targetProvider of ["claude", "codex", "opencode"] as const satisfies VoiceRoundtripProvider[]) {
|
||||||
speechTest(
|
speechTest(
|
||||||
`full roundtrip (${targetProvider}): voice input audio -> voice agent -> output audio -> transcribed output`,
|
`full roundtrip (${targetProvider}): voice input audio -> voice agent -> output audio -> transcribed output`,
|
||||||
async () => {
|
async () => {
|
||||||
@@ -128,7 +158,7 @@ describe("voice roundtrip e2e", () => {
|
|||||||
30000,
|
30000,
|
||||||
ctx.client.createAgent({
|
ctx.client.createAgent({
|
||||||
config: {
|
config: {
|
||||||
...getFullAccessConfig(targetProvider),
|
...getVoiceRoundtripConfig(targetProvider),
|
||||||
cwd: voiceCwd,
|
cwd: voiceCwd,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user