Search

↑↓ to move↵ to openesc to closetop 6 documentation results

Book a demoTry TokenTriage

In development · Explore the product preview. Join the launch list or book a walkthrough.

Browse docs

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.

ExplanationLast updated

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 render

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.