Cost in the SDK
Why the receipt carries no dollar, the three states a cost can be in, and why an unknown price is not zero.
On this page
The first surprising thing about cost in this SDK is that you cannot get one from a response.
Receipt.cost is empty on every request, on every deployment, in contract version 1.
The second is that when you do fetch a cost, it might not be a number, and the SDK will not turn it into one.
Both follow from the same rule, and this page is about why that rule produces a better answer than a figure would. Where the money comes from on the server side is how to read a cost figure; this page is the client’s view of it.
Why the receipt has no dollar
Pricing needs the response’s token counts, and those are known only once the last byte of the body has been read. On a streaming response that is after your code has already started processing tokens. The proxy therefore prices the request after the response has left, and writes the figure to the ledger row.
So there is no cost header to read. Not one the SDK declines to parse — one that does not exist.
Given that, an SDK has three options. It can estimate client-side from a bundled price table. It can report zero. Or it can tell you where the number lives. This one does the third:
inline cost: cost: not disclosed inline — ledger row tt_vjuohnryzpc557cc7vrxnkjv7m
That string is what cost_display renders when there is no cost and there is a request id. It
names the row that will hold the answer. When the response was not from TokenTriage at all, it
says something different, because the reason is different:
cost: not disclosed (not a TokenTriage response)
The field is reserved rather than removed. If a future server discloses cost inline, or a ledger fetch fills it in, the field populates with server bytes and no type changes shape.
The three states
Ask the ledger for a cost and you get a closed union with exactly three states. There is no fourth, no default, and no zero constructor.
| State | Means | Renders as |
|---|---|---|
| absent | nothing disclosed a cost | cost: not disclosed inline — ledger row tt_… |
UNPRICED |
the request happened; its price is not known | UNPRICED (model_unknown) |
PRICED |
the server computed a figure | $0.000027 (snapshot litellm@2026-07-14#179affb) |
UNPRICED is the state that matters, because it is the one every other tool collapses. A model
missing from the price catalog produced a genuine request that genuinely cost money, and nobody
knows how much. Reporting $0.00 there is not a rounding choice; it says the request was free.
row.cost Cost(UNPRICED, reason='model_unknown')
row.cost.render() UNPRICED (model_unknown)
The reason travels with the state, so you can tell “not in the catalog” from the other causes.
When no reason is given, the render is a bare UNPRICED rather than UNPRICED (None).
Reaching the number safely
The two languages enforce this differently, and it is the clearest difference between them.
row = tt.ledger.fetch(rcpt, wait_s=3)
if row.cost and row.cost.state == "priced":
print(row.cost.usd) # a Decimal
else:
print(row.cost.render()) # UNPRICED (reason), or the absent renderconst row = await tt.ledger.fetch(rcpt, { waitS: 3 });
if (row.cost?.state === "priced") {
console.log(row.cost.usd); // a decimal string; a compile error before narrowing
} else {
console.log(tt.renderCost(row.cost));
}TypeScript models Cost as a discriminated union, so cost.usd does not compile until you have
narrowed on the state. Python keeps one class whose off-state fields are present and None,
because making them raise would add a new runtime failure mode inside the host application to buy
a guarantee Python’s type system cannot enforce statically anyway. The Python invariant is that
usd is None when the state is not priced, and None is not a zero.
Money keeps its digits in both. Python decodes with Decimal, TypeScript preserves the source
literal as a string, so neither ever round-trips a price through a float. Those are different
representations of the same guarantee, and parity
covers what else that affects.
Estimates say so, loudly
A PRICED cost carries two pieces of provenance: whether it was estimated, and which pricing
snapshot produced it. An estimate renders differently from an exact figure, and the difference
survives everything:
| State | Render |
|---|---|
| exact, with snapshot | $0.000675 (snapshot litellm@2026-07-01#a1b2c3d) |
| estimated, with snapshot | ~$0.000675 (ESTIMATED; snapshot litellm@2026-07-01#a1b2c3d) |
| exact, no snapshot | $0.000675 |
| estimated, no snapshot | ~$0.000675 (ESTIMATED) |
Losing the snapshot removes the provenance and nothing else. The ~ and the ESTIMATED marker
stay, because they describe what the number is rather than where the price came from. A figure
that quietly stopped looking like an estimate would be the one dangerous simplification here.
Each of those renders is pinned by a conformance vector, which is why both languages print the same bytes. 12 of the 70 shared vectors cover money rendering.
Counterfactuals
When the proxy runs a shadow comparison, the row carries what another tier would have cost.
That reaches you as would_cost, a mapping from tier name to the same three-state Cost:
row.would_cost # {'fast': Cost(PRICED, …), 'deep': Cost(UNPRICED, reason=…)}
The union is closed here for a specific reason. If unpriced counterfactuals were missing
keys, the natural client idiom would_cost.get(tier, 0) would turn every unknown into a free
alternative, and the comparison would always favour the tier nobody could price. An unpriced
counterfactual is an UNPRICED value in the map instead.
A priced counterfactual inherits the row’s estimated flag and pricing snapshot, since it
describes the same usage against the same catalog. It never renders as a bare exact dollar.
Why the SDK will not sum these for you
Row and trace views project server fields verbatim. There is no client-side arithmetic, including the obvious kind: the SDK will not add up money across the steps of a trace.
The reason is that a sum is only meaningful if every term is known. One UNPRICED node in a trace
makes the total unknowable, and every summing implementation has to decide what to do about it.
Skipping it silently produces a total that looks complete and is short by an unstated amount.
So trace totals are server fields, computed where the disclosure of what was excluded can travel
with them, and the SDK projects them as-is. If you need your own total, you have would_cost and
per-row states to work from, and the exclusions are yours to state.