跨模型调用实战:Claude Code与Codex互通的协议转换指南
2026/9/9 6:55:38 网站建设 项目流程

你有没有过这种时刻:电脑上同时装着 Claude Code 和 Codex,一个是以"会读项目上下文"出名的终端智能体,一个是 OpenAI 家把指令遵循做到极致的选手。可你打开 Claude Code,它只能调 Claude 家的模型;打开 Codex,它只能调 GPT 系列。明明你手里有 GPT-4o 的额度,也明明 Claude 的模型在处理某些重构任务时就是更强——但工具和模型之间就像隔了一堵墙,谁都不肯跨过去半步。

这堵墙不是能力差距,不是算力限制,而是协议。Claude Code 只讲 Anthropic Messages API 那套方言,Codex 只认 OpenAI 的接口规矩。两边请求结构、鉴权头、流式事件全不一样,硬接根本接不上。这篇就把我的完整拆墙过程写出来:怎么让 Claude Code 跑 GPT-4o,怎么让 Codex 跑 Claude,中间踩了什么坑,尤其是那个cc switch local proxy failed while handling codex endpoint /responses的报错,我是怎么一步步查明白并解决的。全程不说废话,能直接照着抄。

1. 先搞明白被困住的是什么:工具与模型之间的"接口方言"

1.1 现状:好用的工具被绑死在自家模型上

先说个我自己的使用场景。我同时在两个项目组干活,一个重度依赖 Claude Code 的自动改代码流程,它能把 todo、搜索、编辑、测试串成一个完整闭环;另一个项目风格偏保守,团队习惯用 ChatGPT 生态,Codex CLI 已经写在内部文档里了。

问题来了:Claude Code 在处理那种"跨文件重构 + 保留原有风格"的活确实有一套,但有些时候我需要让 GPT-4o 来掌舵,因为它在某些指令理解上更听话、纠错更利索。反过来,Codex 的沙盒执行和并行工具调用是真舒服,可我想让它用 Claude 的模型来跑复杂推理,别老是被 GPT 系的"过度自信"坑。

按正常思路,这根本不复杂——把 API 地址换一下不就行了?实测下来根本不行。Claude Code 启动时会读ANTHROPIC_BASE_URLANTHROPIC_AUTH_TOKEN,它发出的请求是POST /v1/messages,请求体带anthropic-version头;Codex CLI 走的是 OpenAI 的POST /v1/responses或者兼容的chat/completions,鉴权用的是Authorization: Bearer。两边语言都不一样,你把 Claude Code 的地址指向 OpenAI,OpenAI 直接甩你一个 404;你把 Codex 指向 Anthropic,报错更是五花八门。

这不是配错环境变量的问题,是两套 API 的根本差异。就像 Modbus 和 CAN 总线都是工业现场总线,但你不能把 Modbus 帧直接怼到 CAN 收发器上——中间必须有个网关做协议转换。当时我就想明白了:我要写的不是一个"配置教程",而是一个协议网关。

1.2 协议墙的本质,以及为什么不能用"简单映射"糊弄过去

很多人第一反应是:协议转换不就是字段改名吗?"system" 改成 "system","user" 改 "user",把x-api-key换成Authorization,不就行了?

真不是这么简单。两套协议在三个层面有本质区别。

第一是消息结构。Anthropic 把系统提示词放在请求体的system字段里,消息数组里只有userassistant;而 OpenAI 把系统提示词塞进messages数组里当system角色。这事看起来是小事,但 Claude Code 内部会频繁把上下文压缩、重写、拼接系统提示词,转换层要保证顺序和内容都别丢。

第二是工具调用的信息组织方式。Anthropic 用content块数组来表达工具调用,一个回复可以同时包含文本块和tool_use块,工具结果用tool_result块回传;OpenAI 则是在 assistant 消息上挂tool_calls数组,工具结果单独一条role: "tool"消息,而且工具参数是 JSON 字符串,不是结构化对象。转换的时候,一个不小心参数就解析错了,模型拿到残缺的工具入参,能给你把代码改得面目全非。

