Skip to content

fix(anthropic): report reasoning_tokens in streaming usage - #27318

Closed
mateo-berri wants to merge 1 commit into
litellm_internal_stagingfrom
litellm_anthropic-stream-reasoning-tokens-ccc7
Closed

fix(anthropic): report reasoning_tokens in streaming usage#27318
mateo-berri wants to merge 1 commit into
litellm_internal_stagingfrom
litellm_anthropic-stream-reasoning-tokens-ccc7

Conversation

@mateo-berri

Copy link
Copy Markdown
Contributor

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 into text_tokens.

Linear ticket

Root cause

ModelResponseIterator (in litellm/llms/anthropic/chat/handler.py, also referred to as AnthropicStreamResponse in some bug reports) forwards thinking deltas to the client as streaming chunks but never accumulates the thinking text internally. By the time the stream's message_delta arrives and _handle_usage() builds the final Usage, the thinking text is gone — so it calls AnthropicConfig().calculate_usage(..., reasoning_content=None, ...).

In transformation.py, calculate_usage estimates reasoning tokens by running token_counter() over reasoning_content. With None it short-circuits to 0:

reasoning_tokens = (
    token_counter(text=reasoning_content, count_response_tokens=True)
    if reasoning_content
    else 0
)

The non-streaming path works because the full response (including thinking blocks) is materialized before calculate_usage runs.

Reported numbers from the same prompt ("probability of getting exactly 2 red balls from 3 boxes") with thinking: {"type": "adaptive"}:

Mode reasoning_tokens text_tokens
stream: false 169 332
stream: true (before fix) 0 732

Fix

  • Add self.accumulated_reasoning_content: str = "" to ModelResponseIterator.__init__.
  • In _content_block_delta_helper, append the thinking text from each thinking_delta event to that buffer (no-op for non-thinking deltas, including signature-only deltas).
  • In _handle_usage, pass reasoning_content=self.accumulated_reasoning_content or None so calculate_usage can estimate reasoning_tokens for streams that contain thinking, while preserving the existing None path 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, and tool_results accumulators in the same class.

Pre-Submission checklist

  • I have added testing in tests/test_litellm/ (two new regression tests in tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py):
    • test_streaming_accumulates_reasoning_tokens_in_usage — drives a synthetic Anthropic SSE stream containing thinking_delta chunks and asserts the final Usage reports reasoning_tokens > 0 and text_tokens = output_tokens - reasoning_tokens. Without the fix this asserts on reasoning_tokens=0 and fails.
    • test_streaming_no_thinking_leaves_reasoning_tokens_zero — drives a text-only stream and asserts reasoning_tokens stays 0 and text_tokens equals the full output_tokens. Guards against false positives from the new accumulator.
  • All anthropic chat tests pass: uv run pytest tests/test_litellm/llms/anthropic/chat/ -x -q258 passed.
  • Bedrock anthropic-message tests pass: uv run pytest tests/test_litellm/llms/bedrock/messages/ -q36 passed.
  • PR's scope is isolated to one specific problem (Anthropic streaming reasoning_tokens accounting).

Type

🐛 Bug Fix

Changes

  • litellm/llms/anthropic/chat/handler.py:
    • Added accumulated_reasoning_content buffer to ModelResponseIterator.
    • Append thinking_delta text to the buffer in _content_block_delta_helper.
    • Passed the buffered text into calculate_usage from _handle_usage.
  • tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py:
    • Added two regression tests covering the streaming reasoning token accumulation.

Proof of fix

Output from running the new tests against this branch:

$ uv run pytest tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py -k "reasoning_tokens" -vv
...
collected 30 items / 28 deselected / 2 selected

tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py::test_streaming_accumulates_reasoning_tokens_in_usage PASSED [ 50%]
tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py::test_streaming_no_thinking_leaves_reasoning_tokens_zero PASSED [100%]

======================= 2 passed, 28 deselected in 0.58s =======================

Both new tests pass with the fix. The first test fails (reasoning_tokens == 0) on main without the change, reproducing the bug.

Slack Thread

Open in Web Open in Cursor 

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>
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@mateo-berri
mateo-berri marked this pull request as ready for review May 6, 2026 20:03
@greptile-apps

greptile-apps Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a bug where streaming Anthropic responses with extended thinking always reported reasoning_tokens = 0 in completion_tokens_details, while the identical non-streaming path reported them correctly. The root cause was that thinking deltas were forwarded to the client but never accumulated, so _handle_usage always passed reasoning_content=None to calculate_usage.

  • Adds an accumulated_reasoning_content string buffer to ModelResponseIterator.__init__, appended to in _content_block_delta_helper for each thinking_delta event (no-op for signature-only or text deltas).
  • Passes self.accumulated_reasoning_content or None to calculate_usage in _handle_usage, matching the existing estimation pattern used by the non-streaming path.
  • Two new mock-only regression tests cover both the thinking and non-thinking streaming cases.

Confidence Score: 4/5

Safe 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.

Important Files Changed

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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

@mateo-berri

Copy link
Copy Markdown
Contributor Author

Duplicate of #27319

@mateo-berri mateo-berri marked this as a duplicate of #27319 May 6, 2026
@mateo-berri mateo-berri closed this May 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants