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.
On this page
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 is the field list, and what identity is on the wire 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.
with tt.trace(agent="triage", user="u-42", tenant="acme", feature="chat"):
with tt.step(agent="planner", step_class="user_turn"):
h = tt.headers()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();
});
});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 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.
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.
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.
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)
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.
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.
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.
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.
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:
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:
x-tt-trace-id: 78cd088d6c1708e74d03455937509403
x-tt-step-id: d036deeba8a333a5
traceparent: 00-78cd088d6c1708e74d03455937509403-d036deeba8a333a5-01
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.
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.
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.