第三是流式事件。两台工具都靠 SSE 流式输出逐字渲染,但 Anthropic 的事件是content_block_deltamessage_delta,OpenAI 的则是choices[0].delta。转换层要是没把停止原因(stop_reason/finish_reason)映射对,工具会一直等在那里,表现就是"转圈圈不走下一步"。

所以结论是:拆墙必须有一个完整的双向翻译层,而且不能只翻译请求,还要翻译响应、翻译流、翻译错误信息。这也是后面所有内容的出发点。

2. 协议差异逐个拆:Anthropic Messages API 与 OpenAI API 的对照

2.1 端点、鉴权与版本头:第一道硬门槛

我把两套协议的关键差异整理成了一张对照表,后面所有的转换代码都是围绕这张表写的。

对比项Anthropic Messages APIOpenAI Chat Completions / Responses
端点POST /v1/messagesPOST /v1/chat/completionsPOST /v1/responses
鉴权方式x-api-key: 你的keyAuthorization: Bearer 你的key
版本头必须带anthropic-version: 2023-06-01
系统提示词请求体顶层system字段消息数组里的system角色
工具调用content块数组(tool_use/tool_resultassistant 的tool_calls+ 单独tool消息
流式事件content_block_delta/message_deltachoices[0].delta(chat)或response.output_text.delta(responses)
停止原因stop_reason(end_turn / tool_use / max_tokens)finish_reason(stop / tool_calls / length)

这里最坑的是版本头。Anthropic 要求所有请求必须带anthropic-version,少了直接 400。而 OpenAI 那边的 SDK 根本不会发这个头。反过来,OpenAI 要求Authorization: Bearer,Anthropic 那边虽然也认这种鉴权方式,但 Error 信息写得含含糊糊,第一次调的时候很容易被误导。

我当时在转换层里对头的处理写得很死:从入站请求里解析出 Anthropic 风格的鉴权,出站请求统一重写成 OpenAI 风格。注意,key 的选择权和安全性要留在本地,转换层只是"翻译"而不是"保管"密钥。我在本地跑了一个监听 127.0.0.1 的小服务,把 Anthropic 的 key 映射到 OpenAI 的 key,这个映射关系放在环境变量里,谁改了都逃不过 git diff。

2.2 消息结构的深层差异:system 角色怎么安放

Anthropic 的请求体大概长这样:

{ "model": "claude-sonnet-4-20250514", "max_tokens": 8192, "system": "你是一个资深 Python 工程师……", "messages": [ {"role": "user", "content": "看一下这个项目的结构"}, {"role": "assistant", "content": [ {"type": "text", "text": "我先扫描一下目录。"} ]} ], "tools": [ {"name": "read_file", "description": "读取文件", "input_schema": {"type": "object", "properties": {"path": {"type": "string"}}}} ] }

OpenAI chat 接口是这样的:

{ "model": "gpt-4o", "messages": [ {"role": "system", "content": "你是一个资深 Python 工程师……"}, {"role": "user", "content": "看一下这个项目的结构"}, {"role": "assistant", "content": "我先扫描一下目录。"} ], "tools": [ {"type": "function", "function": {"name": "read_file", "description": "读取文件", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}}}} ] }

差异点非常明显:Anthropic 的systemmessages是分开的两层,OpenAI 则是一个平的messages数组。转换层要做的是把 system 字段抽出来unshift到数组最前面,同时还要注意 Claude Code 在运行过程中可能多次更新 system 内容,得保证每次都同步过来。

另一个隐蔽差异是content的形态。Anthropic 的user消息 content 既可以是字符串也可以是块数组,OpenAI 的 content 基本只接受字符串(多模态才会有image_url之类的块)。我在转换层里做了一个统一处理:所有块数组都先序列化拼成纯文本,图片块转成 data URL 形式传给 GPT-4o 的多模态接口,这样 Claude Code 里那些"看截图写样式"的场景也能在 GPT-4o 上跑。

2.3 工具调用:两套完全不同的"手"和"口"

