Revert "Fix: malformed tool call transformation in bedrock"

This commit is contained in:
YutaSaito
2026-01-17 06:04:24 +09:00
committed by GitHub
parent 5908cd79d8
commit ca2019776e
4 changed files with 10 additions and 175 deletions
@@ -3233,21 +3233,17 @@ 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_input = {}
arguments_dict = {}
else:
# 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
arguments_dict = json.loads(arguments)
bedrock_tool = BedrockToolUseBlock(
input=arguments_input, name=name, toolUseId=id
input=arguments_dict, name=name, toolUseId=id
)
bedrock_content_block = BedrockContentBlock(toolUse=bedrock_tool)
_parts_list.append(bedrock_content_block)
@@ -1395,16 +1395,9 @@ 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=arguments_str,
arguments=json.dumps(content["toolUse"]["input"]),
)
_tool_response_chunk = ChatCompletionToolCallChunk(
+1 -1
View File
@@ -62,7 +62,7 @@ class ToolResultBlock(TypedDict, total=False):
class ToolUseBlock(TypedDict):
input: Any # Per boto3 spec: document type can be dict, list, int, float, str, bool, or None
input: dict
name: str
toolUseId: str
@@ -3954,157 +3954,3 @@ 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")