Skip to Content
RiusSDKReferencePython

Python reference

Generated from the glassflow-rius 1.0.0 docstrings, covering the Python SDK’s public API surface module by module. Wording fixes happen upstream in the SDK repository, never here.

rius.client

SDK entrypoint: configure OpenTelemetry and export GenAI traces via OTLP.

DEFAULT_SPAN_ATTRIBUTE_COUNT_LIMIT

The per-span attribute count init() gives its provider, in place of OTel’s default of 128. The OpenInference instrumentors write one attribute per message field and per tool field, so an agent loop with ten tools passes 128 within about six (anthropic) to nine (openai) turns, and OTel then evicts the OLDEST keys: the request bag (model and parameters), the tool definitions and the system prompt go first. 4096 holds hundreds of turns; the value length is not raised, since our JSON attributes carry their own cap.

build_span_exporter

def build_span_exporter(config: GlassflowConfig) -> SpanExporter

Build the default OTLP/HTTP span exporter for a resolved config.

Arguments:

  • config - A resolved configuration; the exporter posts to config.traces_endpoint with config.headers.

Returns:

A ready-to-use OTLP/HTTP SpanExporter.

GlassflowClient Objects

class GlassflowClient()

Handle over a configured tracer provider.

Returned by init. Exposes the lifecycle operations (flush, shutdown) and tracer access for the pipeline it owns; the resolved configuration is available as client.config.

register_workspace

def register_workspace(alias: str, api_key: str) -> None

Add (or rotate the key of) a workspace destination at runtime.

Requires routing to be enabled at init time via workspaces= (an empty dict opts in with no static routes). Spans started inside rius.workspace(alias) are then exported with api_key.

get_tracer

def get_tracer(name: str = TRACER_NAME) -> trace.Tracer

Return a tracer bound to this client’s provider.

Arguments:

  • name - Instrumentation scope name; defaults to the SDK’s own.

flush

def flush(timeout_millis: int = 30_000) -> bool

Force-flush pending spans and report delivery.

Returns True only when the queue drained within timeout_millis AND the most recent export attempt succeeded. Earlier releases reported queue drain alone, so it returned True even while every batch was being rejected (e.g. 401 on a bad API key). A False return therefore means either a flush timeout or that spans are currently not being delivered; the log carries the distinction.

shutdown

def shutdown() -> None

Drain pending spans and stop. Releases the global init() slot.

Also stops the heartbeat thread and sends its final stopped ping, so the backend can tell a clean shutdown from a vanished agent.

init

def init(*, endpoint: str | None = None, api_key: str | None = None, service_name: str | None = None, service_version: str | None = None, headers: dict[str, str] | None = None, disabled: bool | None = None, sample_rate: float | None = None, capture_content: bool | None = None, mask: Callable[[Any], Any] | None = None, instruments: Sequence[str] | None = None, span_exporter: SpanExporter | None = None, heartbeat: bool | None = None, heartbeat_interval: float | None = None, agent_name: str | None = None, main_agent_id: str | None = None, main_agent_description: str | None = None, main_agent_version: str | None = None, heartbeat_transport: Callable[[dict[str, Any]], None] | None = None, connectivity_transport: ProbeTransport | None = None, partial_spans: bool | None = None, partial_spans_delay: float | None = None, session_id: str | None = None, workspaces: dict[str, str] | None = None, workspace_exporter_factory: ExporterFactory | None = None, set_global: bool = True, bridge_foreign_provider: bool | None = None) -> GlassflowClient

Initialize the SDK: build a tracer provider that exports OTLP traces.

Calling init() again while a global client is active logs a warning and returns the existing client unchanged; call shutdown() on it first to reconfigure. After that the SDK’s own helpers (start_span, observe, the generation helpers) follow the new client, as do the bundled instrumentors. Third-party code that took a tracer from the OpenTelemetry global keeps the first provider, because that global is write-once; its spans still reach the new client through the bridge described under bridge_foreign_provider.

The provider allows up to 4096 attributes per span rather than OTel’s default of 128, which a long OpenInference agent span passes within a few turns and then silently evicts its oldest keys (model, tools, system prompt). OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT or OTEL_ATTRIBUTE_COUNT_LIMIT, when set, wins instead. A span that still hits the limit reports its drop count on the wire.

Arguments:

  • endpoint - Base OTLP endpoint. Traces are sent to <endpoint>/v1/traces.
  • api_key - API key; injected as an Authorization: Bearer header.
  • service_name - Value for the service.name resource attribute.
  • service_version - Value for the service.version resource attribute, e.g. the release or image tag this process is running. Resolution order is this argument, then RIUS_SERVICE_VERSION, then whatever OTEL_RESOURCE_ATTRIBUTES supplies (the OTel SDK merges that variable into every resource), then unset. There is no placeholder default: a fake version would group every deployment into one bucket, which is worse than the attribute being absent.
  • headers - Extra headers for the OTLP exporter.
  • disabled - If True, no exporter is attached (spans are dropped).
  • sample_rate - Head sampling ratio 0.0-1.0 (whole-trace). Default 1.0.
  • capture_content - If False, strip prompt/response content at export. Default True.
  • mask - Redact content attribute values at export (applies to all spans).
  • instruments - Auto-instrumentation selection. None (default) enables every bundled instrumentor whose package is installed; a list restricts to those names; [] disables auto-instrumentation. Instrumentors are process-global, so with set_global=False they are only enabled when instruments is passed explicitly.
  • span_exporter - Override the default OTLP exporter (useful for testing).
  • heartbeat - Enable the agent-lifetime heartbeat thread (RIUS_HEARTBEAT; on by default, set False to opt out). Pings <endpoint>/v1/heartbeat from init until process exit so the platform can tell a live-but-idle agent from a vanished one. Each init() mints one instance id, sent both in heartbeat payloads and on every span as the service.instance.id resource attribute, so the platform can join the two and count replicas. Fork caveat: a child forked after init() heartbeats under a fresh id, but its spans keep the parent’s (the OTel Resource is immutable), identifying the pre-fork process family; for exact per-worker span identity, call init() after the fork (e.g. in gunicorn’s post_fork).
  • heartbeat_interval - Seconds between pings (default 15, clamped to [5, 300]; the backend derives staleness from this).
  • agent_name - Identity both heartbeats and spans group under, stamped on the resource as rius.main_agent.name AND, unchanged, as gen_ai.agent.name; defaults to service_name. The rius.main_agent.* family is where the process-level question “which agent is this?” lives: gen_ai.agent.name has no resource-level meaning in the conventions and on a span means the agent being INVOKED. Both keys are stamped, indefinitely — a swap would break agent identity for every deployment running an SDK newer than the sink, and resource attributes ride once per OTLP batch, so the duplication costs essentially nothing. A process whose name resolves to the unknown_service placeholder emits no rius.main_agent.name at all, the same suppression the span helpers apply.
  • main_agent_id - Stable identifier of the agent this process IS (RIUS_MAIN_AGENT_ID), stamped as rius.main_agent.id. It must be STABLE across restarts and deployments; a transient in-memory or process-local id must not be used, exactly as the conventions require of gen_ai.agent.id, because a value that changes every restart mints a new “agent” every restart. Stamped only when set; there is no placeholder.
  • main_agent_description - What this agent does (RIUS_MAIN_AGENT_DESCRIPTION), stamped as rius.main_agent.description. Stamped only when set.
  • main_agent_version - Version of the agent DEFINITION — its prompt, tools and policy (RIUS_MAIN_AGENT_VERSION), stamped as rius.main_agent.version. Deliberately NOT service.version and never derived from it in either direction: a service can sit at 2.3.1 while its agent definition is at 7, and the two move independently. Stamped only when set.
  • heartbeat_transport - Override the heartbeat HTTP transport (useful for testing, like span_exporter).
  • connectivity_transport - Override the HTTP send used by the one-shot background connectivity check (useful for testing). The check POSTs an empty OTLP request at init and logs an actionable warning on 401/403, unreachable host, or other non-2xx, so a bad key or endpoint is visible immediately instead of surfacing as silently missing traces.
  • session_id - Process-wide session id (RIUS_SESSION_ID), stamped as session.id on every span so the platform groups this process’s traces into one session. For one-run-per-process agents; a server handling many sessions scopes each one with rius.session() instead, which overrides this default.
  • workspaces - Enable multi-workspace routing: a mapping of alias to API key. Spans started inside rius.workspace(alias) are exported with that workspace’s key; spans outside any scope use the default api_key. Pass {} to opt in with no static routes and register destinations later via register_workspace(). One trace must stay inside one workspace; see rius.workspace.
  • workspace_exporter_factory - Override how per-workspace exporters are built from an API key (useful for testing, like span_exporter). Defaults to the standard OTLP exporter against the configured endpoint.
  • set_global - Register the provider as the global OpenTelemetry provider.
  • bridge_foreign_provider - What to do when another SDK already holds the OpenTelemetry global provider (RIUS_BRIDGE_FOREIGN_PROVIDER; off by default). Off, rius keeps to its own provider: LLM spans started inside the other SDK’s spans arrive without their parent, flagged rius.parent.foreign, counted in the heartbeat, and the first one logs a warning. On, rius also exports the other SDK’s spans (the parents), under rius’s resource identity and subject to rius’s sampling and content settings, and it never modifies what the other SDK exports. Either way the resource records the conflict as rius.sdk.global_provider. Applies to a global init() only.

get_tracer

def get_tracer(name: str = TRACER_NAME) -> trace.Tracer

Return a tracer bound to the active global client’s provider.

Falls back to the OpenTelemetry global provider when no global init() is active, so it is safe to call before or without init().

register_workspace

def register_workspace(alias: str, api_key: str) -> None

Add (or rotate the key of) a workspace destination on the global client.

The module-level twin of client.register_workspace(). Requires a global init(workspaces=...) to have opted into routing.

rius.observe

The @observe decorator: trace the developer’s own functions.

Wraps sync, async, generator, and async-generator functions (and methods), recording timing, inputs/outputs, and exceptions as an OpenTelemetry span. Spans nest automatically via OTel context propagation and are exported through whatever provider init() configured (a no-op if the SDK was never initialized).

observe

def observe(func: Callable[..., Any] | None = None, *, name: str | None = None, capture_input: bool = True, capture_output: bool = True, kind: SpanKind = SpanKind.CHAIN, tool_name: str | None = None, data_source_id: str | None = None, top_k: int | None = None, agent_name: str | None = None, agent_id: str | None = None, agent_version: str | None = None, tool_call_id: str | None = None, tool_type: str | None = None) -> Any

