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
This commit is contained in:
mubashir1osmani 2026-08-03 16:15:32 -07:00
parent 95239b9c68
commit 06cbbbb79a
2 changed files with 26 additions and 14 deletions

View file

@ -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,

View file

@ -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