Repository navigation
fix(bedrock): backport the /v1/messages Invoke streaming pass-through to rc/1.103.0 (#42607) - #42658
Conversation
…ding them in a 1024-byte chunker (#42607) * fix(bedrock): stream /v1/messages Invoke bytes through instead of holding them in a 1024-byte chunker Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * style(bedrock): apply ruff format to invoke messages stream passthrough Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * style(bedrock): drop drive-by reformat of existing invoke messages tests Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(bedrock): collect streamed chunks into a tuple in passthrough regression test Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(bedrock): give the passthrough regression test a 10s first-chunk budget * test(bedrock): type the eventstream frame helper's payload as Mapping[str, object] * test(bedrock): take the gated byte stream's chunks as an immutable Sequence --------- Co-authored-by: mateo <mateo@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com> (cherry picked from commit 975bd28)
|
|
|
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 4a00468. Configure here.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
TLDR
Problem this solves:
/v1/messagesstreaming on Bedrock Invoke, but only on mainrc/1.103.0How it solves it:
rc/1.103.0, clean, no conflictsrc/1.103.0carries the fixUser Flow
Before: a Claude Code user on a
bedrock/invoke/Claude deployment sees a dead stream while the model writes a large file"stream": truemessage_startandcontent_block_startright away, but they add up to under 1024 bytes, so nothing reaches the clientAfter: the same request streams each event the moment Bedrock sends it
"stream": truemessage_startandcontent_block_startarrive at the client within milliseconds of Bedrock sending themBackported PR
/v1/messagesInvoke streaming held events behind a 1024-byte chunkerCherry-picked with
git cherry-pick -x 975bd28549, no conflicts, so the diff here is main's byte for byte: the decoder'saiter_bytes()no longer asks httpx for 1024-byte chunks, and the subclass override ofDEFAULT_CHUNK_SIZEis gone. #42607 carries nobackport-stablelabel because the chunker is not a regression: it has been on every line since #10710, which is why the pick targets the line the customer runs instead of the newest stable.stable/*lines are untouchedRelevant issues
Affected release
not a regression: present in every release since #10710, including v1.103.0-rc.1 (tagged at ccf5e8c, the current tip of
rc/1.103.0)Linear ticket
Resolves LIT-8392
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
test_get_async_streaming_response_iterator_yields_small_frame_before_upstream_pausescomes along unchanged; the whole file is 123 passed on this line)uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or moremake checkpasses at 4a00468, and the tip's CI is pipeline 90133, its three reds classified under Caveats@greptileaito re-request a review after pushing changes)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 proxy A/B on this line against real Bedrock, no mocks. One proxy per commit, each from its own worktree, both started with the same one-model config on a random free port. Before is a detached checkout of the
rc/1.103.0tip ccf5e8c (the commit v1.103.0-rc.1 is tagged at) on port 50413, After is this branch at 4a00468 on port 42507:The streaming probe is the customer's request shape, an immediate tool call whose input is large, with no text before it. Each
event:line is stamped with the wall clock and the seconds since the request was sent:Two things to know when reading the timelines. The
pingevents every 15 s are the proxy's own SSE keepalive; Bedrock sends no ping frames on this path. And Bedrock itself goes quiet for 30 to 40 s aftercontent_block_startwhile the model generates the tool input. That upstream pause is the same on both legs, so the thing to compare is when the events Bedrock sent before the pause reach the client. The two probes below were sent at the same instant, one per proxyClaude Code is the customer's client, so it was driven on both legs too: the interactive TUI (v2.1.280) under tmux, pointed at each proxy with
ANTHROPIC_BASE_URL=http://localhost:<port> ANTHROPIC_AUTH_TOKEN=sk-1234 ANTHROPIC_MODEL=bedrock-invoke-sonnet-4-6 ANTHROPIC_SMALL_FAST_MODEL=bedrock-invoke-sonnet-4-6 ANTHROPIC_DEFAULT_HAIKU_MODEL=bedrock-invoke-sonnet-4-6 CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 claude --dangerously-skip-permissions, given/clearand then the prompt "Immediately use the Write tool, with no text before or after the tool call, to create nginx-guide-1.md: a detailed 1500 word guide to configuring nginx as a reverse proxy, with sections, config examples, and troubleshooting.", with the pane captured 12 s, 25 s, and 40 s after the prompt was sentBefore (ccf5e8c)
Large tool input over /v1/messages, streaming
message_startreaches the client at +45.5 s, only once Bedrock resumed and the buffer crossed 1024 bytes. 1868 events in total, the largest gaps 15.0 s before a ping, 12.7 s beforemessage_start, and 0.4 s before acontent_block_deltaNon-streaming /v1/messages parity
curl -s http://localhost:50413/v1/messages -H 'Authorization: Bearer sk-1234' -H 'content-type: application/json' -d '{"model":"bedrock-invoke-sonnet-4-6","max_tokens":60,"messages":[{"role":"user","content":"Reply with exactly the word ok."}]}'typemessage,roleassistant,stop_reasonend_turn, onetextblock; top-level keyscontent, id, model, role, stop_details, stop_reason, stop_sequence, type, usage; usage keyscache_creation, cache_creation_input_tokens, cache_read_input_tokens, input_tokens, output_tokens, all counts positiveText-only streaming parity
curl -sN http://localhost:50413/v1/messages -H 'Authorization: Bearer sk-1234' -H 'content-type: application/json' -d '{"model":"bedrock-invoke-sonnet-4-6","max_tokens":120,"stream":true,"messages":[{"role":"user","content":"Write two short sentences about the sea."}]}'message_start content_block_start content_block_delta x15 content_block_stop message_delta message_stop, themessage_deltausage carrying the same five keys with a positiveoutput_tokensSDK entrypoint,
litellm.anthropic.messages.acreate(stream=True)await litellm.anthropic.messages.acreate(model="bedrock/invoke/us.anthropic.claude-sonnet-4-6", max_tokens=120, stream=True, messages=[{"role": "user", "content": "Write two short sentences about the sea."}])and iterate the streammessage_start content_block_start content_block_delta x14 content_block_stop message_delta message_stopClaude Code, interactive
Processing… ↓ 108 tokensat 12 s, 25 s, and still at 40 s. No "Waiting for API response" or "check your network" line appearedAfter (4a00468)
Large tool input over /v1/messages, streaming
message_start,content_block_start, and the firstcontent_block_deltareach the client at +3.3 s, the moment Bedrock sent them. The keepalives then cover Bedrock's own pause and the deltas resume at +41.2 s. 1768 events in total, the largest gaps 15.0 s and 15.0 s before pings and 8.0 s before acontent_block_delta; no event was held behind the pauseNon-streaming /v1/messages parity
typemessage,roleassistant,stop_reasonend_turn, onetextblock, the same nine top-level keys and the same five usage keys, all counts positiveText-only streaming parity
message_start content_block_start content_block_delta x16 content_block_stop message_delta message_stop, themessage_deltausage carrying the same five keys with a positiveoutput_tokensSDK entrypoint,
litellm.anthropic.messages.acreate(stream=True)message_start content_block_start content_block_delta x16 content_block_stop message_delta message_stopClaude Code, interactive
Metamorphosing… ↓ 111 tokensat 12 s, 25 s, and 40 s. The on-screen stall is the same length on both legs (it is Bedrock's own tool-input pause, and the proxy keepalives keep the connection visibly alive on both), so this leg does not separate the two commits; the wire timelines above doType
🐛 Bug Fix
Caveats (if any)
Low
chunk_size=1024back intoaiter_bytes()makes the picked test failintegration-extensions:test_tool_error_remains_error_and_healthy_sibling_returns_value, the same single failure the base's own run on cc44919 shows (job 2193617), and the rerun (job 2208161) fails it againe2e_ui_testing: theproxy-admin/keyBudgetspec is red on main's scheduled runs 90101, 90114, and 90144 too; themcp/mcpToolsspec waits on a live upstream MCP server's tool list; the rerun (job 2208159) fails the same two specs againlocal_testing_part2:test_openai_stream_options_call_text_completionfails inside the OpenAI text-completion client with pydantic's'MockValSer' object is not an instance of 'SchemaSerializer'; green on main's 90101 and 90114 runs, which fail other tests; green on the rerun (job 2208158)Blast radius at 4a00468:
aiter_bytes()now yields per network read, and botocore'sEventStreamBufferalready reassembles frames across arbitrary byte boundaries, so partial frames are handled exactly as before. httpx is 0.28.1 on this line, same as main__init__; the class-levelDEFAULT_CHUNK_SIZEhad no other reader on this line (theDEFAULT_CHUNK_SIZEinlitellm/constants.pyis the RAG text splitter's, unrelated). Reached by/v1/messagesstreaming onbedrock/invoke/<claude model>and bylitellm.anthropic.messages.acreate(stream=True), both driven above. The mantle path calls the Anthropic base iterator, and Converse (bedrock/<model>) and/v1/chat/completionsnever used this decoderFinal Attestation
Note
Low Risk
Scoped to async Bedrock Invoke Anthropic messages streaming delivery timing; event framing and response shapes are unchanged, with regression coverage for small early frames.
Overview
Bedrock Invoke
/v1/messagesstreaming no longer buffers early SSE behind a 1024-byte read chunk, so clients (e.g. Claude Code) getmessage_startand tool events as soon as Bedrock sends them instead of waiting for the buffer to fill during long upstream pauses.get_async_streaming_response_iteratornow feeds the AWS event-stream decoder fromhttpx_response.aiter_bytes()with httpx’s default chunking, and the messages-specificAmazonAnthropicClaudeMessagesStreamDecoderoverride that forcedDEFAULT_CHUNK_SIZE = 1024is removed. A regression test simulates a gated upstream stream and asserts the firstmessage_startSSE is yielded before the second frame arrives.Reviewed by Cursor Bugbot for commit 4a00468. Bugbot is set up for automated code reviews on this repo. Configure here.