# 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.

Source: https://tokentriage.com/docs/sdks/cost

---
import Callout from '../../../components/Callout.astro';
import NextSteps from '../../../components/NextSteps.astro';
import Details from '../../../components/Details.astro';
import CodeTabs from '../../../components/CodeTabs.astro';

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](/docs/build/cost-figures); 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:

```text
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:

```text
cost: not disclosed (not a TokenTriage response)
```

<Callout type="key">
No public call in either SDK produces a dollar, a saving, or a verdict that the server did not
send. The packages ship no price table and contain no money arithmetic, and a build gate checks
for currency literals and rate tables in the source. A client-side estimate would be a number
your finance team could not reconcile against anything.
</Callout>

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.

```text
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)`.

<Callout type="warning">
The forbidden pattern in both languages is `cost.usd or 0`. It converts "we could not price this"
into "this was free", silently, on exactly the rows where you most need to know. If you must have
a number, branch on the state and decide what an unknown means for your report.
</Callout>

## Reaching the number safely

The two languages enforce this differently, and it is the clearest difference between them.

<CodeTabs>
  <Fragment slot="python">
  ```python
  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
  ```
  </Fragment>
  <Fragment slot="typescript">
  ```ts
  const 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));
  }
  ```
  </Fragment>
</CodeTabs>

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](/docs/sdks/parity#numbers-in-ledger-rows)
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`:

```python
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.

<Details summary="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.

</Details>

<NextSteps items={[
  { title: 'Fetching a costed row', href: '/docs/sdks/ledger', description: 'The call that produces these values, and what happens when it cannot.' },
  { title: 'Provenance and UNPRICED', href: '/docs/concepts/honesty-model#provenance--unpriced', description: 'Where UNPRICED comes from, and the vocabularies it must not be confused with.' },
  { title: 'How to read a cost figure', href: '/docs/build/cost-figures', description: 'The server side: catalogs, snapshots, and what makes a figure exact.' },
  { title: 'Python ↔ TypeScript parity', href: '/docs/sdks/parity', description: 'How the two bindings differ on money and on numbers generally.' }
]} />
