Skip to Content
RiusInteroperabilitySpan attribute reference

Span attribute reference

The wire format Rius reads. If you emit spans from anything other than the SDKs (a vanilla OpenTelemetry SDK, a collector pipeline, another language), this page tells you which attributes to set so your traces get the same analytics as SDK-emitted ones.

For the full list of keys, with each one’s type, stability, and examples, see the attribute registry. It is generated from each release of rius-semconv , the registry the SDKs and the platform share. This page covers which of those keys to set and what the platform does with them.

Where to send spans

OTLP/HTTP with protobuf encoding:

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

The contract in full:

  • Host: region-qualified. All Rius endpoints carry the region in the subdomain (ingest.eu.console.rius-glassflow.com, eu.console.rius-glassflow.com); use your workspace’s region. Examples throughout these docs use eu.
  • Protocol: OTLP/HTTP, either protobuf (http/protobuf, recommended) or JSON (http/json); there is no gRPC endpoint. JSON requests get JSON replies. Protobuf is the default of the Python, Go, and Java OTLP/HTTP exporters and of the collector’s otlphttp exporter. In Node it is not: @opentelemetry/exporter-trace-otlp-http sends JSON, which works, but @opentelemetry/exporter-trace-otlp-proto is the better choice (the Rius TypeScript SDK already uses it).
  • Compression: Content-Encoding: gzip is accepted (and worth enabling; span batches compress well).
  • Body limit: 16 MiB per request. Exporters’ default batch sizes sit far below this.
  • Auth: Authorization: Bearer with an API key from the console.

The receiver answers a rejected request with one of these status codes. JSON requests get the error as a JSON google.rpc.Status body.

StatusMeaningWhat to do
400The body is not valid OTLP for its content type.Fix the exporter’s encoding.
401The API key is wrong or revoked.Check the key in Settings → API keys.
402The organization’s billing blocks ingest: the trial has ended, the subscription is canceled or locked, or the workspace is paused because the organization holds more workspaces than its plan allows.See Settings → Plan & usage. Spans sent while billing is blocked are refused, not stored.
413The request body, or its decompressed form, is over 16 MiB.Send smaller batches.
415The Content-Type is neither application/x-protobuf nor application/json.Set the content type your exporter sends.
429The workspace is over its plan’s ingest rate. The whole request is refused, and the response carries Retry-After, in seconds, up to 60.Back off and retry after the hinted delay, or move to a plan with a higher rate.
503A temporary failure, such as the billing check being unreachable.Retry with backoff.

The ingest rate is per workspace, in spans per minute, with one minute’s worth of burst: 25,000 on Core and during the trial, 50,000 on Growth, and 100,000 on Pro.

Setup for each common sender is one page away: a vanilla OpenTelemetry SDK, OpenLLMetry, OpenInference, or an OpenTelemetry Collector.

Span taxonomy

AttributeValuesEffect
openinference.span.kindAGENT, LLM, TOOL, RETRIEVER, EMBEDDING, CHAIN (and RERANKER, see below)Classifies the span in trace views and per-kind analytics
gen_ai.operation.namechat, text_completion, generate_content, embeddings, execute_tool, invoke_agent, create_agent, retrieval, invoke_workflow, plan, …The OpenTelemetry GenAI operation taxonomy

Set either one and the platform derives the other: chat, text_completion, and generate_content are LLM, invoke_agent and create_agent are AGENT, execute_tool is TOOL, embeddings is EMBEDDING, retrieval is RETRIEVER, and invoke_workflow and plan are CHAIN. A CHAIN span gets no operation in return, because the conventions define none for a generic step. The SDKs set both.

The SDKs also emit OpenInference’s RERANKER kind with the rerank operation for reranking steps (Python 1.1.0, TypeScript 1.2.0). Rius has no reranker category yet: in trace views and kind filters, a reranking span shows as LLM when it carries gen_ai.request.model, and as Other when it does not.

An LLM call, as the console and alerts count it, is an inference call: a span whose kind is LLM, set directly or derived from a chat, text_completion, or generate_content operation. A span with neither a kind nor an operation counts when it carries gen_ai.request.model. Agent, tool, and retrieval spans are not LLM calls even when they carry a model, and embedding calls are not either; their cost still appears in the per-model breakdown and the cost totals.

Name spans the way the GenAI conventions ask, the operation followed by what it acted on: chat gpt-4o, execute_tool get_weather, invoke_agent planner, retrieval product-kb. The platform does not require it, but it is what the SDKs emit, so mixed fleets read alike.

Agents, tools, and retrieval

