Which OpenTelemetry attribute should my spans use for prompt and completion message?
Rius reads prompt and completion messages from gen_ai.input.messages and gen_ai.output.messages on the span for each model call. Each holds a JSON array in the OpenTelemetry GenAI message shape. Rius also reads the flattened message attributes of OpenInference and the legacy ones of OpenLLMetry and moves them onto these two keys at ingest, so existing instrumentation keeps working. Spans that carry them show the prompt and completion on the span’s Input / Output tab.
How it works
Rius expects two keys on LLM spans:
gen_ai.input.messagesfor the prompt side.gen_ai.output.messagesfor the completion.
Each value is a JSON array of messages shaped as role plus parts. Text parts have the type text and a content field. Tool interactions have their own part types, tool_call and tool_call_response:
[
{"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"}]
}
]If you send a system prompt separately from the chat history, put it in gen_ai.system_instructions as a JSON array of text parts. Rius counts it as the system prompt when it breaks down a call’s context.
Other names Rius reads:
- OpenInference: the flattened
llm.input_messages.*andllm.output_messages.*attributes are reassembled into the GenAI message shape. - OpenLLMetry and Traceloop: the legacy flattened
gen_ai.prompt.*andgen_ai.completion.*attributes are reassembled the same way. - Generic input and output:
input.valueandoutput.valuework on any span, as JSON or plain text. They are stored as sent and are not reshaped into messages.
Normalization moves values onto the canonical keys and removes the source keys it mapped. A native gen_ai.* key always wins, and normalization only fills gaps. When you search or filter in trace inspection, use gen_ai.input.messages and gen_ai.output.messages, because a filter on an older name matches nothing.
Set it up
- Set
gen_ai.input.messagesandgen_ai.output.messagesas JSON strings on the span for each model call. Write them yourself in the shape above, because Rius renders messages in the console only from that shape. With the Rius Python SDK,set_input()andset_output()turn OpenAI-style dicts into it for you. - Add the attributes that make the span an LLM call and give it a cost. Rius computes cost from the model and token counts, so send those and never a cost value.
- Name the span the way the GenAI conventions ask, the operation followed by what it acted on. An example span looks like this:
name: chat gpt-4o
attributes:
gen_ai.operation.name: chat
openinference.span.kind: LLM
gen_ai.request.model: gpt-4o
gen_ai.usage.input_tokens: 12
gen_ai.usage.output_tokens: 8
gen_ai.input.messages: '[{"role": "user", "parts": [{"type": "text", "content": "What is 2+2?"}]}]'
gen_ai.output.messages: '[{"role": "assistant", "parts": [{"type": "text", "content": "4"}]}]'- If you export without the Rius SDKs and use an instrumentation that flattens messages into one attribute per field, watch the OpenTelemetry limit of 128 attributes per span. A span over the limit loses attributes without an error. Raise it with
OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, which the Rius SDKs set to 4096 for you. - Send the spans to the Rius OTLP/HTTP endpoint with your API key as a Bearer token. The Span attribute reference has the full contract. The guide Trace LLM calls with OpenTelemetry walks through the sender setup.
What you get
- The prompt and completion render on the span’s Input / Output tab, including tool calls and their responses.
- Spans without message attributes show “Content not captured” there. Timings, tokens and cost are unaffected.
- Instrumentation that emits OpenInference or OpenLLMetry names keeps working, and you search on one set of keys.
- Messages are content attributes, so the SDK’s privacy controls cover them. Setting
RIUS_CAPTURE_CONTENTto false in the SDKs strips them at export. Cost and tokens still arrive. - Inline media bytes in messages are replaced by a placeholder that records the media type and size. Credential header attributes are never stored.
When something else fits
- If your instrumentation already emits OpenInference or OpenLLMetry attributes, leave them as they are. Rius normalizes them at ingest.
- If a span has no message structure, such as a plain function step, use
input.valueandoutput.valueinstead. - For a tool span you trace yourself, send the tool’s arguments and result. Rius moves them to
input.valueandoutput.valueso they render like any other input and output.