How does Rius read OpenInference token usage attributes?
Rius reads OpenInference token usage from the llm.token_count attributes and moves the counts onto the gen_ai.usage keys at ingest, so token analytics and cost work. The prompt count becomes input tokens and the completion count becomes output tokens. Cache and reasoning details are mapped too. You search and filter on the gen_ai.usage keys, not on the original llm.token_count names.
How it works
OpenInference instrumentors write token counts under llm.token_count.*. When a span arrives, Rius normalizes them: it moves the values onto their canonical keys before storage and removes the source keys it mapped.
| OpenInference attribute | Stored as |
|---|---|
llm.token_count.prompt | gen_ai.usage.input_tokens |
llm.token_count.completion | gen_ai.usage.output_tokens |
| The cache and reasoning details of those counts | gen_ai.usage.cache_read.input_tokens, gen_ai.usage.cache_write.input_tokens and gen_ai.usage.reasoning.output_tokens |
Three rules shape the result:
- Native keys win. If a span already carries a
gen_ai.*key, Rius keeps it. Normalization fills gaps and never overwrites. - The input count includes cached tokens.
gen_ai.usage.input_tokenscounts prompt tokens including cached ones, and the cache counts are subsets of it. The output count includes reasoning tokens. - Cost needs a model. Rius computes cost on the server from the request model plus the token counts. For OpenInference spans, the request model comes from
llm.request.model_name, else themodelmember ofllm.invocation_parameters, elsellm.model_name.
Because the source keys are removed, a filter on llm.token_count.prompt or gen_ai.usage.prompt_tokens matches nothing, while a filter on gen_ai.usage.input_tokens matches. The Rius SDKs apply the same OpenInference mapping in your process before export. The full contract is in the span attribute reference.
Set it up
- Point your OpenInference instrumentor at the Rius ingest endpoint with an OTLP exporter. This example uses the OpenAI instrumentor. The OpenInference guide covers the wiring in full.
from opentelemetry.sdk.trace import SpanLimits, TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from openinference.instrumentation.openai import OpenAIInstrumentor
# Raise the attribute limit so long agent loops keep their usage and model
provider = TracerProvider(span_limits=SpanLimits(max_span_attributes=4096))
provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(
endpoint="https://ingest.eu.console.rius-glassflow.com/v1/traces",
headers={"Authorization": "Bearer <your API key>"},
)
)
)
OpenAIInstrumentor().instrument(tracer_provider=provider)- Raise the span attribute limit. OpenInference writes one attribute per message field and per tool field, and OpenTelemetry keeps only 128 attributes per span by default. A span that passes the limit silently loses attributes, and the usage can go with them. If you do not set it in code as above, set it through the environment:
export OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT=4096-
Make sure each model call span carries a model name, through
llm.model_nameor one of the other sources listed above. A span with no model gets no cost. -
Search and filter on the
gen_ai.usage.*keys when you inspect traces. For the other ways to send traces, see Trace LLM calls with OpenTelemetry and the interoperability overview.
What you get
- Token usage on each model call span, stored under
gen_ai.usage.input_tokensandgen_ai.usage.output_tokens, with cache and reasoning counts where the instrumentor reports them. - Server-side cost for each call, computed from the model and the token counts, with cache reads and writes each priced at their own rate. Costs are list-price estimates in US dollars. A span with no model, a model the price catalog does not know, or none of the counts gets no cost.
- Model analytics from the same normalized keys, so OpenInference-instrumented calls are analyzed like spans that use the
gen_ai.*keys natively. - No cost attribute to send. Send tokens and the model name, and Rius does the pricing.
When something else fits
- If your code already emits the
gen_ai.usage.*keys, send those directly. They are stored as sent and need no mapping. - If your app is Python or TypeScript, the Rius SDK ships these same instrumentors as integrations and sets the attribute limit for you.
- If your spans use another convention, the span attribute reference lists which families Rius normalizes.