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.modeland thegen_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.modelandgen_ai.provider.namefill 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 resourcegen_ai.main_agent.name, the resourcerius.main_agent.name, the resourcegen_ai.agent.name, the span’sgen_ai.agent.name, andservice.name. - Span kind.
gen_ai.operation.name(chat,execute_tool,invoke_agentand so on) andopeninference.span.kindmap 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.
- Mark what each span is. Set
gen_ai.operation.nameon every span, such aschatfor a model call,execute_toolfor a tool andinvoke_agentfor an agent run. Name spans the same way, the operation followed by its target, for examplechat gpt-4oorexecute_tool get_weather. - Set the model and the usage on each model call. Send
gen_ai.request.modeland thegen_ai.usage.input_tokensandgen_ai.usage.output_tokenscounts. Addgen_ai.usage.cache_read.input_tokensandgen_ai.usage.cache_write.input_tokenswhen the provider reports them. Never send a cost attribute, because Rius computes it. - Name the agent. Set
service.namealways. Setgen_ai.main_agent.nameon the resource to name your agent, andgen_ai.agent.nameoninvoke_agentspans. On tool spans, addgen_ai.tool.name. - Group by session and user. Set
session.idanduser.idon every span, not only the root. Prefer an opaque user id to an email. - Send content in the GenAI message shape, if you want the Input / Output tab to render.
gen_ai.input.messagesandgen_ai.output.messagescarry 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 carriesgen_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.modelis 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 asunknown_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. Withoutuser.id, the span is anonymous. - Unknown MCP share. Without
mcp.method.nameon 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.
Related
Still stuck? Get help