Decorate a function so each call is traced as a span.

Usable bare (@observe) or parameterized (@observe(name=..., ...)). Supports sync functions, async def functions, generators, and async generators; for generators the span covers the whole iteration and the tracing context is attached only around each step. Exceptions are recorded with ERROR status and always re-raised.

Arguments:

  • func - The decorated function (filled in by bare @observe usage).
  • name - Span name. Unset, the span is named the way the GenAI conventions say it should be — the operation for its kind followed by what it acted on (execute_tool get_weather, invoke_agent planner, retrieval product-kb), or the bare operation when that identifier is unknown. A CHAIN span has no operation and keeps the function’s __qualname__.
  • capture_input - Record call arguments as JSON in input.value.
  • capture_output - Record the return value as JSON in output.value.
  • kind - Span taxonomy (openinference.span.kind); default CHAIN.
  • tool_name - The tool’s own name (gen_ai.tool.name), used only when kind is TOOL. Unset, it falls back to name and then to the function’s __qualname__. Separate from name because the span name may carry an operation prefix and the tool name must not; the fallback through name exists so an existing caller is not silently renamed, and warns so the two can be separated.
  • data_source_id - The index, collection or knowledge base a RETRIEVER span searched (gen_ai.data_source.id). Ignored for other kinds.
  • top_k - How many documents the retrieval asked for (gen_ai.retrieval.top_k). Ignored for other kinds. What came back is not a decorator argument: it is only known once the function returns, so record it from the return value instead.
  • agent_name - The agent an AGENT span invokes (gen_ai.agent.name). Unset, it falls back to the agent name init() was given, and is never taken from the function or the span name: a function name is not an agent’s identity. Ignored for other kinds. A TOOL span carries gen_ai.agent.name too, and there it means the agent EXECUTING the tool: the innermost enclosing kind=AGENT scope, else the configured agent name. It is never an argument — the executor is where the call happened, not something a tool declares about itself.
  • agent_id - The identifier of a HOSTED agent resource (gen_ai.agent.id), such as a Bedrock agent ARN. The conventions advise against recording a transient in-memory instance id here, so an in-process agent leaves it unset.
  • agent_version - The version of the agent definition an AGENT span invoked (gen_ai.agent.version) — its prompt, tools and policy. Taken verbatim; the conventions’ own examples are "1.0.0" and "2025-05-01", so there is no format to validate. Never derived from service.version, which versions the deployed process, nor from the main agent’s version, which versions the agent this process IS rather than the one it called: different agent, different scope. Ignored for other kinds.
  • tool_call_id - The id the MODEL put on the tool-call message this TOOL span answers (gen_ai.tool.call.id), which joins the tool span back to the generation that requested it. Never derived: only the caller has seen the model’s response. Ignored for other kinds, and it never becomes part of the span name.
  • tool_type - What kind of tool ran (gen_ai.tool.type), the conventions’ examples being "function" (client-side), "extension" (agent-side) and "datastore". A fact about the tool’s declaration rather than about the call, so it is supplied rather than guessed. Ignored for other kinds.

Returns:

The wrapped function (or a decorator, when used parameterized).

rius.spans

Manual span API.

Two surfaces, following the OpenTelemetry / Langfuse / Laminar convention:

  • start_as_current_span is the context manager: it activates the span in the OTel context (so children nest under it) and auto-ends it.
  • start_span is manual: it returns an Observation you must .end(). The span is parented to the current span at creation but is NOT set as current and does NOT auto-record exceptions. For lifetimes a with block can’t express (streaming, callbacks, passing a span across boundaries).

start_generation / start_as_current_generation are the LLM-specialized equivalents.

Observation Objects

class Observation()

Handle for annotating a span from start_span / start_as_current_span.

Wraps an OpenTelemetry span and exposes the annotation surface for generic (non-LLM) spans: input, output, and arbitrary attributes. Inputs and outputs are serialized to JSON (with a repr fallback) and truncated at 32 KB.

set_input

def set_input(value: Any) -> None

Record the span input (the input.value attribute).

Arguments:

  • value - Any value; serialized to JSON with a repr fallback.

set_output

def set_output(value: Any) -> None

Record the span output (the output.value attribute).

Arguments:

  • value - Any value; serialized to JSON with a repr fallback.

set_retrieved_documents

def set_retrieved_documents(documents: Any) -> None

Record what a retrieval returned (gen_ai.retrieval.documents).

The conventions define this as an array of objects, each carrying an optional id and an optional score. Identifiers and relevance, never document text, which is why the spec does not mark it sensitive and why it is not treated as content here: it survives capture_content=False the way token counts do. Put the retrieved text in :meth:set_output if you want it captured, and it will be masked and stripped like every other content attribute.

Unlike the data source and top_k, which describe the request and are passed at span creation, this is only knowable once the search has run, so it never reaches a pending snapshot.

Arguments:

  • documents - A sequence of {"id": ..., "score": ...} mappings; serialized to JSON, since OTel attributes carry no structure.

set_attribute

def set_attribute(key: str, value: Any) -> None

Set an arbitrary attribute on the underlying span.

