一个 OpenAI 兼容端点调用 GPT、Claude、Gemini:兼容层是怎么做到的

把现有 OpenAI SDK 的 base_url 指向 https://api.camel-hub.com/v1,只改一个模型名字符串,就能用同一套代码调用 GPT、Claude、Gemini——不用为每家供应商维护单独的 SDK、鉴权方式或请求格式。这篇文章讲清楚 base_url 替换为什么能生效,给出可直接跑的 Python、Node.js、curl 示例,并说明跨模型可移植性真正会踩坑的两处:厂商原生端点和函数调用的 JSON 结构。

"一个端点"到底是什么意思

每家模型厂商的请求/响应结构都不一样。OpenAI 的 Chat Completions 把结果包在 `choices[0].message` 里,`usage` 字段有自己的命名;Anthropic 的 Messages API 把内容放进带类型的 `content` 数组(`text`、`tool_use` 等),token 数记在 `usage.input_tokens` / `usage.output_tokens`;Google 的 Gemini API 又是 `candidates[0].content.parts` 那一套,token 统计叫 `promptTokenCount` / `candidatesTokenCount`。如果直接对接三家官方接口,就得写三套解析逻辑。

兼容网关做的事情,是站在三家厂商 API 前面做双向转换:你的客户端始终发送和接收 OpenAI Chat Completions 的结构,网关在服务端把请求改写成对应厂商(OpenAI / Anthropic / Google)真正需要的格式,拿到响应后再改写回 OpenAI 的结构返回给你。你请求的模型名(`gpt-5.5`、`claude-sonnet-5`、`gemini-3.5-flash`)决定网关把请求路由到哪家厂商,你这边的代码完全不用变。

要强调一点:这不是简单转发字节的代理,而是格式转换器。这个区别很关键——后面讲流式和函数调用的坑,根源都在这里:凡是 OpenAI 结构里没有干净对应关系的东西(某厂商特有的工具调用字段、原生流式事件类型),要么被尽量映射过去,要么就得走该厂商的原生路由才能拿到完整信息。

为什么换 base_url 就能用官方 SDK

OpenAI 的 Python 和 Node.js SDK 设计上就是面向"任何讲 OpenAI HTTP 协议的服务器",api.openai.com 只是默认值,并没有写死在 SDK 内部。两个 SDK 的构造函数都留了 `base_url`(Python)/ `baseURL`(Node.js)参数,专门用来指向兼容服务器。设好这一个参数,你熟悉的 OpenAI SDK 原封不动继续用,之后每次 `chat.completions.create()` 调用都会打到你配置的端点。

这正是 GPT/Claude/Gemini 在客户端层面能互换的原因:只要网关始终收发 OpenAI 结构,不管背后实际是哪家模型在生成,SDK 都感知不到差异。你只需要维护一个依赖、一种鉴权头格式(`Authorization: Bearer <key>`)、一个函数签名——变的只有 `model` 这一个字符串。

快速上手:Python、Node.js、curl

下面三份示例都打的是同一个端点 https://api.camel-hub.com/v1/chat/completions,区别只在 `model` 字段。先在 CaMeL Hub 控制台申请一个 API key。

from openai import OpenAI

client = OpenAI(
    api_key="sk-your-camel-hub-key",
    base_url="https://api.camel-hub.com/v1",
)

# 下面每个模型的调用写法完全一致,只换字符串
for model in ["gpt-5.5", "claude-sonnet-5", "gemini-3.5-flash"]:
    resp = client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": "用一句话解释 base_url 替换的原理。"}],
    )
    print(model, "->", resp.choices[0].message.content)

Node.js 与 curl 版本

Node.js SDK 写法完全一样,只是参数名改成驼峰 `baseURL`。两个 SDK 底层实际发出去的都是一个带 bearer token 的普通 POST 请求——想看 SDK 到底发了什么、或者用没有官方 OpenAI SDK 的语言接入时,直接用 curl 排查最直观。

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: "用一句话解释 base_url 替换的原理。" }],
});

console.log(resp.choices[0].message.content);

// --- 等价的 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"}]}'

格式自动识别:什么时候不用 OpenAI 结构

