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) -> SpanExporterBuild the default OTLP/HTTP span exporter for a resolved config.
Arguments:
config- A resolved configuration; the exporter posts toconfig.traces_endpointwithconfig.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) -> NoneAdd (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.TracerReturn 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) -> boolForce-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() -> NoneDrain 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) -> GlassflowClientInitialize 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 anAuthorization: Bearerheader.service_name- Value for theservice.nameresource attribute.service_version- Value for theservice.versionresource attribute, e.g. the release or image tag this process is running. Resolution order is this argument, thenRIUS_SERVICE_VERSION, then whateverOTEL_RESOURCE_ATTRIBUTESsupplies (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 withset_global=Falsethey are only enabled wheninstrumentsis 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/heartbeatfrom init until process exit so the platform can tell a live-but-idle agent from a vanished one. Eachinit()mints one instance id, sent both in heartbeat payloads and on every span as theservice.instance.idresource attribute, so the platform can join the two and count replicas. Fork caveat: a child forked afterinit()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, callinit()after the fork (e.g. in gunicorn’spost_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 asrius.main_agent.nameAND, unchanged, asgen_ai.agent.name; defaults toservice_name. Therius.main_agent.*family is where the process-level question “which agent is this?” lives:gen_ai.agent.namehas 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 theunknown_serviceplaceholder emits norius.main_agent.nameat all, the same suppression the span helpers apply.main_agent_id- Stable identifier of the agent this process IS (RIUS_MAIN_AGENT_ID), stamped asrius.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 ofgen_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 asrius.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 asrius.main_agent.version. Deliberately NOTservice.versionand 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, likespan_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 assession.idon 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 withrius.session()instead, which overrides this default.workspaces- Enable multi-workspace routing: a mapping of alias to API key. Spans started insiderius.workspace(alias)are exported with that workspace’s key; spans outside any scope use the defaultapi_key. Pass{}to opt in with no static routes and register destinations later viaregister_workspace(). One trace must stay inside one workspace; seerius.workspace.workspace_exporter_factory- Override how per-workspace exporters are built from an API key (useful for testing, likespan_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, flaggedrius.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 asrius.sdk.global_provider. Applies to a globalinit()only.
get_tracer
def get_tracer(name: str = TRACER_NAME) -> trace.TracerReturn 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) -> NoneAdd (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) -> AnyDecorate 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@observeusage).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. ACHAINspan has no operation and keeps the function’s__qualname__.capture_input- Record call arguments as JSON ininput.value.capture_output- Record the return value as JSON inoutput.value.kind- Span taxonomy (openinference.span.kind); defaultCHAIN.tool_name- The tool’s own name (gen_ai.tool.name), used only whenkindisTOOL. Unset, it falls back tonameand then to the function’s__qualname__. Separate fromnamebecause the span name may carry an operation prefix and the tool name must not; the fallback throughnameexists 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 aRETRIEVERspan 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 anAGENTspan invokes (gen_ai.agent.name). Unset, it falls back to the agent nameinit()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. ATOOLspan carriesgen_ai.agent.nametoo, and there it means the agent EXECUTING the tool: the innermost enclosingkind=AGENTscope, 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 anAGENTspan 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 fromservice.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 thisTOOLspan 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_spanis the context manager: it activates the span in the OTel context (so children nest under it) and auto-ends it.start_spanis manual: it returns anObservationyou 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 awithblock 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) -> NoneRecord the span input (the input.value attribute).
Arguments:
value- Any value; serialized to JSON with areprfallback.
set_output
def set_output(value: Any) -> NoneRecord the span output (the output.value attribute).
Arguments:
value- Any value; serialized to JSON with areprfallback.
set_retrieved_documents
def set_retrieved_documents(documents: Any) -> NoneRecord 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) -> NoneSet 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) -> NoneRecord input and/or output in one call.
Arguments:
input- When notNone, forwarded to :meth:set_input.output- When notNone, forwarded to :meth:set_output.
record_exception
def record_exception(exc: BaseException) -> NoneRecord 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.typeis set to its class, module-qualified unless it is a builtin, which is the same spelling the exception event uses.
end
def end() -> NoneEnd 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) -> ObservationCreate 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) -> NoneRecord the request messages (gen_ai.input.messages).
Arguments:
messages- A string or list of messages in any supported format; bare strings default to theuserrole.
set_output
def set_output(messages: Messages) -> NoneRecord the response messages (gen_ai.output.messages).
Arguments:
messages- A string or list of messages in any supported format; bare strings default to theassistantrole.
set_tool_definitions
def set_tool_definitions(tools: list[Any]) -> NoneRecord 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) -> NoneRecord 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) -> NoneRecord 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’schatcmpl-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) -> NoneRecord 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 ofoutput_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() -> NoneMark 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]) -> NoneRecord 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) -> NoneRecord input and/or output messages in one call.
Arguments:
input- When notNone, forwarded to :meth:set_input.output- When notNone, forwarded to :meth:set_output.
record_exception
def record_exception(exc: BaseException) -> NoneRecord 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.typeis set to its class, module-qualified unless it is a builtin, which is the same spelling the exception event uses.
end
def end() -> NoneEnd 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) -> GenerationCreate 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 viaset_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’smax_completion_tokens— is recorded under its canonicalgen_ai.request.*key and only that one. Everything else is recorded underrius.request.<key>, our own namespace, with the key otherwise untouched. Values that are not scalars or homogeneous scalar arrays are JSON-encoded;Nonemeans “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’sreasoning.effortvalues. Provider-defined string, recorded verbatim.tools- The request’s tool/function definitions, recorded immediately viaset_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 useuser().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() -> strFull OTLP/HTTP traces URL (<endpoint>/v1/traces).
heartbeat_endpoint
@property
def heartbeat_endpoint() -> strHeartbeat 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) -> GlassflowConfigResolve 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);Nonesends no Authorization header.service_name-service.nameresource attribute (RIUS_SERVICE_NAME).service_version-service.versionresource attribute (RIUS_SERVICE_VERSION). Unset means unset: there is no placeholder, and the attribute is then left off the resource entirely, whereOTEL_RESOURCE_ATTRIBUTESmay still supply one.headers- Extra exporter headers; an explicitAuthorizationentry wins overapi_key.disabled- Kill switch (RIUS_DISABLED); spans are dropped in-process.sample_rate- Head-sampling ratio for root traces (RIUS_SAMPLE_RATE).capture_content- WhenFalse, content attributes are stripped at export (RIUS_CAPTURE_CONTENT).heartbeat- Enable the agent-lifetime heartbeat thread (RIUS_HEARTBEAT). On by default; setFalseorRIUS_HEARTBEAT=falseto 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 asgen_ai.agent.name; defaults toservice_nameso the agents view and the traces view agree on what an “agent” is. Also stamped asrius.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 asrius.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 forgen_ai.agent.id. Unset means unset; there is no placeholder.main_agent_description- What that agent does (RIUS_MAIN_AGENT_DESCRIPTION), stamped asrius.main_agent.description. Unset means unset.main_agent_version- Version of the agent DEFINITION — its prompt, tools and policy (RIUS_MAIN_AGENT_VERSION), stamped asrius.main_agent.version. Deliberately NOTservice.versionand 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;Nemits 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 assession.idon every span. For one-run-per-process agents; a server handling many sessions uses therius.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 atinit(), 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) -> strThe 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 runLLM: a model call (generations use this)TOOL: a tool executionRETRIEVER: a retrieval / search stepEMBEDDING: an embedding computationCHAIN: a generic processing step (the default)
operation_for_kind
def operation_for_kind(kind: str) -> str | Nonegen_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 | NoneThe taxonomy value for a gen_ai.operation.name, or None if unknown.
otel_span_kind
def otel_span_kind(kind: SpanKind) -> OtelSpanKindThe OpenTelemetry SpanKind field a span of taxonomy kind should carry.
compose_span_name
def compose_span_name(kind: SpanKind, attributes: Mapping[str,
str | int]) -> strThe 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) -> NoneStamp a span with its OpenInference kind and (if applicable) gen_ai operation.