One API Endpoint for GPT, Claude, and Gemini: How the Compatibility Layer Works
You can call GPT, Claude, and Gemini through a single OpenAI-compatible endpoint by pointing your existing OpenAI SDK at https://api.camel-hub.com/v1 and changing only the model name β no separate SDKs, auth schemes, or request bodies to maintain per provider. This post explains why the base_url swap works, walks through working Python, Node.js, and curl examples, and covers the two places cross-provider portability actually breaks: provider-native endpoints and function-calling JSON shapes.
What "one endpoint" really means
Every major model provider ships its own request/response shape. OpenAI's Chat Completions API wraps messages in a `choices[0].message` envelope with a specific `usage` block; Anthropic's Messages API nests content in a `content` array of typed blocks (`text`, `tool_use`, β¦) and reports tokens under `usage.input_tokens` / `usage.output_tokens`; Google's Gemini API wraps everything in `candidates[0].content.parts` and uses `promptTokenCount` / `candidatesTokenCount`. If you called each provider directly, you'd write three different parsers.
A compatibility gateway sits in front of all three and does the translation server-side: your client always sends and receives the OpenAI Chat Completions shape, and the gateway rewrites the request into whatever the target provider β OpenAI, Anthropic, or Google β actually expects, then rewrites the response back into the OpenAI shape before it reaches you. The model you ask for (`gpt-5.5`, `claude-sonnet-5`, `gemini-3.5-flash`) determines which provider the gateway routes to; your code never changes.
This is not a proxy that blindly forwards bytes β it's a format converter. That distinction matters because it's also where the caveats later in this post come from: anything that doesn't have a clean OpenAI-shape equivalent (a provider-specific tool-call field, a native streaming event type) either gets mapped as closely as possible or has to be accessed through that provider's native route instead.
Why the base_url swap works with official SDKs
The OpenAI Python and Node.js SDKs were built to talk to any server that speaks the OpenAI HTTP API β the SDK doesn't hardcode api.openai.com, it just defaults to it. Both SDKs expose a `base_url` (Python) / `baseURL` (Node.js) constructor argument specifically so you can point them at a compatible server. Set it once, keep the OpenAI SDK you already know, and every `chat.completions.create()` call now goes to whichever endpoint you configured.
This is exactly what makes GPT/Claude/Gemini interchangeable at the client level: since the gateway accepts and returns the OpenAI shape regardless of which model is behind it, the SDK never notices it's actually talking to Anthropic's or Google's infrastructure on the other side. You keep one dependency, one auth header format (`Authorization: Bearer <key>`), and one function signature β only the `model` string changes.
Quickstart: Python, Node.js, and curl
All three examples below hit the same endpoint, https://api.camel-hub.com/v1/chat/completions, and differ only in the `model` field. Get an API key from the CaMeL Hub console first.
from openai import OpenAI
client = OpenAI(
api_key="sk-your-camel-hub-key",
base_url="https://api.camel-hub.com/v1",
)
# Same call shape works for every model below β only the string changes.
for model in ["gpt-5.5", "claude-sonnet-5", "gemini-3.5-flash"]:
resp = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": "Explain base_url swapping in one sentence."}],
)
print(model, "->", resp.choices[0].message.content)
Node.js and curl versions
The Node.js SDK follows the identical pattern with `baseURL` in camelCase. The raw curl request underneath both SDKs is a plain POST with a bearer token β useful when you're debugging what the SDK is actually sending, or calling the API from a language without an official OpenAI SDK.
import OpenAI from "openai";
const client = new OpenAI({
apiKey: "sk-your-camel-hub-key",
baseURL: "https://api.camel-hub.com/v1",
});
const resp = await client.chat.completions.create({
model: "claude-sonnet-5",
messages: [{ role: "user", content: "Explain base_url swapping in one sentence." }],
});
console.log(resp.choices[0].message.content);
// --- equivalent curl ---
// curl https://api.camel-hub.com/v1/chat/completions \
// -H "Authorization: Bearer sk-your-camel-hub-key" \
// -H "Content-Type: application/json" \
// -d '{"model": "gemini-3.5-flash", "messages": [{"role": "user", "content": "hi"}]}'
Format auto-detection: when you don't want the OpenAI shape
Sometimes you're not starting from the OpenAI SDK β you're already using Anthropic's `anthropic` client, or you specifically need a Claude-only feature like extended thinking blocks in their native format. The gateway detects intent from request headers rather than forcing everyone through one shape: send `x-api-key` plus `anthropic-version`, and requests to `/v1/messages` are handled with Anthropic's native Messages API request/response format, unchanged. Send `x-goog-api-key` (or a `?key=` query parameter), and `/v1beta/models/{model}:generateContent` (or `:streamGenerateContent`) speaks Gemini's native REST shape.
Practically, this means you can mix and match: use the OpenAI-compatible `/v1/chat/completions` endpoint for the 90% of your code that's provider-agnostic, and drop down to `/v1/messages` or `/v1beta/models/...` for the specific calls where you need a provider's native fields the OpenAI shape can't express. Same base domain, same account, same billing β just a different path and header set.
Streaming works the same way, with one nuance
Set `stream: true` (or `stream=True` in Python) against `/v1/chat/completions` and you get standard OpenAI-format Server-Sent Events β `data: {"choices":[{"delta":{"content":"..."}}]}` chunks ending in `data: [DONE]` β regardless of whether GPT, Claude, or Gemini is generating underneath. The gateway re-chunks each provider's native streaming protocol (OpenAI's own SSE deltas, Anthropic's typed `content_block_delta` events, Gemini's chunked JSON array) into that one consistent shape.
The nuance: if you stream against the native `/v1/messages` or `/v1beta/models/...:streamGenerateContent` endpoints instead, you get that provider's own event types back β Anthropic's `message_start` / `content_block_delta` / `message_stop` sequence, or Gemini's native chunk format β not the OpenAI delta shape. Pick native streaming only when you're already parsing that provider's SSE format elsewhere in your codebase; otherwise the OpenAI-compatible stream is what keeps your parsing logic model-agnostic.
Function calling: the portability gap that actually bites
Function calling is where "just change the model string" gets more nuanced. All three providers support it, and the OpenAI-compatible endpoint normalizes the request format β you declare `tools` with a JSON Schema `parameters` object the same way regardless of target model, and you get `tool_calls` back with an `id`, a `function.name`, and a `function.arguments` JSON string, matching OpenAI's shape. That part is portable.
What's not fully portable is provider behavior once you go past a single round trip. Anthropic's native tool-use format nests calls inside typed `content` blocks (`type: "tool_use"`) rather than a separate `tool_calls` array, and if you ever inspect a raw response from the native `/v1/messages` route instead of the compatible one, you'll see that shape directly. Gemini's native `functionCall` / `functionResponse` parts follow a third structure again. The OpenAI-compatible layer maps all of these to one format for you, but nuances like how strictly a model honors `tool_choice: "required"`, whether it supports parallel tool calls in one turn, and how it expects multi-turn tool results to be threaded back into the conversation still vary by underlying model β because those are model behaviors, not just wire-format differences the gateway can paper over.
The practical takeaway: for basic function calling β one tool call, one result, one follow-up response β the OpenAI-compatible endpoint makes GPT, Claude, and Gemini genuinely interchangeable. For complex multi-tool, multi-turn agent loops, test against your target model specifically before assuming a prompt or tool schema that works well on one model will behave identically on another.
To get started: keep the OpenAI SDK, point `base_url` at https://api.camel-hub.com/v1, and switch models by changing one string β `gpt-5.5`, `claude-sonnet-5`, `gemini-3.5-flash`, or any other model on the platform. Reach for the native `/v1/messages` or `/v1beta/models/...` routes only when you need a provider-specific feature the OpenAI shape doesn't cover. Full model list and current pricing are on the models page; a CaMeL Hub API key from the console is all you need to run the examples above.