ADR 0109 — A spend figure never claims a provenance it does not have
Context
AgentUsage has carried provenance on every field since it existed:
provider-reported | computed | estimated | unavailable. The emission did not
use it honestly, and three separate figures were wrong as a result.
A successful multi-step run reported one step’s money. The step loop assigned
rather than accumulated, and the finish branch grafted the surviving cost
onto the SDK’s token aggregate — which has no cost of its own. A three-step run
costing $0.50, $1.00 and $1.50 reported $1.50, beside all 6 000 of its
input tokens, labelled provider-reported. Half the money, on the ordinary
path, with no abort involved — and the shortfall grows with the number of steps,
though not by any fixed ratio: it is whatever every step but the last cost, and
the last step is often the expensive one.
A run that ended before the provider’s finish reported nothing. The usage
key was absent — not zero — so “never reached the provider” and “burned a minute
of the most expensive model available” were indistinguishable to a reader.
A run terminated by another actor reported nothing either. Both event emits
sat behind committedByCaller, so an executor that lost the terminal CAS
published no record of what it had spent.
Decision
The runtime reports what it observed, names what it did not, and never lets a number it derived wear the provider’s word.
Sums are computed. A total this code produced is computed however
provider-reported its parts were. The distinction is not pedantry:
provenance === 'provider-reported' is the obvious predicate for “a figure I can
bill against unchanged”, and a sum is not that. A single step passes through
untouched, so a one-step run still reports the provider’s number with the
provider’s word.
A run total is computed; a step figure is provider-reported. That line
is where the two labels divide, and it does not move by field. The AI SDK’s
totalUsage is a sum the SDK performed over per-step provider figures, not a
number any provider reported for the run — which loop did the adding is not a
difference a caller filtering for a billable figure cares about. Keeping the
provider’s word on it made one run report provider-reported on success and
computed on abort for the same arithmetic.
A token floor is computed; a cost floor does not exist. A token total
missing a step is still a useful diagnostic, so it is reported as a sum. A cost
missing a step is unavailable — money is what people bill against, and “at
least $1.00” reported as $1.00 is the defect this ADR is about, one level down.
It also has to stay unknown: a floor that forgot it was one recovered into a
confident total as soon as the next step reported.
unavailable is a value, not an omission. A terminal event always carries
usage. A run that reported nothing carries every field unavailable. Silence
is not a smaller version of zero — they are opposite claims, and only one of them
is free.
A field no step reported stays unavailable rather than becoming zero, and a
sum containing an unavailable component is a floor. Treating an unknown as zero
is how a sum quietly becomes a lie.
Two currencies do not add. The core records a currency; it never converts
one. Adding a USD cost to a EUR cost by keeping the first label would report a
number that is quietly not the total, so the sum reports unavailable instead.
The two event channels are gated for their own readers. The delivery
terminal event stays behind committedByCaller: it carries the assistant
message to the application’s transport, and publishing it for a run someone else
committed would deliver the turn twice. The operator run-terminal event is not
gated: a losing executor still ran and still spent. Delivering a turn twice is a
user’s problem; omitting a run’s cost is an operator’s, and neither channel
should pay for the other’s constraint.
Consequences
An existing reader filtering on provenance === 'provider-reported' stops
matching multi-step totals. That is the point — it was matching a wrong number —
and it is a ### ⚠️ Breaking changes entry with no compile error behind it,
which is exactly the shape AGENTS.md means by “never break silently”.
The core still stores no spend. AgentUsage reaches consumers on events and in
AgentRuntimeResult.metrics, and nothing is written to the durable record; where
a figure lives afterwards remains the application’s (→ ADR 0002, no billing in
the core). Two known gaps stay open and are tracked rather than closed here: the
spend figure has no durable home and its channels may drop it under load, and a
cost learned after a run closed has nowhere to attach.
Alternatives considered
Add a provenance member for “at least this much”. The enum answers how did
you get this number; completeness is a second axis, and one enum answering two
questions breaks every exhaustive switch in both directions — a reader handling
'computed' would silently lose the floor case. Completeness belongs beside the
figure, not inside its derivation label.
Report peak input tokens on aborted paths — the context size rather than the
billed sum. It would make one field mean cumulative billing on one path and peak
context on another, distinguished only by terminalReason. Peak stays derivable
from step-finished, where each step already carries its own input count.
Leave the omission and document it. A reader cannot act on a distinction the data does not carry, and every consumer would reinvent the same placeholder.