diff --git a/src/agents/pi-embedded-runner/model.test.ts b/src/agents/pi-embedded-runner/model.test.ts index dbcbfc31d..4a9bba8ca 100644 --- a/src/agents/pi-embedded-runner/model.test.ts +++ b/src/agents/pi-embedded-runner/model.test.ts @@ -172,6 +172,41 @@ describe("resolveModel", () => { }); }); + it("builds an anthropic forward-compat fallback for claude-opus-4-6", () => { + const templateModel = { + id: "claude-opus-4-5", + name: "Claude Opus 4.5", + provider: "anthropic", + api: "anthropic-messages", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"] as const, + cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 }, + contextWindow: 200000, + maxTokens: 64000, + }; + + vi.mocked(discoverModels).mockReturnValue({ + find: vi.fn((provider: string, modelId: string) => { + if (provider === "anthropic" && modelId === "claude-opus-4-5") { + return templateModel; + } + return null; + }), + } as unknown as ReturnType); + + const result = resolveModel("anthropic", "claude-opus-4-6", "/tmp/agent"); + + expect(result.error).toBeUndefined(); + expect(result.model).toMatchObject({ + provider: "anthropic", + id: "claude-opus-4-6", + api: "anthropic-messages", + baseUrl: "https://api.anthropic.com", + reasoning: true, + }); + }); + it("keeps unknown-model errors for non-gpt-5 openai-codex ids", () => { const result = resolveModel("openai-codex", "gpt-4.1-mini", "/tmp/agent"); expect(result.model).toBeUndefined(); diff --git a/src/agents/pi-embedded-runner/model.ts b/src/agents/pi-embedded-runner/model.ts index a11751a46..2f489ffda 100644 --- a/src/agents/pi-embedded-runner/model.ts +++ b/src/agents/pi-embedded-runner/model.ts @@ -23,6 +23,12 @@ const OPENAI_CODEX_GPT_53_MODEL_ID = "gpt-5.3-codex"; const OPENAI_CODEX_TEMPLATE_MODEL_IDS = ["gpt-5.2-codex"] as const; +// pi-ai's built-in Anthropic catalog can lag behind OpenClaw's defaults/docs. +// Add forward-compat fallbacks for known-new IDs by cloning an older template model. +const ANTHROPIC_OPUS_46_MODEL_ID = "claude-opus-4-6"; +const ANTHROPIC_OPUS_46_DOT_MODEL_ID = "claude-opus-4.6"; +const ANTHROPIC_OPUS_TEMPLATE_MODEL_IDS = ["claude-opus-4-5", "claude-opus-4.5"] as const; + function resolveOpenAICodexGpt53FallbackModel( provider: string, modelId: string, @@ -63,6 +69,51 @@ function resolveOpenAICodexGpt53FallbackModel( } as Model); } +function resolveAnthropicOpus46ForwardCompatModel( + provider: string, + modelId: string, + modelRegistry: ModelRegistry, +): Model | undefined { + const normalizedProvider = normalizeProviderId(provider); + if (normalizedProvider !== "anthropic") { + return undefined; + } + + const trimmedModelId = modelId.trim(); + const lower = trimmedModelId.toLowerCase(); + const isOpus46 = + lower === ANTHROPIC_OPUS_46_MODEL_ID || + lower === ANTHROPIC_OPUS_46_DOT_MODEL_ID || + lower.startsWith(`${ANTHROPIC_OPUS_46_MODEL_ID}-`) || + lower.startsWith(`${ANTHROPIC_OPUS_46_DOT_MODEL_ID}-`); + if (!isOpus46) { + return undefined; + } + + const templateIds: string[] = []; + if (lower.startsWith(ANTHROPIC_OPUS_46_MODEL_ID)) { + templateIds.push(lower.replace(ANTHROPIC_OPUS_46_MODEL_ID, "claude-opus-4-5")); + } + if (lower.startsWith(ANTHROPIC_OPUS_46_DOT_MODEL_ID)) { + templateIds.push(lower.replace(ANTHROPIC_OPUS_46_DOT_MODEL_ID, "claude-opus-4.5")); + } + templateIds.push(...ANTHROPIC_OPUS_TEMPLATE_MODEL_IDS); + + for (const templateId of [...new Set(templateIds)].filter(Boolean)) { + const template = modelRegistry.find(normalizedProvider, templateId) as Model | null; + if (!template) { + continue; + } + return normalizeModelCompat({ + ...template, + id: trimmedModelId, + name: trimmedModelId, + } as Model); + } + + return undefined; +} + export function buildInlineProviderModels( providers: Record, ): InlineModelEntry[] { @@ -140,6 +191,14 @@ export function resolveModel( if (codexForwardCompat) { return { model: codexForwardCompat, authStorage, modelRegistry }; } + const anthropicForwardCompat = resolveAnthropicOpus46ForwardCompatModel( + provider, + modelId, + modelRegistry, + ); + if (anthropicForwardCompat) { + return { model: anthropicForwardCompat, authStorage, modelRegistry }; + } const providerCfg = providers[provider]; if (providerCfg || modelId.startsWith("mock-")) { const fallbackModel: Model = normalizeModelCompat({