Fix Bedrock Converse API returning both json_tool_call and real tools when tools and response_format are used#18384
Fix Bedrock Converse API returning both json_tool_call and real tools when tools and response_format are used#18384haggai-backline wants to merge 2 commits intoBerriAI:mainfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Hey, when will this fix will be merged? |
|
Hey! nice PR, thanks.
|
…onse_format are used - Fixed issue BerriAI#18381 where Bedrock returns both json_tool_call and real tools - Added logic to filter out json_tool_call when json_mode is enabled and real tools are called - Added comprehensive tests for the fix including edge cases - All 58 tests in test_converse_transformation.py passing
- Extract _filter_json_mode_tools() helper method from _transform_response() - Reduces statement count from 57 to under 50 (fixes PLR0915) - Improves code organization and maintainability - No functional changes
|
Thanks for the review! I've addressed all of your feedback:
Regarding the CI failure: the mypy error in integrations/opentelemetry.py:1013 is a pre-existing issue unrelated to this PR. |
|
@greptile for review |
|
@haggai-backline can you cover the streaming case as well |
Greptile OverviewGreptile Summary
Confidence Score: 4/5
|
| Filename | Overview |
|---|---|
| litellm/llms/bedrock/chat/converse_transformation.py | Refactors JSON-mode tool filtering into _filter_json_mode_tools and filters out internal json_tool_call when real tools are also present; however _transform_response now mutates optional_params by popping json_mode. |
| tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py | Adds regression tests for Bedrock responses containing both json_tool_call and a real tool; tests appear to validate filtering behavior. |
Sequence Diagram
sequenceDiagram
participant U as User
participant L as LiteLLM
participant B as Bedrock Converse API
U->>L: completion(tools + response_format)
L->>L: add internal RESPONSE_FORMAT_TOOL_NAME (json_tool_call)
L->>B: /converse request (tools includes real + json_tool_call)
B-->>L: response with content blocks
L->>L: _translate_message_content()
L->>L: tools[] includes json_tool_call and/or real tools
L->>L: _filter_json_mode_tools(json_mode, tools)
alt only json_tool_call returned
L->>L: parse tool.arguments JSON
L->>L: unwrap {properties: ...} if present
L->>L: set message.content to JSON string
L->>L: clear tool_calls
else json_tool_call + real tools returned
L->>L: remove json_tool_call from tool_calls
L->>L: keep real tool_calls
end
L-->>U: ChatCompletionResponse (internal tool hidden)
Additional Comments (1)
|
|
@shin-bot-litellm can you take over this PR to cover streaming as well? |
… tools (BerriAI#18384) When both `tools` and `response_format` are used with Bedrock Converse, LiteLLM adds an internal `json_tool_call` tool for structured output. Bedrock may return both this internal tool and real user tools, which breaks consumers like the OpenAI Agents SDK. Changes: - Extract filtering logic into `_filter_json_mode_tools()` method - Filter out `json_tool_call` when mixed with real tools in responses - Add streaming support: `AWSEventStreamDecoder` now accepts `json_mode` and suppresses `json_tool_call` chunks, converting arguments to text - Fix `optional_params.pop("json_mode")` -> `.get()` to avoid mutating the caller's dict (affects logging/retries/metrics downstream) - Preserve original behavior of setting `tool_calls=[]` for empty lists
…ls in both streaming and non-streaming
When using both `tools` and `response_format` with Bedrock Converse API, LiteLLM
internally adds a fake tool called `json_tool_call` to handle structured output.
Bedrock may return both this internal tool AND real user-defined tools, causing
consumers like OpenAI Agents SDK to break trying to dispatch `json_tool_call`.
This fix:
- Extracts `_filter_json_mode_tools()` to handle 3 scenarios: only json_tool_call
(convert to content), mixed with real tools (filter it out), or no json_tool_call
- Fixes streaming by adding json_mode awareness to AWSEventStreamDecoder, converting
json_tool_call chunks to text content while passing real tool chunks through
- Changes `optional_params.pop("json_mode")` to `.get()` to avoid mutating caller dict
Fixes BerriAI#18381
Credits @haggai-backline for the original investigation in PR BerriAI#18384
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…ls in both streaming and non-streaming
When using both `tools` and `response_format` with Bedrock Converse API, LiteLLM
internally adds a fake tool called `json_tool_call` to handle structured output.
Bedrock may return both this internal tool AND real user-defined tools, causing
consumers like OpenAI Agents SDK to break trying to dispatch `json_tool_call`.
This fix:
- Extracts `_filter_json_mode_tools()` to handle 3 scenarios: only json_tool_call
(convert to content), mixed with real tools (filter it out), or no json_tool_call
- Fixes streaming by adding json_mode awareness to AWSEventStreamDecoder, converting
json_tool_call chunks to text content while passing real tool chunks through
- Changes `optional_params.pop("json_mode")` to `.get()` to avoid mutating caller dict
Fixes BerriAI#18381
Credits @haggai-backline for the original investigation in PR BerriAI#18384
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Summary
Fixed issue #18381 where Bedrock Converse API returns both
json_tool_call(internal tool for structured output) and real tools whentoolsandresponse_formatparameters are used together.Changes
Modified
_transform_responsemethod inlitellm/llms/bedrock/chat/converse_transformation.py:json_tool_calland real tools are present in the responsejson_tool_callwhen multiple tools are returned and json_mode is enabledjson_tool_callis the only tool (convert to content)json_tool_callimplementation detailAdded comprehensive tests in
tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py:test_transform_response_with_structured_response_calling_tool: Tests scenario where only real tool is calledtest_transform_response_with_both_json_tool_call_and_real_tool: Tests the bug scenario where both tools are returnedjson_tool_callis properly filtered out from the responseRelevant issues
Fixes #18381
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
tests/litellm/directory, Adding at least 1 test is a hard requirement - see detailsmake test-unitType
🐛 Bug Fix
Changes
Problem
When using both
toolsandresponse_formatparameters with Bedrock Converse API, LiteLLM internally adds a fake tool calledjson_tool_callto handle structured output. However, Bedrock sometimes returns both:json_tool_call(internal implementation detail)This caused the response to contain the internal
json_tool_callin the tool_calls array, which should be hidden from the user.Solution
Modified the
_transform_responsemethod to intelligently filter tool calls based on the scenario:json_tool_calland only return real toolsCode Changes
File:
litellm/llms/bedrock/chat/converse_transformation.pyLines 1473-1525: Refactored tool filtering logic to handle three scenarios:
json_tool_callis present in the responsejson_tool_calland return only real toolstool_callsif there are tools remaining after filteringTesting
Added two comprehensive test cases:
test_transform_response_with_structured_response_calling_tool(line 623):test_transform_response_with_both_json_tool_call_and_real_tool(line 750):json_tool_calland real tooljson_tool_callis filtered out and only real tool is returnedAll 58 tests in
test_converse_transformation.pyare passing.Edge Cases Handled
json_tool_callonly → Convert to contentjson_tool_call→ Filter outjson_tool_calljson_tool_call→ Return all toolsBackward Compatibility
This change is fully backward compatible:
json_tool_callis preserved