This commit is contained in:
Ayush Baluni 2026-10-17 08:01:47 -07:00 • committed by GitHub
commit 2d3cd7fce6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 101 additions and 16 deletions

View file

@ -391,7 +391,7 @@ class GeminiRealtimeConfig(BaseRealtimeConfig):
return None
@staticmethod
def _normalize_session_payload_for_mapping(session: dict) -> dict:
def _normalize_session_payload_for_mapping(session: dict) -> dict[str, object]:
"""Normalize GA-remapped session fields back to their beta keys.
``map_openai_params`` only recognises the flat OpenAI-beta key names
@ -472,6 +472,39 @@ class GeminiRealtimeConfig(BaseRealtimeConfig):
)
return setup
def _warn_session_update_dropped(self, session_payload: Mapping[str, object], *, provider: str) -> None:
"""Report, once per session, that a later ``session.update`` reached nothing.
Live accepts ``setup`` as the first-and-only client message, so a second one
closes the socket with 1007 and the update has to be dropped. That is
unavoidable; being quiet about it is not. This used to log at debug, which on
a proxy means the fields never arrive and nothing says so -- the caller sees
the model running on provider defaults and no instructions, voice or persona
of theirs (#44825).
Warned once because a GA client sends several ``session.update`` messages
while configuring; later drops stay at debug.
"""
dropped: Final = sorted(key for key in session_payload if key != "type")
if getattr(self, "_session_update_drop_warned", False):
verbose_logger.debug(
"%s: dropping a further session.update (setup already sent): %s",
provider,
dropped,
)
return
self._session_update_drop_warned = True
verbose_logger.warning(
"%s: dropping session.update because setup was already sent, so these "
"fields do not reach the model: %s. Live accepts setup as the "
"first-and-only client message. To have the client's first "
"session.update carry them instead, set litellm.gemini_live_defer_setup "
"= True (env LITELLM_GEMINI_LIVE_DEFER_SETUP=true), which defers setup "
"until that update arrives. Further drops in this session log at debug.",
provider,
dropped,
)
def _handle_session_update(
self,
json_message: _OpenAIRealtimeClientEvent,
@ -527,7 +560,7 @@ class GeminiRealtimeConfig(BaseRealtimeConfig):
"suppress the model's auto-response mid-session."
)
else:
verbose_logger.debug("Gemini Realtime: Ignoring session.update (setup already sent)")
self._warn_session_update_dropped(session_payload, provider="Gemini Realtime")
return []
def _handle_conversation_item(self, json_message: _OpenAIRealtimeClientEvent) -> list[str]:

View file

@ -17,7 +17,7 @@ Auth: OAuth2 Bearer token (not an API key).
"""
import json
from collections.abc import Awaitable, Callable
from collections.abc import Awaitable, Callable, Mapping
from typing import Final
from litellm import verbose_logger
@ -238,7 +238,10 @@ class VertexAIRealtimeConfig(GeminiRealtimeConfig):
"Vertex AI in non-deferred mode."
)
else:
verbose_logger.debug("Vertex AI Realtime: Ignoring session.update (setup already sent)")
# Annotated rather than passed straight from json.loads, whose Any
# would reach the helper's Mapping parameter as an unknown type.
dropped_session: Final[Mapping[str, object]] = json_message.get("session") or {}
self._warn_session_update_dropped(dropped_session, provider="Vertex AI Realtime")
return []
return super().transform_realtime_request(message, model, session_configuration_request)

View file

@ -2235,3 +2235,34 @@ def test_gemini_realtime_response_done_reports_no_grounding_when_none_ran():
assert input_details.get("web_search_requests") is None
assert input_details.get("google_maps_grounding_requests") is None
def test_gemini_warns_once_when_dropping_a_session_update(caplog):
"""The Gemini path reports a dropped session.update the same way Vertex does.
Both drop it for the same reason -- Live accepts setup as the first-and-only
client message -- so both should say so rather than log at debug and let the
caller discover their instructions never arrived (#44825).
"""
import logging
cfg = GeminiRealtimeConfig()
session_update = json.dumps({"type": "session.update", "session": {"instructions": "Be concise."}})
already_set_up = json.dumps({"setup": {"model": "x"}})
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
for _ in range(2):
cfg.transform_realtime_request(
session_update,
"gemini-live-2.5-flash-preview-native-audio-09-2025",
session_configuration_request=already_set_up,
)
warnings = [
record
for record in caplog.records
if "Gemini Realtime" in record.message and "session.update" in record.message
]
assert len(warnings) == 1
assert "instructions" in warnings[0].message
assert "gemini_live_defer_setup" in warnings[0].message

View file

@ -418,9 +418,19 @@ def test_vertex_warns_when_dropping_guardrail_turn_detection_update(caplog):
)
def test_vertex_does_not_warn_when_dropping_non_guardrail_session_update(caplog):
"""A subsequent session.update without ``create_response: False`` is a
routine drop and should stay at debug level (no warning)."""
def test_vertex_warns_once_when_dropping_a_session_update(caplog):
"""A dropped session.update is reported, and reported once.
This inverts what the test here previously asserted. It held that dropping a
subsequent session.update was "a routine drop" that should stay at debug --
with ``{"instructions": "Be concise."}`` as its own fixture, which is exactly
the case reported in #44825: the caller's instructions never reach the model
and nothing on a default proxy log says so.
The drop itself is unavoidable (Live rejects a second setup with 1007). Only
the silence changes. It warns once because a GA client sends several
session.updates while configuring; the rest stay at debug.
"""
import logging
cfg = VertexAIRealtimeConfig(
@ -429,20 +439,28 @@ def test_vertex_does_not_warn_when_dropping_non_guardrail_session_update(caplog)
session_update = {
"type": "session.update",
"session": {"instructions": "Be concise."},
"session": {"instructions": "Be concise.", "voice": "Puck"},
}
already_set_up = json.dumps({"setup": {"model": "x"}})
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
cfg.transform_realtime_request(
json.dumps(session_update),
"gemini-live-2.5-flash-preview-native-audio-09-2025",
session_configuration_request=json.dumps({"setup": {"model": "x"}}),
)
for _ in range(3):
cfg.transform_realtime_request(
json.dumps(session_update),
"gemini-live-2.5-flash-preview-native-audio-09-2025",
session_configuration_request=already_set_up,
)
assert not any(
"Vertex AI Realtime" in record.message and "session.update" in record.message
warnings = [
record
for record in caplog.records
)
if "Vertex AI Realtime" in record.message and "session.update" in record.message
]
assert len(warnings) == 1, "three drops, one warning"
# the operator needs to know what was lost and what to do about it
assert "instructions" in warnings[0].message
assert "voice" in warnings[0].message
assert "gemini_live_defer_setup" in warnings[0].message
@pytest.mark.asyncio