从模型直连到统一 AI 网关:多模型 API、Codex 与 Claude Code 接入实践

过去一年,我在不同项目里接过多家大模型:有的使用 OpenAI 风格接口,有的使用自有 SDK,有的鉴权头不同,还有的流式事件格式完全不一样。

只接一个模型时,这些差异不算麻烦。但当项目需要同时使用 GPT、Claude、Gemini、DeepSeek、Qwen 等模型,或者想在 Codex、Claude Code 和业务服务之间复用同一套模型资源时,问题就会逐渐暴露:

  • 每增加一家供应方,都要维护一套 SDK 和环境变量;
  • 模型切换会侵入业务代码;
  • 超时、重试、限流和日志散落在各个调用点;
  • 多个控制台的余额、账单和调用记录难以统一;
  • AI 编程工具与业务程序往往还使用不同的协议。

这篇文章不比较哪个模型"最强",而是讨论一个更工程化的问题:如何用统一 AI 网关降低应用与模型供应方之间的耦合,并让同一套 API 同时服务于代码、Codex 和 Claude Code。

一、为什么不建议让业务代码直接绑定模型厂商

最常见的接入方式,是在业务层直接调用厂商 SDK:

text 复制代码
业务服务 -> 厂商 SDK -> 模型 API

它的优点是链路短,也能第一时间使用厂商的专属能力。但当模型数量增加后,业务层很容易按 OpenAI、Anthropic、Google 等供应方堆叠条件分支。真正麻烦的并不是分支本身,而是它们背后的差异:

关注点 可能存在的差异
鉴权 Bearer Token、自定义请求头、项目凭据
请求结构 messagescontents、system 字段的位置
流式响应 SSE 数据块、事件名称、结束标记
工具调用 参数结构、调用 ID、并行工具调用能力
错误语义 HTTP 状态码、错误码、是否适合重试
用量统计 输入、输出、缓存、推理 Token 的口径

如果这些差异散落在 Controller、定时任务、Agent 和脚本中,后续想换模型时,改动范围通常比预期大。

更稳妥的做法,是在业务和模型供应方之间增加一个稳定的抽象层:

text 复制代码
业务服务 / AI 编程工具
           |
           v
    统一 AI 网关
    ├─ API Key 鉴权
    ├─ 协议适配
    ├─ 模型路由
    ├─ 超时与有限重试
    ├─ 限流与配额
    └─ 日志与用量统计
           |
           v
 OpenAI / Anthropic / Google / 其他模型服务

客户端只需要关心三个配置:

  1. Base URL;
  2. API Key;
  3. Model ID。

模型供应方发生变化时,客户端接口尽量保持稳定,差异由网关内部消化。

二、统一接口不等于抹平所有协议

OpenAI 兼容接口已经成为很多 AI 工具事实上的通用接入方式。例如对话请求通常采用以下结构:

json 复制代码
{
  "model": "模型 ID",
  "messages": [
    { "role": "system", "content": "你是一名编程助手" },
    { "role": "user", "content": "解释这段代码" }
  ]
}

只要网关提供兼容端点,大量 SDK 和客户端就可以通过修改 Base URL 完成迁移,业务代码不必重新实现一遍。

但"兼容"需要有边界。OpenAI、Anthropic 与 Google 的协议并非完全等价,至少有三类能力需要单独处理:

1. 流式事件

不同协议的事件名称、增量字段和结束方式不完全相同。网关不能只转发原始字节,还需要保证客户端能正确识别文本增量、工具调用和结束事件。

2. 工具调用

工具定义、工具选择和调用结果回传的结构可能不同。简单对话可以统一,复杂 Agent 场景则要验证目标模型与兼容层是否完整支持工具调用。

3. 厂商专属参数

推理强度、缓存、视觉输入、结构化输出等能力,可能只在特定协议或模型中存在。好的抽象不是强行取交集,而是让通用能力保持一致,同时允许高级调用显式选择原生协议。

因此,一个实用的网关通常会同时提供 OpenAI、Anthropic 或 Google 兼容入口,而不是把所有请求都压成一种格式。

三、用 Node.js 调用 OpenAI 兼容接口

下面给出一个通用的 OpenAI 兼容接口示例。代码只使用 Node.js 18+ 自带的 fetch,没有第三方依赖,并包含环境变量校验、30 秒超时、HTTP 错误和响应结构检查。接口地址、密钥和模型都从环境变量读取,因此可以用于官方接口、自建网关或其他兼容服务。

