Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions agents/Codex.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <id> 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 <value>` is supported as a compatibility alias and is translated to codex-acp `reasoning_effort`.
2 changes: 2 additions & 0 deletions skills/acpx/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <id>` 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.

Expand Down
17 changes: 16 additions & 1 deletion src/cli-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
31 changes: 31 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down Expand Up @@ -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<ReturnType<typeof connection.newSession>>;
try {
Expand All @@ -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),
Expand Down
53 changes: 53 additions & 0 deletions test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
51 changes: 51 additions & 0 deletions test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> | undefined;
let capturedSetConfigParams:
| {
sessionId: string;
configId: string;
value: string;
}
| undefined;
asInternals(client).connection = {
newSession: async (params: Record<string, unknown>) => {
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({
Expand Down
12 changes: 12 additions & 0 deletions test/mock-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
21 changes: 21 additions & 0 deletions test/prompt-runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down