mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-28 02:12:59 +00:00
fix: prune retired model catalog entries
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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" },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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() : "";
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user