Skip to content
Router One
Back to Blog

OpenAI Agents SDK vs Claude Agent SDK vs Pydantic AI vs CrewAI

PublishedByRouter One TeamHow we measure

Five agent frameworks, one question: what changes when their model calls go through Router One? The frameworks stay what they are — the OpenAI Agents SDK runs a loop with handoffs, guardrails and sessions; the Claude Agent SDK embeds Claude Code's engine and permissions in your app; Pydantic AI adds typed outputs; CrewAI runs role-based crews; LangChain builds agents on the LangGraph runtime. What changes is the request underneath: one sk- key, exact model ids from /models, and one trace per model request in Dashboard → Logs with model, tokens, cost, latency, status and request_id. The gateway serves the requests; the framework owns the loop.

The verdicts. OpenAI Agents SDK: an explicit Chat Completions model on a Router One client, tracing off. Claude Agent SDK: Claude Code's two variables, an exact Claude-family id, one /v1/messages trace per turn. Pydantic AI: OpenAIChatModel plus OpenAIProvider, the smallest surface of the five. CrewAI: custom_openai=True and the openai/ prefix rule. LangChain and LangGraph: ChatOpenAI with use_responses_api=False; LangGraph changes nothing about the request.

Side by side

SDKLanguagePoint it at the gatewayProtocol sentFamilies reachedStays in the SDK
OpenAI Agents SDKPython; TypeScript twinAsyncOpenAI(base_url=…, api_key=…) inside OpenAIChatCompletionsModel; set_tracing_disabled(True)Chat Completions (plain names default to Responses)Every chat model; Responses natively for GPT-family and DeepSeek idsFunction tools, handoffs, guardrails, sessions, tracing
Claude Agent SDKPython, TypeScriptClaudeAgentOptions(env={...}) with ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN, or the shellAnthropic Messages; the engine appends /v1/messagesCurrently listed Claude-family idsTool execution, permissions, hooks, sessions, subagents
Pydantic AIPythonOpenAIChatModel(id, provider=OpenAIProvider(base_url=…, api_key=…))Chat Completions (openai: shorthand means Responses)Every chat modelAgent loop, validation, tools, message history
CrewAIPythonLLM(model="openai/" + id, custom_openai=True, base_url=…, api_key=…) or the MODEL, OPENAI_API_BASE, OPENAI_API_KEY env varsChat Completions (api="responses" switches)Every chat modelAgent loop, task order, delegation, tools, memory
LangChain / LangGraphPython; JS/TS useResponsesApi: falseChatOpenAI(base_url=…, api_key=…, model=…, use_responses_api=False)Chat Completions (Responses when a Responses-only feature is used)Every chat modelGraphs, agent loop, tools, state and persistence

Four of the five speak Chat Completions, and /v1/chat/completions serves every chat model in the catalog, so the same anthropic/claude-sonnet-5 or openai/gpt-5.5 id works in each config below; the Claude Agent SDK's engine speaks Anthropic Messages and reaches Claude-family ids only. The last column is the boundary: nothing in it runs on the gateway.

OpenAI Agents SDK: an explicit Chat Completions model, tracing off

The SDK resolves a plain model name through its default provider on the Responses API, and uploads traces to OpenAI's servers with the key it uses for model calls. The guide sidesteps both: an AsyncOpenAI client with the Router One base URL and key, wrapped in OpenAIChatCompletionsModel so every request is POST /v1/chat/completions and any chat model id works, plus set_tracing_disabled(True), because a Router One key cannot stand in for a platform.openai.com key.

import os
from openai import AsyncOpenAI
from agents import Agent, OpenAIChatCompletionsModel, Runner, set_tracing_disabled

set_tracing_disabled(True)
client = AsyncOpenAI(
    base_url="https://api.router.one/v1",
    api_key=os.environ["ROUTER_ONE_API_KEY"],
)
model = OpenAIChatCompletionsModel(
    model=os.environ["ROUTER_ONE_MODEL_ID"],
    openai_client=client,
)

