Skip to Content

Which OpenTelemetry attribute should my spans use for prompt cache tokens?

Rius reads prompt cache tokens from gen_ai.usage.cache_read.input_tokens for cache reads and gen_ai.usage.cache_write.input_tokens for cache writes. Both are counted inside gen_ai.usage.input_tokens, and Rius prices each at its own rate when it computes cost. Rius also normalizes older spellings at ingest, so spans from older instrumentation still land on these two keys.

How it works

The two cache keys sit beside the other usage keys on the span for a model call. Each one is an integer, and each is a subset of the input count, not an addition to it.

  • gen_ai.usage.input_tokens is the prompt token count, including cached tokens.
  • gen_ai.usage.cache_read.input_tokens is how many of those came from the prompt cache.
  • gen_ai.usage.cache_write.input_tokens is how many were written to the prompt cache.
  • gen_ai.request.model is the requested model. Cost needs it.

Rius computes cost on the server from gen_ai.request.model plus the token counts: input, output, cache read and cache write, each at its own rate. A span needs the model and at least one count, and any 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. Never send a cost attribute. Send the tokens and the model name.

Rius also reads these older or tool-specific names and moves their values onto the canonical keys before storage:

  • gen_ai.usage.cache_creation.input_tokens, the earlier name for the cache-write count, before the GenAI conventions renamed it to cache_write. It is stored under gen_ai.usage.cache_write.input_tokens.
  • gen_ai.usage.details.cache_read_tokens and its cache-write counterpart, which pydantic-ai emits. These are normalized to the same canonical keys.
  • OpenInference spans: the cache and reasoning details of llm.token_count.prompt and llm.token_count.completion fill the token counts.

Native gen_ai.* keys always win. Normalization fills gaps and never overwrites. The source keys it mapped are removed, so search and filter on the canonical key. A filter on an older name such as llm.token_count.prompt matches nothing.

Set it up

  1. Set the cache counts on the span for each model call, next to the model and the input and output counts. This example uses the canonical keys and shows the span’s name and attributes, not a request body. The input count of 1,500 includes the 1,000 cache-read tokens and the 200 cache-write tokens.
{ "name": "chat claude-haiku-4-5-20251001", "attributes": { "gen_ai.operation.name": "chat", "gen_ai.provider.name": "anthropic", "gen_ai.request.model": "claude-haiku-4-5-20251001", "gen_ai.usage.input_tokens": 1500, "gen_ai.usage.output_tokens": 300, "gen_ai.usage.cache_read.input_tokens": 1000, "gen_ai.usage.cache_write.input_tokens": 200 } }
  1. Name the span the way the GenAI conventions ask, the operation followed by the model, as in chat claude-haiku-4-5-20251001. Rius does not require it, but it keeps spans from mixed senders readable alike.

  2. Do not add a cost attribute. Rius derives cost from the model and the counts.

  3. Send the spans as OTLP over HTTP to Rius with your API key in an Authorization: Bearer header. The endpoint is https://ingest.eu.console.rius-glassflow.com/v1/traces. The recommended encoding is http/protobuf, and there is no gRPC endpoint.

POST https://ingest.eu.console.rius-glassflow.com/v1/traces Authorization: Bearer <your API key> Content-Type: application/x-protobuf

The span attribute reference lists every usage key and the conventions Rius normalizes. For the wider setup of sending OpenTelemetry traces, see Trace LLM calls with OpenTelemetry.

What you get

  • Cache reads and cache writes priced at their own rates, so cost reflects how much of each prompt came from the cache.
  • Cost for every span that carries a known model and at least one count, with no cost value for you to send.
  • One canonical key to search and filter on, whichever spelling the sender used.
  • Cache counts from OpenInference and pydantic-ai spans, which Rius maps onto the same keys at ingest.

A span with a model that the price catalog does not know gets no cost, even when the cache counts are present. Costs are list-price estimates in US dollars from LiteLLM’s public model-price catalog, not billed amounts.

When something else fits

If a library already emits one of the older or tool-specific spellings, you can leave it as it is, because Rius normalizes it at ingest. If you write the spans yourself, use the two canonical keys. For other kinds of usage, such as reasoning tokens, the same page lists gen_ai.usage.reasoning.output_tokens, a subset of the output count.

Still stuck? Get help

Last updated on