AttributeApplies toNotes
gen_ai.agent.nameinvoke_agent spansThe agent being invoked
gen_ai.agent.nameexecute_tool spansThe agent executing the tool
gen_ai.agent.id / gen_ai.agent.versioninvoke_agent spansA hosted agent’s id, and the invoked agent definition’s version
gen_ai.tool.nameexecute_tool spansThe tool’s own name, never the span name
gen_ai.tool.call.idexecute_tool spansThe id of the model’s tool call this execution answers
gen_ai.tool.typeexecute_tool spansfunction, extension, datastore, …
gen_ai.data_source.idretrieval spansThe index, collection, or knowledge base searched
gen_ai.retrieval.top_k / gen_ai.retrieval.documentsretrieval spansDocuments asked for, and the {"id", "score"} list returned
error.typeany span that ended in an errorA low-cardinality error class, such as anthropic.NotFoundError or tool_error

MCP tool calls

A tool called over the Model Context Protocol (MCP) is an execute_tool span like any other, plus the OpenTelemetry MCP attributes:

AttributeApplies toNotes
mcp.method.nameexecute_tool spans for MCP callstools/call. Marks the tool as an MCP tool, which is how context attribution tells MCP tools from local ones
mcp.protocol.versionexecute_tool spans for MCP callsThe MCP protocol version the client negotiated. Stored with the span; no analytics read it

A trace’s context attribution reports how many context tokens MCP tools took, and their share of the total, only when at least one span in the trace carries mcp.method.name. Without it, MCP and local tools cannot be told apart, so the MCP share is unknown, not zero. The SDKs’ MCP integration sets both attributes; set them yourself when you instrument MCP calls by hand.

LLM analytics

These drive model analytics and cost computation:

AttributeTypeNotes
gen_ai.request.modelstringThe requested model
gen_ai.usage.input_tokensintPrompt tokens, including cached ones
gen_ai.usage.output_tokensintCompletion tokens, including reasoning ones
gen_ai.usage.cache_read.input_tokens / gen_ai.usage.cache_write.input_tokensintPrompt-cache reads and writes, subsets of the input count
gen_ai.usage.reasoning.output_tokensintReasoning tokens, a subset of the output count
gen_ai.provider.namestringFor example openai or anthropic
gen_ai.response.modelstringModel reported by the provider
gen_ai.response.idstringThe provider’s completion id
gen_ai.request.*per keyThe request parameters the GenAI conventions define, for example gen_ai.request.temperature or gen_ai.request.max_tokens
rius.request.*anyRequest parameters the conventions do not define, under their own key, for example rius.request.tool_choice
gen_ai.response.finish_reasonsstring[]Why generation stopped
gen_ai.response.time_to_first_chunkdoubleSeconds from the span start to the first streamed chunk. Time to first token is derived from it; see Span events
rius.aggregated_usage.*intUsage a wrapper span repeats from the model calls under it, with the same suffixes as gen_ai.usage.*, such as rius.aggregated_usage.input_tokens. Stored with the span, never summed into token totals or cost. See usage counted once

Cost is computed server-side from gen_ai.request.model plus the gen_ai.usage.* token counts: input, output, cache read, and cache write, each priced at its own rate. A span needs the request model and at least one of those counts; any count it leaves 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 tokens and the model name.

Content

AttributeApplies toNotes
gen_ai.input.messages / gen_ai.output.messagesLLM spansJSON in the GenAI message shape (below)
gen_ai.system_instructionsLLM spansA system prompt sent separately from the chat history, as a JSON array of {"type": "text", "content": ...} parts. Counted as the system prompt in context attribution
gen_ai.tool.definitionsLLM spansThe tool definitions the model was shown
gen_ai.tool.call.arguments / gen_ai.tool.call.resultexecute_tool spansThe arguments the tool was called with and the result it returned. The platform moves them to input.value / output.value, so the tool’s input and output render like any other. A native input.value / output.value wins, and the source keys are removed either way, so search on input.value / output.value
input.value / output.valueany spanGeneric input/output, JSON or plain text

Content attributes are stored for trace inspection and are the ones covered by the SDK’s privacy controls when you export through it.

The GenAI message shape

gen_ai.input.messages and gen_ai.output.messages carry a JSON array of messages, each {"role", "parts": [...]}. Text parts are {"type": "text", "content": ...}; tool interactions have dedicated part types:

[ {"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"}] } ]

This is the OpenTelemetry GenAI message schema; the SDK’s set_input() / set_output() normalize OpenAI-style dicts into it (message formats). When emitting directly, produce this shape yourself so message rendering in the console works. Spans carrying it render their prompt and completion on the span’s Input / Output tab:

Prompt and completion rendered from GenAI message attributes

Spans without these attributes show “Content not captured” there; the rest of the span (timings, tokens, cost) is unaffected.

List-valued attributes

An attribute sent as an OTLP array or key-value list, such as gen_ai.response.finish_reasons, is stored as plain JSON: ["stop"], or an object for a key-value list. A saved search or alert that matches the stored text should match that form. Spans ingested before this storage change keep the older OTLP envelope form ({"arrayValue":{"values":[...]}}), and those rows are not rewritten.

Context sizes

The SDKs also send rius.context.sizes on spans from their generation API: a compact JSON record of the byte size of each tool definition and each message part. LLM spans from auto-instrumentation, such as the OpenAI or LangChain integrations, do not carry it. Rius uses it to divide a call’s input tokens across the parts of its context, and it survives content stripping because it holds no content. It is optional: without it, the platform estimates the same breakdown from the captured messages, gen_ai.system_instructions, and tool definitions.

Span events

Event nameMeaning
gen_ai.first_tokenDeprecated. Earlier Rius SDK releases wrote it beside gen_ai.response.time_to_first_chunk. The platform still reads it, but new instrumentation should send only the attribute
exceptionStandard OpenTelemetry exception event (record_exception); surfaces in error analysis

When a span carries a producer’s own first-chunk event, OpenInference’s First Token Stream Event or the Vercel AI SDK’s ai.stream.firstChunk, that event’s time is the first chunk and gen_ai.response.time_to_first_chunk is not used for it. Otherwise time to first token comes from gen_ai.response.time_to_first_chunk: the platform places the first chunk at the span start plus that duration, never later than the span’s end. For streaming LLM calls, set gen_ai.response.time_to_first_chunk once, in seconds from the span start, when the first content chunk arrives, together with gen_ai.request.stream: true. The SDK’s record_first_token() does exactly this, and when you export through the SDKs they also derive the duration from a producer’s first-chunk event. No event is needed.

Sessions and users

AttributeTypeNotes
session.idstringGroups the traces of one session, such as a conversation or an agent run. Set it on every span of the trace, not only the root: the platform reads it per span, and a span without it falls back to gen_ai.conversation.id, then to being grouped by its trace id.
gen_ai.conversation.idstringThe OpenTelemetry GenAI conventions’ session key. Read as the session when session.id is absent, so spans that follow the GenAI conventions group into sessions without session.id.
user.idstringThe user a request served, on every span in scope. The platform also reads enduser.id, then enduser.pseudo.id, when user.id is absent. A span with none of them is anonymous.

The SDK’s session and user scopes stamp these attributes for you; third-party senders set them directly.

Resource attributes

AttributeEffect
service.nameThe service every trace view and filter groups by. Always set it.
service.versionThe build or release the process runs.
service.instance.idThe process that produced the span; the SDKs send the same id in their heartbeats, which joins traces to the Agents view.
gen_ai.main_agent.idThe OpenTelemetry GenAI conventions’ stable identifier for the agent the process runs. The SDKs send the configured main agent id, otherwise the agent name, and send no gen_ai.main_agent.* key when they have neither.
gen_ai.main_agent.name / gen_ai.main_agent.descriptionThe agent the process runs, by name, and what it does. The SDKs send them only alongside gen_ai.main_agent.id. If you send telemetry without the SDK, set gen_ai.main_agent.name to name your agent. The platform files a span under the first of 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 that is set and is not a placeholder such as unknown_service.
rius.main_agent.nameDeprecated in favor of gen_ai.main_agent.name; the SDKs send both through the deprecation window. The agent the process runs. The platform still reads it, second after gen_ai.main_agent.name, so telemetry that sends only this key keeps its agent name.
rius.main_agent.id / rius.main_agent.descriptionDeprecated in favor of gen_ai.main_agent.id and gen_ai.main_agent.description; the SDKs send both through the deprecation window. A stable id for that agent and what it does. rius.main_agent.id carries only a configured id, never the agent name.
rius.main_agent.versionThe version of the agent’s definition (prompt, tools, policy). Not deprecated: the GenAI conventions have no equivalent.

Attribute count limit

OpenTelemetry SDKs keep at most 128 attributes per span unless configured otherwise, and drop the rest without an error. Instrumentations that flatten messages and tools into one attribute per field, OpenInference among them, pass that within a few turns of an agent loop, and the span then loses its model, tools, usage, or messages. The Rius SDKs raise the limit to 4096. When you export without them, set it yourself:

export OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT=4096

Supported conventions

What each attribute family gets today. “Analytics” means the promoted columns above (taxonomy, model, tokens, cost, time to first token). “Stored” says which key the value is kept under on the span, and so which key to search and filter on in trace inspection.