Arguments:

  • key - Attribute name.
  • value - An OpenTelemetry-compatible attribute value.

update

def update(*, input: Any = None, output: Any = None) -> None

Record input and/or output in one call.

Arguments:

  • input - When not None, forwarded to :meth:set_input.
  • output - When not None, forwarded to :meth:set_output.

record_exception

def record_exception(exc: BaseException) -> None

Record a failure on this span: event, ERROR status and error.type.

start_span does not auto-record exceptions, so a manual handle has to be told. Recording the failure through the underlying span object instead leaves error.type unset, and the span then groups differently from every other error span the SDK emits.

Arguments:

  • exc - The exception that ended the operation. error.type is set to its class, module-qualified unless it is a builtin, which is the same spelling the exception event uses.

end

def end() -> None

End the underlying span.

Required for spans created with start_span; spans from start_as_current_span end automatically when the block exits.

start_span

def start_span(name: str | None = None, *, kind: SpanKind = SpanKind.CHAIN, input: Any = None, user_id: str | None = None, tool_name: str | None = None, data_source_id: str | None = None, top_k: int | None = None, agent_name: str | None = None, agent_id: str | None = None, agent_version: str | None = None, tool_call_id: str | None = None, tool_type: str | None = None) -> Observation

Create a span and return an Observation. You MUST call .end().

The span is parented to the current span at creation, but is not set as the current span and does not auto-record exceptions. Use start_as_current_span for block-scoped tracing.

name is optional. Left out, the span is named the way the GenAI conventions say it should be: the operation the kind maps to, followed by what it acted on — execute_tool get_weather, invoke_agent planner, retrieval product-kb — falling back to the bare operation when that identifier is unknown. A CHAIN span is the one degenerate case: the conventions define no operation for a generic step and this surface has no function to borrow a qualname from, so an unnamed one is called chain. An explicit name always wins.

user_id stamps user.id on this span only; it is sugar for a span that has no children of its own. To attribute a whole request, including auto-instrumented spans, use the user() scope instead.

tool_name sets gen_ai.tool.name on a TOOL span; it defaults to the span name, and exists so a span name that is not the bare tool name does not become one.

tool_call_id is the id the MODEL put on the tool-call message this TOOL span answers (gen_ai.tool.call.id), which is what joins the tool span back to the generation that requested it; tool_type is what kind of tool ran (gen_ai.tool.type: "function", "extension", "datastore", …). Both are set at creation, so a still-running tool call is already attributable, and neither is ever derived: only the caller has seen the model’s response, and neither changes the span name. Both are ignored on every other kind.

data_source_id names the index, collection or knowledge base a RETRIEVER span searched (gen_ai.data_source.id), and top_k how many documents it asked for (gen_ai.retrieval.top_k). Both are set at creation, so a still-running retrieval is already attributable to its source. What came back is recorded afterwards with Observation.set_retrieved_documents.

agent_name names the agent an AGENT span INVOKES, which is what gen_ai.agent.name means on an invoke-agent span. Unset, it falls back to the agent name init() was given, and is never taken from the span name. agent_id is for a HOSTED agent resource, such as a Bedrock agent ARN; the conventions advise against putting a transient in-memory instance id there, so an in-process agent leaves it unset. agent_version is that same invoked agent’s definition version (gen_ai.agent.version), taken verbatim — the conventions’ examples are "1.0.0" and "2025-05-01", so there is no format to validate — and never derived from service.version or from the process’s own main-agent version: different agent, different scope. All three are ignored on every other kind.

A TOOL span carries gen_ai.agent.name too, and there it means the agent EXECUTING the tool: the innermost enclosing agent scope, else the configured agent name. This surface does not OPEN such a scope, because it does not activate context at all — an agent span from start_span does not lend its name to the tool spans that follow it, the same limitation user_id has here. Use start_as_current_span for that.

start_as_current_span

@contextmanager def start_as_current_span( name: str | None = None, *, kind: SpanKind = SpanKind.CHAIN, input: Any = None, user_id: str | None = None, tool_name: str | None = None, data_source_id: str | None = None, top_k: int | None = None, agent_name: str | None = None, agent_id: str | None = None, agent_version: str | None = None, tool_call_id: str | None = None, tool_type: str | None = None) -> Iterator[Observation]

Open a span as the current span and yield an Observation; auto-ends.

Exceptions raised in the block are recorded and set the span status to ERROR (OpenTelemetry’s start_as_current_span default), then re-raised.

name is optional. Left out, the span is named the way the GenAI conventions say it should be: the operation the kind maps to, followed by what it acted on — execute_tool get_weather, invoke_agent planner, retrieval product-kb — falling back to the bare operation when that identifier is unknown. A CHAIN span is the one degenerate case: the conventions define no operation for a generic step and this surface has no function to borrow a qualname from, so an unnamed one is called chain. An explicit name always wins.

user_id is sugar for wrapping the block in user(user_id): this span and every span opened inside the block carry user.id.

tool_name sets gen_ai.tool.name on a TOOL span; it defaults to the span name, and exists so a span name that is not the bare tool name does not become one.

tool_call_id is the id the MODEL put on the tool-call message this TOOL span answers (gen_ai.tool.call.id), which is what joins the tool span back to the generation that requested it; tool_type is what kind of tool ran (gen_ai.tool.type: "function", "extension", "datastore", …). Both are set at creation, so a still-running tool call is already attributable, and neither is ever derived: only the caller has seen the model’s response, and neither changes the span name. Both are ignored on every other kind.

