From c6a51d031fc682954adfd24daaee57f7f6168a49 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Mon, 27 Apr 2026 17:46:45 +0530 Subject: [PATCH] Fix greptile review --- .../llms/gemini/realtime/transformation.py | 15 ++++++- .../test_realtime_streaming.py | 44 +++++++++++++++++++ .../test_gemini_realtime_transformation.py | 18 ++++++++ .../test_vertex_ai_realtime_transformation.py | 23 ++++++++++ 4 files changed, 98 insertions(+), 2 deletions(-) diff --git a/litellm/llms/gemini/realtime/transformation.py b/litellm/llms/gemini/realtime/transformation.py index e4cf96eb297..93cdd7c75df 100644 --- a/litellm/llms/gemini/realtime/transformation.py +++ b/litellm/llms/gemini/realtime/transformation.py @@ -667,6 +667,8 @@ class GeminiRealtimeConfig(BaseRealtimeConfig): def transform_tool_call_events( self, tool_call_message: dict, + response_id: Optional[str] = None, + output_item_id: Optional[str] = None, ) -> List[Dict[str, Any]]: """ Transform Gemini toolCall message to OpenAI function call events. @@ -674,6 +676,8 @@ class GeminiRealtimeConfig(BaseRealtimeConfig): Converts Gemini's functionCalls format to OpenAI's response.function_call_arguments.done events. """ function_calls = tool_call_message.get("functionCalls", []) + resolved_response_id = response_id or f"resp_{uuid.uuid4()}" + resolved_output_item_id = output_item_id or f"item_{uuid.uuid4()}" verbose_logger.debug( f"Gemini Realtime: Transforming {len(function_calls)} tool call(s) to OpenAI format" @@ -683,11 +687,14 @@ class GeminiRealtimeConfig(BaseRealtimeConfig): { "type": "response.function_call_arguments.done", "event_id": f"event_{uuid.uuid4()}", + "response_id": resolved_response_id, + "item_id": resolved_output_item_id, + "output_index": idx, "call_id": fc.get("id", ""), "name": fc.get("name", ""), "arguments": json.dumps(fc.get("args", {})), } - for fc in function_calls + for idx, fc in enumerate(function_calls) ] @staticmethod @@ -1067,7 +1074,11 @@ class GeminiRealtimeConfig(BaseRealtimeConfig): returned_message.append(transformed_message) elif openai_event == ResponsesAPIStreamEvents.FUNCTION_CALL_ARGUMENTS_DONE: # Handle toolCall from Gemini - tool_call_events = self.transform_tool_call_events(value) + 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) elif openai_event == OpenAIRealtimeEventTypes.RESPONSE_DONE: transformed_response_done_event = self.transform_response_done_event( diff --git a/tests/test_litellm/litellm_core_utils/test_realtime_streaming.py b/tests/test_litellm/litellm_core_utils/test_realtime_streaming.py index 8d842fefb7b..dab344d45e7 100644 --- a/tests/test_litellm/litellm_core_utils/test_realtime_streaming.py +++ b/tests/test_litellm/litellm_core_utils/test_realtime_streaming.py @@ -254,6 +254,50 @@ async def test_transcription_captured_in_backend_to_client(): assert logging_obj.model_call_details["messages"] == streaming.input_messages +@pytest.mark.asyncio +async def test_client_ack_caches_setup_to_prevent_duplicate_session_update_setup(): + websocket = MagicMock() + backend_ws = MagicMock() + logging_obj = MagicMock() + logging_obj.pre_call = MagicMock() + + # Two session.update messages arrive before setupComplete round-trip. + websocket.receive_text = AsyncMock( + side_effect=[ + json.dumps({"type": "session.update", "session": {"tools": []}}), + json.dumps({"type": "session.update", "session": {"tools": []}}), + Exception("client done"), + ] + ) + + provider_config = MagicMock() + + def _transform(message: str, model: str, session_configuration_request=None): + if session_configuration_request is None: + return [json.dumps({"setup": {"model": "models/gemini-2.5-flash"}})] + return [] + + provider_config.transform_realtime_request = MagicMock(side_effect=_transform) + + backend_ws.send = AsyncMock() + + streaming = RealTimeStreaming( + websocket=websocket, + backend_ws=backend_ws, + logging_obj=logging_obj, + provider_config=provider_config, + model="gemini-2.5-flash", + ) + + await streaming.client_ack_messages() + + # Setup should be forwarded exactly once even with repeated session.update. + assert backend_ws.send.await_count == 1 + assert streaming.session_configuration_request is not None + sent_payload = json.loads(backend_ws.send.await_args_list[0].args[0]) + assert "setup" in sent_payload + + def test_collect_session_tools_from_session_update(): """ Test that tools from session.update events are collected. 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 93ff824aab7..b42f428dd3a 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 @@ -297,6 +297,9 @@ def test_gemini_realtime_tool_call_transformation(): assert function_call_event is not None, "Expected function_call_arguments.done event" assert function_call_event["call_id"] == "call_123" assert function_call_event["name"] == "get_weather" + assert function_call_event["response_id"] == "resp_123" + assert function_call_event["item_id"] == "item_123" + assert function_call_event["output_index"] == 0 # Verify arguments are properly serialized as JSON string args = json.loads(function_call_event["arguments"]) @@ -454,3 +457,18 @@ def test_gemini_realtime_user_text_transformation(): assert len(turn["parts"]) == 1 assert turn["parts"][0]["text"] == "What's the weather in London?" assert client_content["turnComplete"] is True + + +def test_return_new_content_delta_events_without_session_config_does_not_error(): + config = GeminiRealtimeConfig() + + events = config.return_new_content_delta_events( + response_id="resp_1", + output_item_id="item_1", + conversation_id="conv_1", + delta_type="text", + session_configuration_request=None, + ) + + assert len(events) >= 1 + assert events[0]["type"] == "response.created" diff --git a/tests/test_litellm/llms/vertex_ai/realtime/test_vertex_ai_realtime_transformation.py b/tests/test_litellm/llms/vertex_ai/realtime/test_vertex_ai_realtime_transformation.py index e7ce3d343ac..176df19cb3c 100644 --- a/tests/test_litellm/llms/vertex_ai/realtime/test_vertex_ai_realtime_transformation.py +++ b/tests/test_litellm/llms/vertex_ai/realtime/test_vertex_ai_realtime_transformation.py @@ -97,6 +97,29 @@ def test_vertex_requires_session_configuration_feature_flag(monkeypatch): assert cfg.requires_session_configuration() is False +def test_vertex_session_update_defaults_to_audio_modality(): + cfg = VertexAIRealtimeConfig( + access_token="tok", project="my-proj", location="us-central1" + ) + + session_update = { + "type": "session.update", + "session": { + "instructions": "You are a helpful assistant.", + # No modalities provided on purpose + }, + } + + messages = cfg.transform_realtime_request( + json.dumps(session_update), + "gemini-live-2.5-flash-native-audio", + session_configuration_request=None, + ) + assert len(messages) == 1 + setup_payload = json.loads(messages[0])["setup"] + assert setup_payload["generationConfig"]["responseModalities"] == ["AUDIO"] + + # --------------------------------------------------------------------------- # Round-trip test: text-in / text-out via RealTimeStreaming # ---------------------------------------------------------------------------