From 0780e5f69fa3e5cbe9a58f0c8220b87ab6e20238 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Fri, 22 May 2026 21:54:16 +0000 Subject: [PATCH] fix(gemini realtime): empty toolCall must not terminate the WebSocket If Gemini sends a toolCall whose functionCalls list is empty (or absent), the previous `continue` left returned_message empty and the "Unknown message type" guard fired, killing the WebSocket session. Return a normal (empty) result instead so the session keeps going. --- .../llms/gemini/realtime/transformation.py | 22 +++++++++++---- .../test_gemini_realtime_transformation.py | 28 +++++++++++++++++++ 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/litellm/llms/gemini/realtime/transformation.py b/litellm/llms/gemini/realtime/transformation.py index 2a8f4457e9c..12b7f877579 100644 --- a/litellm/llms/gemini/realtime/transformation.py +++ b/litellm/llms/gemini/realtime/transformation.py @@ -1238,12 +1238,24 @@ class GeminiRealtimeConfig(BaseRealtimeConfig): ) returned_message.append(transformed_message) elif openai_event == ResponsesAPIStreamEvents.FUNCTION_CALL_ARGUMENTS_DONE: - # Handle toolCall from Gemini. Skip entirely if there are no - # function calls in the payload — emitting an orphaned - # response.created/response.done pair with no output items - # would confuse OpenAI-compatible clients. + # Handle toolCall from Gemini. If the payload has no function + # calls, emit nothing — an orphaned response.created/done pair + # with no output items would confuse OpenAI-compatible clients. + # Return rather than ``continue`` so a toolCall that is the + # only key in the message doesn't leave ``returned_message`` + # empty and trip the "Unknown message type" guard below + # (which would terminate the WebSocket session). if not value.get("functionCalls"): - continue + return { + "response": returned_message, + "current_output_item_id": current_output_item_id, + "current_response_id": current_response_id, + "current_delta_chunks": current_delta_chunks, + "current_conversation_id": current_conversation_id, + "current_item_chunks": current_item_chunks, + "current_delta_type": current_delta_type, + "session_configuration_request": session_configuration_request, + } if current_conversation_id is None: current_conversation_id = f"conv_{uuid.uuid4()}" 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 f1dae60379a..9db244834c8 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 @@ -826,6 +826,34 @@ def test_gemini_tool_call_resets_ids_for_post_tool_model_turn(): ) +def test_gemini_empty_tool_call_does_not_crash_websocket(): + """A toolCall payload with no functionCalls must not raise the + 'Unknown message type' guard — that would terminate the WebSocket session + on what is at worst a benign no-op from Gemini.""" + config = GeminiRealtimeConfig() + logging_obj = MagicMock() + logging_obj.litellm_trace_id = "trace_empty_tool_call" + + result = config.transform_realtime_response( + json.dumps({"toolCall": {"functionCalls": []}}), + "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, + }, + ) + + assert result["response"] == [] + assert result["current_response_id"] is None + assert result["current_output_item_id"] is None + + def test_gemini_function_call_output_includes_name(): """Verify function_call_output includes name field from stored mapping.""" config = GeminiRealtimeConfig()