data_source_id names the index, collection or knowledge base a RETRIEVER span searched (gen_ai.data_source.id), and top_k how many documents it asked for (gen_ai.retrieval.top_k). Both are set at creation, so a still-running retrieval is already attributable to its source. What came back is recorded afterwards with Observation.set_retrieved_documents.

agent_name names the agent an AGENT span INVOKES, which is what gen_ai.agent.name means on an invoke-agent span. Unset, it falls back to the agent name init() was given, and is never taken from the span name. agent_id is for a HOSTED agent resource, such as a Bedrock agent ARN; the conventions advise against putting a transient in-memory instance id there, so an in-process agent leaves it unset. agent_version is that same invoked agent’s definition version (gen_ai.agent.version), taken verbatim — the conventions’ examples are "1.0.0" and "2025-05-01", so there is no format to validate — and never derived from service.version or from the process’s own main-agent version: different agent, different scope. All three are ignored on every other kind.

A TOOL span carries gen_ai.agent.name too, and there it means the agent EXECUTING the tool. An AGENT block opened here scopes its resolved agent name over everything inside it, so every tool span in the block — including MCP tool calls — names it as the executor; nested agent blocks override outer ones. Without any enclosing block the configured agent name applies, and a process that named nothing emits nothing.

rius.generation

LLM generation capture helpers.

start_as_current_generation (context manager) and start_generation (manual, requires .end()) open an LLM-kind span and return a Generation handle for recording gen_ai-native attributes (messages, model, usage, finish reason). LLM spans are therefore readable by any gen_ai-compatible consumer.

normalize_messages

def normalize_messages(messages: Messages, default_role: str) -> list[dict[str, Any]]

Normalize to the spec message-array shape (a list of {"role", "parts"}).

A bare string becomes a single message with default_role. This is the one place messages are normalized: the content attribute and the context sizes are both derived from its result.

Generation Objects

class Generation()

Handle for recording gen_ai attributes on an LLM span.

Returned by start_generation and start_as_current_generation. Messages passed to :meth:set_input / :meth:set_output are normalized to the GenAI message shape ({"role", "parts": [...]}): bare strings, OpenAI-style dicts (including tool_calls and tool responses), and multimodal content lists are all accepted.

set_input

def set_input(messages: Messages) -> None

Record the request messages (gen_ai.input.messages).

Arguments:

  • messages - A string or list of messages in any supported format; bare strings default to the user role.

set_output

def set_output(messages: Messages) -> None

Record the response messages (gen_ai.output.messages).

Arguments:

  • messages - A string or list of messages in any supported format; bare strings default to the assistant role.

set_tool_definitions

def set_tool_definitions(tools: list[Any]) -> None

Record the request’s tool/function definitions (gen_ai.tool.definitions).

Definitions are serialized verbatim, in whatever shape the provider request used (OpenAI nests each tool under function, Anthropic uses top-level name/input_schema) — no normalization, so what is recorded is exactly what the model was shown. Definitions are content, not identity: they are masked/stripped under capture_content=False like messages are.

Arguments:

  • tools - The tools/functions list passed to the provider, verbatim.

set_response_model

def set_response_model(response_model: str) -> None

Record the model that produced the response (gen_ai.response.model).

Arguments:

  • response_model - Model identifier as reported by the provider, which may differ from the requested model.

set_response_id

def set_response_id(response_id: str) -> None

Record the provider’s id for this completion (gen_ai.response.id).

A setter rather than a creation argument for the reason :meth:set_response_model is one: the id is minted by the provider, so it does not exist when the span (or its pending snapshot) starts. It is metadata, not content, and survives capture_content=False.

Arguments:

  • response_id - The completion identifier the provider reported, e.g. OpenAI’s chatcmpl-123. Recorded verbatim.

set_usage

def set_usage(*, input_tokens: int | None = None, output_tokens: int | None = None, cache_read_input_tokens: int | None = None, cache_write_input_tokens: int | None = None, reasoning_output_tokens: int | None = None) -> None

Record token usage (gen_ai.usage.* attributes).

Send token counts, never cost: cost is computed server-side from model pricing.

Pass provider-reported values as-is; never pre-add anything. Per the GenAI conventions, gen_ai.usage.input_tokens is the total including cached tokens (the cache counts are subsets of it). Anthropic’s API reports input_tokens excluding the cache counts, and the conventions require the instrumentation to do the summing, so when the generation’s provider is "anthropic" the emitted total is input_tokens plus both cache counts. For every other provider the values are recorded verbatim.

Arguments:

  • input_tokens - Prompt tokens consumed, when known.
  • output_tokens - Completion tokens produced, when known.
  • cache_read_input_tokens - Input tokens served from a provider-managed prompt cache (gen_ai.usage.cache_read.input_tokens).
  • cache_write_input_tokens - Input tokens written to a provider-managed prompt cache (gen_ai.usage.cache_write.input_tokens, called “cache creation” by Anthropic).
  • reasoning_output_tokens - Output tokens spent on reasoning / extended thinking (gen_ai.usage.reasoning.output_tokens). A subset of output_tokens, never in addition to it: providers already include reasoning tokens in the output total, so pass both as reported and do no arithmetic.

record_first_token

def record_first_token() -> None

Mark the arrival of the first streamed token.

Call from a streaming loop when the first content chunk arrives. Records the gen_ai.first_token event (the timestamp the backend derives time-to-first-token from) and, per the GenAI conventions, the derived gen_ai.response.time_to_first_chunk (seconds since span start) plus gen_ai.request.stream = True: a first chunk arriving is what tells the SDK the request streamed. Idempotent: only the first call records; safe to call unconditionally per chunk. A no-op after end().

set_finish_reasons

def set_finish_reasons(reasons: str | list[str]) -> None

Record why generation stopped (gen_ai.response.finish_reasons).

Arguments:

  • reasons - A single reason (wrapped into a list) or a list of reasons, e.g. "stop", "length", "tool_calls".

update

def update(*, input: Messages | None = None, output: Messages | None = None) -> None

Record input and/or output messages in one call.

Arguments:

  • input - When not None, forwarded to :meth:set_input.
  • output - When not None, forwarded to :meth:set_output.

record_exception

def record_exception(exc: BaseException) -> None

Record a failure on this span: event, ERROR status and error.type.

start_generation does not auto-record exceptions, so a manual handle has to be told. Recording the failure through the underlying span object instead leaves error.type unset, and the span then groups differently from every other error span the SDK emits.

Arguments:

  • exc - The exception that ended the call. error.type is set to its class, module-qualified unless it is a builtin, which is the same spelling the exception event uses.

end

def end() -> None

End the underlying span.

Required for generations created with start_generation; spans from start_as_current_generation end automatically when the block exits.

start_generation

def start_generation(name: str | None = None, *, model: str | None = None, provider: str | None = None, input: Messages | None = None, model_parameters: dict[str, Any] | None = None, operation: str = "chat", reasoning_level: str | None = None, tools: list[Any] | None = None, user_id: str | None = None, output_type: str | None = None) -> Generation

Create an LLM-kind span and return a Generation. You MUST call .end().

Manual counterpart to start_as_current_generation: not set as current, no auto-recording of exceptions.

Arguments:

  • name - Span name. Optional: left out, the span is named "{operation} {model}" per the GenAI conventions — chat gpt-4o — falling back to the bare operation when no model was given. Composed from the REQUEST model, never the response model: the response model can arrive after the span (and its pending snapshot) started, and the two must carry the same name.
  • model - Requested model (gen_ai.request.model).
  • provider - Provider name (gen_ai.provider.name), e.g. "openai".
  • input - Request messages, recorded immediately via set_input.
  • model_parameters - Request parameters, recorded at span creation so they ride pending snapshots. A parameter the GenAI conventions define — under its canonical name or a recognised provider spelling, e.g. OpenAI’s max_completion_tokens — is recorded under its canonical gen_ai.request.* key and only that one. Everything else is recorded under rius.request.<key>, our own namespace, with the key otherwise untouched. Values that are not scalars or homogeneous scalar arrays are JSON-encoded; None means “not set” and is skipped.
  • operation - Operation name (gen_ai.operation.name); default "chat".
  • reasoning_level - Requested reasoning/thinking effort level (gen_ai.request.reasoning.level), e.g. OpenAI’s reasoning.effort values. Provider-defined string, recorded verbatim.
  • tools - The request’s tool/function definitions, recorded immediately via set_tool_definitions (verbatim, any provider shape).
  • user_id - End-user identity (user.id) stamped on this span. Sugar for a single call; to attribute a whole request use user().
  • output_type - The output modality the request asked for (gen_ai.output.type) — "text", "json", "image" or "speech" in the conventions today. Set at creation, since it describes the request; recorded verbatim, so a modality the conventions have not enumerated yet is kept rather than dropped. What the provider actually returned is a separate question the attribute does not answer.

Returns:

A Generation handle; call .end() when the call completes.

start_as_current_generation

@contextmanager def start_as_current_generation( name: str | None = None, *, model: str | None = None, provider: str | None = None, input: Messages | None = None, model_parameters: dict[str, Any] | None = None, operation: str = "chat", reasoning_level: str | None = None, tools: list[Any] | None = None, user_id: str | None = None, output_type: str | None = None) -> Iterator[Generation]

Open an LLM-kind span as the current span and yield a Generation; auto-ends.

Children created inside the block nest under this span, and exceptions raised in the block are recorded with ERROR status, then re-raised. Accepts the same arguments as start_generation.

Yields:

A Generation handle for recording messages, usage, and response metadata; the span ends when the block exits.

rius.config

Configuration resolution for the GlassFlow SDK.

Values are resolved with the precedence: explicit arguments > environment variables > built-in defaults.

GlassflowConfig Objects

@dataclass(frozen=True) class GlassflowConfig()

Resolved, immutable SDK configuration.

Produced by resolve_config (arguments over environment over defaults); consumed by init and build_span_exporter.

traces_endpoint

@property def traces_endpoint() -> str

Full OTLP/HTTP traces URL (<endpoint>/v1/traces).

heartbeat_endpoint

@property def heartbeat_endpoint() -> str

Heartbeat URL (<endpoint>/v1/heartbeat), same host as traces.

resolve_config

