Skip to content

fix(streaming): tolerate message_start without usage - #2037

Open
Sanath91009 wants to merge 1 commit into
anthropics:mainfrom
Sanath91009:fix/output-tokens-none-1806
Open

Sanath91009 wants to merge 1 commit into
anthropics:mainfrom
Sanath91009:fix/output-tokens-none-1806

Conversation

@Sanath91009

Copy link
Copy Markdown

What breaks

Streaming a message whose message_start event carries no usage object crashes the accumulator
partway through the stream:

AttributeError: 'NoneType' object has no attribute 'output_tokens'

construct_type fills the missing field with None, and accumulate_event then writes straight
through it on the first message_delta:

current_snapshot.usage.output_tokens = event.usage.output_tokens

The failure is easy to miss in testing because the usual fixtures always include usage in
message_start. The streaming docs' own thinking example does not, and any intermediary that strips
or reshapes the first event produces the same shape.

Reproduced offline with an httpx2.MockTransport replaying that event order, no API key:

message_start (no usage) -> content_block_delta -> message_delta(usage) -> message_stop
AttributeError: 'NoneType' object has no attribute 'output_tokens'

Cause

src/anthropic/lib/streaming/_messages.py and the beta equivalent, in accumulate_event: the
message_delta branch assumes current_snapshot.usage is always populated by message_start. It
is not, and the type only promises Usage | None.

Fix

Seed the snapshot's usage from the first delta that carries one, and let a stream that never carries
usage finish with usage=None instead of raising:

  • if event.usage is present and the snapshot has no usage yet, build one from the delta and attach
    it before writing the cumulative counters
  • keep the existing overwrite semantics for the optional counters, so a delta that omits a field
    still leaves the message_start value intact
  • when the delta has no usage at all, skip the block rather than dereferencing it

Applied identically to _messages.py and _beta_messages.py, which duplicate this accumulator.

Tests

Four regression tests per module (eight total) in tests/lib/streaming/, plus two recorded response
fixtures:

  • message_start without usage, delta with usage: accumulator seeds it and finishes with correct counts

  • message_start without usage, no delta ever supplies usage: stream finishes with usage=None, no raise

  • sync and async clients both covered

    with the fix

    83 passed (test_messages.py + test_beta_messages.py)

    fix reverted, same files

    8 failed, 75 passed

No other test changed behaviour. ruff check and ruff format --check pass on every touched file.

Compatibility

No public API change. usage is already typed Usage | None; this makes the runtime match the type
instead of assuming it away. Users who stream from endpoints that always send usage see no
difference.

@Sanath91009
Sanath91009 requested a review from a team as a code owner October 26, 2026 15:11

This branch has not been deployed

No deployments
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.

2 participants