# Read your spend from the terminal

top and report over the decision log: five groupings, a per-trace cost tree, shadow savings, and the three columns that mislead.

Source: https://tokentriage.com/docs/build/cost-cli

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

Two commands read your decision log and print a table. No database, no index, no configuration
— they open the JSONL file and aggregate it, so they work on a fresh install, over a log copied
off a server, and when everything else is off.

Every transcript below came from running `tokentriage demo`, whose synthetic ledger is driven
through the actual pricing pipeline against mock upstreams. The dollars are arithmetic over the
mock's token counts, not anyone's bill, and the shape is what your own log produces.

## `top` — where the money went

```console
$ tokentriage top --by model
MODEL                        REQS  INPUT     OUTPUT  CACHE-R  CACHE-W  REASON  CACHE-SAV  COST        P50        P90        P99
acme/private-model-1         35    97524     42521   7014     0        0       6.7%       -           -          -          -
anthropic/claude-haiku-4-5   57    67144     31370   9608     0        0       12.5%      $0.224955   $0.003544  $0.009641  $0.012402
anthropic/claude-opus-4-8    61    12293266  43051   11830    0        0       0.1%       $62.548520  $0.046033  $0.100000  $10.000000
anthropic/claude-sonnet-4-5  73    179383    74868   17746    0        0       9.0%       $1.666493   $0.022888  $0.034886  $0.042655
gpt-4o-mini                  12    11328     4200    3072     0        0       21.3%      $0.003264   $0.000272  $0.000272  $0.000272
openai/gpt-4o                70    197323    65086   17454    0        0       8.1%       $1.165985   $0.016161  $0.024690  $0.031442
openai/gpt-4o-mini           48    105755    41701   9539     0        0       8.3%       $0.041599   $0.000994  $0.001615  $0.001854
openai/o3                    56    135711    55296   14442    0        0       9.6%       $0.721011   $0.013329  $0.018889  $0.025199
TOTAL                        412   13087434  358093  90705    0        0       0.7%       $66.371826
COST excludes 35 unpriced rows of 412 (no price resolved; the $0 is a missing price, not $0 of spend)
```

Two rows in that table are worth staring at before anything else.

`acme/private-model-1` shows thirty-five requests, genuine token counts, and `-` in every dollar
column. Not one of its rows could be priced, so there is no figure to print and `$0.000000`
would be a lie. The line under the table counts them.

`anthropic/claude-opus-4-8` shows a P99 of `$10.000000` against a P50 of `$0.046033`. One
request in that group cost more than the other sixty put together, which is the shape of a
runaway job and exactly what percentiles are for.

### The groupings

`--by` takes five values, and each answers a different question.

| `--by` | Groups on | Ask it when |
|---|---|---|
| `model` | the model on the request | which model is the bill |
| `agent` | the `x-tt-agent` header | which agent or sub-agent is the bill |
| `step-class` | `user_turn`, `tool_loop`, `provider_state` | whether the tool loop is eating the budget |
| `trace` | the `x-tt-trace-id` header | which conversation was expensive |
| `tool` | MCP tool invocations | how much your tools cost you in tokens |

Rows with no value for the grouping dimension land in a labelled bucket named `(none)`,
`(standalone)` or `(unknown)`, rather than disappearing.

```console
$ tokentriage top --by step-class
STEP-CLASS  REQS  INPUT     OUTPUT  CACHE-R  CACHE-W  REASON  CACHE-SAV  COST        P50        P90        P99
tool_loop   70    177499    72880   16625    0        0       8.6%       $1.060597   $0.010622  $0.033863  $0.065335
user_turn   342   12909935  285213  74080    0        0       0.6%       $65.311229  $0.014169  $0.043115  $10.000000
TOTAL       412   13087434  358093  90705    0        0       0.7%       $66.371826
COST excludes 35 unpriced rows of 412 (no price resolved; the $0 is a missing price, not $0 of spend)
```

### Windowing

`--since` takes a duration or an absolute timestamp. Durations accept Go's units plus day and
week shorthands, so `24h`, `90m`, `30d` and `2w` all work, as does any RFC 3339 timestamp.

