diff --git a/agents/Codex.md b/agents/Codex.md index 2ff4e19..7ed94cc 100644 --- a/agents/Codex.md +++ b/agents/Codex.md @@ -4,4 +4,6 @@ - Default command: `npx @zed-industries/codex-acp` - Upstream: https://github.com/zed-industries/codex-acp - Runtime config options exposed by current codex-acp releases include `mode`, `model`, and `reasoning_effort`. +- `acpx --model codex ...` applies the requested model after session creation via `session/set_config_option`. +- Common Codex model aliases such as `GPT-5-2` are normalized to codex-acp ids such as `gpt-5.2`. - `acpx codex set thought_level ` is supported as a compatibility alias and is translated to codex-acp `reasoning_effort`. diff --git a/skills/acpx/SKILL.md b/skills/acpx/SKILL.md index bed8a70..8fc802a 100644 --- a/skills/acpx/SKILL.md +++ b/skills/acpx/SKILL.md @@ -149,6 +149,8 @@ Behavior: - `set-mode`: calls ACP `session/set_mode`. - `set-mode` mode ids are adapter-defined; unsupported values are rejected by the adapter (often `Invalid params`). - `set`: calls ACP `session/set_config_option`. +- For codex, `--model ` is applied after session creation via `session/set_config_option`. +- For codex, common model aliases like `GPT-5-2` are normalized to codex-acp ids like `gpt-5.2`. - For codex, `thought_level` is accepted as a compatibility alias for codex-acp `reasoning_effort`. - `set-mode`/`set` route through queue-owner IPC when active, otherwise reconnect directly. diff --git a/src/cli-core.ts b/src/cli-core.ts index 244cba4..307cd3a 100644 --- a/src/cli-core.ts +++ b/src/cli-core.ts @@ -174,6 +174,20 @@ function resolveCompatibleConfigId( return configId; } +function resolveCompatibleConfigValue( + agent: { agentName: string; agentCommand: string }, + configId: string, + value: string, +): string { + if (isCodexAgentInvocation(agent) && configId === "model") { + return value + .trim() + .toLowerCase() + .replace(/^gpt-(\d+)-(\d+)(.*)$/u, "gpt-$1.$2$3"); + } + return value; +} + export { parseAllowedTools, parseMaxTurns, parseTtlSeconds }; export { formatPromptSessionBannerLine } from "./cli/output-render.js"; @@ -538,6 +552,7 @@ async function handleSetConfigOption( const globalFlags = resolveGlobalFlags(command, config); const agent = resolveAgentInvocation(explicitAgentName, globalFlags, config); const resolvedConfigId = resolveCompatibleConfigId(agent, configId); + const resolvedValue = resolveCompatibleConfigValue(agent, resolvedConfigId, value); const { setSessionConfigOption } = await loadSessionModule(); const record = await findRoutedSessionOrThrow( agent.agentCommand, @@ -548,7 +563,7 @@ async function handleSetConfigOption( const result = await setSessionConfigOption({ sessionId: record.acpxRecordId, configId: resolvedConfigId, - value, + value: resolvedValue, mcpServers: config.mcpServers, nonInteractivePermissions: globalFlags.nonInteractivePermissions, authCredentials: config.auth, diff --git a/src/client.ts b/src/client.ts index 74edcea..3c729c9 100644 --- a/src/client.ts +++ b/src/client.ts @@ -297,6 +297,24 @@ function isClaudeAcpCommand(command: string, args: readonly string[]): boolean { return args.some((arg) => arg.includes("claude-agent-acp")); } +function isCodexAcpCommand(command: string, args: readonly string[]): boolean { + const commandToken = basenameToken(command); + if (commandToken === "codex-acp") { + return true; + } + return args.some((arg) => arg.includes("codex-acp")); +} + +function normalizeCodexModelId(value: string): string { + const trimmed = value.trim(); + if (trimmed.length === 0) { + return trimmed; + } + + const lower = trimmed.toLowerCase(); + return lower.replace(/^gpt-(\d+)-(\d+)(.*)$/u, "gpt-$1.$2$3"); +} + function isCopilotAcpCommand(command: string, args: readonly string[]): boolean { return basenameToken(command) === "copilot" && args.includes("--acp"); } @@ -1111,6 +1129,7 @@ export class AcpClient { const connection = this.getConnection(); const { command, args } = splitCommandLine(this.options.agentCommand); const claudeAcp = isClaudeAcpCommand(command, args); + const codexAcp = isCodexAcpCommand(command, args); let result: Awaited>; try { @@ -1133,6 +1152,18 @@ export class AcpClient { } this.loadedSessionId = result.sessionId; + if ( + codexAcp && + typeof this.options.sessionOptions?.model === "string" && + this.options.sessionOptions.model.trim().length > 0 + ) { + await this.setSessionConfigOption( + result.sessionId, + "model", + normalizeCodexModelId(this.options.sessionOptions.model), + ); + } + return { sessionId: result.sessionId, agentSessionId: extractRuntimeSessionId(result._meta), diff --git a/test/cli.test.ts b/test/cli.test.ts index 5b0d721..b1a7bc4 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -820,6 +820,59 @@ test("codex thought_level aliases to reasoning_effort", async () => { }); }); +test("codex model aliases normalize to codex-acp model ids", async () => { + await withTempHome(async (homeDir) => { + const cwd = path.join(homeDir, "workspace"); + await fs.mkdir(cwd, { recursive: true }); + await fs.mkdir(path.join(homeDir, ".acpx"), { recursive: true }); + await fs.writeFile( + path.join(homeDir, ".acpx", "config.json"), + `${JSON.stringify( + { + agents: { + codex: { + command: MOCK_AGENT_COMMAND, + }, + }, + }, + null, + 2, + )}\n`, + "utf8", + ); + + const sessionId = "codex-model-alias"; + await writeSessionRecord(homeDir, { + acpxRecordId: sessionId, + acpSessionId: sessionId, + agentCommand: MOCK_AGENT_COMMAND, + cwd, + createdAt: "2026-01-01T00:00:00.000Z", + lastUsedAt: "2026-01-01T00:00:00.000Z", + closed: false, + }); + + const result = await runCli( + ["--cwd", cwd, "--format", "json", "codex", "set", "model", "GPT-5-2"], + homeDir, + ); + assert.equal(result.code, 0, result.stderr); + + const payload = JSON.parse(result.stdout.trim()) as { + action?: string; + configId?: string; + value?: string; + configOptions?: Array<{ id?: string; currentValue?: string; category?: string }>; + }; + assert.equal(payload.action, "config_set"); + assert.equal(payload.configId, "model"); + assert.equal(payload.value, "GPT-5-2"); + const model = payload.configOptions?.find((option) => option.id === "model"); + assert.equal(model?.currentValue, "gpt-5.2"); + assert.equal(model?.category, "model"); + }); +}); + test("set-mode load fallback failure does not persist the fresh session id to disk", async () => { await withTempHome(async (homeDir) => { const cwd = path.join(homeDir, "workspace"); diff --git a/test/client.test.ts b/test/client.test.ts index e928f90..1242776 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -344,6 +344,57 @@ test("AcpClient createSession forwards claudeCode options in _meta", async () => }); }); +test("AcpClient createSession applies codex model via session/set_config_option", async () => { + const client = makeClient({ + agentCommand: "npx @zed-industries/codex-acp", + sessionOptions: { + model: "GPT-5-2", + }, + }); + + let capturedNewSessionParams: Record | undefined; + let capturedSetConfigParams: + | { + sessionId: string; + configId: string; + value: string; + } + | undefined; + asInternals(client).connection = { + newSession: async (params: Record) => { + capturedNewSessionParams = params; + return { sessionId: "session-456" }; + }, + setSessionConfigOption: async (params: { + sessionId: string; + configId: string; + value: string; + }) => { + capturedSetConfigParams = params; + return { configOptions: [] }; + }, + }; + + const result = await client.createSession("/tmp/acpx-client-codex-model"); + assert.equal(result.sessionId, "session-456"); + assert.deepEqual(capturedNewSessionParams, { + cwd: "/tmp/acpx-client-codex-model", + mcpServers: [], + _meta: { + claudeCode: { + options: { + model: "GPT-5-2", + }, + }, + }, + }); + assert.deepEqual(capturedSetConfigParams, { + sessionId: "session-456", + configId: "model", + value: "gpt-5.2", + }); +}); + test("AcpClient session update handling drains queued callbacks and swallows handler failures", async () => { const notifications: string[] = []; const client = makeClient({ diff --git a/test/mock-agent.ts b/test/mock-agent.ts index 2f10598..c6c23f5 100644 --- a/test/mock-agent.ts +++ b/test/mock-agent.ts @@ -417,6 +417,18 @@ function buildConfigOptions(state: SessionState): SetSessionConfigOptionResponse { value: "default", name: "Default" }, ], }, + { + id: "model", + name: "Model", + category: "model", + type: "select", + currentValue: state.configValues.model ?? "default", + options: [ + { value: "default", name: "Default" }, + { value: "gpt-5.4", name: "gpt-5.4" }, + { value: "gpt-5.2", name: "gpt-5.2" }, + ], + }, { id: "reasoning_effort", name: "Reasoning Effort", diff --git a/test/prompt-runner.test.ts b/test/prompt-runner.test.ts index 5413841..d120452 100644 --- a/test/prompt-runner.test.ts +++ b/test/prompt-runner.test.ts @@ -121,6 +121,27 @@ test("runSessionSetConfigOptionDirect falls back to createSession and returns up }, ], }, + { + id: "model", + name: "Model", + category: "model", + type: "select", + currentValue: "default", + options: [ + { + value: "default", + name: "Default", + }, + { + value: "gpt-5.4", + name: "gpt-5.4", + }, + { + value: "gpt-5.2", + name: "gpt-5.2", + }, + ], + }, { id: "reasoning_effort", name: "Reasoning Effort",