def resolve_config( *, endpoint: str | None = None, api_key: str | None = None, service_name: str | None = None, service_version: str | None = None, headers: dict[str, str] | None = None, disabled: bool | None = None, sample_rate: float | None = None, capture_content: bool | None = None, heartbeat: bool | None = None, heartbeat_interval: float | None = None, agent_name: str | None = None, main_agent_id: str | None = None, main_agent_description: str | None = None, main_agent_version: str | None = None, partial_spans: bool | None = None, partial_spans_delay: float | None = None, session_id: str | None = None, bridge_foreign_provider: bool | None = None) -> GlassflowConfig

Resolve SDK configuration from arguments, environment, then defaults.

Explicit arguments win over RIUS_* environment variables, which win over built-in defaults. sample_rate ([0.0, 1.0]), heartbeat_interval ([5, 300]) and partial_spans_delay ([0, 60]) are clamped with a warning, and a NaN or infinite value for any of them means “unset”; boolean environment variables accept 1/true/yes/on (case-insensitive).

Arguments:

  • endpoint - Base OTLP endpoint (RIUS_ENDPOINT).
  • api_key - Bearer token for the managed platform (RIUS_API_KEY); None sends no Authorization header.
  • service_name - service.name resource attribute (RIUS_SERVICE_NAME).
  • service_version - service.version resource attribute (RIUS_SERVICE_VERSION). Unset means unset: there is no placeholder, and the attribute is then left off the resource entirely, where OTEL_RESOURCE_ATTRIBUTES may still supply one.
  • headers - Extra exporter headers; an explicit Authorization entry wins over api_key.
  • disabled - Kill switch (RIUS_DISABLED); spans are dropped in-process.
  • sample_rate - Head-sampling ratio for root traces (RIUS_SAMPLE_RATE).
  • capture_content - When False, content attributes are stripped at export (RIUS_CAPTURE_CONTENT).
  • heartbeat - Enable the agent-lifetime heartbeat thread (RIUS_HEARTBEAT). On by default; set False or RIUS_HEARTBEAT=false to opt out.
  • heartbeat_interval - Seconds between pings (RIUS_HEARTBEAT_INTERVAL), clamped to [5, 300]; the backend derives staleness from this, so the bounds are part of the wire contract.
  • agent_name - Identity both heartbeats and spans group under (RIUS_AGENT_NAME), stamped on the resource as gen_ai.agent.name; defaults to service_name so the agents view and the traces view agree on what an “agent” is. Also stamped as rius.main_agent.name, under the same placeholder suppression the span helpers apply.
  • main_agent_id - Stable identifier of the agent this process IS (RIUS_MAIN_AGENT_ID), stamped as rius.main_agent.id. It must be STABLE across restarts and deployments — a transient in-memory or process-local id must not be used, exactly as the conventions say for gen_ai.agent.id. Unset means unset; there is no placeholder.
  • main_agent_description - What that agent does (RIUS_MAIN_AGENT_DESCRIPTION), stamped as rius.main_agent.description. Unset means unset.
  • main_agent_version - Version of the agent DEFINITION — its prompt, tools and policy (RIUS_MAIN_AGENT_VERSION), stamped as rius.main_agent.version. Deliberately NOT service.version and never derived from it: a service can sit at 2.3.1 while its agent definition is at 7, and the two move independently. Unset means unset.
  • partial_spans - Export a content-free pending snapshot of every sampled span at span START (RIUS_PARTIAL_SPANS), so in-flight work is visible and crashes leave a record. Off by default until the backend’s unfinished-spans storage ships.
  • partial_spans_delay - Debounce for pending snapshots (RIUS_PARTIAL_SPANS_DELAY), clamped to [0, 60] seconds. 0 (default) emits at span start; N emits only if the span is still open after N seconds; spans that finish sooner cost no network at all.
  • session_id - Process-wide session id (RIUS_SESSION_ID), stamped as session.id on every span. For one-run-per-process agents; a server handling many sessions uses the rius.session() scope instead, which overrides this default. Unset means spans outside a scope carry no session id and are grouped per trace.
  • bridge_foreign_provider - When another SDK already holds the OpenTelemetry global provider at init(), attach rius’s span pipeline to that provider too (RIUS_BRIDGE_FOREIGN_PROVIDER). Off by default: rius then exports that SDK’s spans, which is consent the caller gives explicitly. Off, rius keeps to its own provider and flags the parents it never receives.

Returns:

The resolved, immutable GlassflowConfig.

rius.semconv

Semantic conventions for the GlassFlow SDK.

Centralizes the OpenTelemetry instrumentation-scope name, the span-kind taxonomy, and span attribute keys. Span kinds use the OpenInference openinference.span.kind values (understood across the ecosystem); LLM specifics use OTel GenAI gen_ai.*.

request_attribute_key

def request_attribute_key(parameter: str) -> str

The attribute key one caller-supplied request parameter is recorded under.

A parameter the GenAI conventions define — under its canonical name or a recognised provider spelling — normalises to its canonical gen_ai.request.* key. Everything else keeps its key verbatim under rius.request..

SpanKind Objects

class SpanKind(str, Enum)

Observation kind. Values are OpenInference openinference.span.kind values.

  • AGENT: an agent invocation or run
  • LLM: a model call (generations use this)
  • TOOL: a tool execution
  • RETRIEVER: a retrieval / search step
  • EMBEDDING: an embedding computation
  • CHAIN: a generic processing step (the default)

