Skip to content

chore(release): backport #31036, #31342, #31653 to stable/1.90.x and cut 1.90.1 (litellm-enterprise 0.1.43.post1) - #31667

Merged
yuneng-berri merged 6 commits into
stable/1.90.xfrom
litellm_backport_1_90_x_31036_31342_31653
Jun 30, 2026
Merged

chore(release): backport #31036, #31342, #31653 to stable/1.90.x and cut 1.90.1 (litellm-enterprise 0.1.43.post1)#31667
yuneng-berri merged 6 commits into
stable/1.90.xfrom
litellm_backport_1_90_x_31036_31342_31653

Conversation

@yuneng-berri

Copy link
Copy Markdown
Collaborator

Relevant issues

Backports three already-merged staging fixes onto stable/1.90.x and cuts 1.90.1, plus the litellm-enterprise 0.1.43.post1 required because one pick touches an enterprise source file. The headline reason is feature parity on upgrade: stable/1.88.x and stable/1.89.x carry the streaming Vertex batch-upload feature (#31036) but stable/1.90.x does not, so a user upgrading from 1.89.x to 1.90.x silently loses large-file Vertex batch uploads. This brings #31036 to the line together with its follow-up #31653, so 1.90.x gets the fixed version of the feature (a single streaming media upload, not the chunked-resumable path that caused 499s) rather than the buggy interim state the older lines still carry. It also picks an unrelated client-side reliability fix (#31342) that scrubs a secret-bearing query param out of key/info error strings.

There is no litellm double-bump risk: 1.90.0 is already released (tag v1.90.0 and release/v1.90.0 both exist), so the picks cut 1.90.1. 0.1.43.post1 is the correct enterprise increment: 0.1.43 is published and 0.1.44 is staging's next, so the post-release sorts 0.1.43 < 0.1.43.post1 < 0.1.44.

What is included

In cherry-pick (merge) order:

Adaptation notes

Two picks are ADAPTED; #31342 applied verbatim.

#31036: the OOM-fix core (the vertex transformation and handler, the files endpoint, the router and batch utilities, the types and the enterprise managed-files hook) and all test files, including the new 696-line streaming suite, applied as on staging. One file diverges: litellm/llms/custom_httpx/llm_http_handler.py had an import-block conflict from the pick's import-sort. Resolved by keeping the streaming body and dropping three imports the sort carried but that this line's code never references: functools.lru_cache, litellm.litellm_core_utils.asyncify.run_async_function, and a duplicate urllib.parse line. Their only consumer on staging, _responses_api_optional_request_param_names, is not part of #31036 and is absent on this line, so importing them here would be unused-import violations. Unlike the 1.88.x/1.89.x backports, RealtimeQueryParams is genuinely used on this line, so it was kept.

#31653: the upload-path rewrite conflicted against #31036's just-landed resumable code (ten regions in llm_http_handler.py and one in transformation.py), because #31653's upstream base also carried intermediate staging commits this line does not (a repo-wide ruff reformat and unrelated realtime/websearch fixes). Resolved by taking #31653's streaming_media_upload rewrite for every region; the removed resumable code is wider-wrapped on this line, which accounts for the higher deletion count versus the upstream commit. The StreamingMediaUploadConfig and BaseFileUploadStream imports auto-merged. Verified no resumable-upload symbol remains referenced and the added test functions exactly match the upstream PR's own (no absorbed neighbor tests).

Known noise on this line

The targeted test set (each picked PR's own test files plus the mirrored tests for every touched source) was fully green on the line tip before any pick once the proxy extras are synced: 242 passed, 0 failed. The only pre-existing lint nit on a touched file is a bare # noqa (PGH004) in litellm/types/router.py, present on the line tip before the picks; ruff check on every touched module otherwise passes. The line's ruff format baseline is already dirty against its line-length = 120 config (the bulk of the code predates the width change), so no reformat was applied; ruff check, the enforced gate, passes.

Screenshots / Proof of Fix

Proxy booted on the worktree against real provider keys, before and after the picks.

Before the picks:

$ curl -sf "http://localhost:4010/health/liveliness"
"I'm alive!"
$ curl -s "http://localhost:4010/v1/chat/completions" -H "Authorization: Bearer sk-1234" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-4o","messages":[{"role":"user","content":"reply with exactly: baseline ok"}],"max_tokens":20}'
-> content: "baseline ok"

After the picks (proxy auto-reloaded on the picked code; confirms the streaming rewrite and the client redaction do not regress startup or serving):

$ curl -sf "http://localhost:4010/health/liveliness"
"I'm alive!"
$ curl -s "http://localhost:4010/v1/chat/completions" -H "Authorization: Bearer sk-1234" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-4o","messages":[{"role":"user","content":"reply with exactly: post-pick ok"}],"max_tokens":20}'
-> content: "post-pick ok"

Targeted test deltas against the 242-passing baseline: 329 passed / 2 skipped after #31036 (including the new streaming suite), 29 passed for #31342's client redaction suite, 326 passed / 2 skipped after #31653 (the net decrease is the resumable-upload tests being replaced by the media-upload tests; zero failures throughout). A behavioral verification pass with adversarial subagents returned a clean verdict on all three sub-claims: every identifier the picked diffs reference resolves on this line, each pick's own tests pass, and no existing caller of the modified functions (the vertex transformation, the handler create-file paths, the proxy client) regresses. The only dissent was a cosmetic stale docstring on StreamingMediaUploadConfig that exists upstream as well, not a behavioral defect.

The end-to-end large (1GB+) Vertex upload itself needs live GCS and Vertex service-account credentials, which were not available in this environment, so it is covered by the streaming suite rather than a live replay. To confirm on a real deployment: configure a Vertex AI model and a GCS bucket, then POST a >500MB batch JSONL to /v1/files with purpose=batch and submit it via /v1/batches; before #31653 the chunked-resumable upload accumulates hundreds of sequential round-trips and surfaces as a 499, after it the upload streams in one continuous media request and completes.

Type

🐛 Bug Fix
🚄 Infrastructure

Changes

Brings the streaming Vertex batch-upload feature (#31036) and its single-media-upload follow-up (#31653) to stable/1.90.x so upgrading from 1.89.x does not lose the feature, plus a client-side error-redaction fix (#31342), cutting 1.90.1 and the litellm-enterprise 0.1.43.post1 required because #31036 touches an enterprise source file.

mubashir1osmani and others added 6 commits June 29, 2026 17:56
* fix(vertex/files): stream OpenAI->Vertex batch JSONL uploads to fix OOM on large files

Large (1GB+) batch JSONL uploads to Vertex AI / GCS caused OOM or killed the worker
because the request body was buffered and multiplied 2-3x in size. The create-file
path is now streaming end-to-end: transform_create_file_request returns a
ResumableChunkedUploadConfig carrying a lazy _OpenAIToVertexBatchUploadStream, and the
HTTP handler opens a GCS resumable session and PUTs the body in bounded 8 MiB chunks
(Content-Range, 308 between chunks) so the transformed payload is never held in full.
The proxy /v1/files endpoint streams from Starlette's spooled upload handle instead of
reading the whole body, and batch rate limiting counts tokens and models in a single
streaming pass.

Only gcs_bucket_name is supported for the GCS target; the legacy bucket_name key is
intentionally not read.

Also removes the unreachable VertexAIFilesHandler create path and everything only it
kept alive (VertexAIJsonlFilesTransformation, _stream_openai_jsonl_to_vertex, the legacy
transform helpers), plus the orphaned batch_utils helpers the streaming rewrite replaced.

* fix(batches): return original JSONL on unparseable row to avoid silent batch truncation

The streaming rewrite of replace_model_in_jsonl accumulated physical lines and
skipped a row on JSONDecodeError to support multi-line objects, but a genuinely
malformed or truncated row never completes: it poisons the buffer, swallows every
following row, and the function still returned the partial rewrite (the rows before
the bad one, already model-rewritten) as if the batch were complete. That turned the
pre-rewrite behavior of returning the original file unchanged (so the provider rejects
the bad batch loudly) into a silent partial submission.

Restore the original-content fallback: when an unparseable remainder is left after the
loop, return the original file_content (rewinding a consumed seekable source) instead of
the truncated output. The multi-line happy path is unchanged.

* test(batches): mock resumable GCS upload in vertex batch prediction test

The vertex batch file-create path now streams to a GCS resumable session via
_aresumable_chunked_upload (httpx send) instead of AsyncHTTPHandler.post, so the
existing test's post mock no longer intercepted the upload and a real request hit
GCS (401). Mock _aresumable_chunked_upload to return the GCS object response; the
resumable protocol itself is covered in test_vertex_ai_files_streaming.py.

* fix(batches): resilient per-row token accounting; no hard-block on count failure

The batch input-file pass iterated a generator whose json.loads raised on a
malformed line; the outer except caught it and stopped the loop, so any body.model
on rows after a bad line was never collected and the model allowlist check ran
against a partial set. It also hard-blocked the batch with a 400 whenever token
counting raised, a backwards-incompatible change from the prior swallow-and-proceed
behavior that breaks legitimate rows the token counter cannot measure (e.g. some
multimodal content).

Iterate the JSONL line-by-line and account each row independently. A malformed line
is skipped (its request cannot run upstream anyway) and a row the counter cannot
measure falls back to a conservative size-based estimate. The loop never aborts, so
the allowlist check always sees every parseable model, and the token total is never
zeroed, so a crafted uncountable row still cannot evade the TPM limit, without
hard-rejecting a legitimate batch.

* perf(vertex/files): unblock async upload; drop empty finalize; widen batch MIME types

Three review follow-ups on the resumable batch upload:
- _aresumable_chunked_upload pulled chunks from a synchronous generator that runs
  the per-row transform inline on the event loop thread, blocking other requests
  between PUTs on large uploads. Each chunk is now produced via asyncio.to_thread.
- _iter_resumable_chunks no longer yields a trailing empty chunk, so an exactly
  chunk-aligned upload finalizes on its last data chunk instead of an extra
  zero-byte PUT; a 0-byte stream still finalizes via the caller's empty request.
- valid_content_type now accepts the MIME types clients label .jsonl batch uploads
  with (text/plain, application/json, ndjson, ...), so such a batch file no longer
  silently bypasses the streaming path into the buffered media upload.

* fix(vertex/files): keep legacy bucket_name as GCS bucket fallback

The rename to gcs_bucket_name dropped the legacy bucket_name key entirely, so an SDK caller passing bucket_name to a Vertex AI file create/retrieve/content call with GCS_BUCKET_NAME unset got ValueError("GCS bucket_name is required") where it previously resolved the bucket. _get_configured_bucket_name now reads gcs_bucket_name, then bucket_name, then the env var, and bucket_name is restored to OPTIONAL_KWARGS_KEYS so it survives get_litellm_params on the retrieve and content paths. gcs_bucket_name keeps precedence when both are present

* style: sort imports in llm_http_handler to satisfy I001 budget

---------

Co-authored-by: Yuneng Jiang <yuneng@berri.ai>
(cherry picked from commit 5682592)
…#31342)

* fix(proxy/client): redact api key from key/info client error messages

The keys management client builds GET /key/info?key=<key> and lets the
requests HTTPError propagate. str(HTTPError) renders the failing request URL
verbatim ("... for url: .../key/info?key=sk-..."), so any caller that logs the
exception leaks the full key; the 401 branch leaked the same way through
UnauthorizedError(str(orig_exception))

Redact both branches with the existing redact_secrets helper so the
secret-bearing query param is scrubbed to ?REDACTED while the status code,
reason, and response object are preserved. Server-side responses already mask
the key, so this closes the remaining client-side surface

* fix: preserve key info unauthorized response

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
(cherry picked from commit 71ee1a8)
… on large uploads (#31653)

* fix(vertex_ai/files): upload batch files in a single media request to fix 499s on large uploads

PR #31036 switched the vertex batch file upload from a single GCS media
upload to a chunked resumable session. The resumable path sends the body as
many sequential PUTs, each waiting a full round-trip to GCS before the next,
so a multi-GB upload accumulates hundreds of round-trips and overruns the
client/load-balancer request timeout, surfacing as 499s (client closed
connection) on files as small as 500MB. This was a regression from the
last-known-good commit, where the upload completed as one continuous request.

Revert the batch upload to a single uploadType=media request, but stage the
transformed payload to a temp file first so peak memory stays bounded (the
goal of the resumable rewrite) without the per-chunk round-trips. The temp
file is closed deterministically (TemporaryFile unlinks on close), not left
to the GC. The now-unused resumable chunked-upload plumbing is removed.

Also swap the per-row transform's stdlib json for orjson (parse + serialize),
which is ~4x faster on this hot path; the streaming body now emits compact
orjson bytes.

The request stays synchronous, so the returned file object is real and
POST /v1/batches keeps working immediately against the uploaded object.

Tests: single media request carries the whole payload with a real
Content-Length (no chunked transfer-encoding); failed upload raises; the
staged temp file is closed deterministically; byte-for-byte transform parity.

* test(vertex_ai/files): mock single media upload POST instead of removed resumable method

test_avertex_batch_prediction patched BaseLLMHTTPHandler._aresumable_chunked_upload, which was removed when the batch jsonl upload moved from a chunked resumable GCS session to a single uploadType=media request. Patch the raw httpx.AsyncClient.post that _astage_and_upload_media issues so the real staging, upload and response transform run while the GCS object response is mocked, and assert the media URL and Content-Type.

* fix(vertex_ai/files): forward request timeout to media upload, drop orjson, sort imports

Forward the per-request timeout through _stage_and_upload_media /
_astage_and_upload_media to the GCS POST. Every other upload branch forwards
it; the new media path was dropping it, so a caller-provided timeout was
silently ignored (the files path passes 600s by default, but a custom
request_timeout would not have reached this upload). Regression test asserts
the resolved timeout reaches the request (mutation-verified).

Revert the orjson swap in the batch transform: importing orjson at module load
in this core-path file broke `import litellm` on environments without orjson
(the Windows import test). Back to stdlib json; the upload leg dominates large
uploads anyway, so the transform-side win was marginal.

Fix import ordering in llm_http_handler.py (I001) introduced by the new imports.

* fix(vertex_ai/files): stream batch upload to GCS instead of staging to a temp file

Addresses a disk-exhaustion concern: staging the full transformed batch body to
a local temp file before the GCS request meant an authenticated user could fill
the proxy's temp volume with large concurrent uploads (on top of Starlette's
input spool).

GCS's simple/media upload accepts chunked transfer-encoding, so stream the
transform straight to the single media request instead. Each block is produced
on a worker thread (the transform never runs on the event loop) and sent
chunked, so the body is neither buffered in memory nor written to disk, and the
upload is still one continuous request (no per-chunk round-trips, no 499). Drops
the temp-file staging, the tempfile/IO imports, and Content-Length computation.

Regression test asserts the upload streams (chunked transfer-encoding, no
Content-Length) and creates no temp file; mutation-verified that reintroducing
staging fails it.

(cherry picked from commit 85840ae)
@yuneng-berri
yuneng-berri requested a review from a team June 30, 2026 01:35

@mateo-berri mateo-berri left a comment

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.

LGTM; thanks!

@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR backports Vertex batch upload and proxy client reliability fixes to stable/1.90.x. The main changes are:

  • Streams OpenAI batch JSONL uploads into Vertex AI GCS media uploads without buffering the full file.
  • Adds metadata-only file handling and lazy JSONL iteration for large batch files.
  • Updates batch rate limiting to count streamed rows, estimate uncountable rows, and validate embedded batch models.
  • Redacts sensitive values from proxy client key error messages.
  • Bumps litellm to 1.90.1 and litellm-enterprise to 0.1.43.post1 with refreshed lock metadata.
  • Adds targeted tests for streaming Vertex uploads, proxy file endpoints, batch validation, router utilities, and client redaction.

Confidence Score: 5/5

Safe to merge based on the reviewed changes.

The changes are targeted backports with focused test coverage for the streaming upload paths, proxy file handling, batch utilities, and key redaction behavior. No blocking correctness or security issues were identified.

T-Rex T-Rex Logs

What T-Rex did

  • Recorded the vertex streaming upload workflow, capturing the base run and the head run, plus the helper script used for both runs.
  • Executed two HTTP GET checks against the local server for key/info, observing 404 Not Found and 401 Unauthorized responses with a redacted secret, and both checks exited with code 0.
  • Compared build configuration by inspecting pyproject.toml and uv.lock across base and head runs, noting the pyproject version bump from 1.90.0 to 1.90.1 and the enterprise version from 0.1.43 to 0.1.43.post1, with uv.lock entries aligned accordingly.

View all artifacts

T-Rex Ran code and verified through T-Rex

Reviews (1): Last reviewed commit: "chore: refresh uv.lock for 1.90.1 and li..." | Re-trigger Greptile

@yuneng-berri
yuneng-berri merged commit ca6149a into stable/1.90.x Jun 30, 2026
44 of 51 checks passed
@yuneng-berri
yuneng-berri deleted the litellm_backport_1_90_x_31036_31342_31653 branch June 30, 2026 01:40
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.

4 participants