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

Fetching a costed row

Read a finished ledger row back from your admin plane, and handle the four ways the call can decline.

How-to guideLast updated

On this page

The receipt tells you what happened. The ledger row tells you what it cost. This page covers the call that gets it, and the four ways it can decline.

ledger.fetch is the only part of the SDK that opens a socket you did not already have. It is also the only part that touches a credential, which is why it is built the way it is.

Fetch one row

Pass a receipt or a bare request id. wait_s bounds how long the server may take to finish writing the row, which matters when you ask about a request that completed moments ago.

from tokentriage import ledger

row = ledger.fetch(rcpt, wait_s=3)          # or ledger.fetch("tt_vjuoh…")
print(row.cost.render())
print(row.trace["agent"], row.usage["total_tokens"])
$0.000027 (snapshot litellm@2026-07-14#179affb)
planner 120

Python’s ledger calls are synchronous; TypeScript’s return promises. That is an idiom choice per language rather than a capability difference, and it is the one shape difference you will feel most often.

The call is explicit. Nothing hydrates rows in the background, nothing polls, and wait_s is a bounded wait inside one logical fetch rather than a retry loop.

What a row carries

RowView projects the server’s record verbatim. No field is renamed, and nothing is derived.

Block Holds
cost the three-state cost
would_cost per-tier counterfactuals, same three states
trace trace id, step id, parent id, agent, step class
dims user, tenant, feature, and your tags
usage token counts and their source
decision mode, tier, upstream, rewrite, cache outcome
latency, http, request, outcome timing and transport detail
record the whole underlying record, for fields with no named accessor

Note that agent lives on trace, not on dims. Dimensions are the business labels; the agent is part of identity.

row.trace   {'trace_id': '925467c2…', 'step_id': 'c055085e…', 'parent_id': '',
             'agent': 'planner', 'step_class': 'user_turn', …}
row.dims    {'user': 'u-42', 'feature': 'chat'}
row.usage   {'input_tokens': 100, 'output_tokens': 20, …, 'source': 'provider'}

ledger.fetch_trace takes a trace id and returns the whole tree, with per-node and rollup figures. Those totals are server fields projected as they arrived; the SDK does not sum money across nodes, for the reason on cost in the SDK.

The credential protocol

The admin token comes from TOKENTRIAGE_ADMIN_TOKEN and from nowhere else. There is no keyword argument, on any function, in either language.

That is a containment decision. A credential with one source has one place to leak from, and the SDK keeps it out of logs, exception messages and reprs. The module that holds it is imported lazily, so import tokentriage alone opens nothing.

Four rules govern every admin call, including the write paths:

  1. Probe before auth. The first request carries no Authorization header. The token is attached only after a 401 or 403 whose body parses as the admin plane’s own typed error shape, so the far end has identified itself before the credential moves.
  2. Refuse a plane collision. If admin_url and base_url resolve to the same origin, the call fails before any socket opens.
  3. Follow no redirects. Any 3xx is an error, and the message does not echo the Location.
  4. Env-only sourcing, as above.
What probe-before-auth does not prove

Probing proves the peer on the first connection was admin-plane-shaped. It cannot prove the same of the second connection that carries the token, because two requests mean two name resolutions. A DNS rebind between them, or a front that answers one connection from the admin plane and the next from something else, defeats the inference.

That is inherent to any two-request protocol rather than an implementation gap, and it is stated here instead of being closed, because closing it would mean pinning a resolved address across both legs — reaching under the transport in the one module that has to stay small enough to audit.

What the protocol does guarantee holds regardless: the token is not sent on first contact, not sent to the data-plane origin, not followed to a redirect target, and not echoed into any message. The token path is two connections, with no retry ladder. If you need a stronger guarantee, terminate the admin plane on loopback or behind mTLS, where connection identity is not inferred from DNS at all.

When the call declines

Four typed failures, each naming what to do.

When the two planes collide

admin_url resolving to the same origin as base_url fails immediately, before any request:

PlaneCollision: admin_url resolves to the same origin as base_url (http://127.0.0.1:3915) -
refusing to send an admin request to the DATA plane, which would forward it (and any auth)
upstream (SDK-R-050). base_url=http://127.0.0.1:3915 admin_url=http://127.0.0.1:3915

Both settings are named in the message so you can see which one is wrong. Fix whichever is not pointing at its own plane.

When the admin plane wants a token

AdminAuthRequired: the admin plane requires authentication (dashboard.auth) - set
TOKENTRIAGE_ADMIN_TOKEN (there is deliberately no token kwarg)

Set the variable in your environment. Emission is unaffected by this — only ledger reads need the token — so instrumentation keeps working while you sort it out.

When the row is not there yet

A request that has not finished being written raises NotFound. Give wait_s a second or two; that is what it is for. A request id that never existed raises the same thing, since the ledger cannot tell the difference between “not yet” and “never”.

When something else goes wrong

Anything else is a LedgerError carrying the server’s message. URL-embedded credentials are scrubbed from these messages before they are raised, so a token in an admin_url does not reappear in your logs.

Checking the wiring first

check() runs the same containment transport against GET /tt/api/status and reports what it found, without probing the data plane. Run it once when you wire the ledger up rather than debugging a failed fetch:

  [ok  ] TT-SDK-016  admin plane reachable at http://127.0.0.1:7498 - mode=route, tiers=1,
                     upstreams=1, pricing_snapshot=present

Under a plane collision the probe is skipped rather than sent, so the token stays in the process. Install and configure has the full output.