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

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.

How-to guideLast updated

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-id once per run · x-tt-agent per role · x-tt-step-id per call · x-tt-parent-id when 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.