Skip to Content
Rius

Why should my agent’s spans follow the OpenTelemetry GenAI semantic conventions?

Rius computes cost, token totals, model analytics and agent names from the OpenTelemetry GenAI keys on your spans, so you send facts and never prices. Set the model, the token counts and an agent name, and Rius derives the rest. A span that leaves them out is still stored, but the views built on those keys have nothing to read. The span attribute reference lists every key.

How it works

Rius reads the GenAI convention keys directly and does the arithmetic on its side. Here is what each group of keys drives:

  • Cost. Rius computes it from gen_ai.request.model and the gen_ai.usage.* counts: input, output, cache read and cache write. Each is priced at its own rate. A count you leave out is read as 0. Costs are list-price estimates in US dollars from LiteLLM’s public model-price catalog, not billed amounts.
  • Tokens. Totals include prompt cache. Some providers, such as Anthropic, report input tokens without cache reads and writes. Rius recognizes that form per span and adds the cache back, so a total means every token the call processed.
  • Model. gen_ai.request.model, gen_ai.response.model and gen_ai.provider.name fill the span’s Model section and the model analytics. Cost by model also comes from them.
  • Agent. Rius files a span under the first of these that is set and is not a placeholder such as unknown_service: the resource gen_ai.main_agent.name, the resource rius.main_agent.name, the resource gen_ai.agent.name, the span’s gen_ai.agent.name, and service.name.
  • Span kind. gen_ai.operation.name (chat, execute_tool, invoke_agent and so on) and openinference.span.kind map to each other. Set either one and Rius derives the other.
  • Time to first token. For streamed calls it comes from gen_ai.response.time_to_first_chunk, in seconds from the span start.

For the data model behind these, see Concepts.

Set it up

The Rius SDKs and their auto-instrumentation set these keys for you. Follow the steps below when you emit spans from a plain OpenTelemetry SDK, a collector pipeline or your own code. Trace LLM calls with OpenTelemetry covers the sender setup.

  1. Mark what each span is. Set gen_ai.operation.name on every span, such as chat for a model call, execute_tool for a tool and invoke_agent for an agent run. Name spans the same way, the operation followed by its target, for example chat gpt-4o or execute_tool get_weather.
  2. Set the model and the usage on each model call. Send gen_ai.request.model and the gen_ai.usage.input_tokens and gen_ai.usage.output_tokens counts. Add gen_ai.usage.cache_read.input_tokens and gen_ai.usage.cache_write.input_tokens when the provider reports them. Never send a cost attribute, because Rius computes it.
  3. Name the agent. Set service.name always. Set gen_ai.main_agent.name on the resource to name your agent, and gen_ai.agent.name on invoke_agent spans. On tool spans, add gen_ai.tool.name.
  4. Group by session and user. Set session.id and user.id on every span, not only the root. Prefer an opaque user id to an email.
  5. Send content in the GenAI message shape, if you want the Input / Output tab to render. gen_ai.input.messages and gen_ai.output.messages carry a JSON array like this:
[ {"role": "user", "parts": [{"type": "text", "content": "What is 2+2?"}]}, { "role": "assistant", "parts": [ {"type": "tool_call", "id": "call_1", "name": "calculator", "arguments": "{\"expr\": \"2+2\"}"} ] }, { "role": "tool", "parts": [{"type": "tool_call_response", "id": "call_1", "response": "4"}] } ]

What you get

Following the conventions gives you these results:

  • A cost on every model call whose model is in the price catalog, with cache reads and writes priced separately, plus cost by model.
  • Token totals that count every token the call processed, whichever provider sent it.
  • The right call counts. Rius counts an LLM call as a span of kind LLM. A span with no kind or operation counts only when it carries gen_ai.request.model.
  • Tool and agent spans classified as such in trace views and per-kind analytics.
  • Time to first token on streamed calls.

What a span loses when keys are missing:

  • No cost. A span with no model, a model the catalog does not know, or none of the usage counts gets no cost. The cost is left empty, not zero.
  • No LLM-call count. A span with no kind, no operation and no gen_ai.request.model is not counted as an LLM call.
  • Agent grouping falls back. With none of the agent name keys, Rius files the span under service.name. If that is also unset, you get a placeholder such as unknown_service.
  • No content on the Input / Output tab. Spans without the message attributes show “Content not captured”. Timings, tokens and cost are unaffected.
  • No grouping. Without session.id, a trace is its own session. Without user.id, the span is anonymous.
  • Unknown MCP share. Without mcp.method.name on MCP tool spans, Rius cannot tell MCP tools from local ones, so their share of context is unknown, not zero.

Your own attributes are stored as sent and never cause an error, but they feed none of the analytics above.

When something else fits

If your instrumentation already emits OpenInference, OpenLLMetry or Traceloop, Laminar, Vercel AI SDK 5 or 6, or Pydantic AI usage details, you do not need to rewrite keys. Rius normalizes them at ingest, and native gen_ai.* keys always win over the mapped ones. If you use the Rius SDKs, they set the convention keys for you, so you only add session.id, user.id and an agent name where your code needs them.

Still stuck? Get help

Last updated on