Put identity on the wire
Set the trace, step and agent headers so a run collapses into one costed tree. By hand, from a framework, or by wrapping an agent.
On this page
By the end of this page an agent run shows up as one tree with a dollar on every step, instead of a dozen unrelated rows.
Nothing needs enabling first. Identity headers are read whenever a client sends them, in every mode, with no configuration. The work is entirely on your side of the connection.
The rule, in four lines
x-tt-trace-idonce per run ·x-tt-agentper role ·x-tt-step-idper call ·x-tt-parent-idwhen one step spawns another.
Everything below is that rule expressed in a different client.
By hand
One request, so you can see the shape before you wire it into anything:
$ curl -s http://127.0.0.1:8787/v1/chat/completions \
-H 'content-type: application/json' \
-H 'x-tt-trace-id: run-7f3a91c2' \
-H 'x-tt-step-id: step-01' \
-H 'x-tt-agent: planner' \
-H 'x-tt-step-class: user_turn' \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Plan the summary."}]}'
A delegated call adds one header naming the step that spawned it:
$ curl -s http://127.0.0.1:8787/v1/chat/completions \
-H 'content-type: application/json' \
-H 'x-tt-trace-id: run-7f3a91c2' \
-H 'x-tt-step-id: step-02' \
-H 'x-tt-parent-id: step-01' \
-H 'x-tt-agent: researcher' \
-H 'x-tt-step-class: tool_loop' \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Fetch the revenue table."}]}'
Same trace id, new step id, and a parent edge. That is the whole protocol.
From an application
Any client where you control outbound headers works: point base_url at the proxy and attach
the headers per call.
The place to attach them differs by framework, and the pattern is the same in each. Mint the trace id where a run begins and carry it in whatever context object the framework already threads through. Mint a step id per model call. Set the agent name from the node, role or task you are executing.
| Framework | Where the headers go |
|---|---|
| LangChain / LangGraph | default_headers on the chat model client, or extra_headers per call; a RunnableConfig callback stamps a fresh step id per node |
| CrewAI | the LLM client’s default headers; agent name from the crew member’s role, parent id when a task delegates |
| OpenAI and Anthropic SDKs | the per-request extra_headers argument, or client-level defaults for the values that do not change |
| a hand-written loop | wherever you build the request; a small helper that takes agent and parent and returns a header map is usually enough |
Attribute cost per agent step is the full worked version of a hand-written loop, with the Python and the read-back in one place. The Python and TypeScript SDKs mint and propagate the same headers from ambient context so you do not thread them by hand.
By wrapping a CLI agent
For a command-line agent you do not control the HTTP client of, tokentriage wrap sets the
environment for you and executes it:
$ tokentriage wrap --agent researcher -- your-agent
tokentriage wrap: base=http://127.0.0.1:8787 trace=tt_72z5fkfij3tumtec4xsjthlvcu agent="researcher"
The child process inherits four things:
ANTHROPIC_BASE_URL=http://127.0.0.1:8787
OPENAI_BASE_URL=http://127.0.0.1:8787/v1
ANTHROPIC_CUSTOM_HEADERS=x-tt-trace-id: tt_72z5fkfij3tumtec4xsjthlvcu
x-tt-agent: researcher
ANTHROPIC_CUSTOM_HEADERS is newline-separated, which is why the agent header appears on its own
line. The trace id is minted fresh per invocation, so one wrap is one trace.
Two limits are worth knowing before you rely on it. The custom-header environment variable is
read by Anthropic-dialect SDKs only, so a child speaking the OpenAI dialect is routed and costed
but not grouped by trace. And because the headers are fixed for the whole session, wrap gives
you per-session identity rather than per-step: there is no x-tt-step-id to vary. For Claude
Code specifically that means per-session, per-model and per-tool attribution, and not per-subagent
attribution, because Claude Code exposes no sub-agent identity on the wire and TokenTriage does
not guess at one.
Check it worked
Send one request, then read the row back. The trace block is the proof:
$ tokentriage top --trace run-7f3a91c2
trace run-7f3a91c2 (5 steps)
STEP CLASS AGENT MODEL COST IN OUT CACHE-R
step-01 user_turn planner gpt-4o-mini $0.000442 768 512 256
step-02 tool_loop researcher gpt-4o-mini $0.000442 768 512 256
step-03 tool_loop writer gpt-4o-mini $0.000442 768 512 256
step-05 tool_loop reviewer gpt-4o-mini $0.000442 768 512 256
step-04 user_turn planner gpt-4o-mini $0.000442 768 512 256
TOTAL $0.002208 3840 2560 1280
That capture is from a local run against a mock upstream whose token counts are fixed, so the
dollars are arithmetic over those counts rather than a bill. If the command prints nothing, the
trace id in your header and the one you passed differ; if it prints a flat list with no
indentation, no request carried x-tt-parent-id.
What can go wrong
The headers never arrive. A service mesh or API gateway between your application and
TokenTriage is dropping unknown request headers. Add the x-tt-* prefix to its allowlist. You
can tell this apart from a client bug in one request: send it with curl directly at the proxy
and see whether trace.source changes.
Every call has the same step id. A step id captured once at client construction is a constant. Mint it per call, next to where you build the request.
The tree is flat. Parent ids are missing. A step with no x-tt-parent-id renders at the root
whether or not it logically had a parent, so a run where only the root is unparented is correct
and a run where everything is unparented is a bug in your instrumentation.
Turning it on did not fix last week. Identity is recorded as rows are written. Rows logged before you deployed the headers stay standalone, and nothing rebuilds them, because the information was never on the wire.
Naming ids so a collision cannot merge two runs
Ids are opaque and are compared for equality, so two runs that happen to choose the same string
become one trace. Anything with real entropy avoids that: a UUID, a request id from your own
system, or a short random suffix on a readable prefix (run-7f3a91c2).
Values are capped at 128 bytes and truncated at the boundary, so put the varying part near the front. A 200-byte id whose entropy is at the end truncates to a constant.
Agent names are the opposite case: keep them stable and few. They are how a rollup groups, so
planner and Planner split one agent into two rows.