diff --git a/litellm/llms/gemini/realtime/transformation.py b/litellm/llms/gemini/realtime/transformation.py index 3f09c19d3d2..9a13e4f96d5 100644 --- a/litellm/llms/gemini/realtime/transformation.py +++ b/litellm/llms/gemini/realtime/transformation.py @@ -1528,6 +1528,23 @@ class GeminiRealtimeConfig(BaseRealtimeConfig): else: raise ValueError(f"Unknown openai event: {openai_event}") if len(returned_message) == 0: + # A frame whose only top-level keys are sibling metadata (e.g. + # a standalone ``{"usageMetadata": {...}}`` emitted by Gemini + # Live between turns) is not an error — there is just nothing + # to forward to the OpenAI-shaped client. Returning the + # unchanged state keeps the WebSocket alive; raising would + # terminate the session for a benign no-op frame. + if not any(key in _KNOWN_GEMINI_TOP_LEVEL_KEYS for key in json_message): + 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 isinstance(message, bytes): message_str = message.decode("utf-8", errors="replace") else: 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 c386845ce90..1d0ae30f275 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 @@ -1145,3 +1145,43 @@ def test_gemini_tool_call_id_to_name_evicts_oldest_when_capped(): "call_6", "call_7", ] + + +def test_gemini_standalone_usage_metadata_does_not_crash_websocket(): + """A Gemini frame containing only sibling metadata (e.g. a standalone + ``usageMetadata`` block emitted between turns) must not trip the + ``Unknown message type`` guard — that would terminate the WebSocket + session on a benign no-op frame.""" + config = GeminiRealtimeConfig() + logging_obj = MagicMock() + logging_obj.litellm_trace_id = "trace_usage_only" + + result = config.transform_realtime_response( + json.dumps( + { + "usageMetadata": { + "promptTokenCount": 12, + "responseTokenCount": 34, + "totalTokenCount": 46, + } + } + ), + "gemini-2.5-flash", + logging_obj, + realtime_response_transform_input={ + "session_configuration_request": None, + "current_output_item_id": "item_existing", + "current_response_id": "resp_existing", + "current_conversation_id": "conv_existing", + "current_delta_chunks": [], + "current_item_chunks": [], + "current_delta_type": None, + }, + ) + + assert result["response"] == [] + # State must be returned unchanged so subsequent frames continue the + # in-flight response correctly. + assert result["current_output_item_id"] == "item_existing" + assert result["current_response_id"] == "resp_existing" + assert result["current_conversation_id"] == "conv_existing"