fix: resolve bare model IDs to anthropic over claude-code provider (#3076)
When the claude-code-cli extension is active and the session provider is claude-code, bare model IDs in PREFERENCES.md (e.g. "claude-sonnet-4-6") silently resolve to claude-code/* instead of anthropic/*, routing all dispatch through the Claude Code CLI subprocess with different context, tool visibility, and cost characteristics. The fix introduces provider precedence in resolveModelId(): extension providers like claude-code are deprioritized for bare ID resolution. First-class API providers (anthropic, bedrock, openai, azure, etc.) retain the existing current-provider preference behavior. When the session provider is an extension, resolution falls through to prefer anthropic, then any non-extension provider, preserving backward compatibility for all existing provider combinations. Closes #2905 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e7351fbd75
commit
a3fc400c09
2 changed files with 97 additions and 6 deletions
|
|
@ -222,9 +222,30 @@ export function resolveModelId<T extends { id: string; provider: string }>(
|
|||
);
|
||||
}
|
||||
|
||||
// Bare ID — prefer current provider, then first available
|
||||
const exactProviderMatch = availableModels.find(
|
||||
m => m.id === modelId && m.provider === currentProvider,
|
||||
);
|
||||
return exactProviderMatch ?? availableModels.find(m => m.id === modelId);
|
||||
// Bare ID — resolve with provider precedence to avoid silent misrouting.
|
||||
// Extension providers (e.g. claude-code) expose the same model IDs as their
|
||||
// upstream API providers but route through a subprocess with different
|
||||
// context, tool visibility, and cost characteristics (#2905). Bare IDs in
|
||||
// PREFERENCES.md must resolve to the canonical API provider, not to an
|
||||
// extension wrapper that happens to be the current session provider.
|
||||
const candidates = availableModels.filter(m => m.id === modelId);
|
||||
if (candidates.length === 0) return undefined;
|
||||
if (candidates.length === 1) return candidates[0];
|
||||
|
||||
// Extension / CLI-wrapper providers that should never win bare-ID resolution
|
||||
// when a first-class API provider also offers the same model.
|
||||
const EXTENSION_PROVIDERS = new Set(["claude-code"]);
|
||||
|
||||
// Prefer currentProvider only when it is a first-class API provider
|
||||
if (currentProvider && !EXTENSION_PROVIDERS.has(currentProvider)) {
|
||||
const providerMatch = candidates.find(m => m.provider === currentProvider);
|
||||
if (providerMatch) return providerMatch;
|
||||
}
|
||||
|
||||
// Prefer "anthropic" as the canonical provider for Anthropic models
|
||||
const anthropicMatch = candidates.find(m => m.provider === "anthropic");
|
||||
if (anthropicMatch) return anthropicMatch;
|
||||
|
||||
// Fall back to first non-extension candidate, or any candidate
|
||||
return candidates.find(m => !EXTENSION_PROVIDERS.has(m.provider)) ?? candidates[0];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
import { resolvePreferredModelConfig } from "../auto-model-selection.js";
|
||||
import { resolvePreferredModelConfig, resolveModelId } from "../auto-model-selection.js";
|
||||
|
||||
function makeTempDir(prefix: string): string {
|
||||
return mkdtempSync(join(tmpdir(), prefix));
|
||||
|
|
@ -137,3 +137,73 @@ test("resolvePreferredModelConfig keeps explicit phase models as the ceiling", (
|
|||
rmSync(tempGsdHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// ─── resolveModelId tests ─────────────────────────────────────────────────
|
||||
|
||||
test("resolveModelId: bare ID resolves to anthropic over claude-code when session is claude-code (#2905)", () => {
|
||||
const availableModels = [
|
||||
{ id: "claude-sonnet-4-6", provider: "anthropic" },
|
||||
{ id: "claude-sonnet-4-6", provider: "claude-code" },
|
||||
];
|
||||
|
||||
// Bug: when currentProvider is "claude-code", bare ID "claude-sonnet-4-6"
|
||||
// resolves to claude-code/claude-sonnet-4-6 instead of anthropic/claude-sonnet-4-6
|
||||
const result = resolveModelId("claude-sonnet-4-6", availableModels, "claude-code");
|
||||
assert.ok(result, "should resolve a model");
|
||||
assert.equal(result.provider, "anthropic", "bare ID must resolve to anthropic, not claude-code");
|
||||
});
|
||||
|
||||
test("resolveModelId: bare ID still prefers current provider when it is a first-class API provider", () => {
|
||||
const availableModels = [
|
||||
{ id: "claude-sonnet-4-6", provider: "anthropic" },
|
||||
{ id: "claude-sonnet-4-6", provider: "bedrock" },
|
||||
];
|
||||
|
||||
const result = resolveModelId("claude-sonnet-4-6", availableModels, "bedrock");
|
||||
assert.ok(result, "should resolve a model");
|
||||
assert.equal(result.provider, "bedrock", "bare ID should prefer current provider when it is a real API provider");
|
||||
});
|
||||
|
||||
test("resolveModelId: explicit provider/model format still resolves to claude-code when specified", () => {
|
||||
const availableModels = [
|
||||
{ id: "claude-sonnet-4-6", provider: "anthropic" },
|
||||
{ id: "claude-sonnet-4-6", provider: "claude-code" },
|
||||
];
|
||||
|
||||
const result = resolveModelId("claude-code/claude-sonnet-4-6", availableModels, "anthropic");
|
||||
assert.ok(result, "should resolve a model");
|
||||
assert.equal(result.provider, "claude-code", "explicit provider prefix must be respected");
|
||||
});
|
||||
|
||||
test("resolveModelId: bare ID with only one provider works normally", () => {
|
||||
const availableModels = [
|
||||
{ id: "claude-sonnet-4-6", provider: "anthropic" },
|
||||
];
|
||||
|
||||
const result = resolveModelId("claude-sonnet-4-6", availableModels, "anthropic");
|
||||
assert.ok(result, "should resolve a model");
|
||||
assert.equal(result.provider, "anthropic");
|
||||
});
|
||||
|
||||
test("resolveModelId: bare ID with claude-code as only provider still resolves", () => {
|
||||
const availableModels = [
|
||||
{ id: "claude-sonnet-4-6", provider: "claude-code" },
|
||||
];
|
||||
|
||||
// If claude-code is the ONLY provider for this model, it should still resolve
|
||||
const result = resolveModelId("claude-sonnet-4-6", availableModels, "claude-code");
|
||||
assert.ok(result, "should resolve even when only available via claude-code");
|
||||
assert.equal(result.provider, "claude-code");
|
||||
});
|
||||
|
||||
test("resolveModelId: anthropic wins over claude-code regardless of list order", () => {
|
||||
const availableModels = [
|
||||
{ id: "claude-sonnet-4-6", provider: "claude-code" },
|
||||
{ id: "claude-sonnet-4-6", provider: "anthropic" },
|
||||
];
|
||||
|
||||
// Even when claude-code appears first in the list, anthropic should win
|
||||
const result = resolveModelId("claude-sonnet-4-6", availableModels, "claude-code");
|
||||
assert.ok(result, "should resolve a model");
|
||||
assert.equal(result.provider, "anthropic", "anthropic must win over claude-code regardless of list order");
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue