From b4aa520b0765cebb53832115d18aa739d60b8afb Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 10 Jun 2026 16:41:24 +0900 Subject: [PATCH] fix(bedrock): preserve Mythos Preview thinking policy --- extensions/amazon-bedrock/discovery.test.ts | 64 +++++++++++++++++++ extensions/amazon-bedrock/discovery.ts | 15 ++++- extensions/amazon-bedrock/index.test.ts | 38 +++++++++++ .../amazon-bedrock/register.sync.runtime.ts | 15 +++-- extensions/amazon-bedrock/thinking-policy.ts | 28 ++++++++ 5 files changed, 154 insertions(+), 6 deletions(-) diff --git a/extensions/amazon-bedrock/discovery.test.ts b/extensions/amazon-bedrock/discovery.test.ts index 54a0561736b1..4bbf71ca6439 100644 --- a/extensions/amazon-bedrock/discovery.test.ts +++ b/extensions/amazon-bedrock/discovery.test.ts @@ -166,6 +166,70 @@ 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", + }, + ], + }, + ], + }); + + 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: { xhigh: "xhigh", max: "max" }, + }); + }); + + it("marks known Mythos Preview inference profile fallbacks as reasoning capable", async () => { + sendMock + .mockResolvedValueOnce({ + modelSummaries: [], + }) + .mockResolvedValueOnce({ + inferenceProfileSummaries: [ + { + inferenceProfileId: "us.anthropic.claude-mythos-preview", + inferenceProfileName: "US Claude Mythos Preview", + status: "ACTIVE", + type: "SYSTEM_DEFINED", + models: [ + { + modelArn: + "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-mythos-preview", + }, + ], + }, + ], + }); + + const models = await discoverBedrockModels({ region: "us-east-1", clientFactory }); + const model = models[0] as Record | undefined; + + expectModelFields(model, { + id: "us.anthropic.claude-mythos-preview", + reasoning: true, + }); + expect(model).not.toHaveProperty("thinkingLevelMap"); + }); + it("normalizes region-prefixed versioned model ids when resolving context windows", async () => { sendMock .mockResolvedValueOnce({ diff --git a/extensions/amazon-bedrock/discovery.ts b/extensions/amazon-bedrock/discovery.ts index a6fe04959ed4..fc34c5669682 100644 --- a/extensions/amazon-bedrock/discovery.ts +++ b/extensions/amazon-bedrock/discovery.ts @@ -157,6 +157,13 @@ function resolveKnownContextWindow(modelId: string): number | undefined { return undefined; } +function isKnownClaudeMythosPreviewModelId(modelId: string): boolean { + const stripped = modelId.replace(/^(?:us|eu|ap|apac|au|jp|global)\./, ""); + return [modelId, stripped].some((candidate) => + /(?:^|[/.:])anthropic\.claude-mythos-preview(?:$|[-.:/])/i.test(candidate), + ); +} + function resolveKnownThinkingLevelMap( modelId: string, ): ModelDefinitionConfig["thinkingLevelMap"] | undefined { @@ -275,7 +282,10 @@ function mapInputModalities(summary: BedrockModelSummary): Array<"text" | "image } function inferReasoningSupport(summary: BedrockModelSummary): boolean { - if (supportsClaudeAdaptiveThinking({ id: summary.modelId })) { + if ( + supportsClaudeAdaptiveThinking({ id: summary.modelId }) || + isKnownClaudeMythosPreviewModelId(summary.modelId ?? "") + ) { return true; } const haystack = normalizeLowercaseStringOrEmpty( @@ -467,7 +477,8 @@ function resolveInferenceProfiles( name: profile.inferenceProfileName?.trim() || profile.inferenceProfileId, reasoning: baseModel?.reasoning ?? - supportsClaudeAdaptiveThinking({ id: baseModelId ?? profile.inferenceProfileId }), + (supportsClaudeAdaptiveThinking({ id: baseModelId ?? profile.inferenceProfileId }) || + isKnownClaudeMythosPreviewModelId(baseModelId ?? profile.inferenceProfileId)), input: baseModel?.input ?? ["text"], cost: baseModel?.cost ?? DEFAULT_COST, contextWindow: diff --git a/extensions/amazon-bedrock/index.test.ts b/extensions/amazon-bedrock/index.test.ts index acf089c01efe..5e7730cbf434 100644 --- a/extensions/amazon-bedrock/index.test.ts +++ b/extensions/amazon-bedrock/index.test.ts @@ -835,6 +835,44 @@ describe("amazon-bedrock provider plugin", () => { expect(payload.inferenceConfig).toEqual({}); }); + it("does not re-upgrade Mythos Preview max thinking in the final payload", async () => { + const provider = await registerSingleProviderPlugin(amazonBedrockPlugin); + const wrapped = provider.wrapStreamFn?.({ + provider: "amazon-bedrock", + modelId: "us.anthropic.claude-mythos-preview", + streamFn: spyStreamFn, + thinkingLevel: "max", + } as never); + + const result = wrapped?.( + { + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + id: "us.anthropic.claude-mythos-preview", + name: "Claude Mythos Preview", + reasoning: true, + } as never, + { messages: [] } as never, + { reasoning: "max" } as never, + ) as Record | undefined; + + const payload = { + inferenceConfig: { temperature: 0.2 }, + additionalModelRequestFields: { + thinking: { type: "adaptive" }, + output_config: { effort: "high" }, + }, + }; + + await (result?.onPayload as ((p: Record) => unknown) | undefined)?.(payload); + + expect(payload.additionalModelRequestFields).toEqual({ + thinking: { type: "adaptive" }, + output_config: { effort: "high" }, + }); + expect(payload.inferenceConfig).toEqual({}); + }); + it("classifies nested Bedrock deprecated-temperature validation as format failover", async () => { const provider = await registerSingleProviderPlugin(amazonBedrockPlugin); diff --git a/extensions/amazon-bedrock/register.sync.runtime.ts b/extensions/amazon-bedrock/register.sync.runtime.ts index f6d6f5af8dd9..bfcaa682a380 100644 --- a/extensions/amazon-bedrock/register.sync.runtime.ts +++ b/extensions/amazon-bedrock/register.sync.runtime.ts @@ -23,6 +23,7 @@ import { mergeImplicitBedrockProvider, resolveBedrockConfigApiKey } from "./disc import { bedrockMemoryEmbeddingProviderAdapter } from "./memory-embedding-adapter.js"; import { streamBedrock, streamSimpleBedrock } from "./stream.runtime.js"; import { + isLatestAdaptiveBedrockModelRef, isOpus47OrNewerBedrockModelRef, resolveBedrockNativeThinkingLevelMap, resolveBedrockClaudeThinkingProfile, @@ -596,8 +597,10 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void { currentPluginConfig?.discovery?.region; const mayNeedCacheInjection = isBedrockAppInferenceProfile(modelId) && !sharedRuntimeWouldInjectCachePoints(modelId); - const shouldOmitTemperature = opus47OrNewer || fable5; + const shouldOmitTemperature = + opus47OrNewer || fable5 || isLatestAdaptiveBedrockModelRef(modelId, model?.params); const shouldPatchMaxThinking = supportsNativeMax && thinkingLevel === "max"; + const shouldPatchPayload = shouldOmitTemperature || shouldPatchMaxThinking; // For known Anthropic models (heuristic match), enable injection immediately. // For opaque profile IDs, we'll resolve via GetInferenceProfile on first call. @@ -627,13 +630,17 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void { context, withAwsCredentialRefreshOnPayload({ ...merged, - ...(shouldPatchMaxThinking + ...(shouldPatchPayload ? { onPayload: (payload: unknown, payloadModel: unknown) => { if (payload && typeof payload === "object") { const payloadRecord = payload as Record; - patchMaxThinkingEffort(payloadRecord); - omitUnsupportedClaudePayloadTemperature(payloadRecord); + if (shouldPatchMaxThinking) { + patchMaxThinkingEffort(payloadRecord); + } + if (shouldOmitTemperature) { + omitUnsupportedClaudePayloadTemperature(payloadRecord); + } } return originalOnPayload?.(payload, payloadModel); }, diff --git a/extensions/amazon-bedrock/thinking-policy.ts b/extensions/amazon-bedrock/thinking-policy.ts index 1c67daccc6e1..bf5cc8f4d366 100644 --- a/extensions/amazon-bedrock/thinking-policy.ts +++ b/extensions/amazon-bedrock/thinking-policy.ts @@ -43,6 +43,28 @@ export function isOpus47OrNewerBedrockModelRef(modelRef: string): boolean { return isOpus47BedrockModelRef(modelRef) || isOpus48BedrockModelRef(modelRef); } +function isMythosPreviewBedrockModelRef(modelRef: string): boolean { + return /(?:^|[/.:])(?:(?:us|eu|ap|apac|au|jp|global)\.)?(?:anthropic\.)?claude-mythos-preview(?:$|[-.:/])/i.test( + modelRef, + ); +} + +/** Return whether a Bedrock Claude ref needs latest adaptive-thinking request shaping. */ +export function isLatestAdaptiveBedrockModelRef( + modelId: string, + params?: Record, +): boolean { + const modelRef = { id: modelId, params }; + const canonicalModelId = resolveClaudeModelIdentity(modelRef); + return ( + resolveClaudeFable5ModelIdentity(modelRef) !== undefined || + [modelId, canonicalModelId].some( + (candidate) => + isOpus47OrNewerBedrockModelRef(candidate) || isMythosPreviewBedrockModelRef(candidate), + ) + ); +} + /** Return whether a Bedrock Claude ref supports max effort. */ export function supportsBedrockNativeMaxEffort( modelId: string, @@ -109,6 +131,12 @@ export function resolveBedrockClaudeThinkingProfile( defaultLevel: "adaptive", }; } + if (modelRefs.some(isMythosPreviewBedrockModelRef)) { + return { + levels: [...BASE_CLAUDE_THINKING_LEVELS, { id: "adaptive" }], + defaultLevel: "adaptive", + }; + } if (modelRefs.some((modelRef) => /claude-sonnet-4(?:\.|-)6(?:$|[-.])/i.test(modelRef))) { return { levels: [...BASE_CLAUDE_THINKING_LEVELS, { id: "adaptive" }],