工具调用是整个协议转换里最容易翻车的地方。Claude Code 之所以强,很大程度是因为它的工具定义极其丰富——读文件、编辑文件、执行命令、搜索,全部通过tool_use块完成。要让 GPT-4o 接住这些工具,转换层必须把 Anthropic 的工具 schema 转成 OpenAI 的 function schema,再把返回的 tool_calls 转回 tool_use 块。

Anthropic 侧一个典型的工具调用响应片段:

{ "content": [ {"type": "text", "text": "我来读取这个文件"}, {"type": "tool_use", "id": "toolu_01ABC", "name": "read_file", "input": {"path": "/tmp/project/main.py"}} ], "stop_reason": "tool_use" }

OpenAI 侧对应的响应:

{ "choices": [{ "message": { "role": "assistant", "content": "我来读取这个文件", "tool_calls": [{ "id": "call_01XYZ", "type": "function", "function": {"name": "read_file", "arguments": "{\"path\": \"/tmp/project/main.py\"}"} }] }, "finish_reason": "tool_calls" }] }

注意几个关键转换点:

  • tool_use.idtool_calls[i].id可以互相映射,但必须保留原 ID,因为工具执行结果要靠 ID 回填。
  • arguments在 OpenAI 侧是JSON 字符串,得JSON.parse一下再塞进 Anthropic 的input对象。
  • OpenAI 的 assistant 消息在同一轮里只能有一套tool_calls,而 Anthropic 可以在一个content数组里混合多个文本块和工具块,转换时要把文本和工具调用拆开再重组。

工具结果回传也一样。Anthropic 用tool_result块挂在同一条 user 消息里,OpenAI 则要求每条工具结果独立成一条role: "tool"消息,并且用tool_call_id关联。转换层要维护一个消息队列,把工具结果的顺序和 ID 全部对齐,错一条,整个链路的工具调用就彻底乱套了。

2.4 SSE 流式事件:终端里的逐字跳动从哪来

Claude Code 和 Codex 在终端里那种逐字渲染的效果,全靠 SSE 流。如果转换层只处理非流式请求,工具会卡到怀疑人生——模型输出全部积压到最后一次性返回,体验极差。所以流式事件也必须逐条翻译。

Anthropic 的流式事件序列是message_startcontent_block_startcontent_block_deltacontent_block_stopmessage_deltamessage_stop,其中文本增量在delta.text,工具参数增量在delta.input_json_delta

OpenAI chat 的流式则是choices[0].delta.content一条条蹦出来,工具参数增量在delta.tool_calls[0].function.arguments,也是增量字符串。

我做映射时的原则是:以 Anthropic 格式为外围标准(因为 Claude Code 是"消费者",它只认这套),把 OpenAI 的事件翻译成 Anthropic 事件再往终端推。比如收到 OpenAI 的choices[0].delta.content,就构造一个content_block_delta事件,delta.type设为text_delta。收到delta.tool_calls,就翻译成input_json_delta,并且要自己维护一个"当前工具块是否已经开始"的状态机,因为 Anthropic 要求先发content_block_start才能发content_block_delta,而 OpenAI 那边没有这么细的块生命周期。

3. 拆墙设计:为什么必须靠一层"本地协议转换器"

3.1 三条路线摆在我面前,为什么选了转换器

在动手之前,我认真评估了三条路线。

路线一:分别原生使用,不开源脑筋。老老实实用 Claude Code 配 Claude,用 Codex 配 GPT-4o。这当然最简单,但我就是不满意——工具和模型的排列组合是"双选",凭什么只能对角匹配。

路线二:直接改环境变量指向对方的官方地址。前面已经验证过,完全走不通,因为两边的请求格式根本不同,官方接口不会做善意兼容。有些第三方聚合网关确实同时提供 Anthropic 风格和 OpenAI 风格的端点,但它们内部还是固定模型,没法解决"Claude Code 跑 GPT-4o"这种交叉需求。

