# 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.

Source: https://tokentriage.com/docs/build/observability-identity

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

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](/docs/build/observability-headers) 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`](/docs/build/observability-step-classes) 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.

```json
"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.

<Callout type="key">
`traceparent` is recorded on the row as `trace.w3c` for correlation with your existing tracing,
and it is not used to build the trace. If your services already propagate W3C trace context and
you want TokenTriage's tree to match it, copy the value into `x-tt-trace-id` yourself.
</Callout>

## 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.

<HeaderLifecycleDiagram />

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.

<Details summary="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.

</Details>

<NextSteps items={[
  {title: "Put identity on the wire", href: "/docs/build/observability-headers", description: "The four-line rule, a curl you can paste, wrapping a CLI agent, and where each framework lets you inject headers."},
  {title: "Step classes", href: "/docs/build/observability-step-classes", description: "The closed vocabulary, what the proxy guesses when you omit it, and why the guess never changes who a row belongs to."},
  {title: "How steps become a tree", href: "/docs/build/observability-trace-trees", description: "What parent ids build, what a rollup by agent shows instead, and the four ways a tree comes out wrong."},
  {title: "HTTP headers", href: "/docs/reference/headers", description: "The complete wire contract: every request header a client may send and every response header the proxy emits."},
]} />
