☰
Codex CLI 与 Claude Code 接入自建网关:base_url 配置与协议转换避坑指南
2026/10/17 12:00:22 网站建设 项目流程

1. 为什么要在命令行 AI 工具里接自建网关

把 Codex CLI 或者 Claude Code 这类命令行 AI 编程助手接到自建网关,本质上解决的是一个"统一入口"的问题。默认情况下,这些工具会直连官方服务,但实际团队使用时会遇到几个很现实的痛点:多个项目要分别管理密钥、不同模型要切换不同供应商、调用量需要统一统计、内网环境要走自己的出口。自建网关就是在这中间加一层转发,把请求统一收口,再按规则分发到后端。

我最初动这个念头,是因为团队里几个人各自配了不同的 key,月底对账一团乱。后来搭了一个自建网关,所有 CLI 工具都指向同一个base_url,密钥只在网关侧配置一次,客户端不再持有真实凭证。这个改动看起来简单,但真正动手时踩的坑比想象中多——base_url到底该填到哪一层、协议是走 OpenAI 兼容格式还是 Anthropic 原生格式、流式响应为什么断流、工具调用为什么报错,每一个都能卡你半天。

这篇文章适合两类人:一是已经会用 Codex CLI 或 Claude Code,想把它接到自己网关的开发者;二是正在搭 AI 网关,需要知道客户端到底会发什么请求、期望什么响应的后端同学。我会把base_url的拼接规则、两套协议的差异、流式与工具调用的坑,以及排查方法完整讲一遍。文中涉及的具体域名、密钥、项目名都用代称,你按自己的环境替换即可。

先说结论性的认知:Codex CLI 和 Claude Code 虽然都是命令行 AI 工具,但它们默认走的协议不一样。Codex CLI 更偏向 OpenAI 的 Chat Completions / Responses 风格,Claude Code 走的是 Anthropic 的 Messages API。你的自建网关如果只兼容一种协议,另一个工具就会出问题。这是所有坑的根源,理解了这一点,后面的问题基本都能推导出来。

2. base_url 到底该填到哪一层

2.1 路径拼接规则是第一个大坑

很多人第一次配base_url时,会下意识填成https://gateway.example.com/v1/chat/completions这种完整路径,结果请求直接 404。原因在于:客户端会在你填的base_url后面自动拼接它认为的路径。你填得越"完整",重复拼接就越严重。

以 OpenAI 兼容协议为例,客户端内部通常会这样拼:

最终URL = base_url + "/chat/completions"

所以正确的base_url应该填到/v1这一层:

# 正确 export OPENAI_BASE_URL="https://gateway.example.com/v1" # 错误:会变成 .../v1/chat/completions/chat/completions export OPENAI_BASE_URL="https://gateway.example.com/v1/chat/completions"

而 Anthropic 协议这边,Claude Code 期望的路径是/v1/messages,所以它的base_url同样填到/v1:

export ANTHROPIC_BASE_URL="https://gateway.example.com"

注意这里有个微妙差异:有些版本的 Claude Code 会在base_url后自动补/v1/messages,有些则要求你把/v1也带上。我实测下来,最稳妥的做法是先看工具版本对应的文档,然后用一个最简单的curl去验证网关到底收到了什么路径。

2.2 用日志反推真实请求路径

与其猜,不如让网关把收到的请求路径打出来。我在网关侧加了一行访问日志,记录method + path + 关键 header,然后跑一次 CLI 工具,立刻就能看到真相:

# 网关侧日志示例(伪代码) [2024-xx-xx] POST /v1/chat/completions host=gateway.example.com [2024-xx-xx] POST /v1/messages host=gateway.example.com

看到这个日志,你就知道客户端实际请求的是哪个路径,base_url该填到哪一层也就一目了然了。这个"先看日志再改配置"的习惯,帮我省下了大量反复试错的时间。

提示:如果你用的是反向代理(如 Nginx),记得把proxy_pass的路径重写规则也一起检查,否则网关内部转发时可能又拼错一次。

