资讯动态

自建网关接入 Codex CLI 与 Claude Code:base_url 与协议踩坑全解析

发布时间:2026/10/19 2:24:14 来源:尧图企业网站定制
1. 为什么自建网关接入 CLI 工具总在 base_url 上翻车把 Codex CLI 或者 Claude Code 这类命令行 AI 编程助手接到自建网关听起来就是把base_url一改、api_key一填的事。但真正动过手的人都知道这一步能卡掉大半天。我自己前前后后配过五六套不同的网关环境从最简单的反向代理到带鉴权、带路由、带协议转换的中间层几乎每一种都在base_url和协议细节上踩过坑。先说清楚这篇要解决什么问题。Codex CLI 和 Claude Code 本质上都是客户端它们默认会去请求官方托管的接口地址。而自建网关的核心价值在于把请求收敛到自己可控的入口做统一鉴权、日志审计、多模型路由、成本统计或者在内网环境下隔离外部依赖。一旦你决定走自建网关客户端发出的每一个请求——包括路径、请求头、请求体格式、流式响应的解析方式——都必须和网关的预期严丝合缝地对上否则就是各种 404、401、400 甚至请求发出去了但一直转圈。这里最容易出问题的就是base_url。很多人以为它就是一个域名前缀填个https://gateway.example.com就完事。实际上base_url的语义在不同工具里差别很大有的工具会自己在后面拼/v1/messages有的会拼/v1/chat/completions还有的会拼/v1/responses。你填的base_url到底该不该带/v1取决于工具内部拼接逻辑填错一层就是 404。另一个高频翻车点是协议。Codex CLI 走的是 OpenAI 风格的接口协议Claude Code 走的是 Anthropic 风格的接口协议两者在请求体结构、鉴权头字段、流式事件格式上都不一样。如果你的网关只支持其中一种另一种接上去就会报格式错误。很多人图省事用一个兼容层网关同时接两种协议结果发现流式输出时事件类型对不上客户端直接卡死或者只吐一半内容。这篇内容适合三类人看第一类是自己搭了网关、想把 CLI 工具接进来的开发者第二类是运维或平台同学要给团队统一提供 AI 编程助手的入口第三类是纯粹被base_url和协议问题折磨过、想搞清楚底层逻辑的人。我会把踩坑的完整链路、每个参数的来龙去脉、以及实测可用的配置方式都摊开讲尽量让你少走我走过的弯路。提示本文讨论的所有网关均为自建、内网或私有部署场景不涉及任何第三方托管服务的具体配置。所有示例域名、路径均为虚构代称请根据自己实际环境替换。2. base_url 的拼接逻辑多一层少一层都是 4042.1 客户端到底在 base_url 后面拼了什么要搞清楚base_url怎么填第一步是弄明白客户端内部到底怎么拼路径。我一开始也懒得看直接凭感觉填结果连续 404 了七八次才回头去翻源码和抓包。结论是不同工具、不同版本拼接规则不一样而且有些工具会在文档里写得含糊其辞。以 Codex CLI 为例它默认请求的是 OpenAI 风格的接口。当你设置base_url后它通常会在后面拼接/v1/responses或/v1/chat/completions取决于具体调用模式。也就是说如果你填的base_url是https://gateway.example.com/v1最终请求路径可能变成https://gateway.example.com/v1/v1/responses多了一层/v1网关自然找不到路由。Claude Code 这边则是 Anthropic 风格默认拼接的是/v1/messages。同样地如果你在base_url里已经带了/v1就会变成/v1/v1/messages。我实测下来最稳妥的做法是先抓包再填值。具体操作是先把base_url指向一个本地能打印请求日志的临时服务比如用一个最简单的 HTTP 服务把收到的路径和请求头打出来然后运行一次 CLI 工具看它实际请求的完整路径是什么。拿到真实路径后再反推base_url应该填到哪一层。下面这张表是我实测几种常见填法和最终请求路径的对应关系供你对照工具base_url 填法实际请求路径结果Codex CLIhttps://gw.example.comhttps://gw.example.com/v1/responses正常Codex CLIhttps://gw.example.com/v1https://gw.example.com/v1/v1/responses404Claude Codehttps://gw.example.comhttps://gw.example.com/v1/messages正常Claude Codehttps://gw.example.com/v1https://gw.example.com/v1/v1/messages404Claude Codehttps://gw.example.com/apihttps://gw.example.com/api/v1/messages取决于网关路由注意上表是常见版本的实测结果不同版本可能有差异。核心原则是base_url 只填到域名或网关路由前缀不要带协议版本号除非你的网关明确要求带。2.2 网关侧的路由设计怎么接住客户端的拼接理解了客户端怎么拼网关侧的路由就好设计了。我的建议是网关不要假设客户端会拼什么而是把常见路径都显式注册一遍。比如同时注册/v1/responses、/v1/chat/completions、/v1/messages三个路由分别转发到对应的后端处理逻辑。这样无论客户端拼哪一层都能被接住。如果你用的是 Nginx 做反向代理一个常见的坑是proxy_pass结尾有没有斜杠。带斜杠和不带斜杠转发后的路径完全不一样。比如location /v1/ { proxy_pass https://backend.example.com/; }这种写法会把/v1/messages转发成https://backend.example.com/messages把/v1前缀吃掉。而如果写成proxy_pass https://backend.example.com不带结尾斜杠则会保留完整路径转发成https://backend.example.com/v1/messages。这个细节我在第一次配的时候完全没注意导致后端一直收到缺前缀的路径排查了半天才发现是 Nginx 的斜杠问题。2.3 一个可复用的 base_url 排查流程踩了多次坑之后我总结出一套固定的排查流程基本能在十分钟内定位问题确认客户端版本不同版本的拼接逻辑可能不同先--version看一眼。本地抓包把base_url指向本地临时服务打印完整请求路径和请求头。对照网关路由看网关注册了哪些路径和实际请求路径是否匹配。检查代理层如果中间有 Nginx 或其他反向代理检查proxy_pass的斜杠和路径重写规则。逐层验证先用curl直接打网关确认网关本身能通再让客户端打网关。这套流程的核心思路是把客户端到网关和网关到后端两段分开验证不要混在一起调。很多人一上来就端到端测结果不知道问题出在哪一段来回改配置反而越改越乱。3. 协议差异OpenAI 风格和 Anthropic 风格到底差在哪3.1 请求体结构一个用 messages一个也用 messages 但字段不同很多人以为 OpenAI 和 Anthropic 的接口都是messages数组应该差不多。实际用起来才发现字段名、嵌套结构、必填项都不一样。OpenAI 风格的请求体大致是这样{ model: gpt-4o, messages: [ {role: system, content: You are a helpful assistant.}, {role: user, content: Hello} ], stream: true }Anthropic 风格的请求体则是这样{ model: claude-3-5-sonnet, system: You are a helpful assistant., messages: [ {role: user, content: Hello} ], max_tokens: 1024, stream: true }几个关键差异Anthropic 的system是顶层字段不是放在messages里的Anthropic必须指定max_tokens不指定直接报错OpenAI 的system是作为messages里的一条消息。如果你的网关只做简单转发不做协议转换那客户端发什么格式后端就得支持什么格式。如果后端只支持一种另一种就会 400。我遇到过一个典型场景网关后端接的是一个只支持 OpenAI 格式的推理服务但团队里有人用 Claude Code发出来的是 Anthropic 格式。结果就是请求体里system字段位置不对、缺max_tokens后端直接拒绝。解决办法是在网关层加一个协议转换中间件把 Anthropic 格式转成 OpenAI 格式再转发。3.2 鉴权头x-api-key 和 Authorization 不能混鉴权头是另一个高频翻车点。OpenAI 风格用的是Authorization: Bearer tokenAnthropic 风格用的是x-api-key: token。有些网关只检查其中一种另一种就直接 401。更麻烦的是有些客户端在设置api_key后会同时带上两种头或者带一种但格式不对。我见过一个案例客户端配置里填了 key但网关日志显示收到的Authorization头是空的只有x-api-key有值。原因是这个客户端版本对 Anthropic 协议只发x-api-key而网关只认Authorization。最后是在网关侧同时兼容两种头才解决。我的建议是网关侧对鉴权头做兼容处理两种都认。具体逻辑是优先读Authorization如果没有再读x-api-key两者取到任意一个有效 token 就放行。这样无论客户端发哪种都能正常工作。3.3 流式响应SSE 事件格式的坑最深流式响应是协议差异里最隐蔽、最难排查的部分。两种协议都用 SSEServer-Sent Events但事件格式不一样。OpenAI 风格的流式响应大致是data: {choices:[{delta:{content:Hello}}]} data: [DONE]Anthropic 风格的流式响应则是event: content_block_delta data: {type:content_block_delta,delta:{type:text_delta,text:Hello}} event: message_stop data: {type:message_stop}差异很明显Anthropic 有event:行事件类型在type字段里OpenAI 没有event:行结束标志是[DONE]。如果网关做协议转换时只转data不转event或者结束标志没对上客户端就会一直等表现为输出到一半卡住或者一直转圈不出内容。我踩过最坑的一次是网关把 Anthropic 的流式响应转成 OpenAI 格式时忘了把message_stop转成[DONE]。结果 Claude Code 收到流结束后还在等[DONE]界面一直显示加载中实际上后端早就返回完了。这种问题抓包一看就清楚但不抓包的话很难想到是结束标志的问题。提示调试流式响应时建议在网关侧把原始 SSE 流完整打印出来逐行对比客户端期望的格式。不要只看有没有返回内容要看结束标志对不对。4. 网关选型与配置几种常见方案的实测对比4.1 直接反向代理最简单但最不灵活最省事的方案是用 Nginx 或 Caddy 做纯反向代理把客户端的请求原样转发到后端。这种方案适合后端本身就支持客户端协议的场景比如后端就是一个兼容 OpenAI 协议的服务客户端也是 OpenAI 风格那直接转发就行。优点是配置简单、性能好、没有额外依赖。缺点是没有任何协议转换能力一旦客户端和后端协议不一致就无能为力。而且鉴权、日志、限流这些都得靠 Nginx 自己配稍微复杂一点的需求就要写 Lua 或者上 OpenResty。我用这种方案接过一次纯 OpenAI 风格的场景配置大概是这样location /v1/ { proxy_pass https://backend.example.com; proxy_set_header Authorization $http_authorization; proxy_set_header x-api-key $http_x_api_key; proxy_buffering off; proxy_cache off; }关键是proxy_buffering off流式响应必须关掉缓冲否则客户端会等到整个响应结束才收到内容流式就失去意义了。4.2 带协议转换的中间层灵活但要多写代码如果客户端和后端协议不一致就需要一个中间层做转换。我一般用 Python 的 FastAPI 或者 Node.js 的 Express 写一个轻量服务接收客户端请求转换成后端格式再把响应转回来。这种方案的核心工作量在流式转换上。因为流式响应是逐块到达的你不能等整个响应收完再转换必须边收边转边发。实现时要注意几点一是保持 SSE 的data:前缀和空行分隔二是正确处理结束标志三是转换过程中如果出错要能优雅地结束流而不是直接断开。我写过一个 Anthropic 转 OpenAI 的中间层核心逻辑大概是收到 Anthropic 请求后把system字段提到messages开头补上默认max_tokens把x-api-key转成Authorization响应侧则把content_block_delta转成choices[].delta.content把message_stop转成[DONE]。这套逻辑跑通之后Claude Code 就能正常走 OpenAI 后端了。4.3 几种方案的成本与适用场景对比方案配置复杂度协议转换适用场景维护成本纯反向代理低不支持客户端后端协议一致低中间层转换中高支持协议不一致、需统一入口中网关框架中插件支持多模型、多租户、需审计中高自研服务高完全可控特殊需求、深度定制高选型时我的经验是先用最简单的方案跑通再按需加复杂度。很多人一上来就上重型网关框架结果配置项一大堆光搞清楚每个插件干嘛的就花了两天最后发现其实一个 Nginx 加几行配置就够了。反过来如果你的场景确实需要多模型路由、按用户限流、成本统计那早点上框架反而省事。5. 实测踩坑链路从 404 到流式卡死的完整排查5.1 第一坑base_url 多带了一层 /v1最开始我把base_url填成了https://gw.example.com/v1心想接口都是 /v1 开头的带上肯定没错。结果客户端请求的是/v1/v1/messages网关返回 404。当时第一反应是网关路由没配好查了半天路由配置最后抓包才发现是路径多了一层。这个坑的本质是没有搞清楚 base_url 的语义边界。base_url 是基础地址客户端会在这个基础上拼具体路径。你把版本号也塞进 base_url就等于让客户端拼出重复的版本号。正确做法是 base_url 只填到域名或网关前缀版本号交给客户端拼。5.2 第二坑鉴权头对不上导致 401路径修好之后紧接着就是 401。网关日志显示请求进来了但鉴权失败。抓包一看客户端发的是x-api-key而网关只检查Authorization。这个问题的根源是客户端协议风格和网关鉴权逻辑不匹配。解决办法有两种一是改客户端配置让它发Authorization但有些客户端不支持改二是在网关侧兼容两种头。我选了第二种因为改网关比改客户端可控。兼容逻辑很简单先读Authorization读不到再读x-api-key任意一个有有效值就放行。5.3 第三坑请求体缺 max_tokens 被后端拒绝鉴权过了之后后端返回 400提示缺max_tokens。这是因为 Claude Code 发的是 Anthropic 格式而我的后端是 OpenAI 风格OpenAI 风格里max_tokens是可选的但 Anthropic 格式里是必填的。中间层转换时如果没补这个字段后端就会因为请求体不符合预期而拒绝。这个坑提醒我协议转换不能只转看起来不一样的字段还要补全对方必填但自己可选的字段。Anthropic 的max_tokens、system位置OpenAI 的messages结构这些都要在转换时处理到位。5.4 第四坑流式响应结束标志没转导致卡死最隐蔽的是流式卡死。前面都通了内容也能出来但输出到一半就停住界面一直转圈。抓包发现后端已经发完了message_stop但中间层没把它转成[DONE]客户端一直在等结束标志。这个问题的排查难度在于内容出来了容易以为已经通了不会想到是结束标志的问题。我的经验是调试流式响应时一定要把完整的 SSE 流打出来逐行看结束标志对不对。OpenAI 的结束标志是data: [DONE]Anthropic 的是event: message_stop转换时必须一一对应。5.5 排查链路总结分层验证是王道把这四个坑串起来看排查链路其实很清晰先验证路径404再验证鉴权401再验证请求体400最后验证流式卡死。每一层验证通过再进下一层不要跳步。我后来固定用一套分层验证方法第一步用curl直接打网关确认网关本身能通第二步用curl模拟客户端请求体确认后端能处理第三步才让真实客户端接入。这样每层的问题都能单独定位不会互相干扰。6. 配置模板与可复用的参数清单6.1 客户端侧配置要点Codex CLI 和 Claude Code 的配置方式不太一样但核心参数就几个base_url、api_key、model。下面是我实测可用的配置思路具体字段名以你使用的版本为准对于 Codex CLI通常通过环境变量或配置文件设置export OPENAI_BASE_URLhttps://gw.example.com export OPENAI_API_KEYyour-token对于 Claude Code类似地export ANTHROPIC_BASE_URLhttps://gw.example.com export ANTHROPIC_API_KEYyour-token关键点是base_url 不带版本号api_key 填网关认可的 token。如果网关做了鉴权头兼容两种头都能过。6.2 网关侧路由与转发清单网关侧我建议按这张清单逐项检查检查项说明常见错误路由注册注册 /v1/messages、/v1/responses 等只注册了一种鉴权兼容同时认 Authorization 和 x-api-key只认一种流式缓冲关闭 proxy_buffering开着导致流式失效超时设置流式请求超时要够长默认超时太短导致中断路径重写注意 proxy_pass 斜杠多斜杠或少斜杠请求体透传保持原始 JSON 结构中间件误改字段6.3 一个最小可用的中间层转换逻辑如果你需要做协议转换下面是一个简化的转换逻辑示意Python 伪代码仅表达思路def anthropic_to_openai(req): messages [] if system in req: messages.append({role: system, content: req[system]}) messages.extend(req[messages]) return { model: req[model], messages: messages, max_tokens: req.get(max_tokens, 4096), stream: req.get(stream, False) } def openai_stream_to_anthropic(chunk): # 把 OpenAI 的 delta 转成 Anthropic 的 content_block_delta if chunk [DONE]: return event: message_stop\ndata: {\type\:\message_stop\}\n\n content chunk[choices][0][delta].get(content, ) return fevent: content_block_delta\ndata: {{\type\:\content_block_delta\,\delta\:{{\type\:\text_delta\,\text\:\{content}\}}}}\n\n这段逻辑的核心是请求侧补全字段、响应侧对齐事件格式。实际生产环境还要处理错误、超时、并发等问题但思路是一致的。7. 几个只有踩过才知道的细节7.1 模型名映射不能想当然客户端配置里填的模型名和后端实际支持的模型名往往不一样。比如客户端填claude-3-5-sonnet后端可能只认claude-3-5-sonnet-20241022这种带日期的完整名。如果网关不做映射请求就会因为模型名不匹配而失败。我的做法是在网关侧维护一张映射表把客户端常用的简写映射到后端支持的完整名。这样客户端不用改后端也不用改映射逻辑集中在网关。7.2 超时设置要区分流式和非流式流式请求的耗时可能很长尤其是生成长文本时。如果网关或反向代理的超时设置太短流式响应会在中途被切断表现为输出到一半停了。我一般把流式请求的超时设到 300 秒以上非流式请求可以短一些。Nginx 里对应的配置是proxy_read_timeout和proxy_send_timeout默认 60 秒流式场景下明显不够。7.3 日志要打全但别打敏感信息调试阶段日志越全越好请求路径、请求头、请求体、响应状态都打出来。但要注意别把 api_key 完整打进日志尤其是生产环境。我的做法是日志里只打 key 的前几位和后几位中间用星号代替既能定位问题又不泄露。7.4 版本升级后要重新验证CLI 工具升级后拼接逻辑、协议细节都可能变。我遇到过升级后 base_url 拼接规则变了原来能用的配置突然 404。所以每次升级后建议重新跑一遍分层验证流程别假设以前能用现在也能用。8. 写在最后的一点个人体会这套东西我前后折腾了大概两周从最开始的一头雾水到后来能快速定位问题最大的感受是base_url 和协议问题本质上不是配置问题而是理解问题。你只要真正搞清楚了客户端怎么拼路径、怎么发请求、怎么解析响应剩下的就是按图索骥。我现在的习惯是接任何新工具之前先花十分钟抓个包把请求路径、请求头、请求体、响应格式都看一遍再动手配。这十分钟能省掉后面几个小时的瞎猜。另外分层验证真的能救命别嫌麻烦路径、鉴权、请求体、流式一层层过每层都确认没问题再往下走。如果你也在接自建网关遇到卡住的地方先别急着改配置先抓包看看实际请求长什么样。大部分问题抓包一看就明白了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