diff --git a/extensions/anthropic/claude-model-refs.ts b/extensions/anthropic/claude-model-refs.ts index 9946867d53e..0b689954983 100644 --- a/extensions/anthropic/claude-model-refs.ts +++ b/extensions/anthropic/claude-model-refs.ts @@ -4,7 +4,7 @@ import { CLAUDE_CLI_BACKEND_ID, CLAUDE_CLI_MODEL_ALIASES } from "./cli-constants const DEFAULT_CLAUDE_MODEL_BY_FAMILY: Record = { opus: "claude-opus-4-7", sonnet: "claude-sonnet-4-6", - haiku: "claude-sonnet-4-6", + haiku: "claude-haiku-4-5", }; export type ClaudeCliAnthropicModelRefs = { @@ -117,6 +117,10 @@ function upgradeOldClaudeModelId(normalized: string): string | null { if (normalized.startsWith("claude-sonnet-4-6") || normalized.startsWith("claude-sonnet-4.6")) { return null; } + // claude-haiku-4-5 is a current production model and must not be migrated. + if (normalized.startsWith("claude-haiku-4-5") || normalized.startsWith("claude-haiku-4.5")) { + return null; + } if ( normalized === "claude-opus-4" || hasAnyRetiredVersionPrefix(normalized, [ @@ -140,8 +144,6 @@ function upgradeOldClaudeModelId(normalized: string): string | null { "claude-sonnet-4.1", "claude-sonnet-4-0", "claude-sonnet-4.0", - "claude-haiku-4-5", - "claude-haiku-4.5", ]) || /^claude-sonnet-4-20\d{6}/.test(normalized) ) { @@ -172,7 +174,6 @@ function upgradeOldClaudeModelId(normalized: string): string | null { normalized === "sonnet-3.7" || normalized === "sonnet-3.5" || normalized === "sonnet-3" || - normalized === "haiku-4.5" || normalized === "haiku-3.5" || normalized === "haiku-3" ) { diff --git a/extensions/anthropic/cli-migration.test.ts b/extensions/anthropic/cli-migration.test.ts index 857c4fcb734..dad8b369153 100644 --- a/extensions/anthropic/cli-migration.test.ts +++ b/extensions/anthropic/cli-migration.test.ts @@ -55,6 +55,28 @@ describe("anthropic Claude model refs", () => { expect(resolveKnownAnthropicModelRef("anthropic/claude-sonnet-4-7")).toBe( "anthropic/claude-sonnet-4-7", ); + expect(resolveKnownAnthropicModelRef("anthropic/claude-haiku-4-5")).toBe( + "anthropic/claude-haiku-4-5", + ); + }); + + it("preserves the current claude-haiku-4-5 model and its bare alias", () => { + // claude-haiku-4-5 is a current production model (not retired), so neither + // its full ref, its dotted variant, nor the bare "haiku" family alias must + // be rewritten to sonnet. + expect(resolveKnownAnthropicModelRef("anthropic/claude-haiku-4-5")).toBe( + "anthropic/claude-haiku-4-5", + ); + expect(resolveKnownAnthropicModelRef("anthropic/claude-haiku-4.5")).toBe( + "anthropic/claude-haiku-4.5", + ); + expect(resolveKnownAnthropicModelRef("anthropic/claude-haiku-4-5@anthropic:work")).toBe( + "anthropic/claude-haiku-4-5@anthropic:work", + ); + // Genuinely retired Claude 3 Haiku still upgrades to the current sonnet. + expect(resolveKnownAnthropicModelRef("anthropic/claude-3-5-haiku-20241022")).toBe( + "anthropic/claude-sonnet-4-6", + ); }); }); diff --git a/src/commands/doctor/shared/legacy-config-migrate.test.ts b/src/commands/doctor/shared/legacy-config-migrate.test.ts index 172ba70c6da..c58903fe91e 100644 --- a/src/commands/doctor/shared/legacy-config-migrate.test.ts +++ b/src/commands/doctor/shared/legacy-config-migrate.test.ts @@ -1707,7 +1707,7 @@ describe("legacy model compat migrate", () => { }, }); - expect(res.config?.agents?.defaults?.imageModel).toBe("anthropic/claude-sonnet-4-6"); + expect(res.config?.agents?.defaults?.imageModel).toBe("anthropic/claude-haiku-4-5"); expect(res.config?.agents?.defaults?.imageGenerationModel).toEqual({ primary: "github-copilot/claude-sonnet-4.6", fallbacks: ["github-copilot/gpt-5.4-mini"], @@ -1751,6 +1751,7 @@ describe("legacy model compat migrate", () => { }); expect(res.config?.agents?.defaults?.workspace).toBe("/tmp/claude-3-sonnet"); expect(res.config?.agents?.defaults?.models).toEqual({ + "anthropic/claude-haiku-4-5": { alias: "haiku" }, "anthropic/claude-sonnet-4-6": { alias: "current-sonnet" }, "github-copilot/claude-opus-4.7": { alias: "copilot-opus" }, "openai/gpt-5.5-pro": { alias: "old-pro" }, @@ -1770,10 +1771,9 @@ describe("legacy model compat migrate", () => { subagent?: { allowedModels?: string[] }; } )?.subagent?.allowedModels, - ).toEqual(["anthropic/claude-sonnet-4-6", "*"]); + ).toEqual(["anthropic/claude-haiku-4-5", "*"]); expect(res.config?.channels?.modelByChannel?.telegram?.["*"]).toBe("anthropic/claude-opus-4-7"); expectMigrationChangesToIncludeFragments(res.changes, [ - 'config.agents.defaults.imageModel from "anthropic/claude-haiku-4-5" to "anthropic/claude-sonnet-4-6"', 'config.agents.defaults.imageGenerationModel.primary from "github-copilot/claude-sonnet-4" to "github-copilot/claude-sonnet-4.6"', 'config.agents.defaults.imageGenerationModel.fallbacks.0 from "github-copilot/grok-code-fast-1" to "github-copilot/gpt-5.4-mini"', 'config.agents.defaults.musicGenerationModel from "vercel-ai-gateway/anthropic/claude-opus-4-5" to "vercel-ai-gateway/anthropic/claude-opus-4-6"', @@ -1800,7 +1800,6 @@ describe("legacy model compat migrate", () => { 'config.agents.defaults.models key from "openai/gpt-5.2-pro" to "openai/gpt-5.5-pro"', 'config.agents.defaults.models key from "github-copilot/gpt-5-mini" to "github-copilot/gpt-5.4-mini"', 'config.plugins.entries.lossless-claw.config.summaryModel from "anthropic/claude-3-5-sonnet" to "anthropic/claude-sonnet-4-6"', - 'config.plugins.entries.lossless-claw.subagent.allowedModels.0 from "anthropic/claude-haiku-4-5" to "anthropic/claude-sonnet-4-6"', 'config.channels.modelByChannel.telegram.* from "anthropic/claude-opus-4-5" to "anthropic/claude-opus-4-7"', ]); }); diff --git a/src/commands/doctor/shared/legacy-config-migrations.runtime.models.ts b/src/commands/doctor/shared/legacy-config-migrations.runtime.models.ts index 91a55962f4a..ed51e54c0b4 100644 --- a/src/commands/doctor/shared/legacy-config-migrations.runtime.models.ts +++ b/src/commands/doctor/shared/legacy-config-migrations.runtime.models.ts @@ -635,6 +635,13 @@ function upgradeOldClaudeToken( ) { return null; } + // claude-haiku-4-5 is a current production model and must not be migrated. + if ( + normalized.startsWith("claude-haiku-4-5") || + normalized.startsWith("claude-haiku-4.5") + ) { + return null; + } if ( normalized === "claude-opus-4" || hasAnyRetiredVersionPrefix(normalized, [ @@ -658,8 +665,6 @@ function upgradeOldClaudeToken( "claude-sonnet-4.1", "claude-sonnet-4-0", "claude-sonnet-4.0", - "claude-haiku-4-5", - "claude-haiku-4.5", ]) || /^claude-sonnet-4-20\d{6}/.test(normalized) ) { @@ -714,7 +719,6 @@ function upgradeOldClaudeToken( normalized === "sonnet-3.7" || normalized === "sonnet-3.5" || normalized === "sonnet-3" || - normalized === "haiku-4.5" || normalized === "haiku-3.5" || normalized === "haiku-3" ) {