fix(anthropic): report reasoning_tokens in streaming usage - #27318
fix(anthropic): report reasoning_tokens in streaming usage#27318mateo-berri wants to merge 1 commit into
Conversation
Anthropic streaming responses with extended thinking always returned completion_tokens_details.reasoning_tokens=0 because the ModelResponseIterator forwarded thinking deltas to the client but never accumulated them. By the time message_delta arrived and _handle_usage built the final Usage object, the thinking text had been discarded, so calculate_usage was called with reasoning_content=None and the reasoning token estimate was always 0. All thinking tokens were instead counted as text_tokens. Non-streaming responses worked because the full response (including thinking blocks) is available when calculate_usage runs. This change adds an accumulator for thinking deltas during streaming and passes the accumulated text to calculate_usage at message_delta time. The accumulator stays empty for non-thinking streams, preserving existing behavior. Adds two regression tests covering both the bug repro (reasoning_tokens > 0 in the final usage chunk) and the no-thinking case (reasoning_tokens stays 0). Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>
|
|
Greptile SummaryThis PR fixes a bug where streaming Anthropic responses with extended thinking always reported
Confidence Score: 4/5Safe to merge; the change is narrowly scoped to the streaming reasoning-token accounting path and is guarded by two new regression tests. The fix is minimal and correct, following exactly the same accumulator pattern already used for web_search_results, compaction_blocks, and tool_results in the same class. The only finding is a placeholder issue URL left in a comment, which has no runtime impact. The placeholder issue URL on line 593 of handler.py should be updated before merge, but it has no functional impact.
|
| Filename | Overview |
|---|---|
| litellm/llms/anthropic/chat/handler.py | Adds accumulated_reasoning_content buffer to ModelResponseIterator, appends thinking deltas in _content_block_delta_helper, and passes the buffer to calculate_usage in _handle_usage. Fix is minimal and correct; placeholder issue URL left in a comment. |
| tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py | Two new regression tests added: one verifies reasoning_tokens > 0 is reported when thinking deltas are streamed, the other verifies reasoning_tokens == 0 for plain-text streams. Both use only mocks — no real network calls. |
Reviews (1): Last reviewed commit: "fix(anthropic): report reasoning_tokens ..." | Re-trigger Greptile
| # message_delta only reports total output_tokens, so without this | ||
| # accumulator completion_tokens_details.reasoning_tokens is always 0 | ||
| # for streaming responses even when extended thinking is enabled. | ||
| # See: https://github.com/BerriAI/litellm/issues/<TBD> |
There was a problem hiding this comment.
Placeholder issue URL left in comment
The comment references https://github.com/BerriAI/litellm/issues/<TBD>, which is an unfilled placeholder. This will be confusing to any contributor who tries to follow the link. Either replace it with the real issue number or remove the See: line entirely.
|
Duplicate of #27319 |
Relevant issues
Fixes a bug where streaming Anthropic chat completions with extended thinking always returned
completion_tokens_details.reasoning_tokens = 0, while non-streaming requests for the exact same prompt correctly reported the reasoning token count. All thinking tokens were silently absorbed intotext_tokens.Linear ticket
Root cause
ModelResponseIterator(inlitellm/llms/anthropic/chat/handler.py, also referred to asAnthropicStreamResponsein some bug reports) forwards thinking deltas to the client as streaming chunks but never accumulates the thinking text internally. By the time the stream'smessage_deltaarrives and_handle_usage()builds the finalUsage, the thinking text is gone — so it callsAnthropicConfig().calculate_usage(..., reasoning_content=None, ...).In
transformation.py,calculate_usageestimates reasoning tokens by runningtoken_counter()overreasoning_content. WithNoneit short-circuits to0:The non-streaming path works because the full response (including thinking blocks) is materialized before
calculate_usageruns.Reported numbers from the same prompt ("probability of getting exactly 2 red balls from 3 boxes") with
thinking: {"type": "adaptive"}:stream: falsestream: true(before fix)Fix
self.accumulated_reasoning_content: str = ""toModelResponseIterator.__init__._content_block_delta_helper, append the thinking text from eachthinking_deltaevent to that buffer (no-op for non-thinking deltas, including signature-only deltas)._handle_usage, passreasoning_content=self.accumulated_reasoning_content or Nonesocalculate_usagecan estimatereasoning_tokensfor streams that contain thinking, while preserving the existingNonepath for non-thinking streams.The buffer is purely additive and stays empty for any stream that doesn't carry thinking deltas, so non-thinking streaming responses are unaffected. Same pattern used by the existing
web_search_results,compaction_blocks, andtool_resultsaccumulators in the same class.Pre-Submission checklist
tests/test_litellm/(two new regression tests intests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py):test_streaming_accumulates_reasoning_tokens_in_usage— drives a synthetic Anthropic SSE stream containingthinking_deltachunks and asserts the finalUsagereportsreasoning_tokens > 0andtext_tokens = output_tokens - reasoning_tokens. Without the fix this asserts onreasoning_tokens=0and fails.test_streaming_no_thinking_leaves_reasoning_tokens_zero— drives a text-only stream and assertsreasoning_tokensstays0andtext_tokensequals the fulloutput_tokens. Guards against false positives from the new accumulator.uv run pytest tests/test_litellm/llms/anthropic/chat/ -x -q→ 258 passed.uv run pytest tests/test_litellm/llms/bedrock/messages/ -q→ 36 passed.Type
🐛 Bug Fix
Changes
litellm/llms/anthropic/chat/handler.py:accumulated_reasoning_contentbuffer toModelResponseIterator.thinking_deltatext to the buffer in_content_block_delta_helper.calculate_usagefrom_handle_usage.tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py:Proof of fix
Output from running the new tests against this branch:
Both new tests pass with the fix. The first test fails (
reasoning_tokens == 0) onmainwithout the change, reproducing the bug.Slack Thread