2.3 环境变量与配置文件优先级

Codex CLI 和 Claude Code 都支持环境变量和配置文件两种方式。环境变量优先级通常更高,但不同版本行为不一致。我的建议是:只保留一种配置来源,避免环境变量和配置文件打架。

常见的环境变量名有:

工具常用环境变量说明
Codex CLIOPENAI_BASE_URL/OPENAI_API_KEYOpenAI 兼容协议
Claude CodeANTHROPIC_BASE_URL/ANTHROPIC_API_KEYAnthropic 协议
通用HTTPS_PROXY走代理时用,注意别和 base_url 冲突

配置文件一般在用户目录下,比如~/.codex/config或~/.claude/settings.json之类。改完记得重启终端,环境变量不会自动刷新。

3. 两套协议的核心差异与适配

3.1 OpenAI 兼容格式 vs Anthropic Messages

这是整个接入过程中最需要理解的部分。两套协议的请求体和响应体结构差异很大,网关必须做转换,或者分别提供两个端点。

先看请求体的核心字段对比:

维度OpenAI Chat CompletionsAnthropic Messages
系统提示messages里 role=system顶层system字段
消息角色system/user/assistant/tooluser/assistant
工具调用tools+tool_callstools+tool_useblock
最大长度max_tokensmax_tokens(必填)
流式stream: truestream: true
结束原因finish_reasonstop_reason

最典型的差异是系统提示的位置。OpenAI 把它当成一条 role=system 的消息塞进messages数组,Anthropic 则要求单独放在顶层system字段。如果你的网关只做简单透传,Claude Code 发来的请求里system字段就会被丢掉,模型行为立刻变得奇怪。

3.2 工具调用格式的映射

工具调用是第二个重灾区。OpenAI 的响应里,工具调用长这样:

{ "choices": [{ "message": { "tool_calls": [{ "id": "call_abc", "type": "function", "function": {"name": "read_file", "arguments": "{\"path\":\"a.txt\"}"} }] } }] }

Anthropic 的响应则是 content block 数组:

{ "content": [ {"type": "text", "text": "..."}, {"type": "tool_use", "id": "toolu_abc", "name": "read_file", "input": {"path": "a.txt"}} ], "stop_reason": "tool_use" }

注意两个关键点:一是arguments是字符串,input是对象;二是 Anthropic 用stop_reason: tool_use表示要调工具,OpenAI 用finish_reason: tool_calls。网关做协议转换时,这两处必须正确映射,否则客户端要么解析失败,要么根本不知道要执行工具。

我踩过的一个坑是:网关把arguments字符串原样塞进了input,结果客户端拿到的是一个字符串而不是对象,工具参数解析直接报错。正确做法是JSON.parse(arguments)之后再放进input。

3.3 流式响应的 SSE 事件格式

流式响应两边都用 SSE(Server-Sent Events),但事件结构完全不同。

OpenAI 的流式 chunk:

data: {"choices":[{"delta":{"content":"你"}}]} data: {"choices":[{"delta":{"content":"好"}}]} data: [DONE]

Anthropic 的流式事件:

event: content_block_delta data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"你"}} event: message_stop data: {"type":"message_stop"}

差异点在于:Anthropic 有明确的事件类型(event 字段),OpenAI 只有 data。而且 Anthropic 的流式有完整的生命周期事件:message_start、content_block_start、content_block_delta、content_block_stop、message_delta、message_stop。网关如果只转发data而丢掉event行,Claude Code 会解析不出内容。

注意:做协议转换时,流式部分是最容易出 bug 的。建议先用非流式跑通,再开流式,逐段对比原始响应和转换后响应。

4. 实操:从零搭一个能同时接两个工具的网关

4.1 网关的最小可用结构

我用一个轻量 HTTP 服务做演示,核心逻辑就三件事:识别请求路径、转换协议、转发到后端。下面是一个简化的伪代码结构:

