fix(http): stop advertising zstd for streamed responses - #35601
Open
akashkokare2910 wants to merge 3 commits into
Open
fix(http): stop advertising zstd for streamed responses#35601akashkokare2910 wants to merge 3 commits into
akashkokare2910 wants to merge 3 commits into
Conversation
httpx's ZStandardDecoder resets its decompressor only when `unused_data`
is non-empty within the same `decode()` call. A streamed SSE flush
delivers exactly one complete zstd frame per network chunk, so the
decoder ends at `eof` with no leftover bytes and the next chunk reuses a
finished decompressobj:
httpx.DecodingError: cannot use a decompressobj multiple times
└─ zstandard.backend_c.ZstdError: cannot use a decompressobj multiple times
Amazon Nova's direct API (api.nova.amazon.com) returns
`content-encoding: zstd` for streaming SSE and emits the stream as
independent frames, so every `amazon_nova` streaming request failed while
non-streaming succeeded. litellm advertises zstd only because
`zstandard` is installed, which is commonly transitive (langsmith
requires zstandard>=0.23.0), so this is the default state of an ordinary
install rather than an opt-in.
Omit zstd from the `Accept-Encoding` litellm advertises, via a shared
constant consumed by `get_default_headers()` - already used by both the
sync and async client factories. gzip/deflate/br are unaffected, so
responses are still compressed, and `LITELLM_ACCEPT_ENCODING` allows an
override (mirroring the existing `LITELLM_USER_AGENT` pattern).
Verified live against amazon_nova/nova-micro-v1: `gzip, deflate, br`
streams 79 chunks with content and both parallel tool calls intact,
where the current default fails 5/5 with zero usable chunks.
The durable fix belongs upstream and already exists as encode/httpx#3697
(open since 2025-10-27, tracked in encode/httpx#3538). This is interim
mitigation until that ships. The synthetic multi-frame characterization
test is self-retiring: it skips, rather than failing, once httpx decodes
frame-aligned chunks correctly, and says so in the skip reason.
Tests are offline and need no credentials.
Refs: BerriAI#35589
Refs: encode/httpx#3697
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
Greptile SummaryThe PR replaces LiteLLM’s implicit httpx
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains; the prior Brotli capability issue is addressed by deriving the header from the installed decoder registry, with fallback and sync/async coverage.
|
| Filename | Overview |
|---|---|
| litellm/constants.py | Adds centralized zstd exclusion and standard-library fallback encoding constants. |
| litellm/llms/custom_httpx/http_handler.py | Builds a capability-aware Accept-Encoding header that resolves the previously reported Brotli-without-decoder issue and excludes zstd. |
| tests/test_litellm/llms/custom_httpx/test_http_handler.py | Adds focused offline coverage for decoder capabilities, overrides, fallback behavior, client propagation, and the motivating zstd decoder behavior. |
Reviews (2): Last reviewed commit: "Merge upstream/litellm_internal_staging ..." | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Contributor
The previous commit hardcoded `Accept-Encoding: gzip, deflate, br`, which advertises Brotli even on installations without `brotli` or `brotlicffi`. A server could then legitimately reply with `content-encoding: br` that nothing here can decode, handing compressed bytes to downstream text or JSON parsing. Derive the advertised set from httpx's own capability-aware registry instead. `httpx._decoders.SUPPORTED_DECODERS` already drops `br` when no Brotli binding is importable and `zstd` when `zstandard` is absent, so mirroring it keeps litellm in step with what the installed stack can actually decode. `zstd` is then removed unconditionally, which is the original fix. If that private symbol ever moves, construction falls back to the codecs the standard library always provides (gzip, deflate) rather than raising. `LITELLM_ACCEPT_ENCODING` still overrides the whole value, per-request headers still win over the client default, and both the sync and async client factories are covered as before. Tests cover Brotli available and unavailable, zstd installed and absent, neither optional decoder present, the registry being unreadable, and the environment and per-request overrides. Refs: BerriAI#35601 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-decode Resolves a conflict in litellm/llms/custom_httpx/http_handler.py: upstream's LIT010 pass annotated `user_agent` as `Final` on the same line region where this branch introduced `accept_encoding`. Kept both, with `accept_encoding` annotated `Final` to match the new rule. Adjusted this branch's code for lint rules added upstream since it was opened: - narrowed the decoder-registry guard from `except Exception` to `except ImportError`, the only way that import can fail (BLE001) - split the registry lookup into `_installed_content_decoders()` so no local is rebound across a try/except, and annotated the remaining locals `Final` (LIT010) - annotated EXCLUDED_ACCEPT_ENCODINGS and FALLBACK_ACCEPT_ENCODINGS as `Final` (LIT010) Behaviour is unchanged: on a Brotli-capable install the derived header is still `gzip, deflate, br`. Verified after the merge: 235 passed in tests/test_litellm/llms/custom_httpx/, format check clean, full lint gate green.
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TLDR
Problem this solves:
amazon_novastreaming fails 100% of the time, non-streaming workszstandardis transitively installedHow it solves it:
zstdfrom the defaultAccept-Encodinglitellm sendsLITELLM_ACCEPT_ENCODINGas an escape hatchRelevant issues
Fixes #35589
Linear ticket
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito re-request a review after pushing changes)Local gates all pass (details under Changes), and CI is green on the current head (
df9359cb04: 76 successful, 1 skipped, 0 failed). The remaining unchecked box is Greptile: its 4/5 score was given on the first commit, before the Brotli fix it asked for and before the upstream merge, so it does not yet apply to this code. I've re-requested a review and will tick it once it re-scores.Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Live end-to-end against the real
amazon_novaprovider (api.nova.amazon.com, modelnova-micro-v1, real paid calls — no mocks).BEFORE — at upstream
491eda319c, where the defaultAccept-Encodingisgzip, deflate, br, zstd. Failed 5/5 streaming attempts:AFTER — validated by sending
gzip, deflate, br, same account, model and prompt. On the Brotli-capable machine used for this capture, that string is exactly what the implementation now derives, so the live run reproduces the resulting default rather than a hand-picked value:Encoding matrix on the same live provider:
Accept-Encodinggzip, deflate, br, zstd(current default)gzipfinish_reason=stopbrfinish_reason=stopgzip, deflate, br(this PR's derived default on a Brotli-capable install, + tools)Proof commits: before =
491eda319c(upstream base), after =b49dd32d5f(first commit on this PR). The follow-up commite8f58b1ed1changed only how the header value is derived, not the value sent on a Brotli-capable install, anddf9359cb04is a merge of upstream that adapts this branch to lint rules added since it was opened. Neither changes the header. Re-verified at the current head:_supported_accept_encodings()→('gzip', 'deflate', 'br').This change is transport-level and endpoint-agnostic — it alters only the request header litellm sends, so it applies identically to
/v1/chat/completions,/v1/responsesand/v1/messages. The live proof above is on/v1/chat/completionsbecauseamazon_novais the provider that surfaces the defect.Type
🐛 Bug Fix
Changes
Root cause. Amazon Nova returns
content-encoding: zstdfor streaming SSE and emits the body as independent zstd frames — a 785-byte capture contained 5 frame magics (28b52ffd) at offsets[0, 210, 405, 584, 762].httpx.ZStandardDecoder.decode()resets its decompressor only whenunused_datais non-empty within the same call, so a read that consumes exactly one whole frame leaveseof=Truewith no leftover bytes and the next read reuses a finished decompressobj:The same bytes fed in arbitrary 64-byte chunks decode fine, which is why this only appears over a real network and not in replay-based tests.
litellm advertises
zstdonly becausezstandardis installed, and that is commonly transitive (langsmithrequireszstandard>=0.23.0) — so this is the default state of an ordinary install, not an opt-in.The durable fix is upstream and already exists as encode/httpx#3697 (open since 2025-10-27, tracked in encode/httpx#3538). I verified locally that its
if self.decompressor.eof: ...resetlogic fixes exactly this reproduction. It has not shipped, so this PR is interim mitigation.The change.
get_default_headers()inlitellm/llms/custom_httpx/http_handler.pynow derivesAccept-Encodingfrom the decoders the installation actually has, and removes onlyzstd.httpx already maintains that capability set:
httpx._decoders.SUPPORTED_DECODERSpopsbrwhen neitherbrotlinorbrotlicffiis importable, and popszstdwhenzstandardis absent — it is the same registry httpx joins into its ownAccept-Encodingin_client.py. Deriving from it keeps litellm in step with what the stack can decode, sobris advertised only when Brotli decoding is available. An earlier revision of this PR hardcodedgzip, deflate, br, which would have advertised Brotli on installs with no Brotli decoder; that is fixed ine8f58b1ed1.That registry is a private httpx symbol, so this is not risk-free: if it is renamed or moved, the import fails. Construction is therefore wrapped, falling back to the codecs the standard library always provides (
gzip, deflate) rather than raising, and there is a test that deletes the symbol to prove the fallback path works.LITELLM_ACCEPT_ENCODINGstill overrides the entire value (mirroring the existingLITELLM_USER_AGENTpattern), per-request headers still win over the client default, and both the sync (HTTPHandler) and async (AsyncHTTPHandler) client factories consume the same derived default, so one change covers both.It does not touch httpx internals, and does not affect Bedrock Nova (
bedrock/amazon.nova-*), which dispatches through a different provider path.Scope note. I raised shared-vs-provider-specific in #35589. This PR takes the shared default because the incompatibility is between litellm's shared httpx transport and valid multi-frame zstd streaming, not something specific to Amazon Nova — any provider streaming multi-frame zstd hits it. A provider-specific pin would additionally require
_complete_amazon_novato forward callerheaders=/client=(it currently drops both), making it a larger diff that leaves the transport-level failure reachable elsewhere. Happy to switch to the narrower version if maintainers prefer it.Tests (offline, no credentials, no network). Capability combinations:
bradvertisedbrnot advertisedzstdstill excludedgzip, deflategzip, deflatePlus:
LITELLM_ACCEPT_ENCODINGoverride, per-requestAccept-Encodingoverride, sync and async client request headers, theLITELLM_USER_AGENTbranch still emittingAccept-Encoding, and a deterministic multi-frame decoder characterization test. That characterization test is self-retiring — once httpx decodes frame-aligned chunks correctly it skips instead of failing, so an httpx bump won't break CI, and the skip reason tells the reader the pin can then be reconsidered.Local verification at
df9359cb04:pytest tests/test_litellm/llms/custom_httpx/→ 235 passedmake format-check→ clean (2169 files)make lint→ all gates pass (ruff, strict-rule budgets, type-discipline, basedpyright, circular imports,from litellm import *import safety)tests/test_litellmrun compared against a clean-tree baseline run with identical flags: no failures attributable to this change (the suite has pre-existing flakiness under-n 4; every branch-only failure passed in isolation)Final Attestation