Skip to content

fix(anthropic): report token usage for streaming responses#2112

Draft
vjsai wants to merge 1 commit into
agentscope-ai:mainfrom
vjsai:fix/anthropic-stream-usage
Draft

fix(anthropic): report token usage for streaming responses#2112
vjsai wants to merge 1 commit into
agentscope-ai:mainfrom
vjsai:fix/anthropic-stream-usage

Conversation

@vjsai

@vjsai vjsai commented Jul 11, 2026

Copy link
Copy Markdown

AgentScope-Java Version

2.0.0-SNAPSHOT (reproduced on main; reported against 2.0.0-RC4/RC5)

Description

Fixes #2094.

Background. Anthropic reports token usage in SSE events that carry no content:

  • message_startmessage.usage.input_tokens
  • message_deltausage.output_tokens

The bug. parseStreamEvents ends with a filter that drops any response with empty content:

.filter(response -> response != null && !response.getContent().isEmpty());

Both usage-bearing events have empty content, so both were discarded and ChatResponse.getUsage() was always null when streaming. On top of that, parseStreamEvent read only the message id from message_start and never its input_tokens, and because it is a stateless static method, the message_delta branch could only ever build a ChatUsage with outputTokens — the input count had nowhere to live. Non-streaming (parseMessage) was unaffected.

The change.

  1. Carry the input token count across the stream and attach it to the usage-bearing message_delta response, so the caller sees both counts on one response (inputTokens=5, outputTokens=10 for the issue's example stream). The counter is held per-subscription via Flux.defer, so concurrent streams don't share state.
  2. Let responses that report usage through the filter (!content.isEmpty() || usage != null).

message_start itself is still filtered out — it has neither content nor usage of its own — so the existing stream-filtering contract is unchanged. This keeps parseStreamEvent's signature intact.

The issue proposed either attaching usage to the final event or emitting a usage-only response; this does both at once — the message_delta response is usage-only, and it reports the full input+output counts rather than output alone.

How to test

New regression test AnthropicResponseParserTest#testParseStreamEventsReportsUsage feeds a message_start (input_tokens=5) followed by a message_delta (output_tokens=10) and asserts the caller receives a response reporting both.

Fails before the change (the usage-bearing response never reaches the subscriber):

[ERROR] AnthropicResponseParserTest.testParseStreamEventsReportsUsage:338
        expectation "assertNext" failed (expected: onNext(); actual: onComplete())

Passes after. The full module is green — note the two pre-existing stream tests (testParseStreamEventsMessageStart, testParseStreamEventsFiltersEmptyContent) still pass unmodified:

mvn -pl agentscope-extensions/.../agentscope-extensions-model-anthropic verify
[INFO] Tests run: 146, Failures: 0, Errors: 0, Skipped: 1
[INFO] BUILD SUCCESS

Checklist

  • Code has been formatted with mvn spotless:apply
  • All tests are passing (mvn test)
  • Javadoc comments are complete and follow project conventions
  • Related documentation has been updated (e.g. links, examples, etc.) — not applicable, no user-facing API change
  • Code is ready for review

Anthropic reports token usage in content-less SSE events: message_start
carries input_tokens and message_delta carries output_tokens. Both were
discarded by parseStreamEvents, whose filter drops any response with
empty content, so ChatResponse.getUsage() was always null when streaming
(non-streaming was unaffected).

Carry the input token count across the stream and attach it to the
usage-bearing message_delta response, and let responses that report usage
through the filter. The count is held per-subscription via Flux.defer so
concurrent streams do not share state.

message_start itself is still filtered out, as it has neither content nor
usage of its own.

Closes agentscope-ai#2094
@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.

@codecov

codecov Bot commented Jul 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.18919% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...l/anthropic/formatter/AnthropicResponseParser.java 89.18% 0 Missing and 4 partials ⚠️

📢 Thoughts on this report? Let us know!

@AgentScopeJavaBot AgentScopeJavaBot added bug Something isn't working area/core/model Model providers and formatters labels Jul 12, 2026

@AgentScopeJavaBot AgentScopeJavaBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 AI Review

Fixes #2094: Anthropic streaming responses had null usage because parseStreamEvents filter dropped usage-bearing events. Carries input token count via Flux.defer and lets usage responses through filter. Sound approach with regression test. Three recommended improvements: (1) flatMap should be concatMap for ordering guarantees; (2) parseStreamEvent lacks null guards on event fields; (3) withInputTokens doesn't copy metadata/finishReason fields.

(inline comments could not be attached — line numbers fell outside PR hunks. See archived report.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core/model Model providers and formatters bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]:Anthropic streaming responses drop ChatUsage

3 participants