Agent(..., model=model) and Runner.run_sync(agent, prompt, max_turns=3) complete the file. Keep the Responses default only when every id your agents use lists /v1/responses on its model page and you want hosted tools or previous_response_id; a Claude, Gemini or Grok id sent there gets HTTP 400 model '<id>' must be called via … before any model is called (see the Responses API page).

The gotchas. OPENAI_AGENTS_DISABLE_TRACING=1 also turns tracing off; to keep the OpenAI Traces dashboard, give the exporter its own key with set_tracing_export_api_key(...) and pass use_for_tracing=False when registering the client globally. Guide: OpenAI Agents SDK + Router One.

Claude Agent SDK: two variables, an exact Claude id, one trace per turn

The SDK spawns the bundled Claude Code engine, which runs tools on your machine and sends each model turn as an Anthropic Messages request, so the setup is Claude Code's: ANTHROPIC_BASE_URL is the host root https://api.router.one without /v1, because the engine appends /v1/messages itself, and ANTHROPIC_AUTH_TOKEN carries your key as a Bearer header; ANTHROPIC_API_KEY is not required. Both go through env or the shell; the Python SDK merges env on top of the inherited environment.

options = ClaudeAgentOptions(
    model=os.environ["ROUTER_ONE_MODEL_ID"],
    env={
        "ANTHROPIC_BASE_URL": "https://api.router.one",
        "ANTHROPIC_AUTH_TOKEN": os.environ["ROUTER_ONE_API_KEY"],
    },
    max_turns=3,
)

query(prompt=..., options=options) yields the engine's messages. model must be an exact id from /models whose page lists POST /v1/messages, not an alias such as sonnet, which resolves client-side to a built-in default id. The endpoint also lists DeepSeek ids, but Claude Code's gateway documentation says Anthropic does not support routing it to non-Claude models, so keep the SDK on a Claude-family id.

The gotchas. In TypeScript options.env replaces the environment, so spread process.env into it. An env block in a Claude Code settings file overrides the shell and options.env — remove an old ANTHROPIC_BASE_URL there or pass setting_sources=[]. A leftover ANTHROPIC_API_KEY goes out as a second header, so unset it. max_budget_usd compares a client-side estimate from a bundled price table that the docs say must not drive financial decisions; the cap that stops spend is maxSpend on the key. Guide: Claude Agent SDK + Router One, and Claude Code from China for the terminal.

Pydantic AI: OpenAIChatModel first, a typed-output mode second

Current Pydantic AI documentation maps the bare openai: prefix of Agent('openai:...') to OpenAIResponsesModel, so the guide constructs the model explicitly: OpenAIChatModel selects /v1/chat/completions, OpenAIProvider receives the /v1 base URL and the key, and the catalog id is passed unchanged, prefix included.

from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider

model = OpenAIChatModel(
    os.environ["ROUTER_ONE_MODEL_ID"],
    provider=OpenAIProvider(
        base_url="https://api.router.one/v1",
        api_key=os.environ["ROUTER_ONE_API_KEY"],
    ),
)
agent = Agent(model, output_type=str)

output_type=str keeps the first request free of tools and JSON-schema output; agent.run_sync(prompt, usage_limits=UsageLimits(request_limit=3)) bounds the model requests Pydantic AI tracks for the run.

The gotchas. Typed output is where model capabilities start to matter: a BaseModel as output_type (or ToolOutput) uses an output tool, so the model needs tool calling; NativeOutput uses the model's native JSON-schema response format; PromptedOutput puts the schema in the prompt and validates afterwards. Validation retries make additional model requests, each with its own trace, so when typed output fails, return to output_type=str first. Guide: Pydantic AI + Router One; see also the structured outputs page.

CrewAI: custom_openai=True and the openai/ prefix rule

