From f305e707a35941eb99aecd9c4e0fdda3215a4966 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Mon, 6 Jul 2026 17:19:49 -0700 Subject: [PATCH] feat(models): add Claude Mythos 5 support (#101238) --- docs/providers/anthropic.md | 18 ++-- docs/providers/bedrock-mantle.md | 10 ++- docs/providers/bedrock.md | 14 +++ .../amazon-bedrock-mantle/discovery.test.ts | 16 +++- extensions/amazon-bedrock-mantle/discovery.ts | 12 +++ .../mantle-anthropic.runtime.test.ts | 34 ++++++++ .../mantle-anthropic.runtime.ts | 41 ++++++--- extensions/amazon-bedrock/bedrock-options.ts | 9 +- extensions/amazon-bedrock/discovery.test.ts | 77 ++++++++++------- extensions/amazon-bedrock/discovery.ts | 5 ++ extensions/amazon-bedrock/index.test.ts | 8 +- .../provider-policy-api.test.ts | 5 ++ .../amazon-bedrock/register.sync.runtime.ts | 4 +- .../amazon-bedrock/stream.runtime.test.ts | 85 +++++++++++++++---- extensions/amazon-bedrock/stream.runtime.ts | 25 +++--- extensions/amazon-bedrock/thinking-policy.ts | 6 +- extensions/anthropic-vertex/index.test.ts | 44 +++++++++- .../anthropic-vertex/provider-catalog.ts | 26 ++++-- .../provider-policy-api.test.ts | 21 +++-- extensions/anthropic-vertex/stream-runtime.ts | 17 ++-- extensions/anthropic/index.test.ts | 52 ++++++++++++ extensions/anthropic/openclaw.plugin.json | 19 +++++ .../anthropic/provider-policy-api.test.ts | 71 +++++++++------- extensions/anthropic/provider-policy-api.ts | 8 +- extensions/anthropic/register.runtime.ts | 44 +++++++--- packages/ai/src/model-utils.ts | 8 +- .../src/providers/anthropic-model-contract.ts | 58 ++++++++++--- packages/ai/src/providers/anthropic.test.ts | 78 +++++++++++++++++ packages/ai/src/providers/anthropic.ts | 24 ++++-- .../llm-core/src/model-contracts/anthropic.ts | 37 +++++++- src/agents/anthropic-transport-stream.test.ts | 66 ++++++++++++++ src/agents/anthropic-transport-stream.ts | 19 ++--- src/agents/context-resolution.ts | 7 ++ src/agents/context.test.ts | 19 +++++ src/agents/context.ts | 1 + .../transport-message-transform.test.ts | 25 +++++- src/plugin-sdk/provider-model-shared.test.ts | 36 ++++++-- src/plugin-sdk/provider-model-shared.ts | 3 + src/plugins/provider-claude-thinking.ts | 3 +- 39 files changed, 848 insertions(+), 207 deletions(-) diff --git a/docs/providers/anthropic.md b/docs/providers/anthropic.md index 10fa965cd695..46cdbf8c841d 100644 --- a/docs/providers/anthropic.md +++ b/docs/providers/anthropic.md @@ -185,7 +185,7 @@ OpenClaw release: -## Thinking defaults (Claude Sonnet 5, Fable 5, 4.8, and 4.6) +## Thinking defaults (Claude Sonnet 5, Mythos 5, Fable 5, 4.8, and 4.6) `anthropic/claude-sonnet-5` uses adaptive thinking at `high` effort by default. Use `/think off` to disable thinking, or `/think xhigh|max` for the model's @@ -201,6 +201,12 @@ effort. Anthropic does not allow thinking to be disabled for this model, so omits custom temperature values for Fable 5 requests, since Anthropic rejects a temperature override on any thinking-enabled request. +`anthropic/claude-mythos-5` is a limited-access model with the same always-on +adaptive-thinking contract. OpenClaw defaults to `high`, maps `/think off` and +`/think minimal` to `low`, and omits caller-selected sampling parameters. +The catalog publishes its 1,000,000-token context window, 128,000-token output +limit, image input, and `$10/$50` input/output pricing. + Claude Opus 4.8 keeps thinking off by default in OpenClaw. When you explicitly enable adaptive thinking with `/think high|xhigh|max`, OpenClaw sends Anthropic's Opus 4.8 effort values; Claude 4.6 models (Opus 4.6 and Sonnet 4.6) @@ -414,10 +420,11 @@ OpenClaw supports Anthropic's prompt caching feature for API-key auth. - Claude Sonnet 5 has an exact 1,000,000-token input window and supports up to - 128,000 output tokens. Anthropic's 1M context window is also GA on Claude 4.x - models with adaptive thinking: Opus 4.8, Opus 4.7, Opus 4.6, and Sonnet 4.6. - OpenClaw sizes these models automatically, no `params.context1m` needed: + Claude Sonnet 5, Mythos 5, and Fable 5 have an exact 1,000,000-token input + window and support up to 128,000 output tokens. Anthropic's 1M context + window is also GA on Claude 4.x models with adaptive thinking: Opus 4.8, + Opus 4.7, Opus 4.6, and Sonnet 4.6. OpenClaw sizes these models + automatically, no `params.context1m` needed: ```json5 { @@ -425,6 +432,7 @@ OpenClaw supports Anthropic's prompt caching feature for API-key auth. defaults: { models: { "anthropic/claude-sonnet-5": {}, + "anthropic/claude-mythos-5": {}, "anthropic/claude-opus-4-6": {}, }, }, diff --git a/docs/providers/bedrock-mantle.md b/docs/providers/bedrock-mantle.md index 80905110b38c..9a4d380777c0 100644 --- a/docs/providers/bedrock-mantle.md +++ b/docs/providers/bedrock-mantle.md @@ -3,7 +3,7 @@ summary: "Use Amazon Bedrock Mantle OpenAI-compatible and Claude Messages models read_when: - You want to use Bedrock Mantle hosted OSS models with OpenClaw - You need the Mantle OpenAI-compatible endpoint for GPT-OSS, Qwen, Kimi, or GLM - - You want to use Claude Sonnet 5 through Amazon Bedrock Mantle + - You want to use Claude Sonnet 5 or Mythos 5 through Amazon Bedrock Mantle title: "Amazon Bedrock Mantle" --- @@ -162,10 +162,11 @@ want to use. - When automatic discovery owns the model list, OpenClaw appends three Claude + When automatic discovery owns the model list, OpenClaw appends four Claude models after a successful lookup, regardless of what `/v1/models` returns: `amazon-bedrock-mantle/anthropic.claude-sonnet-5` (Claude Sonnet 5), `amazon-bedrock-mantle/anthropic.claude-opus-4-7` (Claude Opus 4.7), and + `amazon-bedrock-mantle/anthropic.claude-mythos-5` (Claude Mythos 5), plus `amazon-bedrock-mantle/anthropic.claude-mythos-preview` (Claude Mythos Preview). They use the `anthropic-messages` API surface and stream through the same bearer-authenticated Anthropic-compatible endpoint @@ -177,6 +178,11 @@ want to use. route cannot disable thinking. OpenClaw also omits custom temperature for Sonnet 5 requests. + Claude Mythos 5 is limited access. It publishes a 1,000,000-token context + window and 128,000-token output limit, always uses adaptive thinking, maps + `/think off` and `/think minimal` to `low`, and omits caller-selected + sampling parameters. + Claude Mythos Preview always requests reasoning, defaulting to `high` effort when no `/think` level is set (mapped from `xhigh`/`max` down to `high`, and `minimal` up to `low`). Opus 4.7 on Mantle streams without diff --git a/docs/providers/bedrock.md b/docs/providers/bedrock.md index 0de2600361e8..d3da2f9e4bf7 100644 --- a/docs/providers/bedrock.md +++ b/docs/providers/bedrock.md @@ -347,6 +347,20 @@ openclaw models list + + Claude Mythos 5 is available through Bedrock only for accounts with the + required limited-access approval. OpenClaw recognizes the foundation model + `anthropic.claude-mythos-5` and regional or global inference profiles such + as `us.anthropic.claude-mythos-5`. + + OpenClaw applies the 1,000,000-token context window, 128,000-token output + limit, image input, prompt caching, refusal-safe streaming, and native + effort levels. Adaptive thinking is always enabled: `/think off` and + `/think minimal` map to `low`, while `xhigh` and `max` remain available. + Custom sampling and forced tool choice values are omitted. + + + AWS documents Sonnet 5 for both the [`bedrock-runtime` and `bedrock-mantle` endpoints](https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-sonnet-5.html). diff --git a/extensions/amazon-bedrock-mantle/discovery.test.ts b/extensions/amazon-bedrock-mantle/discovery.test.ts index 3e8175e49dd7..5efe210b838f 100644 --- a/extensions/amazon-bedrock-mantle/discovery.test.ts +++ b/extensions/amazon-bedrock-mantle/discovery.test.ts @@ -520,7 +520,7 @@ describe("bedrock mantle discovery", () => { expect(provider?.api).toBe("openai-completions"); expect(provider?.auth).toBe("api-key"); expect(provider?.apiKey).toBe("env:AWS_BEARER_TOKEN_BEDROCK"); - expect(provider?.models).toHaveLength(4); + expect(provider?.models).toHaveLength(5); const sonnet = provider?.models?.find((model) => model.id === "anthropic.claude-sonnet-5"); expect(sonnet).toMatchObject({ api: "anthropic-messages", @@ -536,10 +536,20 @@ describe("bedrock mantle discovery", () => { expect(opus?.api).toBe("anthropic-messages"); expect(opus?.reasoning).toBe(false); expect(opus).not.toHaveProperty("baseUrl"); - const mythos = provider?.models?.find( + const mythos = provider?.models?.find((model) => model.id === "anthropic.claude-mythos-5"); + expect(mythos).toMatchObject({ + api: "anthropic-messages", + reasoning: true, + params: { canonicalModelId: "claude-mythos-5" }, + cost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 }, + contextWindow: 1_000_000, + maxTokens: 128_000, + thinkingLevelMap: { off: "low", minimal: "low", xhigh: "xhigh", max: "max" }, + }); + const mythosPreview = provider?.models?.find( (model) => model.id === "anthropic.claude-mythos-preview", ); - expect(mythos).toMatchObject({ + expect(mythosPreview).toMatchObject({ api: "anthropic-messages", reasoning: true, params: { canonicalModelId: "claude-mythos-preview" }, diff --git a/extensions/amazon-bedrock-mantle/discovery.ts b/extensions/amazon-bedrock-mantle/discovery.ts index da3841759e4f..4020c5fd66f6 100644 --- a/extensions/amazon-bedrock-mantle/discovery.ts +++ b/extensions/amazon-bedrock-mantle/discovery.ts @@ -463,6 +463,18 @@ export async function resolveImplicitMantleProvider(params: { contextWindow: 1_000_000, maxTokens: 128_000, }, + { + id: "anthropic.claude-mythos-5", + name: "Claude Mythos 5", + api: "anthropic-messages" as const, + reasoning: true, + params: { canonicalModelId: "claude-mythos-5" }, + input: ["text", "image"], + cost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 }, + contextWindow: 1_000_000, + maxTokens: 128_000, + thinkingLevelMap: { off: "low", minimal: "low", xhigh: "xhigh", max: "max" }, + }, { id: "anthropic.claude-mythos-preview", name: "Claude Mythos Preview", diff --git a/extensions/amazon-bedrock-mantle/mantle-anthropic.runtime.test.ts b/extensions/amazon-bedrock-mantle/mantle-anthropic.runtime.test.ts index 485f78fce42f..3e69c1601cd2 100644 --- a/extensions/amazon-bedrock-mantle/mantle-anthropic.runtime.test.ts +++ b/extensions/amazon-bedrock-mantle/mantle-anthropic.runtime.test.ts @@ -213,6 +213,40 @@ describe("createMantleAnthropicStreamFn", () => { expect(streamOptions.effort).toBe("low"); }); + it.each([ + { reasoning: undefined, effort: "high" }, + { reasoning: "off" as const, effort: "low" }, + { reasoning: "max" as const, effort: "max" }, + ])("maps Mythos 5 $reasoning reasoning to adaptive $effort", ({ reasoning, effort }) => { + const model = createTestModel({ + id: "anthropic.claude-mythos-5", + name: "Claude Mythos 5", + reasoning: true, + params: { canonicalModelId: "claude-mythos-5" }, + thinkingLevelMap: { off: "low", minimal: "low", xhigh: "xhigh", max: "max" }, + }); + const deps = createTestDeps(); + deps.stream.mockReturnValue({ kind: "anthropic-stream" } as never); + + void createMantleAnthropicStreamFn(deps)( + model, + { messages: [] }, + { + apiKey: "bedrock-bearer-token", + maxTokens: 1_000, + temperature: 0.2, + ...(reasoning ? { reasoning } : {}), + }, + ); + + const streamOptions = firstStreamOptions(deps); + expect(streamOptions.thinkingEnabled).toBe(true); + expect(streamOptions.effort).toBe(effort); + expect(streamOptions.maxTokens).toBe(1_000); + expect(streamOptions).not.toHaveProperty("thinkingBudgetTokens"); + expect(streamOptions.temperature).toBeUndefined(); + }); + it("normalizes Mantle provider URLs to the Anthropic endpoint", () => { expect(resolveMantleAnthropicBaseUrl("https://bedrock-mantle.us-east-1.api.aws/v1")).toBe( "https://bedrock-mantle.us-east-1.api.aws/anthropic", diff --git a/extensions/amazon-bedrock-mantle/mantle-anthropic.runtime.ts b/extensions/amazon-bedrock-mantle/mantle-anthropic.runtime.ts index 3486f0451e18..54bbfec6f001 100644 --- a/extensions/amazon-bedrock-mantle/mantle-anthropic.runtime.ts +++ b/extensions/amazon-bedrock-mantle/mantle-anthropic.runtime.ts @@ -10,7 +10,11 @@ import { type SimpleStreamOptions, type ThinkingLevel, } from "openclaw/plugin-sdk/llm"; -import { resolveClaudeSonnet5ModelIdentity } from "openclaw/plugin-sdk/provider-model-shared"; +import { + requiresClaudeDefaultSampling, + resolveClaudeMythos5ModelIdentity, + resolveClaudeSonnet5ModelIdentity, +} from "openclaw/plugin-sdk/provider-model-shared"; const MANTLE_ANTHROPIC_BETA = "fine-grained-tool-streaming-2025-05-14"; type AnthropicOptions = ConstructorParameters[0]; @@ -33,7 +37,7 @@ function isClaudeSonnet5Model(model: Model): boolean { } function requiresDefaultSampling(model: Model): boolean { - return model.id.includes("claude-opus-4-7") || isClaudeSonnet5Model(model); + return requiresClaudeDefaultSampling(model); } function isClaudeMythosPreviewModel(model: Model): boolean { @@ -49,6 +53,14 @@ function isClaudeMythosPreviewModel(model: Model): boolean { ); } +function isClaudeMythos5Model(model: Model): boolean { + return resolveClaudeMythos5ModelIdentity(model) !== undefined; +} + +function requiresClaudeMythosAdaptiveThinking(model: Model): boolean { + return isClaudeMythos5Model(model) || isClaudeMythosPreviewModel(model); +} + function resolveMantleReasoning( model: Model, options: SimpleStreamOptions | undefined, @@ -57,21 +69,19 @@ function resolveMantleReasoning( return undefined; } const sonnet5 = isClaudeSonnet5Model(model); - const reasoning = - options?.reasoning ?? (isClaudeMythosPreviewModel(model) || sonnet5 ? "high" : undefined); + const mythosPreview = isClaudeMythosPreviewModel(model); + const mandatoryMythos = isClaudeMythos5Model(model) || mythosPreview; + const reasoning = options?.reasoning ?? (mandatoryMythos || sonnet5 ? "high" : undefined); if (sonnet5) { return reasoning === "off" || reasoning === "minimal" ? "low" : reasoning; } - if (!isClaudeMythosPreviewModel(model)) { + if (!mandatoryMythos) { return reasoning; } - if (reasoning === "off") { - return "high"; - } - if (reasoning === "minimal") { + if (reasoning === "off" || reasoning === "minimal") { return "low"; } - return reasoning === "xhigh" || reasoning === "max" ? "high" : reasoning; + return mythosPreview && (reasoning === "xhigh" || reasoning === "max") ? "high" : reasoning; } function mapSonnet5Effort( @@ -107,7 +117,9 @@ function buildMantleAnthropicBaseOptions( ...(requiresDefaultSampling(model) ? {} : { temperature: options?.temperature }), maxTokens: options?.maxTokens || - (isClaudeSonnet5Model(model) ? model.maxTokens : Math.min(model.maxTokens, 32_000)), + (isClaudeSonnet5Model(model) || isClaudeMythos5Model(model) + ? model.maxTokens + : Math.min(model.maxTokens, 32_000)), signal: options?.signal, apiKey, cacheRetention: options?.cacheRetention, @@ -172,6 +184,7 @@ export function createMantleAnthropicStreamFn(deps?: { const streamClient = client as unknown as Anthropic; const reasoning = resolveMantleReasoning(model, options); const sonnet5 = isClaudeSonnet5Model(model); + const mythos5 = isClaudeMythos5Model(model); if (!reasoning || reasoning === "off") { return streamFn(model as Model<"anthropic-messages">, context, { ...base, @@ -180,12 +193,12 @@ export function createMantleAnthropicStreamFn(deps?: { }); } - if (sonnet5) { + if (sonnet5 || mythos5) { return streamFn(model as Model<"anthropic-messages">, context, { ...base, client: streamClient, thinkingEnabled: true, - effort: mapSonnet5Effort(reasoning), + effort: sonnet5 ? mapSonnet5Effort(reasoning) : reasoning, }); } @@ -200,7 +213,7 @@ export function createMantleAnthropicStreamFn(deps?: { client: streamClient, maxTokens: adjusted.maxTokens, thinkingEnabled: true, - ...(isClaudeMythosPreviewModel(model) ? { effort: reasoning } : {}), + ...(requiresClaudeMythosAdaptiveThinking(model) ? { effort: reasoning } : {}), thinkingBudgetTokens: adjusted.thinkingBudget, }); }; diff --git a/extensions/amazon-bedrock/bedrock-options.ts b/extensions/amazon-bedrock/bedrock-options.ts index e0e6b37b8ce6..ab29f48977a7 100644 --- a/extensions/amazon-bedrock/bedrock-options.ts +++ b/extensions/amazon-bedrock/bedrock-options.ts @@ -41,7 +41,14 @@ export function supportsBedrockPromptCaching(modelId: string, modelName?: string if (candidates.some((s) => s.includes("-4-"))) { return true; } - if (candidates.some((s) => s.includes("claude-fable-5") || s.includes("claude-sonnet-5"))) { + if ( + candidates.some( + (candidate) => + candidate.includes("claude-fable-5") || + candidate.includes("claude-mythos-5") || + candidate.includes("claude-sonnet-5"), + ) + ) { return true; } if (candidates.some((s) => s.includes("claude-3-7-sonnet"))) { diff --git a/extensions/amazon-bedrock/discovery.test.ts b/extensions/amazon-bedrock/discovery.test.ts index 098e75f9ac3f..c4591a55a4c3 100644 --- a/extensions/amazon-bedrock/discovery.test.ts +++ b/extensions/amazon-bedrock/discovery.test.ts @@ -166,37 +166,54 @@ describe("bedrock discovery", () => { }); }); - it("marks known Fable inference profile fallbacks as reasoning capable", async () => { - sendMock - .mockResolvedValueOnce({ - modelSummaries: [], - }) - .mockResolvedValueOnce({ - inferenceProfileSummaries: [ - { - inferenceProfileId: "us.anthropic.claude-fable-5", - inferenceProfileName: "US Claude Fable 5", - status: "ACTIVE", - type: "SYSTEM_DEFINED", - models: [ - { - modelArn: "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-fable-5", - }, - ], - }, - ], + it.each([ + { + label: "Fable", + profileId: "us.anthropic.claude-fable-5", + profileName: "US Claude Fable 5", + foundationId: "anthropic.claude-fable-5", + }, + { + label: "Mythos", + profileId: "us.anthropic.claude-mythos-5", + profileName: "US Claude Mythos 5", + foundationId: "anthropic.claude-mythos-5", + }, + ])( + "marks known $label inference profile fallbacks as reasoning capable", + async ({ profileId, profileName, foundationId }) => { + sendMock + .mockResolvedValueOnce({ + modelSummaries: [], + }) + .mockResolvedValueOnce({ + inferenceProfileSummaries: [ + { + inferenceProfileId: profileId, + inferenceProfileName: profileName, + status: "ACTIVE", + type: "SYSTEM_DEFINED", + models: [ + { + modelArn: `arn:aws:bedrock:us-east-1::foundation-model/${foundationId}`, + }, + ], + }, + ], + }); + + const models = await discoverBedrockModels({ region: "us-east-1", clientFactory }); + + expect(models).toHaveLength(1); + expectModelFields(models[0], { + id: profileId, + reasoning: true, + contextWindow: 1_000_000, + maxTokens: 128_000, + thinkingLevelMap: { off: "low", minimal: "low", xhigh: "xhigh", max: "max" }, }); - - const models = await discoverBedrockModels({ region: "us-east-1", clientFactory }); - - expect(models).toHaveLength(1); - expectModelFields(models[0], { - id: "us.anthropic.claude-fable-5", - reasoning: true, - contextWindow: 1_000_000, - thinkingLevelMap: { off: "low", minimal: "low", xhigh: "xhigh", max: "max" }, - }); - }); + }, + ); it("applies the Sonnet 5 contract to inference-profile-only discovery", async () => { sendMock.mockResolvedValueOnce({ modelSummaries: [] }).mockResolvedValueOnce({ diff --git a/extensions/amazon-bedrock/discovery.ts b/extensions/amazon-bedrock/discovery.ts index ff4c9ae80d72..8185fbfc283b 100644 --- a/extensions/amazon-bedrock/discovery.ts +++ b/extensions/amazon-bedrock/discovery.ts @@ -21,6 +21,7 @@ import type { import { resolveClaudeFable5ModelIdentity, resolveClaudeModelIdentity, + resolveClaudeMythos5ModelIdentity, resolveClaudeSonnet5ModelIdentity, supportsClaudeAdaptiveThinking, } from "openclaw/plugin-sdk/provider-model-shared"; @@ -61,6 +62,7 @@ const DEFAULT_MAX_TOKENS = 4096; const KNOWN_CONTEXT_WINDOWS: Record = { // Anthropic Claude "anthropic.claude-fable-5": 1_000_000, + "anthropic.claude-mythos-5": 1_000_000, // AWS publishes Sonnet 5 on both bedrock-runtime (Invoke/Converse) and Mantle. "anthropic.claude-sonnet-5": 1_000_000, "anthropic.claude-3-7-sonnet-20250219-v1:0": 200_000, @@ -142,6 +144,7 @@ function resolveKnownContextWindow(modelId: string): number | undefined { for (const candidate of candidates) { if ( resolveClaudeFable5ModelIdentity({ id: candidate }) || + resolveClaudeMythos5ModelIdentity({ id: candidate }) || resolveClaudeSonnet5ModelIdentity({ id: candidate }) ) { return 1_000_000; @@ -178,6 +181,7 @@ function resolveKnownThinkingLevelMap( function resolveKnownMaxTokens(modelId: string): number | undefined { return resolveClaudeFable5ModelIdentity({ id: modelId }) || + resolveClaudeMythos5ModelIdentity({ id: modelId }) || resolveClaudeSonnet5ModelIdentity({ id: modelId }) ? 128_000 : undefined; @@ -185,6 +189,7 @@ function resolveKnownMaxTokens(modelId: string): number | undefined { function resolveKnownInput(modelId: string): ModelDefinitionConfig["input"] | undefined { return resolveClaudeFable5ModelIdentity({ id: modelId }) || + resolveClaudeMythos5ModelIdentity({ id: modelId }) || resolveClaudeSonnet5ModelIdentity({ id: modelId }) ? ["text", "image"] : undefined; diff --git a/extensions/amazon-bedrock/index.test.ts b/extensions/amazon-bedrock/index.test.ts index 568d45f5021b..f6e5bca57ff8 100644 --- a/extensions/amazon-bedrock/index.test.ts +++ b/extensions/amazon-bedrock/index.test.ts @@ -377,13 +377,16 @@ describe("amazon-bedrock provider plugin", () => { } }); - it("keeps Claude Fable 5 always adaptive with high default effort", async () => { + it("keeps mandatory-adaptive Claude 5 models at high default effort", async () => { const provider = await registerSingleProviderPlugin(amazonBedrockPlugin); for (const modelId of [ "anthropic.claude-fable-5", "us.anthropic.claude-fable-5", "global.anthropic.claude-fable-5", + "anthropic.claude-mythos-5", + "us.anthropic.claude-mythos-5", + "global.anthropic.claude-mythos-5", ]) { expectThinkingProfile( provider.resolveThinkingProfile?.({ @@ -414,8 +417,9 @@ describe("amazon-bedrock provider plugin", () => { ); }); - it("recognizes direct Fable model refs as prompt-cache eligible", () => { + it("recognizes direct Claude 5 model refs as prompt-cache eligible", () => { expect(supportsBedrockPromptCaching("us.anthropic.claude-fable-5")).toBe(true); + expect(supportsBedrockPromptCaching("us.anthropic.claude-mythos-5")).toBe(true); expect(supportsBedrockPromptCaching("global.anthropic.claude-sonnet-5")).toBe(true); }); diff --git a/extensions/amazon-bedrock/provider-policy-api.test.ts b/extensions/amazon-bedrock/provider-policy-api.test.ts index 11a984f63990..864418f186c2 100644 --- a/extensions/amazon-bedrock/provider-policy-api.test.ts +++ b/extensions/amazon-bedrock/provider-policy-api.test.ts @@ -73,6 +73,11 @@ describe("amazon-bedrock provider-policy-api", () => { defaultLevel: "high", preservesCatalogOptOut: true, }, + { + canonicalModelId: "claude-mythos-5", + defaultLevel: "high", + preservesCatalogOptOut: true, + }, { canonicalModelId: "claude-opus-4-8", defaultLevel: "off", diff --git a/extensions/amazon-bedrock/register.sync.runtime.ts b/extensions/amazon-bedrock/register.sync.runtime.ts index 254eabc4a918..176c42d816ca 100644 --- a/extensions/amazon-bedrock/register.sync.runtime.ts +++ b/extensions/amazon-bedrock/register.sync.runtime.ts @@ -15,6 +15,7 @@ import { normalizeProviderId, resolveClaudeFable5ModelIdentity, resolveClaudeModelIdentity, + resolveClaudeMythos5ModelIdentity, resolveClaudeSonnet5ModelIdentity, } from "openclaw/plugin-sdk/provider-model-shared"; import { streamWithPayloadPatch } from "openclaw/plugin-sdk/provider-stream-shared"; @@ -57,7 +58,8 @@ function normalizeBedrockResolvedModel({ modelId, model }: ProviderNormalizeReso } const reasoning = model.reasoning || - resolveClaudeFable5ModelIdentity({ id: modelId, params: model.params }) !== undefined; + resolveClaudeFable5ModelIdentity({ id: modelId, params: model.params }) !== undefined || + resolveClaudeMythos5ModelIdentity({ id: modelId, params: model.params }) !== undefined; const current = model.thinkingLevelMap; const currentEfforts = current as Record | undefined; if ( diff --git a/extensions/amazon-bedrock/stream.runtime.test.ts b/extensions/amazon-bedrock/stream.runtime.test.ts index 9676a5dfda51..10a2a28bfba2 100644 --- a/extensions/amazon-bedrock/stream.runtime.test.ts +++ b/extensions/amazon-bedrock/stream.runtime.test.ts @@ -254,23 +254,47 @@ describe("Bedrock thinking effort mapping", () => { expect(options.maxTokens).toBe(32_000); }); - it("forces adaptive thinking for Bedrock Mythos Preview when callers omit reasoning", () => { - const model = bedrockModel({ - id: "us.anthropic.claude-mythos-preview", - name: "US Claude Mythos Preview", - reasoning: true, - contextWindow: 1_000_000, - maxTokens: 128_000, - }); - const options = testing.resolveSimpleBedrockOptions(model, {}); + it.each(["claude-mythos-preview", "claude-mythos-5"])( + "forces adaptive thinking for Bedrock %s when callers omit reasoning", + (modelId) => { + const model = bedrockModel({ + id: `us.anthropic.${modelId}`, + name: modelId, + reasoning: true, + contextWindow: 1_000_000, + maxTokens: 128_000, + }); + const options = testing.resolveSimpleBedrockOptions(model, {}); - expect(options.reasoning).toBe("high"); - expect(options.maxTokens).toBe(128_000); - expect(testing.buildAdditionalModelRequestFields(model, options)).toEqual({ - thinking: { type: "adaptive", display: "summarized" }, - output_config: { effort: "high" }, - }); - }); + expect(options.reasoning).toBe("high"); + expect(options.maxTokens).toBe(128_000); + expect(testing.buildAdditionalModelRequestFields(model, options)).toEqual({ + thinking: { type: "adaptive", display: "summarized" }, + output_config: { effort: "high" }, + }); + }, + ); + + it.each(["claude-mythos-preview", "claude-mythos-5"])( + "maps explicit off to low effort for Bedrock %s", + (modelId) => { + const model = bedrockModel({ + id: `us.anthropic.${modelId}`, + name: modelId, + reasoning: true, + contextWindow: 1_000_000, + maxTokens: 128_000, + }); + const options = testing.resolveSimpleBedrockOptions(model, { reasoning: "off" }); + + expect(options.reasoning).toBe("low"); + expect(options.maxTokens).toBe(128_000); + expect(testing.buildAdditionalModelRequestFields(model, options)).toEqual({ + thinking: { type: "adaptive", display: "summarized" }, + output_config: { effort: "low" }, + }); + }, + ); it("clamps max effort for Claude models without native max support", () => { expect( @@ -308,6 +332,18 @@ describe("Bedrock thinking effort mapping", () => { ).toBe("max"); }); + it("preserves max effort for Claude Mythos 5", () => { + expect( + testing.mapThinkingLevelToEffort( + bedrockModel({ + id: "anthropic.claude-mythos-5", + name: "Claude Mythos 5", + }), + "max", + ), + ).toBe("max"); + }); + it("uses canonical Claude policy for deployment aliases", () => { expect( testing.mapThinkingLevelToEffort( @@ -422,7 +458,20 @@ describe("Bedrock Fable contract", () => { expect(command.input?.toolConfig).toBeUndefined(); }); - it("quarantines partial output when Fable returns a terminal refusal", async () => { + it.each([ + ["Fable", () => fableModel()], + [ + "Mythos 5", + () => + bedrockModel({ + id: "production-mythos", + name: "Production deployment", + params: { canonicalModelId: "claude-mythos-5" }, + contextWindow: 1_000_000, + maxTokens: 128_000, + }), + ], + ])("quarantines partial output when %s returns a terminal refusal", async (_name, model) => { vi.spyOn(BedrockRuntimeClient.prototype, "send").mockResolvedValue({ $metadata: { httpStatusCode: 200 }, stream: streamEvents([ @@ -446,7 +495,7 @@ describe("Bedrock Fable contract", () => { ]), } as never); - const stream = streamSimpleBedrock(fableModel(), context()); + const stream = streamSimpleBedrock(model(), context()); const eventTypes: string[] = []; for await (const event of stream) { eventTypes.push(event.type); diff --git a/extensions/amazon-bedrock/stream.runtime.ts b/extensions/amazon-bedrock/stream.runtime.ts index 4a6f93272bce..52e07d3cf993 100644 --- a/extensions/amazon-bedrock/stream.runtime.ts +++ b/extensions/amazon-bedrock/stream.runtime.ts @@ -55,7 +55,9 @@ import { import { resolveClaudeFable5ModelIdentity, resolveClaudeModelIdentity, + resolveClaudeMythos5ModelIdentity, resolveClaudeSonnet5ModelIdentity, + requiresClaudeMandatoryAdaptiveThinking, supportsClaudeAdaptiveThinking, supportsClaudeNativeXhighEffort, } from "openclaw/plugin-sdk/provider-model-shared"; @@ -81,7 +83,11 @@ function usesClaudeSonnet5BedrockContract(model: Model<"bedrock-converse-stream" function usesClaudeStreamingRefusalBedrockContract( model: Model<"bedrock-converse-stream">, ): boolean { - return usesClaudeFable5BedrockContract(model) || usesClaudeSonnet5BedrockContract(model); + return ( + usesClaudeFable5BedrockContract(model) || + resolveClaudeMythos5ModelIdentity(model) !== undefined || + usesClaudeSonnet5BedrockContract(model) + ); } function readBedrockStopDetails(fields: DocumentType | undefined): unknown { @@ -387,7 +393,7 @@ function resolveSimpleBedrockOptions( options?: SimpleStreamOptions, ): BedrockOptions { const base = buildBaseOptions(model, options, undefined); - if (usesClaudeFable5BedrockContract(model) || usesClaudeSonnet5BedrockContract(model)) { + if (requiresMandatoryAdaptiveThinking(model)) { return { ...base, maxTokens: resolveAdaptiveBedrockMaxTokens(model, base.maxTokens), @@ -600,7 +606,7 @@ function resolveClaudeProfileNameModelId(modelName?: string): string | undefined if (!normalized.includes("claude")) { return undefined; } - const family = /(?:fable-5|mythos-preview|opus-4-(?:6|7|8)|sonnet-(?:5|4-6))(?:$|-)/.exec( + const family = /(?:fable-5|mythos-(?:5|preview)|opus-4-(?:6|7|8)|sonnet-(?:5|4-6))(?:$|-)/.exec( normalized, )?.[0]; return family ? `claude-${family.replace(/-$/, "")}` : undefined; @@ -631,6 +637,8 @@ function supportsAdaptiveThinking(model: Model<"bedrock-converse-stream">): bool function requiresMandatoryAdaptiveThinking(model: Model<"bedrock-converse-stream">): boolean { const profileModelId = resolveClaudeProfileNameModelId(model.name); return ( + requiresClaudeMandatoryAdaptiveThinking(model) || + requiresClaudeMandatoryAdaptiveThinking({ id: profileModelId }) || isClaudeMythosPreviewModelId(resolveClaudeModelIdentity(model)) || isClaudeMythosPreviewModelId(profileModelId) || usesClaudeSonnet5BedrockContract(model) || @@ -1060,17 +1068,14 @@ function buildAdditionalModelRequestFields( model: Model<"bedrock-converse-stream">, options: BedrockOptions, ): DocumentType | undefined { - // Bedrock keeps Fable/Sonnet 5 adaptive. Preserve the public `off` control by + // Mandatory-adaptive Claude routes preserve the public `off` control by // lowering effort instead of silently falling back to the route's high default. - const mandatoryAdaptiveThinking = - usesClaudeFable5BedrockContract(model) || requiresMandatoryAdaptiveThinking(model); + const mandatoryAdaptiveThinking = requiresMandatoryAdaptiveThinking(model); const reasoning = options.reasoning === "off" - ? usesClaudeFable5BedrockContract(model) || usesClaudeSonnet5BedrockContract(model) + ? mandatoryAdaptiveThinking ? "low" - : mandatoryAdaptiveThinking - ? "high" - : "off" + : "off" : (options.reasoning ?? (mandatoryAdaptiveThinking ? "high" : undefined)); if (reasoning === "off") { return undefined; diff --git a/extensions/amazon-bedrock/thinking-policy.ts b/extensions/amazon-bedrock/thinking-policy.ts index cd8c7ec4bfd8..e8e861b47383 100644 --- a/extensions/amazon-bedrock/thinking-policy.ts +++ b/extensions/amazon-bedrock/thinking-policy.ts @@ -9,6 +9,7 @@ import type { import { resolveClaudeFable5ModelIdentity, resolveClaudeModelIdentity, + resolveClaudeMythos5ModelIdentity, resolveClaudeSonnet5ModelIdentity, } from "openclaw/plugin-sdk/provider-model-shared"; @@ -59,6 +60,7 @@ export function isLatestAdaptiveBedrockModelRef( const canonicalModelId = resolveClaudeModelIdentity(modelRef); return ( resolveClaudeFable5ModelIdentity(modelRef) !== undefined || + resolveClaudeMythos5ModelIdentity(modelRef) !== undefined || resolveClaudeSonnet5ModelIdentity(modelRef) !== undefined || [modelId, canonicalModelId].some( (candidate) => @@ -74,6 +76,7 @@ export function supportsBedrockNativeMaxEffort( ): boolean { if ( resolveClaudeFable5ModelIdentity({ id: modelId, params }) || + resolveClaudeMythos5ModelIdentity({ id: modelId, params }) || resolveClaudeSonnet5ModelIdentity({ id: modelId, params }) ) { return true; @@ -90,7 +93,7 @@ export function resolveBedrockNativeThinkingLevelMap( params?: Record, ): ProviderRuntimeModel["thinkingLevelMap"] | undefined { const modelRef = { id: modelId, params }; - if (resolveClaudeFable5ModelIdentity(modelRef)) { + if (resolveClaudeFable5ModelIdentity(modelRef) || resolveClaudeMythos5ModelIdentity(modelRef)) { return { off: "low", minimal: "low", xhigh: "xhigh", max: "max" }; } if (resolveClaudeSonnet5ModelIdentity(modelRef)) { @@ -116,6 +119,7 @@ export function resolveBedrockClaudeThinkingProfile( const modelRefs = [trimmed, canonicalModelId]; if ( resolveClaudeFable5ModelIdentity({ id: trimmed, params }) || + resolveClaudeMythos5ModelIdentity({ id: trimmed, params }) || resolveClaudeSonnet5ModelIdentity({ id: trimmed, params }) ) { return { diff --git a/extensions/anthropic-vertex/index.test.ts b/extensions/anthropic-vertex/index.test.ts index bf05f162c28a..cc3f95053d79 100644 --- a/extensions/anthropic-vertex/index.test.ts +++ b/extensions/anthropic-vertex/index.test.ts @@ -80,6 +80,7 @@ describe("anthropic-vertex provider plugin", () => { expect(result.provider.headers).toEqual({ "x-test-header": "1" }); expect(result.provider.models.map((model) => model.id)).toEqual([ "claude-fable-5", + "claude-mythos-5", "claude-opus-4-8", "claude-opus-4-6", "claude-sonnet-4-6", @@ -90,9 +91,15 @@ describe("anthropic-vertex provider plugin", () => { xhigh: "xhigh", max: "max", }); - expect(result.provider.models[1]?.thinkingLevelMap).toEqual({ xhigh: "xhigh", max: "max" }); - expect(result.provider.models[2]?.thinkingLevelMap).toEqual({ xhigh: null, max: "max" }); + expect(result.provider.models[1]?.thinkingLevelMap).toEqual({ + off: "low", + minimal: "low", + xhigh: "xhigh", + max: "max", + }); + expect(result.provider.models[2]?.thinkingLevelMap).toEqual({ xhigh: "xhigh", max: "max" }); expect(result.provider.models[3]?.thinkingLevelMap).toEqual({ xhigh: null, max: "max" }); + expect(result.provider.models[4]?.thinkingLevelMap).toEqual({ xhigh: null, max: "max" }); }); it.each(["global", "us", "eu"])("publishes Sonnet 5 for the %s endpoint", (region) => { @@ -289,6 +296,39 @@ describe("anthropic-vertex provider plugin", () => { }); }); + it("restores Mythos 5 metadata for explicit Vertex catalog rows", async () => { + const provider = await registerSingleProviderPlugin(anthropicVertexPlugin); + const normalized = provider.normalizeResolvedModel?.({ + provider: "anthropic-vertex", + modelId: "claude-mythos-5", + model: { + id: "claude-mythos-5", + name: "Claude Mythos 5", + api: "anthropic-messages", + provider: "anthropic-vertex", + baseUrl: "https://aiplatform.googleapis.com", + reasoning: false, + input: ["text"], + cost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 }, + contextWindow: 200_000, + maxTokens: 8192, + }, + } as never); + + expect(normalized).toMatchObject({ + reasoning: true, + input: ["text", "image"], + contextWindow: 1_000_000, + contextTokens: 1_000_000, + maxTokens: 128_000, + thinkingLevelMap: { + off: "low", + minimal: "low", + xhigh: "xhigh", + max: "max", + }, + }); + }); it("resolves synthetic auth when ADC is available", async () => { hasAnthropicVertexAvailableAuthMock.mockReturnValue(true); const provider = await registerSingleProviderPlugin(anthropicVertexPlugin); diff --git a/extensions/anthropic-vertex/provider-catalog.ts b/extensions/anthropic-vertex/provider-catalog.ts index d80d0a2a982f..84c41c1e80af 100644 --- a/extensions/anthropic-vertex/provider-catalog.ts +++ b/extensions/anthropic-vertex/provider-catalog.ts @@ -9,6 +9,7 @@ import type { } from "openclaw/plugin-sdk/provider-model-shared"; import { resolveClaudeFable5ModelIdentity, + resolveClaudeMythos5ModelIdentity, resolveClaudeSonnet5ModelIdentity, } from "openclaw/plugin-sdk/provider-model-shared"; import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime"; @@ -16,7 +17,7 @@ import { resolveAnthropicVertexClientRegion, resolveAnthropicVertexRegion } from /** Default Anthropic Vertex model used for implicit provider catalogs. */ export const ANTHROPIC_VERTEX_DEFAULT_MODEL_ID = "claude-sonnet-4-6"; const ANTHROPIC_VERTEX_DEFAULT_CONTEXT_WINDOW = 1_000_000; -const ANTHROPIC_VERTEX_FABLE_MAX_TOKENS = 128_000; +const ANTHROPIC_VERTEX_CLAUDE_5_MAX_TOKENS = 128_000; // Vertex's introductory rate expires at the documented UTC month boundary. const SONNET_5_STANDARD_PRICING_START_MS = Date.UTC(2026, 8, 1); const SONNET_5_SUPPORTED_REGIONS = new Set(["global", "us", "eu"]); @@ -96,7 +97,16 @@ function buildAnthropicVertexCatalog(region: string, nowMs: number): ModelDefini reasoning: true, input: ["text", "image"], cost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 }, - maxTokens: ANTHROPIC_VERTEX_FABLE_MAX_TOKENS, + maxTokens: ANTHROPIC_VERTEX_CLAUDE_5_MAX_TOKENS, + thinkingLevelMap: { off: "low", minimal: "low", xhigh: "xhigh", max: "max" }, + }), + buildAnthropicVertexModel({ + id: "claude-mythos-5", + name: "Claude Mythos 5", + reasoning: true, + input: ["text", "image"], + cost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 }, + maxTokens: ANTHROPIC_VERTEX_CLAUDE_5_MAX_TOKENS, thinkingLevelMap: { off: "low", minimal: "low", xhigh: "xhigh", max: "max" }, }), ...sonnet5, @@ -137,15 +147,16 @@ export function normalizeAnthropicVertexResolvedModel( ): ProviderRuntimeModel | undefined { const ref = { id: modelId, params: model.params }; const fable5 = resolveClaudeFable5ModelIdentity(ref) !== undefined; + const mythos5 = resolveClaudeMythos5ModelIdentity(ref) !== undefined; const sonnet5 = resolveClaudeSonnet5ModelIdentity(ref) !== undefined; - if (!fable5 && !sonnet5) { + if (!fable5 && !mythos5 && !sonnet5) { return undefined; } const input: ProviderRuntimeModel["input"] = model.input.includes("image") ? model.input : [...model.input, "image"]; const nativeThinkingLevelMap = { - ...(fable5 ? { off: "low" as const, minimal: "low" as const } : {}), + ...(fable5 || mythos5 ? { off: "low" as const, minimal: "low" as const } : {}), xhigh: "xhigh", max: "max", }; @@ -156,7 +167,8 @@ export function normalizeAnthropicVertexResolvedModel( const nativeThinkingLevelsMatch = model.thinkingLevelMap?.xhigh === "xhigh" && model.thinkingLevelMap.max === "max" && - (!fable5 || (model.thinkingLevelMap.off === "low" && model.thinkingLevelMap.minimal === "low")); + (!(fable5 || mythos5) || + (model.thinkingLevelMap.off === "low" && model.thinkingLevelMap.minimal === "low")); const cost = sonnet5 ? resolveSonnet5Cost(resolveAnthropicVertexClientRegion({ baseUrl: model.baseUrl })) : undefined; @@ -171,7 +183,7 @@ export function normalizeAnthropicVertexResolvedModel( input === model.input && model.contextWindow === ANTHROPIC_VERTEX_DEFAULT_CONTEXT_WINDOW && model.contextTokens === ANTHROPIC_VERTEX_DEFAULT_CONTEXT_WINDOW && - (model.maxTokens ?? 0) >= ANTHROPIC_VERTEX_FABLE_MAX_TOKENS && + (model.maxTokens ?? 0) >= ANTHROPIC_VERTEX_CLAUDE_5_MAX_TOKENS && nativeThinkingLevelsMatch && costMatches ) { @@ -183,7 +195,7 @@ export function normalizeAnthropicVertexResolvedModel( input, contextWindow: ANTHROPIC_VERTEX_DEFAULT_CONTEXT_WINDOW, contextTokens: ANTHROPIC_VERTEX_DEFAULT_CONTEXT_WINDOW, - maxTokens: Math.max(model.maxTokens ?? 0, ANTHROPIC_VERTEX_FABLE_MAX_TOKENS), + maxTokens: Math.max(model.maxTokens ?? 0, ANTHROPIC_VERTEX_CLAUDE_5_MAX_TOKENS), thinkingLevelMap, ...(cost ? { cost } : {}), }; diff --git a/extensions/anthropic-vertex/provider-policy-api.test.ts b/extensions/anthropic-vertex/provider-policy-api.test.ts index 7d2cdcb75f6e..ea69240cf080 100644 --- a/extensions/anthropic-vertex/provider-policy-api.test.ts +++ b/extensions/anthropic-vertex/provider-policy-api.test.ts @@ -32,16 +32,19 @@ describe("anthropic-vertex provider-policy-api", () => { expect(profile?.levels.map((level) => level.id)).not.toContain("xhigh"); }); - it("inherits Claude Fable 5's provider-agnostic thinking contract", () => { - const profile = resolveThinkingProfile({ - provider: "anthropic-vertex", - modelId: "claude-fable-5", - }); + it.each(["claude-fable-5", "claude-mythos-5"])( + "inherits %s's provider-agnostic thinking contract", + (modelId) => { + const profile = resolveThinkingProfile({ + provider: "anthropic-vertex", + modelId, + }); - expect(profile?.defaultLevel).toBe("high"); - expect(profile?.preserveWhenCatalogReasoningFalse).toBe(true); - expect(profile?.levels.map((level) => level.id)).toContain("max"); - }); + expect(profile?.defaultLevel).toBe("high"); + expect(profile?.preserveWhenCatalogReasoningFalse).toBe(true); + expect(profile?.levels.map((level) => level.id)).toContain("max"); + }, + ); it("resolves deployment aliases from canonical model metadata", () => { const profile = resolveThinkingProfile({ diff --git a/extensions/anthropic-vertex/stream-runtime.ts b/extensions/anthropic-vertex/stream-runtime.ts index a51ab34e7f40..48e19d83968a 100644 --- a/extensions/anthropic-vertex/stream-runtime.ts +++ b/extensions/anthropic-vertex/stream-runtime.ts @@ -14,7 +14,9 @@ import { import { resolveClaudeFable5ModelIdentity, resolveClaudeModelIdentity, + resolveClaudeMythos5ModelIdentity, resolveClaudeSonnet5ModelIdentity, + requiresClaudeMandatoryAdaptiveThinking, supportsClaudeAdaptiveThinking, supportsClaudeNativeMaxEffort, supportsClaudeNativeXhighEffort, @@ -60,11 +62,11 @@ function isClaudeSonnet5Model(modelId: string): boolean { } function isClaudeMythos5Model(modelId: string): boolean { - return /(?:^|-)claude-mythos-5(?=$|[^a-z0-9])/.test(resolveClaudeModelIdentity({ id: modelId })); + return resolveClaudeMythos5ModelIdentity({ id: modelId }) !== undefined; } function supportsAdaptiveThinking(modelId: string): boolean { - return supportsClaudeAdaptiveThinking({ id: modelId }) || isClaudeMythos5Model(modelId); + return supportsClaudeAdaptiveThinking({ id: modelId }); } function mapAnthropicAdaptiveEffort( @@ -153,15 +155,14 @@ export function createAnthropicVertexStreamFn( requestedMaxTokens: options?.maxTokens, }); const contractModelId = resolveClaudeModelIdentity(model); - const fable5 = isClaudeFable5Model(contractModelId); const sonnet5 = isClaudeSonnet5Model(contractModelId); - const mandatoryAdaptiveThinking = fable5 || isClaudeMythos5Model(contractModelId); + const mandatoryAdaptiveThinking = requiresClaudeMandatoryAdaptiveThinking({ + id: contractModelId, + }); const requestedReasoning = options?.reasoning; const reasoning = requestedReasoning === "off" && mandatoryAdaptiveThinking - ? fable5 - ? "low" - : "high" + ? "low" : (requestedReasoning ?? (mandatoryAdaptiveThinking || sonnet5 ? "high" : undefined)); const adaptiveThinking = mandatoryAdaptiveThinking || @@ -206,7 +207,7 @@ export function createAnthropicVertexStreamFn( ? budgets[reasoning as keyof typeof budgets] : undefined) ?? 10000; } - } else if (fable5) { + } else if (mandatoryAdaptiveThinking) { opts.thinkingEnabled = true; opts.effort = "high"; } else { diff --git a/extensions/anthropic/index.test.ts b/extensions/anthropic/index.test.ts index f550d03567c5..c6075e239979 100644 --- a/extensions/anthropic/index.test.ts +++ b/extensions/anthropic/index.test.ts @@ -696,6 +696,58 @@ describe("anthropic provider replay hooks", () => { }); }); + it("resolves Claude Mythos 5 with its direct-only mandatory-adaptive contract", async () => { + const provider = await registerSingleProviderPlugin(anthropicPlugin); + const resolved = provider.resolveDynamicModel?.({ + provider: "anthropic", + modelId: "claude-mythos-5", + modelRegistry: createModelRegistry([]), + } as ProviderResolveDynamicModelContext); + + expectFields(resolved, { + provider: "anthropic", + id: "claude-mythos-5", + api: "anthropic-messages", + reasoning: true, + input: ["text", "image"], + cost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 }, + contextWindow: 1_000_000, + contextTokens: 1_000_000, + maxTokens: 128_000, + thinkingLevelMap: { + off: "low", + minimal: "low", + xhigh: "xhigh", + max: "max", + }, + }); + expect(requireRecord(resolved, "Mythos model").mediaInput).toEqual({ + image: { maxSidePx: 2576, preferredSidePx: 2576, tokenMode: "provider" }, + }); + expect( + provider.resolveThinkingProfile?.({ + provider: "anthropic", + modelId: "claude-mythos-5", + } as never)?.defaultLevel, + ).toBe("high"); + expect( + provider.resolveDynamicModel?.({ + provider: "claude-cli", + modelId: "claude-mythos-5", + modelRegistry: createModelRegistry([]), + } as ProviderResolveDynamicModelContext), + ).toBeUndefined(); + expect( + provider.resolveThinkingProfile?.({ + provider: "claude-cli", + modelId: "claude-mythos-5", + } as never), + ).toEqual({ + levels: [{ id: "off" }], + defaultLevel: "off", + }); + }); + it("rolls Claude Sonnet 5 to standard pricing on September 1, 2026", async () => { vi.useFakeTimers(); vi.setSystemTime(Date.UTC(2026, 8, 1)); diff --git a/extensions/anthropic/openclaw.plugin.json b/extensions/anthropic/openclaw.plugin.json index 1a6ceb4a0446..9abc894521ac 100644 --- a/extensions/anthropic/openclaw.plugin.json +++ b/extensions/anthropic/openclaw.plugin.json @@ -110,6 +110,24 @@ "max": "max" } }, + { + "id": "claude-mythos-5", + "name": "Claude Mythos 5", + "reasoning": true, + "input": ["text", "image"], + "mediaInput": { + "image": { "maxSidePx": 2576, "preferredSidePx": 2576, "tokenMode": "provider" } + }, + "cost": { "input": 10, "output": 50, "cacheRead": 1, "cacheWrite": 12.5 }, + "contextWindow": 1000000, + "maxTokens": 128000, + "thinkingLevelMap": { + "off": "low", + "minimal": "low", + "xhigh": "xhigh", + "max": "max" + } + }, { "id": "claude-opus-4-8", "name": "Claude Opus 4.8", @@ -194,6 +212,7 @@ "opus-4.8": "claude-opus-4-8", "opus": "claude-opus-4-8", "opus-4.6": "claude-opus-4-6", + "mythos-5": "claude-mythos-5", "sonnet-5": "claude-sonnet-5", "sonnet": "claude-sonnet-5", "sonnet-4.6": "claude-sonnet-4-6" diff --git a/extensions/anthropic/provider-policy-api.test.ts b/extensions/anthropic/provider-policy-api.test.ts index d8c09ffc681e..e13cc0f8ad8d 100644 --- a/extensions/anthropic/provider-policy-api.test.ts +++ b/extensions/anthropic/provider-policy-api.test.ts @@ -136,36 +136,39 @@ describe("anthropic provider policy public artifact", () => { expect(profile?.defaultLevel).toBe("off"); }); - it("exposes the always-adaptive Claude Fable 5 thinking profile", () => { - const profile = resolveThinkingProfile({ - provider: "anthropic", - modelId: "claude-fable-5", - }); + it.each(["claude-fable-5", "claude-mythos-5"])( + "exposes the mandatory-adaptive %s thinking profile", + (modelId) => { + const profile = resolveThinkingProfile({ + provider: "anthropic", + modelId, + }); - expect(profile).toEqual({ - levels: [ - { id: "off" }, - { id: "minimal" }, - { id: "low" }, - { id: "medium" }, - { id: "high" }, - { id: "xhigh" }, - { id: "adaptive" }, - { id: "max" }, - ], - defaultLevel: "high", - preserveWhenCatalogReasoningFalse: true, - }); - expect( - resolveThinkingProfile({ - provider: "claude-cli", - modelId: "claude-fable-5", - }), - ).toEqual({ - levels: [{ id: "off" }], - defaultLevel: "off", - }); - }); + expect(profile).toEqual({ + levels: [ + { id: "off" }, + { id: "minimal" }, + { id: "low" }, + { id: "medium" }, + { id: "high" }, + { id: "xhigh" }, + { id: "adaptive" }, + { id: "max" }, + ], + defaultLevel: "high", + preserveWhenCatalogReasoningFalse: true, + }); + expect( + resolveThinkingProfile({ + provider: "claude-cli", + modelId, + }), + ).toEqual({ + levels: [{ id: "off" }], + defaultLevel: "off", + }); + }, + ); it("does not return fable-5 off-thinking profile for claude-fable-50 (prefix boundary check)", () => { const profile = resolveThinkingProfile({ @@ -177,6 +180,16 @@ describe("anthropic provider policy public artifact", () => { expect(profile?.defaultLevel).not.toBe("off"); }); + it("preserves the existing Claude CLI Mythos Preview thinking profile", () => { + const profile = resolveThinkingProfile({ + provider: "claude-cli", + modelId: "claude-mythos-preview", + }); + + expect(profile?.defaultLevel).toBe("adaptive"); + expect(profile?.levels.map((level) => level.id)).toContain("max"); + }); + it("exposes native max without xhigh for direct Claude 4.6 routes", () => { for (const provider of ["anthropic", "claude-cli"]) { const profile = resolveThinkingProfile({ diff --git a/extensions/anthropic/provider-policy-api.ts b/extensions/anthropic/provider-policy-api.ts index d5292a16e4fc..b2f36d1da179 100644 --- a/extensions/anthropic/provider-policy-api.ts +++ b/extensions/anthropic/provider-policy-api.ts @@ -3,8 +3,9 @@ * path for config defaults and thinking profiles. */ import { - resolveClaudeFable5ModelIdentity, resolveClaudeModelIdentity, + resolveClaudeFable5ModelIdentity, + resolveClaudeMythos5ModelIdentity, resolveClaudeThinkingProfile, } from "openclaw/plugin-sdk/provider-model-shared"; import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-types"; @@ -40,7 +41,10 @@ export function resolveThinkingProfile(params: { includeNativeMax: true, }); case "claude-cli": - if (resolveClaudeFable5ModelIdentity({ id: contractModelId })) { + if ( + resolveClaudeFable5ModelIdentity({ id: contractModelId }) || + resolveClaudeMythos5ModelIdentity({ id: contractModelId }) + ) { return CLAUDE_CLI_OFF_THINKING_PROFILE; } return resolveClaudeThinkingProfile(contractModelId, undefined, { diff --git a/extensions/anthropic/register.runtime.ts b/extensions/anthropic/register.runtime.ts index 6ff4087791d3..dc828695d069 100644 --- a/extensions/anthropic/register.runtime.ts +++ b/extensions/anthropic/register.runtime.ts @@ -30,6 +30,7 @@ import { type ProviderPlugin, resolveClaudeFable5ModelIdentity, resolveClaudeModelIdentity, + resolveClaudeMythos5ModelIdentity, resolveClaudeSonnet5ModelIdentity, resolveClaudeThinkingProfile, supportsClaudeAdaptiveThinking, @@ -292,7 +293,7 @@ function buildAnthropicForwardCompatModel( if (trimmedModelId !== lower || !matchesAnthropicModernModel(lower)) { return undefined; } - if (isAnthropicFable5Model(lower) && normalizedProvider !== PROVIDER_ID) { + if (isAnthropicMandatoryClaude5Model(lower) && normalizedProvider !== PROVIDER_ID) { return undefined; } const provider = @@ -305,7 +306,7 @@ function buildAnthropicForwardCompatModel( baseUrl: "https://api.anthropic.com", reasoning: true, input: ["text", "image"], - cost: isAnthropicFable5Model(trimmedModelId) + cost: isAnthropicMandatoryClaude5Model(trimmedModelId) ? { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 } : isAnthropicSonnet5Model(trimmedModelId) && provider === PROVIDER_ID ? resolveAnthropicSonnet5Cost() @@ -315,7 +316,15 @@ function buildAnthropicForwardCompatModel( ? ANTHROPIC_MODERN_MAX_OUTPUT_TOKENS : 64_000, ...(supportsClaudeNativeXhighEffort({ id: trimmedModelId }) - ? { thinkingLevelMap: { xhigh: "xhigh", max: "max" } } + ? { + thinkingLevelMap: { + ...(isAnthropicMandatoryClaude5Model(trimmedModelId) + ? { off: "low" as const, minimal: "low" as const } + : {}), + xhigh: "xhigh", + max: "max", + }, + } : supportsAnthropicNativeMaxEffort(trimmedModelId) ? { thinkingLevelMap: { max: "max" } } : {}), @@ -370,19 +379,27 @@ function isAnthropicFable5Model(modelId: string): boolean { return resolveClaudeFable5ModelIdentity({ id: modelId }) !== undefined; } +function isAnthropicMythos5Model(modelId: string): boolean { + return resolveClaudeMythos5ModelIdentity({ id: modelId }) !== undefined; +} + +function isAnthropicMandatoryClaude5Model(modelId: string): boolean { + return isAnthropicFable5Model(modelId) || isAnthropicMythos5Model(modelId); +} + function isAnthropicSonnet5Model(modelId: string): boolean { return resolveClaudeSonnet5ModelIdentity({ id: modelId }) !== undefined; } function resolveAnthropicFixedContextWindow(modelId: string): number | undefined { - if (isAnthropicFable5Model(modelId) || isAnthropicSonnet5Model(modelId)) { + if (isAnthropicMandatoryClaude5Model(modelId) || isAnthropicSonnet5Model(modelId)) { return ANTHROPIC_EXACT_1M_CONTEXT_TOKENS; } return isAnthropicGa1MModel(modelId) ? ANTHROPIC_GA_1M_CONTEXT_TOKENS : undefined; } function isAnthropic128kOutputModel(modelId: string): boolean { - if (isAnthropicFable5Model(modelId) || isAnthropicSonnet5Model(modelId)) { + if (isAnthropicMandatoryClaude5Model(modelId) || isAnthropicSonnet5Model(modelId)) { return true; } return /^claude-opus-4-8(?=$|[^a-z0-9])/.test(resolveClaudeModelIdentity({ id: modelId })); @@ -453,7 +470,7 @@ function applyAnthropicFixedContextWindow(params: { return undefined; } const exactContextWindow = - isAnthropicFable5Model(params.contractModelId) || + isAnthropicMandatoryClaude5Model(params.contractModelId) || isAnthropicSonnet5Model(params.contractModelId); const nextContextWindow = exactContextWindow ? fixedContextWindow @@ -496,8 +513,8 @@ function applyAnthropicThinkingLevelMap(params: { modelId: string; model: ProviderRuntimeModel; }): ProviderRuntimeModel | undefined { - const fable5 = isAnthropicFable5Model(params.modelId); - const nativeXhigh = fable5 || supportsClaudeNativeXhighEffort({ id: params.modelId }); + const mandatoryClaude5 = isAnthropicMandatoryClaude5Model(params.modelId); + const nativeXhigh = mandatoryClaude5 || supportsClaudeNativeXhighEffort({ id: params.modelId }); if (!supportsAnthropicNativeMaxEffort(params.modelId)) { return undefined; } @@ -505,7 +522,7 @@ function applyAnthropicThinkingLevelMap(params: { const nativeDefaults = isAnthropicMythosPreviewModel(params.modelId) ? { max: "max" as const } : { - ...(fable5 ? { off: "low" as const, minimal: "low" as const } : {}), + ...(mandatoryClaude5 ? { off: "low" as const, minimal: "low" as const } : {}), xhigh: nativeXhigh ? ("xhigh" as const) : null, max: "max" as const, }; @@ -594,13 +611,14 @@ function normalizeAnthropicResolvedModel( params: ctx.model.params, }); if ( - isAnthropicFable5Model(contractModelId) && + isAnthropicMandatoryClaude5Model(contractModelId) && normalizeLowercaseStringOrEmpty(ctx.provider) !== PROVIDER_ID ) { return undefined; } const contractModel = - (isAnthropicFable5Model(contractModelId) || isAnthropicSonnet5Model(contractModelId)) && + (isAnthropicMandatoryClaude5Model(contractModelId) || + isAnthropicSonnet5Model(contractModelId)) && !ctx.model.reasoning ? { ...ctx.model, reasoning: true } : ctx.model; @@ -877,12 +895,12 @@ export function buildAnthropicProvider(): ProviderPlugin { ...NATIVE_ANTHROPIC_REPLAY_HOOKS, isModernModelRef: ({ provider, modelId }) => matchesAnthropicModernModel(modelId) && - (!isAnthropicFable5Model(modelId) || + (!isAnthropicMandatoryClaude5Model(modelId) || normalizeLowercaseStringOrEmpty(provider) === PROVIDER_ID), resolveReasoningOutputMode: () => "native", resolveThinkingProfile: ({ provider, modelId, params }) => { const contractModelId = resolveClaudeModelIdentity({ id: modelId, params }); - return isAnthropicFable5Model(contractModelId) && + return isAnthropicMandatoryClaude5Model(contractModelId) && normalizeLowercaseStringOrEmpty(provider) !== PROVIDER_ID ? CLAUDE_CLI_OFF_THINKING_PROFILE : resolveClaudeThinkingProfile(contractModelId, undefined, { diff --git a/packages/ai/src/model-utils.ts b/packages/ai/src/model-utils.ts index c8a4598f051a..2413877f911c 100644 --- a/packages/ai/src/model-utils.ts +++ b/packages/ai/src/model-utils.ts @@ -1,7 +1,7 @@ // Provides model selection, usage, and thinking-level utility helpers. import { - resolveClaudeFable5ModelIdentity, resolveClaudeNativeThinkingLevelMap, + requiresClaudeMandatoryAdaptiveThinking, } from "@openclaw/llm-core"; import type { Api, Model, ModelThinkingLevel, Usage } from "./types.js"; @@ -36,9 +36,9 @@ function resolveThinkingLevelMap(model: Model) { export function getSupportedThinkingLevels( model: Model, ): ModelThinkingLevel[] { - const fableContract = - model.api === "anthropic-messages" && resolveClaudeFable5ModelIdentity(model) !== undefined; - if (!model.reasoning && !fableContract) { + const mandatoryAdaptiveContract = + model.api === "anthropic-messages" && requiresClaudeMandatoryAdaptiveThinking(model); + if (!model.reasoning && !mandatoryAdaptiveContract) { return ["off"]; } const thinkingLevelMap = resolveThinkingLevelMap(model); diff --git a/packages/ai/src/providers/anthropic-model-contract.ts b/packages/ai/src/providers/anthropic-model-contract.ts index 4860e1f00150..bb754f8ce67c 100644 --- a/packages/ai/src/providers/anthropic-model-contract.ts +++ b/packages/ai/src/providers/anthropic-model-contract.ts @@ -1,14 +1,19 @@ // Model-bound thinking cannot be exposed or replayed after a model switch. import { + requiresClaudeDefaultSampling, + requiresClaudeMandatoryAdaptiveThinking, resolveClaudeFable5ModelIdentity, - resolveClaudeModelIdentity, + resolveClaudeMythos5ModelIdentity, resolveClaudeSonnet5ModelIdentity, } from "@openclaw/llm-core"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import type { Context, Model } from "../types.js"; export { + requiresClaudeDefaultSampling, + requiresClaudeMandatoryAdaptiveThinking, resolveClaudeFable5ModelIdentity, resolveClaudeModelIdentity, + resolveClaudeMythos5ModelIdentity, resolveClaudeNativeThinkingLevelMap, resolveClaudeSonnet5ModelIdentity, supportsClaudeAdaptiveThinking, @@ -66,6 +71,7 @@ export function usesClaudeStreamingRefusalContract(model: { } return ( resolveClaudeFable5ModelIdentity(model) !== undefined || + resolveClaudeMythos5ModelIdentity(model) !== undefined || resolveClaudeSonnet5ModelIdentity(model) !== undefined ); } @@ -78,11 +84,7 @@ export function requiresClaudeAdaptiveThinking(model: { if (normalizeApi(model.api) !== "anthropic-messages") { return false; } - const modelId = resolveClaudeModelIdentity(model); - return ( - resolveClaudeFable5ModelIdentity(model) !== undefined || - /(?:^|-)claude-mythos-preview(?=$|[^a-z0-9])/.test(modelId) - ); + return requiresClaudeMandatoryAdaptiveThinking(model); } /** Return whether omitted thinking should default to adaptive/high. */ @@ -120,14 +122,46 @@ export function prepareClaudeSonnet5RequestContext(model: Model, context: Contex : { ...context, messages: context.messages.slice(0, end) }; } -function resolveReplayFableIdentity(ref: ReplayModelRef): string | undefined { +export function applyClaudeRequestContract( + params: Record, + model: { + id?: string; + params?: Record; + api?: string; + }, +): void { + if (normalizeApi(model.api) !== "anthropic-messages") { + return; + } + const sonnet5 = resolveClaudeSonnet5ModelIdentity(model) !== undefined; + if (!requiresClaudeDefaultSampling(model) && !sonnet5) { + return; + } + delete params.temperature; + delete params.top_p; + delete params.top_k; + if (sonnet5) { + delete params.service_tier; + } +} + +function resolveReplayModelBoundIdentity(ref: ReplayModelRef): string | undefined { if (normalizeApi(ref.api) !== "anthropic-messages") { return undefined; } - if (hasConcreteResponseModel(ref)) { - return resolveClaudeFable5ModelIdentity({ id: ref.responseModelId }); + const modelRef = hasConcreteResponseModel(ref) + ? { id: ref.responseModelId } + : { id: ref.modelId, params: ref.modelParams }; + const fableIdentity = resolveClaudeFable5ModelIdentity(modelRef); + if (fableIdentity) { + return `fable:${fableIdentity}`; } - return resolveClaudeFable5ModelIdentity({ id: ref.modelId, params: ref.modelParams }); + const mythosIdentity = resolveClaudeMythos5ModelIdentity(modelRef); + if (mythosIdentity) { + return `mythos:${mythosIdentity}`; + } + const sonnetIdentity = resolveClaudeSonnet5ModelIdentity(modelRef); + return sonnetIdentity ? `sonnet:${sonnetIdentity}` : undefined; } export function resolveModelBoundThinkingReplayMode(params: { @@ -136,8 +170,8 @@ export function resolveModelBoundThinkingReplayMode(params: { }): "default" | "preserve" | "drop" { const sourceApi = normalizeApi(params.source.api); const targetApi = normalizeApi(params.target.api); - const sourceIdentity = resolveReplayFableIdentity(params.source); - const targetIdentity = resolveReplayFableIdentity(params.target); + const sourceIdentity = resolveReplayModelBoundIdentity(params.source); + const targetIdentity = resolveReplayModelBoundIdentity(params.target); const sameRoute = normalizeLowercaseStringOrEmpty(params.source.provider) === normalizeLowercaseStringOrEmpty(params.target.provider) && diff --git a/packages/ai/src/providers/anthropic.test.ts b/packages/ai/src/providers/anthropic.test.ts index 59e2c8a260ea..d8dfec19ad99 100644 --- a/packages/ai/src/providers/anthropic.test.ts +++ b/packages/ai/src/providers/anthropic.test.ts @@ -862,6 +862,8 @@ describe("Anthropic provider", () => { it.each([ ["claude-fable-5", "Claude Fable 5", "anthropic", "sk-ant-provider"], + ["claude-mythos-5", "Claude Mythos 5", "anthropic", "sk-ant-provider"], + ["claude-mythos-5", "Claude Mythos 5", "anthropic-vertex", "vertex-token"], ["claude-sonnet-5", "Claude Sonnet 5", "anthropic", "sk-ant-provider"], ["claude-sonnet-5", "Claude Sonnet 5", "anthropic-vertex", "vertex-token"], ])("surfaces structured %s streaming refusals for %s", async (id, name, provider, apiKey) => { @@ -1424,6 +1426,47 @@ describe("Anthropic provider", () => { expect(capturedPayload).not.toHaveProperty("temperature"); }); + it("uses mandatory adaptive thinking and default sampling for Claude Mythos 5", async () => { + let capturedPayload: unknown; + const stream = streamSimpleAnthropic( + makeAnthropicModel({ + id: "prod-mythos", + name: "Production Claude", + provider: "microsoft-foundry", + params: { canonicalModelId: "claude-mythos-5" }, + reasoning: false, + baseUrl: "https://example.services.ai.azure.com/anthropic", + maxTokens: 128_000, + }), + { + messages: [{ role: "user", content: "hello", timestamp: 0 }], + }, + { + apiKey: "sk-ant-provider", + reasoning: "off", + temperature: 0.2, + onPayload: (payload) => { + capturedPayload = { + ...(payload as Record), + top_p: 0.9, + top_k: 40, + }; + return capturedPayload; + }, + }, + ); + + await stream.result(); + + expect(capturedPayload).toMatchObject({ + thinking: { type: "adaptive", display: "summarized" }, + output_config: { effort: "low" }, + }); + expect(capturedPayload).not.toHaveProperty("temperature"); + expect(capturedPayload).not.toHaveProperty("top_p"); + expect(capturedPayload).not.toHaveProperty("top_k"); + }); + it("preserves native max effort for Claude Mythos Preview", async () => { let capturedPayload: unknown; const stream = streamSimpleAnthropic( @@ -1546,6 +1589,41 @@ describe("Anthropic provider", () => { expect((capturedPayload as { output_config?: unknown }).output_config).toBeUndefined(); }); + it.each(["claude-opus-4-8", "claude-mythos-preview"])( + "restores default sampling for %s after payload hooks", + async (modelId) => { + let capturedPayload: unknown; + const stream = streamSimpleAnthropic( + makeAnthropicModel({ + id: modelId, + name: modelId, + maxTokens: 128_000, + }), + { messages: [{ role: "user", content: "hello", timestamp: 0 }] }, + { + apiKey: "sk-ant-provider", + reasoning: "high", + temperature: 0.2, + onPayload: (payload) => { + capturedPayload = { + ...(payload as Record), + temperature: 0.2, + top_p: 0.9, + top_k: 40, + }; + return capturedPayload; + }, + }, + ); + + await stream.result(); + + expect(capturedPayload).not.toHaveProperty("temperature"); + expect(capturedPayload).not.toHaveProperty("top_p"); + expect(capturedPayload).not.toHaveProperty("top_k"); + }, + ); + it.each([ { id: "prod-primary", diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 2e183271d313..a1614478ae76 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -46,6 +46,7 @@ import { usesFoundryBearerAuth, } from "./anthropic-auth-headers.js"; import { + applyClaudeRequestContract, prepareClaudeSonnet5RequestContext, resolveClaudeNativeThinkingLevelMap, resolveClaudeSonnet5ModelIdentity, @@ -575,6 +576,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOpti if (nextParams !== undefined) { params = nextParams as MessageCreateParamsStreaming; } + applyClaudeRequestContract(params as unknown as Record, model); const requestOptions = { ...(options?.signal ? { signal: options.signal } : {}), ...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}), @@ -1014,14 +1016,18 @@ export const streamSimpleAnthropic: StreamFunction< toolChoice: options?.toolChoice, }; const mandatoryAdaptiveThinking = requiresClaudeAdaptiveThinking(model); - const fable5 = usesClaudeFable5MessagesContract(model); if (options?.reasoning === "off" && !mandatoryAdaptiveThinking) { return streamAnthropic(model, context, { ...base, thinkingEnabled: false, } satisfies AnthropicOptions); } - const reasoning = options?.reasoning === "off" ? (fable5 ? "low" : "high") : options?.reasoning; + const reasoning = + options?.reasoning === "off" + ? mandatoryAdaptiveThinking + ? "low" + : "high" + : options?.reasoning; if (resolveClaudeSonnet5ModelIdentity(model)) { return streamAnthropic(model, context, { ...base, @@ -1239,8 +1245,8 @@ function buildParams( params: MessageCreateParamsStreaming; toolProjection?: AnthropicToolProjection; } { - const fable5 = usesClaudeFable5MessagesContract(model); - const replayThinkingEnabled = fable5 || options?.thinkingEnabled === true; + const mandatoryAdaptiveThinking = requiresClaudeAdaptiveThinking(model); + const replayThinkingEnabled = mandatoryAdaptiveThinking || options?.thinkingEnabled === true; const { cacheControl } = getCacheControl(model, options?.cacheRetention); const system = buildAnthropicSystemBlocks(context.systemPrompt, isOAuthTokenResult, cacheControl); const compat = context.tools ? getAnthropicCompat(model) : undefined; @@ -1304,18 +1310,18 @@ function buildParams( params.tools = tools; } - // Configure thinking mode: always-on adaptive (Fable 5), adaptive (Opus - // 4.6+ and Sonnet 4.6), + // Configure thinking mode: always-on adaptive (Fable 5 and Mythos 5), + // adaptive (Opus 4.6+ and Sonnet 4.6), // budget-based (older models), or explicitly disabled. - if (fable5 || model.reasoning || supportsAdaptiveThinking(model)) { - if (fable5 || options?.thinkingEnabled) { + if (mandatoryAdaptiveThinking || model.reasoning || supportsAdaptiveThinking(model)) { + if (mandatoryAdaptiveThinking || options?.thinkingEnabled) { // Default to "summarized" so Opus 4.7+ and Mythos Preview behave like // older Claude 4 models (whose API default is also "summarized"). const display: AnthropicThinkingDisplay = options?.thinkingDisplay ?? "summarized"; if (supportsAdaptiveThinking(model)) { // Adaptive thinking: Claude decides when and how much to think. params.thinking = { type: "adaptive", display }; - const effort = options?.effort ?? (fable5 ? "high" : undefined); + const effort = options?.effort ?? (mandatoryAdaptiveThinking ? "high" : undefined); if (effort) { // The Anthropic SDK types can lag newly supported effort values such as "xhigh". params.output_config = diff --git a/packages/llm-core/src/model-contracts/anthropic.ts b/packages/llm-core/src/model-contracts/anthropic.ts index bd9fa7b0c653..f66337034ad0 100644 --- a/packages/llm-core/src/model-contracts/anthropic.ts +++ b/packages/llm-core/src/model-contracts/anthropic.ts @@ -65,6 +65,26 @@ export function resolveClaudeFable5ModelIdentity(ref: ClaudeModelRef): string | return normalized.slice((match.index ?? 0) + (match[0].startsWith("-") ? 1 : 0)); } +/** Resolve Claude Mythos 5 through direct ids, cloud ids, or deployment metadata. */ +export function resolveClaudeMythos5ModelIdentity(ref: ClaudeModelRef): string | undefined { + const normalized = resolveClaudeModelIdentity(ref); + const match = /(?:^|-)claude-mythos-5(?=$|[^a-z0-9])/.exec(normalized); + if (!match) { + return undefined; + } + return normalized.slice((match.index ?? 0) + (match[0].startsWith("-") ? 1 : 0)); +} + +/** Return whether a Claude model requires adaptive thinking instead of manual budgets. */ +export function requiresClaudeMandatoryAdaptiveThinking(ref: ClaudeModelRef): boolean { + const modelId = resolveClaudeModelIdentity(ref); + return ( + resolveClaudeFable5ModelIdentity(ref) !== undefined || + resolveClaudeMythos5ModelIdentity(ref) !== undefined || + /(?:^|-)claude-mythos-preview(?=$|[^a-z0-9])/.test(modelId) + ); +} + /** Resolve Claude Sonnet 5 through direct ids, cloud ids, or deployment metadata. */ export function resolveClaudeSonnet5ModelIdentity(ref: ClaudeModelRef): string | undefined { const normalized = resolveClaudeModelIdentity(ref); @@ -78,7 +98,7 @@ export function resolveClaudeSonnet5ModelIdentity(ref: ClaudeModelRef): string | /** Return whether a Claude model supports adaptive thinking. */ export function supportsClaudeAdaptiveThinking(ref: ClaudeModelRef): boolean { const modelId = resolveClaudeModelIdentity(ref); - return /(?:^|-)claude-(?:fable-5|mythos-preview|opus-4-(?:6|7|8)|sonnet-(?:5|4-6))(?=$|[^a-z0-9])/.test( + return /(?:^|-)claude-(?:fable-5|mythos-(?:5|preview)|opus-4-(?:6|7|8)|sonnet-(?:5|4-6))(?=$|[^a-z0-9])/.test( modelId, ); } @@ -86,13 +106,24 @@ export function supportsClaudeAdaptiveThinking(ref: ClaudeModelRef): boolean { /** Return whether a Claude model supports native max effort. */ export function supportsClaudeNativeMaxEffort(ref: ClaudeModelRef): boolean { const modelId = resolveClaudeModelIdentity(ref); - return /(?:^|-)claude-(?:fable-5|opus-4-(?:6|7|8)|sonnet-(?:5|4-6))(?=$|[^a-z0-9])/.test(modelId); + return /(?:^|-)claude-(?:fable-5|mythos-5|opus-4-(?:6|7|8)|sonnet-(?:5|4-6))(?=$|[^a-z0-9])/.test( + modelId, + ); } /** Return whether a Claude model supports native xhigh effort. */ export function supportsClaudeNativeXhighEffort(ref: ClaudeModelRef): boolean { const modelId = resolveClaudeModelIdentity(ref); - return /(?:^|-)claude-(?:fable-5|opus-4-(?:7|8)|sonnet-5)(?=$|[^a-z0-9])/.test(modelId); + return /(?:^|-)claude-(?:fable-5|mythos-5|opus-4-(?:7|8)|sonnet-5)(?=$|[^a-z0-9])/.test(modelId); +} + +/** Return whether a Claude model rejects caller-selected sampling parameters. */ +export function requiresClaudeDefaultSampling(ref: ClaudeModelRef): boolean { + const modelId = resolveClaudeModelIdentity(ref); + return ( + supportsClaudeNativeXhighEffort(ref) || + /(?:^|-)claude-mythos-preview(?=$|[^a-z0-9])/.test(modelId) + ); } /** diff --git a/src/agents/anthropic-transport-stream.test.ts b/src/agents/anthropic-transport-stream.test.ts index 3e61b3c688ad..db5df8a76a49 100644 --- a/src/agents/anthropic-transport-stream.test.ts +++ b/src/agents/anthropic-transport-stream.test.ts @@ -3519,6 +3519,42 @@ describe("anthropic transport stream", () => { expect(result.responseModel).toBe("claude-fable-5"); }); + it("uses mandatory adaptive thinking and default sampling for Claude Mythos 5 transport runs", async () => { + const model = makeAnthropicTransportModel({ + id: "prod-mythos", + name: "Production Claude", + provider: "microsoft-foundry", + params: { canonicalModelId: "claude-mythos-5" }, + reasoning: false, + baseUrl: "https://example.services.ai.azure.com/anthropic", + maxTokens: 128_000, + }); + + await runTransportStream( + model, + { + messages: [{ role: "user", content: "Think." }], + } as AnthropicStreamContext, + { + apiKey: "sk-ant-api", + reasoning: "off", + temperature: 0.2, + onPayload: (payload) => ({ + ...(payload as Record), + top_p: 0.9, + top_k: 40, + }), + } as AnthropicStreamOptions, + ); + + const payload = latestAnthropicRequest().payload; + expect(payload.thinking).toEqual({ type: "adaptive", display: "summarized" }); + expect(payload.output_config).toEqual({ effort: "low" }); + expect(payload).not.toHaveProperty("temperature"); + expect(payload).not.toHaveProperty("top_p"); + expect(payload).not.toHaveProperty("top_k"); + }); + it("uses adaptive thinking for canonical Claude Mythos Preview transport aliases", async () => { const model = makeAnthropicTransportModel({ id: "prod-mythos-preview", @@ -3574,6 +3610,36 @@ describe("anthropic transport stream", () => { expect(payload.output_config).toEqual({ effort: "high" }); }); + it.each(["claude-opus-4-8", "claude-mythos-preview"])( + "restores default sampling for %s transport requests after payload hooks", + async (modelId) => { + await runTransportStream( + makeAnthropicTransportModel({ + id: modelId, + name: modelId, + maxTokens: 128_000, + }), + { messages: [{ role: "user", content: "hello" }] } as AnthropicStreamContext, + { + apiKey: "sk-ant-api", + reasoning: "high", + temperature: 0.2, + onPayload: (payload) => ({ + ...(payload as Record), + temperature: 0.2, + top_p: 0.9, + top_k: 40, + }), + } as AnthropicStreamOptions, + ); + + const payload = latestAnthropicRequest().payload; + expect(payload).not.toHaveProperty("temperature"); + expect(payload).not.toHaveProperty("top_p"); + expect(payload).not.toHaveProperty("top_k"); + }, + ); + it("maps Claude Fable 5 transport thinking levels to adaptive effort", async () => { const model = makeAnthropicTransportModel({ id: "claude-fable-5", diff --git a/src/agents/anthropic-transport-stream.ts b/src/agents/anthropic-transport-stream.ts index a79723b0ebe4..d37da6c723be 100644 --- a/src/agents/anthropic-transport-stream.ts +++ b/src/agents/anthropic-transport-stream.ts @@ -2,6 +2,7 @@ import { ANTHROPIC_OMITTED_REASONING_TEXT, ANTHROPIC_SERVER_SIDE_FALLBACK_BETA, CLAUDE_FABLE_5_FALLBACK_MODEL_COST, + applyClaudeRequestContract, applyAnthropicFallbackBoundary, buildAnthropicServerSideFallbacks, defaultsClaudeAdaptiveThinking, @@ -979,8 +980,8 @@ function buildAnthropicParams( params: Record; toolProjection?: AnthropicToolProjection; } { - const fable5 = usesClaudeFable5MessagesContract(model); - const replayThinkingEnabled = fable5 || options?.thinkingEnabled === true; + const mandatoryAdaptiveThinking = requiresClaudeAdaptiveThinking(model); + const replayThinkingEnabled = mandatoryAdaptiveThinking || options?.thinkingEnabled === true; const maxTokens = resolveAnthropicMessagesMaxTokens({ modelMaxTokens: model.maxTokens, requestedMaxTokens: options?.maxTokens, @@ -1060,8 +1061,8 @@ function buildAnthropicParams( params.tools = convertedTools.tools; } } - if (fable5 || model.reasoning || supportsAdaptiveThinking(model)) { - if (fable5 || options?.thinkingEnabled) { + if (mandatoryAdaptiveThinking || model.reasoning || supportsAdaptiveThinking(model)) { + if (mandatoryAdaptiveThinking || options?.thinkingEnabled) { if (supportsAdaptiveThinking(model)) { // Default display to "summarized" so Opus 4.7+/Fable 5 return a thinking // summary like older Claude 4 models — mirrors the provider path @@ -1070,7 +1071,7 @@ function buildAnthropicParams( // blank (the live agent transport previously sent this for opus-4-8). const display: AnthropicThinkingDisplay = options?.thinkingDisplay ?? "summarized"; params.thinking = { type: "adaptive", display }; - const effort = options?.effort ?? (fable5 ? "high" : undefined); + const effort = options?.effort ?? (mandatoryAdaptiveThinking ? "high" : undefined); if (effort) { params.output_config = { effort }; } @@ -1121,13 +1122,8 @@ function resolveAnthropicTransportOptions( const reasoningModelMaxTokens = resolvePositiveAnthropicMaxTokens(model.maxTokens) ?? baseMaxTokens; const mandatoryAdaptiveThinking = requiresClaudeAdaptiveThinking(model); - const fable5 = usesClaudeFable5MessagesContract(model); const reasoning = - options?.reasoning === "off" && mandatoryAdaptiveThinking - ? fable5 - ? "low" - : "high" - : options?.reasoning; + options?.reasoning === "off" && mandatoryAdaptiveThinking ? "low" : options?.reasoning; const resolved: AnthropicTransportOptions = { temperature: options?.temperature, stop: options?.stop, @@ -1229,6 +1225,7 @@ export function createAnthropicMessagesTransportStreamFn(): StreamFn { if (nextParams !== undefined) { params = nextParams as Record; } + applyClaudeRequestContract(params, model); const anthropicStream = client.messages.stream( { ...params, stream: true }, transportOptions.signal ? { signal: transportOptions.signal } : undefined, diff --git a/src/agents/context-resolution.ts b/src/agents/context-resolution.ts index 87aab9ba747e..28180dfefbd1 100644 --- a/src/agents/context-resolution.ts +++ b/src/agents/context-resolution.ts @@ -44,6 +44,7 @@ const ANTHROPIC_GA_1M_MODEL_PREFIXES = [ export const ANTHROPIC_CONTEXT_1M_TOKENS = 1_048_576; export const ANTHROPIC_VERTEX_CONTEXT_1M_TOKENS = 1_000_000; export const ANTHROPIC_FABLE_CONTEXT_TOKENS = 1_000_000; +export const ANTHROPIC_MYTHOS_5_CONTEXT_TOKENS = 1_000_000; export const ANTHROPIC_SONNET_5_CONTEXT_TOKENS = 1_000_000; type ConfiguredContextTokens = { @@ -160,6 +161,12 @@ export function resolveAnthropicFixedContextWindow( ) { return ANTHROPIC_FABLE_CONTEXT_TOKENS; } + if ( + (provider === "anthropic" || provider === "anthropic-vertex") && + /^claude-mythos-5(?=$|[^a-z0-9])/.test(modelId) + ) { + return ANTHROPIC_MYTHOS_5_CONTEXT_TOKENS; + } if (resolveClaudeSonnet5ModelIdentity({ id: modelId })) { return ANTHROPIC_SONNET_5_CONTEXT_TOKENS; } diff --git a/src/agents/context.test.ts b/src/agents/context.test.ts index 1face6c8b754..74a14b029414 100644 --- a/src/agents/context.test.ts +++ b/src/agents/context.test.ts @@ -11,6 +11,7 @@ import { import { ANTHROPIC_CONTEXT_1M_TOKENS, ANTHROPIC_FABLE_CONTEXT_TOKENS, + ANTHROPIC_MYTHOS_5_CONTEXT_TOKENS, ANTHROPIC_SONNET_5_CONTEXT_TOKENS, ANTHROPIC_VERTEX_CONTEXT_1M_TOKENS, applyConfiguredContextWindows, @@ -419,6 +420,8 @@ describe("resolveContextTokensForModel", () => { it.each([ ["anthropic", "claude-fable-5", ANTHROPIC_FABLE_CONTEXT_TOKENS], ["anthropic-vertex", "claude-fable-5", ANTHROPIC_FABLE_CONTEXT_TOKENS], + ["anthropic", "claude-mythos-5", ANTHROPIC_MYTHOS_5_CONTEXT_TOKENS], + ["anthropic-vertex", "claude-mythos-5", ANTHROPIC_MYTHOS_5_CONTEXT_TOKENS], ["anthropic", "claude-sonnet-5", ANTHROPIC_SONNET_5_CONTEXT_TOKENS], ["anthropic-vertex", "claude-sonnet-5", ANTHROPIC_SONNET_5_CONTEXT_TOKENS], ["claude-cli", "claude-sonnet-5", ANTHROPIC_SONNET_5_CONTEXT_TOKENS], @@ -449,6 +452,17 @@ describe("resolveContextTokensForModel", () => { expect(result).toBe(200_000); }); + it("does not give mythos-5 context window to claude-mythos-50", () => { + const result = resolveContextTokensForModel({ + provider: "anthropic", + model: "claude-mythos-50", + fallbackContextTokens: 200_000, + allowAsyncLoad: false, + }); + + expect(result).toBe(200_000); + }); + it.each([ ["anthropic", "claude-fable-5"], ["anthropic-vertex", "claude-fable-5"], @@ -545,6 +559,11 @@ describe("resolveContextTokensForModel", () => { it.each([ ["anthropic", "claude-fable-5", ANTHROPIC_FABLE_CONTEXT_TOKENS], ["anthropic-vertex", "claude-fable-5", ANTHROPIC_FABLE_CONTEXT_TOKENS], + ["anthropic", "claude-mythos-5", ANTHROPIC_MYTHOS_5_CONTEXT_TOKENS], + ["anthropic-vertex", "claude-mythos-5", ANTHROPIC_MYTHOS_5_CONTEXT_TOKENS], + ["anthropic", "claude-sonnet-5", ANTHROPIC_SONNET_5_CONTEXT_TOKENS], + ["anthropic-vertex", "claude-sonnet-5", ANTHROPIC_SONNET_5_CONTEXT_TOKENS], + ["claude-cli", "claude-sonnet-5", ANTHROPIC_SONNET_5_CONTEXT_TOKENS], ["anthropic", "claude-sonnet-4-6", ANTHROPIC_CONTEXT_1M_TOKENS], ["anthropic-vertex", "claude-sonnet-4-6", ANTHROPIC_VERTEX_CONTEXT_1M_TOKENS], ])( diff --git a/src/agents/context.ts b/src/agents/context.ts index 7ee8fb49d6e3..4784eb29d7b4 100644 --- a/src/agents/context.ts +++ b/src/agents/context.ts @@ -30,6 +30,7 @@ import { normalizeProviderId } from "./model-selection.js"; export { ANTHROPIC_CONTEXT_1M_TOKENS, ANTHROPIC_FABLE_CONTEXT_TOKENS, + ANTHROPIC_MYTHOS_5_CONTEXT_TOKENS, ANTHROPIC_SONNET_5_CONTEXT_TOKENS, ANTHROPIC_VERTEX_CONTEXT_1M_TOKENS, } from "./context-resolution.js"; diff --git a/src/agents/transport-message-transform.test.ts b/src/agents/transport-message-transform.test.ts index e9f69ae24392..8233c02e1134 100644 --- a/src/agents/transport-message-transform.test.ts +++ b/src/agents/transport-message-transform.test.ts @@ -60,6 +60,18 @@ describe("transformTransportMessages synthetic tool-result policy", () => { source: { provider: "anthropic", model: "claude-fable-5" }, target: { provider: "anthropic-vertex", model: "claude-opus-4-8" }, }, + { + source: { provider: "anthropic", model: "claude-mythos-5" }, + target: { provider: "anthropic", model: "claude-opus-4-8" }, + }, + { + source: { provider: "anthropic", model: "claude-fable-5" }, + target: { provider: "anthropic-vertex", model: "claude-mythos-5" }, + }, + { + source: { provider: "anthropic", model: "claude-mythos-5" }, + target: { provider: "anthropic", model: "claude-fable-5" }, + }, { source: { provider: "anthropic", model: "claude-sonnet-4-6" }, target: { provider: "anthropic", model: "claude-fable-5" }, @@ -100,7 +112,7 @@ describe("transformTransportMessages synthetic tool-result policy", () => { canonicalModelId: "claude-fable-5", }, }, - ])("drops model-bound thinking for Fable switches", ({ source, target }) => { + ])("drops model-bound thinking for Fable/Mythos switches", ({ source, target }) => { const result = transformTransportMessages( [ { @@ -185,8 +197,17 @@ describe("transformTransportMessages synthetic tool-result policy", () => { targetModel: "prod-primary", targetCanonicalModelId: "claude-fable-5", }, + { + sourceProvider: "anthropic", + sourceModel: "claude-mythos-5", + sourceResponseModel: undefined, + targetProvider: "anthropic-vertex", + targetApi: "anthropic-messages" as const, + targetModel: "claude-mythos-5", + targetCanonicalModelId: undefined, + }, ])( - "preserves Fable thinking across compatible Anthropic transports", + "preserves Fable/Mythos thinking across compatible Anthropic transports", ({ sourceProvider, sourceModel, diff --git a/src/plugin-sdk/provider-model-shared.test.ts b/src/plugin-sdk/provider-model-shared.test.ts index 101c5fc6a787..bcaf5986b7b4 100644 --- a/src/plugin-sdk/provider-model-shared.test.ts +++ b/src/plugin-sdk/provider-model-shared.test.ts @@ -9,8 +9,10 @@ import { OPENAI_COMPATIBLE_REPLAY_HOOKS, PASSTHROUGH_GEMINI_REPLAY_HOOKS, resolveClaudeFable5ModelIdentity, + resolveClaudeMythos5ModelIdentity, resolveClaudeSonnet5ModelIdentity, resolveClaudeThinkingProfile, + requiresClaudeDefaultSampling, supportsClaudeAdaptiveThinking, supportsClaudeNativeMaxEffort, supportsClaudeNativeXhighEffort, @@ -51,9 +53,24 @@ describe("Claude model contracts", () => { ); }); + it("recognizes Claude Mythos 5 as mandatory adaptive with native max effort", () => { + expect(resolveClaudeMythos5ModelIdentity({ id: "us.anthropic.claude-mythos-5-v1:0" })).toBe( + "claude-mythos-5-v1:0", + ); + expect(supportsClaudeAdaptiveThinking({ id: "claude-mythos-5" })).toBe(true); + expect(supportsClaudeNativeMaxEffort({ id: "claude-mythos-5" })).toBe(true); + expect(supportsClaudeNativeXhighEffort({ id: "anthropic.claude-mythos-5" })).toBe(true); + expect(requiresClaudeDefaultSampling({ id: "claude-mythos-5" })).toBe(true); + }); + it("does not classify later numeric model versions as supported aliases", () => { expect(supportsClaudeAdaptiveThinking({ id: "claude-sonnet-4-60" })).toBe(false); + expect(supportsClaudeAdaptiveThinking({ id: "claude-sonnet-50" })).toBe(false); + expect(supportsClaudeAdaptiveThinking({ id: "claude-mythos-50" })).toBe(false); expect(supportsClaudeNativeXhighEffort({ id: "claude-opus-4-80" })).toBe(false); + expect(requiresClaudeDefaultSampling({ id: "claude-opus-4-8" })).toBe(true); + expect(requiresClaudeDefaultSampling({ id: "claude-mythos-preview" })).toBe(true); + expect(requiresClaudeDefaultSampling({ id: "claude-opus-4-6" })).toBe(false); expect(readLevelIds(resolveClaudeThinkingProfile("claude-opus-4-80"))).toEqual([ "off", "minimal", @@ -318,14 +335,17 @@ describe("resolveClaudeThinkingProfile", () => { expectLevelIdsInclude(profile, ["off", "xhigh", "adaptive", "max"]); }); - it("exposes Fable 5's always-adaptive profile to Claude providers", () => { - const profile = resolveClaudeThinkingProfile("claude-fable-5"); - expectFields(profile, { - defaultLevel: "high", - preserveWhenCatalogReasoningFalse: true, - }); - expectLevelIdsInclude(profile, ["xhigh", "adaptive", "max"]); - }); + it.each(["claude-fable-5", "claude-mythos-5"])( + "exposes %s's mandatory-adaptive profile to Claude providers", + (modelId) => { + const profile = resolveClaudeThinkingProfile(modelId); + expectFields(profile, { + defaultLevel: "high", + preserveWhenCatalogReasoningFalse: true, + }); + expectLevelIdsInclude(profile, ["xhigh", "adaptive", "max"]); + }, + ); it("does not match longer unrelated Claude ids by prefix only", () => { expect(resolveClaudeThinkingProfile("vendor/claude-fable-500").defaultLevel).toBeUndefined(); diff --git a/src/plugin-sdk/provider-model-shared.ts b/src/plugin-sdk/provider-model-shared.ts index 167e249dbc7f..eb65ea7e4cf1 100644 --- a/src/plugin-sdk/provider-model-shared.ts +++ b/src/plugin-sdk/provider-model-shared.ts @@ -29,8 +29,11 @@ export type { export { resolveClaudeFable5ModelIdentity, resolveClaudeModelIdentity, + resolveClaudeMythos5ModelIdentity, resolveClaudeNativeThinkingLevelMap, resolveClaudeSonnet5ModelIdentity, + requiresClaudeDefaultSampling, + requiresClaudeMandatoryAdaptiveThinking, supportsClaudeAdaptiveThinking, supportsClaudeNativeMaxEffort, supportsClaudeNativeXhighEffort, diff --git a/src/plugins/provider-claude-thinking.ts b/src/plugins/provider-claude-thinking.ts index c6d36c7c286e..ca3aa74fe2b0 100644 --- a/src/plugins/provider-claude-thinking.ts +++ b/src/plugins/provider-claude-thinking.ts @@ -6,6 +6,7 @@ import { CLAUDE_SONNET_5_THINKING_PROFILE, resolveClaudeFable5ModelIdentity, resolveClaudeModelIdentity, + resolveClaudeMythos5ModelIdentity, resolveClaudeSonnet5ModelIdentity, supportsClaudeAdaptiveThinking, supportsClaudeNativeXhighEffort, @@ -38,7 +39,7 @@ export function resolveClaudeThinkingProfile( ): ProviderThinkingProfile { const ref = { id: modelId, params }; const canonicalModelId = resolveClaudeModelIdentity(ref); - if (resolveClaudeFable5ModelIdentity(ref)) { + if (resolveClaudeFable5ModelIdentity(ref) || resolveClaudeMythos5ModelIdentity(ref)) { return CLAUDE_FABLE_5_THINKING_PROFILE; } if (resolveClaudeSonnet5ModelIdentity(ref)) {