路线三:本地起一个协议转换层。把 Claude Code 的ANTHROPIC_BASE_URL指向http://127.0.0.1:8080,转换层收到 Anthropic 格式请求后转成 OpenAI 格式,发给真实的 GPT-4o 端点;同理,让 Codex 走另一个监听端口,把 OpenAI 格式转成 Anthropic 格式发给 Claude。这条路线完全可控,还能顺手加日志、加熔断。

我选了路线三,同时也明确了边界:这个转换层只负责协议翻译,不负责模型路由决策。模型选谁、key 用谁的,由上游工具配置决定。

3.2 转换层的数据流与配置链路

整个系统的数据流长这样:

Claude Code ──Anthropic 格式──> 本地转换层(监听 8080) ──OpenAI 格式──> GPT-4o Codex CLI ──OpenAI 格式──> 本地转换层(监听 8081) ──Anthropic 格式──> Claude

这里有个讲究:我只在一台机器上跑转换层,所以两个方向的服务都绑127.0.0.1,避免局域网暴露。如果你要多人共用,记得加一层最简单的鉴权,别让同事把整个转换层当公共接口用,key 泄露了不好追溯。

配置链路方面,Claude Code 认的环境变量是ANTHROPIC_BASE_URLANTHROPIC_AUTH_TOKENANTHROPIC_MODEL。你可以在启动 Claude Code 前 export,也可以写进~/.claude/settings.jsonenv块里。Codex 认OPENAI_BASE_URLOPENAI_API_KEY,以及~/.codex/config.toml里的 model provider 配置。我建议统一用配置文件管理,别散落在 shell 历史里,不然换个项目就忘了自己配过什么。

当时我正好用上了 cc switch 这个工具来管理这些配置。它本质上是 Claude Code 和 Codex 的"配置切换器",可以在多个 provider 的配置之间快速切换,避免我手动去改环境变量。我原本以为 cc switch 只做配置管理,后来发现它还带了一个 local proxy 模式,能在本机做协议转发——也正是这个模式,把我引到了后面那个/responses的坑里。

3.3 cc switch 在链条里的定位:配置管家,不是万灵药

用 cc switch 的好处是:provider 配置、key、模型映射都能在界面上管理,切换的时候一键生效。我配置了两套 Claude Code provider——一套指向 Anthropic 官方,一套指向本地转换层;Codex 那边也是一样。

但这里必须打个预防针:cc switch 只是把"墙"的管理变方便了,拆墙本身还是得靠协议转换层。如果你只是把 provider 从官方切到某个第三方,而第三方不兼容 Anthropic 格式,照样报错。我踩的那个cc switch local proxy failed while handling codex endpoint /responses,就是因为它内置的转发功能在处理 Codex 发出的/v1/responses请求时没处理好。后面我会专门讲这个排查过程。

所以我的最终架构是:cc switch 管配置切换,本地转换层管协议翻译,两者各司其职。这样即使 cc switch 升级换代,我的协议转换层也不受影响。

4. 第一拆实录:让 Claude Code 跑 GPT-4o,从配置到跑通

4.1 前置准备:你需要哪几样东西

动手之前确认三件事:一是有 OpenAI 的 API key(要有 GPT-4o 模型访问权限);二是有 Claude Code 的最新版本,它通过ANTHROPIC_BASE_URL支持自定义端点;三就是 cc switch,用于管理 provider 配置。

还建议装一个 Node.js 或 Python 运行时,因为小型的协议转换服务用这两种语言写起来最快。我这次用的是 Python + FastAPI,理由就俩:FastAPI 对 SSE 的支持直接,异步处理不会阻塞;Python 里解析 JSON 和字符串转换心智负担低。

我的目录结构非常朴素:

~/protocol-wall-buster/ ├── translator_anthropic_to_openai.py # 8080 端口,服务 Claude Code ├── translator_openai_to_anthropic.py # 8081 端口,服务 Codex ├── providers.json # provider 与 key 的映射 └── run.sh # 一键启动

4.2 配置 provider:把 Claude Code 的出口指向本地

在 cc switch 里新建一个 provider,名字叫ClaudeCode -> GPT4o,然后做两件事:

第一,设置本地转换层的地址和 key。我给 Claude Code 配置的环境变量如下:

export ANTHROPIC_BASE_URL="http://127.0.0.1:8080" export ANTHROPIC_AUTH_TOKEN="sk-local-translator" export ANTHROPIC_MODEL="gpt-4o"

这里的ANTHROPIC_AUTH_TOKEN可以随便设,它只用于让转换层识别"这个请求是从 Claude Code 来的"。真正的 OpenAI key 放在转换层的providers.json里:

{ "clients": { "claude_code": { "auth_token": "sk-local-translator", "upstream": { "base_url": "https://api.openai.com/v1", "api_key": "sk-你的真实key", "model": "gpt-4o" } } } }

注意一点:ANTHROPIC_MODEL里写gpt-4o会不会让 Claude Code 错乱?实测不会。模型名只是一个字符串,Claude Code 本身不对名字做合法性校验,真正消费模型名的是转换层——转换层收到请求后把model字段替换成 upstream 里的gpt-4o,然后走 OpenAI 接口。也就是说,你甚至可以写gpt-4o的任意别名,只要转换层最后映射成有效模型名就行。

第二,把~/.claude/settings.json里的 env 块也同步一份,这样每次启动 Claude Code 都会自动带上环境变量,不用每次 export:

{ "env": { "ANTHROPIC_BASE_URL": "http://127.0.0.1:8080", "ANTHROPIC_AUTH_TOKEN": "sk-local-translator", "ANTHROPIC_MODEL": "gpt-4o" } }

4.3 转换层核心代码:Anthropic 请求转 OpenAI 请求

转换层最核心的是请求翻译。贴一段我当时写的核心逻辑,代码不追求完整,重点是思路:

def translate_messages(anthropic_body: dict) -> dict: openai_messages = [] # 1. system 字段抽出来放到数组头部 if anthropic_body.get("system"): openai_messages.append({"role": "system", "content": anthropic_body["system"]}) # 2. 逐条转换 messages for msg in anthropic_body.get("messages", []): role = msg["role"] content = msg.get("content") if isinstance(content, str): openai_messages.append({"role": role, "content": content}) else: # 3. 处理块数组:文本、图片、tool_result text_parts, image_parts, tool_results = [], [], [] for block in content: if block["type"] == "text": text_parts.append(block["text"]) elif block["type"] == "image": image_parts.append({"type": "image_url", "image_url": {"url": f"data:{block['source']['media_type']};base64,{block['source']['data']}"}}) elif block["type"] == "tool_result": tool_results.append({"role": "tool", "tool_call_id": block["tool_use_id"], "content": extract_text(block["content"])}) # 4. tool_result 作为独立 tool 消息追加 openai_messages.extend(tool_results) # 5. 文本/图片组装成普通消息 if text_parts or image_parts: content_parts = [{"type": "text", "text": "\n".join(text_parts)}] if text_parts else [] content_parts.extend(image_parts) openai_messages.append({"role": role, "content": content_parts}) return openai_messages

工具定义和工具调用的转换类似:input_schema改成parameters,外层包一层{"type": "function", "function": {...}}。流式事件通过 FastAPI 的StreamingResponse逐条转发,每收到 OpenAI 的一个 SSE chunk,就按 2.4 节说的状态机翻译成 Anthropic 事件。

4.4 跑通验证:第一个命令就暴露了一个问题

启动转换层后,我在 Claude Code 里敲了第一句:

claude "看一下当前目录,告诉我这个项目用了哪些技术栈"

结果它顺利读出了项目文件,列出了技术栈,全程终端渲染流畅——协议转换层基本工作正常。但紧接着我让它"修改某个文件里的一段逻辑",问题来了:Claude Code 发了tool_result之后,GPT-4o 的响应格式和 Claude 不一样,转换层没有把finish_reason: "tool_calls"正确翻译成stop_reason: "tool_use",导致 Claude Code 认为工具没执行完,卡在处理中。