js 复制代码
const apiKey = process.env.AI_GATEWAY_API_KEY;
const model = process.env.AI_GATEWAY_MODEL;
const baseUrl = (process.env.AI_GATEWAY_BASE_URL || '').replace(/\/$/, '');

if (!apiKey) {
  console.error('请先设置 AI_GATEWAY_API_KEY');
  process.exit(1);
}

if (!model) {
  console.error('请先设置 AI_GATEWAY_MODEL');
  process.exit(1);
}

if (!baseUrl) {
  console.error('请先设置 AI_GATEWAY_BASE_URL,例如兼容服务的 /v1 地址');
  process.exit(1);
}

async function createChatCompletion() {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 30_000);

  try {
    const response = await fetch(`${baseUrl}/chat/completions`, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${apiKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model,
        messages: [
          {
            role: 'system',
            content: '你是一名严谨的 JavaScript 助手。'
          },
          {
            role: 'user',
            content: '请用三句话解释什么是事件循环。'
          }
        ],
        temperature: 0.3
      }),
      signal: controller.signal
    });

    const responseText = await response.text();

    if (!response.ok) {
      throw new Error(`请求失败:HTTP ${response.status} ${responseText}`);
    }

    const data = JSON.parse(responseText);
    const content = data.choices?.[0]?.message?.content;

    if (!content) {
      throw new Error(`响应中没有可读取的文本:${responseText}`);
    }

    console.log(content);
  } catch (error) {
    if (error.name === 'AbortError') {
      console.error('请求超时:30 秒内未收到完整响应');
    } else {
      console.error(error instanceof Error ? error.message : String(error));
    }
    process.exitCode = 1;
  } finally {
    clearTimeout(timeoutId);
  }
}

createChatCompletion();

保存为 chat.js 后,先设置密钥和模型:

bash 复制代码
export AI_GATEWAY_BASE_URL="https://vsoui.com/v1"
export AI_GATEWAY_API_KEY="sk-你的密钥"
export AI_GATEWAY_MODEL="your-model-id"
node chat.js

把示例地址和模型 ID 替换成实际服务提供的配置。可用模型不应该长期写死在代码里;如果兼容服务实现了模型列表接口,可以通过环境变量查询:

bash 复制代码
curl "$AI_GATEWAY_BASE_URL/models" \
  -H "Authorization: Bearer $AI_GATEWAY_API_KEY"

这里有两个容易忽略的细节:

  • Base URL 已经包含 /v1,代码里只需追加 /chat/completions
  • 错误响应也可能是 JSON,但排查阶段保留原始响应文本往往更有用。

四、Codex 和 Claude Code 本质上也在配置网关

AI 编程工具的界面不同,底层配置思路却很接近:告诉工具使用哪个协议、请求地址是什么、密钥从哪里读取,以及默认使用哪个模型。

Codex:OpenAI Responses 协议

Codex 支持自定义模型供应方。一个典型的 ~/.codex/config.toml 配置如下:

toml 复制代码
model_provider = "custom_gateway"
model = "your-model-id"
model_reasoning_effort = "high"

[model_providers.custom_gateway]
name = "custom_gateway"
base_url = "https://vsoui.com/v1"
env_key = "OPENAI_API_KEY"
wire_api = "responses"

密钥仍然通过环境变量注入:

bash 复制代码
export OPENAI_API_KEY="sk-你的密钥"
codex

这里最值得注意的是 wire_api = "responses"。业务代码常用 /chat/completions,而新版 Agent 工具可能使用 Responses API。配置兼容网关时,不能只确认"兼容 OpenAI",还要确认它是否支持具体客户端使用的端点。

base_urlmodel 是配置模板,使用时替换为目标服务的实际地址和模型 ID。

Claude Code:Anthropic 协议

Claude Code 更适合走 Anthropic 兼容入口,核心配置可以通过环境变量表达:

bash 复制代码
export ANTHROPIC_AUTH_TOKEN="sk-你的密钥"
export ANTHROPIC_BASE_URL="https://vsoui.com"
export ANTHROPIC_MODEL="your-model-id"
claude

这里使用的是 Anthropic 兼容协议,而不是上一节的 OpenAI 兼容协议。不同网关对路径前缀的约定可能不同,Base URL 和模型名称都应以目标服务的接口说明为准。

