Skip to content

chore(release): backport #31519, #31733 to stable/1.90.x and cut 1.90.2 - #31782

Merged
yuneng-berri merged 4 commits into
stable/1.90.xfrom
litellm_backport_1_90_x_bp_31519_31733
Jul 1, 2026
Merged

chore(release): backport #31519, #31733 to stable/1.90.x and cut 1.90.2#31782
yuneng-berri merged 4 commits into
stable/1.90.xfrom
litellm_backport_1_90_x_bp_31519_31733

Conversation

@yuneng-berri

@yuneng-berri yuneng-berri commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

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 setup on a follow-up session.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 a realtime_input_transcription guardrail keeps gating the turn. #31733 routes realtime success logging through the bounded logging worker instead of a bare asyncio.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 memory

Cuts 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

  • I have added meaningful tests
  • My PR's scope is as isolated as possible; it only solves 1 specific problem

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

uv run --extra proxy pytest \
  tests/test_litellm/litellm_core_utils/test_realtime_streaming.py \
  tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py \
  tests/test_litellm/llms/gemini/realtime/test_gemini_realtime_transformation.py -q
# baseline (pre-pick): 144 passed
# after both picks:    151 passed

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:

# before picks
curl -sf localhost:PORT/health/liveliness            -> "I'm alive!"
curl localhost:PORT/v1/chat/completions -d '{"model":"gpt-4o",...}'  -> "Backport baseline approved."

# after picks
curl -sf localhost:PORT/health/liveliness            -> "I'm alive!"
curl localhost:PORT/v1/chat/completions -d '{"model":"gpt-4o",...}'  -> "Backport post OK."

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_setup and 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 differ

Known 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

…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)
@yuneng-berri
yuneng-berri requested a review from a team July 1, 2026 01:53
@greptile-apps

greptile-apps Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR backports two realtime fixes from staging onto stable/1.90.x: it stops Gemini Live from receiving a second setup message (which caused 1007 disconnects), adds a per-attempt timeout with retry for hung backend open handshakes, folds the transcription-guardrail auto-response disable into the one-and-only setup, and routes realtime success logging through GLOBAL_LOGGING_WORKER instead of a bare asyncio.create_task.

  • Gemini setup hardening (transformation.py, realtime_streaming.py, llm_http_handler.py): subsequent session.updates are now dropped instead of forwarded as a second setup; the RealTimeStreaming object is constructed before the initial setup is sent so _maybe_inject_guardrail_auto_response_disable can fold automaticActivityDetection.disabled=true into the one-and-only setup message.
  • Handshake retry (llm_http_handler.py): _open_realtime_backend_ws wraps each connect attempt with open_timeout and retries on TimeoutError/OSError/WebSocketException while immediately re-raising deterministic status rejections (InvalidStatus/InvalidStatusCode), covering both websockets <15 and ≥15.
  • Logging worker routing (realtime_streaming.py): async success logging is enqueued on GLOBAL_LOGGING_WORKER, bounding concurrency and preventing per-turn suspended-task memory growth.

Confidence Score: 4/5

Safe 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 map_openai_params call in the drop path of _handle_session_update (wastes a little CPU per dropped update but has no functional impact) and an assert guarding the post-loop raise in _open_realtime_backend_ws that would disappear under -O, leaving a confusing TypeError if max_attempts were ever zero. Neither affects normal operation. The four removed Gemini tests correctly reflect the intentional behaviour reversal and are replaced by equivalent new tests.

No files require special attention; litellm/llms/gemini/realtime/transformation.py and litellm/llms/custom_httpx/llm_http_handler.py carry the minor items flagged above.

Important Files Changed

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)

  1. litellm/llms/gemini/realtime/transformation.py, line 442-455 (link)

    P2 When session_configuration_request is not None, the function always returns [] and never uses new_overrides, yet the code still runs the full _normalize_session_payload_for_mapping + map_openai_params pipeline. _extract_turn_detection only needs the normalized session_payload, so the map_openai_params call 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 10.63830% with 42 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/llms/custom_httpx/llm_http_handler.py 9.09% 20 Missing ⚠️
litellm/litellm_core_utils/realtime_streaming.py 15.00% 17 Missing ⚠️
litellm/llms/gemini/realtime/transformation.py 0.00% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

@yuneng-berri yuneng-berri changed the title chore(release): backport realtime setup, handshake-retry, and logging fixes (#31519, #31733) to stable/1.90.x chore(release): backport #31519, #31733 to stable/1.90.x and cut 1.90.2 Jul 1, 2026
@yuneng-berri
yuneng-berri merged commit 1e60f26 into stable/1.90.x Jul 1, 2026
43 of 50 checks passed
@yuneng-berri
yuneng-berri deleted the litellm_backport_1_90_x_bp_31519_31733 branch July 1, 2026 02:09
blake-hamm added a commit to blake-hamm/bhamm-lab that referenced this pull request Jul 4, 2026
…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 [#&#8203;31923](BerriAI/litellm#31923), [#&#8203;31929](BerriAI/litellm#31929), [#&#8203;31393](BerriAI/litellm#31393) to stable/1.90.x and cut 1.90.3 by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;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 [#&#8203;31519](BerriAI/litellm#31519), [#&#8203;31733](BerriAI/litellm#31733) to stable/1.90.x and cut 1.90.2 by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;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 [#&#8203;31036](BerriAI/litellm#31036), [#&#8203;31342](BerriAI/litellm#31342), [#&#8203;31653](BerriAI/litellm#31653) to stable/1.90.x and cut 1.90.1 (litellm-enterprise 0.1.43.post1) by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants