By now you've called plenty of REST APIs, so a chat completion request should feel familiar: you POST some messages, you get a response back. But as soon as you try to point the same client code at two different model providers, you'll discover that "familiar" only goes so far. OpenAI's Chat Completions API and Anthropic's Messages API agree on the big idea — send a conversation, get a completion — but they disagree on almost every detail of the contract. Understanding exactly where they diverge is the difference between an agent that "mostly works" and one you can actually trust in production. System messages are the first fault line. OpenAI's API treats the system (or "developer") role as just another message that can appear anywhere in the messages array — first, middle, interleaved with turns, repeated multiple times. Anthropic's Messages API has no system role in the messages array at all; instead, there's a single top-level system parameter that represents one leading instruction block. This isn't a cosmetic difference. If your code was written against OpenAI's flexible model and assumes you can drop a system message wherever you like, moving to Claude natively requires you to restructure that logic. Tool-calling and sampling parameters are the second fault line. OpenAI exposes a strict parameter on function/tool definitions that (when true) guarantees the model's output conforms exactly to your JSON schema. Claude's native tool use has its own robust mechanism for schema-constrained output, but it is invoked and configured differently. Temperature ranges differ too: OpenAI accepts 0–2, while Claude's native range is 0–1. So how do you migrate incrementally? Anthropic ships an official OpenAI-SDK compatibility layer: you keep using the openai Python/JS SDK, just point base_url at Anthropic's endpoint and use a Claude model string. This is genuinely useful for quick comparisons — "will Claude do roughly the same thing as GPT for this prompt?" — but Anthropic is explicit that it's meant for testing and comparison, not production use. The reason is that the layer doesn't fail loudly when it can't honor a request; it silently drops or reinterprets fields. Here's what actually happens under the hood:
| OpenAI field/behavior | What the compatibility layer does |
|---|---|
strict (tool calling) |
Ignored — schema adherence is not guaranteed |
response_format |
Ignored |
presence_penalty / frequency_penalty |
Ignored |
seed |
Ignored (no reproducibility guarantee) |
logit_bias / logprobs |
Ignored |
reasoning_effort |
Ignored |
| System messages in multiple positions | Hoisted and concatenated into a single leading system message |
temperature (0–2 range) |
Silently clamped to Claude's 0–1 range |
| Prompt caching, PDF processing, citations, full extended-thinking output | Not available at all through this layer |
Notice the pattern: almost nothing throws an error. Your code runs, you get a 200 response, and everything looks fine — until you inspect the output closely and realize the model never saw your seed, your JSON schema wasn't strictly enforced, or your temperature of 1.4 was quietly capped at 1.0. This is exactly the kind of gap that unit tests checking only "did I get a response" will never catch. What should you actually do with this knowledge? Three good options, in order of engineering maturity:
/chat/completions-shaped API, but the gateway itself — not a thin SDK shim — does the real translation into each provider's native format at runtime, along with logging, caching, and cost tracking. This decouples your application code from any single provider's SDK while keeping the translation logic in a piece of infrastructure designed for exactly that job, rather than a documented-as-temporary compatibility mode.A related tool-design note: current Claude models (Sonnet 4.6, Opus 4.8) tend to complete agentic tool-calling tasks in meaningfully fewer steps than earlier-generation models for equivalent task intelligence. When you're comparing providers for an agent's tool loop, don't just compare single-call latency or cost — compare the number of round trips the agent needs to finish the task, since that compounds directly into both latency and API spend. Finally, a framework worth knowing about: Pydantic AI, built by the Pydantic team, gives you multi-provider agents out of the box. Install it with pip install pydantic-ai (or scoped extras like pydantic-ai[openai], pydantic-ai[anthropic], pydantic-ai[gemini]), then instantiate an agent with a simple provider-prefixed model string:
from pydantic_ai import Agent
agent = Agent('anthropic:claude-sonnet-4-6', system_prompt="You are a helpful assistant.")
result = agent.run_sync("Explain the OpenAI compatibility layer in one sentence.")
print(result.output)
Swapping to 'openai:gpt-5' or 'google-gla:gemini-2.5-flash' is a one-line change — no manual request-shape translation required, because Pydantic AI's internals handle that per provider. Correcting three common misconceptions before the lab:
strict parameter — so your JSON output is not guaranteed to match your schema even if it usually does. Silent failure is more dangerous than a loud error, because it can pass casual testing and break in production.temperature=1.6 for OpenAI silently behaves like temperature=1.0 on Claude through the shim.