operation_for_kind

def operation_for_kind(kind: str) -> str | None

gen_ai.operation.name for a taxonomy value, or None if it has none.

Takes the raw attribute string rather than the enum: the caller is normalizing a third-party span, where the value is whatever the instrumentation wrote. An unrecognised one yields None rather than raising, because a span carrying a kind we do not know is not a reason to lose the span.

kind_for_operation

def kind_for_operation(operation: str) -> str | None

The taxonomy value for a gen_ai.operation.name, or None if unknown.

otel_span_kind

def otel_span_kind(kind: SpanKind) -> OtelSpanKind

The OpenTelemetry SpanKind field a span of taxonomy kind should carry.

compose_span_name

def compose_span_name(kind: SpanKind, attributes: Mapping[str, str | int]) -> str

The span name for a span of kind: "{operation} {target}".

The GenAI conventions give every operation they define a SHOULD-level span name of the operation followed by the one identifier saying what it acted on: chat gpt-4o, embeddings text-embedding-3-small, execute_tool get_weather, invoke_agent planner, retrieval kb.

Composed from the span’s OWN creation attributes rather than from the caller’s arguments, which is what makes the name and the attributes agree by construction instead of by discipline. Three things follow for free: an operation overridden per generation is picked up, because the override is already in the map; an agent name that fell back to the configured one is picked up for the same reason; and the rendered execute_tool x can never become gen_ai.tool.name, because this function only ever reads. The TypeScript SDK composes the identical way, so the two cannot drift as new identifiers are added.

The model read here is the REQUEST model, since that is the only one in the creation attributes. A response model arriving later would make a pending snapshot and its final span disagree on their name, and that equality is part of the pending wire contract.

The identifier is optional on every kind, and the name degrades to the bare operation rather than to a name with a hole in it. CHAIN has no operation at all and falls back to :data:CHAIN_SPAN_NAME.

kind_attributes

def kind_attributes(kind: SpanKind, tool_name: str | None = None, *, data_source_id: str | None = None, top_k: int | None = None, agent_name: str | None = None, agent_id: str | None = None, agent_version: str | None = None, executing_agent_name: str | None = None, tool_call_id: str | None = None, tool_type: str | None = None) -> dict[str, str | int]

Identity attributes for a span of kind, for setting at CREATION.

Pending snapshots (pending.py) are built at on_start, so taxonomy set via set_attribute afterwards is invisible to them; passing these at span creation is what makes a pending span classifiable.

tool_name is the tool’s own name, which the GenAI execute-tool convention requires as gen_ai.tool.name on a TOOL span. It is deliberately NOT the span name: the two coincide today but stop coinciding as soon as span names carry an operation prefix, and the tool name feeds the MCP detection predicate, the tool-loop analysis and the tool-arguments fingerprint, none of which tolerate a prefix.

data_source_id is the index or collection a RETRIEVER span searched (gen_ai.data_source.id), and top_k how many documents it asked for (gen_ai.retrieval.top_k). Only the caller knows either, so both are arguments rather than something derived. What the search RETURNED is not here: it is unknown at creation, and set through the observation.

agent_name and agent_id identify the agent an AGENT span invokes, which the conventions make Conditionally Required on an invoke-agent span. Neither is ever derived from the span name: unlike a tool, whose name and span name coincided historically, a function name is not an agent’s identity, and guessing one mislabels every span beneath it. The caller supplies them, or the span helper falls back to the configured agent name.

agent_version is gen_ai.agent.version, the version of that SAME invoked agent’s definition. AGENT-only, caller-supplied, taken verbatim, and never derived: not from service.version, which versions the deployed process, and not from the main agent’s version, which versions the agent this process IS rather than the one it just called. Different agent, different scope; borrowing one for the other is confidently wrong data. It does not touch the span NAME either — _NAME_TARGET_BY_KIND maps AGENT to gen_ai.agent.name alone.

tool_call_id is gen_ai.tool.call.id, the identifier the MODEL put on the tool-call message this execution answers, and tool_type is gen_ai.tool.type (“function”, “extension”, “datastore”, …). Both are TOOL-only, both are caller-supplied, and neither is ever guessed: only the caller has seen the model’s response, and a tool’s type is a fact about its declaration rather than about the call. Neither touches the span NAME, because _NAME_TARGET_BY_KIND maps TOOL to gen_ai.tool.name alone.

executing_agent_name is a SEPARATE argument writing the SAME key, gen_ai.agent.name, on a TOOL span — where the conventions define it as “the human-readable name of the agent executing the tool”. One key, two meanings, told apart by gen_ai.operation.name: on invoke_agent it is the agent being invoked, on execute_tool the agent doing the call. Two arguments rather than one precisely so the two can never be fed from the same source by accident; the TOOL one is never passed by a caller, it is resolved from the enclosing agent scope (see _agent). It does not touch the span NAME: _NAME_TARGET_BY_KIND maps TOOL to gen_ai.tool.name, so a tool span stays execute_tool {tool}.

set_span_kind

def set_span_kind(span: Span, kind: SpanKind) -> None

Stamp a span with its OpenInference kind and (if applicable) gen_ai operation.

Last updated on