```console
$ tokentriage top --by model --since 24h
MODEL                        REQS  INPUT  OUTPUT  CACHE-R  CACHE-W  REASON  CACHE-SAV  COST       P50        P90        P99
acme/private-model-1         1     4097   614     67       0        0       1.6%       -          -          -          -
anthropic/claude-haiku-4-5   1     0      0       0        0        0       0.0%       $0.000000  $0.000000  $0.000000  $0.000000
anthropic/claude-opus-4-8    2     5079   1608    886      0        0       14.9%      $0.066038  $0.028682  $0.037356  $0.037356
anthropic/claude-sonnet-4-5  1     760    1089    205      0        0       21.2%      $0.018676  $0.018676  $0.018676  $0.018676
gpt-4o-mini                  12    11328  4200    3072     0        0       21.3%      $0.003264  $0.000272  $0.000272  $0.000272
openai/gpt-4o                2     7128   2243    529      0        0       6.9%       $0.040911  $0.017421  $0.023490  $0.023490
openai/gpt-4o-mini           1     850    1823    212      0        0       20.0%      $0.001237  $0.001237  $0.001237  $0.001237
openai/o3                    1     3067   1362    317      0        0       9.4%       $0.017188  $0.017188  $0.017188  $0.017188
TOTAL                        21    32309  12939   5288     0        0       14.1%      $0.147315
COST excludes 1 unpriced row of 21 (no price resolved; the $0 is a missing price, not $0 of spend)
```

A row whose timestamp cannot be parsed is dropped from a windowed view, because it cannot be
placed on the timeline. Unwindowed, it still counts.

### One conversation, step by step

`--trace` renders a single trace as a tree, linked by parent step id, with per-step cost:

```console
$ tokentriage top --trace tt_00000000000000000000000001
trace tt_00000000000000000000000001  (1 steps)
STEP                           CLASS      AGENT         MODEL                      COST       IN     OUT  CACHE-R
tt_00000000000000000000000001  user_turn  batch-runner  anthropic/claude-opus-4-8  $0.100000  20000  0    0
TOTAL                                                                              $0.100000  20000  0    0
```

The demo's synthetic traces are one step deep. A client that sets `x-tt-parent-id` on delegated
calls gets an indented tree, and an unpriced step there carries the same `-` marker and stays
out of the total.

## The three columns that mislead

Each of these is doing exactly what it says, and each is routinely read as something else.

<Callout type="warning">
**`P50` / `P90` / `P99` are dollars, not milliseconds.** They are per-request *cost*
percentiles, computed over the priced rows of that group by nearest rank. An unpriced row is
excluded from them for the same reason it is excluded from the total: its zero would drag them
down. For latency percentiles, use the dashboard or your metrics stack.
</Callout>

**`CACHE-SAV` is a token ratio, not a dollar.** It is the cache-read share of input-side tokens
(`100 × CACHE-R / (INPUT + CACHE-R)`), and you can check it against the row: the `gpt-4o-mini`
line above shows 3,072 cache reads against 11,328 fresh input, and 3072 ÷ 14400 is 21.3%. It is
a ratio rather than money because this view cannot see the price table, so it reports what it
measured instead of a figure it would have to invent. Cache savings *in dollars* are a separate
and more careful calculation, in
[two kinds of cache savings](/docs/concepts/cache-accounting).

**`COST` under `--by tool` is not the tokens the tool cost you.** It is a flat per-call price for
the tool itself, and this build ships no way to configure one, so it prints `$0.000000` for
every tool on every install.

```console
$ tokentriage top --by tool
# tool_result tokens are an INPUT cost of the NEXT call; IN-TOK-ATTR is an ESTIMATE (char/4).
TOOL        SERVER     REQS  CALLS  RESULT-BYTES  IN-TOK-ATTR  COST
code_exec   tools-mcp  35    35     108565        19697        $0.000000
web_search  tools-mcp  70    108    284598        50213        $0.000000
TOTAL                  105   143    393163        69910        $0.000000
```

The column that carries the signal is `IN-TOK-ATTR`: the input tokens a tool's results cost
the *next* call, because a tool result is fed back into the following request. It is an estimate
(characters over four) and the header says so. For a tool loop's actual dollars, use
`--by step-class` and read the `tool_loop` row.

