From 053973160d79072f1788d93d400386d473401b43 Mon Sep 17 00:00:00 2001 From: mubashir1osmani Date: Wed, 5 Aug 2026 15:28:03 -0700 Subject: [PATCH] fix(bedrock/realtime): skip message output_item.done on TOOL contentEnd Minted tool ids must not unlock unpaired message completion events after TOOL-first turns; only text/audio content ends emit output_item.done --- .../llms/bedrock/realtime/transformation.py | 5 +- .../test_bedrock_realtime_transformation.py | 76 ++++++++++++++++--- 2 files changed, 70 insertions(+), 11 deletions(-) diff --git a/litellm/llms/bedrock/realtime/transformation.py b/litellm/llms/bedrock/realtime/transformation.py index 3902c791f06..1510211c77c 100644 --- a/litellm/llms/bedrock/realtime/transformation.py +++ b/litellm/llms/bedrock/realtime/transformation.py @@ -871,6 +871,9 @@ class BedrockRealtimeConfig(BaseRealtimeConfig): if not current_output_item_id or not current_response_id: return [], current_delta_chunks + if content_end.get("type") == "TOOL" or current_delta_type not in ("text", "audio"): + return [], current_delta_chunks + returned_messages: list[OpenAIRealtimeEvents] = [] # Send appropriate done event based on type @@ -903,7 +906,7 @@ class BedrockRealtimeConfig(BaseRealtimeConfig): ) returned_messages.append(content_part_done) - elif current_delta_type == "audio": + else: audio_done = OpenAIRealtimeResponseAudioDone( type="response.audio.done", content_index=0, 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 521bcfa3992..9b7456c5155 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 @@ -684,6 +684,35 @@ class TestBedrockRealtimeResponseTransformation: logging_obj = MagicMock() logging_obj.litellm_trace_id = "trace_123" + state = { + "session_configuration_request": json.dumps({"configured": True}), + "current_output_item_id": None, + "current_response_id": None, + "current_conversation_id": "conv_123", + "current_delta_chunks": [], + "current_item_chunks": [], + "current_delta_type": None, + } + + content_start_result = config.transform_realtime_response( + json.dumps({"event": {"contentStart": {"role": "TOOL", "type": "TOOL"}}}), + "amazon.nova-2-sonic-v1:0", + logging_obj, + realtime_response_transform_input=state, + ) + assert content_start_result["response"] == [] + assert content_start_result["current_delta_type"] is None + state.update( + { + "current_output_item_id": content_start_result["current_output_item_id"], + "current_response_id": content_start_result["current_response_id"], + "current_conversation_id": content_start_result["current_conversation_id"], + "current_delta_chunks": content_start_result["current_delta_chunks"], + "current_item_chunks": content_start_result["current_item_chunks"], + "current_delta_type": content_start_result["current_delta_type"], + } + ) + tool_use_message = { "event": { "toolUse": { @@ -698,15 +727,7 @@ class TestBedrockRealtimeResponseTransformation: json.dumps(tool_use_message), "amazon.nova-2-sonic-v1:0", logging_obj, - realtime_response_transform_input={ - "session_configuration_request": json.dumps({"configured": True}), - "current_output_item_id": None, - "current_response_id": None, - "current_conversation_id": "conv_123", - "current_delta_chunks": [], - "current_item_chunks": [], - "current_delta_type": "text", - }, + realtime_response_transform_input=state, ) assert len(result["response"]) == 1 @@ -720,7 +741,6 @@ class TestBedrockRealtimeResponseTransformation: assert function_call["item_id"] == result["current_output_item_id"] assert json.loads(function_call["arguments"]) == {"location": "Seattle"} - # A follow-up contentEnd must reuse the same persisted ids content_end_message = { "event": { "contentEnd": { @@ -745,6 +765,42 @@ class TestBedrockRealtimeResponseTransformation: ) assert follow_up["current_response_id"] == result["current_response_id"] assert follow_up["current_output_item_id"] == result["current_output_item_id"] + assert follow_up["response"] == [] + assert all(msg["type"] != "response.output_item.done" for msg in follow_up["response"]) + + def test_tool_content_end_does_not_emit_message_output_item_done(self): + """Minted tool ids must not unlock unpaired message output_item.done on TOOL contentEnd""" + config = BedrockRealtimeConfig() + logging_obj = MagicMock() + logging_obj.litellm_trace_id = "trace_123" + + content_end_message = { + "event": { + "contentEnd": { + "stopReason": "TOOL_USE", + "type": "TOOL", + } + } + } + result = config.transform_realtime_response( + json.dumps(content_end_message), + "amazon.nova-2-sonic-v1:0", + logging_obj, + realtime_response_transform_input={ + "session_configuration_request": json.dumps({"configured": True}), + "current_output_item_id": "item_minted_for_tool", + "current_response_id": "resp_minted_for_tool", + "current_conversation_id": "conv_123", + "current_delta_chunks": [], + "current_item_chunks": [], + "current_delta_type": "text", + }, + ) + + assert result["response"] == [] + assert result["current_response_id"] == "resp_minted_for_tool" + assert result["current_output_item_id"] == "item_minted_for_tool" + assert result["current_delta_type"] == "text" def test_transform_content_end_text(self): """Test contentEnd for text response"""