Repository navigation
fix(bedrock): send json_schema as a forced tool on Claude Opus 4.7 and 4.8 Converse - #42644
Conversation
…d 4.8 Converse Bedrock rejects outputConfig.textFormat on Opus 4.7 and 4.8 with "output_config.format: Extra inputs are not permitted", and the AWS model cards list structured outputs as not supported for both, so their cost-map entries no longer claim supports_native_structured_output and json_schema requests fall back to the json_tool_call tool. Fixes #27846
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
|
| "supports_vision": true, | ||
| "supports_xhigh_reasoning_effort": true, | ||
| "supports_native_structured_output": true, | ||
| "supports_native_structured_output": false, |
There was a problem hiding this comment.
On Opus 4.7/4.8, this false flag drops output_config.format schemas, allowing successful unconstrained responses. Use the same tool fallback here
Knowledge Base Used: Provider adapters and capabilities
There was a problem hiding this comment.
The Messages route already inlines the schema for every non-native model (#22797); a forced-tool fallback there is a separate change. Caveat noted
There was a problem hiding this comment.
That distinction is correct. The Messages route already preserves the schema through its existing non-native inline path, so this flag change does not introduce the schema loss I called out. A forced-tool fallback there is a separate enhancement, and the caveat accurately documents the current best-effort behavior. I’m withdrawing the prior finding.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…ove the native case to Sonnet 4.6
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit b214a41. Configure here.
TLDR
Problem this solves:
json_schemaon Bedrock Claude Opus 4.7 and 4.8 returns HTTP 400output_config.formaton those two models ("Extra inputs are not permitted")supports_native_structured_output: trueHow it solves it:
falseon all 15 Bedrock Opus 4.7 and 4.8 entriesjson_tool_calltool, like other non-native modelsUser Flow
Before: a developer calling Bedrock Claude Opus 4.8 from the litellm Python SDK's Responses API with a pydantic
text_formatgets a 400 from Bedrock instead of JSONlitellm.responses(model="bedrock/us.anthropic.claude-opus-4-8", input=[one user message "why is the sky blue? format your response as json"], text_format=TextResponse), whereTextResponseis a pydantic model with a singletext: strfield; the SDK sends POST https://bedrock-runtime.us-east-1.amazonaws.com/model/us.anthropic.claude-opus-4-8/converselitellm.BadRequestError: BedrockException - {"message":"The model returned the following errors: output_config.format: Extra inputs are not permitted"}, soTextResponse.model_validate_json(response.output[0].content[0].text)never runsstream=Trueand the sameBadRequestErroris raised before any event arrivesmodel="bedrock/us.anthropic.claude-sonnet-4-6"returns a response whoseoutput[0].content[0].textvalidates asTextResponse, so only Opus 4.7 and 4.8 failAfter: the same Opus 4.8 call returns a response whose text validates against the pydantic model
litellm.responses(model="bedrock/us.anthropic.claude-opus-4-8", ..., text_format=TextResponse)call; the SDK sends the same POST https://bedrock-runtime.us-east-1.amazonaws.com/model/us.anthropic.claude-opus-4-8/converseresponse.output[0].content[0].textis a JSON object with thetextkey the schema asked for, andTextResponse.model_validate_json(...)succeedsstream=True; theresponse.output_text.deltaevents add up to the same JSON object and the stream ends withresponse.completedmodel="bedrock/us.anthropic.claude-sonnet-4-6"still validates asTextResponseRelevant issues
Fixes #27846
Affected release
Linear ticket
Resolves LIT-8378
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Two legs against real Bedrock (account 888602223428, us-east-1, real $$$), identical except for the LiteLLM commit: the merge base with main (1953603) and commit 6135e76. Each leg ran two things
b214a41 landed after this run and only rewrites one
tests/unitInvoke test (nothing underlitellm/or the cost maps changed), so it cannot change behavior and the proof keeps 6135e76responses()call from [Bug]: Structured Output fails for Anthropic models on bedrock/converse #27846 with a pydantictext_format, run as a plain script withLITELLM_LOCAL_MODEL_COST_MAP=Trueso each leg reads its own checkout's cost mappython litellm/proxy/proxy_cli.py --config config.yaml --port $PORT --num_workers 2(one proxy process, 2 uvicorn workers, no database,LITELLM_LOCAL_MODEL_COST_MAP=True):/v1/chat/completions,/v1/responses, and/v1/messages, stream and non-stream, with Sonnet 4.6 as the control on eachconfig.yamlmapsbedrock-opus-4-8,bedrock-opus-4-7, andbedrock-sonnet-4-6tobedrock/us.anthropic.claude-opus-4-8,bedrock/us.anthropic.claude-opus-4-7, andbedrock/us.anthropic.claude-sonnet-4-6withdrop_params: trueSDK script (the reporter's flow):
Proxy requests (the leg loops
MODELover the three deployments; the output blocks below print the content, finish reason, and HTTP status of each):Before (1953603)
SDK, the reporter's flow: Opus 4.8 and 4.7 raise the 400, Sonnet 4.6 validates
Proxy, every unified endpoint: 7 x HTTP 400 on Opus 4.8 and 4.7, 3 x HTTP 200 on the Sonnet 4.6 control
After (6135e76)
SDK, the reporter's flow: every call returns and
TextResponse.model_validate_jsonsucceeds, the stream ends inresponse.completedProxy, every unified endpoint: 10 x HTTP 200, schema-matching JSON on each
Observations
textstring; the prompt asks for JSON twice, this PR leaves it alone/v1/messageson Opus 4.7 and 4.8 is prompt-hinted JSON, not tool-enforced; this PR causes it (the Medium caveat)Live PR risk
Ran at 6135e76 (b214a41 is test-only) against real Bedrock, merge base 1953603 vs tip, then again on the merged tree ecaff1b943 (b214a41 plus origin/main b395bfe, a clean merge): the nine requests of the proof plus the legs named below, one proxy process per side with 2 uvicorn workers and
--detailed_debug, so the outbound Bedrock request body was diffed per request (SigV4 signs the host, so a forwarding recorder cannot sit in front of Bedrock). Every request reached Bedrock exactly once on every sideBreaking
None observed. Sonnet 4.6 sends the same
output_config.format(Invoke) andoutputConfig.textFormat(Converse) bodies at base, tip, and merged and answers 200 with schema JSON on all three;json_objectrequests carry no structured-output key on any side and return the same fenced JSON on all three; the directanthropic/claude-opus-4-7andclaude-opus-4-8keys still resolve totrueat headBackward incompatible
Every entry below is a request that got Bedrock's 400 (
output_config.format: Extra inputs are not permitted) at base and now gets 200 through the non-native path every other non-native Bedrock Claude model already takes, so no client that worked before answers differently. Decision: ship them on that existing path instead of building new enforcement for these two models; approved by mateo-berri in this PR's approving review/v1/messageswithoutput_config.formaton Opus 4.7 and 4.8 (Invoke route): the schema is inlined as text into the last user message and nothing enforces it. Observed: 400 at base; 200 at tip and merged with prose plus a fenced JSON block; the outbound body carries the schema text in the last user message and nooutput_config.format/v1/chat/completionswith an Anthropic-styleoutput_config.formaton the Converse route: the key is dropped with a warning log and nothing constrains the answer. Observed: 400 at base; 200 with plain prose (The capital of France is **Paris**.) at tip and merged; the outbound body has nooutputConfigand notoolConfig/v1/chat/completionsjson_schema withreasoning_efforton the Converse route:json_tool_callis offered but not forced, since a forced tool choice is not allowed with thinking on, so the model may answer in prose. Observed: 400 at base; 200 at tip and merged over three runs each with identical outbound bodies (json_tool_callintoolConfig, notoolChoice, adaptive thinking), schema JSON twice and prose once at tip, prose three times on the merged tree/v1/chat/completionsjson_schema together with the caller's own tools on Converse:toolChoiceis forced tojson_tool_call, so the model cannot call the caller's tools on that request. Observed (request senttoolsandresponse_format, notool_choice): 400 at base; 200 at tip and merged with schema JSON, the outboundtoolConfigcarryingjson_tool_callandget_weatherwithtoolChoiceset tojson_tool_call, no tool call in the answerstream: truewith json_schema on Converse now runs as one non-streamed Bedrock call re-chunked to the client, so the first delta arrives after the whole completion. Observed: 400 at base; 200 at tip and merged, deltas assembling to the same JSON as the non-stream call and ending in[DONE]GET /v1/model/inforeportssupports_native_structured_output: falsefor these deployments (trueat base) withsupports_response_schemastilltrue;/model_group/infonever carried the field (nullon every side)Regression risk
apac., andbedrock_mantle/names) fall through to the bareanthropic.claude-opus-4-7/-4-8key, so the flag flips for them too. No mantle code path reads it (mantle rides the OpenAI-compatible transformer), so the delta there is the reported value only; not driven/v1/messagesInvoke branch for these models has no unit test of its own beyond the sharedapply_bedrock_invoke_structured_outputtests and the label-gated e2e suites (tests/e2e/claude_code/structured_outputs/test_bedrock_{converse,invoke}.py), so it is covered here by the live legs only; the Converse and Invoke chat branches have cost-map-driven unit tests in this PRDependency graph
Readers of
supports_native_structured_output(complete grep overlitellm/,enterprise/,litellm/proxy/client/, the dashboard, and the litellm-docs checkout; no string dispatch, registry, constant, or dashboard reader exists):litellm.utils.supports_native_structured_outputand_supports_factory, key resolution forbedrock/,bedrock/invoke/,bedrock/converse/, region-prefixed andus-govkeys: verified-live through/v1/model/info, plus every prefix form resolved at headModelInfo.supports_native_structured_outputandget_model_infobehind/v1/model/info: verified-liveconverse_transformation.py), chat completions and the Responses bridge on top of it: verified-live (non-stream, stream, tools plus schema, json_object,output_config.format, reasoning plus schema), testedanthropic_claude3_transformation.py): verified-live (Opus 4.7, Opus 4.8, Sonnet 4.6 control), tested at b214a41/v1/messagesgate (common_utils.apply_bedrock_invoke_structured_output): verified-live, untested at unit level for these modelssupports_response_schemaandsupports_function_calling, both stilltrue), so no deployment is dropped or rejected/model_group/info,/v1/models, the dashboard,litellm/proxy/client/: no reader, unchanged/reload/model_cost_map, periodic reload,adopt_model_cost_map): not driven; a running proxy picks the flip up at the next reload event or restartNot verified
eu.,jp.,au.,us-gov.andus-gov-east-1Opus 4.7 and 4.8 keys: not routable from the us-east-1 credentials this run has;global.was driven through thebedrock/*wildcardanthropic.claude-opus-4-8id and thebedrock/converse/prefix: resolved at head, not called/v1/messageswith the legacy top-leveloutput_formatfield: same fallback by code, not drivenPOST /reload/model_cost_map: not drivenType
🐛 Bug Fix
Caveats (if any)
Low
/v1/messageswithoutput_config.format: schema inlined as a prompt hint, not enforcedoutput_config.formaton Converse chat completions: key dropped with a warningreasoning_effortplus json_schema on Converse:json_tool_calloffered, not forcedtoolChoiceforced tojson_tool_callstream: trueon Converse: one Bedrock call, re-chunked to the client/v1/messagesor under thinking is its own feature with its own QA, and the 400 it replaces served nobodymisc / Run testsis red on a pre-existing failure and is not a required checktest_wrapper_async_does_not_fire_failure_hook_for_post_success_errorfails on main run 35807522965 and at the merge baseLITELLM_LOCAL_MODEL_COST_MAP=Truedeployments need the releasemodel_infooverride pinningsupports_native_structured_output: truekeeps the native path and the 400 (by code, not driven)apac.,bedrock_mantle/) fall through to the bare key, so they flip tooeu.,jp.,au., andus-govkeys (no credentials for those regions),POST /reload/model_cost_mappickup, legacyoutput_formaton/v1/messagestrueFinal Attestation