Skip to Content
Rius

How do I record token counts with the OpenTelemetry Python SDK?

Rius reads token counts from the gen_ai.usage.input_tokens and gen_ai.usage.output_tokens attributes, which you set on the span with the OpenTelemetry Python SDK. Set the model in gen_ai.request.model too, and Rius computes the cost on its side from the model and the counts. You never send a cost value. If an instrumentation such as OpenInference’s OpenAI instrumentor already emits token counts, Rius normalizes them and you add nothing.

How it works

Rius ingests OTLP over HTTP and reads the OpenTelemetry GenAI attributes on each span. The ones that drive token analytics and cost are:

  • gen_ai.request.model: the requested model.
  • 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 subsets of the input count.
  • gen_ai.usage.reasoning.output_tokens: reasoning tokens, a subset of the output count.

Does your setup set them already? A vanilla OpenTelemetry SDK gives you spans, and you put the attributes on them yourself. An instrumentation library can do it for you. Rius also maps other conventions onto the canonical keys at ingest: OpenInference’s llm.token_count.prompt and completion, and the legacy OpenLLMetry gen_ai.usage.prompt_tokens and completion_tokens. With those instrumentors the counts arrive without extra code. Native gen_ai.* keys always win, and normalization only fills gaps.

Cost needs the request model plus at least one count. A count you leave 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. Costs are list-price estimates in US dollars, not billed amounts.

The span attribute reference lists every attribute Rius reads, and Vanilla OpenTelemetry SDKs shows a well-formed LLM span.

Set it up

  1. Create an API key in the console. The key decides which workspace the traces land in.
  2. Point the exporter at Rius with the standard OpenTelemetry variables. The Python SDK reads them once a tracer provider with an OTLP exporter is registered, as in step 3. Use your workspace’s region in the host; these examples use eu.
export OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.eu.console.rius-glassflow.com" export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf" export OTEL_EXPORTER_OTLP_HEADERS="authorization=Bearer <your API key>" export OTEL_SERVICE_NAME="my-agent" # Keep long LLM spans whole; the default of 128 attributes is too low export OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT=4096
  1. Set the model and operation when you start the span, then add the token counts from the provider’s response once the call returns. The attribute keys are the ones Rius reads. The function call_the_model and the response fields are your own code.
from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter # With no arguments, the exporter reads the OTEL_EXPORTER_OTLP_* variables provider = TracerProvider() provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter())) trace.set_tracer_provider(provider) tracer = trace.get_tracer("my-agent") span = tracer.start_span("chat gpt-4o") span.set_attributes({ "openinference.span.kind": "LLM", "gen_ai.operation.name": "chat", "gen_ai.request.model": "gpt-4o", "gen_ai.provider.name": "openai", }) response = call_the_model() span.set_attributes({ "gen_ai.usage.input_tokens": response.usage.prompt_tokens, "gen_ai.usage.output_tokens": response.usage.completion_tokens, "gen_ai.response.model": response.model, }) span.end()
  1. Add the cache and reasoning counts when your provider reports them, under gen_ai.usage.cache_read.input_tokens, gen_ai.usage.cache_write.input_tokens and gen_ai.usage.reasoning.output_tokens. Each is priced at its own rate or counted as a subset, as described above.
  2. Keep the attribute limit at 4096. OpenTelemetry SDKs keep 128 attributes per span by default and drop the rest without an error, so a span can lose its model or usage. The OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT line in step 2 sets it. If you build the provider in code, as in the OpenInference setup, pass SpanLimits(max_span_attributes=4096).
  3. Run one traced call, open Traces in the console, and open the new trace. The span should show a model name, token counts and cost.

If you would rather not write this by hand, the Trace LLM calls with OpenTelemetry guide covers the Rius SDK, which auto-instruments OpenAI, Anthropic and other libraries with one init() call.

What you get

  • Token counts on each LLM span, visible in the trace and in model analytics.
  • Cost per call, computed by Rius from the model and the counts, with cache reads and writes at their own rates.
  • Cost totals and a per-model breakdown across your traces.
  • If something goes wrong: a 401 from the endpoint means the API key is wrong or revoked. A span that arrives without a model or tokens usually lost its attributes to the 128 limit, or a Collector processor stripped the gen_ai.* keys.

When something else fits

  • If you want token counts without writing attribute code, use an instrumentation that emits them, such as OpenInference’s OpenAI instrumentor, or the Rius SDK’s auto-instrumentation.
  • If your spans pass through an OpenTelemetry Collector, check that no attributes or transform processor removes the gen_ai.* keys on the way.

Still stuck? Get help

Last updated on