From 91c0da5644dfbbd6217cbc683131476ddda89d30 Mon Sep 17 00:00:00 2001 From: aayushbaluni <73417844+aayushbaluni@users.noreply.github.com> Date: Thu, 8 Oct 2026 13:16:56 +0530 Subject: [PATCH 1/5] fix(gemini/vertex realtime): say when a session.update is dropped A session.update that arrives after setup cannot be forwarded: Live accepts setup as the first-and-only client message and answers a second one with 1007, closing the socket. Dropping it is right. Doing so at debug level is what #44825 is about -- on a proxy the caller's instructions, voice and persona never reach the model, the session runs on provider defaults, and nothing in the log says so. The reporter had to find gemini_live_defer_setup by reading the source. Both paths now warn once per session, naming the fields that were dropped and the flag that would carry them: Vertex AI Realtime: dropping session.update because setup was already sent, so these fields do not reach the model: ['instructions', 'voice']. ... set litellm.gemini_live_defer_setup = True (env LITELLM_GEMINI_LIVE_DEFER_SETUP=true) ... Once, because a GA client sends several session.updates while configuring a session; the rest stay at debug. The helper lives on GeminiRealtimeConfig so the Vertex subclass inherits it, and the turn_detection branch that already warned is untouched. No behaviour change: the same messages are dropped, and the same setup is sent. This inverts test_vertex_does_not_warn_when_dropping_non_guardrail_session_update, which asserted the silence was intended -- with {"instructions": "Be concise."} as its own fixture, which is the reported case. Renamed, with the reasoning in its docstring, and the PR says so. Closes #44825 --- .../llms/gemini/realtime/transformation.py | 35 +++++++++++++++- .../llms/vertex_ai/realtime/transformation.py | 2 +- .../test_gemini_realtime_transformation.py | 31 ++++++++++++++ .../test_vertex_ai_realtime_transformation.py | 42 +++++++++++++------ 4 files changed, 96 insertions(+), 14 deletions(-) diff --git a/litellm/llms/gemini/realtime/transformation.py b/litellm/llms/gemini/realtime/transformation.py index 7c04ebf311f..9d9c2ea35ba 100644 --- a/litellm/llms/gemini/realtime/transformation.py +++ b/litellm/llms/gemini/realtime/transformation.py @@ -463,6 +463,39 @@ class GeminiRealtimeConfig(BaseRealtimeConfig): ) return setup + def _warn_session_update_dropped(self, session_payload: dict, *, 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, @@ -518,7 +551,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]: diff --git a/litellm/llms/vertex_ai/realtime/transformation.py b/litellm/llms/vertex_ai/realtime/transformation.py index c14dd535145..408ffcc274c 100644 --- a/litellm/llms/vertex_ai/realtime/transformation.py +++ b/litellm/llms/vertex_ai/realtime/transformation.py @@ -235,7 +235,7 @@ class VertexAIRealtimeConfig(GeminiRealtimeConfig): "Vertex AI in non-deferred mode." ) else: - verbose_logger.debug("Vertex AI Realtime: Ignoring session.update (setup already sent)") + self._warn_session_update_dropped(json_message.get("session") or {}, provider="Vertex AI Realtime") return [] return super().transform_realtime_request(message, model, session_configuration_request) diff --git a/tests/unit/llms/gemini/realtime/test_gemini_realtime_transformation.py b/tests/unit/llms/gemini/realtime/test_gemini_realtime_transformation.py index bcd5f3d8d19..852158203da 100644 --- a/tests/unit/llms/gemini/realtime/test_gemini_realtime_transformation.py +++ b/tests/unit/llms/gemini/realtime/test_gemini_realtime_transformation.py @@ -2201,3 +2201,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 diff --git a/tests/unit/llms/vertex_ai/realtime/test_vertex_ai_realtime_transformation.py b/tests/unit/llms/vertex_ai/realtime/test_vertex_ai_realtime_transformation.py index d6986a2697a..edd11baa26f 100644 --- a/tests/unit/llms/vertex_ai/realtime/test_vertex_ai_realtime_transformation.py +++ b/tests/unit/llms/vertex_ai/realtime/test_vertex_ai_realtime_transformation.py @@ -382,9 +382,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( @@ -393,20 +403,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 From 4399854e2878547a6820198838ff90857e7ec752 Mon Sep 17 00:00:00 2001 From: aayushbaluni <73417844+aayushbaluni@users.noreply.github.com> Date: Thu, 8 Oct 2026 14:03:01 +0530 Subject: [PATCH 2/5] fix(gemini realtime): annotate the warning helper's payload as Mapping The type-discipline gate counts a bare `dict` annotation as LIT001, and the new helper pushed the repo total from 22153 to 22154. Mapping[str, object] is also the more accurate type: the helper only reads keys off the payload to name what was dropped. The file already uses Mapping for the same reason in _gemini_live_speech_config and _grounding_metadata_from_frame, so this follows rather than suppresses -- no `# mutable-ok` needed. --- litellm/llms/gemini/realtime/transformation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/llms/gemini/realtime/transformation.py b/litellm/llms/gemini/realtime/transformation.py index 9d9c2ea35ba..48b78b35f99 100644 --- a/litellm/llms/gemini/realtime/transformation.py +++ b/litellm/llms/gemini/realtime/transformation.py @@ -463,7 +463,7 @@ class GeminiRealtimeConfig(BaseRealtimeConfig): ) return setup - def _warn_session_update_dropped(self, session_payload: dict, *, provider: str) -> None: + 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 From 718a36530ec5cb14b9bddcf38c33e76dda7de09d Mon Sep 17 00:00:00 2001 From: aayushbaluni <73417844+aayushbaluni@users.noreply.github.com> Date: Thu, 8 Oct 2026 14:14:43 +0530 Subject: [PATCH 3/5] fix(vertex realtime): give the dropped-session payload a declared type The Mapping annotation on the helper cleared LIT001 but traded it for one basedpyright reportUnknownArgumentType: json_message comes from json.loads, so `json_message.get("session") or {}` arrives as Any and the gate counts passing it into a typed parameter. Binding an annotated local declares the type at the boundary, which is what the value is, and avoids a cast -- the type-discipline gate counts those too and a `# cast-ok` would be a suppression where a declaration does the job. --- litellm/llms/vertex_ai/realtime/transformation.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/litellm/llms/vertex_ai/realtime/transformation.py b/litellm/llms/vertex_ai/realtime/transformation.py index 408ffcc274c..671f21c7a04 100644 --- a/litellm/llms/vertex_ai/realtime/transformation.py +++ b/litellm/llms/vertex_ai/realtime/transformation.py @@ -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 @@ -235,7 +235,10 @@ class VertexAIRealtimeConfig(GeminiRealtimeConfig): "Vertex AI in non-deferred mode." ) else: - self._warn_session_update_dropped(json_message.get("session") or {}, provider="Vertex AI Realtime") + # Annotated rather than passed straight from json.loads, whose Any + # would reach the helper's Mapping parameter as an unknown type. + dropped_session: 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) From 751527e27687f122327a449d59cb8818970a4541 Mon Sep 17 00:00:00 2001 From: aayushbaluni <73417844+aayushbaluni@users.noreply.github.com> Date: Thu, 8 Oct 2026 14:56:30 +0530 Subject: [PATCH 4/5] fix(vertex realtime): declare the dropped-session local Final LIT010 counts an assignment without a Final declaration, which is why this file annotates its locals that way throughout. Final[Mapping[str, object]] satisfies all three gates at once: Final for LIT010, Mapping for LIT001, and a declared type so the value from json.loads does not reach the helper's parameter as Any. --- litellm/llms/vertex_ai/realtime/transformation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/llms/vertex_ai/realtime/transformation.py b/litellm/llms/vertex_ai/realtime/transformation.py index 671f21c7a04..4cf395c0a2b 100644 --- a/litellm/llms/vertex_ai/realtime/transformation.py +++ b/litellm/llms/vertex_ai/realtime/transformation.py @@ -237,7 +237,7 @@ class VertexAIRealtimeConfig(GeminiRealtimeConfig): else: # Annotated rather than passed straight from json.loads, whose Any # would reach the helper's Mapping parameter as an unknown type. - dropped_session: Mapping[str, object] = json_message.get("session") or {} + dropped_session: Final[Mapping[str, object]] = json_message.get("session") or {} self._warn_session_update_dropped(dropped_session, provider="Vertex AI Realtime") return [] From 67bcf90068fd203178ef5eb887505989bacd2d3b Mon Sep 17 00:00:00 2001 From: aayushbaluni <73417844+aayushbaluni@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:26:33 +0530 Subject: [PATCH 5/5] fix(gemini realtime): type the normalized session payload's return The dropped-session warning passes the result of `_normalize_session_payload_for_mapping` to a `Mapping[str, object]` parameter. The helper was annotated to return a bare `dict`, so the payload's key and value types were unknown at the call site and the type-check gate counted one new `reportUnknownArgumentType`. Declaring the return as `dict[str, object]` fixes it at the source instead of asserting it with a cast at the call site. It also removes one `reportMissingTypeArgument`, one `reportUnknownParameterType` and two `reportUnknownVariableType` from the two realtime transformations. --- litellm/llms/gemini/realtime/transformation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/llms/gemini/realtime/transformation.py b/litellm/llms/gemini/realtime/transformation.py index 48b78b35f99..546f711a0c9 100644 --- a/litellm/llms/gemini/realtime/transformation.py +++ b/litellm/llms/gemini/realtime/transformation.py @@ -382,7 +382,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