From dfb44912ed285a0163c576c727632d00cfdf39f3 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 9 Jun 2026 10:01:47 +0900 Subject: [PATCH] fix(acpx): normalize Claude ACP model refs --- extensions/acpx/src/runtime.test.ts | 56 +++++++++++++++++++++++++++-- extensions/acpx/src/runtime.ts | 30 ++++++++++++++-- 2 files changed, 80 insertions(+), 6 deletions(-) diff --git a/extensions/acpx/src/runtime.test.ts b/extensions/acpx/src/runtime.test.ts index 3d952b4e0755..97c4e059aa05 100644 --- a/extensions/acpx/src/runtime.test.ts +++ b/extensions/acpx/src/runtime.test.ts @@ -224,6 +224,56 @@ describe("AcpxRuntime fresh reset wrapper", () => { }); }); + it("strips the OpenClaw Anthropic provider prefix for Claude ACP startup", async () => { + const baseStore: TestSessionStore = { + load: vi.fn(async () => undefined), + save: vi.fn(async () => {}), + }; + const { runtime, delegate } = makeRuntime(baseStore, { + agentRegistry: { + resolve: (agentName: string) => + agentName === "claude" ? "npx @agentclientprotocol/claude-agent-acp" : agentName, + list: () => ["claude", "openclaw"], + }, + }); + const ensure = vi.spyOn(delegate, "ensureSession").mockResolvedValue({ + sessionKey: "agent:claude:acp:test", + backend: "acpx", + runtimeSessionName: "claude", + }); + + await runtime.ensureSession({ + sessionKey: "agent:claude:acp:test", + agent: "claude", + mode: "persistent", + model: "anthropic/claude-sonnet-4-6", + }); + + expect(readFirstEnsureSessionInput(ensure)).toEqual({ + sessionKey: "agent:claude:acp:test", + agent: "claude", + mode: "persistent", + model: "claude-sonnet-4-6", + sessionOptions: { model: "claude-sonnet-4-6" }, + }); + }); + + it("keeps Claude ACP model ids intact after stripping the OpenClaw provider prefix", () => { + expect(testing.normalizeClaudeAcpModelOverride("anthropic/claude-sonnet-4-6")).toBe( + "claude-sonnet-4-6", + ); + expect(testing.normalizeClaudeAcpModelOverride("anthropic/claude-opus-4-8")).toBe( + "claude-opus-4-8", + ); + expect(testing.normalizeClaudeAcpModelOverride("anthropic/claude-haiku-4-5")).toBe( + "claude-haiku-4-5", + ); + expect(testing.normalizeClaudeAcpModelOverride("anthropic/claude-sonnet-4-6-1m")).toBe( + "claude-sonnet-4-6-1m", + ); + expect(testing.normalizeClaudeAcpModelOverride("custom-model")).toBe("custom-model"); + }); + it("leaves Codex ACP startup defaults alone when no model or thinking is provided", async () => { const baseStore: TestSessionStore = { load: vi.fn(async () => undefined), @@ -898,7 +948,7 @@ describe("AcpxRuntime fresh reset wrapper", () => { expect(setConfigOption).not.toHaveBeenCalled(); }); - it("still forwards non-timeout config controls for claude-agent-acp", async () => { + it("normalizes model config controls for claude-agent-acp", async () => { const baseStore: TestSessionStore = { load: vi.fn(async () => ({ acpxRecordId: "agent:claude:acp:test", @@ -918,14 +968,14 @@ describe("AcpxRuntime fresh reset wrapper", () => { await runtime.setConfigOption({ handle, key: "model", - value: "claude-sonnet-4.6", + value: "anthropic/claude-sonnet-4-6", }); expect(setConfigOption).toHaveBeenCalledOnce(); expect(setConfigOption).toHaveBeenCalledWith({ handle, key: "model", - value: "claude-sonnet-4.6", + value: "claude-sonnet-4-6", }); }); diff --git a/extensions/acpx/src/runtime.ts b/extensions/acpx/src/runtime.ts index fb79fa37e322..d8fd3b7e0b47 100644 --- a/extensions/acpx/src/runtime.ts +++ b/extensions/acpx/src/runtime.ts @@ -329,6 +329,7 @@ const OPENCLAW_BRIDGE_EXECUTABLE = "openclaw"; const OPENCLAW_BRIDGE_SUBCOMMAND = "acp"; const CODEX_ACP_AGENT_ID = "codex"; const CODEX_ACP_OPENCLAW_PREFIX = "openai/"; +const CLAUDE_ACP_OPENCLAW_PREFIX = "anthropic/"; const CODEX_ACP_REASONING_EFFORTS = new Set(["low", "medium", "high", "xhigh"]); const CODEX_ACP_THINKING_ALIASES = new Map([ ["off", undefined], @@ -564,6 +565,17 @@ function codexAcpSessionModelId(override: CodexAcpModelOverride): string { : override.model; } +function normalizeClaudeAcpModelOverride(rawModel: string | undefined): string | undefined { + const raw = rawModel?.trim(); + if (!raw) { + return undefined; + } + if (!raw.toLowerCase().startsWith(CLAUDE_ACP_OPENCLAW_PREFIX)) { + return raw; + } + return raw.slice(CLAUDE_ACP_OPENCLAW_PREFIX.length).trim() || undefined; +} + function withAcpxSessionOptions(input: OpenClawRuntimeEnsureInput): AcpxDelegateEnsureInput { const existingOptions = (input as { sessionOptions?: SessionAgentOptions }).sessionOptions; const model = input.model?.trim() || existingOptions?.model; @@ -948,10 +960,14 @@ export class AcpxRuntime implements AcpRuntime { agentRegistry: this.agentRegistry, }); const delegate = this.resolveDelegateForCommand(command); + const claudeModelOverride = isClaudeAcpCommand(command) + ? normalizeClaudeAcpModelOverride(input.model) + : undefined; const codexModelOverride = normalizeAgentName(input.agent) === CODEX_ACP_AGENT_ID && isCodexAcpCommand(command) ? normalizeCodexAcpModelOverride(input.model, input.thinking) : undefined; + const ensureInput = claudeModelOverride ? { ...input, model: claudeModelOverride } : input; const stableLaunchCommand = codexModelOverride && command ? appendCodexAcpConfigOverrides(command, codexModelOverride) @@ -966,20 +982,20 @@ export class AcpxRuntime implements AcpRuntime { if (!codexModelOverride) { return await this.runWithLaunchLease({ - sessionKey: input.sessionKey, + sessionKey: ensureInput.sessionKey, command: stableLaunchCommand, enabled: shouldStartWithLease, run: () => this.withCodexWrapperDiagnostics({ command: stableLaunchCommand, fallbackCode: "ACP_SESSION_INIT_FAILED", - run: () => delegate.ensureSession(withAcpxSessionOptions(input)), + run: () => delegate.ensureSession(withAcpxSessionOptions(ensureInput)), }), }); } const normalizedInput = { - ...input, + ...ensureInput, ...(codexAcpSessionModelId(codexModelOverride) ? { model: codexAcpSessionModelId(codexModelOverride) } : {}), @@ -1208,6 +1224,13 @@ export class AcpxRuntime implements AcpRuntime { } } } + if (isClaudeAcpCommand(command) && key === "model") { + await delegate.setConfigOption({ + ...input, + value: normalizeClaudeAcpModelOverride(input.value) ?? input.value, + }); + return; + } await delegate.setConfigOption(input); } @@ -1260,6 +1283,7 @@ export const testing = { codexAcpSessionModelId, isClaudeAcpCommand, isCodexAcpCommand, + normalizeClaudeAcpModelOverride, normalizeCodexAcpModelOverride, };