# From your call to the wire

What the SDK does to a value between your argument and the header it sends, from coercion and caps to the charset guard.

Source: https://tokentriage.com/docs/sdks/emission

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

`headers()` turns your ambient context into a header map. Between the value you pass and the
bytes that leave the process, the SDK coerces types, enforces byte caps, drops anything that
would corrupt the request, and sorts the tag family. This page is the complete list of what it
does and when it tells you.

The header **names and their meaning** are not here. [The header reference](/docs/reference/headers)
is the field list, and [what identity is on the wire](/docs/build/observability-identity) explains
what the server does with them. This page covers the rules the SDK applies on the way.

## Calling it

`headers()` is pure. It reads context, returns a dict or record, and opens no socket.

<CodeTabs>
  <Fragment slot="python">
  ```python
  with tt.trace(agent="triage", user="u-42", tenant="acme", feature="chat"):
      with tt.step(agent="planner", step_class="user_turn"):
          h = tt.headers()
  ```
  </Fragment>
  <Fragment slot="typescript">
  ```ts
  await tt.trace({ agent: "triage", user: "u-42", tenant: "acme", feature: "chat" }, async () => {
    await tt.step({ agent: "planner", stepClass: "user_turn" }, async () => {
      const h = tt.headers();
    });
  });
  ```
  </Fragment>
</CodeTabs>

```text
x-tt-trace-id: a9f41cddf4c3e50eb640ef35772f660b
x-tt-step-id: e2bb463aeeb30722
x-tt-agent: planner
x-tt-step-class: user_turn
x-tt-user: u-42
x-tt-tenant: acme
x-tt-feature: chat
```

Merge that map into whatever your HTTP client sends. An [adapter](/docs/sdks/adapters) does the
merge for you; `headers()` is the library-agnostic path that works with any client.

## What identity appears, and when

Identity headers are emitted only where they mean something:

| Open scopes | Identity emitted |
|---|---|
| none | none — dimensions only |
| trace | `x-tt-trace-id` |
| trace + step | `x-tt-trace-id`, `x-tt-step-id` |
| trace + nested steps | the above, plus `x-tt-parent-id` |

A request made outside any trace is a standalone row rather than a phantom trace invented to fill
the slot. A trace with no step is legal: the row groups, and carries no step edge.

Minted ids are cryptographically random: a trace id is 16 bytes rendered as 32 lowercase hex
characters, a step id is 8 bytes as 16 hex characters. Those widths match the W3C trace-context
shape, which is what allows the `traceparent` rendering below. You can supply your own opaque ids
instead; they are treated as any other value, capped at 128 bytes.

## Value rules

Every value passes the same five checks before it reaches the wire, in this order.

**1. Type coercion.** Integers, floats and booleans are rendered as strings. Booleans render
lowercase, as `true` and `false`, in both languages, so a Python `True` and a TypeScript `true`
produce identical bytes.

```text
headers(user=42, tenant=1.5, feature=True)
  → {'x-tt-user': '42', 'x-tt-tenant': '1.5', 'x-tt-feature': 'true'}
```

**2. Absent and non-coercible values are dropped**, one header at a time, with `TT-SDK-010`. A
`None` user does not empty the header; it removes it, and the rest of the map is unaffected.

```text
headers(user=None, tenant="kept")
  → {'x-tt-trace-id': '3225e6d2…', 'x-tt-tenant': 'kept'}
TT-SDK-010: dropped x-tt-user (None or non-coercible value)
```

**3. The charset guard.** A value containing any byte outside `0x20`–`0x7E`, which is visible
ASCII plus space, is dropped with `TT-SDK-008`. That covers two hazards at once. Carriage returns and
newlines are the header-injection class, and a value carrying them could forge a header the
proxy does not strip. Non-ASCII crashes HTTP clients that encode headers as latin-1, at send
time, where the SDK cannot catch it.

```text
headers(user="bad\r\nX-Injected: 1")   → the user header is absent
headers(user="café")                   → the user header is absent
TT-SDK-008: dropped x-tt-user (value byte outside 0x20-0x7E)
```

<Callout type="warning">
This is a drop, not an escape. A user id with an accent in it will silently stop being attributed
after one warning, and the rest of the request goes through fine. If your dimension values can
contain non-ASCII, transliterate or hash them before they reach the SDK.
</Callout>

**4. Byte caps.** Values are truncated to 128 bytes, and tag keys to 32, on a byte boundary
rather than a character boundary, matching what the server would record. Truncation is
announced with the header named and the original length.

