mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 04:30:59 +00:00
fix: Prevent cross-family model matching in resolveModelPreference (#731)
## Summary Partially addresses #639. Builds on #702 which added version-aware matching. When the SDK's `opus` alias resolves to a newer version than what's in `availableModels` (e.g. the SDK was bumped to Opus 4.8 but `availableModels` still contains `claude-opus-4-6[1m]`), the version check from #702 correctly rejects the `opus` entry (4.8 ≠ 4.6). However, the tokenized matcher then falls through to `sonnet[1m]` because the `1m` context hint token alone scores 3 points — enough to win. This causes `claude-opus-4-6[1m]` to display as "Sonnet (1M context)". Fix: `scoreModelMatch` now returns 0 when the only matching token is the context hint. This prevents cross-family matches without hardcoding model family names. Note: Claude initially recommended hardcoding model families to be 100% sure we avoid cross-family matching. While it would be a stronger signal, I didn't find references to model families in the codebase (except for opusplan --> opus) so I'm assuming the family list is transparent to claude-agent-acp. ## Test plan - [x] Added test: `does not inherit display info across mismatched model families` — verifies `claude-opus-4-6[1m]` does not resolve to `Sonnet (1M context)` - [x] Existing tests pass (20/20 in `acp-agent-settings.test.ts`) - [x] New test confirmed to fail on unmodified `main` and pass with the fix --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2739,11 +2739,14 @@ function tokenizeModelPreference(model: string): { tokens: string[]; contextHint
|
||||
function scoreModelMatch(model: ModelInfo, tokens: string[], contextHint?: string): number {
|
||||
const haystack = `${model.value} ${model.displayName}`.toLowerCase();
|
||||
let score = 0;
|
||||
let nonHintMatched = false;
|
||||
for (const token of tokens) {
|
||||
if (haystack.includes(token)) {
|
||||
if (token !== contextHint) nonHintMatched = true;
|
||||
score += token === contextHint ? 3 : 1;
|
||||
}
|
||||
}
|
||||
if (contextHint && !nonHintMatched) return 0;
|
||||
return score;
|
||||
}
|
||||
|
||||
|
||||
@@ -585,7 +585,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("does not inherit display info across mismatched model family versions", async () => {
|
||||
it("does not inherit display info across mismatched model versions", async () => {
|
||||
// https://github.com/agentclientprotocol/claude-agent-acp/issues/639:
|
||||
// when the SDK's `opus` alias resolves to Opus 4.7, an allowlist entry
|
||||
// of `claude-opus-4-6` (or `claude-opus-4-6[1m]`) used to substring-match
|
||||
@@ -646,6 +646,60 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
expect(byValue["claude-opus-4-7"].name).toBe("Opus 4.7");
|
||||
expect(byValue["claude-opus-4-7[1m]"].name).toMatch(/Opus 4\.7/);
|
||||
});
|
||||
|
||||
it("does not inherit display info across mismatched model families", async () => {
|
||||
// https://github.com/agentclientprotocol/claude-agent-acp/issues/639:
|
||||
// when the SDK's `opus` alias resolves to Opus 4.8, `claude-opus-4-6[1m]`
|
||||
// fails the version check against `opus` but the tokenized matcher
|
||||
// falls through to `sonnet[1m]` because the `1m` context hint alone
|
||||
// is enough to score a match.
|
||||
await fs.promises.writeFile(
|
||||
path.join(tempDir, "settings.json"),
|
||||
JSON.stringify({
|
||||
availableModels: ["claude-opus-4-6", "claude-opus-4-6[1m]"],
|
||||
}),
|
||||
);
|
||||
|
||||
const projectDir = path.join(tempDir, "project");
|
||||
await fs.promises.mkdir(projectDir, { recursive: true });
|
||||
|
||||
mockQueryWithModels([
|
||||
{ value: "default", displayName: "Default", description: "Default model" },
|
||||
{
|
||||
value: "sonnet",
|
||||
displayName: "Sonnet",
|
||||
description: "Sonnet 4.6 · Best for everyday tasks",
|
||||
},
|
||||
{
|
||||
value: "sonnet[1m]",
|
||||
displayName: "Sonnet (1M context)",
|
||||
description: "Sonnet 4.6 for long sessions",
|
||||
},
|
||||
{
|
||||
value: "opus",
|
||||
displayName: "Opus 4.8",
|
||||
description: "Claude Opus 4.8",
|
||||
},
|
||||
]);
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
cwd: projectDir,
|
||||
mcpServers: [],
|
||||
_meta: { disableBuiltInTools: true },
|
||||
});
|
||||
|
||||
const modelOption = response.configOptions.find((o: any) => o.id === "model");
|
||||
const byValue: Record<string, { name: string; description?: string }> = {};
|
||||
for (const opt of modelOption.options) {
|
||||
byValue[opt.value] = { name: opt.name, description: opt.description };
|
||||
}
|
||||
|
||||
expect(byValue["claude-opus-4-6[1m]"].name).toBe("claude-opus-4-6[1m]");
|
||||
expect(byValue["claude-opus-4-6"].name).toBe("claude-opus-4-6");
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves model aliases like opus[1m] to the correct model", async () => {
|
||||
|
||||
Reference in New Issue
Block a user