fix(anthropic_messages): forward provider response headers on streaming /v1/messages responses - #32160
Conversation
…ng /v1/messages responses
Greptile SummaryStreaming
Confidence Score: 5/5Safe to merge — change is narrowly scoped to wrapping the streaming return value without touching request logic, auth, billing, or any non-streaming path. The wrapper is a thin delegation layer over an already-working async iterator; headers are captured once from the completed httpx response before iteration starts, so no race conditions. The aclose delegation chain is correct for both the direct and agentic branches. Tests use mocks only (no real network calls) and cover all four relevant scenarios. No changes to auth, cost tracking, or request transformation. No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/llms/anthropic/experimental_pass_through/messages/streaming_iterator.py | Adds AnthropicMessagesStreamingResponse wrapper class, SupportsAclose protocol, aclose_if_supported helper, and anthropic_messages_stream_hidden_params; all logic is straightforward and delegation is correct |
| litellm/llms/anthropic/experimental_pass_through/messages/agentic_streaming_iterator.py | Adds aclose method to AgenticAnthropicStreamingIterator that properly closes both inner and follow-up iterators via aclose_if_supported |
| litellm/llms/custom_httpx/llm_http_handler.py | Both streaming return sites now wrap in AnthropicMessagesStreamingResponse with headers captured once from response.headers before iteration begins; non-streaming path is correctly left unchanged |
| tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py | Four new mock-only tests cover direct streaming headers, agentic streaming headers, and aclose delegation on both paths; no real network calls |
Reviews (3): Last reviewed commit: "fix(anthropic_messages): forward aclose ..." | Re-trigger Greptile
Greptile SummaryThis PR fixes streaming
Confidence Score: 4/5The change is safe to merge: it adds a thin wrapper that the existing proxy header-forwarding machinery already knows how to read, and the non-streaming path is untouched. The core fix is correct and well-targeted. The new AnthropicMessagesStreamingResponse class properly implements the async-iterator protocol and the _hidden_params dict is wired into the same plumbing that CustomStreamWrapper and the google_genai path already use. The test covers the primary (non-agentic) streaming path with real headers and byte assertions. The only gap is that the agentic branch — where AgenticAnthropicStreamingIterator is wrapped — has no dedicated test, so a regression there would be invisible in CI. The agentic streaming path in llm_http_handler.py (lines 2115–2128) and the corresponding AnthropicMessagesStreamingResponse wrapping of AgenticAnthropicStreamingIterator deserve a second look, since that branch has no test coverage in this PR.
|
| Filename | Overview |
|---|---|
| litellm/llms/anthropic/experimental_pass_through/messages/streaming_iterator.py | Adds AnthropicMessagesStreamingResponse wrapper class and anthropic_messages_stream_hidden_params helper to carry provider response headers; _RESPONSE_HEADERS_ADAPTER TypeAdapter validates the header dict at module level. |
| litellm/llms/custom_httpx/llm_http_handler.py | Wraps both the direct and agentic streaming paths in AnthropicMessagesStreamingResponse before returning; stream_hidden_params is built once from the first HTTP response's headers. |
| tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py | New mock test validates that the returned stream carries llm_provider-prefixed headers and still yields SSE bytes; only the non-agentic (direct-passthrough) code path is exercised. |
Reviews (2): Last reviewed commit: "fix(anthropic_messages): forward provide..." | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
@coderabbitai review |
|
bugbot run |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Missing stream aclose forwarding
- Added an aclose method on AnthropicMessagesStreamingResponse that forwards to the wrapped completion_stream's aclose so the proxy's streaming cleanup can tear down the upstream iterator.
You can send follow-ups to the cloud agent here.
|
|
📝 WalkthroughWalkthroughAdds a new ChangesAnthropic streaming header propagation
Estimated code review effort: 2 (Simple) | ~15 minutes Sequence Diagram(s)sequenceDiagram
participant Handler as BaseLLMHTTPHandler
participant Helper as anthropic_messages_stream_hidden_params
participant Wrapper as AnthropicMessagesStreamingResponse
participant Stream as completion_stream/AgenticAnthropicStreamingIterator
Handler->>Helper: extract headers from response.headers
Helper-->>Handler: hidden_params (additional_headers)
Handler->>Wrapper: new(completion_stream, hidden_params)
Wrapper->>Stream: forward __anext__ calls
Stream-->>Wrapper: bytes chunk
Wrapper-->>Handler: async iterator with _hidden_params exposed
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
…e wrapper The proxy's streaming cleanup closes the handler's return value via hasattr(response, "aclose"); the new wrapper hid the upstream generator's aclose, so provider connections could linger on client disconnect. The wrapper now delegates aclose to the wrapped stream and AgenticAnthropicStreamingIterator closes its inner and follow-up streams. Also adds test coverage for the agentic streaming branch
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit af067b1. Configure here.
…ng /v1/messages responses (BerriAI#32160) * fix(anthropic_messages): forward provider response headers on streaming /v1/messages responses * fix(anthropic_messages): forward aclose to inner streaming iterator * fix(anthropic_messages): forward aclose through the streaming response wrapper The proxy's streaming cleanup closes the handler's return value via hasattr(response, "aclose"); the new wrapper hid the upstream generator's aclose, so provider connections could linger on client disconnect. The wrapper now delegates aclose to the wrapped stream and AgenticAnthropicStreamingIterator closes its inner and follow-up streams. Also adds test coverage for the agentic streaming branch --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Relevant issues
Linear ticket
Part of LIT-3724 (issue 2:
x-amzn-RequestId/x-amzn-trace-idnot returned for Bedrock)Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewScreenshots / Proof of Fix
Live proxy with a Bedrock invoke model (real Bedrock API,
us.anthropic.claude-opus-4-1-20250805-v1:0):Before (proxy on
litellm_internal_staging, port 4003): a streaming/v1/messagescall returns zero provider headers, so there is no way to get the Bedrock request id for an AWS support caseAfter (proxy on this branch, port 4002): the same call surfaces Bedrock's response headers, including the request id AWS support asks for
QA rerun (e2e, real Bedrock)
Independent end-to-end rerun against the real Bedrock API (
us.anthropic.claude-opus-4-1-20250805-v1:0, us-east-1) through a fresh venv and a fresh proxy on a random port (64843), same config and identical commands on both legs. The before leg ran with the worktree checked out at base26c0c93dece5182921e11387253a39dd8086db6e(origin/litellm_internal_staging) and the after leg at PR headaf067b10c9289124a323f8b520aac6f41004535b, restarting the proxy between legsBefore, at the base SHA, the streaming call returns no provider headers (
grep -icE "amzn|llm_provider"counts 0)After, at the PR head, the identical call surfaces the Bedrock headers (count 6)
A non-streaming (
"stream": false) call was also run on both legs and behaves identically before and after (nollm_provider-*headers on either, matching the non-streaming limitation described under Changes)Type
🐛 Bug Fix
Changes
Streaming
/v1/messagesresponses were returned to the proxy as bare async generators, which cannot carry_hidden_params, so the upstream provider's HTTP response headers were dropped andreturn_response_headers: truehad no effect on this route. For Bedrock that meantx-amzn-RequestId/x-amzn-trace-idwere never surfaced, which customers need to open AWS support cases about streaming failuresasync_anthropic_messages_handlernow wraps the streaming return (both the direct stream and the agentic iterator) inAnthropicMessagesStreamingResponse, a thin async-iterator wrapper that carries_hidden_params["additional_headers"]built from the upstreamhttpxresponse headers via the existingprocess_response_headershelper (same mechanismCustomStreamWrapperand the google_genai streaming path use). The proxy's existing header plumbing inbase_process_llm_requestpicks these up unchanged and emits them asllm_provider-*response headers on the SSE response for every provider on this route (Bedrockx-amzn-*, Anthropicrequest-id, etc)Non-streaming
/v1/messagesresponses are aTypedDictthat cannot carry_hidden_paramsat all (a known limitation, see_response_cost_from_logging_obj); forwarding headers there needs proxy-side plumbing changes and is intentionally left out of this PR's scopeThe wrapper also forwards
acloseto the wrapped stream, andAgenticAnthropicStreamingIteratorcloses its inner and follow-up streams, so the proxy's streaming cleanup (thehasattr(response, "aclose")check in_finalize_streaming_generator_cleanup) still releases the upstream provider connection on client disconnect instead of leaving it to garbage collectionThe added tests drive
async_anthropic_messages_handlerwith an injected mock HTTP client whose response carriesx-amzn-requestid/x-amzn-trace-id, for both the direct streaming branch and the agentic branch (a callback overridingasync_should_run_agentic_loop), and assert the returned stream satisfies the async-iterator protocol the proxy detects, exposes thellm_provider-*prefixed headers in_hidden_params["additional_headers"], and still yields the SSE bytes unchanged. Two aclose regression tests assert that closing the wrapper closes the upstream generator on both branchesNote
Medium Risk
Touches the proxy-facing streaming path for all /v1/messages providers; behavior change is additive (headers + cleanup) with regression tests, but mis-wrapped streams could still affect disconnect handling.
Overview
Fixes LIT-3724 where streaming
/v1/messagesdropped upstream provider HTTP headers (e.g. Bedrockx-amzn-requestid) because the handler returned bare async generators with no_hidden_params.async_anthropic_messages_handlernow wraps both the direct SSE stream and the agenticAgenticAnthropicStreamingIteratorinAnthropicMessagesStreamingResponse, which keeps_hidden_params["additional_headers"](viaprocess_response_headers, same pattern as other streaming paths) so the proxy can emitllm_provider-*headers on the SSE response. SSE bytes are unchanged.The wrapper and agentic iterator implement
aclose(viaaclose_if_supported) so proxy streaming cleanup still tears down the upstream connection on client disconnect. Non-streaming header forwarding is out of scope.Reviewed by Cursor Bugbot for commit af067b1. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit
New Features
Bug Fixes