这个 bug 调了我一个多小时。根因在转换工具调用响应时,我只转了内容和 arguments,没有转finish_reason。修复很简单:把 OpenAI 的finish_reason: "tool_calls"映射成 Anthropic 的stop_reason: "tool_use"finish_reason: "stop"映射成end_turn。这个映射逻辑必须放在消息转换那一层,不能放在流式事件层,因为非流式请求的停止原因在 body 里,流式请求在最后一个事件里,两处都要处理。

5. 第二拆实录:让 Codex 跑 Claude,以及 /responses 那个报错的完整排查

5.1 反向转换:OpenAI 请求转 Anthropic 请求

搞定第一拆后,第二拆其实就是"反过来再来一遍"。Codex CLI 默认走 OpenAI 的 Responses API,请求体是input数组、工具在tools数组里、鉴权用Authorization: Bearer。要把这些翻译成 Anthropic 格式发给 Claude,需要做一个反向的openai_to_anthropic转换。

核心逻辑正好是 4.3 的逆过程:input数组里的system角色消息抽出来放到 Anthropic 请求体的顶层systemtool角色消息转换成tool_result块;assistant 消息里的tool_calls拆成tool_use块。我把两个转换脚本放在同一个目录下,互相引用同一套类型定义,避免字段名不一致。

关键配置在 Codex 侧的~/.codex/config.toml

model = "claude-sonnet-4-20250514" model_provider = "local-anthropic" [model_providers.local-anthropic] name = "Local Anthropic Translator" base_url = "http://127.0.0.1:8081" wire_api = "responses" env_key = "OPENAI_API_KEY"

Codex 会把请求发到http://127.0.0.1:8081,然后由反向转换层转成 Anthropic 格式发给 Claude。wire_api字段会决定 Codex 用哪一个 API 风格,我一开始用的是responses,结果就是那个著名的报错现场。

5.2 报错现场:cc switch local proxy failed while handling codex endpoint /responses

事情是这样的:我图省事,想直接用 cc switch 的 local proxy 功能来转发 Codex 的请求,结果一启动,Codex 那边直接抛了一行日志:

cc switch local proxy failed while handling codex endpoint /responses. provider ...

后面 provider 的信息被截断了,但从日志能看出两个信息:一是 cc switch 确实把本地代理启动起来了,二是它收到 Codex 发出的/v1/responses请求时没处理成功。这行报错一开始让我很懵——cc switch 既然宣称支持 Codex,怎么会连 endpoints 都处理不了?

我先把错误日志级别调高,抓到了完整堆栈。问题指向非常清晰:cc switch 本地代理的实现里,只注册了/v1/chat/completions这个路由的兼容处理逻辑,但对新版 Codex 默认使用的/v1/responses端点只做了透传转发;当目标 provider 不认 Responses API 格式时,转换层没做降级,直接把错误抛出来了。

换句话说:Codex 用 Responses API 说话,cc switch 的本地代理用 Chat Completions 的翻译逻辑去接,中间差了整整一代接口的映射。这不是 cc switch 的路径写错了,是它内置转发能力落后于 Codex 的接口演进。

5.3 完整排查链路:从日志到修复

我把排查和修复的完整链路列出来,方便你复现:

第一步,确认报错发生在哪一层。把 Codex 的日志级别调成 debug,加上RUST_LOG=debug(Codex 是 Rust 写的,支持这个环境变量),看到cc switch local proxy字样后基本锁定:转发层出了问题。

第二步,手动 curl 本地代理。我直接模拟 Codex 的请求:

curl -X POST http://127.0.0.1:8081/v1/responses \ -H "Authorization: Bearer sk-fake" \ -H "Content-Type: application/json" \ -d '{"model":"gpt-4o-mini","input":"hello"}'

返回的错误体给出了具体信息——后端 provider 返回 400,说是不认识input字段。到这里,问题就很明确了:本地代理把 Responses API 的input原样转发给了 Anthropic,而 Anthropic 只认messages,不认input

第三步,决定修的方向。两个选择:改配置让 Codex 走 chat/completions 风格(这样 cc switch 的兼容逻辑能接住),或者自己写一个专门的 Responses API → Anthropic 转换层。我选了后者,原因很实在:Codex 新版对 Responses API 支持最完整,包括并行工具调用、存储会话这些特性,强制降级到 chat 会损失能力。