ConventionAttributesAnalyticsStored
OTel GenAIgen_ai.* as documented above✓ Full: taxonomy, model, tokens, cost, TTFTAs sent, except gen_ai.tool.call.arguments / result (moved to input.value / output.value) and the deprecated gen_ai.system (moved to gen_ai.provider.name)
OpenInference kindopeninference.span.kind✓ TaxonomyAs sent
OpenInference generic I/Oinput.value / output.value✓ ContentAs sent
OpenInference LLMllm.model_name, llm.provider, llm.token_count.*, llm.finish_reason, llm.invocation_parameters, llm.tools.*, llm.input_messages.* / llm.output_messages.*✓ Normalized at ingestUnder the canonical keys
OpenInference session and usersession.id, user.id✓ Session and user groupingAs sent
OpenLLMetry / Tracelooptraceloop.span.kind, traceloop.entity.*, llm.request.type, legacy gen_ai.usage.prompt_tokens / completion_tokens, legacy gen_ai.prompt.* / gen_ai.completion.*✓ Normalized at ingestUnder the canonical keys
Earlier GenAI spellinggen_ai.usage.cache_creation.input_tokens, the cache-write count’s name before the conventions renamed it to cache_write✓ Normalized at ingestUnder the canonical key
pydantic-ai usage detailsgen_ai.usage.details.reasoning_tokens / thinking_tokens / thoughts_tokens, gen_ai.usage.details.cache_read_tokens / cache_write_tokens✓ Normalized at ingest: reasoning and cache countsUnder the canonical keys
Laminar (lmnr)lmnr.span.type, lmnr.span.input / lmnr.span.output, lmnr.association.properties.session_id / user_id, lmnr.association.properties.metadata.tool_call_id / agent.name✓ Normalized at ingest: taxonomy (LLM, TOOL), input and output, session, user, tool call id, agent nameUnder the canonical keys
Vercel AI SDK (v5, v6)ai.operationId, ai.model.*, ai.settings.*, ai.usage.*, ai.prompt*, ai.response.*, ai.toolCall.*✓ Normalized at ingest: taxonomy, model, tokens, messages, tool calls, TTFT. A wrapper span’s usage moves to rius.aggregated_usage.*, so it is not counted twiceUnder the canonical keys
Anything elseyour own attributes✗As sent

“Normalized at ingest” means the platform moves the values onto their canonical keys before storage and removes the source keys it mapped. Native gen_ai.* keys always win; normalization fills gaps, never overwrites. Search and filter on the canonical key: a filter on llm.token_count.prompt or gen_ai.usage.prompt_tokens matches nothing, while gen_ai.usage.input_tokens matches. A few source keys stay on the span on purpose: for OpenInference, llm.system and the members of llm.invocation_parameters that no canonical key takes; for OpenLLMetry, traceloop.entity.name; for the Vercel AI SDK, ai.operationId. For OpenInference:

  • The request model comes from llm.request.model_name, else the model member of llm.invocation_parameters, else llm.model_name. The response model comes from llm.response.model_name, else llm.model_name, but only when the span’s status is OK. An ERROR or UNSET span gets no response model from llm.model_name, because on those spans it may still hold the requested model rather than the one the provider served.
  • llm.token_count.prompt / completion and their cache and reasoning details fill the token counts, which is what makes cost computation work.
  • The recognized members of llm.invocation_parameters (model, model_name, temperature, top_p, top_k, max_tokens, max_completion_tokens, num_output, frequency_penalty, presence_penalty, seed, n, stop, stop_sequences, and stream) become gen_ai.request.*, and tool_choice becomes rius.request.tool_choice. model_name is a fallback for model, num_output for the maximum tokens, and stop and stop_sequences both become gen_ai.request.stop_sequences. Other members stay in the bag. Its tools become gen_ai.tool.definitions, as do the indexed llm.tools.* schemas, and Anthropic’s system parameter becomes the leading system message.
  • The flattened llm.input_messages.* / llm.output_messages.* are reassembled into the GenAI message shape, and at ingest the first-token span event is renamed to gen_ai.first_token.

For OpenLLMetry, the Traceloop decorator kinds (workflow, task, agent, tool) and the provider span’s llm.request.type become the span taxonomy; the decorated entity’s name becomes the tool or invoked agent name; traceloop.entity.input / output become input.value / output.value; the older usage and streaming spellings move to their current keys; and the flattened gen_ai.prompt.* / gen_ai.completion.* messages are reassembled into the GenAI message shape. Keys with no canonical equivalent, such as llm.usage.total_tokens, ride through under their own names.

The Rius SDKs apply the same OpenInference mapping in your process before export, with one difference: they skip the llm.model_name fallback, which only the platform applies. See what the SDK normalizes.

Unknown attributes are stored as sent and are never an error, with three exceptions that apply to every span:

  • Credential header attributes, such as http.request.header.authorization or ai.request.headers.x-api-key, are never stored, on the span, its events, or its resource.
  • Inline media bytes in messages and raw payloads are replaced by a placeholder recording the media type and size.
  • Embedding vectors (OpenInference’s embedding.embeddings.N.embedding.vector, the Vercel AI SDK’s ai.embedding / ai.embeddings) are dropped. The embedded texts are kept.

Next steps

Last updated on