mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 12:40:32 +00:00
fix: Avoid cross-version model alias matches (#702)
Partially addresses #639 Deduping may reintroduce issues from #620 so going to hold off on that for now.
This commit is contained in:
@@ -2413,6 +2413,27 @@ function buildConfigOptions(
|
||||
// but the SDK model list uses IDs like "claude-opus-4-6-1m".
|
||||
const MODEL_CONTEXT_HINT_PATTERN = /\[(\d+m)\]$/i;
|
||||
|
||||
// Captures a model family version such as `4-6` or `4.7` so we can keep
|
||||
// `claude-opus-4-6` from being copied onto the SDK's `opus` alias when that
|
||||
// alias currently resolves to a different family version (e.g. Opus 4.7).
|
||||
const MODEL_FAMILY_VERSION_PATTERN = /\b(\d+)[-.](\d+)\b/;
|
||||
|
||||
function extractModelFamilyVersion(s: string): string | null {
|
||||
const match = s.match(MODEL_FAMILY_VERSION_PATTERN);
|
||||
return match ? `${match[1]}.${match[2]}` : null;
|
||||
}
|
||||
|
||||
function modelVersionsCompatible(preference: string, candidate: ModelInfo): boolean {
|
||||
const preferred = extractModelFamilyVersion(preference);
|
||||
if (!preferred) return true;
|
||||
const candidateVersion =
|
||||
extractModelFamilyVersion(candidate.value) ??
|
||||
extractModelFamilyVersion(candidate.displayName) ??
|
||||
extractModelFamilyVersion(candidate.description);
|
||||
if (!candidateVersion) return true;
|
||||
return preferred === candidateVersion;
|
||||
}
|
||||
|
||||
function tokenizeModelPreference(model: string): { tokens: string[]; contextHint?: string } {
|
||||
const lower = model.trim().toLowerCase();
|
||||
const contextHint = lower.match(MODEL_CONTEXT_HINT_PATTERN)?.[1]?.toLowerCase();
|
||||
@@ -2459,6 +2480,7 @@ function resolveModelPreference(models: ModelInfo[], preference: string): ModelI
|
||||
|
||||
// Substring match
|
||||
const includesMatch = models.find((model) => {
|
||||
if (!modelVersionsCompatible(trimmed, model)) return false;
|
||||
const value = model.value.toLowerCase();
|
||||
const display = model.displayName.toLowerCase();
|
||||
return value.includes(lower) || display.includes(lower) || lower.includes(value);
|
||||
@@ -2472,6 +2494,7 @@ function resolveModelPreference(models: ModelInfo[], preference: string): ModelI
|
||||
let bestMatch: ModelInfo | null = null;
|
||||
let bestScore = 0;
|
||||
for (const model of models) {
|
||||
if (!modelVersionsCompatible(trimmed, model)) continue;
|
||||
const score = scoreModelMatch(model, tokens, contextHint);
|
||||
if (0 < score && (!bestMatch || bestScore < score)) {
|
||||
bestMatch = model;
|
||||
|
||||
@@ -578,6 +578,68 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
expect(setModelSpy).toHaveBeenCalledWith("claude-haiku-4-5");
|
||||
expect(response.models.currentModelId).toBe("claude-haiku-4-5");
|
||||
});
|
||||
|
||||
it("does not inherit display info across mismatched model family 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
|
||||
// `opus` and inherit the "Opus 4.7" display info. With version-aware
|
||||
// matching, these entries fall back to showing their literal ID rather
|
||||
// than a misleading newer name.
|
||||
await fs.promises.writeFile(
|
||||
path.join(tempDir, "settings.json"),
|
||||
JSON.stringify({
|
||||
availableModels: [
|
||||
"claude-opus-4-6",
|
||||
"claude-opus-4-6[1m]",
|
||||
"claude-opus-4-7",
|
||||
"claude-opus-4-7[1m]",
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
const projectDir = path.join(tempDir, "project");
|
||||
await fs.promises.mkdir(projectDir, { recursive: true });
|
||||
|
||||
mockQueryWithModels([
|
||||
{ value: "default", displayName: "Default", description: "Default model" },
|
||||
{
|
||||
value: "opus",
|
||||
displayName: "Opus 4.7",
|
||||
description: "Claude Opus 4.7 — complex tasks, higher cost",
|
||||
},
|
||||
{
|
||||
value: "opus[1m]",
|
||||
displayName: "Opus 4.7 (1M context)",
|
||||
description: "Opus 4.7 with 1M context",
|
||||
},
|
||||
]);
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
// 4-6 entries must NOT inherit the 4.7 SDK alias display info.
|
||||
expect(byValue["claude-opus-4-6"].name).toBe("claude-opus-4-6");
|
||||
expect(byValue["claude-opus-4-6"].description).toBe("");
|
||||
expect(byValue["claude-opus-4-6[1m]"].name).toBe("claude-opus-4-6[1m]");
|
||||
expect(byValue["claude-opus-4-6[1m]"].description).toBe("");
|
||||
|
||||
// 4-7 entries continue to inherit display info from a 4.7 SDK alias.
|
||||
expect(byValue["claude-opus-4-7"].name).toBe("Opus 4.7");
|
||||
expect(byValue["claude-opus-4-7[1m]"].name).toMatch(/Opus 4\.7/);
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves model aliases like opus[1m] to the correct model", async () => {
|
||||
|
||||
Reference in New Issue
Block a user