第四步,给转换层补上 Responses API 的解析逻辑。核心是把input数组里面的消息类型映射到 Anthropic 角色。Responses API 的消息类型比 chat 多,比如message里面有rolecontentfunction_call类型的消息要转成 tool_use;function_call_output要转成 tool_result。我加了一个专门的分发函数,按input[i].type做分支处理。

第五步,回归验证。先在 Codex 里跑一个无工具调用的简单问答,再跑一个需要改文件的完整任务,确认工具调用链路没断。同时把 cc switch 的 local proxy 关掉,彻底换成自建转换层,保留 cc switch 只做配置管理。

这条排查链路走下来,我最大的体会是:报错文本里的endpoint往往不是根因,根因是"谁在处理这个 endpoint、处理得全不全"。遇到这种报错,先别急着找"升级版本"这种万能解,先把请求链路拆开,用 curl 一层层试,问题定位会快得多。

6. 拆完之后的实测:延迟、流式、工具调用一个都别想省

6.1 延迟损耗到底有多少

协议转换不是零成本,但延迟损耗完全在可接受范围内。我在同一台机器上做了对比测试:直接调用 Claude 官方接口 vs 经过转换层调用 GPT-4o,等一个中长回答(约 500 token)的耗时。

场景首 token 延迟完整响应时间
Claude Code 直接连 Claude约 1.2s约 6.5s
Claude Code → 转换层 → GPT-4o约 1.5s约 7.2s
Codex 直接连 GPT-4o约 1.1s约 6.1s
Codex → 转换层 → Claude约 1.4s约 7.0s

额外增加的时间主要是两次 HTTP 转发 + 字符串 JSON 序列化的开销,对于终端交互来说体感差异很小。真正影响体验的是流式刷新频率——如果你的转换层是攒一批再推,终端就会一卡一卡;如果逐条转发,就完全无感。我最后用了"收到 OpenAI chunk 立即翻译立即推送"的模式,终端渲染稳稳的。

6.2 工具调用这个真坑,我替你们踩熟了

工具调用兼容性是最容易出问题的环节,我实测下来有三个高频坑。

第一个坑是工具入参的类型校验。GPT-4o 生成的arguments是 JSON 字符串但格式偶尔不严格(缺引号、多逗号),JSON.parse会直接挂。我在转换层里加了一个 lenient JSON 解析函数,先试标准 parse,失败后用正则修常见错误,再不行就返回一个带原始字符串的特殊错误给模型重试。

第二个坑是并发工具调用的顺序。GPT-4o 一次可以返回多个tool_calls,转换层把它们都拆成 tool_use 块之后,工具执行结果回传时必须保持同一个 ID 映射。有些转换层偷懒用数组下标,一旦并发乱序,整个会话就废了。我用的是一个dict保存openai_call_id → anthropic_tool_use_id的映射,回传时按映射查找,稳得很。

第三个坑是停止原因没映射干净。前面我提了finish_reasonstop_reason的事,这里再补充一个细节:Anthropic 的stop_reasonmax_tokens,OpenAI 的finish_reasonlength。你要是不映射,模型回答到一半被截断时,Claude Code 会把它当成正常结束,导致它继续执行不存在的后续步骤。这个映射必须放在响应体和流式事件两处都要做。

6.3 上下文长度与限流:两边的脾气不一样

Claude 和 GPT-4o 的上下文窗口大小不一致,工具使用习惯也不一样。Claude Code 默认会往上下文里塞很多系统提示和工具定义,这些 token 在转换层原样转发给 GPT-4o 之后,可能会把 GPT-4o 的上下文预算吃掉很多。我的应对是:在转换层加了一个"系统提示词压缩"开关,当原始 system 超过一定长度时,用另一个小模型(比如gpt-4o-mini)做一次摘要压缩,保住主模型的上下文空间。这个功能要谨慎用,摘要会丢信息,我只在确实超限时才启用。

