Skip to Content
Rius

How does Rius read OpenTelemetry GenAI token usage attributes?

Rius reads token counts from the OpenTelemetry GenAI usage attributes, mainly gen_ai.usage.input_tokens and gen_ai.usage.output_tokens, and uses them to show tokens and compute cost. Cache and reasoning counts have their own attributes. Spellings from OpenInference, OpenLLMetry, pydantic-ai and the Vercel AI SDK are moved onto these same keys when the span arrives. You then search and filter on one set of names, whichever sender produced the span.

How it works

The OpenTelemetry GenAI convention names five usage attributes, and Rius reads each one as an integer on the span for a model call:

  • gen_ai.usage.input_tokens: prompt tokens, including cached ones.
  • gen_ai.usage.output_tokens: completion tokens, including reasoning ones.
  • gen_ai.usage.cache_read.input_tokens and gen_ai.usage.cache_write.input_tokens: prompt-cache reads and writes. Both are subsets of the input count.
  • gen_ai.usage.reasoning.output_tokens: reasoning tokens, a subset of the output count.

Because the cache and reasoning counts are subsets, you do not subtract them from the totals and you do not add them on top.

Other senders spell usage differently, so Rius normalizes at ingest. It moves the values onto the keys above and removes the source keys it mapped:

  • OpenInference: llm.token_count.prompt and llm.token_count.completion, plus their cache and reasoning details, fill the token counts.
  • OpenLLMetry and Traceloop: the legacy gen_ai.usage.prompt_tokens and completion_tokens move to the current keys.
  • Earlier GenAI spelling: gen_ai.usage.cache_creation.input_tokens, the old name of the cache-write count, becomes gen_ai.usage.cache_write.input_tokens.
  • pydantic-ai: the gen_ai.usage.details.* reasoning and cache keys become the reasoning and cache counts.
  • Vercel AI SDK v5 and v6: ai.usage.* is normalized to the same counts.

Native gen_ai.* keys always win. Normalization fills gaps and never overwrites a value you sent. A wrapper span that repeats usage from the model calls under it has that usage moved to rius.aggregated_usage.*. Rius stores it with the span but never adds it to token totals or cost, so nothing is counted twice.

Keys with no canonical equivalent, such as llm.usage.total_tokens, are stored under their own names.

Set it up

  1. Send traces to Rius over OTLP/HTTP with an ingest key. The full sender setup is on Interoperability, and Trace LLM calls with OpenTelemetry compares each path with the SDK.
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://ingest.eu.console.rius-glassflow.com/v1/traces export OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf export OTEL_EXPORTER_OTLP_TRACES_HEADERS="Authorization=Bearer%20ri_..."
  1. On each span for a model call, set gen_ai.request.model and the usage counts you have. If your instrumentation already emits GenAI, OpenInference, OpenLLMetry, pydantic-ai or Vercel AI SDK attributes, change nothing, because Rius normalizes them.
  2. If you emit spans by hand, set gen_ai.usage.input_tokens and gen_ai.usage.output_tokens as integers. Add the cache read, cache write and reasoning counts when your provider reports them.
  3. Do not send a cost attribute. Rius computes cost from the model name and the counts.
  4. Check a span in the console, then filter on the canonical key. A filter on gen_ai.usage.input_tokens matches. A filter on llm.token_count.prompt or gen_ai.usage.prompt_tokens matches nothing, because those source keys are removed after mapping.

The span attribute reference has the full conventions matrix and the content shapes.

What you get

  • Token counts on the span for each model call, with timings and cost, and the same names whichever instrumentation sent the span.
  • Cost per call, computed by Rius from gen_ai.request.model plus the usage counts. Input, output, cache read and cache write are each priced at their own rate. Costs are list-price estimates in US dollars from LiteLLM’s public model-price catalog, not billed amounts.
  • Cost totals and per-model breakdowns that count each call once, because wrapper-span usage is kept apart.
  • Filters and saved searches that work on one key per count.

A span needs the request model and at least one count to get a cost, and any count it leaves out is read as 0. A span with no model, a model the price catalog does not know, or none of the counts gets no cost. Embedding calls are not counted as LLM calls, but their cost still appears in the per-model breakdown and the cost totals.

When something else fits

If your instrumentation emits only a spelling Rius does not normalize, the values are stored as sent and do not drive tokens or cost. Map them to the gen_ai.usage.* keys in your exporter or collector. If you want the counts set for you, the Rius SDKs and their integrations do that, as covered in the span attribute reference.

Still stuck? Get help

Last updated on