[DOCS] `total_cost_usd` is cumulative per session under `--output-format stream-json`, but per-call under `--output-format json` — docs describe only the per-call behavior

Status Open
Maintainer reply None cached
Activity 1 comment · opened Aug 2, 2026

Documentation Type

Incorrect/outdated documentation

Documentation Location

https://code.claude.com/docs/en/agent-sdk/cost-tracking and https://code.claude.com/docs/en/headless

Section/Topic

total_cost_usd scoping on the result message — --output-format json vs --output-format stream-json

Current Documentation

The cost-tracking guide states, twice, that a result reflects only its own call:

When the query() call completes, the SDK emits a result message with total_cost_usd and cumulative usage. [...] If you make multiple query() calls (for example, in a multi-turn session), each result only reflects the cost of that individual call.
The result message [...] includes total_cost_usd, the cumulative estimated cost across all steps in that call. [...] If you use sessions to make multiple query() calls, each result only reflects the cost of that individual call.

The headless guide describes the CLI equivalent as per-invocation:

With --output-format json, the response payload includes total_cost_usd and a per-model cost breakdown, so scripted callers can track spend per invocation.

Neither page documents what total_cost_usd is scoped to when multiple user turns are fed to a single process via --input-format stream-json --output-format stream-json.

What's Wrong or Missing?

The two CLI output modes scope total_cost_usd differently, and only one of them matches the documented behavior.

  • --output-format json (one process per turn, context via --resume): total_cost_usd is per-turn. Matches the docs.
  • --output-format stream-json (one warm process, many turns on stdin): total_cost_usd is cumulative for the session — every result event restates the running total. Not documented anywhere.

Same field name, same result event type, opposite meaning depending on a flag.

Measured. Five sequential context-dependent turns, Sonnet, MCP disabled. Per-event total_cost_usd in emission order, from one isolated pair:

--output-format json         [0.192788, 0.016876, 0.016874, 0.016877, 0.016933]
                             -> per-turn;  sum = 0.260348

--output-format stream-json  [0.192830, 0.209708, 0.226584, 0.243463, 0.260398]
                             -> cumulative; last = 0.260398

The two lines are the same underlying spend reported two different ways, and they pin each other down:

  • turn 1 agrees almost exactly (0.192788 vs 0.192830),
  • the stream-json final value (0.260398) equals the json sum (0.260348),
  • stream-json increments by a near-constant ~0.01688/turn, which is the json per-turn value.

Both arms did identical work in this pair — 31,221 vs 31,228 cache-creation tokens, 242,507 vs 242,535 cache-read tokens — so this is purely a reporting-scope difference, not a cost difference. The semantics split reproduced on every trial I ran (5 trials across two batches), and was classified from monotonicity rather than assumed.

Why it matters: a caller who follows the documented "each result only reflects the cost of that individual call" and sums total_cost_usd across result events in stream-json mode over-reports spend by roughly the turn count — ~4x over 5 turns, and it grows without bound in a long-lived session. That is the exact shape of an over-reporting cost bug, and the docs currently steer people into it. Anything driving Claude Code as a long-lived subprocess (agent harnesses, CI wrappers, coprocess/REPL setups) hits this, and it silently produces plausible-but-wrong numbers rather than an error.

Related but distinct: #45482 covers transcript-derived per-block usage via getSessionMessages(), not the CLI output-format difference.

Suggested Improvement

  1. In the headless guide, state the scope for both modes explicitly, e.g.:

> With --output-format json, total_cost_usd on the result payload is the cost of that invocation. With --output-format stream-json, each result event reports the cumulative cost of the session so far — take the value from the most recent result rather than summing across events.

  1. In the cost-tracking guide, scope the existing "each result only reflects the cost of that individual call" sentence to query() / --output-format json, and add the streaming-session case beside it.
  1. If the divergence is unintentional, the alternative fix is to make stream-json report per-turn and expose the running total as a separate field — but the docs should describe whichever is intended.

Impact

Medium - Makes feature difficult to understand

Additional Context

Reproduced with a small harness that runs both arms with a unique --append-system-prompt nonce per arm, so each creates its own prompt cache rather than reading one the other just paid for, with arm order alternated across trials. Worth noting for anyone else measuring this: without that isolation the two arms share a server-side prompt cache and whichever runs second looks ~68% cheaper, which is an artifact and not a real saving.

Detection that does not depend on which mode you are in:

seq = [e["total_cost_usd"] for e in result_events]
cumulative = all(b >= a for a, b in zip(seq, seq[1:])) and seq[-1] > seq[0]
total = seq[-1] if cumulative else sum(seq)

Version: Claude Code v2.1.220, macOS.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