From 06cbbbb79aedc9baafc78c4f9aa296fc5cef6c8e Mon Sep 17 00:00:00 2001 From: mubashir1osmani Date: Mon, 3 Aug 2026 16:15:32 -0700 Subject: [PATCH] fix(bedrock/realtime): mint response/item ids so toolUse is not dropped Nova Sonic starts tool turns with contentStart role=TOOL, so ASSISTANT-only id init never runs and transform_tool_use_event used to return empty events even after parsing content. Mint resp_/item_ ids when missing so function_call_arguments.done reaches the client with non-empty args --- .../llms/bedrock/realtime/transformation.py | 12 +++----- .../test_bedrock_realtime_transformation.py | 28 +++++++++++++++---- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/litellm/llms/bedrock/realtime/transformation.py b/litellm/llms/bedrock/realtime/transformation.py index 4435fba9d2d..b0b2c4f0201 100644 --- a/litellm/llms/bedrock/realtime/transformation.py +++ b/litellm/llms/bedrock/realtime/transformation.py @@ -1026,11 +1026,9 @@ class BedrockRealtimeConfig(BaseRealtimeConfig): verbose_logger.debug("Handling toolUse") tool_use = event["toolUse"] - if not current_output_item_id or not current_response_id: - return [], "", "" + response_id = current_response_id or f"resp_{uuid.uuid4()}" + item_id = current_output_item_id or f"item_{uuid.uuid4()}" - # Parse the tool input. Nova 2 Sonic sends arguments in `content`; - # fall back to `input` for backward compatibility. tool_input = {} raw_input = tool_use["content"] if "content" in tool_use else tool_use.get("input") if raw_input: @@ -1042,15 +1040,13 @@ class BedrockRealtimeConfig(BaseRealtimeConfig): tool_call_id = tool_use.get("toolUseId", "") tool_name = tool_use.get("toolName", "") - # Create a function call arguments done event - # This is a custom event format that matches what clients expect from typing import cast function_call_event: dict[str, Any] = { "type": "response.function_call_arguments.done", "event_id": f"event_{uuid.uuid4()}", - "response_id": current_response_id, - "item_id": current_output_item_id, + "response_id": response_id, + "item_id": item_id, "output_index": 0, "call_id": tool_call_id, "name": tool_name, diff --git a/tests/test_litellm/llms/bedrock/realtime/test_bedrock_realtime_transformation.py b/tests/test_litellm/llms/bedrock/realtime/test_bedrock_realtime_transformation.py index 6611f3b431e..e45a39187e0 100644 --- a/tests/test_litellm/llms/bedrock/realtime/test_bedrock_realtime_transformation.py +++ b/tests/test_litellm/llms/bedrock/realtime/test_bedrock_realtime_transformation.py @@ -614,16 +614,30 @@ class TestBedrockRealtimeResponseTransformation: assert args["location"] == "San Francisco" def test_transform_tool_use_event_directly(self): - """Test transform_tool_use_event directly for guard clause and input parsing""" + """Test transform_tool_use_event directly for input parsing and missing IDs""" config = BedrockRealtimeConfig() - # Guard clause: missing IDs returns empty result + # Missing IDs still emit a function call (Nova Sonic starts tools with role=TOOL) events, tool_call_id, tool_name = config.transform_tool_use_event( - {"toolUse": {}}, None, "resp_123" + { + "toolUse": { + "toolUseId": "tool_call_no_ids", + "toolName": "get_weather", + "content": json.dumps({"location": "Seattle"}), + } + }, + None, + None, ) - assert events == [] - assert tool_call_id == "" - assert tool_name == "" + assert len(events) == 1 + assert events[0]["type"] == "response.function_call_arguments.done" + assert events[0]["call_id"] == "tool_call_no_ids" + assert events[0]["name"] == "get_weather" + assert events[0]["response_id"].startswith("resp_") + assert events[0]["item_id"].startswith("item_") + assert json.loads(events[0]["arguments"]) == {"location": "Seattle"} + assert tool_call_id == "tool_call_no_ids" + assert tool_name == "get_weather" # JSON string content is parsed and converted to a function call event events, tool_call_id, tool_name = config.transform_tool_use_event( @@ -641,6 +655,8 @@ class TestBedrockRealtimeResponseTransformation: assert events[0]["type"] == "response.function_call_arguments.done" assert events[0]["call_id"] == "tool_call_123" assert events[0]["name"] == "get_weather" + assert events[0]["response_id"] == "resp_123" + assert events[0]["item_id"] == "item_123" assert json.loads(events[0]["arguments"]) == {"location": "San Francisco"} # Invalid JSON content falls back to empty arguments