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

The attribute mapping

Every attribute on an exported span, which ones follow the OpenTelemetry GenAI convention, and where TokenTriage extends it and why.

ReferenceLast updated

On this page

One ledger row becomes one span. Its attributes come from two namespaces: gen_ai.*, which follows the OpenTelemetry GenAI semantic conventions, and tokentriage.*, which carries what the conventions do not yet cover.

Both are emitted on every span. A consumer that understands only the convention gets the model, the provider, the token counts and the dollar; a consumer that understands the extension additionally gets the provenance of that dollar, the counterfactual prices, and the identity the row was attributed under.

The span envelope

Field Value
traceId the first 16 bytes of SHA-256(trace.trace_id), lowercase hex
spanId the first 8 bytes of SHA-256(req_id)
parentSpanId the first 8 bytes of SHA-256(trace.parent_id), absent when there is no parent
name the served model, after any rewrite; gen_ai.request when the model is unknown
kind 3, SPAN_KIND_CLIENT
startTimeUnixNano the row’s timestamp
endTimeUnixNano the start plus the row’s total latency in milliseconds
resource one attribute, service.name: tokentriage
scope github.com/tokentriage/platform/internal/otlpexport, version 1

Ids are derived from the ledger’s own ids rather than adopted from an inbound traceparent, so the same row always encodes to the same span. A row with no trace id yields no span at all, which is why untraced traffic does not reach a collector.

spanId and parentSpanId are derived from different id spaces in this build, so a child’s parent reference does not resolve. The export page carries the captured evidence and what it costs.

gen_ai.* — the convention attributes

Attribute From Notes
gen_ai.request.model request.model what the client asked for
gen_ai.response.model the served model after any rewrite
gen_ai.provider.name http.provider falls back to a name derived from the wire dialect
gen_ai.usage.input_tokens usage.input_tokens omitted when zero
gen_ai.usage.output_tokens usage.output_tokens omitted when zero
gen_ai.usage.cache_read.input_tokens usage.cache_read_tokens the provider’s prompt-cache discount
gen_ai.usage.cache_creation.input_tokens usage.cache_write_tokens tokens written to the provider’s cache
gen_ai.usage.reasoning_tokens usage.reasoning_tokens see the divergence noted below
gen_ai.cost.amount cost.usd proposed, not yet a convention
gen_ai.cost.currency fixed USD present when amount is
gen_ai.cost.pricing_source cost.pricing_snapshot proposed
gen_ai.cost.estimated cost.estimated proposed; emitted only when true

The first seven follow the registry’s current spelling, including gen_ai.provider.name, which replaced the older gen_ai.system.

One diverges. The registry names the reasoning-token attribute gen_ai.usage.reasoning.output_tokens, and TokenTriage emits gen_ai.usage.reasoning_tokens. A consumer looking for the convention spelling will not find it, and a dashboard that sums reasoning tokens across producers will miss these. This is filed; until it is fixed, map the extension spelling in your collector if you need the aggregate.

The rule that the cost attributes exist for

gen_ai.cost.* is absent as a whole on a row with no price. There is no gen_ai.cost.amount: 0.

That distinction is the reason the attribute group was proposed rather than invented locally. A genuinely free call and a call whose price is unknown are different facts, and a consumer that sums the attribute has to be able to tell them apart. Emitting zero for an unknown price silently understates spend for every consumer downstream, and the understatement is invisible because the sum still looks like a sum.

So a priced row carries the whole group, and an unpriced row carries none of it plus tokentriage.cost.unpriced: true in the extension namespace. The count of rows that carried no amount is recoverable; a fabricated zero would not have been.

tokentriage.* — the extension attributes

Attribute Carries Present when
tokentriage.cost.usd the dollar the row is priced
tokentriage.cost.unpriced true the row has no price
tokentriage.cost.pricing_snapshot which price table produced the figure a snapshot is known, priced or not
tokentriage.cost.pricing_key the key the price was looked up under a key was resolved
tokentriage.cost.estimated true the figure rests on estimated token counts
tokentriage.cost.would_usd.<tier> what this call would have cost on each tier counterfactual prices were computed
tokentriage.usage.source where the token counts came from always
tokentriage.decision.mode ledger, shadow or route always
tokentriage.decision.tier the chosen tier a tier was chosen
tokentriage.decision.rule the policy rule that fired a rule fired
tokentriage.trace.agent the x-tt-agent value the header was set
tokentriage.trace.step_class the step class in force always
tokentriage.dim.<name> one attribute per business dimension dimensions are on and set

Four of these have no convention equivalent to converge on, and they are the reason the extension namespace stays even if the cost proposal is accepted. pricing_snapshot and pricing_key are provenance: they let somebody re-derive the figure months later against the table that produced it. would_usd.<tier> is the counterfactual that makes shadow mode readable, and no convention models a price that was not paid. And dim.<name> is your own vocabulary rather than a standard one.

Attributes with a variable suffix are emitted in sorted order, so two identical rows encode to byte-identical spans.

Where the convention stands

The GenAI conventions moved out of the main OpenTelemetry semantic-conventions repository into a dedicated one in June 2026, and no GenAI span, event, metric or attribute was marked stable as of July 2026. There is no versioned release to pin a schema URL against yet.

That is worth knowing before you build a pipeline on these names: the convention half of this table can change under you on the convention’s own schedule, and the extension half changes on TokenTriage’s. Dual emission is the hedge. Both namespaces carry the same dollar today, so a collector rule can prefer either and fall back to the other.

TokenTriage’s maintainers filed two proposals against the conventions, and both address gaps this mapping had to work around: gen_ai.cost.* for monetary cost, which the registry has no attribute of any kind for, and gen_ai.evaluation.* extensions for how a quality score was obtained, which quality on the wire covers.

What TokenTriage will do if the cost proposal is accepted

Emit both vocabularies through a deprecation window, then converge on the standard keys for the fields they overlap and keep the extension keys for what the standard does not cover.

That is already the shape of what ships: both namespaces are on the wire now, carrying the identical dollar, so the migration is a consumer-side change rather than a producer-side cutover. The rule about absence rather than zero carries across unchanged in either vocabulary.