fix: prune retired model catalog entries

This commit is contained in:
Peter Steinberger
2026-05-23 16:46:36 +01:00
parent 0c192e2915
commit b6530beb05
21 changed files with 693 additions and 225 deletions

View File

@@ -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.

View File

@@ -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" },
},
},
},

View File

@@ -166,13 +166,11 @@ DeepSeek provider's thinking controls.
| `minimax-m25` | MiniMax M2.5 | 198k | Reasoning |
</Accordion>
<Accordion title="Anonymized models (15) — via Venice proxy">
<Accordion title="Anonymized models (12) — via Venice proxy">
| 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 |
</Accordion>
</AccordionGroup>

View File

@@ -4,6 +4,7 @@ import { CLAUDE_CLI_BACKEND_ID, CLAUDE_CLI_MODEL_ALIASES } from "./cli-constants
const DEFAULT_CLAUDE_MODEL_BY_FAMILY: Record<string, string> = {
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(

View File

@@ -7,10 +7,7 @@ const CLAUDE_CLI_DEFAULT_CONTEXT_WINDOW = 200_000;
const CLAUDE_CLI_MODEL_LABELS: Record<string, string> = {
"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[] {

View File

@@ -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<string, string> = {
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 = [

View File

@@ -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" } },
},
},
},

View File

@@ -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() : "";

View File

@@ -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({

View File

@@ -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"
}
}
}

View File

@@ -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],
})
);
}

View File

@@ -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<string, string> } = {};
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<string, string> } = {};
const wrapped = wrapAnthropicProviderStream({

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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", () => {

View File

@@ -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",

View File

@@ -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
}
}
]
}

View File

@@ -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": [
{

View File

@@ -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: {

View File

@@ -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<string, unknown>,
path: string,
changes: string[],
): { value: Record<string, unknown>; changed: boolean } {
let changed = false;
const next: Record<string, unknown> = {};
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<string, unknown> = {};
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",

View File

@@ -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: {