Skip to content

Commit 9a804eb

Browse files
allquixoticoz-agent
andcommitted
fix(bedrock): send adaptive thinking + output_config.effort for Claude Opus 4.7 (3.53.5)
AWS Bedrock Converse rejects the legacy 'thinking: { type: "enabled", budget_tokens }' payload for Claude Opus 4.7 with: invalid_request_error "thinking.type.enabled" is not supported for this model. Use "thinking.type.adaptive" and "output_config.effort" to control thinking behavior. Resolves the "API Streaming Failed" error that appeared whenever reasoning was enabled on Opus 4.7. Change - Add BEDROCK_ADAPTIVE_THINKING_MODEL_IDS in packages/types/src/providers/bedrock.ts, seeded with anthropic.claude-opus-4-7. - bedrock.ts: compute baseModelId early, branch the thinking payload. Adaptive models emit additionalModelRequestFields.thinking = { type: 'adaptive' } plus a top-level output_config: { effort } derived from the user's reasoningEffort setting (or mapped from the budget token count: <=4096 low, <=16384 medium, >16384 high). Older Claudes keep the legacy budget_tokens path. - Extend BedrockPayload typings to allow thinking.type 'adaptive' and top-level output_config.effort. - bedrock-reasoning.spec.ts: cover the new Opus 4.7 behavior end-to-end (budget-derived effort, explicit reasoningEffort honored, and regression case that Sonnet 4 still uses the legacy shape). Co-Authored-By: Oz <oz-agent@warp.dev>
1 parent 733b3af commit 9a804eb

4 files changed

Lines changed: 192 additions & 17 deletions

File tree

