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-protobufThe 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 useeu. - 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’sotlphttpexporter. In Node it is not:@opentelemetry/exporter-trace-otlp-httpsends JSON, which works, but@opentelemetry/exporter-trace-otlp-protois the better choice (the Rius TypeScript SDK already uses it). - Compression:
Content-Encoding: gzipis accepted (and worth enabling; span batches compress well). - Body limit: 16 MiB per request. Exporters’ default batch sizes sit far below this.
- Auth:
Authorization: Bearerwith 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.
| Status | Meaning | What to do |
|---|---|---|
400 | The body is not valid OTLP for its content type. | Fix the exporter’s encoding. |
401 | The API key is wrong or revoked. | Check the key in Settings → API keys. |
402 | The 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. |
413 | The request body, or its decompressed form, is over 16 MiB. | Send smaller batches. |
415 | The Content-Type is neither application/x-protobuf nor application/json. | Set the content type your exporter sends. |
429 | The 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. |
503 | A 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
| Attribute | Values | Effect |
|---|---|---|
openinference.span.kind | AGENT, LLM, TOOL, RETRIEVER, EMBEDDING, CHAIN (and RERANKER, see below) | Classifies the span in trace views and per-kind analytics |
gen_ai.operation.name | chat, 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
| Attribute | Applies to | Notes |
|---|---|---|
gen_ai.agent.name | invoke_agent spans | The agent being invoked |
gen_ai.agent.name | execute_tool spans | The agent executing the tool |
gen_ai.agent.id / gen_ai.agent.version | invoke_agent spans | A hosted agent’s id, and the invoked agent definition’s version |
gen_ai.tool.name | execute_tool spans | The tool’s own name, never the span name |
gen_ai.tool.call.id | execute_tool spans | The id of the model’s tool call this execution answers |
gen_ai.tool.type | execute_tool spans | function, extension, datastore, … |
gen_ai.data_source.id | retrieval spans | The index, collection, or knowledge base searched |
gen_ai.retrieval.top_k / gen_ai.retrieval.documents | retrieval spans | Documents asked for, and the {"id", "score"} list returned |
error.type | any span that ended in an error | A 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:
| Attribute | Applies to | Notes |
|---|---|---|
mcp.method.name | execute_tool spans for MCP calls | tools/call. Marks the tool as an MCP tool, which is how context attribution tells MCP tools from local ones |
mcp.protocol.version | execute_tool spans for MCP calls | The 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:
| Attribute | Type | Notes |
|---|---|---|
gen_ai.request.model | string | The requested model |
gen_ai.usage.input_tokens | int | Prompt tokens, including cached ones |
gen_ai.usage.output_tokens | int | Completion tokens, including reasoning ones |
gen_ai.usage.cache_read.input_tokens / gen_ai.usage.cache_write.input_tokens | int | Prompt-cache reads and writes, subsets of the input count |
gen_ai.usage.reasoning.output_tokens | int | Reasoning tokens, a subset of the output count |
gen_ai.provider.name | string | For example openai or anthropic |
gen_ai.response.model | string | Model reported by the provider |
gen_ai.response.id | string | The provider’s completion id |
gen_ai.request.* | per key | The request parameters the GenAI conventions define, for example gen_ai.request.temperature or gen_ai.request.max_tokens |
rius.request.* | any | Request parameters the conventions do not define, under their own key, for example rius.request.tool_choice |
gen_ai.response.finish_reasons | string[] | Why generation stopped |
gen_ai.response.time_to_first_chunk | double | Seconds from the span start to the first streamed chunk. Time to first token is derived from it; see Span events |
rius.aggregated_usage.* | int | Usage 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
| Attribute | Applies to | Notes |
|---|---|---|
gen_ai.input.messages / gen_ai.output.messages | LLM spans | JSON in the GenAI message shape (below) |
gen_ai.system_instructions | LLM spans | A 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.definitions | LLM spans | The tool definitions the model was shown |
gen_ai.tool.call.arguments / gen_ai.tool.call.result | execute_tool spans | The 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.value | any span | Generic 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:

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 name | Meaning |
|---|---|
gen_ai.first_token | Deprecated. 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 |
exception | Standard 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
| Attribute | Type | Notes |
|---|---|---|
session.id | string | Groups 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.id | string | The 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.id | string | The 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
| Attribute | Effect |
|---|---|
service.name | The service every trace view and filter groups by. Always set it. |
service.version | The build or release the process runs. |
service.instance.id | The 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.id | The 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.description | The 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.name | Deprecated 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.description | Deprecated 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.version | The 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=4096Supported 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.
| Convention | Attributes | Analytics | Stored |
|---|---|---|---|
| OTel GenAI | gen_ai.* as documented above | ✓ Full: taxonomy, model, tokens, cost, TTFT | As 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 kind | openinference.span.kind | ✓ Taxonomy | As sent |
| OpenInference generic I/O | input.value / output.value | ✓ Content | As sent |
| OpenInference LLM | llm.model_name, llm.provider, llm.token_count.*, llm.finish_reason, llm.invocation_parameters, llm.tools.*, llm.input_messages.* / llm.output_messages.* | ✓ Normalized at ingest | Under the canonical keys |
| OpenInference session and user | session.id, user.id | ✓ Session and user grouping | As sent |
| OpenLLMetry / Traceloop | traceloop.span.kind, traceloop.entity.*, llm.request.type, legacy gen_ai.usage.prompt_tokens / completion_tokens, legacy gen_ai.prompt.* / gen_ai.completion.* | ✓ Normalized at ingest | Under the canonical keys |
| Earlier GenAI spelling | gen_ai.usage.cache_creation.input_tokens, the cache-write count’s name before the conventions renamed it to cache_write | ✓ Normalized at ingest | Under the canonical key |
| pydantic-ai usage details | gen_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 counts | Under 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 name | Under 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 twice | Under the canonical keys |
| Anything else | your 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 themodelmember ofllm.invocation_parameters, elsellm.model_name. The response model comes fromllm.response.model_name, elsellm.model_name, but only when the span’s status is OK. An ERROR or UNSET span gets no response model fromllm.model_name, because on those spans it may still hold the requested model rather than the one the provider served. llm.token_count.prompt/completionand 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, andstream) becomegen_ai.request.*, andtool_choicebecomesrius.request.tool_choice.model_nameis a fallback formodel,num_outputfor the maximum tokens, andstopandstop_sequencesboth becomegen_ai.request.stop_sequences. Other members stay in the bag. Itstoolsbecomegen_ai.tool.definitions, as do the indexedllm.tools.*schemas, and Anthropic’ssystemparameter 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 togen_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.authorizationorai.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’sai.embedding/ai.embeddings) are dropped. The embedded texts are kept.
Next steps
- Vanilla OpenTelemetry SDKs: emitting these attributes from any language.
- OpenTelemetry Collector: routing them through a collector pipeline.