diff --git a/extensions/amazon-bedrock/index.test.ts b/extensions/amazon-bedrock/index.test.ts index 4d021e771ab..8420a5ddce2 100644 --- a/extensions/amazon-bedrock/index.test.ts +++ b/extensions/amazon-bedrock/index.test.ts @@ -412,6 +412,58 @@ describe("amazon-bedrock provider plugin", () => { ).toEqual({ maxTokens: 12 }); }); + it("preserves Bedrock Opus 4.7 max thinking in the final payload", async () => { + const provider = await registerSingleProviderPlugin(amazonBedrockPlugin); + const wrapped = provider.wrapStreamFn?.({ + provider: "amazon-bedrock", + modelId: "us.anthropic.claude-opus-4-7", + streamFn: spyStreamFn, + thinkingLevel: "max", + } as never); + + const result = wrapped?.( + { + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + id: "us.anthropic.claude-opus-4-7", + } as never, + { messages: [] } as never, + { reasoning: "xhigh" } as never, + ) as Record | undefined; + const payload = { + additionalModelRequestFields: { + thinking: { type: "adaptive" }, + output_config: { effort: "xhigh" }, + }, + }; + + await (result?.onPayload as ((p: Record) => unknown) | undefined)?.(payload); + + expect(payload.additionalModelRequestFields.output_config).toEqual({ effort: "max" }); + }); + + it("keeps Bedrock Opus 4.7 xhigh thinking distinct from max", async () => { + const provider = await registerSingleProviderPlugin(amazonBedrockPlugin); + const wrapped = provider.wrapStreamFn?.({ + provider: "amazon-bedrock", + modelId: "us.anthropic.claude-opus-4-7", + streamFn: spyStreamFn, + thinkingLevel: "xhigh", + } as never); + + const result = wrapped?.( + { + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + id: "us.anthropic.claude-opus-4-7", + } as never, + { messages: [] } as never, + { reasoning: "xhigh" } as never, + ) as Record | undefined; + + expect(result).not.toHaveProperty("onPayload"); + }); + 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 d213eb848f2..19a7e18fb6b 100644 --- a/extensions/amazon-bedrock/register.sync.runtime.ts +++ b/extensions/amazon-bedrock/register.sync.runtime.ts @@ -285,6 +285,22 @@ function injectBedrockCachePoints( } } +function patchOpus47MaxThinkingEffort(payload: Record): void { + const fieldsValue = payload.additionalModelRequestFields; + const fields = + fieldsValue && typeof fieldsValue === "object" && !Array.isArray(fieldsValue) + ? (fieldsValue as Record) + : {}; + const outputConfigValue = fields.output_config; + const outputConfig = + outputConfigValue && typeof outputConfigValue === "object" && !Array.isArray(outputConfigValue) + ? (outputConfigValue as Record) + : {}; + outputConfig.effort = "max"; + fields.output_config = outputConfig; + payload.additionalModelRequestFields = fields; +} + export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void { // Keep registration-local constants inside the function so partial module // initialization during test bootstrap cannot trip TDZ reads. @@ -441,7 +457,7 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void { }, resolveConfigApiKey: ({ env }) => resolveBedrockConfigApiKey(env), ...anthropicByModelReplayHooks, - wrapStreamFn: ({ modelId, config, model, streamFn }) => { + wrapStreamFn: ({ modelId, config, model, streamFn, thinkingLevel }) => { const currentGuardrail = resolveCurrentPluginConfig(config)?.guardrail; // Apply cache + guardrail wrapping. const wrapped = @@ -452,12 +468,13 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void { const mayNeedCacheInjection = isBedrockAppInferenceProfile(modelId) && !piAiWouldInjectCachePoints(modelId); const shouldOmitTemperature = isOpus47BedrockModelRef(modelId); + const shouldPatchMaxThinking = shouldOmitTemperature && thinkingLevel === "max"; // For known Anthropic models (heuristic match), enable injection immediately. // For opaque profile IDs, we'll resolve via GetInferenceProfile on first call. const heuristicMatch = needsCachePointInjection(modelId); - if (!region && !mayNeedCacheInjection && !shouldOmitTemperature) { + if (!region && !mayNeedCacheInjection && !shouldOmitTemperature && !shouldPatchMaxThinking) { return wrapped; } @@ -471,8 +488,24 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void { Object.assign({}, options, region ? { region } : {}), ); + const originalOnPayload = merged.onPayload as + | ((payload: unknown, model: unknown) => unknown) + | undefined; + if (!mayNeedCacheInjection) { - return underlying(streamModel, context, merged); + return underlying(streamModel, context, { + ...merged, + ...(shouldPatchMaxThinking + ? { + onPayload: (payload: unknown, payloadModel: unknown) => { + if (payload && typeof payload === "object") { + patchOpus47MaxThinkingEffort(payload as Record); + } + return originalOnPayload?.(payload, payloadModel); + }, + } + : {}), + }); } // Use the cacheRetention from options if explicitly set. @@ -485,10 +518,6 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void { // want caching enabled, so defaulting to "short" is the safer behavior. const cacheRetention = typeof merged.cacheRetention === "string" ? merged.cacheRetention : "short"; - const originalOnPayload = merged.onPayload as - | ((payload: unknown, model: unknown) => unknown) - | undefined; - if (heuristicMatch) { // Fast path: ARN heuristic already identified this as Claude, but the // concrete target may still need profile traits for Opus 4.7 payloads. @@ -499,6 +528,9 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void { if (payload && typeof payload === "object") { const payloadRecord = payload as Record; injectBedrockCachePoints(payloadRecord, cacheRetention); + if (shouldPatchMaxThinking) { + patchOpus47MaxThinkingEffort(payloadRecord); + } if (mayNeedTemperatureTrait) { const traits = await resolveAppProfileTraits(modelId, region); if (traits.omitTemperature) { @@ -522,6 +554,9 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void { if (traits.cacheEligible) { injectBedrockCachePoints(payloadRecord, cacheRetention); } + if (shouldPatchMaxThinking) { + patchOpus47MaxThinkingEffort(payloadRecord); + } if (traits.omitTemperature) { omitDeprecatedOpus47PayloadTemperature(payloadRecord); }