> Markdown mirror of https://router.one/blog/openai-agents-sdk-vs-claude-agent-sdk-vs-pydantic-ai for AI assistants and crawlers. Router One is an OpenAI-compatible LLM API gateway.
> Published: 2026-09-11 · Author: Router One Team

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

_OpenAI Agents SDK, Claude Agent SDK, Pydantic AI, CrewAI, LangChain: how each points at Router One, the protocol it speaks, and one trace per model call._

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](https://router.one/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

| SDK | Language | Point it at the gateway | Protocol sent | Families reached | Stays in the SDK |
| --- | --- | --- | --- | --- | --- |
| OpenAI Agents SDK | Python; TypeScript twin | `AsyncOpenAI(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 ids | Function tools, handoffs, guardrails, sessions, tracing |
| Claude Agent SDK | Python, TypeScript | `ClaudeAgentOptions(env={...})` with `ANTHROPIC_BASE_URL` and `ANTHROPIC_AUTH_TOKEN`, or the shell | Anthropic Messages; the engine appends `/v1/messages` | Currently listed Claude-family ids | Tool execution, permissions, hooks, sessions, subagents |
| Pydantic AI | Python | `OpenAIChatModel(id, provider=OpenAIProvider(base_url=…, api_key=…))` | Chat Completions (`openai:` shorthand means Responses) | Every chat model | Agent loop, validation, tools, message history |
| CrewAI | Python | `LLM(model="openai/" + id, custom_openai=True, base_url=…, api_key=…)` or the `MODEL`, `OPENAI_API_BASE`, `OPENAI_API_KEY` env vars | Chat Completions (`api="responses"` switches) | Every chat model | Agent loop, task order, delegation, tools, memory |
| LangChain / LangGraph | Python; JS/TS `useResponsesApi: false` | `ChatOpenAI(base_url=…, api_key=…, model=…, use_responses_api=False)` | Chat Completions (Responses when a Responses-only feature is used) | Every chat model | Graphs, 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.

```python
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](https://router.one/codex-responses-api)).

**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](https://router.one/integrations/openai-agents-sdk).

## 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.

```python
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](https://router.one/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](https://router.one/integrations/claude-agent-sdk), and [Claude Code from China](https://router.one/claude-code-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.

```python
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](https://router.one/integrations/pydantic-ai); see also the [structured outputs page](https://router.one/llm-structured-outputs).

## 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.

```python
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](https://router.one/integrations/crewai).

## 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.

```python
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](https://router.one/integrations/langchain); endpoint details: [streaming](https://router.one/llm-streaming), [tool calling](https://router.one/llm-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](https://router.one/facts/api-compatibility.md) 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](https://router.one/integrations/haystack), the RAG-pipeline sibling of these five, shows where that boundary falls. The ledger, field by field: [cost tracking](https://router.one/llm-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](https://router.one/facts/pricing.md)).

Create one key per framework at [router.one](https://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.

## See also

- Canonical page: https://router.one/blog/openai-agents-sdk-vs-claude-agent-sdk-vs-pydantic-ai
- LLM API Gateway and Routing: https://router.one/llm-api-gateway
- All blog posts: https://router.one/blog
- Models and per-model token rates: https://router.one/models (markdown: https://router.one/models.md)
- Pricing: https://router.one/pricing
- API docs (markdown): https://router.one/docs.md
- Company facts: https://router.one/facts/company.md
