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.
This commit is contained in:
mateo-berri 2026-05-22 21:54:16 +00:00
parent c59260fc70
commit 0780e5f69f
No known key found for this signature in database
2 changed files with 45 additions and 5 deletions

View file

@ -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()}"

View file

@ -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()