refactor(models): bump claude-opus-4-6 to claude-opus-4-7 across fallback chains, categories, and hooks

Updates the canonical Anthropic Opus model in every fallback chain
(sisyphus, oracle, prometheus, metis, momus, visual-engineering,
ultrabrain, deep, artistry, unspecified-high), the unspecified-high
category default, the think-mode HIGH_VARIANT_MAP, the Claude Code
alias map, the claude-thinking legacy alias, the context-limit GA
regex, and event.ts fallback strings.

Widens supportsCachedAnthropicLimit to accept both claude-*-4-6 and
claude-*-4-7 so the 1M context cache still applies across the bump.

Regenerates the bundled model-capabilities snapshot from models.dev
and the model-fallback snapshot to match the new source output.
This commit is contained in:
YeonGyu-Kim
2026-04-17 14:51:52 +09:00
parent b1764a880c
commit def44338ff
85 changed files with 39904 additions and 38116 deletions
+15 -15
View File
@@ -8,9 +8,9 @@ describe("Agent Config Integration", () => {
test("migrates old format agent keys to lowercase", () => {
// given - config with old format keys
const oldConfig = {
Sisyphus: { model: "anthropic/claude-opus-4-6" },
Atlas: { model: "anthropic/claude-opus-4-6" },
"Prometheus - Plan Builder": { model: "anthropic/claude-opus-4-6" },
Sisyphus: { model: "anthropic/claude-opus-4-7" },
Atlas: { model: "anthropic/claude-opus-4-7" },
"Prometheus - Plan Builder": { model: "anthropic/claude-opus-4-7" },
"Metis - Plan Consultant": { model: "anthropic/claude-sonnet-4-6" },
"Momus - Plan Critic": { model: "anthropic/claude-sonnet-4-6" },
}
@@ -33,9 +33,9 @@ describe("Agent Config Integration", () => {
expect(result.migrated).not.toHaveProperty("Momus - Plan Critic")
// then - values are preserved
expect(result.migrated.sisyphus).toEqual({ model: "anthropic/claude-opus-4-6" })
expect(result.migrated.atlas).toEqual({ model: "anthropic/claude-opus-4-6" })
expect(result.migrated.prometheus).toEqual({ model: "anthropic/claude-opus-4-6" })
expect(result.migrated.sisyphus).toEqual({ model: "anthropic/claude-opus-4-7" })
expect(result.migrated.atlas).toEqual({ model: "anthropic/claude-opus-4-7" })
expect(result.migrated.prometheus).toEqual({ model: "anthropic/claude-opus-4-7" })
// then - changed flag is true
expect(result.changed).toBe(true)
@@ -44,7 +44,7 @@ describe("Agent Config Integration", () => {
test("preserves already lowercase keys", () => {
// given - config with lowercase keys
const config = {
sisyphus: { model: "anthropic/claude-opus-4-6" },
sisyphus: { model: "anthropic/claude-opus-4-7" },
oracle: { model: "openai/gpt-5.4" },
librarian: { model: "opencode/big-pickle" },
}
@@ -62,9 +62,9 @@ describe("Agent Config Integration", () => {
test("handles mixed case config", () => {
// given - config with mixed old and new format
const mixedConfig = {
Sisyphus: { model: "anthropic/claude-opus-4-6" },
Sisyphus: { model: "anthropic/claude-opus-4-7" },
oracle: { model: "openai/gpt-5.4" },
"Prometheus - Plan Builder": { model: "anthropic/claude-opus-4-6" },
"Prometheus - Plan Builder": { model: "anthropic/claude-opus-4-7" },
librarian: { model: "opencode/big-pickle" },
}
@@ -173,8 +173,8 @@ describe("Agent Config Integration", () => {
test("old config migrates and displays correctly", () => {
// given - old format config
const oldConfig = {
Sisyphus: { model: "anthropic/claude-opus-4-6", temperature: 0.1 },
"Prometheus - Plan Builder": { model: "anthropic/claude-opus-4-6" },
Sisyphus: { model: "anthropic/claude-opus-4-7", temperature: 0.1 },
"Prometheus - Plan Builder": { model: "anthropic/claude-opus-4-7" },
}
// when - config is migrated
@@ -193,15 +193,15 @@ describe("Agent Config Integration", () => {
expect(prometheusDisplay).toBe("Prometheus - Plan Builder")
// then - config values are preserved
expect(result.migrated.sisyphus).toEqual({ model: "anthropic/claude-opus-4-6", temperature: 0.1 })
expect(result.migrated.prometheus).toEqual({ model: "anthropic/claude-opus-4-6" })
expect(result.migrated.sisyphus).toEqual({ model: "anthropic/claude-opus-4-7", temperature: 0.1 })
expect(result.migrated.prometheus).toEqual({ model: "anthropic/claude-opus-4-7" })
})
test("new config works without migration", () => {
// given - new format config (already lowercase)
const newConfig = {
sisyphus: { model: "anthropic/claude-opus-4-6" },
atlas: { model: "anthropic/claude-opus-4-6" },
sisyphus: { model: "anthropic/claude-opus-4-7" },
atlas: { model: "anthropic/claude-opus-4-7" },
}
// when - migration is applied (should be no-op)
+5 -5
View File
@@ -84,14 +84,14 @@ describe("applyAgentVariant", () => {
describe("resolveVariantForModel", () => {
test("returns agent override variant when configured", () => {
// given - use a model in sisyphus chain (claude-opus-4-6 has default variant "max")
// given - use a model in sisyphus chain (claude-opus-4-7 has default variant "max")
// to verify override takes precedence over fallback chain
const config = {
agents: {
sisyphus: { variant: "high" },
},
} as OhMyOpenCodeConfig
const model = { providerID: "anthropic", modelID: "claude-opus-4-6" }
const model = { providerID: "anthropic", modelID: "claude-opus-4-7" }
// when
const variant = resolveVariantForModel(config, "sisyphus", model)
@@ -103,7 +103,7 @@ describe("resolveVariantForModel", () => {
test("returns correct variant for anthropic provider", () => {
// given
const config = {} as OhMyOpenCodeConfig
const model = { providerID: "anthropic", modelID: "claude-opus-4-6" }
const model = { providerID: "anthropic", modelID: "claude-opus-4-7" }
// when
const variant = resolveVariantForModel(config, "sisyphus", model)
@@ -151,7 +151,7 @@ describe("resolveVariantForModel", () => {
test("returns undefined for unknown agent", () => {
// given
const config = {} as OhMyOpenCodeConfig
const model = { providerID: "anthropic", modelID: "claude-opus-4-6" }
const model = { providerID: "anthropic", modelID: "claude-opus-4-7" }
// when
const variant = resolveVariantForModel(config, "nonexistent-agent", model)
@@ -203,7 +203,7 @@ describe("resolveVariantForModel", () => {
test("returns correct variant for oracle agent with anthropic", () => {
// given
const config = {} as OhMyOpenCodeConfig
const model = { providerID: "anthropic", modelID: "claude-opus-4-6" }
const model = { providerID: "anthropic", modelID: "claude-opus-4-7" }
// when
const variant = resolveVariantForModel(config, "oracle", model)
+2 -2
View File
@@ -61,7 +61,7 @@ describe("updateConnectedProvidersCache", () => {
name: "Anthropic",
env: [],
models: {
"claude-opus-4-6": { id: "claude-opus-4-6", name: "Claude Opus 4.6" },
"claude-opus-4-7": { id: "claude-opus-4-7", name: "Claude Opus 4.6" },
"claude-sonnet-4-6": { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
},
},
@@ -84,7 +84,7 @@ describe("updateConnectedProvidersCache", () => {
{ id: "gpt-5.4", name: "GPT-5.4" },
],
anthropic: [
{ id: "claude-opus-4-6", name: "Claude Opus 4.6" },
{ id: "claude-opus-4-7", name: "Claude Opus 4.6" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
],
})
+6 -6
View File
@@ -28,15 +28,15 @@ describe("resolveActualContextLimit", () => {
resetContextLimitEnv()
})
it("returns cached limit for Anthropic 4.6 models when 1M mode is disabled (GA support)", () => {
it("returns cached limit for Anthropic 4.7 models when 1M mode is disabled (GA support)", () => {
// given
delete process.env[ANTHROPIC_CONTEXT_ENV_KEY]
delete process.env[VERTEX_CONTEXT_ENV_KEY]
const modelContextLimitsCache = new Map<string, number>()
modelContextLimitsCache.set("anthropic/claude-opus-4-6", 1_000_000)
modelContextLimitsCache.set("anthropic/claude-opus-4-7", 1_000_000)
// when
const actualLimit = resolveActualContextLimit("anthropic", "claude-opus-4-6", {
const actualLimit = resolveActualContextLimit("anthropic", "claude-opus-4-7", {
anthropicContext1MEnabled: false,
modelContextLimitsCache,
})
@@ -107,15 +107,15 @@ describe("resolveActualContextLimit", () => {
expect(actualLimit).toBe(200000)
})
it("supports Anthropic 4.6 dot-version model IDs without explicit 1M mode", () => {
it("supports Anthropic 4.7 dot-version model IDs without explicit 1M mode", () => {
// given
delete process.env[ANTHROPIC_CONTEXT_ENV_KEY]
delete process.env[VERTEX_CONTEXT_ENV_KEY]
const modelContextLimitsCache = new Map<string, number>()
modelContextLimitsCache.set("anthropic/claude-opus-4.6", 1_000_000)
modelContextLimitsCache.set("anthropic/claude-opus-4.7", 1_000_000)
// when
const actualLimit = resolveActualContextLimit("anthropic", "claude-opus-4.6", {
const actualLimit = resolveActualContextLimit("anthropic", "claude-opus-4.7", {
anthropicContext1MEnabled: false,
modelContextLimitsCache,
})
+1 -1
View File
@@ -20,7 +20,7 @@ function getAnthropicActualLimit(modelCacheState?: ContextLimitModelCacheState):
}
function supportsCachedAnthropicLimit(modelID: string): boolean {
return /^claude-(opus|sonnet)-4(?:-|\.)6(?:-high)?$/.test(modelID)
return /^claude-(opus|sonnet)-4(?:-|\.)(?:6|7)(?:-high)?$/.test(modelID)
}
export function resolveActualContextLimit(
+2 -2
View File
@@ -71,7 +71,7 @@ describe("mergeCategories", () => {
it("user overrides merge with defaults", () => {
//#given
const userCategories = {
"ultrabrain": { model: "anthropic/claude-opus-4-6" },
"ultrabrain": { model: "anthropic/claude-opus-4-7" },
}
//#when
@@ -79,6 +79,6 @@ describe("mergeCategories", () => {
//#then
expect(result["ultrabrain"]).toBeDefined()
expect(result["ultrabrain"].model).toBe("anthropic/claude-opus-4-6")
expect(result["ultrabrain"].model).toBe("anthropic/claude-opus-4-7")
})
})
+43 -43
View File
@@ -65,7 +65,7 @@ describe("fetchAvailableModels", () => {
it("#given cache file with models #when fetchAvailableModels called with connectedProviders #then returns Set of model IDs", async () => {
writeModelsCache({
openai: { id: "openai", models: { "gpt-5.4": { id: "gpt-5.4" } } },
anthropic: { id: "anthropic", models: { "claude-opus-4-6": { id: "claude-opus-4-6" } } },
anthropic: { id: "anthropic", models: { "claude-opus-4-7": { id: "claude-opus-4-7" } } },
google: { id: "google", models: { "gemini-3.1-pro": { id: "gemini-3.1-pro" } } },
})
@@ -76,7 +76,7 @@ describe("fetchAvailableModels", () => {
expect(result).toBeInstanceOf(Set)
expect(result.size).toBe(3)
expect(result.has("openai/gpt-5.4")).toBe(true)
expect(result.has("anthropic/claude-opus-4-6")).toBe(true)
expect(result.has("anthropic/claude-opus-4-7")).toBe(true)
expect(result.has("google/gemini-3.1-pro")).toBe(true)
})
@@ -145,7 +145,7 @@ describe("fetchAvailableModels", () => {
it("#given cache read twice #when second call made with same providers #then reads fresh each time", async () => {
writeModelsCache({
openai: { id: "openai", models: { "gpt-5.4": { id: "gpt-5.4" } } },
anthropic: { id: "anthropic", models: { "claude-opus-4-6": { id: "claude-opus-4-6" } } },
anthropic: { id: "anthropic", models: { "claude-opus-4-7": { id: "claude-opus-4-7" } } },
})
const result1 = await fetchAvailableModels(undefined, { connectedProviders: ["openai"] })
@@ -192,7 +192,7 @@ describe("fuzzyMatchModel", () => {
const available = new Set([
"openai/gpt-5.4",
"openai/gpt-5.3-codex",
"anthropic/claude-opus-4-6",
"anthropic/claude-opus-4-7",
])
const result = fuzzyMatchModel("gpt-5.4", available)
expect(result).toBe("openai/gpt-5.4")
@@ -239,25 +239,25 @@ describe("fuzzyMatchModel", () => {
// given available models with claude variants
// when searching for claude-opus
// then return matching claude-opus model
it("should match claude-opus to claude-opus-4-6", () => {
it("should match claude-opus to claude-opus-4-7", () => {
const available = new Set([
"anthropic/claude-opus-4-6",
"anthropic/claude-opus-4-7",
"anthropic/claude-sonnet-4-6",
])
const result = fuzzyMatchModel("claude-opus", available)
expect(result).toBe("anthropic/claude-opus-4-6")
expect(result).toBe("anthropic/claude-opus-4-7")
})
// given github-copilot serves claude versions with dot notation
// when fallback chain uses hyphen notation in requested model
// then normalize both forms and match github-copilot model
it("should match github-copilot claude-opus-4-6 to claude-opus-4.6", () => {
it("should match github-copilot claude-opus-4-7 to claude-opus-4.7", () => {
const available = new Set([
"github-copilot/claude-opus-4.6",
"github-copilot/claude-opus-4.7",
"opencode/big-pickle",
])
const result = fuzzyMatchModel("claude-opus-4-6", available, ["github-copilot"])
expect(result).toBe("github-copilot/claude-opus-4.6")
const result = fuzzyMatchModel("claude-opus-4-7", available, ["github-copilot"])
expect(result).toBe("github-copilot/claude-opus-4.7")
})
// given claude models can evolve to newer version numbers
@@ -275,7 +275,7 @@ describe("fuzzyMatchModel", () => {
it("should filter by provider when providers array is given", () => {
const available = new Set([
"openai/gpt-5.4",
"anthropic/claude-opus-4-6",
"anthropic/claude-opus-4-7",
"google/gemini-3",
])
const result = fuzzyMatchModel("gpt", available, ["openai"])
@@ -288,7 +288,7 @@ describe("fuzzyMatchModel", () => {
it("should return null when provider filter excludes all matches", () => {
const available = new Set([
"openai/gpt-5.4",
"anthropic/claude-opus-4-6",
"anthropic/claude-opus-4-7",
])
const result = fuzzyMatchModel("claude", available, ["openai"])
expect(result).toBeNull()
@@ -300,7 +300,7 @@ describe("fuzzyMatchModel", () => {
it("should return null when no match found", () => {
const available = new Set([
"openai/gpt-5.4",
"anthropic/claude-opus-4-6",
"anthropic/claude-opus-4-7",
])
const result = fuzzyMatchModel("gemini", available)
expect(result).toBeNull()
@@ -312,7 +312,7 @@ describe("fuzzyMatchModel", () => {
it("should match case-insensitively", () => {
const available = new Set([
"openai/gpt-5.4",
"anthropic/claude-opus-4-6",
"anthropic/claude-opus-4-7",
])
const result = fuzzyMatchModel("GPT-5.4", available)
expect(result).toBe("openai/gpt-5.4")
@@ -323,11 +323,11 @@ describe("fuzzyMatchModel", () => {
// then return exact match first
it("should prioritize exact match over longer variants", () => {
const available = new Set([
"anthropic/claude-opus-4-6",
"anthropic/claude-opus-4-6-extended",
"anthropic/claude-opus-4-7",
"anthropic/claude-opus-4-7-extended",
])
const result = fuzzyMatchModel("claude-opus-4-6", available)
expect(result).toBe("anthropic/claude-opus-4-6")
const result = fuzzyMatchModel("claude-opus-4-7", available)
expect(result).toBe("anthropic/claude-opus-4-7")
})
// given available models with similar model IDs (e.g., glm-5 and big-pickle)
@@ -372,7 +372,7 @@ describe("fuzzyMatchModel", () => {
it("should search all specified providers", () => {
const available = new Set([
"openai/gpt-5.4",
"anthropic/claude-opus-4-6",
"anthropic/claude-opus-4-7",
"google/gemini-3",
])
const result = fuzzyMatchModel("gpt", available, ["openai", "google"])
@@ -520,7 +520,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
it("should filter models by connected providers", async () => {
writeModelsCache({
openai: { models: { "gpt-5.4": { id: "gpt-5.4" } } },
anthropic: { models: { "claude-opus-4-6": { id: "claude-opus-4-6" } } },
anthropic: { models: { "claude-opus-4-7": { id: "claude-opus-4-7" } } },
google: { models: { "gemini-3.1-pro": { id: "gemini-3.1-pro" } } },
})
@@ -529,7 +529,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
})
expect(result.size).toBe(1)
expect(result.has("anthropic/claude-opus-4-6")).toBe(true)
expect(result.has("anthropic/claude-opus-4-7")).toBe(true)
expect(result.has("openai/gpt-5.4")).toBe(false)
expect(result.has("google/gemini-3.1-pro")).toBe(false)
})
@@ -540,7 +540,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
it("should filter models by multiple connected providers", async () => {
writeModelsCache({
openai: { models: { "gpt-5.4": { id: "gpt-5.4" } } },
anthropic: { models: { "claude-opus-4-6": { id: "claude-opus-4-6" } } },
anthropic: { models: { "claude-opus-4-7": { id: "claude-opus-4-7" } } },
google: { models: { "gemini-3.1-pro": { id: "gemini-3.1-pro" } } },
})
@@ -549,7 +549,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
})
expect(result.size).toBe(2)
expect(result.has("anthropic/claude-opus-4-6")).toBe(true)
expect(result.has("anthropic/claude-opus-4-7")).toBe(true)
expect(result.has("google/gemini-3.1-pro")).toBe(true)
expect(result.has("openai/gpt-5.4")).toBe(false)
})
@@ -560,7 +560,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
it("should return empty set when connectedProviders is empty", async () => {
writeModelsCache({
openai: { models: { "gpt-5.4": { id: "gpt-5.4" } } },
anthropic: { models: { "claude-opus-4-6": { id: "claude-opus-4-6" } } },
anthropic: { models: { "claude-opus-4-7": { id: "claude-opus-4-7" } } },
})
const result = await fetchAvailableModels(undefined, {
@@ -576,7 +576,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
it("should return empty set when connectedProviders not specified", async () => {
writeModelsCache({
openai: { models: { "gpt-5.4": { id: "gpt-5.4" } } },
anthropic: { models: { "claude-opus-4-6": { id: "claude-opus-4-6" } } },
anthropic: { models: { "claude-opus-4-7": { id: "claude-opus-4-7" } } },
})
const result = await fetchAvailableModels()
@@ -605,7 +605,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
it("should return models from providers that exist in both cache and connected list", async () => {
writeModelsCache({
openai: { models: { "gpt-5.4": { id: "gpt-5.4" } } },
anthropic: { models: { "claude-opus-4-6": { id: "claude-opus-4-6" } } },
anthropic: { models: { "claude-opus-4-7": { id: "claude-opus-4-7" } } },
})
const result = await fetchAvailableModels(undefined, {
@@ -613,7 +613,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
})
expect(result.size).toBe(1)
expect(result.has("anthropic/claude-opus-4-6")).toBe(true)
expect(result.has("anthropic/claude-opus-4-7")).toBe(true)
})
// given filtered fetch
@@ -622,7 +622,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
it("should not cache filtered results", async () => {
writeModelsCache({
openai: { models: { "gpt-5.4": { id: "gpt-5.4" } } },
anthropic: { models: { "claude-opus-4-6": { id: "claude-opus-4-6" } } },
anthropic: { models: { "claude-opus-4-7": { id: "claude-opus-4-7" } } },
})
// First call with anthropic
@@ -706,13 +706,13 @@ describe("fetchAvailableModels with provider-models cache (whitelist-filtered)",
writeProviderModelsCache({
models: {
opencode: ["big-pickle", "gpt-5-nano"],
anthropic: ["claude-opus-4-6"]
anthropic: ["claude-opus-4-7"]
},
connected: ["opencode", "anthropic"]
})
writeModelsCache({
opencode: { models: { "big-pickle": {}, "gpt-5-nano": {}, "gpt-5.4": {} } },
anthropic: { models: { "claude-opus-4-6": {}, "claude-sonnet-4-6": {} } }
anthropic: { models: { "claude-opus-4-7": {}, "claude-sonnet-4-6": {} } }
})
const result = await fetchAvailableModels(undefined, {
@@ -722,7 +722,7 @@ describe("fetchAvailableModels with provider-models cache (whitelist-filtered)",
expect(result.size).toBe(3)
expect(result.has("opencode/big-pickle")).toBe(true)
expect(result.has("opencode/gpt-5-nano")).toBe(true)
expect(result.has("anthropic/claude-opus-4-6")).toBe(true)
expect(result.has("anthropic/claude-opus-4-7")).toBe(true)
expect(result.has("opencode/gpt-5.4")).toBe(false)
expect(result.has("anthropic/claude-sonnet-4-6")).toBe(false)
})
@@ -773,7 +773,7 @@ describe("fetchAvailableModels with provider-models cache (whitelist-filtered)",
writeProviderModelsCache({
models: {
opencode: ["big-pickle"],
anthropic: ["claude-opus-4-6"],
anthropic: ["claude-opus-4-7"],
google: ["gemini-3.1-pro"]
},
connected: ["opencode", "anthropic", "google"]
@@ -785,7 +785,7 @@ describe("fetchAvailableModels with provider-models cache (whitelist-filtered)",
expect(result.size).toBe(1)
expect(result.has("opencode/big-pickle")).toBe(true)
expect(result.has("anthropic/claude-opus-4-6")).toBe(false)
expect(result.has("anthropic/claude-opus-4-7")).toBe(false)
expect(result.has("google/gemini-3.1-pro")).toBe(false)
})
@@ -812,7 +812,7 @@ describe("fetchAvailableModels with provider-models cache (whitelist-filtered)",
it("should handle mixed string[] and object[] formats across providers", async () => {
writeProviderModelsCache({
models: {
anthropic: ["claude-opus-4-6", "claude-sonnet-4-6"],
anthropic: ["claude-opus-4-7", "claude-sonnet-4-6"],
ollama: [
{ id: "ministral-3:14b-32k-agent", provider: "ollama" },
{ id: "qwen3-coder:32k-agent", provider: "ollama" }
@@ -826,7 +826,7 @@ describe("fetchAvailableModels with provider-models cache (whitelist-filtered)",
})
expect(result.size).toBe(4)
expect(result.has("anthropic/claude-opus-4-6")).toBe(true)
expect(result.has("anthropic/claude-opus-4-7")).toBe(true)
expect(result.has("anthropic/claude-sonnet-4-6")).toBe(true)
expect(result.has("ollama/ministral-3:14b-32k-agent")).toBe(true)
expect(result.has("ollama/qwen3-coder:32k-agent")).toBe(true)
@@ -859,7 +859,7 @@ describe("fetchAvailableModels with provider-models cache (whitelist-filtered)",
describe("isModelAvailable", () => {
it("returns true when model exists via fuzzy match", () => {
// given
const available = new Set(["openai/gpt-5.3-codex", "anthropic/claude-opus-4-6"])
const available = new Set(["openai/gpt-5.3-codex", "anthropic/claude-opus-4-7"])
// when
const result = isModelAvailable("gpt-5.3-codex", available)
@@ -870,7 +870,7 @@ describe("isModelAvailable", () => {
it("returns false when model not found", () => {
// given
const available = new Set(["anthropic/claude-opus-4-6"])
const available = new Set(["anthropic/claude-opus-4-7"])
// when
const result = isModelAvailable("gpt-5.3-codex", available)
@@ -924,7 +924,7 @@ describe("fallback model availability", () => {
it("returns null for completely unknown model", () => {
// given
const available = new Set(["openai/gpt-5.4", "anthropic/claude-opus-4-6"])
const available = new Set(["openai/gpt-5.4", "anthropic/claude-opus-4-7"])
// when
const result = fuzzyMatchModel("non-existent-model-family", available)
@@ -936,7 +936,7 @@ describe("fallback model availability", () => {
it("returns true when models do not match but provider is connected", () => {
// given
const fallbackChain = [{ providers: ["openai"], model: "gpt-5.4" }]
const availableModels = new Set(["anthropic/claude-opus-4-6"])
const availableModels = new Set(["anthropic/claude-opus-4-7"])
writeConnectedProvidersCache(["openai"])
// when
@@ -950,10 +950,10 @@ describe("fallback model availability", () => {
// given
const fallbackChain = [
{ providers: ["openai"], model: "gpt-5.4" },
{ providers: ["anthropic"], model: "claude-opus-4-6" },
{ providers: ["anthropic"], model: "claude-opus-4-7" },
]
const availableModels = new Set([
"anthropic/claude-opus-4-6",
"anthropic/claude-opus-4-7",
"openai/gpt-5.4-preview",
])
@@ -968,7 +968,7 @@ describe("fallback model availability", () => {
// given
const fallbackChain = [
{ providers: ["openai"], model: "gpt-5.4" },
{ providers: ["anthropic"], model: "claude-opus-4-6" },
{ providers: ["anthropic"], model: "claude-opus-4-7" },
]
const availableModels = new Set(["google/gemini-3.1-pro"])
+1 -1
View File
@@ -21,7 +21,7 @@ import { normalizeSDKResponse } from "./normalize-sdk-response"
* If providers array is given, only models starting with "provider/" are considered.
*
* @example
* const available = new Set(["openai/gpt-5.4", "openai/gpt-5.3-codex", "anthropic/claude-opus-4-6"])
* const available = new Set(["openai/gpt-5.4", "openai/gpt-5.3-codex", "anthropic/claude-opus-4-7"])
* fuzzyMatchModel("gpt-5.4", available) // → "openai/gpt-5.4"
* fuzzyMatchModel("claude", available, ["openai"]) // → null (provider filter excludes anthropic)
*/
+9 -9
View File
@@ -16,8 +16,8 @@ describe("getModelCapabilities", () => {
generatedAt: "2026-03-25T00:00:00.000Z",
sourceUrl: "https://models.dev/api.json",
models: {
"claude-opus-4-6": {
id: "claude-opus-4-6",
"claude-opus-4-7": {
id: "claude-opus-4-7",
family: "claude-opus",
reasoning: true,
temperature: true,
@@ -66,7 +66,7 @@ describe("getModelCapabilities", () => {
findProviderModelMetadataSpy = spyOn(connectedProvidersCache, "findProviderModelMetadata").mockReturnValue(undefined)
const result = getModelCapabilities({
providerID: "anthropic",
modelID: "claude-opus-4-6",
modelID: "claude-opus-4-7",
runtimeModel: {
variants: {
low: {},
@@ -78,7 +78,7 @@ describe("getModelCapabilities", () => {
})
expect(result).toMatchObject({
canonicalModelID: "claude-opus-4-6",
canonicalModelID: "claude-opus-4-7",
family: "claude-opus",
variants: ["low", "medium", "high"],
supportsThinking: true,
@@ -173,12 +173,12 @@ describe("getModelCapabilities", () => {
findProviderModelMetadataSpy = spyOn(connectedProvidersCache, "findProviderModelMetadata").mockReturnValue(undefined)
const result = getModelCapabilities({
providerID: "anthropic",
modelID: "claude-opus-4-6-thinking",
modelID: "claude-opus-4-7-thinking",
bundledSnapshot,
})
expect(result).toMatchObject({
canonicalModelID: "claude-opus-4-6",
canonicalModelID: "claude-opus-4-7",
family: "claude-opus",
supportsThinking: true,
supportsTemperature: true,
@@ -247,13 +247,13 @@ describe("getModelCapabilities", () => {
test("canonicalizes provider-prefixed Claude thinking aliases to bare snapshot IDs", () => {
const result = getModelCapabilities({
providerID: "anthropic",
modelID: "anthropic/claude-opus-4-6-thinking",
modelID: "anthropic/claude-opus-4-7-thinking",
bundledSnapshot,
})
expect(result).toMatchObject({
requestedModelID: "anthropic/claude-opus-4-6-thinking",
canonicalModelID: "claude-opus-4-6",
requestedModelID: "anthropic/claude-opus-4-7-thinking",
canonicalModelID: "claude-opus-4-7",
family: "claude-opus",
supportsThinking: true,
supportsTemperature: true,
+9 -9
View File
@@ -67,22 +67,22 @@ describe("model-capability-aliases", () => {
})
test("normalizes provider-prefixed Claude thinking aliases through a pattern rule", () => {
const result = resolveModelIDAlias("anthropic/claude-opus-4-6-thinking")
const result = resolveModelIDAlias("anthropic/claude-opus-4-7-thinking")
expect(result).toEqual({
requestedModelID: "anthropic/claude-opus-4-6-thinking",
canonicalModelID: "claude-opus-4-6",
requestedModelID: "anthropic/claude-opus-4-7-thinking",
canonicalModelID: "claude-opus-4-7",
source: "pattern-alias",
ruleID: "claude-thinking-legacy-alias",
})
})
test("does not pattern-match nearby canonical Claude IDs incorrectly", () => {
const result = resolveModelIDAlias("claude-opus-4-6-think")
const result = resolveModelIDAlias("claude-opus-4-7-think")
expect(result).toEqual({
requestedModelID: "claude-opus-4-6-think",
canonicalModelID: "claude-opus-4-6-think",
requestedModelID: "claude-opus-4-7-think",
canonicalModelID: "claude-opus-4-7-think",
source: "canonical",
})
})
@@ -98,11 +98,11 @@ describe("model-capability-aliases", () => {
})
test("normalizes legacy Claude thinking aliases through a pattern rule", () => {
const result = resolveModelIDAlias("claude-opus-4-6-thinking")
const result = resolveModelIDAlias("claude-opus-4-7-thinking")
expect(result).toEqual({
requestedModelID: "claude-opus-4-6-thinking",
canonicalModelID: "claude-opus-4-6",
requestedModelID: "claude-opus-4-7-thinking",
canonicalModelID: "claude-opus-4-7",
source: "pattern-alias",
ruleID: "claude-thinking-legacy-alias",
})
+2 -2
View File
@@ -42,8 +42,8 @@ const PATTERN_ALIAS_RULES: ReadonlyArray<PatternAliasRule> = [
{
ruleID: "claude-thinking-legacy-alias",
description: "Normalizes the legacy Claude Opus 4.6 thinking suffix to the canonical snapshot ID.",
match: (normalizedModelID) => /^claude-opus-4-6-thinking$/.test(normalizedModelID),
canonicalize: () => "claude-opus-4-6",
match: (normalizedModelID) => /^claude-opus-4-7-thinking$/.test(normalizedModelID),
canonicalize: () => "claude-opus-4-7",
},
{
ruleID: "gemini-3.1-pro-tier-alias",
@@ -19,7 +19,7 @@ describe("model-capability-guardrails", () => {
expect(modelIDs).toEqual([...modelIDs].sort())
expect(new Set(modelIDs).size).toBe(modelIDs.length)
expect(modelIDs).toContain("claude-opus-4-6")
expect(modelIDs).toContain("claude-opus-4-7")
expect(modelIDs).toContain("gpt-5.4")
expect(modelIDs).toContain("kimi-k2.5")
})
+1 -1
View File
@@ -31,7 +31,7 @@ describe("model-error-classifier", () => {
//#given
const error = {
message:
"All credentials for model claude-opus-4-6-thinking are cooling down [retrying in ~5 days attempt #1]",
"All credentials for model claude-opus-4-7-thinking are cooling down [retrying in ~5 days attempt #1]",
}
//#when
+2 -2
View File
@@ -9,8 +9,8 @@ describe("normalizeModelFormat", () => {
})
it("handles provider with multiple slashes", () => {
const result = normalizeModelFormat("anthropic/claude-opus-4-6/max")
expect(result).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-6/max" })
const result = normalizeModelFormat("anthropic/claude-opus-4-7/max")
expect(result).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-7/max" })
})
it("returns undefined for malformed string without separator", () => {
+16 -16
View File
@@ -23,7 +23,7 @@ describe("AGENT_MODEL_REQUIREMENTS", () => {
expect(primary.variant).toBe("high")
})
test("sisyphus has claude-opus-4-6 as primary with k2p5, kimi-k2.5, gpt-5.4 medium fallbacks", () => {
test("sisyphus has claude-opus-4-7 as primary with k2p5, kimi-k2.5, gpt-5.4 medium fallbacks", () => {
// #given - sisyphus agent requirement
const sisyphus = AGENT_MODEL_REQUIREMENTS["sisyphus"]
@@ -36,7 +36,7 @@ describe("AGENT_MODEL_REQUIREMENTS", () => {
const primary = sisyphus.fallbackChain[0]
expect(primary.providers).toEqual(["anthropic", "github-copilot", "opencode", "vercel"])
expect(primary.model).toBe("claude-opus-4-6")
expect(primary.model).toBe("claude-opus-4-7")
expect(primary.variant).toBe("max")
const second = sisyphus.fallbackChain[1]
@@ -148,34 +148,34 @@ describe("AGENT_MODEL_REQUIREMENTS", () => {
expect(last.model).toBe("gpt-5-nano")
})
test("prometheus has claude-opus-4-6 as primary", () => {
test("prometheus has claude-opus-4-7 as primary", () => {
// #given - prometheus agent requirement
const prometheus = AGENT_MODEL_REQUIREMENTS["prometheus"]
// #when - accessing Prometheus requirement
// #then - claude-opus-4-6 is first
// #then - claude-opus-4-7 is first
expect(prometheus).toBeDefined()
expect(prometheus.fallbackChain).toBeArray()
expect(prometheus.fallbackChain.length).toBeGreaterThan(1)
const primary = prometheus.fallbackChain[0]
expect(primary.model).toBe("claude-opus-4-6")
expect(primary.model).toBe("claude-opus-4-7")
expect(primary.providers).toEqual(["anthropic", "github-copilot", "opencode", "vercel"])
expect(primary.variant).toBe("max")
})
test("metis has claude-opus-4-6 as primary", () => {
test("metis has claude-opus-4-7 as primary", () => {
// #given - metis agent requirement
const metis = AGENT_MODEL_REQUIREMENTS["metis"]
// #when - accessing Metis requirement
// #then - claude-opus-4-6 is first
// #then - claude-opus-4-7 is first
expect(metis).toBeDefined()
expect(metis.fallbackChain).toBeArray()
expect(metis.fallbackChain.length).toBeGreaterThan(1)
const primary = metis.fallbackChain[0]
expect(primary.model).toBe("claude-opus-4-6")
expect(primary.model).toBe("claude-opus-4-7")
expect(primary.providers).toEqual(["anthropic", "github-copilot", "opencode", "vercel"])
expect(primary.variant).toBe("max")
@@ -356,7 +356,7 @@ describe("CATEGORY_MODEL_REQUIREMENTS", () => {
expect(second.model).toBe("glm-5")
const third = visualEngineering.fallbackChain[2]
expect(third.model).toBe("claude-opus-4-6")
expect(third.model).toBe("claude-opus-4-7")
expect(third.variant).toBe("max")
const fourth = visualEngineering.fallbackChain[3]
@@ -402,18 +402,18 @@ describe("CATEGORY_MODEL_REQUIREMENTS", () => {
expect(primary.providers[0]).toBe("anthropic")
})
test("unspecified-high has claude-opus-4-6 as primary and gpt-5.4 as secondary", () => {
test("unspecified-high has claude-opus-4-7 as primary and gpt-5.4 as secondary", () => {
// #given - unspecified-high category requirement
const unspecifiedHigh = CATEGORY_MODEL_REQUIREMENTS["unspecified-high"]
// #when - accessing unspecified-high requirement
// #then - claude-opus-4-6 is first and gpt-5.4 is second
// #then - claude-opus-4-7 is first and gpt-5.4 is second
expect(unspecifiedHigh).toBeDefined()
expect(unspecifiedHigh.fallbackChain).toBeArray()
expect(unspecifiedHigh.fallbackChain.length).toBeGreaterThan(1)
const primary = unspecifiedHigh.fallbackChain[0]
expect(primary.model).toBe("claude-opus-4-6")
expect(primary.model).toBe("claude-opus-4-7")
expect(primary.variant).toBe("max")
expect(primary.providers).toEqual(["anthropic", "github-copilot", "opencode", "vercel"])
@@ -505,14 +505,14 @@ describe("FallbackEntry type", () => {
// given - a valid FallbackEntry object
const entry: FallbackEntry = {
providers: ["anthropic", "github-copilot", "opencode"],
model: "claude-opus-4-6",
model: "claude-opus-4-7",
variant: "high",
}
// when - accessing properties
// then - all properties are accessible
expect(entry.providers).toEqual(["anthropic", "github-copilot", "opencode"])
expect(entry.model).toBe("claude-opus-4-6")
expect(entry.model).toBe("claude-opus-4-7")
expect(entry.variant).toBe("high")
})
@@ -534,7 +534,7 @@ describe("ModelRequirement type", () => {
// given - a valid ModelRequirement object
const requirement: ModelRequirement = {
fallbackChain: [
{ providers: ["anthropic", "github-copilot"], model: "claude-opus-4-6", variant: "max" },
{ providers: ["anthropic", "github-copilot"], model: "claude-opus-4-7", variant: "max" },
{ providers: ["openai", "github-copilot"], model: "gpt-5.4", variant: "high" },
],
}
@@ -543,7 +543,7 @@ describe("ModelRequirement type", () => {
// then - fallbackChain is accessible with correct structure
expect(requirement.fallbackChain).toBeArray()
expect(requirement.fallbackChain).toHaveLength(2)
expect(requirement.fallbackChain[0].model).toBe("claude-opus-4-6")
expect(requirement.fallbackChain[0].model).toBe("claude-opus-4-7")
expect(requirement.fallbackChain[1].model).toBe("gpt-5.4")
})
+10 -10
View File
@@ -22,7 +22,7 @@ export const AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
fallbackChain: [
{
providers: ["anthropic", "github-copilot", "opencode", "vercel"],
model: "claude-opus-4-6",
model: "claude-opus-4-7",
variant: "max",
},
{ providers: ["opencode-go", "vercel"], model: "kimi-k2.5" },
@@ -69,7 +69,7 @@ export const AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
},
{
providers: ["anthropic", "github-copilot", "opencode", "vercel"],
model: "claude-opus-4-6",
model: "claude-opus-4-7",
variant: "max",
},
{ providers: ["opencode-go", "vercel"], model: "glm-5" },
@@ -104,7 +104,7 @@ export const AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
fallbackChain: [
{
providers: ["anthropic", "github-copilot", "opencode", "vercel"],
model: "claude-opus-4-6",
model: "claude-opus-4-7",
variant: "max",
},
{
@@ -123,7 +123,7 @@ export const AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
fallbackChain: [
{
providers: ["anthropic", "github-copilot", "opencode", "vercel"],
model: "claude-opus-4-6",
model: "claude-opus-4-7",
variant: "max",
},
{
@@ -144,7 +144,7 @@ export const AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
},
{
providers: ["anthropic", "github-copilot", "opencode", "vercel"],
model: "claude-opus-4-6",
model: "claude-opus-4-7",
variant: "max",
},
{
@@ -193,7 +193,7 @@ export const CATEGORY_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
{ providers: ["zai-coding-plan", "opencode", "vercel"], model: "glm-5" },
{
providers: ["anthropic", "github-copilot", "opencode", "vercel"],
model: "claude-opus-4-6",
model: "claude-opus-4-7",
variant: "max",
},
{ providers: ["opencode-go", "vercel"], model: "glm-5" },
@@ -214,7 +214,7 @@ export const CATEGORY_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
},
{
providers: ["anthropic", "github-copilot", "opencode", "vercel"],
model: "claude-opus-4-6",
model: "claude-opus-4-7",
variant: "max",
},
{ providers: ["opencode-go", "vercel"], model: "glm-5" },
@@ -229,7 +229,7 @@ export const CATEGORY_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
},
{
providers: ["anthropic", "github-copilot", "opencode", "vercel"],
model: "claude-opus-4-6",
model: "claude-opus-4-7",
variant: "max",
},
{
@@ -248,7 +248,7 @@ export const CATEGORY_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
},
{
providers: ["anthropic", "github-copilot", "opencode", "vercel"],
model: "claude-opus-4-6",
model: "claude-opus-4-7",
variant: "max",
},
{ providers: ["openai", "github-copilot", "opencode", "vercel"], model: "gpt-5.4" },
@@ -296,7 +296,7 @@ export const CATEGORY_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
fallbackChain: [
{
providers: ["anthropic", "github-copilot", "opencode", "vercel"],
model: "claude-opus-4-6",
model: "claude-opus-4-7",
variant: "max",
},
{
+64 -64
View File
@@ -12,7 +12,7 @@ describe("resolveModel", () => {
test("returns userModel when all three are set", () => {
// given
const input: ModelResolutionInput = {
userModel: "anthropic/claude-opus-4-6",
userModel: "anthropic/claude-opus-4-7",
inheritedModel: "openai/gpt-5.4",
systemDefault: "google/gemini-3.1-pro",
}
@@ -21,7 +21,7 @@ describe("resolveModel", () => {
const result = resolveModel(input)
// then
expect(result).toBe("anthropic/claude-opus-4-6")
expect(result).toBe("anthropic/claude-opus-4-7")
})
test("returns inheritedModel when userModel is undefined", () => {
@@ -91,7 +91,7 @@ describe("resolveModel", () => {
test("same input returns same output (referential transparency)", () => {
// given
const input: ModelResolutionInput = {
userModel: "anthropic/claude-opus-4-6",
userModel: "anthropic/claude-opus-4-7",
inheritedModel: "openai/gpt-5.4",
systemDefault: "google/gemini-3.1-pro",
}
@@ -122,11 +122,11 @@ describe("resolveModelWithFallback", () => {
// given
const input: ExtendedModelResolutionInput = {
uiSelectedModel: "opencode/big-pickle",
userModel: "anthropic/claude-opus-4-6",
userModel: "anthropic/claude-opus-4-7",
fallbackChain: [
{ providers: ["anthropic", "github-copilot"], model: "claude-opus-4-6" },
{ providers: ["anthropic", "github-copilot"], model: "claude-opus-4-7" },
],
availableModels: new Set(["anthropic/claude-opus-4-6", "github-copilot/claude-opus-4-6-preview"]),
availableModels: new Set(["anthropic/claude-opus-4-7", "github-copilot/claude-opus-4-7-preview"]),
systemDefaultModel: "google/gemini-3.1-pro",
}
@@ -143,8 +143,8 @@ describe("resolveModelWithFallback", () => {
// given
const input: ExtendedModelResolutionInput = {
uiSelectedModel: "opencode/big-pickle",
userModel: "anthropic/claude-opus-4-6",
availableModels: new Set(["anthropic/claude-opus-4-6"]),
userModel: "anthropic/claude-opus-4-7",
availableModels: new Set(["anthropic/claude-opus-4-7"]),
systemDefaultModel: "google/gemini-3.1-pro",
}
@@ -160,8 +160,8 @@ describe("resolveModelWithFallback", () => {
// given
const input: ExtendedModelResolutionInput = {
uiSelectedModel: " ",
userModel: "anthropic/claude-opus-4-6",
availableModels: new Set(["anthropic/claude-opus-4-6"]),
userModel: "anthropic/claude-opus-4-7",
availableModels: new Set(["anthropic/claude-opus-4-7"]),
systemDefaultModel: "google/gemini-3.1-pro",
}
@@ -169,16 +169,16 @@ describe("resolveModelWithFallback", () => {
const result = resolveModelWithFallback(input)
// then
expect(result!.model).toBe("anthropic/claude-opus-4-6")
expect(logSpy).toHaveBeenCalledWith("Model resolved via config override", { model: "anthropic/claude-opus-4-6" })
expect(result!.model).toBe("anthropic/claude-opus-4-7")
expect(logSpy).toHaveBeenCalledWith("Model resolved via config override", { model: "anthropic/claude-opus-4-7" })
})
test("empty string uiSelectedModel falls through to config override", () => {
// given
const input: ExtendedModelResolutionInput = {
uiSelectedModel: "",
userModel: "anthropic/claude-opus-4-6",
availableModels: new Set(["anthropic/claude-opus-4-6"]),
userModel: "anthropic/claude-opus-4-7",
availableModels: new Set(["anthropic/claude-opus-4-7"]),
systemDefaultModel: "google/gemini-3.1-pro",
}
@@ -186,7 +186,7 @@ describe("resolveModelWithFallback", () => {
const result = resolveModelWithFallback(input)
// then
expect(result!.model).toBe("anthropic/claude-opus-4-6")
expect(result!.model).toBe("anthropic/claude-opus-4-7")
})
})
@@ -194,11 +194,11 @@ describe("resolveModelWithFallback", () => {
test("returns userModel with override source when userModel is provided", () => {
// given
const input: ExtendedModelResolutionInput = {
userModel: "anthropic/claude-opus-4-6",
userModel: "anthropic/claude-opus-4-7",
fallbackChain: [
{ providers: ["anthropic", "github-copilot"], model: "claude-opus-4-6" },
{ providers: ["anthropic", "github-copilot"], model: "claude-opus-4-7" },
],
availableModels: new Set(["anthropic/claude-opus-4-6", "github-copilot/claude-opus-4-6-preview"]),
availableModels: new Set(["anthropic/claude-opus-4-7", "github-copilot/claude-opus-4-7-preview"]),
systemDefaultModel: "google/gemini-3.1-pro",
}
@@ -206,9 +206,9 @@ describe("resolveModelWithFallback", () => {
const result = resolveModelWithFallback(input)
// then
expect(result!.model).toBe("anthropic/claude-opus-4-6")
expect(result!.model).toBe("anthropic/claude-opus-4-7")
expect(result!.source).toBe("override")
expect(logSpy).toHaveBeenCalledWith("Model resolved via config override", { model: "anthropic/claude-opus-4-6" })
expect(logSpy).toHaveBeenCalledWith("Model resolved via config override", { model: "anthropic/claude-opus-4-7" })
})
test("override takes priority even if model not in availableModels", () => {
@@ -216,9 +216,9 @@ describe("resolveModelWithFallback", () => {
const input: ExtendedModelResolutionInput = {
userModel: "custom/my-model",
fallbackChain: [
{ providers: ["anthropic"], model: "claude-opus-4-6" },
{ providers: ["anthropic"], model: "claude-opus-4-7" },
],
availableModels: new Set(["anthropic/claude-opus-4-6"]),
availableModels: new Set(["anthropic/claude-opus-4-7"]),
systemDefaultModel: "google/gemini-3.1-pro",
}
@@ -235,9 +235,9 @@ describe("resolveModelWithFallback", () => {
const input: ExtendedModelResolutionInput = {
userModel: " ",
fallbackChain: [
{ providers: ["anthropic"], model: "claude-opus-4-6" },
{ providers: ["anthropic"], model: "claude-opus-4-7" },
],
availableModels: new Set(["anthropic/claude-opus-4-6"]),
availableModels: new Set(["anthropic/claude-opus-4-7"]),
systemDefaultModel: "google/gemini-3.1-pro",
}
@@ -253,9 +253,9 @@ describe("resolveModelWithFallback", () => {
const input: ExtendedModelResolutionInput = {
userModel: "",
fallbackChain: [
{ providers: ["anthropic"], model: "claude-opus-4-6" },
{ providers: ["anthropic"], model: "claude-opus-4-7" },
],
availableModels: new Set(["anthropic/claude-opus-4-6"]),
availableModels: new Set(["anthropic/claude-opus-4-7"]),
systemDefaultModel: "google/gemini-3.1-pro",
}
@@ -272,9 +272,9 @@ describe("resolveModelWithFallback", () => {
// given
const input: ExtendedModelResolutionInput = {
fallbackChain: [
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6" },
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-7" },
],
availableModels: new Set(["github-copilot/claude-opus-4-6-preview", "opencode/claude-opus-4-7"]),
availableModels: new Set(["github-copilot/claude-opus-4-7-preview", "opencode/claude-opus-4-7"]),
systemDefaultModel: "google/gemini-3.1-pro",
}
@@ -282,12 +282,12 @@ describe("resolveModelWithFallback", () => {
const result = resolveModelWithFallback(input)
// then
expect(result!.model).toBe("github-copilot/claude-opus-4-6-preview")
expect(result!.model).toBe("github-copilot/claude-opus-4-7-preview")
expect(result!.source).toBe("provider-fallback")
expect(logSpy).toHaveBeenCalledWith("Model resolved via fallback chain (availability confirmed)", {
provider: "github-copilot",
model: "claude-opus-4-6",
match: "github-copilot/claude-opus-4-6-preview",
model: "claude-opus-4-7",
match: "github-copilot/claude-opus-4-7-preview",
variant: undefined,
})
})
@@ -298,7 +298,7 @@ describe("resolveModelWithFallback", () => {
fallbackChain: [
{ providers: ["openai", "anthropic", "google"], model: "gpt-5.4" },
],
availableModels: new Set(["openai/gpt-5.4", "anthropic/claude-opus-4-6", "google/gemini-3.1-pro"]),
availableModels: new Set(["openai/gpt-5.4", "anthropic/claude-opus-4-7", "google/gemini-3.1-pro"]),
systemDefaultModel: "google/gemini-3.1-pro",
}
@@ -334,7 +334,7 @@ describe("resolveModelWithFallback", () => {
fallbackChain: [
{ providers: ["anthropic", "github-copilot"], model: "claude-opus" },
],
availableModels: new Set(["anthropic/claude-opus-4-6", "github-copilot/claude-opus-4-6-preview"]),
availableModels: new Set(["anthropic/claude-opus-4-7", "github-copilot/claude-opus-4-7-preview"]),
systemDefaultModel: "google/gemini-3.1-pro",
}
@@ -342,14 +342,14 @@ describe("resolveModelWithFallback", () => {
const result = resolveModelWithFallback(input)
// then
expect(result!.model).toBe("anthropic/claude-opus-4-6")
expect(result!.model).toBe("anthropic/claude-opus-4-7")
expect(result!.source).toBe("provider-fallback")
})
test("skips fallback chain when not provided", () => {
// given
const input: ExtendedModelResolutionInput = {
availableModels: new Set(["anthropic/claude-opus-4-6"]),
availableModels: new Set(["anthropic/claude-opus-4-7"]),
systemDefaultModel: "google/gemini-3.1-pro",
}
@@ -364,7 +364,7 @@ describe("resolveModelWithFallback", () => {
// given
const input: ExtendedModelResolutionInput = {
fallbackChain: [],
availableModels: new Set(["anthropic/claude-opus-4-6"]),
availableModels: new Set(["anthropic/claude-opus-4-7"]),
systemDefaultModel: "google/gemini-3.1-pro",
}
@@ -381,7 +381,7 @@ describe("resolveModelWithFallback", () => {
fallbackChain: [
{ providers: ["anthropic"], model: "CLAUDE-OPUS" },
],
availableModels: new Set(["anthropic/claude-opus-4-6"]),
availableModels: new Set(["anthropic/claude-opus-4-7"]),
systemDefaultModel: "google/gemini-3.1-pro",
}
@@ -389,7 +389,7 @@ describe("resolveModelWithFallback", () => {
const result = resolveModelWithFallback(input)
// then
expect(result!.model).toBe("anthropic/claude-opus-4-6")
expect(result!.model).toBe("anthropic/claude-opus-4-7")
expect(result!.source).toBe("provider-fallback")
})
@@ -480,7 +480,7 @@ describe("resolveModelWithFallback", () => {
fallbackChain: [
{ providers: ["anthropic"], model: "nonexistent-model" },
],
availableModels: new Set(["openai/gpt-5.4", "anthropic/claude-opus-4-6"]),
availableModels: new Set(["openai/gpt-5.4", "anthropic/claude-opus-4-7"]),
systemDefaultModel: "google/gemini-3.1-pro",
}
@@ -498,7 +498,7 @@ describe("resolveModelWithFallback", () => {
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null)
const input: ExtendedModelResolutionInput = {
fallbackChain: [
{ providers: ["anthropic"], model: "claude-opus-4-6" },
{ providers: ["anthropic"], model: "claude-opus-4-7" },
],
availableModels: new Set(),
systemDefaultModel: undefined, // no system default configured
@@ -517,7 +517,7 @@ describe("resolveModelWithFallback", () => {
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai", "google"])
const input: ExtendedModelResolutionInput = {
fallbackChain: [
{ providers: ["anthropic", "openai"], model: "claude-opus-4-6" },
{ providers: ["anthropic", "openai"], model: "claude-opus-4-7" },
],
availableModels: new Set(),
systemDefaultModel: "google/gemini-3.1-pro",
@@ -527,7 +527,7 @@ describe("resolveModelWithFallback", () => {
const result = resolveModelWithFallback(input)
// then - should use connected provider (openai) from fallback chain
expect(result!.model).toBe("openai/claude-opus-4-6")
expect(result!.model).toBe("openai/claude-opus-4-7")
expect(result!.source).toBe("provider-fallback")
cacheSpy.mockRestore()
})
@@ -561,14 +561,14 @@ describe("resolveModelWithFallback", () => {
{ providers: ["openai", "opencode"], model: "claude-haiku-4-5" },
],
availableModels: new Set(),
systemDefaultModel: "anthropic/claude-opus-4-6-20251101",
systemDefaultModel: "anthropic/claude-opus-4-7-20251101",
}
// when
const result = resolveModelWithFallback(input)
// then - no provider in fallback is connected, fall through to system default
expect(result!.model).toBe("anthropic/claude-opus-4-6-20251101")
expect(result!.model).toBe("anthropic/claude-opus-4-7-20251101")
expect(result!.source).toBe("system-default")
cacheSpy.mockRestore()
})
@@ -578,7 +578,7 @@ describe("resolveModelWithFallback", () => {
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null)
const input: ExtendedModelResolutionInput = {
fallbackChain: [
{ providers: ["anthropic"], model: "claude-opus-4-6" },
{ providers: ["anthropic"], model: "claude-opus-4-7" },
],
availableModels: new Set(),
systemDefaultModel: "google/gemini-3.1-pro",
@@ -612,20 +612,20 @@ describe("resolveModelWithFallback", () => {
describe("Multi-entry fallbackChain", () => {
test("resolves to claude-opus when OpenAI unavailable but Anthropic available (oracle scenario)", () => {
// given
const availableModels = new Set(["anthropic/claude-opus-4-6"])
const availableModels = new Set(["anthropic/claude-opus-4-7"])
// when
const result = resolveModelWithFallback({
fallbackChain: [
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.4", variant: "high" },
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-7", variant: "max" },
],
availableModels,
systemDefaultModel: "system/default",
})
// then
expect(result!.model).toBe("anthropic/claude-opus-4-6")
expect(result!.model).toBe("anthropic/claude-opus-4-7")
expect(result!.source).toBe("provider-fallback")
})
@@ -652,14 +652,14 @@ describe("resolveModelWithFallback", () => {
// given
const availableModels = new Set([
"openai/gpt-5.4",
"anthropic/claude-opus-4-6",
"anthropic/claude-opus-4-7",
])
// when
const result = resolveModelWithFallback({
fallbackChain: [
{ providers: ["openai"], model: "gpt-5.4" },
{ providers: ["anthropic"], model: "claude-opus-4-6" },
{ providers: ["anthropic"], model: "claude-opus-4-7" },
],
availableModels,
systemDefaultModel: "system/default",
@@ -678,7 +678,7 @@ describe("resolveModelWithFallback", () => {
const result = resolveModelWithFallback({
fallbackChain: [
{ providers: ["openai"], model: "gpt-5.4" },
{ providers: ["anthropic"], model: "claude-opus-4-6" },
{ providers: ["anthropic"], model: "claude-opus-4-7" },
{ providers: ["google"], model: "gemini-3.1-pro" },
],
availableModels,
@@ -695,7 +695,7 @@ describe("resolveModelWithFallback", () => {
test("result has correct ModelResolutionResult shape", () => {
// given
const input: ExtendedModelResolutionInput = {
userModel: "anthropic/claude-opus-4-6",
userModel: "anthropic/claude-opus-4-7",
availableModels: new Set(),
systemDefaultModel: "google/gemini-3.1-pro",
}
@@ -718,7 +718,7 @@ describe("resolveModelWithFallback", () => {
fallbackChain: [
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3.1-pro" },
],
availableModels: new Set(["google/gemini-3.1-pro-preview", "anthropic/claude-opus-4-6"]),
availableModels: new Set(["google/gemini-3.1-pro-preview", "anthropic/claude-opus-4-7"]),
systemDefaultModel: "anthropic/claude-sonnet-4-6",
}
@@ -754,9 +754,9 @@ describe("resolveModelWithFallback", () => {
const input: ExtendedModelResolutionInput = {
categoryDefaultModel: "google/gemini-3.1-pro",
fallbackChain: [
{ providers: ["anthropic"], model: "claude-opus-4-6" },
{ providers: ["anthropic"], model: "claude-opus-4-7" },
],
availableModels: new Set(["anthropic/claude-opus-4-6"]),
availableModels: new Set(["anthropic/claude-opus-4-7"]),
systemDefaultModel: "system/default",
}
@@ -764,19 +764,19 @@ describe("resolveModelWithFallback", () => {
const result = resolveModelWithFallback(input)
// then - should fall through to fallbackChain
expect(result!.model).toBe("anthropic/claude-opus-4-6")
expect(result!.model).toBe("anthropic/claude-opus-4-7")
expect(result!.source).toBe("provider-fallback")
})
test("userModel takes priority over categoryDefaultModel", () => {
// given - both userModel and categoryDefaultModel provided
const input: ExtendedModelResolutionInput = {
userModel: "anthropic/claude-opus-4-6",
userModel: "anthropic/claude-opus-4-7",
categoryDefaultModel: "google/gemini-3.1-pro",
fallbackChain: [
{ providers: ["google"], model: "gemini-3.1-pro" },
],
availableModels: new Set(["google/gemini-3.1-pro-preview", "anthropic/claude-opus-4-6"]),
availableModels: new Set(["google/gemini-3.1-pro-preview", "anthropic/claude-opus-4-7"]),
systemDefaultModel: "system/default",
}
@@ -784,7 +784,7 @@ describe("resolveModelWithFallback", () => {
const result = resolveModelWithFallback(input)
// then - userModel wins
expect(result!.model).toBe("anthropic/claude-opus-4-6")
expect(result!.model).toBe("anthropic/claude-opus-4-7")
expect(result!.source).toBe("override")
})
@@ -916,7 +916,7 @@ describe("resolveModelWithFallback", () => {
test("still returns override when userModel provided even if systemDefaultModel undefined", () => {
// given
const input: ExtendedModelResolutionInput = {
userModel: "anthropic/claude-opus-4-6",
userModel: "anthropic/claude-opus-4-7",
availableModels: new Set(),
systemDefaultModel: undefined,
}
@@ -926,7 +926,7 @@ describe("resolveModelWithFallback", () => {
// then
expect(result).toBeDefined()
expect(result!.model).toBe("anthropic/claude-opus-4-6")
expect(result!.model).toBe("anthropic/claude-opus-4-7")
expect(result!.source).toBe("override")
})
@@ -934,9 +934,9 @@ describe("resolveModelWithFallback", () => {
// given
const input: ExtendedModelResolutionInput = {
fallbackChain: [
{ providers: ["anthropic"], model: "claude-opus-4-6" },
{ providers: ["anthropic"], model: "claude-opus-4-7" },
],
availableModels: new Set(["anthropic/claude-opus-4-6"]),
availableModels: new Set(["anthropic/claude-opus-4-7"]),
systemDefaultModel: undefined,
}
@@ -945,7 +945,7 @@ describe("resolveModelWithFallback", () => {
// then
expect(result).toBeDefined()
expect(result!.model).toBe("anthropic/claude-opus-4-6")
expect(result!.model).toBe("anthropic/claude-opus-4-7")
expect(result!.source).toBe("provider-fallback")
})
})
@@ -6,7 +6,7 @@ describe("resolveCompatibleModelSettings", () => {
test("keeps supported Claude Opus variant unchanged", () => {
const result = resolveCompatibleModelSettings({
providerID: "anthropic",
modelID: "claude-opus-4-6",
modelID: "claude-opus-4-7",
desired: { variant: "max" },
})
@@ -20,7 +20,7 @@ describe("resolveCompatibleModelSettings", () => {
test("uses model metadata first for variant support", () => {
const result = resolveCompatibleModelSettings({
providerID: "anthropic",
modelID: "claude-opus-4-6",
modelID: "claude-opus-4-7",
desired: { variant: "max" },
capabilities: { variants: ["low", "medium", "high"] },
})
@@ -42,7 +42,7 @@ describe("resolveCompatibleModelSettings", () => {
test("prefers metadata over family heuristics even when family would allow a higher level", () => {
const result = resolveCompatibleModelSettings({
providerID: "anthropic",
modelID: "claude-opus-4-6",
modelID: "claude-opus-4-7",
desired: { variant: "max" },
capabilities: { variants: ["low", "medium"] },
})
@@ -514,7 +514,7 @@ describe("resolveCompatibleModelSettings", () => {
test("no-op when desired settings are empty", () => {
const result = resolveCompatibleModelSettings({
providerID: "anthropic",
modelID: "claude-opus-4-6",
modelID: "claude-opus-4-7",
desired: {},
})
+3 -3
View File
@@ -23,9 +23,9 @@ function applyGatewayTransforms(model: string): string {
}
export function transformModelForProvider(provider: string, model: string): string {
// Vercel AI Gateway expects <sub-provider>/<model> (e.g. anthropic/claude-opus-4.6).
// Canonical names in model-requirements.ts may be bare (claude-opus-4-6) or
// already prefixed (anthropic/claude-opus-4-6). Both need gateway-specific transforms.
// Vercel AI Gateway expects <sub-provider>/<model> (e.g. anthropic/claude-opus-4.7).
// Canonical names in model-requirements.ts may be bare (claude-opus-4-7) or
// already prefixed (anthropic/claude-opus-4-7). Both need gateway-specific transforms.
if (provider === "vercel") {
// Already prefixed — transform only the model part
const slashIndex = model.indexOf("/")