OpenAI, Claude, and Gemini now look surprisingly similar at the API level. In some cases, switching providers requires little more than changing an API key, a base URL, and a model name.
But this compatibility disappears almost as soon as the first successful response comes back.
Tool calls behave differently. Reasoning controls use different parameters. Streaming events arrive in different shapes. Rate limits, retries, usage data, and error handling each follow their own rules.
AI APIs are becoming syntactically compatible. They are still far from operationally interchangeable.
Google and Anthropic both provide compatibility with the OpenAI SDK. That means the same basic client can now be pointed at three different providers:
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.API_KEY,
baseURL: process.env.BASE_URL
});
const response = await client.chat.completions.create({
model: process.env.MODEL,
messages: [
{
role: "user",
content: "Explain vector databases in one sentence."
}
]
});
console.log(response.choices[0].message.content);
The configuration changes, but most of the request does not
// OpenAI
baseURL: "https://api.openai.com/v1"
// Claude
baseURL: "https://api.anthropic.com/v1/"
// Gemini
baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/"
For a prototype, this is genuinely useful. A developer can test several models without rewriting the entire application. Anthropic even describes its compatibility layer as a way to evaluate Claude using an existing OpenAI-based setup, while Google provides similar support for Gemini.
The problem is that a real AI product needs much more than a text response.
A production system needs to know when to retry, how long to wait, how to record token usage, what to do when a tool call fails, and whether a model stopped because it finished or because it reached a limit.
That is where the APIs begin to separate again.
| Area | What changes between providers |
|---|---|
| Reasoning | Different controls, supported values and response formats |
| Tool calling | Different schemas, validation behaviour and edge cases |
| Streaming | Different event types and chunk structures |
| Errors | Different status details and retry guidance |
| Rate limits | Different limits for requests, input tokens and output tokens |
| Usage | Different token categories and accounting rules |
| New features | Native APIs usually receive capabilities before compatibility layers |
Gemini, for example, maps OpenAI-style reasoning settings to its own thinking configuration. Its newer native Interactions API can also return structured steps such as thoughts, tool activity and model output—not simply one message string.
Claude’s native Messages API has its own request model and headers. Its rate limits can separately track requests per minute, input tokens and output tokens. An application that treats every 429 response identically may retry too aggressively, wait unnecessarily, or send traffic to a fallback model that has the same limitation.
Even successful responses can hide differences. Two providers might both return a tool call, but disagree on argument validation or how parallel calls are represented. The code passes the first demo, then breaks when a model returns something slightly unexpected.
This suggests that the useful abstraction is not simply:
generateText(prompt, model)
const result = await generate({
provider: "anthropic",
model: "selected-model",
messages,
tools,
timeout: 30_000
});
console.log({
text: result.text,
toolCalls: result.toolCalls,
usage: result.usage,
finishReason: result.finishReason
});
The wrapper should not pretend every model behaves identically. It should provide a stable application-facing format while still exposing provider-specific capabilities when they matter.
This is also one of the problems we are thinking about at TokenBay: not simply placing several model names behind one endpoint, but reducing the operational friction that appears after the first successful request.
The APIs are converging enough to make experimentation easier. That is real progress. But changing three configuration values is not the same thing as changing providers safely in production.
The first request is becoming standardized. Everything after it is where the integration work still lives.
Top comments (0)