I've got an OpenAI client instantiated in about six different repos at this point. Same four lines every time: import the SDK, set an API key, point it at a base URL, call chat.completions.create. The annoying part isn't writing that code — it's what happens later, when someone asks "can we try this with Claude?" and you realize half your app assumes the string gpt- is going to show up in the model name somewhere.
Here's the thing that actually saves you: you probably don't need to rewrite anything. If your code already talks to OpenAI's /chat/completions endpoint, you can usually get it talking to Claude, Gemini, or a hundred other models by changing three values — the base URL, the API key, and the model string. That's the entire premise of an OpenAI-compatible API. No new SDK, no new request shape, no new mental model.
My name is Artem, and I write about the practical side of building with AI at Writingmate. I've swapped enough base URLs in enough test scripts to know where this trick works cleanly and where it quietly breaks your app in production. This guide is the code, the comparison, and the specific list of things to test before you trust a model swap with real traffic.
What "OpenAI-compatible" actually means
OpenAI didn't invent a universal AI protocol on purpose. What happened is simpler: the /v1/chat/completions request shape — a model string, a messages array with roles, optional tools, an optional stream flag — became so widely used that other providers started accepting the exact same JSON body at their own endpoints. Anthropic built a compatibility layer that accepts OpenAI-shaped requests and translates them into Claude's native Messages API behind the scenes. Google did the same thing for Gemini. OpenRouter, Groq, Together, and dozens of smaller providers went further and built their entire API around that shape from day one.
So "OpenAI-compatible" doesn't mean those providers licensed OpenAI's code or copied their models. It means they agreed to accept the same request format so that any client already speaking that format — the official openai Python and Node SDKs, the Vercel AI SDK, LangChain, LiteLLM, Aider, Continue — can point at them without a rewrite. You change where the request goes, not how you build it.
That's genuinely useful, and it's also easy to over-trust. The request shape is standardized; the model's behavior is not. A schema field the sender includes doesn't mean the receiver enforces it. That gap is where most "it worked in my test script but broke in prod" stories come from, and I'll get specific about it further down.
The code: one client, four base URLs
Here's the part that actually matters — a working example, not a diagram. This is the OpenAI Python SDK, unmodified, hitting four different providers by changing exactly three values per call.
from openai import OpenAI
def ask(base_url, api_key, model, prompt):
client = OpenAI(base_url=base_url, api_key=api_key)
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
)
return response.choices[0].message.content
PROVIDERS = {
"openai": {
"base_url": "https://api.openai.com/v1",
"api_key": OPENAI_KEY,
"model": "gpt-5-mini",
},
"claude": {
"base_url": "https://api.anthropic.com/v1/",
"api_key": ANTHROPIC_KEY,
"model": "claude-opus-5",
},
"gemini": {
"base_url": "https://generativelanguage.googleapis.com/v1beta/openai/",
"api_key": GEMINI_KEY,
"model": "gemini-2.5-flash",
},
"writingmate": {
"base_url": "https://writingmate.ai/api/openai/v1",
"api_key": WRITINGMATE_KEY,
"model": "anthropic/claude-sonnet-4.5",
},
}
for name, cfg in PROVIDERS.items():
reply = ask(cfg["base_url"], cfg["api_key"], cfg["model"], "Say hello in five words.")
print(name, "->", reply)
Nothing else changes. Same import, same function, same call signature. The only per-provider knowledge you need lives in that PROVIDERS dict, which means it's also the only thing you need to touch if a provider adds a new model or you want to add another one. Writingmate's own OpenAI-compatible endpoint follows the identical pattern — set base_url to https://writingmate.ai/api/openai/v1, use a Developer Key as the bearer token, and pick any model slug from the live model directory, including Claude, Gemini, and open-weight models behind the same account. The full parameter reference is in the OpenAI-compatible API docs, including the exact curl calls for chat completions, the Responses API, and image and video generation through the same key.
The model-swap comparison: what stays the same, what doesn't
Before you wire this into a real feature, it helps to see the compatibility gaps side by side instead of discovering them one support ticket at a time. This table is built from the official compatibility docs for each provider, not guesswork.
Provider | Base URL | Tool calling |
| Vision input |
|---|---|---|---|---|
OpenAI (native) | api.openai.com/v1 | Full support, including | Full JSON-schema enforcement | Full support, incl. |
Anthropic (OpenAI-compat layer) | api.anthropic.com/v1/ | Supported, but | Ignored entirely; use native Structured Outputs instead |
|
Google Gemini (OpenAI-compat layer) | generativelanguage.googleapis.com/v1beta/openai/ | Fully supported | Fully supported (JSON schema) | Fully supported |
Writingmate | writingmate.ai/api/openai/v1 | Supported on | Depends on the selected model's own provider behavior | Depends on the selected model |
That last row is the detail people miss. Writingmate's endpoint is OpenAI-compatible at the transport level, but it's still routing to the actual underlying model — so if you point it at anthropic/claude-sonnet-4.5, you inherit Claude's own compatibility quirks, and if you point it at a Gemini model, you get Gemini's behavior. Choosing a gateway doesn't erase the provider's own limitations; it just gives you one key and one base URL to manage while you test each model on its own terms. The pricing page breaks down which plan tier unlocks which models if you're deciding what to test first.
"Run Claude Code with GPT-5! Or Qwen3, kimi, grok4, gemini, local models or basically any OpenAI compatible model provider." — @unclecode on X, describing a CLI proxy that remaps coding-agent calls onto whichever OpenAI-compatible provider you point it at
Where compatibility actually breaks
This is the section I'd read twice before shipping a swap. The request shape being accepted is not the same thing as every field being honored. Anthropic's own OpenAI SDK compatibility docs are refreshingly specific about what gets silently dropped:
Tool calling — the
strictflag. OpenAI'sstrict: trueon a tool definition guarantees the returned arguments match your JSON schema. On Anthropic's compatibility layer,strictis accepted but ignored — the tool call still happens, but nothing guarantees the output validates against your schema. If your code parses tool arguments without a validation step, this is where it breaks first.Structured outputs. If your OpenAI code sets
response_format: {"type": "json_schema", ...}to force valid JSON, know that Anthropic's compatibility layer ignoresresponse_formatcompletely. It won't error — it just won't enforce it. You'll get a normal text response and have to catch the parsing failure yourself, or switch that specific call to Claude's native Structured Outputs endpoint.Vision parameters. Sending an
image_urlworks across OpenAI, Anthropic, and Gemini's compatible endpoints. Thedetailfield that controls image resolution/cost tradeoffs on OpenAI, though, is silently ignored on Anthropic's layer. Audio input blocks are stripped entirely.Sampling and metadata fields.
nmust be exactly 1 on Anthropic's compat layer (no batch completions in one call).logprobs,seed,presence_penalty,frequency_penalty, andlogit_biasare all accepted and ignored rather than rejected — meaning your code won't throw an error, it'll just quietly not do what you asked.System messages. OpenAI lets you interleave multiple system/developer messages through a conversation. Anthropic's layer only supports one initial system message, so it hoists and concatenates every system/developer message you send into a single block at the start. If your prompt logic depends on system messages appearing mid-conversation, that ordering assumption breaks.
Gemini's compatibility layer is more permissive by comparison — tool calling, structured JSON output, and vision are all documented as fully supported — but Google is explicit that the OpenAI compatibility surface is still in beta, and several Gemini-only features (cached content, custom safety thresholds, video generation parameters) only work if you pass them through an extra_body field, which sits outside the standard OpenAI request shape entirely.
What I check before trusting a swap
None of the above means the compatible endpoint is broken — it means "compatible" describes the request format, not the guarantee. Before I let a model swap anywhere near production traffic, I run the same three calls against the new endpoint and compare:
A plain chat call with a system message and a couple of turns, checking that formatting and tone instructions still land the way they did on the original provider.
A tool-calling call with a schema that has a required enum field and a nested object, then I actually validate the returned JSON against that schema instead of assuming it matches.
A structured-output call using whatever mechanism your code relies on for guaranteed JSON — and I check whether the new endpoint enforces it or just returns prose that happens to look like JSON.
If all three come back clean, the swap is probably safe for that use case. If the tool-calling or structured-output call comes back with a field missing or malformed, that's your answer before a user ever sees it. This is a five-minute check, and it's the difference between "the base URL trick worked" and "the base URL trick worked until someone sent a weird prompt."
Where this fits if you're already building on Writingmate
If you're testing this pattern because you want to compare GPT, Claude, and Gemini without juggling three separate API keys and three separate billing dashboards, that's exactly what Writingmate's OpenAI-compatible API is for. One Developer Key, one base URL, and GET /models gives you the live list of everything your workspace plan can access — so you can run the same three-call test above against GPT, Claude, and Gemini models one after another without re-authenticating each time. It's currently alpha and fair-use, built for coding tools, CLI agents, and internal scripts rather than production customer-facing traffic, so treat it the way you'd treat any early API: test the specific calls your feature depends on, not just the happy path.
The same account also works with tools that already speak OpenAI-compatible config — LiteLLM, Continue, Aider, OpenCode — so if your team is already standardized on one of those, adding Writingmate as a provider is a config change, not a new integration.
The honest summary: the base URL trick is real and it will save you real time. Just don't confuse "the request went through" with "the response means what you think it means" — test tool calls and structured output explicitly, every time you add a new endpoint to that dict.
See you in the next one!
Artem
Frequently Asked Questions
Sources
- Writingmate OpenAI-Compatible API docs
- Anthropic OpenAI SDK compatibility — official limitations reference
- Google Gemini OpenAI compatibility docs
- Reddit r/LocalLLaMA search: OpenAI-compatible API discussions
- @unclecode on X
- Access all the LLM models with ONE unified OpenAI compatible API — Eden AI (YouTube)
Written by
Artem Vysotsky
Ex-Staff Engineer at Meta. Building the technical foundation to make AI accessible to everyone.
Reviewed by
Sergey Vysotsky
Ex-Chief Editor / PM at Mosaic. Passionate about making AI accessible and affordable for everyone.