Skip to Content

How does Rius read OpenInference tool call attributes?

Rius reads an OpenInference tool span by its openinference.span.kind value of TOOL, and stores input.value and output.value as the tool’s input and output. It also normalizes the model-side tool attributes, such as llm.tools.* and llm.input_messages., onto gen_ai. keys at ingest. You send standard OpenInference spans to Rius and they show up in the console as tool spans, with tool definitions and tool call messages in the GenAI shape.

How it works

Rius handles tool call data from OpenInference in five ways. Details are in the span attribute reference and on the OpenInference page.

  • Span kind. openinference.span.kind set to TOOL classifies the span as a tool span. Set either the kind or gen_ai.operation.name and Rius derives the other, so a TOOL span gets the execute_tool operation.
  • Tool input and output. input.value and output.value are kept as sent and shown as the span’s content.
  • Tool definitions. The tools member of llm.invocation_parameters and the indexed llm.tools.* schemas both become gen_ai.tool.definitions. The tool_choice member becomes rius.request.tool_choice.
  • Tool calls in messages. The flattened llm.input_messages.* and llm.output_messages.* attributes are reassembled into the GenAI message shape. There, a tool call is a part of type tool_call and its result is a part of type tool_call_response.
  • Tool span attributes. For execute_tool spans the reference defines gen_ai.tool.name, gen_ai.tool.call.id and gen_ai.tool.type. Rius reads them as sent.

This is how the reassembled messages look:

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

Normalization moves each value onto its canonical key and removes the source key it mapped. A native gen_ai.* key always wins, because normalization fills gaps and never overwrites. Search and filter on the canonical key, for example gen_ai.tool.definitions, not on llm.tools.*.

Set it up

  1. Point an OpenInference instrumentor at the ingest endpoint. This is the full wiring without the Rius SDK. The OpenInference page covers it in more detail.
from opentelemetry.sdk.trace import SpanLimits, TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter from openinference.instrumentation.openai import OpenAIInstrumentor # Raise the attribute limit: see "Raise the span attribute limit" below. provider = TracerProvider(span_limits=SpanLimits(max_span_attributes=4096)) provider.add_span_processor( BatchSpanProcessor( OTLPSpanExporter( endpoint="https://ingest.eu.console.rius-glassflow.com/v1/traces", headers={"Authorization": "Bearer <your API key>"}, ) ) ) OpenAIInstrumentor().instrument(tracer_provider=provider)
  1. Raise the span attribute limit. OpenInference writes one attribute per message field and per tool field, and OpenTelemetry keeps only 128 attributes per span by default. Past that, a span silently loses attributes such as the model, tool definitions or messages. The code above sets 4096 in Python. For any language you can set it through the environment instead:
export OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT=4096
  1. Make sure your tool spans carry openinference.span.kind set to TOOL. Instrumentors that set it need no change. If you create tool spans by hand, set it, or set gen_ai.operation.name to execute_tool, and Rius derives the other. Start those spans from provider.get_tracer(__name__), or register the provider with trace.set_tracer_provider(provider), because a tracer from the unregistered global provider exports nothing.

  2. Run a call that uses a tool, then open the trace in the console and look at the tool span and the model span that requested it.

If your app is Python, the Rius SDK ships these same instrumentors as integrations and sets the exporter and the attribute limit for you.

What you get

  • Tool spans in trace views. A span with kind TOOL is classified as a tool span in trace views and in the per-kind analytics.
  • Tool input and output. input.value and output.value appear as the content of the tool span.
  • Tool calls in the conversation. A model span with llm.input_messages.* and llm.output_messages.* renders its prompt and completion on the span’s Input / Output tab, with the tool call in the requesting call’s output messages and the tool’s response in the next call’s input messages. A span without message attributes shows “Content not captured” there, and its timings, tokens and cost are unaffected.
  • Tool definitions on the model call. The tools the model was shown are stored under gen_ai.tool.definitions, and the tool choice under rius.request.tool_choice.
  • Model analytics and cost. llm.token_count.* and the model name fill the gen_ai.* token and model keys, so the model span that decides to call a tool gets a server-side cost. It is a list-price estimate in US dollars. Do not send a cost attribute.

Rius also never stores credential header attributes, such as http.request.header.authorization.

When something else fits

  • If your app is Python, the Rius SDK integrations apply the same OpenInference mapping in your process before export. The one difference is that they skip the llm.model_name fallback, which only the platform applies.
  • If you write tool spans by hand, set the gen_ai.* attributes directly, and for MCP tool calls also set mcp.method.name. Trace LLM calls with OpenTelemetry compares every sender path, and the interoperability overview lists the others.

Still stuck? Get help

Last updated on