From 77571e811d637645a30b90fa8ef63c302381695f Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Mon, 27 Apr 2026 19:37:05 +0530 Subject: [PATCH] fix(realtime): emit response.created preamble before tool-call events - Emit response.created, output_item.added, and conversation.item.created for function calls - Ensures OpenAI Realtime API spec compliance - Add test for preamble emission Made-with: Cursor --- .../llms/gemini/realtime/transformation.py | 55 ++++++++++++++++++- .../test_gemini_realtime_transformation.py | 47 ++++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/litellm/llms/gemini/realtime/transformation.py b/litellm/llms/gemini/realtime/transformation.py index 94090950ced..8f67f3f34a3 100644 --- a/litellm/llms/gemini/realtime/transformation.py +++ b/litellm/llms/gemini/realtime/transformation.py @@ -1077,12 +1077,65 @@ class GeminiRealtimeConfig(BaseRealtimeConfig): returned_message.append(transformed_message) elif openai_event == ResponsesAPIStreamEvents.FUNCTION_CALL_ARGUMENTS_DONE: # Handle toolCall from Gemini + # Emit response.created preamble if this is the first event in the response + if current_response_id is None: + current_response_id = f"resp_{uuid.uuid4()}" + current_output_item_id = f"item_{uuid.uuid4()}" + current_conversation_id = current_conversation_id or f"conv_{uuid.uuid4()}" + + # Emit response.created + returned_message.append({ + "type": "response.created", + "event_id": f"event_{uuid.uuid4()}", + "response": { + "object": "realtime.response", + "id": current_response_id, + "status": "in_progress", + "output": [], + "conversation_id": current_conversation_id, + }, + }) + tool_call_events = self.transform_tool_call_events( value, response_id=current_response_id, output_item_id=current_output_item_id, ) - returned_message.extend(tool_call_events) + # Emit output_item.added and conversation.item.created for each function call + for idx, tool_call in enumerate(tool_call_events): + item_id = tool_call["item_id"] + # response.output_item.added + returned_message.append({ + "type": "response.output_item.added", + "event_id": f"event_{uuid.uuid4()}", + "response_id": current_response_id, + "output_index": idx, + "item": { + "id": item_id, + "object": "realtime.item", + "type": "function_call", + "status": "in_progress", + "call_id": tool_call["call_id"], + "name": tool_call["name"], + "arguments": "", + }, + }) + # response.function_call_arguments.done + returned_message.append(tool_call) + # conversation.item.created + returned_message.append({ + "type": "conversation.item.created", + "event_id": f"event_{uuid.uuid4()}", + "item": { + "id": item_id, + "object": "realtime.item", + "type": "function_call", + "status": "completed", + "call_id": tool_call["call_id"], + "name": tool_call["name"], + "arguments": tool_call["arguments"], + }, + }) elif openai_event == OpenAIRealtimeEventTypes.RESPONSE_DONE: transformed_response_done_event = self.transform_response_done_event( message=BidiGenerateContentServerMessage(**json_message), # type: ignore diff --git a/tests/test_litellm/llms/gemini/realtime/test_gemini_realtime_transformation.py b/tests/test_litellm/llms/gemini/realtime/test_gemini_realtime_transformation.py index 201899b8924..17aea5faf61 100644 --- a/tests/test_litellm/llms/gemini/realtime/test_gemini_realtime_transformation.py +++ b/tests/test_litellm/llms/gemini/realtime/test_gemini_realtime_transformation.py @@ -618,3 +618,50 @@ def test_gemini_session_update_includes_input_audio_transcription_default(): assert "setup" in setup assert "inputAudioTranscription" in setup["setup"] assert setup["setup"]["inputAudioTranscription"] == {} + + +def test_gemini_tool_call_emits_response_created_preamble(): + """Verify response.created is emitted before tool call events when response_id is None.""" + config = GeminiRealtimeConfig() + logging_obj = MagicMock() + logging_obj.litellm_trace_id = "trace_123" + + gemini_tool_call = { + "toolCall": { + "functionCalls": [ + { + "id": "call_123", + "name": "get_weather", + "args": {"location": "San Francisco", "unit": "fahrenheit"} + } + ] + } + } + + # Transform with current_response_id=None to trigger preamble emission + result = config.transform_realtime_response( + json.dumps(gemini_tool_call), + "gemini-2.5-flash", + logging_obj, + realtime_response_transform_input={ + "session_configuration_request": None, + "current_output_item_id": None, + "current_response_id": None, + "current_conversation_id": None, + "current_delta_chunks": [], + "current_item_chunks": [], + "current_delta_type": None, + }, + ) + + responses = result["response"] + # Should have: response.created, output_item.added, function_call_arguments.done, conversation.item.created + assert len(responses) >= 4 + assert responses[0]["type"] == "response.created" + assert "response" in responses[0] + assert responses[0]["response"]["status"] == "in_progress" + assert responses[1]["type"] == "response.output_item.added" + assert responses[1]["item"]["type"] == "function_call" + assert responses[2]["type"] == "response.function_call_arguments.done" + assert responses[3]["type"] == "conversation.item.created" + assert responses[3]["item"]["type"] == "function_call"