How do I switch from Arize Phoenix to Rius for CrewAI tracing?
To switch CrewAI tracing from Arize Phoenix to Rius, replace phoenix.otel.register() with rius.init() and wrap your crew entrypoint and tools with observe. CrewAI’s model calls then arrive in Rius as generations with model, tokens and cost, and your tool calls appear as their own spans. You can run both backends side by side while you compare them.
How it works
CrewAI 1.x drives the OpenAI client natively, so the Rius openai integration captures its model calls. If LiteLLM is installed, CrewAI uses it as a fallback, and you rely on the litellm integration instead. rius.init() turns on every supported integration it finds installed, so the auto_instrument=True flag and any OpenAIInstrumentor().instrument() calls of your own go away together with register().
The crew structure runs inside the framework, where auto-instrumentation cannot see it. That is why you add observe wrappers: one around the function that calls crew.kickoff(), and one on each tool body.
The Phoenix concepts map across like this:
project_namebecomesservice_name. Rius names the agent afterservice.name.using_sessionbecomesrius.session, andusing_userbecomesrius.user.- Cost is computed by Rius from the model and token counts. You never send a cost value.
The CrewAI integration page covers the framework in full, and the integrations overview lists everything else the SDK covers.
Set it up
-
Swap the packages. Python 3.10 or newer is required. Create a workspace API key in the console and export it.
pip uninstall arize-phoenix-otel pip install "glassflow-rius[openai]" crewai export RIUS_API_KEY="<your API key>" -
Replace the Phoenix setup. Your code probably looks like this today:
from phoenix.otel import register from openinference.instrumentation import using_session register(project_name="my-agent", auto_instrument=True) with using_session(conversation_id): run_agent(message) -
Initialise Rius, wrap the entrypoint with the AGENT kind, and trace each tool with the TOOL kind. Put
@tooloutermost andobserveon the function body. Userius.sessionin place ofusing_session, because the OpenInference context managers do not stamp spans thatobservecreates.import rius from crewai import Agent, Crew, Task from crewai.tools import tool from rius import SpanKind rius.init(service_name="my-agent") @tool("account_status") @rius.observe(kind=SpanKind.TOOL) def account_status(email: str) -> str: """Fetch a customer's account standing by email.""" return lookup(email) support_agent = Agent( role="Support specialist", goal="Answer support tickets accurately and concisely.", backstory="You resolve product and account questions.", tools=[account_status], llm="gpt-5-mini", ) @rius.observe(name="handle-ticket", kind=SpanKind.AGENT) def handle_ticket(ticket: str) -> str: task = Task( description=f"Resolve this support ticket: {ticket}", expected_output="A short, direct answer.", agent=support_agent, ) return str(Crew(agents=[support_agent], tasks=[task]).kickoff()) with rius.session(conversation_id): handle_ticket(ticket) -
Set
CREWAI_TRACING_ENABLED=falsewherever no terminal is attached, such as CI or containers. CrewAI otherwise asks about its own tracing product on first run. -
Run one ticket that calls a tool and open the trace in the console. Check the root span, the model calls with tokens and cost, and your named tool span.
To keep Phoenix running during a comparison, start there instead of with step 1: keep arize-phoenix-otel and register(), add Rius as a second span processor as the side-by-side steps show, then make the swap above once Rius shows what you need.
What you get
A run that calls one tool looks like this:
handle-ticket AGENT 8.4s
├─ ChatCompletion LLM 3.9s agent decides to call the tool
├─ execute_tool account_status TOOL 0ms
└─ ChatCompletion LLM 4.3s agent answers- A root AGENT span from your
observewrapper. - A generation for each model call, with model, tokens, cost and time to first token. Costs are list-price estimates in US dollars.
- A TOOL span for each traced tool.
- Traces grouped under the session you set with
rius.session.
Two things differ from what you may expect. Crews and tasks produce no spans of their own, so wrap per-task entrypoints with observe if you need to see which task a call belongs to. Generation spans are named ChatCompletion. Streamed calls carry token counts only when usage is enabled on the underlying client.
When something else fits
- If you want the smallest diff, keep your OpenInference instrumentation and only re-point the exporter at Rius. The Phoenix migration guide shows how, and the Migrate to Rius overview covers other starting points.
- Rius has no evals, datasets, annotations, prompt management or import of historical Phoenix traces. If you depend on those, keep Phoenix for them and send new traces to both.