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

What identity is on the wire

Attribution comes from headers your code sets. What each identity header means, and what happens to it before the request is forwarded.

ExplanationLast updated

On this page

An agent run is a dozen model calls that belong together. Your gateway sees a dozen unrelated HTTP requests. Something has to say which ones are the same run, which step spawned which, and which sub-agent made each call, and that something is a set of request headers your application sets.

This page is about what those headers mean, why none of them is required, and what happens to them between your client and your provider. If you are here to write the code, put identity on the wire is the how-to.

The six headers

Two of them build the tree, two of them label it, and two change how the request is handled.

Header What it says Shape
x-tt-trace-id which run this call belongs to opaque, capped at 128 bytes
x-tt-step-id which step within the run opaque, capped at 128 bytes
x-tt-parent-id which step spawned this one the parent’s x-tt-step-id
x-tt-agent which logical agent made the call free text, capped at 128 bytes
x-tt-step-class what kind of step this is user_turn, tool_loop or provider_state
x-tt-tier serve this from a named tier a tier name from your config

The values are opaque. TokenTriage does not parse a trace id, require a format, or check that a parent exists; it records what you sent and groups on equality. A value longer than 128 bytes is truncated at the byte boundary rather than being rejected, so an over-long id costs you a collision risk, not a failed request.

Two of the six behave differently from the rest. x-tt-step-class is filled in with a guess when you omit it, and the guess is marked as one. x-tt-tier is an enforcement input rather than an identity field: it forces a tier in route mode, it is recorded in every mode, and its header name is configurable through routing.overrides.tier_header, which defaults to x-tt-tier.

Every one of them is optional

A request with no identity headers at all is priced, logged, counted in the metrics, and exported. It lands as a standalone row with an empty trace block, and it is aggregable by model, provider, tier and credential like any other. What it cannot do is join a tree.

The row says which case it was, in trace.source:

trace.source Means
x-tt at least one of trace-id, step-id, parent-id or agent arrived
traceparent none of those arrived, but a W3C traceparent did
none the request carried no trace identity at all

That list is worth reading closely, because x-tt-step-class and x-tt-tier are not in it. A request carrying only a step class records trace.source: none: the class was read and stored, and it did not give the request an identity.

Here is the trace block of a row from a request that sent no headers, taken from a local run against a mock upstream. It is still costed; it has nothing to join to.

"trace": {
  "trace_id": "", "step_id": "", "parent_id": "", "agent": "",
  "step_class": "user_turn", "step_class_source": "derived",
  "w3c": "", "source": "none"
}

Nothing is inferred

TokenTriage builds a trace from explicit identity and from nothing else. It does not stitch calls together by arrival time, by a shared prompt prefix, by source IP, or by conversation heuristics. It does not adopt an inbound traceparent as the trace id.

The alternative is tempting and it is what makes this worth a paragraph. A gateway that guessed would show a fuller tree on a client that sets no headers at all, which is most clients on day one. It would also be wrong in ways nobody can see. A trace that silently merged two users’ sessions, or split one agent run into six, produces a per-step cost table that looks right and settles an argument in the wrong direction.

So the gap stays a gap. Requests with no identity aggregate under an unnamed bucket you can count, and the fix is to set the headers rather than to trust a heuristic.

What happens to the headers before the request leaves

Every x-tt-* name is removed from the request before it is forwarded upstream. The provider receives your body and your headers minus TokenTriage’s control namespace.

Where x-tt-* headers are read, recorded and removedA client request carrying x-tt identity headers, business-dimension headers and a W3C traceparent reaches the proxy. The proxy reads the identity and dimension values once, early, and copies them onto the decision-ledger row. It then removes every x-tt name from the request, so what reaches the provider is the client's own body and headers minus TokenTriage's control namespace. Removal happens whether or not dimension recording is switched on. traceparent takes the lower path: it is copied to the row for correlation and is also forwarded unchanged, because it belongs to the caller's tracing system rather than to TokenTriage. On the way back the proxy stamps x-tt-request-id on the response, which is the same value as the row's req_id.Client requestx-tt-trace-idx-tt-agentx-tt-tenanttraceparentRead oncevalues capped at 128 BRemove x-tt-*always, flag or no flagProvidersees no x-tt-* nameDecision-ledger rowtrace · dims · trace.w3ctraceparent is copied to the row and forwarded unchangedx-tt-request-id is stamped on the response and equals the row's req_id

Read it left to right. The values are read once, early, and copied to the ledger row; the names are then removed; what continues to the provider carries none of them. traceparent takes the lower path, because it is not in the x-tt-* namespace and is not TokenTriage’s to remove: it is copied to the row and also forwarded unchanged.

Three details follow from that, and each has bitten somebody.

Removal is unconditional. The dimension headers are removed whether or not ledger.dimensions.enabled is set. Recording is a setting; not leaking a customer identifier to a model provider is not.

The removal list is a list of names, not a prefix match. It covers the identity family, the dimension family, the cache-control and cache-disclosure headers, and the response-only headers, so a client that echoes one of ours back inbound cannot leak it upstream either.

Some x-tt-* headers are yours to send and some are not. x-tt-request-id, x-tt-team and x-tt-key are stripped inbound and never read as assertions. TokenTriage mints the request id itself and stamps team and key from the credential that authenticated, so a client cannot bill its usage to another team by setting a header.

What can go wrong

A proxy in front of TokenTriage drops unknown headers. Service meshes and API gateways frequently allowlist request headers. If trace.source reads none on every row and your client is definitely setting the headers, that hop is the first place to look.

The trace id is per-process rather than per-run. A client that mints one id at start-up and reuses it puts a week of traffic in one trace. Mint the id where the run begins.

Two runs share an id. Nothing checks for collisions, so a short or non-random id can merge unrelated runs. A random suffix is enough; the value is opaque and never parsed.

The exact byte caps, and what truncation does

Identity values are capped at 128 bytes each. Truncation happens at a byte boundary, so a multi-byte character can be split, which is acceptable because the values are treated as opaque identifiers rather than as text.

The practical consequence is collision, not corruption. Two ids that differ only past byte 128 become the same id, and their rows merge into one trace. Keep the varying part of an id near the front.

The step-class vocabulary is closed and is checked: a value outside the three known classes is ignored rather than stored, and the class falls back to the derived guess.