diff --git a/litellm/litellm_core_utils/prompt_templates/factory.py b/litellm/litellm_core_utils/prompt_templates/factory.py index d8ce48f05de..71e4f0c1a2e 100644 --- a/litellm/litellm_core_utils/prompt_templates/factory.py +++ b/litellm/litellm_core_utils/prompt_templates/factory.py @@ -3682,7 +3682,10 @@ def _convert_to_bedrock_tool_call_invoke( # '{"cmd":"a"}{"cmd":"b"}{"cmd":"c"}' # Split them and emit one toolUse block per object. # Fixes: https://github.com/BerriAI/litellm/issues/20543 - parsed_objects = split_concatenated_json_objects(arguments) + try: + parsed_objects = split_concatenated_json_objects(arguments) + except json.JSONDecodeError: + parsed_objects = [] if parsed_objects: # First object keeps the original tool id. for obj_idx, obj in enumerate(parsed_objects): @@ -3718,8 +3721,13 @@ def _convert_to_bedrock_tool_call_invoke( _parts_list.append(cache_point_block) return _parts_list except Exception as e: - raise Exception( - "Unable to convert openai tool calls={} to bedrock tool calls. Received error={}".format(tool_calls, str(e)) + tool_call_ids = [tool.get("id") for tool in tool_calls if isinstance(tool, dict)] + raise litellm.BadRequestError( + message="Unable to convert openai tool calls with ids={} to bedrock tool calls. Received error={}".format( + tool_call_ids, str(e) + ), + model=model or "", + llm_provider="bedrock", ) diff --git a/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py index 9565de1139c..4c754f4056c 100644 --- a/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py +++ b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py @@ -2286,6 +2286,44 @@ def test_bedrock_tool_call_invoke_non_dict_arguments(): assert result[0]["toolUse"]["input"] == {} +def test_bedrock_tool_call_invoke_truncated_json_arguments(): + """ + Truncated tool call arguments (issue #35303) must not raise. A client replaying a + partially streamed tool call would otherwise trigger a pre-network exception that the + router maps to a retryable APIConnectionError and retries through the fallback graph. + """ + tool_calls = [ + { + "id": "tooluse_MAh2QLVjBRkvi5QJkLQ08V", + "type": "function", + "function": { + "name": "replace_note_content", + "arguments": '{"note_id": "999af35c-4061-4ece-8581-7d43fc988ba4", "title": "WG"', + }, + } + ] + result = _convert_to_bedrock_tool_call_invoke(tool_calls) + assert len(result) == 1 + assert result[0]["toolUse"]["toolUseId"] == "tooluse_MAh2QLVjBRkvi5QJkLQ08V" + assert result[0]["toolUse"]["input"] == {} + + +def test_bedrock_tool_call_invoke_unconvertible_raises_non_retryable_bad_request(): + """ + Conversion failures are client input errors, so they must surface as a non-retryable + BadRequestError instead of a bare Exception that maps to APIConnectionError, and the + message must not embed the tool call payload (issue #35303). + """ + tool_calls = [{"id": "call_bad", "type": "function", "function": None}] + + with pytest.raises(litellm.BadRequestError) as exc_info: + _convert_to_bedrock_tool_call_invoke(tool_calls) + + assert exc_info.value.status_code == 400 + assert "call_bad" in str(exc_info.value) + assert "function" not in str(exc_info.value).split("Received error=")[0] + + def test_make_valid_bedrock_tool_name_preserves_hyphens(): assert make_valid_bedrock_tool_name("my-tool") == "my-tool" assert (