限流方面,Anthropic 和 OpenAI 的限流策略都基于"每分钟请求数 + 每分钟 token 数",但报错格式完全不同。OpenAI 的 429 响应里有retry-after头,Anthropic 的 429 响应里有retry-after-ms。转换层要把这些头原样传给调用方,否则 Claude Code 和 Codex 的重试逻辑会失效,表现为"一限流就整个会话崩掉"。这些细节不处理,日常使用会频繁翻车。

7. 同一套思路还能指哪打哪:Ollama、DeepSeek 与其它兼容端点

7.1 把 Claude Code 接到本地 Ollama

拆完那两堵墙之后,我发现这套"本地协议转换层"的思路可以泛化到任何模型端点。比如本地跑 Ollama,它自己只暴露 OpenAI 兼容接口,那我想让 Claude Code 用它怎么办?同样的套路:转换层把 Anthropic 格式翻译成 OpenAI 兼容格式,只不过上游从api.openai.com换成了http://127.0.0.1:11434/v1

cc switch 对 Ollama 也做了专门适配,很多人在热词里搜claude code + cc switch + ollama,说明这是个高频需求。我的建议是:本地模型的工具调用能力普遍弱于云端模型,转换层里的工具 schema 一定要精简,别把 Claude Code 那几十个工具全塞给本地小模型,它根本处理不过来。我实测qwen2.5-coder:32b这种规模的模型,跑简单文件操作还行,复杂重构会乱,适合做"预算敏感型"的轻量任务。

7.2 接 DeepSeek 这类第三方模型的注意点

第三方模型端点(比如 DeepSeek)通常都提供 OpenAI 兼容接口,所以"Claude Code 接 DeepSeek"只要把转换层的 upstream 地址改成 DeepSeek 的 endpoint 就行,几乎零成本。但要注意两个点:一是 DeepSeek 官网有比较严格的并发限制,Claude Code 经常并发调用工具,转换层需要加一个简单的信号量做限流;二是 DeepSeek 对超长上下文的处理策略和 OpenAI 不同,Claude Code 的自动 diff、自动重试会频繁触发长上下文请求,建议在转换层里把max_tokens掐小一点,避免尾部长输出被截断。

7.3 给转换层做的最后一点"产品化"建议

折腾完这一圈,我把这套转换层整理成了一个小工具,供自己团队使用。所谓产品化,其实就是加了三个东西:

  • 请求日志:每次转换都记录来源工具、目标模型、耗时、错误码,有问题一眼定位。
  • 一键切换配置:配合 cc switch,预置了"官方 Claude""Claude Code + GPT-4o""Codex + Claude""本地 Ollama"四套配置,切换不再改环境变量。
  • 健康检查端点GET /health返回当前所有 upstream 的连通状态,CI 脚本和同事的日常检查都能用。

这三个东西看着简单,实际使用中价值极大。比如上次同事反馈"Codex 响应很慢",我打开日志一看,发现他配错了 key,转换层每次都在等 401 超时后重试,而不是立刻失败。日志一查就清楚了。

回到最开始的问题:为什么非要拆这堵墙?我的个人体会是,AI 编程工具生态现在还处于"百家争鸣"的阶段,没有哪个模型的全部能力都碾压其他模型。Claude Code 的终端体验和 Codex 的沙盒执行各有拥趸,但模型本身的差距每隔几个月就会换一次风向。如果工具和模型被协议死死绑定,你就等于把自己锁死在一个厂商的技术路线上,想切换的代价会越来越高。

拆掉协议这堵墙之后,你手里的工具和模型就像积木一样可以自由组合。今天喜欢让 Claude Code 跑 GPT-4o,明天想让 Codex 用 Claude 做复杂推理,后天想接本地模型省点钱——改一下配置就行。最后再分享一个实际操作中的小技巧:如果你只是临时想试试某种组合,直接在启动命令前 export 环境变量就行,不用大动干戈部署转换层;一旦发现某个组合是长期需求,再把它固化成配置文件、交给 cc switch 统一管理。这套"先临时验证、后固化配置"的节奏,能帮你少走很多弯路。

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

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

立即咨询