From b6530beb05da7441f1cbd832d470feb8a06dd4ee Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 23 May 2026 16:46:36 +0100 Subject: [PATCH] fix: prune retired model catalog entries --- CHANGELOG.md | 1 + docs/help/faq-models.md | 1 - docs/providers/venice.md | 5 +- extensions/anthropic/claude-model-refs.ts | 129 ++++++- extensions/anthropic/cli-catalog.ts | 3 - extensions/anthropic/cli-constants.ts | 15 - extensions/anthropic/cli-migration.test.ts | 33 +- extensions/anthropic/config-defaults.ts | 2 +- extensions/anthropic/index.test.ts | 46 +-- extensions/anthropic/openclaw.plugin.json | 25 +- extensions/anthropic/register.runtime.ts | 22 +- extensions/anthropic/stream-wrappers.test.ts | 17 - extensions/copilot-proxy/index.ts | 6 +- extensions/github-copilot/models-defaults.ts | 5 - extensions/github-copilot/models.test.ts | 8 +- .../github-copilot/openclaw.plugin.json | 44 --- extensions/venice/openclaw.plugin.json | 33 -- .../vercel-ai-gateway/openclaw.plugin.json | 4 +- .../shared/legacy-config-migrate.test.ts | 136 ++++++- ...legacy-config-migrations.runtime.models.ts | 365 ++++++++++++++++++ src/config/config-misc.test.ts | 18 + 21 files changed, 693 insertions(+), 225 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ede47084ead..2515fd9040f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Models: prune retired GitHub Copilot model rows and old Claude catalog entries below 4.6, with doctor migration to upgrade existing configs to current Claude/Copilot refs. - Doctor/update: recognize junction-backed source checkouts as git installs by comparing canonical paths before showing package-manager update guidance. Fixes #82215. Thanks @igormf. - CLI/skills: show an all-ready note with next-step commands when skill setup has no missing dependencies to install. (#85032) Thanks @aniruddhaadak80. - Microsoft Foundry: route DeepSeek V4 Pro and Flash models through the Foundry Responses API while keeping older DeepSeek models on their existing path. (#85549) Thanks @roslinmahmud. diff --git a/docs/help/faq-models.md b/docs/help/faq-models.md index 9c9b65ca4ba..7522947f876 100644 --- a/docs/help/faq-models.md +++ b/docs/help/faq-models.md @@ -306,7 +306,6 @@ troubleshooting, see the main [FAQ](/help/faq). models: { "anthropic/claude-opus-4-6": { alias: "opus" }, "anthropic/claude-sonnet-4-6": { alias: "sonnet" }, - "anthropic/claude-haiku-4-5": { alias: "haiku" }, }, }, }, diff --git a/docs/providers/venice.md b/docs/providers/venice.md index 59c8c717f99..92e480d09f6 100644 --- a/docs/providers/venice.md +++ b/docs/providers/venice.md @@ -166,13 +166,11 @@ DeepSeek provider's thinking controls. | `minimax-m25` | MiniMax M2.5 | 198k | Reasoning | - + | Model ID | Name | Context | Features | | ------------------------------- | ------------------------------ | ------- | ------------------------- | | `claude-opus-4-6` | Claude Opus 4.6 (via Venice) | 1M | Reasoning, vision | - | `claude-opus-4-5` | Claude Opus 4.5 (via Venice) | 198k | Reasoning, vision | | `claude-sonnet-4-6` | Claude Sonnet 4.6 (via Venice) | 1M | Reasoning, vision | - | `claude-sonnet-4-5` | Claude Sonnet 4.5 (via Venice) | 198k | Reasoning, vision | | `openai-gpt-54` | GPT-5.4 (via Venice) | 1M | Reasoning, vision | | `openai-gpt-53-codex` | GPT-5.3 Codex (via Venice) | 400k | Reasoning, vision, coding | | `openai-gpt-52` | GPT-5.2 (via Venice) | 256k | Reasoning | @@ -183,7 +181,6 @@ DeepSeek provider's thinking controls. | `gemini-3-pro-preview` | Gemini 3 Pro (via Venice) | 198k | Reasoning, vision | | `gemini-3-flash-preview` | Gemini 3 Flash (via Venice) | 256k | Reasoning, vision | | `grok-41-fast` | Grok 4.1 Fast (via Venice) | 1M | Reasoning, vision | - | `grok-code-fast-1` | Grok Code Fast 1 (via Venice) | 256k | Reasoning, coding | diff --git a/extensions/anthropic/claude-model-refs.ts b/extensions/anthropic/claude-model-refs.ts index c7f75cfa7ad..9946867d53e 100644 --- a/extensions/anthropic/claude-model-refs.ts +++ b/extensions/anthropic/claude-model-refs.ts @@ -4,6 +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", }; export type ClaudeCliAnthropicModelRefs = { @@ -12,6 +13,47 @@ export type ClaudeCliAnthropicModelRefs = { rewriteRef?: string; }; +function splitTrailingModelAuthProfile(raw: string): { model: string; profile?: string } { + const trimmed = raw.trim(); + if (!trimmed) { + return { model: "" }; + } + const lastSlash = trimmed.lastIndexOf("/"); + let delimiter = trimmed.indexOf("@", lastSlash + 1); + if (delimiter <= 0) { + return { model: trimmed }; + } + if (/^\d{8}(?:@|$)/.test(trimmed.slice(delimiter + 1))) { + const nextDelimiter = trimmed.indexOf("@", delimiter + 9); + if (nextDelimiter < 0) { + return { model: trimmed }; + } + delimiter = nextDelimiter; + } + const model = trimmed.slice(0, delimiter).trim(); + const profile = trimmed.slice(delimiter + 1).trim(); + return model && profile ? { model, profile } : { model: trimmed }; +} + +function attachModelAuthProfile(model: string, profile?: string): string { + return profile ? `${model}@${profile}` : model; +} + +function hasRetiredVersionPrefix(normalized: string, prefix: string): boolean { + if (normalized === prefix) { + return true; + } + if (!normalized.startsWith(prefix)) { + return false; + } + const next = normalized[prefix.length]; + return next === "-" || next === "." || next === ":" || next === "@"; +} + +function hasAnyRetiredVersionPrefix(normalized: string, prefixes: readonly string[]): boolean { + return prefixes.some((prefix) => hasRetiredVersionPrefix(normalized, prefix)); +} + function parseProviderModelRef( raw: string, defaultProvider: string, @@ -37,17 +79,22 @@ function parseProviderModelRef( } function canonicalizeKnownClaudeCliModelId(modelId: string): string | null { - const trimmed = modelId.trim(); + const split = splitTrailingModelAuthProfile(modelId); + const trimmed = split.model.trim(); const normalized = normalizeLowercaseStringOrEmpty(trimmed); if (!normalized) { return null; } + const upgraded = upgradeOldClaudeModelId(normalized); + if (upgraded) { + return attachModelAuthProfile(upgraded, split.profile); + } if (normalized.startsWith("claude-")) { - return trimmed; + return attachModelAuthProfile(trimmed, split.profile); } const defaultModel = DEFAULT_CLAUDE_MODEL_BY_FAMILY[normalized]; if (defaultModel) { - return defaultModel; + return attachModelAuthProfile(defaultModel, split.profile); } const family = CLAUDE_CLI_MODEL_ALIASES[normalized]; if (!family) { @@ -57,7 +104,81 @@ function canonicalizeKnownClaudeCliModelId(modelId: string): string | null { if (!version || version === normalized) { return null; } - return `claude-${family}-${version.replaceAll(".", "-")}`; + return attachModelAuthProfile(`claude-${family}-${version.replaceAll(".", "-")}`, split.profile); +} + +function upgradeOldClaudeModelId(normalized: string): string | null { + if (normalized.startsWith("claude-opus-4-7") || normalized.startsWith("claude-opus-4.7")) { + return null; + } + if (normalized.startsWith("claude-opus-4-6") || normalized.startsWith("claude-opus-4.6")) { + return null; + } + if (normalized.startsWith("claude-sonnet-4-6") || normalized.startsWith("claude-sonnet-4.6")) { + return null; + } + if ( + normalized === "claude-opus-4" || + hasAnyRetiredVersionPrefix(normalized, [ + "claude-opus-4-5", + "claude-opus-4.5", + "claude-opus-4-1", + "claude-opus-4.1", + "claude-opus-4-0", + "claude-opus-4.0", + ]) || + /^claude-opus-4-20\d{6}/.test(normalized) + ) { + return "claude-opus-4-7"; + } + if ( + normalized === "claude-sonnet-4" || + hasAnyRetiredVersionPrefix(normalized, [ + "claude-sonnet-4-5", + "claude-sonnet-4.5", + "claude-sonnet-4-1", + "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) + ) { + return "claude-sonnet-4-6"; + } + if (normalized.startsWith("claude-3") && normalized.includes("opus")) { + return "claude-opus-4-7"; + } + if ( + normalized.startsWith("claude-3") && + (normalized.includes("sonnet") || normalized.includes("haiku")) + ) { + return "claude-sonnet-4-6"; + } + if ( + normalized === "opus-4.5" || + normalized === "opus-4.1" || + normalized === "opus-4" || + normalized === "opus-3" + ) { + return "claude-opus-4-7"; + } + if ( + normalized === "sonnet-4.5" || + normalized === "sonnet-4.1" || + normalized === "sonnet-4.0" || + normalized === "sonnet-4" || + normalized === "sonnet-3.7" || + normalized === "sonnet-3.5" || + normalized === "sonnet-3" || + normalized === "haiku-4.5" || + normalized === "haiku-3.5" || + normalized === "haiku-3" + ) { + return "claude-sonnet-4-6"; + } + return null; } export function resolveClaudeCliAnthropicModelRefs( diff --git a/extensions/anthropic/cli-catalog.ts b/extensions/anthropic/cli-catalog.ts index a3f21a5085e..b3ef4099193 100644 --- a/extensions/anthropic/cli-catalog.ts +++ b/extensions/anthropic/cli-catalog.ts @@ -7,10 +7,7 @@ const CLAUDE_CLI_DEFAULT_CONTEXT_WINDOW = 200_000; const CLAUDE_CLI_MODEL_LABELS: Record = { "claude-opus-4-7": "Claude Opus 4.7 (Claude CLI)", "claude-opus-4-6": "Claude Opus 4.6 (Claude CLI)", - "claude-opus-4-5": "Claude Opus 4.5 (Claude CLI)", "claude-sonnet-4-6": "Claude Sonnet 4.6 (Claude CLI)", - "claude-sonnet-4-5": "Claude Sonnet 4.5 (Claude CLI)", - "claude-haiku-4-5": "Claude Haiku 4.5 (Claude CLI)", }; function extractClaudeCliModelIds(): string[] { diff --git a/extensions/anthropic/cli-constants.ts b/extensions/anthropic/cli-constants.ts index e8af5956b1f..ccf81e0c64f 100644 --- a/extensions/anthropic/cli-constants.ts +++ b/extensions/anthropic/cli-constants.ts @@ -4,33 +4,18 @@ export const CLAUDE_CLI_DEFAULT_ALLOWLIST_REFS = [ CLAUDE_CLI_DEFAULT_MODEL_REF, `${CLAUDE_CLI_BACKEND_ID}/claude-sonnet-4-6`, `${CLAUDE_CLI_BACKEND_ID}/claude-opus-4-6`, - `${CLAUDE_CLI_BACKEND_ID}/claude-opus-4-5`, - `${CLAUDE_CLI_BACKEND_ID}/claude-sonnet-4-5`, - `${CLAUDE_CLI_BACKEND_ID}/claude-haiku-4-5`, ] as const; export const CLAUDE_CLI_MODEL_ALIASES: Record = { opus: "opus", "opus-4.7": "opus", "opus-4.6": "opus", - "opus-4.5": "opus", - "opus-4": "opus", "claude-opus-4-7": "opus", "claude-opus-4-6": "opus", - "claude-opus-4-5": "opus", - "claude-opus-4": "opus", sonnet: "sonnet", "sonnet-4.6": "sonnet", - "sonnet-4.5": "sonnet", - "sonnet-4.1": "sonnet", - "sonnet-4.0": "sonnet", "claude-sonnet-4-6": "sonnet", - "claude-sonnet-4-5": "sonnet", - "claude-sonnet-4-1": "sonnet", - "claude-sonnet-4-0": "sonnet", haiku: "haiku", - "haiku-3.5": "haiku", - "claude-haiku-3-5": "haiku", }; export const CLAUDE_CLI_SESSION_ID_FIELDS = [ diff --git a/extensions/anthropic/cli-migration.test.ts b/extensions/anthropic/cli-migration.test.ts index d9ee5d16102..4e918d892ea 100644 --- a/extensions/anthropic/cli-migration.test.ts +++ b/extensions/anthropic/cli-migration.test.ts @@ -20,6 +20,7 @@ vi.mock("./cli-auth-seam.js", async (importActual) => { }); const { buildAnthropicCliMigrationResult, hasClaudeCliAuth } = await import("./cli-migration.js"); +const { resolveKnownAnthropicModelRef } = await import("./claude-model-refs.js"); const { createTestWizardPrompter, registerSingleProviderPlugin } = await import("openclaw/plugin-sdk/plugin-test-runtime"); const { default: anthropicPlugin } = await import("./index.js"); @@ -34,6 +35,29 @@ afterAll(() => { vi.resetModules(); }); +describe("anthropic Claude model refs", () => { + it("upgrades retired refs without rewriting future canonical refs", () => { + expect(resolveKnownAnthropicModelRef("anthropic/claude-opus-4-5")).toBe( + "anthropic/claude-opus-4-7", + ); + expect(resolveKnownAnthropicModelRef("anthropic/claude-opus-4-5@anthropic:work")).toBe( + "anthropic/claude-opus-4-7@anthropic:work", + ); + expect(resolveKnownAnthropicModelRef("anthropic/claude-sonnet-4-20250514")).toBe( + "anthropic/claude-sonnet-4-6", + ); + expect(resolveKnownAnthropicModelRef("anthropic/claude-opus-5-0")).toBe( + "anthropic/claude-opus-5-0", + ); + expect(resolveKnownAnthropicModelRef("anthropic/claude-opus-4-10")).toBe( + "anthropic/claude-opus-4-10", + ); + expect(resolveKnownAnthropicModelRef("anthropic/claude-sonnet-4-7")).toBe( + "anthropic/claude-sonnet-4-7", + ); + }); +}); + async function resolveAnthropicCliAuthMethod() { const provider = await registerSingleProviderPlugin(anthropicPlugin); const method = provider.auth.find((entry) => entry.id === "cli"); @@ -142,9 +166,6 @@ describe("anthropic cli migration", () => { alias: "Opus", agentRuntime: { id: "claude-cli" }, }, - "anthropic/claude-opus-4-5": { agentRuntime: { id: "claude-cli" } }, - "anthropic/claude-sonnet-4-5": { agentRuntime: { id: "claude-cli" } }, - "anthropic/claude-haiku-4-5": { agentRuntime: { id: "claude-cli" } }, "openai/gpt-5.2": {}, }, }, @@ -235,9 +256,6 @@ describe("anthropic cli migration", () => { "anthropic/claude-opus-4-7": { agentRuntime: { id: "claude-cli" } }, "anthropic/claude-sonnet-4-6": { agentRuntime: { id: "claude-cli" } }, "anthropic/claude-opus-4-6": { agentRuntime: { id: "claude-cli" } }, - "anthropic/claude-opus-4-5": { agentRuntime: { id: "claude-cli" } }, - "anthropic/claude-sonnet-4-5": { agentRuntime: { id: "claude-cli" } }, - "anthropic/claude-haiku-4-5": { agentRuntime: { id: "claude-cli" } }, }, }, }, @@ -282,9 +300,6 @@ describe("anthropic cli migration", () => { "anthropic/claude-opus-4-7": { agentRuntime: { id: "claude-cli" } }, "anthropic/claude-sonnet-4-6": { agentRuntime: { id: "claude-cli" } }, "anthropic/claude-opus-4-6": { agentRuntime: { id: "claude-cli" } }, - "anthropic/claude-opus-4-5": { agentRuntime: { id: "claude-cli" } }, - "anthropic/claude-sonnet-4-5": { agentRuntime: { id: "claude-cli" } }, - "anthropic/claude-haiku-4-5": { agentRuntime: { id: "claude-cli" } }, }, }, }, diff --git a/extensions/anthropic/config-defaults.ts b/extensions/anthropic/config-defaults.ts index f25b20af68b..c62aef547a5 100644 --- a/extensions/anthropic/config-defaults.ts +++ b/extensions/anthropic/config-defaults.ts @@ -6,7 +6,7 @@ import { import { CLAUDE_CLI_BACKEND_ID, CLAUDE_CLI_DEFAULT_ALLOWLIST_REFS } from "./cli-constants.js"; const ANTHROPIC_PROVIDER_API = "anthropic-messages"; -const ANTHROPIC_API_KEY_DEFAULT_ALLOWLIST_REFS = ["anthropic/claude-haiku-4-5"] as const; +const ANTHROPIC_API_KEY_DEFAULT_ALLOWLIST_REFS = ["anthropic/claude-sonnet-4-6"] as const; function normalizeLowercaseStringOrEmpty(value: unknown): string { return typeof value === "string" ? value.trim().toLowerCase() : ""; diff --git a/extensions/anthropic/index.test.ts b/extensions/anthropic/index.test.ts index 00a406da83d..06d6d26a169 100644 --- a/extensions/anthropic/index.test.ts +++ b/extensions/anthropic/index.test.ts @@ -182,7 +182,7 @@ describe("anthropic provider replay hooks", () => { }, agents: { defaults: { - model: { primary: "anthropic/claude-opus-4-5" }, + model: { primary: "anthropic/claude-opus-4-6" }, }, }, }, @@ -196,11 +196,11 @@ describe("anthropic provider replay hooks", () => { every: "30m", }); expect( - next?.agents?.defaults?.models?.["anthropic/claude-opus-4-5"]?.params?.cacheRetention, + next?.agents?.defaults?.models?.["anthropic/claude-opus-4-6"]?.params?.cacheRetention, ).toBe("short"); }); - it("backfills Haiku into API-key agent model allowlists", async () => { + it("backfills Sonnet into API-key agent model allowlists", async () => { const provider = await registerSingleProviderPlugin(anthropicPlugin); const next = provider.applyConfigDefaults?.({ @@ -214,9 +214,9 @@ describe("anthropic provider replay hooks", () => { }, agents: { defaults: { - model: { primary: "anthropic/claude-sonnet-4-6" }, + model: { primary: "anthropic/claude-opus-4-6" }, models: { - "anthropic/claude-sonnet-4-6": {}, + "anthropic/claude-opus-4-6": {}, }, }, }, @@ -224,8 +224,8 @@ describe("anthropic provider replay hooks", () => { } as never); const models = next?.agents?.defaults?.models; + expectModelParams(models, "anthropic/claude-opus-4-6", { cacheRetention: "short" }); expectModelParams(models, "anthropic/claude-sonnet-4-6", { cacheRetention: "short" }); - expectModelParams(models, "anthropic/claude-haiku-4-5", { cacheRetention: "short" }); }); it("backfills Claude CLI allowlist defaults through plugin hooks for older configs", async () => { @@ -260,9 +260,6 @@ describe("anthropic provider replay hooks", () => { "anthropic/claude-opus-4-7", "anthropic/claude-sonnet-4-6", "anthropic/claude-opus-4-6", - "anthropic/claude-opus-4-5", - "anthropic/claude-sonnet-4-5", - "anthropic/claude-haiku-4-5", ]) { expect(models[modelId]).toEqual({ agentRuntime: { id: "claude-cli" } }); } @@ -524,15 +521,15 @@ describe("anthropic provider replay hooks", () => { expect(resolved).toBeUndefined(); }); - it("normalizes stale text-only Claude vision rows to image-capable", async () => { + it("normalizes stale text-only modern Claude vision rows to image-capable", async () => { const provider = await registerSingleProviderPlugin(anthropicPlugin); const normalized = provider.normalizeResolvedModel?.({ provider: "anthropic", - modelId: "claude-sonnet-4-5", + modelId: "claude-sonnet-4-6", model: { - id: "claude-sonnet-4-5", - name: "Claude Sonnet 4.5", + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", provider: "anthropic", api: "anthropic-messages", reasoning: true, @@ -580,29 +577,6 @@ describe("anthropic provider replay hooks", () => { } }); - it("does not normalize legacy Claude 4.5 models to 1M context", async () => { - const provider = await registerSingleProviderPlugin(anthropicPlugin); - - const normalized = provider.normalizeResolvedModel?.({ - provider: "anthropic", - modelId: "claude-sonnet-4-5", - model: { - id: "claude-sonnet-4-5", - name: "Claude Sonnet 4.5", - provider: "anthropic", - api: "anthropic-messages", - reasoning: true, - input: ["text", "image"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 200_000, - contextTokens: 200_000, - maxTokens: 32_000, - }, - } as never); - - expect(normalized).toBeUndefined(); - }); - it("resolves claude-cli synthetic oauth auth", async () => { readClaudeCliCredentialsForRuntimeMock.mockReset(); readClaudeCliCredentialsForRuntimeMock.mockReturnValue({ diff --git a/extensions/anthropic/openclaw.plugin.json b/extensions/anthropic/openclaw.plugin.json index 6ffd034f3a0..84313e45f7c 100644 --- a/extensions/anthropic/openclaw.plugin.json +++ b/extensions/anthropic/openclaw.plugin.json @@ -30,27 +30,6 @@ "reasoning": true, "input": ["text", "image"], "contextWindow": 200000 - }, - { - "id": "claude-opus-4-5", - "name": "Claude Opus 4.5 (Claude CLI)", - "reasoning": true, - "input": ["text", "image"], - "contextWindow": 200000 - }, - { - "id": "claude-sonnet-4-5", - "name": "Claude Sonnet 4.5 (Claude CLI)", - "reasoning": true, - "input": ["text", "image"], - "contextWindow": 200000 - }, - { - "id": "claude-haiku-4-5", - "name": "Claude Haiku 4.5 (Claude CLI)", - "reasoning": true, - "input": ["text", "image"], - "contextWindow": 200000 } ] } @@ -67,9 +46,7 @@ "anthropic": { "aliases": { "opus-4.6": "claude-opus-4-6", - "opus-4.5": "claude-opus-4-5", - "sonnet-4.6": "claude-sonnet-4-6", - "sonnet-4.5": "claude-sonnet-4-5" + "sonnet-4.6": "claude-sonnet-4-6" } } } diff --git a/extensions/anthropic/register.runtime.ts b/extensions/anthropic/register.runtime.ts index 15f1d5e9703..9868efbd3e1 100644 --- a/extensions/anthropic/register.runtime.ts +++ b/extensions/anthropic/register.runtime.ts @@ -54,13 +54,9 @@ const ANTHROPIC_OPUS_46_DOT_MODEL_ID = "claude-opus-4.6"; const ANTHROPIC_OPUS_47_TEMPLATE_MODEL_IDS = [ ANTHROPIC_OPUS_46_MODEL_ID, ANTHROPIC_OPUS_46_DOT_MODEL_ID, - "claude-opus-4-5", - "claude-opus-4.5", ] as const; -const ANTHROPIC_OPUS_TEMPLATE_MODEL_IDS = ["claude-opus-4-5", "claude-opus-4.5"] as const; const ANTHROPIC_SONNET_46_MODEL_ID = "claude-sonnet-4-6"; const ANTHROPIC_SONNET_46_DOT_MODEL_ID = "claude-sonnet-4.6"; -const ANTHROPIC_SONNET_TEMPLATE_MODEL_IDS = ["claude-sonnet-4-5", "claude-sonnet-4.5"] as const; const ANTHROPIC_GA_1M_MODEL_PREFIXES = [ ANTHROPIC_OPUS_46_MODEL_ID, ANTHROPIC_OPUS_46_DOT_MODEL_ID, @@ -76,12 +72,6 @@ const ANTHROPIC_MODERN_MODEL_PREFIXES = [ "claude-opus-4.6", "claude-sonnet-4-6", "claude-sonnet-4.6", - "claude-opus-4-5", - "claude-opus-4.5", - "claude-sonnet-4-5", - "claude-sonnet-4.5", - "claude-haiku-4-5", - "claude-haiku-4.5", ] as const; const ANTHROPIC_SETUP_TOKEN_NOTE_LINES = [ "Anthropic setup-token auth is supported in OpenClaw.", @@ -287,17 +277,17 @@ function resolveAnthropicForwardCompatModel( ctx, dashModelId: ANTHROPIC_OPUS_46_MODEL_ID, dotModelId: ANTHROPIC_OPUS_46_DOT_MODEL_ID, - dashTemplateId: "claude-opus-4-5", - dotTemplateId: "claude-opus-4.5", - fallbackTemplateIds: ANTHROPIC_OPUS_TEMPLATE_MODEL_IDS, + dashTemplateId: ANTHROPIC_OPUS_47_MODEL_ID, + dotTemplateId: ANTHROPIC_OPUS_46_MODEL_ID, + fallbackTemplateIds: ANTHROPIC_OPUS_47_TEMPLATE_MODEL_IDS, }) ?? resolveAnthropic46ForwardCompatModel({ ctx, dashModelId: ANTHROPIC_SONNET_46_MODEL_ID, dotModelId: ANTHROPIC_SONNET_46_DOT_MODEL_ID, - dashTemplateId: "claude-sonnet-4-5", - dotTemplateId: "claude-sonnet-4.5", - fallbackTemplateIds: ANTHROPIC_SONNET_TEMPLATE_MODEL_IDS, + dashTemplateId: ANTHROPIC_SONNET_46_MODEL_ID, + dotTemplateId: ANTHROPIC_SONNET_46_MODEL_ID, + fallbackTemplateIds: [ANTHROPIC_SONNET_46_MODEL_ID, ANTHROPIC_SONNET_46_DOT_MODEL_ID], }) ); } diff --git a/extensions/anthropic/stream-wrappers.test.ts b/extensions/anthropic/stream-wrappers.test.ts index 3f4ebc4ce91..10c2d1a0280 100644 --- a/extensions/anthropic/stream-wrappers.test.ts +++ b/extensions/anthropic/stream-wrappers.test.ts @@ -154,23 +154,6 @@ describe("anthropic stream wrappers", () => { expect(captured.headers?.["anthropic-beta"]).not.toContain(CONTEXT_1M_BETA); }); - it("does not add beta headers for context1m-only legacy Sonnet 4.5 configs", () => { - const captured: { headers?: Record } = {}; - const wrapped = wrapAnthropicProviderStream({ - streamFn: createPayloadCapturingBaseStream(captured), - modelId: "claude-sonnet-4-5", - extraParams: { context1m: true }, - } as never); - - void wrapped?.( - { provider: "anthropic", api: "anthropic-messages", id: "claude-sonnet-4-5" } as never, - {} as never, - { apiKey: "sk-ant-api-123" } as never, - ); - - expect(captured.headers?.["anthropic-beta"]).toBeUndefined(); - }); - it("preserves OAuth-required betas when legacy context-1m is the only configured beta", () => { const captured: { headers?: Record } = {}; const wrapped = wrapAnthropicProviderStream({ diff --git a/extensions/copilot-proxy/index.ts b/extensions/copilot-proxy/index.ts index b14847df6a8..42f96501200 100644 --- a/extensions/copilot-proxy/index.ts +++ b/extensions/copilot-proxy/index.ts @@ -16,12 +16,10 @@ const DEFAULT_MODEL_IDS = [ "gpt-5.1-codex-max", "gpt-5-mini", "claude-opus-4.6", - "claude-opus-4.5", - "claude-sonnet-4.5", - "claude-haiku-4.5", + "claude-opus-4.7", + "claude-sonnet-4.6", "gemini-3-pro", "gemini-3-flash", - "grok-code-fast-1", ] as const; function normalizeBaseUrl(value: string): string { diff --git a/extensions/github-copilot/models-defaults.ts b/extensions/github-copilot/models-defaults.ts index faed5e68b4b..6adeecab90e 100644 --- a/extensions/github-copilot/models-defaults.ts +++ b/extensions/github-copilot/models-defaults.ts @@ -12,13 +12,9 @@ const DEFAULT_MAX_TOKENS = 8192; // We keep this list intentionally broad; if a model isn't available Copilot will // return an error and users can remove it from their config. const DEFAULT_MODEL_IDS = [ - "claude-haiku-4.5", - "claude-opus-4.5", "claude-opus-4.6", "claude-opus-4.7", - "claude-sonnet-4", "claude-sonnet-4.6", - "claude-sonnet-4.5", "gemini-2.5-pro", "gemini-3-flash", "gemini-3.1-pro", @@ -31,7 +27,6 @@ const DEFAULT_MODEL_IDS = [ "gpt-5.4-mini", "gpt-5.4-nano", "gpt-5.5", - "grok-code-fast-1", "raptor-mini", "goldeneye", ] as const; diff --git a/extensions/github-copilot/models.test.ts b/extensions/github-copilot/models.test.ts index 93edc1a6a3f..afd2298a010 100644 --- a/extensions/github-copilot/models.test.ts +++ b/extensions/github-copilot/models.test.ts @@ -74,8 +74,12 @@ describe("github-copilot model defaults", () => { expect(getDefaultCopilotModelIds()).toContain("claude-sonnet-4.6"); }); - it("includes claude-sonnet-4.5", () => { - expect(getDefaultCopilotModelIds()).toContain("claude-sonnet-4.5"); + it("excludes retired and old Claude fallback rows", () => { + expect(getDefaultCopilotModelIds()).not.toContain("claude-sonnet-4"); + expect(getDefaultCopilotModelIds()).not.toContain("claude-sonnet-4.5"); + expect(getDefaultCopilotModelIds()).not.toContain("claude-opus-4.5"); + expect(getDefaultCopilotModelIds()).not.toContain("claude-haiku-4.5"); + expect(getDefaultCopilotModelIds()).not.toContain("grok-code-fast-1"); }); it("returns a mutable copy", () => { diff --git a/extensions/github-copilot/openclaw.plugin.json b/extensions/github-copilot/openclaw.plugin.json index eddf819f509..8282f5abf0e 100644 --- a/extensions/github-copilot/openclaw.plugin.json +++ b/extensions/github-copilot/openclaw.plugin.json @@ -24,24 +24,6 @@ "baseUrl": "https://api.individual.githubcopilot.com", "api": "openai-responses", "models": [ - { - "id": "claude-haiku-4.5", - "name": "Claude Haiku 4.5", - "api": "anthropic-messages", - "input": ["text", "image"], - "contextWindow": 128000, - "maxTokens": 8192, - "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 } - }, - { - "id": "claude-opus-4.5", - "name": "Claude Opus 4.5", - "api": "anthropic-messages", - "input": ["text", "image"], - "contextWindow": 128000, - "maxTokens": 8192, - "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 } - }, { "id": "claude-opus-4.6", "name": "Claude Opus 4.6", @@ -60,24 +42,6 @@ "maxTokens": 8192, "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 } }, - { - "id": "claude-sonnet-4", - "name": "Claude Sonnet 4", - "api": "anthropic-messages", - "input": ["text", "image"], - "contextWindow": 128000, - "maxTokens": 8192, - "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 } - }, - { - "id": "claude-sonnet-4.5", - "name": "Claude Sonnet 4.5", - "api": "anthropic-messages", - "input": ["text", "image"], - "contextWindow": 128000, - "maxTokens": 8192, - "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 } - }, { "id": "claude-sonnet-4.6", "name": "Claude Sonnet 4.6", @@ -191,14 +155,6 @@ "maxTokens": 8192, "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 } }, - { - "id": "grok-code-fast-1", - "name": "Grok Code Fast 1", - "input": ["text"], - "contextWindow": 128000, - "maxTokens": 8192, - "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 } - }, { "id": "raptor-mini", "name": "Raptor mini", diff --git a/extensions/venice/openclaw.plugin.json b/extensions/venice/openclaw.plugin.json index 56e6cf5435e..c6367a77f87 100644 --- a/extensions/venice/openclaw.plugin.json +++ b/extensions/venice/openclaw.plugin.json @@ -329,17 +329,6 @@ "supportsUsageInStreaming": false } }, - { - "id": "claude-opus-4-5", - "name": "Claude Opus 4.5 (via Venice)", - "reasoning": true, - "input": ["text", "image"], - "contextWindow": 198000, - "maxTokens": 32768, - "compat": { - "supportsUsageInStreaming": false - } - }, { "id": "claude-opus-4-6", "name": "Claude Opus 4.6 (via Venice)", @@ -351,17 +340,6 @@ "supportsUsageInStreaming": false } }, - { - "id": "claude-sonnet-4-5", - "name": "Claude Sonnet 4.5 (via Venice)", - "reasoning": true, - "input": ["text", "image"], - "contextWindow": 198000, - "maxTokens": 64000, - "compat": { - "supportsUsageInStreaming": false - } - }, { "id": "claude-sonnet-4-6", "name": "Claude Sonnet 4.6 (via Venice)", @@ -482,17 +460,6 @@ "compat": { "supportsUsageInStreaming": false } - }, - { - "id": "grok-code-fast-1", - "name": "Grok Code Fast 1 (via Venice)", - "reasoning": true, - "input": ["text"], - "contextWindow": 256000, - "maxTokens": 10000, - "compat": { - "supportsUsageInStreaming": false - } } ] } diff --git a/extensions/vercel-ai-gateway/openclaw.plugin.json b/extensions/vercel-ai-gateway/openclaw.plugin.json index 99f2930b6d0..65905f098c1 100644 --- a/extensions/vercel-ai-gateway/openclaw.plugin.json +++ b/extensions/vercel-ai-gateway/openclaw.plugin.json @@ -10,9 +10,7 @@ "vercel-ai-gateway": { "aliases": { "opus-4.6": "claude-opus-4-6", - "opus-4.5": "claude-opus-4-5", - "sonnet-4.6": "claude-sonnet-4-6", - "sonnet-4.5": "claude-sonnet-4-5" + "sonnet-4.6": "claude-sonnet-4-6" }, "prefixWhenBareAfterAliasStartsWith": [ { diff --git a/src/commands/doctor/shared/legacy-config-migrate.test.ts b/src/commands/doctor/shared/legacy-config-migrate.test.ts index 757feb0f90c..0f6bca4750f 100644 --- a/src/commands/doctor/shared/legacy-config-migrate.test.ts +++ b/src/commands/doctor/shared/legacy-config-migrate.test.ts @@ -855,9 +855,12 @@ describe("legacy migrate heartbeat config", () => { }, }); - expect(res.changes).toStrictEqual(["Moved heartbeat → agents.defaults.heartbeat."]); + expect(res.changes).toStrictEqual([ + "Moved heartbeat → agents.defaults.heartbeat.", + 'Upgraded config.agents.defaults.heartbeat.model from "anthropic/claude-3-5-haiku-20241022" to "anthropic/claude-sonnet-4-6".', + ]); expect(res.config?.agents?.defaults?.heartbeat).toEqual({ - model: "anthropic/claude-3-5-haiku-20241022", + model: "anthropic/claude-sonnet-4-6", every: "30m", }); expect((res.config as { heartbeat?: unknown } | null)?.heartbeat).toBeUndefined(); @@ -901,11 +904,12 @@ describe("legacy migrate heartbeat config", () => { expect(res.changes).toStrictEqual([ "Merged heartbeat → agents.defaults.heartbeat (filled missing fields from legacy; kept explicit agents.defaults values).", + 'Upgraded config.agents.defaults.heartbeat.model from "anthropic/claude-3-5-haiku-20241022" to "anthropic/claude-sonnet-4-6".', ]); expect(res.config?.agents?.defaults?.heartbeat).toEqual({ every: "1h", target: "telegram", - model: "anthropic/claude-3-5-haiku-20241022", + model: "anthropic/claude-sonnet-4-6", }); expect((res.config as { heartbeat?: unknown } | null)?.heartbeat).toBeUndefined(); }); @@ -957,7 +961,7 @@ describe("legacy migrate heartbeat config", () => { expect(res.config?.agents?.defaults?.heartbeat).toEqual({ every: "1h", target: "telegram", - model: "anthropic/claude-3-5-haiku-20241022", + model: "anthropic/claude-sonnet-4-6", }); expect((res.config as { heartbeat?: unknown } | null)?.heartbeat).toBeUndefined(); }); @@ -1138,6 +1142,130 @@ describe("legacy migrate controlUi.allowedOrigins seed (issue #29385)", () => { }); describe("legacy model compat migrate", () => { + it("upgrades retired Claude and Copilot model refs", () => { + const res = migrateLegacyConfigForTest({ + agents: { + defaults: { + workspace: "/tmp/claude-3-sonnet", + imageModel: "anthropic/claude-haiku-4-5", + imageGenerationModel: { + primary: "github-copilot/claude-sonnet-4", + fallbacks: ["github-copilot/grok-code-fast-1"], + }, + musicGenerationModel: "vercel-ai-gateway/anthropic/claude-opus-4-5", + pdfModel: "anthropic/claude-3-5-sonnet", + videoGenerationModel: "anthropic/claude-opus-4-10", + model: { + primary: "anthropic/claude-opus-4-5@anthropic:work", + fallbacks: [ + "anthropic/claude-sonnet-4-20250514", + "github-copilot/claude-sonnet-4", + "github-copilot/grok-code-fast-1@github:work", + "venice/claude-opus-4-5", + "vercel-ai-gateway/anthropic/claude-opus-4-5", + "anthropic/claude-opus-5-0", + "anthropic/claude-sonnet-4-7", + "anthropic/claude-opus-4-10", + "kilocode/anthropic/claude-sonnet-4", + "amazon-bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0", + "openai/gpt-5.5", + ], + }, + models: { + "anthropic/claude-haiku-4-5": { alias: "haiku" }, + "anthropic/claude-sonnet-4-6": { alias: "current-sonnet" }, + "github-copilot/claude-opus-4.5": { alias: "copilot-opus" }, + "github-copilot/gpt-5-mini": { alias: "mini" }, + }, + }, + }, + plugins: { + entries: { + "lossless-claw": { + config: { + summaryModel: "anthropic/claude-3-5-sonnet", + dataPath: "/tmp/claude-opus-4-5", + }, + subagent: { + allowedModels: ["anthropic/claude-haiku-4-5", "*"], + }, + }, + }, + }, + channels: { + modelByChannel: { + telegram: { + "*": "anthropic/claude-opus-4-5", + }, + }, + }, + }); + + expect(res.config?.agents?.defaults?.imageModel).toBe("anthropic/claude-sonnet-4-6"); + expect(res.config?.agents?.defaults?.imageGenerationModel).toEqual({ + primary: "github-copilot/claude-sonnet-4.6", + fallbacks: ["github-copilot/gpt-5-mini"], + }); + expect(res.config?.agents?.defaults?.musicGenerationModel).toBe( + "vercel-ai-gateway/anthropic/claude-opus-4-6", + ); + expect(res.config?.agents?.defaults?.pdfModel).toBe("anthropic/claude-sonnet-4-6"); + expect(res.config?.agents?.defaults?.videoGenerationModel).toBe("anthropic/claude-opus-4-10"); + expect(res.config?.agents?.defaults?.model).toEqual({ + primary: "anthropic/claude-opus-4-7@anthropic:work", + fallbacks: [ + "anthropic/claude-sonnet-4-6", + "github-copilot/claude-sonnet-4.6", + "github-copilot/gpt-5-mini@github:work", + "venice/claude-opus-4-6", + "vercel-ai-gateway/anthropic/claude-opus-4-6", + "anthropic/claude-opus-5-0", + "anthropic/claude-sonnet-4-7", + "anthropic/claude-opus-4-10", + "kilocode/anthropic/claude-sonnet-4", + "amazon-bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0", + "openai/gpt-5.5", + ], + }); + expect(res.config?.agents?.defaults?.workspace).toBe("/tmp/claude-3-sonnet"); + expect(res.config?.agents?.defaults?.models).toEqual({ + "anthropic/claude-sonnet-4-6": { alias: "current-sonnet" }, + "github-copilot/claude-opus-4.7": { alias: "copilot-opus" }, + "github-copilot/gpt-5-mini": { alias: "mini" }, + }); + expect( + (res.config?.plugins?.entries?.["lossless-claw"] as { config?: { summaryModel?: string } }) + ?.config?.summaryModel, + ).toBe("anthropic/claude-sonnet-4-6"); + expect( + (res.config?.plugins?.entries?.["lossless-claw"] as { config?: { dataPath?: string } }) + ?.config?.dataPath, + ).toBe("/tmp/claude-opus-4-5"); + expect( + ( + res.config?.plugins?.entries?.["lossless-claw"] as { + subagent?: { allowedModels?: string[] }; + } + )?.subagent?.allowedModels, + ).toEqual(["anthropic/claude-sonnet-4-6", "*"]); + 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-mini"', + 'config.agents.defaults.musicGenerationModel from "vercel-ai-gateway/anthropic/claude-opus-4-5" to "vercel-ai-gateway/anthropic/claude-opus-4-6"', + 'config.agents.defaults.pdfModel from "anthropic/claude-3-5-sonnet" to "anthropic/claude-sonnet-4-6"', + 'config.agents.defaults.model.primary from "anthropic/claude-opus-4-5@anthropic:work" to "anthropic/claude-opus-4-7@anthropic:work"', + 'config.agents.defaults.model.fallbacks.2 from "github-copilot/grok-code-fast-1@github:work" to "github-copilot/gpt-5-mini@github:work"', + 'config.agents.defaults.model.fallbacks.3 from "venice/claude-opus-4-5" to "venice/claude-opus-4-6"', + 'config.agents.defaults.model.fallbacks.4 from "vercel-ai-gateway/anthropic/claude-opus-4-5" to "vercel-ai-gateway/anthropic/claude-opus-4-6"', + 'config.agents.defaults.models key from "github-copilot/claude-opus-4.5" to "github-copilot/claude-opus-4.7"', + '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"', + ]); + }); + it("removes unrecognized model compat thinkingFormat values", () => { const res = migrateLegacyConfigForTest({ models: { 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 c4dfb35b2bd..e53390fecb5 100644 --- a/src/commands/doctor/shared/legacy-config-migrations.runtime.models.ts +++ b/src/commands/doctor/shared/legacy-config-migrations.runtime.models.ts @@ -1,3 +1,4 @@ +import { splitTrailingAuthProfile } from "../../../agents/model-ref-profile.js"; import { defineLegacyConfigMigration, getRecord, @@ -37,7 +38,371 @@ const INVALID_THINKING_FORMAT_RULE: LegacyConfigRule = { match: (value) => hasInvalidThinkingFormat(value), }; +function normalizeString(value: unknown): string { + return typeof value === "string" ? value.trim().toLowerCase() : ""; +} + +function preferredClaudeSeparator(provider: string | undefined): "." | "-" { + return provider === "github-copilot" || provider === "copilot-proxy" ? "." : "-"; +} + +function claudeTargetModelId( + family: "opus" | "sonnet", + separator: "." | "-", + provider?: string, +): string { + const version = + family === "opus" && provider !== "venice" && provider !== "vercel-ai-gateway" ? "4.7" : "4.6"; + return `claude-${family}-${separator === "." ? version : version.replace(".", "-")}`; +} + +function shouldUpgradeClaudeProvider(provider: string | undefined): boolean { + return ( + !provider || + provider === "anthropic" || + provider === "github-copilot" || + provider === "copilot-proxy" || + provider === "venice" || + provider === "vercel-ai-gateway" + ); +} + +function hasRetiredVersionPrefix(normalized: string, prefix: string): boolean { + if (normalized === prefix) { + return true; + } + if (!normalized.startsWith(prefix)) { + return false; + } + const next = normalized[prefix.length]; + return next === "-" || next === "." || next === ":" || next === "@"; +} + +function hasAnyRetiredVersionPrefix(normalized: string, prefixes: readonly string[]): boolean { + return prefixes.some((prefix) => hasRetiredVersionPrefix(normalized, prefix)); +} + +function upgradeOldClaudeToken( + token: string, + separator: "." | "-", + provider?: string, +): string | null { + const normalized = normalizeString(token); + if (!normalized) { + return null; + } + const opusTarget = claudeTargetModelId("opus", separator, provider); + const sonnetTarget = claudeTargetModelId("sonnet", separator, provider); + if ( + normalized.startsWith("claude-opus-4-7") || + normalized.startsWith("claude-opus-4.7") || + normalized.startsWith("claude-opus-4-6") || + normalized.startsWith("claude-opus-4.6") || + normalized.startsWith("claude-sonnet-4-6") || + normalized.startsWith("claude-sonnet-4.6") + ) { + return null; + } + if ( + normalized === "claude-opus-4" || + hasAnyRetiredVersionPrefix(normalized, [ + "claude-opus-4-5", + "claude-opus-4.5", + "claude-opus-4-1", + "claude-opus-4.1", + "claude-opus-4-0", + "claude-opus-4.0", + ]) || + /^claude-opus-4-20\d{6}/.test(normalized) + ) { + return opusTarget; + } + if ( + normalized === "claude-sonnet-4" || + hasAnyRetiredVersionPrefix(normalized, [ + "claude-sonnet-4-5", + "claude-sonnet-4.5", + "claude-sonnet-4-1", + "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) + ) { + return sonnetTarget; + } + if (normalized.startsWith("claude-3") && normalized.includes("opus")) { + return opusTarget; + } + if ( + normalized.startsWith("claude-3") && + (normalized.includes("sonnet") || normalized.includes("haiku")) + ) { + return sonnetTarget; + } + if (normalized.startsWith("anthropic.claude-opus-")) { + if (provider === "amazon-bedrock" || provider === "amazon-bedrock-mantle") { + return null; + } + if ( + normalized.startsWith("anthropic.claude-opus-4-7") || + normalized.startsWith("anthropic.claude-opus-4-6") + ) { + return null; + } + return `anthropic.${claudeTargetModelId("opus", "-", provider)}`; + } + if ( + normalized.startsWith("anthropic.claude-sonnet-") || + normalized.startsWith("anthropic.claude-haiku-") + ) { + if (provider === "amazon-bedrock" || provider === "amazon-bedrock-mantle") { + return null; + } + if (normalized.startsWith("anthropic.claude-sonnet-4-6")) { + return null; + } + return `anthropic.${claudeTargetModelId("sonnet", "-", provider)}`; + } + if ( + normalized === "opus-4.5" || + normalized === "opus-4.1" || + normalized === "opus-4" || + normalized === "opus-3" + ) { + return opusTarget; + } + if ( + normalized === "sonnet-4.5" || + normalized === "sonnet-4.1" || + normalized === "sonnet-4.0" || + normalized === "sonnet-4" || + normalized === "sonnet-3.7" || + normalized === "sonnet-3.5" || + normalized === "sonnet-3" || + normalized === "haiku-4.5" || + normalized === "haiku-3.5" || + normalized === "haiku-3" + ) { + return sonnetTarget; + } + return null; +} + +function upgradeOldClaudeModelPart(model: string, provider: string | undefined): string | null { + const separator = preferredClaudeSeparator(provider); + const slashParts = model.split("/"); + const lastPart = slashParts.at(-1); + if (lastPart) { + const upgraded = upgradeOldClaudeToken(lastPart, separator, provider); + if (upgraded) { + return [...slashParts.slice(0, -1), upgraded].join("/"); + } + } + return upgradeOldClaudeToken(model, separator, provider); +} + +function upgradeRetiredModelRef(value: string): string | null { + const trimmed = value.trim(); + if (!trimmed) { + return null; + } + const split = splitTrailingAuthProfile(trimmed); + const modelRef = split.model; + const slash = modelRef.indexOf("/"); + const provider = slash > 0 ? modelRef.slice(0, slash).trim() : undefined; + const model = slash > 0 ? modelRef.slice(slash + 1).trim() : modelRef; + const normalizedProvider = normalizeString(provider); + const normalizedModel = normalizeString(model); + + if ( + (normalizedProvider === "github-copilot" || normalizedProvider === "copilot-proxy") && + normalizedModel === "grok-code-fast-1" + ) { + return `${provider}/gpt-5-mini${split.profile ? `@${split.profile}` : ""}`; + } + if (!shouldUpgradeClaudeProvider(normalizedProvider || undefined)) { + return null; + } + + const upgradedModel = upgradeOldClaudeModelPart(model, normalizedProvider || undefined); + if (!upgradedModel || upgradedModel === model) { + return null; + } + const upgraded = provider ? `${provider}/${upgradedModel}` : upgradedModel; + return `${upgraded}${split.profile ? `@${split.profile}` : ""}`; +} + +const MODEL_REF_STRING_KEYS = new Set([ + "model", + "primary", + "summaryModel", + "imageModel", + "imageGenerationModel", + "musicGenerationModel", + "pdfModel", + "videoGenerationModel", +]); +const MODEL_REF_ARRAY_KEYS = new Set([ + "fallback", + "fallbacks", + "allowedModels", + "modelFallbacks", + "imageModelFallbacks", +]); +const MODEL_REF_MAP_KEYS = new Set(["models"]); + +function pathKey(path: string): string { + return path.slice(path.lastIndexOf(".") + 1); +} + +function isChannelModelOverridePath(path: string): boolean { + return path.includes(".modelByChannel."); +} + +function scanKnownModelRefs(value: unknown, key?: string, path = ""): boolean { + if (typeof value === "string") { + return Boolean( + key && + (MODEL_REF_STRING_KEYS.has(key) || isChannelModelOverridePath(path)) && + upgradeRetiredModelRef(value), + ); + } + if (Array.isArray(value)) { + return value.some((entry, index) => + typeof entry === "string" && key && MODEL_REF_ARRAY_KEYS.has(key) + ? Boolean(upgradeRetiredModelRef(entry)) + : scanKnownModelRefs(entry, undefined, `${path}.${index}`), + ); + } + const record = getRecord(value); + if (!record) { + return false; + } + if (key && MODEL_REF_MAP_KEYS.has(key)) { + return Object.keys(record).some((entryKey) => Boolean(upgradeRetiredModelRef(entryKey))); + } + return Object.entries(record).some(([childKey, child]) => + scanKnownModelRefs(child, childKey, `${path}.${childKey}`), + ); +} + +function rewriteModelRefString(value: string, path: string, changes: string[]): string { + const upgraded = upgradeRetiredModelRef(value); + if (!upgraded) { + return value; + } + changes.push(`Upgraded ${path} from ${JSON.stringify(value)} to ${JSON.stringify(upgraded)}.`); + return upgraded; +} + +function rewriteModelRefMapKeys( + record: Record, + path: string, + changes: string[], +): { value: Record; changed: boolean } { + let changed = false; + const next: Record = {}; + for (const [key, child] of Object.entries(record)) { + const upgradedKey = upgradeRetiredModelRef(key); + const nextKey = upgradedKey ?? key; + if (upgradedKey) { + changes.push( + `Upgraded ${path} key from ${JSON.stringify(key)} to ${JSON.stringify(upgradedKey)}.`, + ); + changed = true; + } + if (nextKey in next && upgradedKey) { + continue; + } + next[nextKey] = child; + } + return { value: changed ? next : record, changed }; +} + +function rewriteKnownModelRefs( + value: unknown, + path: string, + changes: string[], +): { value: unknown; changed: boolean } { + const key = pathKey(path); + if (typeof value === "string") { + if (!MODEL_REF_STRING_KEYS.has(key) && !isChannelModelOverridePath(path)) { + return { value, changed: false }; + } + const next = rewriteModelRefString(value, path, changes); + return { value: next, changed: next !== value }; + } + if (Array.isArray(value)) { + let changed = false; + const next = value.map((entry, index) => { + if (typeof entry === "string" && MODEL_REF_ARRAY_KEYS.has(key)) { + const rewritten = rewriteModelRefString(entry, `${path}.${index}`, changes); + changed ||= rewritten !== entry; + return rewritten; + } + const rewritten = rewriteKnownModelRefs(entry, `${path}.${index}`, changes); + changed ||= rewritten.changed; + return rewritten.value; + }); + return { value: changed ? next : value, changed }; + } + const record = getRecord(value); + if (!record) { + return { value, changed: false }; + } + + let working = record; + let changed = false; + if (MODEL_REF_MAP_KEYS.has(key)) { + const rewrittenKeys = rewriteModelRefMapKeys(record, path, changes); + working = rewrittenKeys.value; + changed ||= rewrittenKeys.changed; + } + + const next: Record = {}; + for (const [childKey, child] of Object.entries(working)) { + const rewritten = rewriteKnownModelRefs(child, `${path}.${childKey}`, changes); + changed ||= rewritten.changed; + next[childKey] = rewritten.value; + } + return { value: changed ? next : value, changed }; +} + +const RETIRED_MODEL_REF_MESSAGE = + 'Configured Claude models older than 4.6 or retired Copilot model refs are no longer in the bundled catalogs; run "openclaw doctor --fix" to upgrade them.'; +const RETIRED_MODEL_REF_RULES: LegacyConfigRule[] = [ + "agents", + "plugins", + "messages", + "tools", + "hooks", + "channels", + "models", +].map((section) => ({ + path: [section], + message: RETIRED_MODEL_REF_MESSAGE, + match: (value) => scanKnownModelRefs(value), +})); + export const LEGACY_CONFIG_MIGRATIONS_RUNTIME_MODELS: LegacyConfigMigrationSpec[] = [ + defineLegacyConfigMigration({ + id: "models.retired-claude-and-copilot-refs", + describe: "Upgrade retired Claude/Copilot model refs to current catalog entries", + legacyRules: RETIRED_MODEL_REF_RULES, + apply: (raw, changes) => { + const rewritten = rewriteKnownModelRefs(raw, "config", changes); + if (!rewritten.changed || !getRecord(rewritten.value)) { + return; + } + for (const key of Object.keys(raw)) { + delete raw[key]; + } + Object.assign(raw, rewritten.value); + }, + }), defineLegacyConfigMigration({ id: "models.providers.*.models.*.compat.thinkingFormat-invalid", describe: "Remove unrecognized compat.thinkingFormat values from provider model entries", diff --git a/src/config/config-misc.test.ts b/src/config/config-misc.test.ts index 60d047e143c..09fde09586f 100644 --- a/src/config/config-misc.test.ts +++ b/src/config/config-misc.test.ts @@ -1267,6 +1267,24 @@ describe("config strict validation", () => { expect(raw.messages.tts).not.toHaveProperty("providers"); }); + it("reports retired plugin model refs without an agents section", () => { + const raw = { + plugins: { + entries: { + "lossless-claw": { + config: { + summaryModel: "anthropic/claude-opus-4-5", + }, + }, + }, + }, + }; + const issues = findLegacyConfigIssues(raw); + + expect(issuePaths(issues)).toContain("plugins"); + expect(issuePaths(issues)).not.toContain("agents"); + }); + it("reports retired queue steering modes without read-time auto-migration", async () => { const raw = { messages: {