fix(acpx): normalize Claude ACP model refs

This commit is contained in:
Vincent Koc
2026-06-09 10:01:47 +09:00
parent 80f1ae6ffe
commit dfb44912ed
2 changed files with 80 additions and 6 deletions

View File

@@ -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",
});
});

View File

@@ -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<string, string | undefined>([
["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,
};