I trace the Claude Agent SDK with Arize Phoenix. How do I switch to Rius?
You switch the Claude Agent SDK from Arize Phoenix to Rius by replacing register() with rius.init() and tracing each task with an observe root span and one generation. The Claude Agent SDK runs the Claude Code runtime in a separate process, so Rius coverage for it is explicit rather than automatic. You get one trace per task, with the model, tokens and cost on the model call. Cost is computed by Rius from the model and token counts.
How it works
The Claude Agent SDK drives the Claude Code runtime outside your process. No model client runs in your code, so Rius auto-instrumentation has nothing to hook. Rius covers it with two explicit pieces, described on the Claude Agent SDK integration page:
- An
observewrapper makes each task the rootAGENTspan. It replaces the agent decorator or span you used with Phoenix. - A manual generation wraps the
query()loop. You fill it from the run’sResultMessagewith the token usage, the real model id and the collected answer. Rius then computes cost server-side, as for any generation.
This is the one integration in the integrations list that works this way. Most others are picked up automatically once installed.
The Phoenix concepts map across like this:
register(project_name=...)becomesrius.init(service_name=...). Rius groups traces by the service name, not by the Phoenix project attribute.using_session(session_id)becomeswith rius.session(session_id):.@tracer.agentbecomes@rius.observe(kind=rius.SpanKind.AGENT).
The general Phoenix mapping and a cutover checklist are in Migrate from Arize Phoenix. The overview of all paths is Migrate to Rius.
Set it up
-
Swap the packages. The base SDK is enough, and no extra applies. The
claudeCLI must still be installed and authenticated.pip uninstall arize-phoenix-otel pip install glassflow-rius claude-agent-sdk -
Replace your Phoenix setup and agent wrapper with this. Delete the
register()call and any Phoenix instrumentor calls for this agent, so only Rius records the run. Put your own Rius API key whereri_...is.import rius from claude_agent_sdk import ( AssistantMessage, ClaudeAgentOptions, ResultMessage, TextBlock, query, ) from rius import SpanKind rius.init(api_key="ri_...", service_name="my-agent") MODEL = "claude-haiku-4-5" @rius.observe(name="handle-task", kind=SpanKind.AGENT) async def handle_task(task: str) -> str: options = ClaudeAgentOptions(model=MODEL, max_turns=1) parts = [] with rius.start_as_current_generation( "claude_agent_turn", model=MODEL, provider="anthropic", input=task ) as gen: async for message in query(prompt=task, options=options): if isinstance(message, AssistantMessage): parts.extend( b.text for b in message.content if isinstance(b, TextBlock) ) elif isinstance(message, ResultMessage): usage = message.usage or {} gen.set_usage( input_tokens=usage.get("input_tokens"), output_tokens=usage.get("output_tokens"), cache_read_input_tokens=usage.get("cache_read_input_tokens"), # Anthropic's payload still calls the write count "cache creation" cache_write_input_tokens=usage.get("cache_creation_input_tokens"), ) models = list((message.model_usage or {}).keys()) if models: gen.set_response_model(models[0]) answer = "".join(parts) gen.set_output(answer) return answer -
Use a full model id such as
claude-haiku-4-5, not an alias such ashaiku, for both the CLI option and the generation. Cost is computed from the requested model, and an alias gives a generation with no cost. -
Pass the cache counts from the
ResultMessageas they are, and keepprovider="anthropic"on the generation. Rius then adds the cache counts to the input tokens for you. Without the cache fields, cache-heavy agent loops would undercount badly. -
Replace
using_sessionwithrius.session. The Claude Agent SDK’s ownsession_idfor resumable conversations works well as the Rius session id.with rius.session(conversation_id): await handle_task(message) -
Run one task and open the trace in the console. You should see the root
AGENTspan and a generation with a full model id, tokens and cost.
What you get
Each task shows as one trace with this shape:
handle-task AGENT 5.0s
└─ claude_agent_turn LLM 5.0s claude-haiku-4-5, tokens + cost- A root agent span per task, named by your
observewrapper, with the generation under it. - Tokens and cost on the model call. Cost is a list-price estimate in US dollars, computed by Rius. You never send a cost value.
- Sessions. Traces from one conversation group under the session you set with
rius.session.
The generation records the turn’s totals only. Tool calls, sub-turns and file operations happen inside the Claude Code process and produce no spans, so they do not appear as steps in the trace. Auth stays the CLI’s: an exported ANTHROPIC_API_KEY takes precedence over a claude.ai login.
When something else fits
- If you rely on Phoenix evals, datasets, experiments, annotations or prompt management, keep Phoenix for those. Rius does not offer them, and the Phoenix migration guide shows how to send spans to both backends while you decide.
- If you need a span for every tool call inside the Claude Code runtime, note that Rius records the turn as one generation with totals and no inner spans.
- Rius cannot import your historical Phoenix traces. It starts from the first span you send.