Usage and cost¶
What a run consumed and what that came to are two different questions with two different
answers, and the kit keeps them apart. Usage is counted — by the vendor where it reports,
by the kit where it does not. Cost is decided, by a price list with a date on it. Folding
them into one number loses both: the cache saving disappears into a total, and a hundred
thousand fractions of a cent drift away from the invoice.
Usage — what was consumed¶
Usage(
input_tokens=1000, # everything sent, cache reads included
cached_tokens=400, # of that, what the vendor served from its own cache
cache_write_tokens=200, # what it charged to put into that cache
output_tokens=500, # generated and shown
reasoning_tokens=300, # hidden reasoning, beside the answer rather than inside it
image_units=2, # images, tiles or audio seconds, priced per unit
source=CountSource.PROVIDER,
)
Two containment rules, and they are not the same rule:
cached_tokensis part ofinput_tokens, because those tokens were sent. Pricing billsinput_tokens - cached_tokensat the fresh rate and the remainder at the cache rate.reasoning_tokensis besideoutput_tokens, not inside it. OpenAI reports reasoning within its completion total, so the adapter subtracts it; Gemini reports the two apart and the adapter leaves them apart. One workload therefore reads the same way whoever answered it, which is the point — a field that lands inextrasunder one vendor and in a column under another cannot be budgeted against.
extras holds usage a vendor reports that the kit does not model. Nothing in the kit reads
it; it is evidence, not input.
Who counted¶
source is PROVIDER, TOKENISER or HEURISTIC, and usage.estimated is true for the
last two. An endpoint that reports nothing is counted with the model's own tokeniser and
marked TOKENISER; a prompt the kit never got a count for at all is characters over a
constant and marked HEURISTIC. Adding two usages keeps the weaker of the two sources: a
total is only as trustworthy as its worst part, and a budget enforced against a guess
presented as a count is not enforcement.
Failed attempts are on the ledger¶
A vendor that read a prompt and then rate-limited still charged for reading it. Every
ATTEMPT_FAILED event carries a HEURISTIC usage for what that attempt burned, and the
run's total includes it. A run that never got an answer still says what it spent.
Cost — what that came to¶
Every component is a Decimal. Vendors quote in millionths of a dollar per token and a run
makes enough calls for binary floating point to disagree with the invoice. Components stay
separate and unrounded; quantised() rounds for presentation, which is the only place to
round.
confidence says how much of the number is known:
COUNTED |
Vendor-reported usage at a price the list knows. |
ESTIMATED |
A known price applied to counts the kit worked out. |
UNKNOWN |
No price for this model on this day. |
UNKNOWN has zero components because there is nothing to put in them, not because the call
was free — Cost.unknown() is never a silent zero. Totalling keeps the weakest confidence,
and totalling two currencies raises rather than producing a number true in neither.
Prices¶
A price list is not a constant, so a PriceCard carries the day it took effect, the request
shape it answers for, and the rate:
[[cards]]
ref = "anthropic:claude-sonnet-4-5"
effective_from = 2026-03-01
rate = { input_per_mtok = "2.40", output_per_mtok = "12.00", cache_read_per_mtok = "0.24" }
Point ADK_PRICE_LIST at the file, or pass the path to price_list(). Nothing is
discovered by convention: a deployment billing against a file nobody named is one where the
answer to "what is this costing" lives on somebody's laptop. An unreadable or wrong-shaped
file is a ConfigurationError rather than a quiet fall back to list price — that gets found
on the invoice.
The kit ships the catalogue snapshot's rates, effective from the snapshot day and not before. A deployment costing older runs supplies its own dated cards; back-dating the shipped ones would be a fiction about what was charged.
Selection is by date and request shape, never by position. The narrowest card wins — the
batch tier where a batch call asked for it, then the highest long-context threshold the
prompt clears — and among cards of one shape, the latest already in force. A price change is
a new card, never an edit: overwriting one rewrites what last week's runs cost. Two cards
for one shape on one day is a ConfigurationError, because a deployment cannot bill both.
overridden_by() lays a negotiated list over the shipped one, replacing every shipped card
for the models it names. A negotiated rate that only won until the vendor's next list price
landed would be no agreement at all.
Reading a run¶
run = await runner.run(agent, "…", tenant="acme")
print(run.usage.input_tokens, run.usage.output_tokens, run.usage.source)
print(run.usage.cost.total if run.usage.cost else "unpriced")
The run total is summed as the run goes, never recomputed by the caller. cost_of() prices
a usage directly where something outside a run needs the number:
A model no card covers on that day warns UnknownPricing and returns Cost.unknown(). A
deployment that requires every call to be priced turns that warning into an error with a
filter.
What this does not do¶
Ceilings are not enforced here — that is the budget policy's job, and this module only supplies the number it enforces against. Chargeback attribution is not here either.
Exercised by examples/cost.py and tests/test_cost.py.