Skip to content

fix(gateway): stream v1/messages usage tokens - #2380

Merged
smakosh merged 2 commits into
theopenco:mainfrom
RATCHAW:RATCHAW/messages-stream-token-counts
May 23, 2026
Merged

smakosh merged 2 commits into
theopenco:mainfrom
RATCHAW:RATCHAW/messages-stream-token-counts

Conversation

@RATCHAW

@RATCHAW RATCHAW commented May 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The Anthropic Messages translator at /v1/messages was reporting input_tokens: 0, output_tokens: 0 in the streamed message_delta event. The upstream /v1/chat/completions endpoint emits finish_reason and usage in separate chunks (the usage chunk has finish_reason: null), so gating usage extraction on finish_reason always missed it. The fix defers message_delta until [DONE] (or stream end), extracts usage from any chunk that carries it, and makes the terminator events idempotent. Added paired Anthropic/Bedrock e2e tests in native-anthropic-cache.e2e.ts that fail on the old code (zero usage on bedrock, duplicate message_delta on anthropic) and pass with the fix.

Test plan

  • Verified zero-token repro against local gateway with stashed fix
  • Verified correct tokens (14/14 matching non-streaming) with fix applied
  • New e2e tests fail on reverted code, pass with fix restored
  • Verify on prod (api.llmgateway.io) after deploy

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Fixed streaming message events to eliminate duplicates and ensure proper sequencing
    • Corrected token usage data reporting in streaming API responses, including cache metrics
  • Tests

    • Added end-to-end test coverage for streaming message token usage validation

Review Change Stack

The upstream /v1/chat/completions endpoint emits finish_reason and
usage in separate chunks. The Anthropic translator only read usage
when it saw finish_reason, so the streamed message_delta carried
zeros for input_tokens/output_tokens. Defer message_delta until
[DONE] (or stream end) and extract usage from any chunk that
carries it. Add e2e regression tests for anthropic + bedrock.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 22, 2026 •

Copy link
Copy Markdown
Contributor

Walkthrough

Refactors Anthropic streaming SSE event flow to deduplicate and order terminal events (content_block_stop, message_delta, message_stop) with usage extraction decoupled from finish-reason gating, plus adds e2e coverage for streaming usage validation across both Anthropic and AWS Bedrock model backends.

Changes

Anthropic Streaming Event Centralization

Layer / File(s) Summary
Streaming state and deduplication helpers
apps/gateway/src/anthropic/anthropic.ts
New streaming variables (stopReason, one-time flags) and helpers (extractUsage, sendContentBlockStops, sendMessageDelta) decouple usage normalization from finish-reason gating and coordinate ordered emission of terminal events to prevent duplication.
Terminal event emission in streaming paths
apps/gateway/src/anthropic/anthropic.ts
Integrates helpers into three points: unconditional usage extraction per chunk, flush of deferred content_block_stop and message_delta on upstream [DONE], and fallback emission plus conditional message_stop if stream ends without explicit [DONE].
E2E streaming usage validation
apps/gateway/src/native-anthropic-cache.e2e.ts
Test helpers and dual-provider test cases confirm exactly one message_delta is emitted and that both Anthropic (anthropic/claude-haiku-4-5) and AWS Bedrock (aws-bedrock/claude-haiku-4-5) report non-zero token counts in the usage field.

Possibly related PRs

  • theopenco/llmgateway#2271: Both PRs modify the Anthropic /v1/messages streaming SSE handling in apps/gateway/src/anthropic/anthropic.ts, specifically the terminal event emission logic (message_stop sequencing) so they overlap in how streaming completion events are produced.

Suggested reviewers

  • smakosh
  • proxysoul

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: fixing streaming usage token reporting in v1/messages endpoint, which is the core issue addressed throughout the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/gateway/src/native-anthropic-cache.e2e.ts`:
- Around line 724-744: These two tests ("streaming /v1/messages surfaces
non-zero usage tokens for anthropic" and "streaming /v1/messages surfaces
non-zero usage tokens for bedrock") exercise external provider streams and
cache-sensitive behavior and must not run inside the concurrent suite using
getConcurrentTestOptions(); fix by running them serially — either move these two
test(...) calls out of the describe that uses getConcurrentTestOptions() into a
serial describe (using getTestOptions()) or mark the individual tests as serial
(e.g., replace test with test.serial when invoking the tests that call
assertStreamingUsage("anthropic/claude-haiku-4-5", ...) and
assertStreamingUsage("aws-bedrock/claude-haiku-4-5", ...)) so they never execute
in parallel with other concurrent tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b3499a3b-326b-4aac-87b8-53e5cc454c1d

📥 Commits

Reviewing files that changed from the base of the PR and between 7664eec and 3dd493f.

📒 Files selected for processing (2)
  • apps/gateway/src/anthropic/anthropic.ts
  • apps/gateway/src/native-anthropic-cache.e2e.ts

Comment on lines +724 to +744
(hasAnthropicKey ? test : test.skip)(
"streaming /v1/messages surfaces non-zero usage tokens for anthropic",
getTestOptions(),
async () => {
await assertStreamingUsage(
"anthropic/claude-haiku-4-5",
"streaming /v1/messages anthropic usage",
);
},
);

(hasBedrockKey ? test : test.skip)(
"streaming /v1/messages surfaces non-zero usage tokens for bedrock",
getTestOptions(),
async () => {
await assertStreamingUsage(
"aws-bedrock/claude-haiku-4-5",
"streaming /v1/messages bedrock usage",
);
},
);

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Run these new e2e cases serially.

These tests are being added under the concurrent describe(..., getConcurrentTestOptions(), ...) suite on Line 114, but they exercise external provider streams and cache-sensitive behavior. That is a flake-prone combination, and it conflicts with the repo rule for *.e2e.ts suites.

As per coding guidelines, "**/*.{spec.ts,e2e.ts}: Do not run test files or suites in parallel unless the repository instructions for that exact suite explicitly require it".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/gateway/src/native-anthropic-cache.e2e.ts` around lines 724 - 744, These
two tests ("streaming /v1/messages surfaces non-zero usage tokens for anthropic"
and "streaming /v1/messages surfaces non-zero usage tokens for bedrock")
exercise external provider streams and cache-sensitive behavior and must not run
inside the concurrent suite using getConcurrentTestOptions(); fix by running
them serially — either move these two test(...) calls out of the describe that
uses getConcurrentTestOptions() into a serial describe (using getTestOptions())
or mark the individual tests as serial (e.g., replace test with test.serial when
invoking the tests that call assertStreamingUsage("anthropic/claude-haiku-4-5",
...) and assertStreamingUsage("aws-bedrock/claude-haiku-4-5", ...)) so they
never execute in parallel with other concurrent tests.

@smakosh
smakosh added this pull request to the merge queue May 23, 2026
Merged via the queue into theopenco:main with commit d4d6751 May 23, 2026
11 checks passed
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