packages/types/src/providers/bedrock.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,16 @@ export const BEDROCK_1M_CONTEXT_MODEL_IDS = [
566566
// behavior that surfaced this issue.
567567
export const BEDROCK_NATIVE_1M_CONTEXT_MODEL_IDS = ["anthropic.claude-opus-4-7"] as const
568568

569+
// Models that REJECT the legacy `thinking: { type: "enabled", budget_tokens: N }` payload
570+
// on the Bedrock Converse API and instead require the newer adaptive thinking format:
571+
// additionalModelRequestFields.thinking = { type: "adaptive" }
572+
// payload.output_config = { effort: "low" | "medium" | "high" }
573+
//
574+
// Attempting to send the legacy shape results in:
575+
// invalid_request_error: "thinking.type.enabled" is not supported for this model.
576+
// Use "thinking.type.adaptive" and "output_config.effort" to control thinking behavior.
577+
export const BEDROCK_ADAPTIVE_THINKING_MODEL_IDS = ["anthropic.claude-opus-4-7"] as const
578+
569579
// Previously Claude 4.6 Sonnet/Opus auto-advertised 1M. With the new dual dropdown
570580
// (default-context + `:1m` variant) the UI always exposes both tiers explicitly, so
571581
// we no longer auto-flip any model to 1M at resolve time. The opt-in toggle + `:1m`

src/api/providers/__tests__/bedrock-reasoning.spec.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,101 @@ describe("AwsBedrockHandler - Extended Thinking", () => {
284284
expect(reasoningChunks[1].text).toBe(" about this problem.")
285285
})
286286

287+
it("should send adaptive thinking payload (not budget_tokens) for Claude Opus 4.7", async () => {
288+
handler = new AwsBedrockHandler({
289+
apiProvider: "bedrock",
290+
apiModelId: "anthropic.claude-opus-4-7",
291+
awsRegion: "us-east-1",
292+
enableReasoningEffort: true,
293+
modelMaxThinkingTokens: 8192,
294+
})
295+
296+
mockSend.mockResolvedValue({
297+
stream: (async function* () {
298+
yield { messageStart: { role: "assistant" } }
299+
yield { metadata: { usage: { inputTokens: 100, outputTokens: 50 } } }
300+
})(),
301+
})
302+
303+
const messages = [{ role: "user" as const, content: "Test message" }]
304+
const stream = handler.createMessage("System prompt", messages)
305+
306+
for await (const _chunk of stream) {
307+
// consume stream
308+
}
309+
310+
expect(mockSend).toHaveBeenCalledTimes(1)
311+
expect(capturedPayload).toBeDefined()
312+
313+
// Opus 4.7 must use the adaptive thinking shape.
314+
expect(capturedPayload.additionalModelRequestFields).toBeDefined()
315+
expect(capturedPayload.additionalModelRequestFields.thinking).toEqual({ type: "adaptive" })
316+
expect(capturedPayload.additionalModelRequestFields.thinking.budget_tokens).toBeUndefined()
317+
318+
// output_config.effort must live at the top level of the payload.
319+
expect(capturedPayload.output_config).toBeDefined()
320+
expect(["low", "medium", "high"]).toContain(capturedPayload.output_config.effort)
321+
// 8192 tokens falls in the "medium" bucket per mapReasoningBudgetToBedrockEffort.
322+
expect(capturedPayload.output_config.effort).toBe("medium")
323+
})
324+
325+
it("should honor explicit reasoningEffort for Claude Opus 4.7", async () => {
326+
handler = new AwsBedrockHandler({
327+
apiProvider: "bedrock",
328+
apiModelId: "anthropic.claude-opus-4-7",
329+
awsRegion: "us-east-1",
330+
enableReasoningEffort: true,
331+
modelMaxThinkingTokens: 4096,
332+
reasoningEffort: "high" as any,
333+
})
334+
335+
mockSend.mockResolvedValue({
336+
stream: (async function* () {
337+
yield { messageStart: { role: "assistant" } }
338+
yield { metadata: { usage: { inputTokens: 100, outputTokens: 50 } } }
339+
})(),
340+
})
341+
342+
const messages = [{ role: "user" as const, content: "Test message" }]
343+
const stream = handler.createMessage("System prompt", messages)
344+
345+
for await (const _chunk of stream) {
346+
// consume stream
347+
}
348+
349+
expect(capturedPayload.output_config).toEqual({ effort: "high" })
350+
})
351+
352+
it("should still use legacy budget_tokens thinking payload for Claude Sonnet 4", async () => {
353+
handler = new AwsBedrockHandler({
354+
apiProvider: "bedrock",
355+
apiModelId: "anthropic.claude-sonnet-4-20250514-v1:0",
356+
awsRegion: "us-east-1",
357+
enableReasoningEffort: true,
358+
modelMaxThinkingTokens: 4096,
359+
})
360+
361+
mockSend.mockResolvedValue({
362+
stream: (async function* () {
363+
yield { messageStart: { role: "assistant" } }
364+
yield { metadata: { usage: { inputTokens: 100, outputTokens: 50 } } }
365+
})(),
366+
})
367+
368+
const messages = [{ role: "user" as const, content: "Test message" }]
369+
const stream = handler.createMessage("System prompt", messages)
370+
371+
for await (const _chunk of stream) {
372+
// consume stream
373+
}
374+
375+
expect(capturedPayload.additionalModelRequestFields.thinking).toEqual({
376+
type: "enabled",
377+
budget_tokens: 4096,
378+
})
379+
expect(capturedPayload.output_config).toBeUndefined()
380+
})
381+
287382
it("should support API key authentication", async () => {
288383
handler = new AwsBedrockHandler({
289384
apiProvider: "bedrock",

src/api/providers/bedrock.ts

Lines changed: 86 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
BEDROCK_DEFAULT_TEMPERATURE,
2626
AWS_INFERENCE_PROFILE_MAPPING,
2727
BEDROCK_1M_CONTEXT_MODEL_IDS,
28+
BEDROCK_ADAPTIVE_THINKING_MODEL_IDS,
2829
BEDROCK_GLOBAL_INFERENCE_MODEL_IDS,
2930
BEDROCK_NATIVE_1M_CONTEXT_MODEL_IDS,
3031
BEDROCK_SERVICE_TIER_MODEL_IDS,
@@ -62,11 +63,13 @@ interface BedrockInferenceConfig {
6263

6364
// Define interface for Bedrock additional model request fields
6465
// This includes thinking configuration, 1M context beta, and other model-specific parameters
66+
//
67+
// Two shapes are supported for the `thinking` field:
68+
// - Legacy (Claude Sonnet/Opus 4.x, Claude 3.7): { type: "enabled", budget_tokens: N }
69+
// - Adaptive (Claude Opus 4.7+): { type: "adaptive" } paired with a
70+
// top-level `output_config.effort` string on the payload itself.
6571
interface BedrockAdditionalModelFields {
66-
thinking?: {
67-
type: "enabled"
68-
budget_tokens: number
69-
}
72+
thinking?: { type: "enabled"; budget_tokens: number } | { type: "adaptive" }
7073
anthropic_beta?: string[]
7174
[key: string]: any // Add index signature to be compatible with DocumentType
7275
}
@@ -80,6 +83,39 @@ interface BedrockPayload {
8083
anthropic_version?: string
8184
additionalModelRequestFields?: BedrockAdditionalModelFields
8285
toolConfig?: ToolConfiguration
86+
// Adaptive-thinking models (e.g. Claude Opus 4.7 on Bedrock) use this top-level
87+
// `output_config.effort` knob instead of the legacy `thinking.budget_tokens` number.
88+
output_config?: { effort: "low" | "medium" | "high" }
89+
}
90+
91+
/**
92+
* Map a reasoning budget (in tokens) to a coarse effort bucket for the adaptive
93+
* thinking payload. Used only when invoking Bedrock models that require the newer
94+
* `thinking: { type: "adaptive" }` + `output_config.effort` shape.
95+
*
96+
* The thresholds mirror the historical budget ranges exposed by the reasoning UI:
97+
* <= 4096 tokens → "low"
98+
* <= 16384 tokens → "medium"
99+
* > 16384 tokens → "high"
100+
*/
101+
function mapReasoningBudgetToBedrockEffort(budget: number | undefined): "low" | "medium" | "high" {
102+
const b = typeof budget === "number" && Number.isFinite(budget) && budget > 0 ? budget : 0
103+
if (b <= 4096) return "low"
104+
if (b <= 16384) return "medium"
105+
return "high"
106+
}
107+
108+
/**
109+
* Normalize a freeform reasoning-effort setting string to the three buckets Bedrock
110+
* accepts on `output_config.effort`. Unknown or disabled values return undefined so
111+
* the caller can fall back to the budget-derived mapping.
112+
*/
113+
function normalizeReasoningEffortForBedrock(value: unknown): "low" | "medium" | "high" | undefined {
114+
if (typeof value !== "string") return undefined
115+
const v = value.toLowerCase()
116+
if (v === "low" || v === "medium" || v === "high") return v
117+
if (v === "minimal") return "low"
118+
return undefined
83119
}
84120

85121
// Extended payload type that includes service_tier as a top-level parameter
@@ -322,6 +358,14 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
322358

323359
let additionalModelRequestFields: BedrockAdditionalModelFields | undefined
324360
let thinkingEnabled = false
361+
let adaptiveThinkingEffort: "low" | "medium" | "high" | undefined
362+
363+
// Resolve the base model id first so the thinking branch can decide between the
364+
// legacy budget_tokens payload and the newer adaptive + output_config.effort payload.
365+
// parseBaseModelId strips cross-region inference prefixes (e.g. `us.`, `eu.`) and the
366+
// synthetic `:1m` dropdown suffix.
367+
const baseModelId = this.parseBaseModelId(modelConfig.id)
368+
const requiresAdaptiveThinking = BEDROCK_ADAPTIVE_THINKING_MODEL_IDS.includes(baseModelId as any)
325369

326370
// Determine if thinking should be enabled
327371
// metadata?.thinking?.enabled: Explicitly enabled through API metadata (direct request)
@@ -334,17 +378,42 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
334378

335379
if ((isThinkingExplicitlyEnabled || isThinkingEnabledBySettings) && modelConfig.info.supportsReasoningBudget) {
336380
thinkingEnabled = true
337-
additionalModelRequestFields = {
338-
thinking: {
339-
type: "enabled",
340-
budget_tokens: metadata?.thinking?.maxThinkingTokens || modelConfig.reasoningBudget || 4096,
341-
},
381+
const effectiveBudget = metadata?.thinking?.maxThinkingTokens || modelConfig.reasoningBudget || 4096
382+
383+
if (requiresAdaptiveThinking) {
384+
// Newer Claude models on Bedrock (e.g. Opus 4.7) reject the legacy
385+
// `thinking: { type: "enabled", budget_tokens: N }` shape with:
386+
// invalid_request_error: "thinking.type.enabled" is not supported for this model.
387+
// Use "thinking.type.adaptive" and "output_config.effort" to control thinking behavior.
388+
// Honor that by emitting `thinking: { type: "adaptive" }` plus a top-level
389+
// `output_config.effort`. Effort comes from the user's reasoningEffort setting
390+
// when present, otherwise we derive it from the token budget.
391+
adaptiveThinkingEffort =
392+
normalizeReasoningEffortForBedrock(
393+
(this.options as ProviderSettings & { reasoningEffort?: unknown }).reasoningEffort,
394+
) ?? mapReasoningBudgetToBedrockEffort(effectiveBudget)
395+
additionalModelRequestFields = {
396+
thinking: { type: "adaptive" },
397+
}
398+
logger.info("Adaptive thinking enabled for Bedrock request", {
399+
ctx: "bedrock",
400+
modelId: modelConfig.id,
401+
thinking: additionalModelRequestFields.thinking,
402+
effort: adaptiveThinkingEffort,
403+
})
404+
} else {
405+
additionalModelRequestFields = {
406+
thinking: {
407+
type: "enabled",
408+
budget_tokens: effectiveBudget,
409+
},
410+
}
411+
logger.info("Extended thinking enabled for Bedrock request", {
412+
ctx: "bedrock",
413+
modelId: modelConfig.id,
414+
thinking: additionalModelRequestFields.thinking,
415+
})
342416
}
343-
logger.info("Extended thinking enabled for Bedrock request", {
344-
ctx: "bedrock",
345-
modelId: modelConfig.id,
346-
thinking: additionalModelRequestFields.thinking,
347-
})
348417
}
349418

350419
const inferenceConfig: BedrockInferenceConfig = {
@@ -357,8 +426,6 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
357426
// - the configured target id contains the `:1m` / `[1m]` indicator (user picked the
358427
// 1M variant from the dropdown), OR
359428
// - the awsBedrock1MContext opt-in toggle is set by the user.
360-
// Use parseBaseModelId to handle cross-region inference prefixes.
361-
const baseModelId = this.parseBaseModelId(modelConfig.id)
362429
const configuredTargetForIndicator =
363430
this.options.awsBedrockInvokeTarget || this.options.awsCustomArn || modelConfig.id
364431
const is1MContextEnabled =
@@ -429,6 +496,9 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
429496
...(additionalModelRequestFields && { additionalModelRequestFields }),
430497
// Add anthropic_version at top level when using thinking features
431498
...(thinkingEnabled && { anthropic_version: "bedrock-2023-05-31" }),
499+
// Adaptive-thinking models require the effort knob at the top level alongside
500+
// `thinking: { type: "adaptive" }` inside additionalModelRequestFields.
501+
...(adaptiveThinkingEffort && { output_config: { effort: adaptiveThinkingEffort } }),
432502
toolConfig,
433503
// Add service_tier as a top-level parameter (not inside additionalModelRequestFields)
434504
...(useServiceTier && { service_tier: this.options.awsBedrockServiceTier }),

src/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"displayName": "%extension.displayName%",
44
"description": "%extension.description%",
55
"publisher": "allquixotic",
6-
"version": "3.53.4",
6+
"version": "3.53.5",
77
"icon": "assets/icons/icon.png",
88
"galleryBanner": {
99
"color": "#617A91",

0 commit comments

Comments
 (0)