fix(gemini realtime): tolerate sibling-only frames (e.g. standalone usageMetadata)

A Gemini Live frame that contains only metadata keys outside
_KNOWN_GEMINI_TOP_LEVEL_KEYS (e.g. a bare {"usageMetadata": {...}}
emitted between turns) leaves returned_message empty after the
transform loop and was tripping the 'Unknown message type' guard,
which raised ValueError and terminated the WebSocket session.

Treat such frames as no-ops and return the unchanged state instead.
This commit is contained in:
mateo-berri 2026-05-22 23:54:40 +00:00 committed by Claude
parent 1e4f86e19a
commit 84791fc3f5
No known key found for this signature in database
2 changed files with 57 additions and 0 deletions

View file

@ -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:

View file

@ -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"