# 伪代码,仅示意逻辑 def handle_request(req): path = req.path if path.endswith("/chat/completions"): # OpenAI 协议入口 body = convert_openai_to_backend(req.json) resp = forward(body) return convert_backend_to_openai(resp) elif path.endswith("/messages"): # Anthropic 协议入口 body = convert_anthropic_to_backend(req.json) resp = forward(body) return convert_backend_to_anthropic(resp) else: return 404

关键设计点是:两个入口分别做各自的协议转换,后端只认一种内部格式。这样后端供应商换起来也方便,不用改客户端。

4.2 参数计算:max_tokens 与上下文窗口

接入时经常遇到max_tokens相关的报错。原因是不同模型对max_tokens的上限要求不同,而客户端默认值可能超出后端限制。

我的处理方式是:在网关侧做一次 clamp(截断)。比如后端模型最大输出 8192,客户端传了 16384,网关就把它压到 8192,而不是直接报错。计算逻辑很简单:

MAX_OUTPUT = 8192 def clamp_max_tokens(requested): if requested is None: return MAX_OUTPUT return min(requested, MAX_OUTPUT)

另外要注意上下文窗口。如果客户端把整个对话历史都发过来,加上系统提示可能超过模型上下文,网关需要做截断或摘要。我一般保留最近 N 轮对话,N 根据模型窗口和平均消息长度估算,比如窗口 128k、平均每条消息 500 token,那保留 200 条左右是安全的。

4.3 密钥与鉴权头的处理

客户端发来的鉴权头,OpenAI 用Authorization: Bearer xxx,Anthropic 用x-api-key: xxx。网关需要统一识别,然后替换成后端真实密钥。

def extract_key(headers): if "authorization" in headers: return headers["authorization"].replace("Bearer ", "") if "x-api-key" in headers: return headers["x-api-key"] return None

这里有个安全细节:网关侧要校验客户端传来的 key 是否合法,不能无脑转发。我一般维护一个允许列表,只有列表内的 key 才放行,否则直接 401。这样即使客户端 key 泄露,也能快速吊销。

4.4 完整请求链路验证

搭好之后,用curl分别验证两个协议入口:

# 验证 OpenAI 兼容入口 curl -s https://gateway.example.com/v1/chat/completions \ -H "Authorization: Bearer test-key" \ -H "Content-Type: application/json" \ -d '{"model":"gpt-x","messages":[{"role":"user","content":"hi"}]}' # 验证 Anthropic 入口 curl -s https://gateway.example.com/v1/messages \ -H "x-api-key: test-key" \ -H "anthropic-version: 2023-06-01" \ -H "Content-Type: application/json" \ -d '{"model":"claude-x","max_tokens":100,"messages":[{"role":"user","content":"hi"}]}'

两个都返回正常内容后,再把 CLI 工具的base_url指过来。这个顺序很重要:先验证网关本身,再验证客户端,否则出问题你分不清是哪一层的锅。

5. 常见问题与排查速查表

5.1 报错速查表

现象可能原因排查方向
404 Not Foundbase_url 路径拼错看网关访问日志的真实 path
401 Unauthorized鉴权头格式不对检查 Bearer / x-api-key
400 max_tokens 超限客户端默认值过大网关侧 clamp
流式无输出SSE event 行丢失检查是否转发 event 字段
工具调用报错arguments 未解析成对象检查 JSON.parse
响应截断上下文超窗口做历史截断或摘要
系统提示失效system 字段被丢弃检查协议转换逻辑

5.2 流式断流的排查思路

流式断流是最烦的问题,表现是输出到一半突然停住。我的排查顺序是:

  1. 先看网关日志,确认后端是否正常返回了完整流。
  2. 抓包对比,看客户端收到的 SSE 事件是否完整。
  3. 检查缓冲,有些反向代理会缓冲响应,导致流式变成一次性返回。Nginx 需要关掉proxy_buffering。
  4. 检查超时,长响应可能触发网关或代理的超时设置。
# Nginx 关闭缓冲,保证流式 proxy_buffering off; proxy_cache off; proxy_read_timeout 300s;