如果同时使用 Codex、Claude Code、OpenCode 等工具,可以借助配置管理工具切换供应方;不过无论界面如何封装,最终都要检查四件事:

  • Base URL 是否与协议匹配;
  • 密钥读取的环境变量名是否正确;
  • 客户端使用 Chat Completions、Responses 还是 Anthropic Messages;
  • 所选模型是否支持工具调用、视觉或推理等所需能力。

五、一个统一模型网关真正需要解决什么

把请求成功转发,只完成了最基础的一步。用于真实项目时,还需要重点处理下面几类问题。

1. 超时分层

连接超时、首 Token 超时和完整响应超时应该分别观察。推理模型可能首 Token 较慢,如果只有一个很短的总超时,会把正常的深度推理误判为故障。

2. 有限重试

可以考虑重试网络错误、429 和部分 5xx,但不要无条件重试所有请求:

  • 400 一般是参数错误,重试没有意义;
  • 流式响应已向用户输出部分内容后,自动重试可能产生重复文本;
  • 带工具副作用的 Agent 请求,需要幂等键或业务去重。

推荐采用有限次数的指数退避,并为单次请求设置总时间预算。

3. 模型路由与降级

路由不只是"模型 A 失败就换模型 B"。两个模型的上下文长度、工具调用、视觉能力和输出风格可能不同。降级策略应该按能力标签配置,例如:

text 复制代码
代码任务 -> 代码模型主路由 -> 同能力模型备用路由
视觉任务 -> 支持图片输入的模型集合
批处理  -> 低成本模型 + 更严格的并发限制

4. 日志脱敏

建议记录请求 ID、模型、耗时、状态码、Token 用量和错误类型,但不要默认保存完整 Prompt。API Key、Authorization 头、用户隐私和上传文件信息必须脱敏。

5. 密钥隔离

生产密钥不应写在前端代码、仓库或截图中。至少应按环境和应用拆分密钥,并支持轮换、吊销、额度限制和异常用量告警。

6. 成本可观测

仅看请求次数不够。不同模型的输入、输出、缓存和推理 Token 价格可能不同,网关需要统一记录用量口径,才能回答"哪个功能、哪个用户、哪个模型消耗最多"。

六、统一网关并不适合所有项目

统一网关能降低接入成本,但它不是所有系统的默认答案。以下场景更应该认真评估官方直连或企业级专线方案:

  • 对数据驻留地区有明确要求;
  • 处理医疗、金融、政务等敏感数据;
  • 依赖某家厂商刚发布、尚未被兼容层支持的专属能力;
  • 需要与厂商签署单独的 SLA、DPA 或审计协议;
  • 调用规模足够大,值得直接谈企业合同和专属配额。

无论选择官方接口、自建网关还是第三方兼容服务,都建议先用非敏感数据进行验证,测试模型可用性、流式响应、工具调用、超时、错误码和用量记录,再决定是否进入正式环境。

总结

多模型时代,真正值得稳定下来的不是某个具体模型名,而是应用与模型之间的接口边界。

一个实用的统一 AI 网关,至少应该做到:

  1. 用稳定入口隔离上游变化;
  2. 明确处理不同协议的兼容边界;
  3. 统一超时、重试、限流、日志和用量;
  4. 让业务代码和 AI 编程工具复用同一套接入方式;
  5. 对安全、合规和厂商专属能力保留清晰边界。

当接入规模扩大后,还可以继续把模型能力标签、动态路由、请求幂等和成本归因拆成独立模块。这样即使上游模型持续变化,业务层仍然可以维持稳定、可测试的调用边界。

接入地址是:https://vsoui.com/

相关推荐
想要成为糕糕手1 小时前
🐎 从“幻觉”到“可控”:手把手构建一个 LLM 自优化流水线 Harness
前端·llm·agent
sunly_1 小时前
React Suspense 用法详解
前端·javascript·react.js
一心只读圣贤书1 小时前
AI 辅助前端空状态体验治理:从无数据页面到可行动引导
前端·人工智能
沐土Arvin1 小时前
音频h5录制开发
前端
PedroQue991 小时前
uni-app路由插件化:解锁高效开发新姿势
前端·uni-app
何时梦醒1 小时前
TypeScript 类型系统核心:type 与 interface 全方位深度对比(附实战案例)
前端·面试·typescript
渣波1 小时前
TS 必考题深度解析:type 与 interface 的终极对决
前端·javascript
书源1 小时前
AI 能写代码之后,前端工程师的价值在哪里?
前端·面试·程序员
恋猫de小郭2 小时前
Flutter iOS Deep Link 为什么会突然失效:一系列难以言喻的问题
android·前端·flutter