fix(anthropic_adapter): preserve first chunk on content-block transitions - #25216
Conversation
…ions The `/v1/messages` endpoint's AnthropicStreamWrapper silently dropped the trigger chunk of every new content block on a detected block transition (e.g. text -> thinking, thinking -> text, text -> tool_use). The code emitted `content_block_stop` -> `content_block_start` and then returned without re-enqueuing `processed_chunk`, relying on an incorrect assumption that `content_block_start` already carries the trigger chunk's content. In practice, `_translate_streaming_openai_chunk_to_anthropic_content_block()` returns an empty `TextBlock(text="")` for text transitions, so the first characters of every new text/thinking block were lost on the wire. Symptoms for Bedrock Converse reasoning providers (MiniMax, Kimi, Claude extended thinking) using `/v1/messages` + `stream=true`: - Responses start mid-sentence (leading characters missing). - If the model emits the text as a single Bedrock chunk, the text block is streamed with zero `content_block_delta` events, causing clients like Claude Code CLI to see an empty response. - Non-streaming on the same deployment returns the full text correctly. Fix: after emitting the synthetic `content_block_stop` + `content_block_start` pair, re-enqueue `processed_chunk` when it is a non-empty `content_block_delta`. A small `_trigger_delta_has_content` helper inspects the standard Anthropic delta variants (`text`, `thinking`, `partial_json`, `signature`) so that empty tool_use openers (whose tool name is already carried by `content_block_start`) are intentionally skipped. Applied to both the sync (`__next__`) and async (`__anext__`) paths. Tests: - Add `test_first_chunk_on_block_transition.py` with a focused regression test exercising a text -> thinking -> text sequence (the exact Bedrock Converse reasoning-model pattern). The new tests fail on the unpatched `main` and pass after the fix, for both sync and async iterators. - Update the existing `test_parallel_tool_calls.py::test_anthropic_stream_wrapper_interleaved_tool_calls_and_text` expected sequence: two `content_block_delta` events for the interleaved text chunks were previously missing because the wrapper dropped them; the test now also asserts the text content is preserved verbatim.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
@greptileai please review |
Greptile SummaryThis PR fixes a silent content-loss bug in
Confidence Score: 5/5Safe to merge — minimal, symmetric fix across sync/async paths fully covered by new and updated tests All findings are P2 or lower. The fix is logically correct and well-scoped. Tests explicitly verify the previously-broken behavior and pass with the fix applied. Tool-use paths are verified unaffected by existing tests. No files require special attention
|
| Filename | Overview |
|---|---|
| litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py | Adds _trigger_delta_has_content helper and correctly re-enqueues trigger delta after content_block_stop/start pair in both sync and async paths |
| tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_first_chunk_on_block_transition.py | New regression test file with sync, async, and event-ordering tests using in-memory mocks; no network calls |
| tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_parallel_tool_calls.py | Updated interleaved test to expect the two previously-dropped trigger content_block_delta events and adds text content round-trip assertions |
Sequence Diagram
sequenceDiagram
participant S as Upstream Stream
participant W as AnthropicStreamWrapper
participant C as Client
Note over W,C: Before fix — trigger chunk dropped
S->>W: chunk (first thinking/text chunk)
W->>C: content_block_stop
W->>C: content_block_start (empty body)
Note over W: processed_chunk discarded
Note over W,C: After fix — trigger chunk preserved
S->>W: chunk (first thinking/text chunk)
W->>C: content_block_stop
W->>C: content_block_start (empty body)
W->>C: content_block_delta (trigger chunk content)
S->>W: subsequent chunk
W->>C: content_block_delta
Reviews (1): Last reviewed commit: "fix(anthropic_adapter): preserve first c..." | Re-trigger Greptile
|
We're hitting this bug in production — LiteLLM proxying to Vertex AI with Claude Opus/Sonnet extended thinking models. The thinking→tool_use content block transition intermittently drops tool_use argument chunks, which causes downstream clients (Claude Code in our case) to receive empty/invalid tool inputs and enter retry loops. The failure is non-deterministic and consistent with a chunk-boundary race condition. Disabling extended thinking eliminates it, confirming the transition logic as the root cause. The Greptile review came back 5/5 confidence and the fix is small and symmetric across both paths. Would appreciate a human reviewer picking this up — it's a meaningful quality-of-life fix for anyone streaming extended thinking models through /v1/messages. |
|
Any insight on if this can get merged soon? |
|
Confirming this still in Where it bites is together with #29533 / #29600: once that change emits a real I tested the same fix independently (re-queue the trigger chunk whenever its delta carries content - One caveat for reviewers: this fixes the transition drop, but not the case where a single upstream chunk carries both That one's in the translator rather than the wrapper, reported in #27492 and isn't addressed here. With this PR the combined chunk re-queues a |
|
@dkssudgo112 can you rebase this? Thank you! |
Relevant issues
Fixes #25214
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
tests/test_litellm/— new regression suitetests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_first_chunk_on_block_transition.pyplus updates totest_parallel_tool_calls.pytests/test_litellm/llms/anthropic/experimental_pass_through/: 179 passed)AnthropicStreamWrapper@greptileai— will do after the PR is openType
🐛 Bug Fix
✅ Test
Changes
Problem
The
/v1/messagesendpoint'sAnthropicStreamWrappersilently dropped the trigger chunk of every new content block whenever a block transition was detected (text → thinking, thinking → text, text → tool_use). On the wire, the wrapper emittedcontent_block_stop→content_block_startand then returned early, discarding theprocessed_chunkcomputed from the triggering delta.The old comment claimed "the content_block_start already carries the relevant information", but
_translate_streaming_openai_chunk_to_anthropic_content_block()actually returns an empty body for text transitions:So the first characters of every new text/thinking block were permanently lost.
Symptoms (reproduced on
main@ v1.83.1)Using
/v1/messageswithstream=trueagainst a Bedrock Converse reasoning model (minimax.minimax-m2.5,moonshotai.kimi-k2.5, Claude extended thinking):content_block_deltaevents — clients likeclaude -p(Claude Code CLI) see an empty response.Raw SSE diff (prompt:
Respond with exactly: 안녕하세요, 저는 MiniMax입니다.)Non-streaming (correct):
Streaming on
main:Streaming with this PR:
Fix
In
litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py, after emitting the syntheticcontent_block_stop→content_block_startpair on a detected block transition, also enqueueprocessed_chunkwhenever it is a non-emptycontent_block_delta. A tiny helper_trigger_delta_has_content()inspects the four Anthropic delta variants (text,thinking,partial_json,signature) so that empty tool_use openers (whose tool name is already carried bycontent_block_start) are intentionally skipped and existing tool-call test expectations are preserved.Applied to both the sync
__next__path and the async__anext__path.Tests
tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_first_chunk_on_block_transition.pytext → thinking → textsequence throughAnthropicStreamWrapperwith a mockedModelResponseStream. They assert both the concatenated content of each block and the event ordering (content_block_startimmediately followed by the trigger chunk's delta).mainand pass with this PR (both sync and async).tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_parallel_tool_calls.py::test_anthropic_stream_wrapper_interleaved_tool_calls_and_textcontent_block_deltaevents for the interleaved text chunks (the wrapper was dropping them). The test now expects those deltas and also asserts the text content round-trips verbatim.tests/test_litellm/llms/anthropic/experimental_pass_through/suite (179 tests) is green with the fix.Scope
streaming_iterator.py) and the/v1/messagesexperimental pass-through streaming path./chat/completions, non-streaming/v1/messages, or for providers that don't transition between content-block types.content_block_deltaevents keep working, and clients that previously saw truncated or empty text now receive the full content.