```text
headers() with a 130-byte user   → 128 bytes emitted
TT-SDK-001: truncated x-tt-user to 128 bytes (was 130)
```

**5. Closed vocabularies.** `step_class` must be one of `user_turn`, `tool_loop` or
`provider_state`; the per-call `cache` control must be one of `off`, `refresh` or `no-store`.
Anything else drops that single header and says which value it rejected.

```text
TT-SDK-002: dropped x-tt-step-class (not_a_class not in user_turn|tool_loop|provider_state)
TT-SDK-003: dropped x-tt-cache (warm not in off|refresh|no-store)
```

No value input raises. Dimensions and tags carry request-scoped runtime data in production, so
a surprising value degrades the telemetry rather than the request.

## Tags

Tags render as `x-tt-tag-<key>` and are emitted in byte-wise sorted key order, which mirrors the
server's admission order, so what you see truncated or dropped is what the server would have
dropped too.

```text
trace(tags={"zeta": "1", "alpha": "2", "mid": "3"})
  → x-tt-tag-alpha: 2
    x-tt-tag-mid:   3
    x-tt-tag-zeta:  1
```

Keys must match `^[a-z0-9_-]+$`. A key outside that set drops that tag with `TT-SDK-008`; an
uppercase or spaced key is the common case.

```text
trace(tags={"Good_key-1": "v", "BAD KEY": "v"})   → both tags absent
```

Note that `Good_key-1` is also dropped there: the capital `G` puts it outside the charset. Keys
are lowercase.

If two keys collide after the 32-byte key cap, the last one in sorted order wins, and
`TT-SDK-004` names the key.

## The closed name set

The SDK emits only header names that exist in its contract, plus the `x-tt-tag-` family and
`traceparent`. This is not stylistic. The proxy's upstream strip list is an enumerated set of
names rather than a prefix match, so a header called `x-tt-something-invented` would not be
stripped and **would reach your provider**. There is no API through which you can add one.

The one exception is a name you control. `tier_header` lets you rename the routing-tier header,
and a custom name is stripped only if your server config names the same header:

```text
init(tier_header="x-my-tier")
TT-SDK-014: custom tier_header x-my-tier - ensure the server strips it
            (routing.overrides.tier_header) or it egresses upstream
```

Set `routing.overrides.tier_header` to match, or leave the default `x-tt-tier` alone.

## traceparent, which is not stripped

With `emit_traceparent` enabled and both ids valid, the SDK also emits a W3C `traceparent`
carrying the same identity:

```text
x-tt-trace-id: 78cd088d6c1708e74d03455937509403
x-tt-step-id:  d036deeba8a333a5
traceparent:   00-78cd088d6c1708e74d03455937509403-d036deeba8a333a5-01
```

<Callout type="key">
Unlike every `x-tt-*` header, `traceparent` is **not** stripped by the proxy and will reach your
provider. That is the standard's intent, since it is how downstream systems join a trace, but it
does mean enabling it sends your trace and span ids to a third party. It is off by default.
</Callout>

If either id is not valid 32/16-character lowercase hex, which happens when you supply your own
opaque ids, the header is skipped with `TT-SDK-005` rather than emitted malformed.

<Details summary="Emission order, and why it is fixed">

Headers come out in contract declaration order, with tags sorted among themselves and
`traceparent` last. Order carries no meaning over HTTP, but fixing it makes the emission
vectors byte-comparable across the two bindings, which is how parity is checked rather than
assumed. 27 of the 70 conformance vectors cover emission.

</Details>

## When nothing is emitted

`headers()` returns an empty map in two cases, and they are worth telling apart. If
`TOKENTRIAGE_DISABLED` is set, you get `{}` plus the one-time `TT-SDK-030` kill-switch warning.
If the SDK hits an internal fault, the shield returns `{}` plus `TT-SDK-000` naming the entry
point and exception type. Your request still goes out, unlabelled.

Neither is silent. An instrumentation library that turns itself off without saying so produces
empty dashboards nobody can explain.

<NextSteps items={[
  { title: 'Traces, steps, and context', href: '/docs/sdks/context', description: 'Where these values come from and how the four layers resolve.' },
  { title: 'Adapters', href: '/docs/sdks/adapters', description: 'Injecting this map automatically, scoped to one origin.' },
  { title: 'Diagnostics', href: '/docs/sdks/diagnostics', description: 'Every TT-SDK code on this page, and what to do about it.' },
  { title: 'Header reference', href: '/docs/reference/headers', description: 'The wire fields themselves: names, meaning, and what the server records.' }
]} />
