Skip to content

V2: align Anthropic streaming usage aggregation with upstream SDKs #40713

Closed

Description

@opencode-agent

Problem

The native Anthropic Messages parser currently normalizes usage independently on message_start and message_delta, then merges normalized Usage values. Anthropic usage is split across those events, and newer responses can include usage.iterations for compaction, message, advisor, and fallback inference.

This makes correct aggregation harder than it needs to be and risks losing or undercounting partial usage details. Vercel AI SDK instead accumulates raw Anthropic usage across stream events and normalizes once when message_stop arrives.

Follow-up scope

  • Accumulate typed raw Anthropic usage in parser state across message_start and message_delta.
  • Normalize once at message_stop rather than merging partially normalized Usage objects.
  • Match Anthropic and AI SDK nullability for streamed usage fields.
  • Define and test usage.iterations accounting:
    • include executor compaction and message work
    • keep advisor_message separately attributable
    • avoid double-counting fallback responses
    • handle cache-read and cache-creation counters consistently
  • Preserve the complete provider payload in providerMetadata.anthropic.
  • Keep normalized Usage invariants intact when only partial counters are available.

Acceptance criteria

  • Fixtures cover split start/delta usage, compaction, advisor, and fallback cases.
  • Normalized totals match the current Anthropic/AI SDK semantics.
  • No provider metadata has to be reparsed to reconstruct normalized usage.

Activity

  1. Zaczero commented on Aug 18, 2026

    @Zaczero

    The Anthropic merge-normalized-Usage risk named here already has a worse sibling on the OpenAI Chat path: usage is replaced, not merged.

    const usage = mapUsage(event.usage) ?? state.usage

    The last chunk that carries any usage object wins outright. A stream that emits prompt_tokens_details on one chunk and bare totals on the next drops the cache read/write. Every OpenAI-compatible provider reuses this protocol, so litellm / DeepSeek-compat / GLM-compat all have the same exposure.

    Anthropic is the better of the two — it folds per-field:

    const mergeUsage = (left: Usage | undefined, right: Usage | undefined) => {
    if (!left) return right
    if (!right) return left
    const nonCachedInputTokens = right.nonCachedInputTokens ?? left.nonCachedInputTokens
    const cacheReadInputTokens = right.cacheReadInputTokens ?? left.cacheReadInputTokens
    const cacheWriteInputTokens = right.cacheWriteInputTokens ?? left.cacheWriteInputTokens
    const inputTokens = ProviderShared.sumTokens(nonCachedInputTokens, cacheReadInputTokens, cacheWriteInputTokens)
    const outputTokens = right.outputTokens ?? left.outputTokens

    But it still merges already-normalized Usage, which is exactly the gap this issue describes. Accumulating the raw events and normalizing once at message_stop / the final Chat chunk fixes both parsers the same way.

    mapUsage also stashes providerMetadata: { openai: usage }. That object is not on the persisted message (105k assistant rows, none carry the key), so a later replay cannot recover the dropped details. The fold has to happen while the stream is still in hand. #34022 is the “cache read is zero” report this replace would produce on a multi-usage stream.

  2. neriousy commented on Oct 21, 2026

    @neriousy
    Member

    Partly done: #46431 added typed AnthropicUsage with iterations. mapUsage sums the iterations and takes context tokens from the last one. The parser still merges usage normalized separately at message_start and message_delta, advisor usage isn't counted separately, and nothing de-duplicates fallback responses. Closing this tracker.

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions