From bf99cea82fd8bb437188a25abf7c528d368c56d2 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Fri, 16 Jan 2026 14:33:25 +0530 Subject: [PATCH] Fix malformed tool call tranform --- .../prompt_templates/factory.py | 20 ++- .../bedrock/chat/converse_transformation.py | 9 +- litellm/types/llms/bedrock.py | 2 +- .../test_bedrock_completion.py | 154 ++++++++++++++++++ 4 files changed, 175 insertions(+), 10 deletions(-) diff --git a/litellm/litellm_core_utils/prompt_templates/factory.py b/litellm/litellm_core_utils/prompt_templates/factory.py index 4320f75645..01bf18d79b 100644 --- a/litellm/litellm_core_utils/prompt_templates/factory.py +++ b/litellm/litellm_core_utils/prompt_templates/factory.py @@ -3233,17 +3233,21 @@ def _convert_to_bedrock_tool_call_invoke( id = tool["id"] name = tool["function"].get("name", "") arguments = tool["function"].get("arguments", "") - arguments_dict = json.loads(arguments) if arguments else {} - # Ensure arguments_dict is always a dict (Bedrock requires toolUse.input to be an object) - # When some providers return arguments: '""' (JSON-encoded empty string), json.loads returns "" - if not isinstance(arguments_dict, dict): - arguments_dict = {} if not arguments or not arguments.strip(): - arguments_dict = {} + arguments_input = {} else: - arguments_dict = json.loads(arguments) + # Try to parse the arguments JSON + try: + arguments_input = json.loads(arguments) + except json.JSONDecodeError as e: + verbose_logger.warning( + f"Malformed JSON in tool call arguments for tool '{name}': {str(e)}. " + f"Storing as raw string to allow conversation to continue." + ) + arguments_input = arguments + bedrock_tool = BedrockToolUseBlock( - input=arguments_dict, name=name, toolUseId=id + input=arguments_input, name=name, toolUseId=id ) bedrock_content_block = BedrockContentBlock(toolUse=bedrock_tool) _parts_list.append(bedrock_content_block) diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index 59590e464f..9bc1e8c85e 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -1395,9 +1395,16 @@ class AmazonConverseConfig(BaseConfig): response_tool_name = get_bedrock_tool_name( response_tool_name=_response_tool_name ) + tool_input = content["toolUse"]["input"] + if isinstance(tool_input, str): + arguments_str = tool_input + else: + # Otherwise, serialize it to JSON + arguments_str = json.dumps(tool_input) + _function_chunk = ChatCompletionToolCallFunctionChunk( name=response_tool_name, - arguments=json.dumps(content["toolUse"]["input"]), + arguments=arguments_str, ) _tool_response_chunk = ChatCompletionToolCallChunk( diff --git a/litellm/types/llms/bedrock.py b/litellm/types/llms/bedrock.py index ef2f1ba4d5..e0858898ea 100644 --- a/litellm/types/llms/bedrock.py +++ b/litellm/types/llms/bedrock.py @@ -62,7 +62,7 @@ class ToolResultBlock(TypedDict, total=False): class ToolUseBlock(TypedDict): - input: dict + input: Any # Per boto3 spec: document type can be dict, list, int, float, str, bool, or None name: str toolUseId: str diff --git a/tests/llm_translation/test_bedrock_completion.py b/tests/llm_translation/test_bedrock_completion.py index 7c0db41d13..f08060214c 100644 --- a/tests/llm_translation/test_bedrock_completion.py +++ b/tests/llm_translation/test_bedrock_completion.py @@ -3954,3 +3954,157 @@ def test_bedrock_openai_error_handling(): assert exc_info.value.status_code == 422 print("✓ Error handling works correctly") + + +def test_bedrock_malformed_tool_json_handling(): + """ + Test that Bedrock handles malformed JSON in tool call arguments gracefully. + + This test covers the issue where: + 1. LLM generates malformed JSON in tool call arguments + 2. Subsequent requests with conversation history should not crash + 3. The toolUse.input field should handle any JSON value type per boto3 spec + + Related issue: https://github.com/BerriAI/litellm/issues/[issue_number] + """ + from litellm.litellm_core_utils.prompt_templates.factory import ( + _convert_to_bedrock_tool_call_invoke, + ) + from litellm.llms.bedrock.chat.converse_transformation import AmazonConverseConfig + from litellm.types.llms.bedrock import ContentBlock + + # Test 1: Malformed JSON in tool call arguments + malformed_tool_calls = [ + { + "id": "call_123", + "type": "function", + "function": { + "name": "get_weather", + "arguments": '{"location": "Paris", "invalid_json', # Malformed JSON + }, + } + ] + + # Should not raise an exception, but store as raw string + result = _convert_to_bedrock_tool_call_invoke(malformed_tool_calls) + assert len(result) == 1 + assert result[0]["toolUse"]["name"] == "get_weather" + # The malformed JSON should be stored as a string + assert isinstance(result[0]["toolUse"]["input"], str) + assert result[0]["toolUse"]["input"] == '{"location": "Paris", "invalid_json' + print("✓ Malformed JSON stored as raw string") + + # Test 2: Valid JSON should still work normally + valid_tool_calls = [ + { + "id": "call_456", + "type": "function", + "function": { + "name": "get_weather", + "arguments": '{"location": "London"}', + }, + } + ] + + result = _convert_to_bedrock_tool_call_invoke(valid_tool_calls) + assert len(result) == 1 + assert result[0]["toolUse"]["name"] == "get_weather" + assert isinstance(result[0]["toolUse"]["input"], dict) + assert result[0]["toolUse"]["input"] == {"location": "London"} + print("✓ Valid JSON parsed correctly") + + # Test 3: Empty arguments should create empty dict + empty_tool_calls = [ + { + "id": "call_789", + "type": "function", + "function": { + "name": "no_args_function", + "arguments": "", + }, + } + ] + + result = _convert_to_bedrock_tool_call_invoke(empty_tool_calls) + assert len(result) == 1 + assert result[0]["toolUse"]["input"] == {} + print("✓ Empty arguments handled correctly") + + # Test 4: Bedrock to OpenAI conversion handles string input + converse_config = AmazonConverseConfig() + content_blocks = [ + ContentBlock( + toolUse={ + "name": "get_weather", + "toolUseId": "call_123", + "input": '{"location": "Paris", "invalid_json', # String input (malformed) + } + ) + ] + + content_str, tools, reasoning = converse_config._translate_message_content( + content_blocks + ) + assert len(tools) == 1 + assert tools[0]["function"]["name"] == "get_weather" + # Should return the string as-is + assert tools[0]["function"]["arguments"] == '{"location": "Paris", "invalid_json' + print("✓ Bedrock to OpenAI conversion handles string input") + + # Test 5: Bedrock to OpenAI conversion handles dict input + content_blocks_dict = [ + ContentBlock( + toolUse={ + "name": "get_weather", + "toolUseId": "call_456", + "input": {"location": "London"}, # Dict input (normal case) + } + ) + ] + + content_str, tools, reasoning = converse_config._translate_message_content( + content_blocks_dict + ) + assert len(tools) == 1 + assert tools[0]["function"]["name"] == "get_weather" + # Should serialize dict to JSON string + assert tools[0]["function"]["arguments"] == '{"location": "London"}' + print("✓ Bedrock to OpenAI conversion handles dict input") + + # Test 6: Round-trip conversion with malformed JSON + # Test that we can convert OpenAI -> Bedrock -> OpenAI with malformed JSON + malformed_tool_calls_roundtrip = [ + { + "id": "call_999", + "type": "function", + "function": { + "name": "test_function", + "arguments": '{"key": "value", "broken', # Malformed + }, + } + ] + + # Step 1: OpenAI to Bedrock (should store as string) + bedrock_blocks = _convert_to_bedrock_tool_call_invoke(malformed_tool_calls_roundtrip) + assert isinstance(bedrock_blocks[0]["toolUse"]["input"], str) + + # Step 2: Bedrock back to OpenAI (should preserve the string) + content_blocks_roundtrip = [ + ContentBlock( + toolUse={ + "name": bedrock_blocks[0]["toolUse"]["name"], + "toolUseId": bedrock_blocks[0]["toolUse"]["toolUseId"], + "input": bedrock_blocks[0]["toolUse"]["input"], + } + ) + ] + + content_str, tools_roundtrip, reasoning = converse_config._translate_message_content( + content_blocks_roundtrip + ) + + # Should preserve the malformed JSON string through the round trip + assert tools_roundtrip[0]["function"]["arguments"] == '{"key": "value", "broken' + print("✓ Round-trip conversion preserves malformed JSON") + + print("✓ All malformed JSON handling tests passed")