CrewAI picks a client from the provider prefix of every model string: an anthropic/ or google/ id goes to CrewAI's own path for that vendor, which expects that vendor's API and key. custom_openai=True forces the OpenAI SDK's Chat Completions path for any id; in that mode CrewAI strips exactly one leading openai/ segment and sends the rest unchanged.

from crewai import Agent, Crew, LLM, Task

llm = LLM(
    model="openai/" + os.environ["ROUTER_ONE_MODEL_ID"],
    custom_openai=True,
    base_url="https://api.router.one/v1",
    api_key=os.environ["ROUTER_ONE_API_KEY"],
)

Each Agent takes llm=llm, and crew.kickoff() returns the text in .raw. Because one prefix is consumed, a GPT-family id is written twice — openai/openai/gpt-5.5 — or the bare gpt-5.5 goes on the wire; check the model column in Logs. A scaffolded project can use MODEL, OPENAI_API_BASE and OPENAI_API_KEY instead, with the same openai/ plus catalog id form in MODEL.

The gotchas. A plain string in agents.yaml or Agent(llm="...") is built without the base URL and handed to CrewAI's LiteLLM fallback, which is not installed by default — keep the model in MODEL or an LLM object. api="responses" moves the same object to /v1/responses, so leave api at its default. crew.usage_metrics is a token tally, not a bill, and CREWAI_TRACING_ENABLED is CrewAI's tracing, not the gateway's. Guide: CrewAI + Router One.

LangChain and LangGraph: ChatOpenAI with use_responses_api=False

ChatOpenAI takes the base URL and key directly, and the guide pins use_responses_api=False so the entry stays on Chat Completions (useResponsesApi: false in JS/TS). The official integration page says ChatOpenAI routes to the Responses API when a Responses-only feature is used or use_responses_api=True is set, and that an explicit base_url wins over the OPENAI_API_BASE and OPENAI_BASE_URL variables — so pin both in code.

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(
    base_url="https://api.router.one/v1",
    api_key="sk-your-router-one-key",
    model="<model-id-from-/models>",
    use_responses_api=False,
)

print(llm.invoke("Hello!").content)

LangGraph does not change this. The LangChain docs describe LangChain as the agent framework and LangGraph as the runtime underneath — durable execution, streaming, human-in-the-loop, persistence — and create_agent accepts an initialized model instance, so the llm above is what an agent or a hand-built graph node sends to Router One. Checkpoints and state stay in your process.

The gotchas. Native Anthropic features or non-standard provider fields need a matching integration and endpoint, and built-in tools or conversation-state features can move LangChain onto Responses — recheck the model page first. Guide: LangChain + Router One; endpoint details: streaming, tool calling.

What the gateway does and does not do

Router One serves the model requests. For each one it records a trace in Dashboard → Logs — model, input and output tokens, cost, latency, HTTP status, request_id — and applies the caps on the key: maxSpend, plus rateLimit and tokenLimitTpm for runaway loops. An id sent to an endpoint that does not serve it is rejected with HTTP 400 before any model is called; the API compatibility fact sheet has each endpoint's accept and reject lists.

It does not run tools, keep agent state, or orchestrate. Function tools, handoffs, permission checks, sessions, subagents, checkpoints and validation retries run in your process; hosted tool fields on /v1/responses are accepted and metered, but Router One does not execute tools itself. There is no embeddings endpoint either — the Haystack guide, the RAG-pipeline sibling of these five, shows where that boundary falls. The ledger, field by field: cost tracking.

Budgeting an agent run

Every framework above has a loop bound, and none is a spend cap: max_turns in the OpenAI Agents SDK counts model invocations (10 unless you pass a value), max_turns in the Claude Agent SDK counts tool-use round trips, UsageLimits(request_limit=...) in Pydantic AI counts the requests it tracks. Retries sit outside all of them, and every attempt that reaches the gateway is its own request, trace and charge; client-side figures such as total_cost_usd or crew.usage_metrics are estimates, not what you are billed.