Tool accounting is per turn. Agentic clients resend the whole conversation each turn, so each
invocation is counted once, at the turn that first feeds its result back. An N-turn loop reports
N calls rather than roughly N²/2.

## `report` — what routing saved, or would save

`report` answers a different question: for each row, what would this have cost at the other
tiers, and what did routing actually pick?

```console
$ tokentriage report
=== TokenTriage shadow-savings report ===
verdict: routing sits BELOW the observed-model frontier (Δ -0.0074) — served-model labels (quality source 'oracle') score only the tier that served each row, so routing cannot be shown to beat a blind model mix. Enable shadow duplication (shadow.duplicate) for a counterfactual routing-skill verdict; use calibrate to sweep thresholds.

routed rows:        35 of 412 (rows with a routing decision + would_usd)
not-routing spend:  $2.306799  (served on the top tier)
routed spend:       $0.720880  (under the routing decision)
savings:            $1.585919  (68.7% vs not routing; 35 of 35 rows routed cheaper)

BY STEP-CLASS
STEP-CLASS  REQS  NO-ROUTE   ROUTED     SAVINGS    SAVINGS%
user_turn   35    $2.306799  $0.720880  $1.585919  68.7%
TOTAL       35    $2.306799  $0.720880  $1.585919  68.7%

BY RULE
RULE             REQS  NO-ROUTE   ROUTED     SAVINGS    SAVINGS%
default          13    $0.862162  $0.269428  $0.592734  68.7%
override         7     $0.434116  $0.135661  $0.298455  68.8%
pin              8     $0.513757  $0.160551  $0.353206  68.7%
simple-to-cheap  7     $0.496764  $0.155240  $0.341524  68.7%
TOTAL            35    $2.306799  $0.720880  $1.585919  68.7%
```

Two things about that output are the point of the command.

**The denominator is disclosed.** `35 of 412`. Only rows carrying a routing decision *and* a
usable counterfactual license a savings claim. Rows on an endpoint that is not shadow-eligible,
or produced by a cross-dialect translation, are left out of the arithmetic and counted rather
than quietly folded in.

**The baseline is named, and it is generous.** `not-routing spend` prices every row at the most
expensive tier it could have used, which assumes clients would otherwise have asked for the top
tier. That is the `top-tier-would` baseline, and it is the right comparison when your clients
really do default to the best model and an overstatement when they do not.

The verdict line at the top is a *routing* verdict, not a savings verdict: a separate closed
vocabulary answering whether routing beat a blind model mix. The two are explained side by side
in [two verdict vocabularies](/docs/concepts/honesty-model#two-verdict-vocabularies--dont-conflate-them).

## What can go wrong

**`report` says `0 of N` rows.** Nothing in the log carries a counterfactual. That is the
normal state in `ledger` mode with no routing rules, because there was no other tier to price.

**A `warning: skipped N torn/corrupt line(s)`.** The log is appended to by a live process, so a
partially written final line is expected and skipped. A large count on an idle process is worth
investigating.

**The numbers disagree with the dashboard.** They should not, and if they do, check you are
reading the same log: `--log` overrides the path, and without it both commands resolve the
configured default.

<Details summary="Where the log lives and how to point at another one">

Both commands take `--log`, which accepts a file or a directory. Without it they resolve the
configured decision-log path, defaulting to `~/.tokentriage/decisions.jsonl`.

That makes an incident workflow simple: copy the log off the affected host and run `top` against
it locally. Nothing else needs to come with it. The file is self-contained, and the costs in it
were computed when the rows were written.

</Details>

<NextSteps items={[
  {title: "How to read a cost figure", href: "/docs/build/cost-figures", description: "What the exclusion line, the EST flag and a savings baseline are telling you."},
  {title: "How a dollar gets a name", href: "/docs/build/cost-attribution", description: "Group by your own tenants and features rather than by model."},
  {title: "What the rollups make cheap", href: "/docs/build/cost-rollups", description: "The indexed path these commands do not use, and the month-scale questions it exists for."},
  {title: "CLI reference", href: "/docs/reference/cli", description: "Every command and flag, including the ones this page did not need."},
]} />
