Skip to main content
This page is the canonical contract for reading token usage off a Meerkat event stream: which number is per-call, which number is already a total, which calls the event stream does not report at all, how to attribute a call to the model that produced it, and what you must not sum.

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.
run_completed.usage is session-cumulative, not run-scoped. It is Session::total_usage(), which is persisted with the session and restored on resume, so on the second and later runs of a session it already contains the earlier runs’ calls. There is no per-run token account on the event stream.

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 into run_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:
Read that as follows.
  • 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.
These numbers are pinned by 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

Three specific mistakes, all of which produce a wrong number silently.
1. Do not sum run_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 (equivalently RunResult.usage, which is the same value). Nothing to add.
  • Per-model breakdown of the observable calls: sum turn_completed.usage.accounting.presented_tokens and turn_completed.usage.output_tokens, grouped by turn_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.usage values for the same session.
  • compaction_completed.summary_tokens is 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.usage carries no per-model breakdown. For a session that switched models, group the per-call rows yourself; the cumulative total is model-agnostic on purpose.