chore(release): backport #31519, #31733 to stable/1.90.x and cut 1.90.2 - #31782
Conversation
…lose guardrail bypass (#31519) * fix(realtime): stop sending a second Gemini Live setup on follow-up session.update Gemini Live (BidiGenerateContent) accepts setup as the first-and-only client message; a second setup closes the socket with 1007 Request contains an invalid argument. The AI Studio Gemini path forwarded every client session.update after the first as a follow-up setup, and GA clients (pipecat) send several while configuring the session, so the second one tore the session down before the first turn. Callers saw silence after the first response, exponential per-turn latency from reconnect/retry churn, and intermittent 1011 errors. Drop subsequent session.updates instead of resending setup, matching what the Vertex subclass already does. Tools and instructions must ride on the first session.update before any conversation content. Adds regression tests covering the plain follow-up, a follow-up that adds tools (the case the previous identical-only dedup still forwarded), and the guardrail create_response=False warning path. * fix(realtime): retry the backend open handshake instead of failing with 1011 The upstream Live API open handshake (e.g. Gemini Live) intermittently hangs; waiting longer never recovers a hung attempt, but a fresh attempt almost always connects in ~1s. The proxy opened the backend websocket once with the default open_timeout and no retry, so a single slow handshake surfaced to the caller as a fatal 1011 internal error and dropped the call. Bound each open attempt with a short open_timeout and retry; a bounded attempt that already timed out spaces out the next try, so no backoff is needed. Deterministic handshake-status rejections (auth/4xx) are not retried, and the retry only ever wraps the open, never a live session. Adds tests for retry-then-succeed, raise-after-max-attempts, and no-retry-on-auth-failure. * fix(realtime): close guardrail bypass + surface handshake status; drop obsolete tests Three review fixes on the Gemini Live realtime path. Transcription-guardrail bypass: Gemini Live rejects a second setup (1007), so once the initial setup is sent the guardrail's automaticActivityDetection.disabled=true can no longer be delivered as a follow-up session.update. With that follow-up now dropped, the model's auto-response stayed enabled and a realtime_input_transcription guardrail was bypassed (the model answered before the proxy could gate the turn). Fold the disable into the one-and-only setup instead: the handler injects it into the auto-sent setup (gemini_live_defer_setup false) and _send_to_backend injects it into the deferred first setup. OpenAI sessions accept follow-up updates and are left untouched. Backend handshake status: the open-retry treated only InvalidStatusCode as deterministic; websockets>=15 raises InvalidStatus for a rejected client handshake, so a 401/403 fell into the broad WebSocketException branch and was retried before the caller closed the client with 1011 instead of the upstream status. Treat both as non-retryable. Obsolete tests: the four tests asserting a follow-up session.update is merged and re-sent as a second setup asserted behavior that crashes Gemini Live with 1007 (verified directly against the API). Removed; the drop is covered by new regression tests. * style(realtime): reformat changed files to ruff line-length 120 Post-merge with litellm_internal_staging, which unified ruff format width to 120 (#31518). The realtime change set was formatted at 88, so the changed lines tripped the whole-file ruff format check. Reformat with ruff 0.15.3 at the repo's 120 width; no logic changes. (cherry picked from commit ef5d05f)
…er (#31733) RealTimeStreaming.log_messages dispatched the success handler with a bare asyncio.create_task, bypassing GLOBAL_LOGGING_WORKER (which gives a per-coroutine timeout and a concurrency cap). On a long-lived realtime websocket a slow logging callback left one suspended task per logged turn, each pinning that turn's assembled response, accumulating without bound (~12-15k in-flight under load in a repro) until OOM. Route realtime success logging through the bounded worker so in-flight logging is capped and a hung callback is cancelled at the worker timeout. The chat and responses streaming success-logging paths are intentionally left unchanged: their success callbacks must complete within the call's event-loop run (the non-streaming path pairs the worker with a synchronous callback; the streaming path has no such companion), so deferring them through the worker would drop logs for one-shot SDK calls and breaks test_async_custom_handler_stream. Bounding those paths needs a load-shedding approach and is left to a follow-up. (cherry picked from commit d4c33b2)
Greptile SummaryThis PR backports two realtime fixes from staging onto
Confidence Score: 4/5Safe to merge; the three independent fixes are well-scoped and the targeted test suite passes cleanly after both picks. The core logic is sound across all three fixes. The only callouts are a redundant No files require special attention;
|
| Filename | Overview |
|---|---|
| litellm/litellm_core_utils/realtime_streaming.py | Adds _maybe_inject_guardrail_auto_response_disable to fold transcription-guardrail's auto-response disable into the one-and-only Gemini setup, and routes async success logging through GLOBAL_LOGGING_WORKER instead of a bare asyncio.create_task. |
| litellm/llms/custom_httpx/llm_http_handler.py | Extracts _open_realtime_backend_ws with retry logic for hung handshakes; reorders realtime setup so the RealTimeStreaming object exists before the initial setup is sent (enabling guardrail inject into the one-and-only setup message). |
| litellm/llms/gemini/realtime/transformation.py | Replaces the follow-up setup merge logic in _handle_session_update with a simple drop; subsequent session.updates now return [] with an appropriate log (warning for the create_response=False guardrail signal, debug otherwise). Minor inefficiency: map_openai_params is still computed for the drop path. |
| tests/test_litellm/litellm_core_utils/test_realtime_streaming.py | Adds 4 new tests: guardrail inject into setup, no-op without guardrail, non-bidi message left untouched, and regression test verifying success logging routes through GLOBAL_LOGGING_WORKER. No existing tests modified. |
| tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py | Adds 3 new backend-open retry tests via the _FakeWebsocketsModule fixture: retry-then-succeed, exhaust-max-attempts, and deterministic-rejection (both websockets<15 and >=15 variants). No existing tests modified. |
| tests/test_litellm/llms/gemini/realtime/test_gemini_realtime_transformation.py | Removes 4 tests that verified the old follow-up-merge behaviour (now intentionally replaced with drop logic) and adds 3 new tests verifying the new drop behaviour, including a warning check for the create_response=False case. |
Comments Outside Diff (1)
-
litellm/llms/gemini/realtime/transformation.py, line 442-455 (link)When
session_configuration_requestis notNone, the function always returns[]and never usesnew_overrides, yet the code still runs the full_normalize_session_payload_for_mapping+map_openai_paramspipeline._extract_turn_detectiononly needs the normalizedsession_payload, so themap_openai_paramscall is dead work for every dropped follow-up update.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Reviews (1): Last reviewed commit: "fix(logging): route realtime success log..." | Re-trigger Greptile
| websockets_module.exceptions.WebSocketException, | ||
| ) as e: | ||
| last_exc = e | ||
| assert last_exc is not None # loop only exits via return or a captured exc |
There was a problem hiding this comment.
assert can be silently dropped when the interpreter runs with -O/-OO. If max_attempts=0 is ever passed, the assertion is eliminated, last_exc is None, and raise last_exc raises TypeError: exceptions must derive from BaseException rather than a useful error. A real guard is safer: if last_exc is None: raise RuntimeError(...) before raise last_exc.
| @@ -1145,60 +1145,6 @@ def test_gemini_function_call_output_includes_name(): | |||
| assert "response" in function_response | |||
There was a problem hiding this comment.
Four existing tests are removed here per the custom rule on test modifications. The removed tests (
test_gemini_subsequent_session_update_forwards_tools_merged_with_original_setup, test_gemini_subsequent_session_update_with_turn_detection_only_preserves_original_tools, test_gemini_follow_up_session_update_preserves_response_modalities_on_partial_generation_config, test_gemini_subsequent_session_update_preserves_automatic_activity_detection_subfields) all asserted the old follow-up-merge behaviour, which is now replaced by the intentional drop. The three new tests assert the opposite (drop) contract, so coverage is updated rather than weakened — confirming this is a legitimate behaviour-aligned removal rather than a mask for a regression.
Rule Used: What: Flag any modifications to existing tests and... (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
…to v1.90.3 (#257) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [https://github.com/BerriAI/litellm.git](https://github.com/BerriAI/litellm) | patch | `v1.90.0` → `v1.90.3` | --- ### Release Notes <details> <summary>BerriAI/litellm (https://github.com/BerriAI/litellm.git)</summary> ### [`v1.90.3`](https://github.com/BerriAI/litellm/releases/tag/v1.90.3) [Compare Source](BerriAI/litellm@v1.90.2...v1.90.3) #### Verify Docker Image Signature All LiteLLM Docker images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/). Every release is signed with the same key introduced in [commit `0112e53`](BerriAI/litellm@0112e53). **Verify using the pinned commit hash (recommended):** A commit hash is cryptographically immutable, so this is the strongest way to ensure you are using the original signing key: ```bash cosign verify \ --key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \ ghcr.io/berriai/litellm:v1.90.3 ``` **Verify using the release tag (convenience):** Tags are protected in this repository and resolve to the same key. This option is easier to read but relies on tag protection rules: ```bash cosign verify \ --key https://raw.githubusercontent.com/BerriAI/litellm/v1.90.3/cosign.pub \ ghcr.io/berriai/litellm:v1.90.3 ``` Expected output: ``` The following checks were performed on each of these signatures: - The cosign claims were validated - The signatures were verified against the specified public key ``` *** #### What's Changed - chore(release): backport [#​31923](BerriAI/litellm#31923), [#​31929](BerriAI/litellm#31929), [#​31393](BerriAI/litellm#31393) to stable/1.90.x and cut 1.90.3 by [@​mateo-berri](https://github.com/mateo-berri) in [#​32025](BerriAI/litellm#32025) **Full Changelog**: <BerriAI/litellm@v1.90.2...v1.90.3> ### [`v1.90.2`](https://github.com/BerriAI/litellm/releases/tag/v1.90.2) [Compare Source](BerriAI/litellm@v1.90.1...v1.90.2) #### Verify Docker Image Signature All LiteLLM Docker images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/). Every release is signed with the same key introduced in [commit `0112e53`](BerriAI/litellm@0112e53). **Verify using the pinned commit hash (recommended):** A commit hash is cryptographically immutable, so this is the strongest way to ensure you are using the original signing key: ```bash cosign verify \ --key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \ ghcr.io/berriai/litellm:v1.90.2 ``` **Verify using the release tag (convenience):** Tags are protected in this repository and resolve to the same key. This option is easier to read but relies on tag protection rules: ```bash cosign verify \ --key https://raw.githubusercontent.com/BerriAI/litellm/v1.90.2/cosign.pub \ ghcr.io/berriai/litellm:v1.90.2 ``` Expected output: ``` The following checks were performed on each of these signatures: - The cosign claims were validated - The signatures were verified against the specified public key ``` *** #### What's Changed - chore(release): backport [#​31519](BerriAI/litellm#31519), [#​31733](BerriAI/litellm#31733) to stable/1.90.x and cut 1.90.2 by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​31782](BerriAI/litellm#31782) **Full Changelog**: <BerriAI/litellm@v1.90.1...v1.90.2> ### [`v1.90.1`](https://github.com/BerriAI/litellm/releases/tag/v1.90.1) [Compare Source](BerriAI/litellm@v1.90.0-rc.1...v1.90.1) #### Verify Docker Image Signature All LiteLLM Docker images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/). Every release is signed with the same key introduced in [commit `0112e53`](BerriAI/litellm@0112e53). **Verify using the pinned commit hash (recommended):** A commit hash is cryptographically immutable, so this is the strongest way to ensure you are using the original signing key: ```bash cosign verify \ --key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \ ghcr.io/berriai/litellm:v1.90.1 ``` **Verify using the release tag (convenience):** Tags are protected in this repository and resolve to the same key. This option is easier to read but relies on tag protection rules: ```bash cosign verify \ --key https://raw.githubusercontent.com/BerriAI/litellm/v1.90.1/cosign.pub \ ghcr.io/berriai/litellm:v1.90.1 ``` Expected output: ``` The following checks were performed on each of these signatures: - The cosign claims were validated - The signatures were verified against the specified public key ``` *** #### What's Changed - chore(release): backport [#​31036](BerriAI/litellm#31036), [#​31342](BerriAI/litellm#31342), [#​31653](BerriAI/litellm#31653) to stable/1.90.x and cut 1.90.1 (litellm-enterprise 0.1.43.post1) by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​31667](BerriAI/litellm#31667) **Full Changelog**: <BerriAI/litellm@v1.90.0...v1.90.1> </details> --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNDkuNSIsInVwZGF0ZWRJblZlciI6IjQzLjI0OS41IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=--> Co-authored-by: Renovate Bot <renovate@bhamm-lab.com> Reviewed-on: https://codeberg.org/blake-hamm/bhamm-lab/pulls/257
Relevant issues
Backports two already-merged realtime fixes from litellm_internal_staging onto stable/1.90.x. #31519 hardens the Gemini Live realtime path: it stops sending a second
setupon a follow-upsession.update(Gemini Live accepts setup as the first-and-only client message and closes the socket with 1007 on a second one, which showed up as silence after the first turn and intermittent 1011s), retries a hung backend open handshake instead of surfacing a single slow handshake to the caller as a fatal 1011, and folds the transcription-guardrail auto-response disable into the one-and-only setup so arealtime_input_transcriptionguardrail keeps gating the turn. #31733 routes realtime success logging through the bounded logging worker instead of a bareasyncio.create_task, so a slow logging callback on a long-lived realtime websocket can no longer leave one suspended task per turn pinning that turn's response in memoryCuts 1.90.2. The line's tip was 1.90.1 and 1.90.1 has already shipped (the litellm/litellm:v1.90.1 image is published on DockerHub, pushed 2026-06-30), so these picks take the next patch, 1.90.2. The git tag and release notes for 1.90.1 are still in flight, so the release does not yet show in the repo tags; the published image is the authoritative signal
Linear ticket
N/A
Pre-Submission checklist
Screenshots / Proof of Fix
Both picks' own regression tests pass on stable/1.90.x, judged as a delta against a clean baseline captured on the line's tip before picking (144 passed, 0 failures). After both picks the same targeted suite is 151 passed, 0 failures (baseline 144 plus 7 net-new tests: 6 from #31519, 1 from #31733), so every new test the picks add passes and nothing that passed before regressed
Live proxy sanity on the picked code (per-worktree proxy on the line, real OpenAI call), before and after the picks, to confirm the line still boots and serves with these changes wired in:
The original bug repros are Gemini Live websocket sessions, which the default proxy config does not wire up (no Gemini Live model, and it is a websocket rather than an HTTP call), so the behavioral proof here is the picks' own tests plus the proxy loading the picked realtime code with no import or wiring errors
Type
Bug Fix
Changes
What is included, in merge order:
Adaptation notes
Both picks are adaptations rather than verbatim cherry-picks; 1.90.x predates the realtime refactor (#30960, which introduced
_content_sent_after_setupand reshaped_handle_session_update), a realtime fix (#30446), and the formatter migration (black to ruff, width 120) that these staging commits were built on. So the picks were hand-resolved onto 1.90.x's older realtime code and its black formatting; each pick's own added or changed lines are preserved, only surrounding context and formatting differbackend_ws.send(msg)in_send_to_backend(1.90.x has no_content_sent_after_setupguard there, which is a separate test: add realtime proxy e2e suite across providers #30960 concern the inject line does not depend on); the follow-up-setup merge block in_handle_session_updateis replaced wholesale with the drop logic;_open_realtime_backend_wsand the reordered auto-send are applied as in the source. In the gemini test suite the 4 obsolete follow-up-merge tests are removed and the 3 drop tests added, matching the source PR's own test changes exactly (name-equality verified)log_messagessuccess logging throughGLOBAL_LOGGING_WORKER) applied cleanly on top of fix(realtime): stop second Gemini Live setup, retry hung handshake, close guardrail bypass #31519; it is marked adapted only because the surrounding file is black-formatted on this line rather than the source's ruff-120Known noise on this line
The baseline for the three targeted test files was clean (0 failures), so there is no pre-existing test noise to discount for this PR