5.3 独家避坑经验

几个文档里不会写、但实际很关键的技巧:

  • 版本锁定:CLI 工具更新频繁,协议细节可能变。生产环境建议锁定版本,升级前先在测试环境验证。
  • 保留原始请求日志:出问题时,能回放原始请求是最快的定位方式。注意脱敏,别把密钥打进日志。
  • 双协议端点分开:不要试图用一个端点同时兼容两种协议,靠路径区分最清晰。
  • 健康检查:网关加一个/health端点,CLI 工具启动时可以先探活,避免配置错误时一脸懵。
  • 超时分层设置:连接超时短一点(如 5s),读取超时长一点(如 300s),适配长响应。

提示:如果你在团队内推广这套方案,建议写一份"配置模板 + 排查手册",把 base_url 填法、环境变量、常见报错都列清楚,能省下大量重复答疑。

6. 协议转换中的几个细节陷阱

6.1 stop_reason 与 finish_reason 的映射

前面提过这两个字段,但实际映射时还有细节。OpenAI 的finish_reason有stop、length、tool_calls、content_filter等值,Anthropic 的stop_reason有end_turn、max_tokens、tool_use、stop_sequence。映射关系大致是:

OpenAIAnthropic
stopend_turn
lengthmax_tokens
tool_callstool_use
content_filter(无直接对应,需自定义)

如果映射错了,客户端可能误判响应是否结束,导致对话提前终止或死循环。

6.2 多模态内容的处理

如果客户端发送图片等多模态内容,两套协议的结构差异更大。OpenAI 用content数组里的image_url,Anthropic 用imageblock 加 base64。网关做转换时要注意:图片数据体积大,转换时别做无谓的拷贝,直接透传或流式处理。

6.3 错误响应的格式统一

后端出错时,两套协议的错误格式也不同。OpenAI 是:

{"error": {"message": "...", "type": "..."}}

Anthropic 是:

{"type": "error", "error": {"type": "...", "message": "..."}}

网关需要把后端错误转换成客户端期望的格式,否则客户端可能显示一个无意义的解析错误,掩盖了真实问题。

7. 性能与稳定性的一些实测体会

7.1 连接复用能省不少时间

CLI 工具会频繁发请求,如果网关每次都新建到后端的连接,握手开销累积起来很可观。我在网关侧启用了 HTTP 连接池,实测下来整体延迟能降 20% 到 30%。配置上就是复用同一个 client 实例,别每次请求都 new 一个。

7.2 重试要谨慎

网络抖动时重试是必要的,但流式请求重试要特别小心,因为可能已经输出了一部分内容,重试会导致重复输出。我的做法是:非流式请求可以自动重试 2 到 3 次,流式请求只在连接建立阶段重试,一旦开始输出就不再重试。

7.3 限流与配额

团队使用时,网关侧加一层限流很有必要。可以按 key 或按用户维度限制每分钟请求数,避免某个人跑批量任务把配额打满。实现上用一个简单的滑动窗口计数器就够,不必上重型方案。

8. 写在最后的一点个人经验

这套东西我从第一次配到跑顺,前后折腾了差不多一周,大部分时间都花在"猜客户端到底发了什么"上。后来养成习惯:任何接入问题,第一步永远是看网关收到的原始请求。日志一打,路径、header、body 全清楚了,剩下的就是对照协议文档做映射。

另外一个深刻体会是,别迷信"兼容"两个字。很多网关号称兼容 OpenAI 协议,但实际只兼容了最基础的对话,工具调用、流式、多模态这些高级特性一测就露馅。选网关或者自己写的时候,一定要用真实的 CLI 工具去跑一遍完整流程,而不是只测一个curl就完事。

最后分享一个小技巧:如果你同时用 Codex CLI 和 Claude Code,可以在网关侧给它们分配不同的路径前缀,比如/openai/*和/anthropic/*,这样日志和限流都能分开统计,排查问题时一眼就能看出是哪个工具在报错。这个改动很小,但后期维护省心很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询