The cap that stops spend is on the key: one key per framework, each with maxSpend, and a runaway run stops at the cap with a 402 while the wallet and the other keys are untouched. Reconcile by request_id — filter Logs by key, time window and exact model, match the count against the framework's turn or step figure, and keep the request_id of any failed trace. Cancelled streams are neither a silent charge nor a silent zero: on POST /v1/chat/completions and POST /v1/responses a request the client cancels mid-stream is recorded as HTTP 499 client_cancelled and billed only for the usage the upstream reported; the gateway waits up to five seconds for that usage, releases the reserved balance, and never retries or fails it over (pricing facts).

Create one key per framework at router.one, cap each, and let the traces show which loop earns its bill.

FAQ

Which SDK should a Claude-first team use? The Claude Agent SDK if you want Claude Code's engine — file and shell tools, permission modes, subagents — inside an application; it speaks Anthropic Messages and stays on Claude-family ids. Otherwise the other four reach Claude ids on /v1/chat/completions, and a later switch of family is a change of id, not of framework.

Which SDK should a GPT-first team use? The OpenAI Agents SDK, where the endpoint is the real decision: its Responses default is served natively for the currently listed GPT-family and DeepSeek ids and brings hosted tools and previous_response_id, while the explicit OpenAIChatCompletionsModel works with every chat model. The SDK recommends one model shape per workflow.

Can one key serve all five at once? Yes — the same sk- key works in every config on this page, and every call lands in the same wallet. One key per framework is the better setup: each carries its own maxSpend, and Dashboard → Logs filters by key, so per-framework spend is a fact, not an estimate.

Does Responses versus Chat Completions matter? It decides which ids work and which features exist: /v1/chat/completions serves every chat model, /v1/responses is native for GPT-family and DeepSeek ids with the Responses-only features, and /v1/messages serves Claude-family and DeepSeek ids in the Anthropic format. A wrong pairing gets HTTP 400 must be called via … before any model runs — change the id or the model class, not the base URL.

Related canonical pages

This article belongs to the LLM API Gateway and Routing cluster. These pages are the commercial page, setup docs, evidence source, and trust references.

Commercial pageRouter One API gatewayThe product homepage for unified model calls, routing, fallback, budgets, and observability.API docsRouter One API documentationOpenAI-compatible endpoints, CLI setup, and model invocation examples.EvidenceSmart routing methodologyRouting signals, the final model and provider, and the customer-visible trace field boundaries.ComparisonOpenRouter alternativeA professional comparison of global catalog breadth versus China-friendly routing and payments.TrustCiteable factsStable product facts for crawlers, AI answer engines, and customers.Data retentionData retention policyPrompt/completion retention boundaries and request metadata policy.Gateway pageUnified LLM API gatewayOne OpenAI-compatible endpoint for the whole catalog, with routing, fallback, and budgets.Routing pageSmart model routingHow candidate ranking uses latency, posted cost, and reliability signals.Fallback pageLLM provider fallbackWhat makes a request eligible for a retry on another healthy provider route.Observability pagePer-request trace logFinal model and provider, tokens, latency, status, and errors for every request.Compatibility pageOpenAI-compatible endpointKeep the OpenAI SDK and change only the base URL to reach every model family.Cost tracking pageLLM cost trackingPer-key, per-model, and per-request spend attribution with hard spend ceilings.Reseller pageBuild your own LLM API serviceSpend-capped customer keys, per-key usage attribution, and an explicit list of what key-level reselling does not give you.Client integrationsSDK and client setup guidesPoint any coding agent, SDK, chat client, or LLM app platform at one endpoint — a dedicated guide for each.Model comparisonsSide-by-side model pricing and contextPer-1M rates, context windows, and capabilities rendered from the live catalog.

Related reads