Two accounts, one field name
Meerkat reports token usage in two places with the same field names and two different denominators.
A session may span providers and models, so the cumulative account deliberately
carries no single provider/model and no cache detail counters: their
relationship to the input total is provider-specific.
Which calls emit a usage row
The event stream reports strictly fewer provider calls than the cumulative total charges. Every one of these calls is folded intorun_completed.usage, and only the first kind publishes a turn_completed row:
Consequently
sum(turn rows) <= run_completed.usage, usually strictly less.
Treat the difference as unattributed cost rather than as a reconciliation error.
Per-model attribution lives on the per-call event
turn_completed.usage.accounting is the single owner of attribution for that
call:
provider and model name the resolved model the request was actually lowered
to, not configuration intent, so a consumer reading only the durable event log
(.rkat/sessions/<id>/events.jsonl) can attribute the calls it does see without
joining against session metadata. presented_tokens is the provider-normalized
input presented to the model for that one call, and convention records how the
provider’s counters were normalized to produce it.
There is intentionally no second copy of the model or provider beside
accounting. Attribution has one owner.
accounting first appears on turn_completed in 0.8.22. Rows written by 0.8.21
and earlier carry a bare usage object with no attribution; recover the model
for those from session metadata.Worked example
One session, two runs. The first run makes three Anthropic calls (the first two request tools, the third closes the run); the second run makes one call.
What the stream publishes:
- The session total is the latest
run_completed.usage: 18920 tokens. The first run’s 14160 is the same account observed earlier, not a separate run’s cost. - What the turn rows attribute is 9120 presented input plus 150 output, so 9270 of the 18920 tokens. The other 9650 belong to calls that publish no turn row (2 intermediate tool-loop calls here). Within run 1 alone the unattributed input is 13720 - 4420 = 9300.
turn_rows_cover_one_call_while_the_run_total_is_session_cumulative in
meerkat-core/src/agent/usage_accounting_tests.rs, which drives the real agent
loop, and the CumulativeUsage arithmetic is pinned by
cumulative_usage_matches_documented_aggregation_example in
meerkat-core/src/types/tests.rs. If this page and those tests disagree,
make docs-check fails.
What not to sum
1. Do not sumrun_completed.usage. Each one is already the session total to
date. For the session above, adding the two observed run totals reports
14160 + 18920 = 33080 instead of 18920. This is the double-count the field
report hit. Take the latest value; never add.
2. Do not sum per-call input_tokens. Across the two turn rows above that
gives 120 + 200 = 320, against 9120 presented tokens, because the raw
Anthropic counter excludes cache-write and cache-read input. Sum
accounting.presented_tokens instead.
3. Do not compare total_tokens across the two accounts. On a per-call
usage, input_tokens + output_tokens uses the uncached denominator (210 for
call 3); the comparable per-call figure is
accounting.presented_tokens + output_tokens (4510). On the wire,
WireTurnUsage.total_tokens is already the normalized form, while
WireUsage.total_tokens built from a cumulative usage is the session total.
The two aggregations that are always safe:
- Session cost: the latest
run_completed.usage(equivalentlyRunResult.usage, which is the same value). Nothing to add. - Per-model breakdown of the observable calls: sum
turn_completed.usage.accounting.presented_tokensandturn_completed.usage.output_tokens, grouped byturn_completed.usage.accounting.model. Report the residual against the session total explicitly; do not present this breakdown as the full cost.
SDK surfaces
Python and TypeScript expose one
Usage type for both the per-call and the
cumulative account, so accounting is typed optional there and is always absent
on the cumulative value. Rust keeps the two apart as TurnUsage and
CumulativeUsage.
The session.usage / session.text style accessors on the Python and
TypeScript SDK session objects read RunResult, so session.usage is the
session-cumulative account, not the last call’s usage.
Known limits
- There is no per-run token account. If you need one, difference consecutive
run_completed.usagevalues for the same session. compaction_completed.summary_tokensis the output size of the compaction summary call and carries no provider/model attribution and no presented-input count, so compaction cost cannot be attributed from the event stream alone.- Intermediate tool-loop calls and the structured-output extraction call publish no usage-bearing event at all, so per-model attribution is inherently partial for tool-heavy or structured-output runs.
run_completed.usagecarries no per-model breakdown. For a session that switched models, group the per-call rows yourself; the cumulative total is model-agnostic on purpose.