有些场景你本来就不是从 OpenAI SDK 出发——可能已经在用 Anthropic 官方的 `anthropic` 客户端,或者需要 Claude 原生格式下的扩展思考(extended thinking)内容块这类专属能力。网关不强迫所有请求走同一种结构,而是靠请求头判断意图:带上 `x-api-key` 和 `anthropic-version`,打到 `/v1/messages` 的请求会按 Anthropic 原生 Messages API 的请求/响应格式原样处理;带上 `x-goog-api-key`(或 URL 上的 `?key=` 参数),`/v1beta/models/{model}:generateContent`(或 `:streamGenerateContent`)就走 Gemini 原生 REST 结构。

实际用起来可以混搭:代码里跟供应商无关的那部分(通常占大头)走 OpenAI 兼容的 `/v1/chat/completions`;需要用到某厂商原生字段、OpenAI 结构表达不了的特定调用,再单独走 `/v1/messages` 或 `/v1beta/models/...`。域名、账号、计费都是同一套,只是路径和请求头不同。

流式响应同理,但有一处要注意

对 `/v1/chat/completions` 传 `stream: true`(Python 里是 `stream=True`),拿到的都是标准 OpenAI 格式的 Server-Sent Events——`data: {"choices":[{"delta":{"content":"..."}}]}` 一路到 `data: [DONE]` 结束,不管背后是 GPT、Claude 还是 Gemini 在生成。网关把每家厂商各自的原生流式协议(OpenAI 自己的 SSE delta、Anthropic 带类型的 `content_block_delta` 事件、Gemini 的分块 JSON 数组)重新切成这一套统一格式。

需要注意的地方是:如果改走原生端点 `/v1/messages` 或 `/v1beta/models/...:streamGenerateContent` 做流式,拿到的就是该厂商自己的事件类型——Anthropic 的 `message_start` / `content_block_delta` / `message_stop` 序列,或者 Gemini 原生的分块格式,不再是 OpenAI 的 delta 结构。只有当你代码里其他地方本来就在解析该厂商原生 SSE 格式时,才有必要选原生流式;否则用 OpenAI 兼容流式,解析逻辑才能跨模型复用。

函数调用:真正会咬人的可移植性缺口

"只换模型名字符串"这句话在函数调用场景要打个折扣。三家都支持函数调用,OpenAI 兼容端点也把请求格式统一了——声明 `tools` 时用同一套 JSON Schema `parameters` 结构,不管目标模型是谁;拿到的返回也统一成 `tool_calls` 数组,每项带 `id`、`function.name`、`function.arguments`(JSON 字符串),跟 OpenAI 原生格式一致。这一层是可移植的。

不完全可移植的是超出单轮往返之后的模型行为本身。Anthropic 原生的工具调用格式把调用嵌在带类型的 `content` 块里(`type: "tool_use"`),而不是单独的 `tool_calls` 数组——如果直接查看走原生 `/v1/messages` 路由(而非兼容端点)的原始响应,看到的就是这个结构。Gemini 原生的 `functionCall` / `functionResponse` part 又是第三种结构。OpenAI 兼容层把这些都统一映射成一套格式返回给你,但诸如模型对 `tool_choice: "required"` 的服从程度、是否支持一轮内并行调用多个工具、多轮工具结果该怎么拼回对话历史里,这些仍然因底层模型而异——因为这些是模型自身的行为差异,不是网关能靠改写协议格式抹平的。

实用结论:简单的函数调用场景——一次工具调用、一个结果、一次后续回复——OpenAI 兼容端点确实能让 GPT、Claude、Gemini 互换。复杂的多工具、多轮 agent 循环,在切换到目标模型之前务必单独测一遍,不要假设在一个模型上调好的 prompt 或工具 schema 换到另一个模型上行为完全一致。

上手方式很简单:继续用你熟悉的 OpenAI SDK,把 `base_url` 指向 https://api.camel-hub.com/v1,换模型只改一个字符串——`gpt-5.5`、`claude-sonnet-5`、`gemini-3.5-flash`,或平台上的其他任意模型。只有需要某厂商原生专属能力、OpenAI 结构表达不了时,才去用 `/v1/messages` 或 `/v1beta/models/...` 这类原生路由。完整模型列表和当前价格在模型页可查;在控制台申请一个 CaMeL Hub API key,就能直接跑上面的示例。