
1. 先搞清楚 400 到底在报什么thinking mode 与 Claude Code 的历史序列化冲突如果你最近在用 Claude Code 接 DeepSeek V4 的 thinking mode大概率会撞上这个报错API Error: 400 The content[].thinking in the thinking mode must be passed back to the API.这个报错的特点是第一轮对话正常工具调用tool calls也正常但从第二轮开始每次请求都 400。很多人第一反应是 Key 错了、额度没了、模型名写错了其实都不是。它跟鉴权无关跟网络无关纯粹是对话历史在序列化时丢字段导致的。先把链路讲清楚。DeepSeek V4 在 thinking mode 下返回的 assistant 消息里会带一个reasoning_content或者 Anthropic 风格下的thinking块。这个字段不是给你看的是给下一轮请求用的。DeepSeek 的 API 在 multi-turn 场景下有个硬性校验只要历史里存在role: assistant的消息且当前处于 thinking mode那么这条 assistant 消息必须把 thinking 块原样带回来。缺了就直接 400。问题出在 Claude Code 这一侧。Claude Code 内部是按 Anthropic Messages API 的格式来组织对话历史的它在保存 assistant 回复时只保留它认识的字段text、tool_use等对于thinking这种非标准块序列化时会被丢掉。于是下一轮请求发出去DeepSeek 一看assistant 消息没有 thinking 块判定为非法请求返回 400。用一句话概括根因Claude Code 保存历史时丢弃了 thinking 字段 → 下一轮请求缺少 thinking 块 → DeepSeek V4 校验失败 → 400。这个坑的迷惑性在于它不是每次都报。纯聊天不带 tool calls有时能撑几轮一旦涉及工具调用第一轮工具结果回填后第二轮几乎必炸。所以你会看到「有时候能用一用工具就挂」的现象。那怎么修核心思路不是去改 Claude Code 的源码改不动也不是去改 DeepSeek 的校验改不了而是在两者之间插一层转发在请求发出前把被丢掉的 thinking 字段补回去。这层转发可以是一个本地 Node.js 代理也可以走统一的 API 通道。下面我会先讲统一 Key 通道的接入方式再给一个可复制的本地代理方案两条路你按自己的环境选。适合谁看正在用 Claude Code DeepSeek V4 做 Agent / 工具调用开发被 400 卡住的人以及想把多个模型的 Key 收敛到一个通道、少维护几套配置的人。读完你能拿到可直接粘贴的settings.json、Base URL 配置、代理脚本以及用 curl 和日志验证 400 是否真的消失的方法。2. 用 TaoToken 统一 Key 通道收敛配置Base URL、Key 与 Model ID 三件套在动手写代理之前先解决一个更基础的问题你的 Claude Code 到底该往哪个 Base URL 发请求。很多人 400 修不好是因为配置本身就乱——Key 是 A 家的Base URL 是 B 家的模型名又是 C 家的报错自然五花八门。我建议的做法是把模型访问收敛到一个统一通道Claude Code 只认一个 Base URL、一个 Key模型切换通过 Model ID 控制。这样出问题时排查面小很多。TaoToken 就是干这个的它提供一个统一的 API 入口你拿一个 Key就能在 Claude Code 里切换不同模型不用为每个模型单独维护一套环境变量。先拿 Key。打开控制台创建 API Keyhttps://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite创建完在 API Keys 页面能看到你的 Key形如sk-xxxx。这个 Key 后面会同时用在环境变量和settings.json里。然后是 Base URL。统一通道的 API 地址是https://taotoken.net/api注意Claude Code 走的是 Anthropic 兼容协议所以实际填的时候通常要带上协议路径。你在 Claude Code 里配置时Base URL 填https://taotoken.net/apiClaude Code 会自动拼接/v1/messages这类路径。如果你用的是 cc-switch 之类的切换工具也是填这个地址。Model ID 这块要特别注意。DeepSeek V4 在 thinking mode 下的模型名和普通模式不一样。你在配置里要写清楚是哪个模型否则请求发过去通道不知道该路由到哪。常见的写法是deepseek-v4系列具体以你控制台里模型列表显示的 ID 为准。Base URL Key Model ID 这三件套必须来自同一个通道这是排查 400 的第一原则。为什么统一通道能帮上忙因为它在转发层做了协议适配。Claude Code 发出来的是 Anthropic 格式的请求DeepSeek 期望的是它自己的格式中间这层转换如果处理得当thinking 字段的保留问题就有机会在转发层被兜住。但要注意统一通道解决的是「配置收敛」和「协议适配」它不保证一定能自动补全被 Claude Code 丢掉的 thinking 块。如果你的场景里 400 依然出现那就需要下面的本地代理来兜底。所以正确的姿势是先用统一通道把 Base URL、Key、Model ID 三件套配好跑通基础请求如果 thinking mode tool calls 仍然 400再叠加本地代理。两层配合才是稳的。这里给一个环境变量的配置示例Windows PowerShell$env:ANTHROPIC_BASE_URL https://taotoken.net/api $env:ANTHROPIC_API_KEY sk-你的TaoToken Key $env:ANTHROPIC_MODEL deepseek-v4 claudemacOS / Linux 下换成export即可export ANTHROPIC_BASE_URLhttps://taotoken.net/api export ANTHROPIC_API_KEYsk-你的TaoToken Key export ANTHROPIC_MODELdeepseek-v4 claude配完先别急着上工具调用先用一句普通对话验证通道通不通。如果普通对话都 400那是 Key 或 Base URL 的问题跟 thinking mode 无关先解决这个。3. 可复制配置settings.json 与本地代理脚本 ds-proxy.js这一节是全文的核心给你两份可直接复制的配置一份是 Claude Code 的settings.json一份是补全 thinking 字段的本地代理脚本。先说settings.json。Claude Code 的用户级配置一般放在~/.claude/settings.jsonWindows 是C:\Users\你的用户名\.claude\settings.json。如果你走统一通道配置长这样{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的TaoToken Key, ANTHROPIC_MODEL: deepseek-v4, ANTHROPIC_SMALL_FAST_MODEL: deepseek-v4 }, permissions: { allow: [], deny: [] } }如果你决定叠加本地代理那么 Base URL 要指向本地代理而不是直连通道{ env: { ANTHROPIC_BASE_URL: http://127.0.0.1:15722/anthropic, ANTHROPIC_API_KEY: sk-你的TaoToken Key, ANTHROPIC_MODEL: deepseek-v4 } }注意这里的逻辑Claude Code → 本地代理127.0.0.1:15722→ 统一通道 → DeepSeek。代理负责补 thinking 字段通道负责协议适配和路由。Key 依然是 TaoToken 的 Key代理只做字段修补不碰鉴权。下面是代理脚本ds-proxy.js。它的职责很单一拦截发往上游的请求体遍历messages对每条role: assistant的消息检查有没有 thinking 块没有就补一个空的进去。const http require(http); const { Readable } require(stream); // 上游统一通道地址 const UPSTREAM_BASE https://taotoken.net/api; const PORT 15722; function fixMessages(data) { if (!data || !Array.isArray(data.messages)) return; for (const msg of data.messages) { if (msg.role ! assistant) continue; if (Array.isArray(msg.content)) { const hasThinking msg.content.some(b b.type thinking); if (!hasThinking) { msg.content.unshift({ type: thinking, thinking: }); } } else { msg.reasoning_content msg.reasoning_content || ; } } } const server http.createServer(async (req, res) { console.log(${req.method} ${req.url}); res.setHeader(Access-Control-Allow-Origin, *); if (req.method OPTIONS) { res.writeHead(204); res.end(); return; } if (req.method GET) { res.writeHead(200, { Content-Type: application/json }); res.end(JSON.stringify({ id: root, object: list })); return; } if (req.method ! POST) { res.writeHead(404); res.end(Not Found); return; } let body ; req.on(data, chunk body chunk); req.on(end, async () { try { const data JSON.parse(body); fixMessages(data); const target UPSTREAM_BASE req.url; console.log(-, target); const apiRes await fetch(target, { method: POST, headers: { Content-Type: application/json, Authorization: req.headers.authorization, anthropic-version: req.headers[anthropic-version] || }, body: JSON.stringify(data) }); console.log(-, apiRes.status); const headers {}; apiRes.headers.forEach((v, k) { headers[k] v; }); res.writeHead(apiRes.status, headers); if (apiRes.body) { Readable.fromWeb(apiRes.body).pipe(res); } else { res.end(); } } catch (err) { console.error(Proxy error:, err.message); res.writeHead(500); res.end(JSON.stringify({ error: Proxy Error: err.message })); } }); }); server.listen(PORT, () { console.log(DS4 Proxy on http://127.0.0.1: PORT); });几个关键点解释一下。fixMessages是核心它对数组型content补thinking块对字符串型content补reasoning_content字段两种格式都兜住。UPSTREAM_BASE指向统一通道这样代理不需要自己处理鉴权Authorization头原样透传。流式响应用Readable.fromWeb转发保证 SSE 不被打断。启动代理node ds-proxy.js看到DS4 Proxy on http://127.0.0.1:15722就说明起来了。保持它在后台运行别关窗口。如果你用 cc-switch把 DeepSeek provider 的 Base URL 改成http://127.0.0.1:15722/anthropic即可不用 cc-switch 的话直接改settings.json里的ANTHROPIC_BASE_URL。4. 验证请求用 curl 与日志对比确认 400 是否消除配置改完不算完得验证。验证分两步先用 curl 直接打代理确认字段补全逻辑生效再跑 Claude Code 实际对话看日志里 400 有没有消失。先看 curl。构造一个故意缺少 thinking 块的 assistant 历史直接发给代理看它能不能补上并成功返回curl -X POST http://127.0.0.1:15722/anthropic/v1/messages \ -H Content-Type: application/json \ -H Authorization: Bearer sk-你的TaoToken Key \ -H anthropic-version: 2023-06-01 \ -d { model: deepseek-v4, max_tokens: 256, messages: [ {role: user, content: 你好}, {role: assistant, content: [{type: text, text: 你好有什么可以帮你}]}, {role: user, content: 继续} ] }注意第二条 assistant 消息它的content里只有text没有thinking。如果代理工作正常它会在转发前给这条消息补上{ type: thinking, thinking: }上游就不会因为缺 thinking 块而 400。你会在代理窗口看到类似日志POST /anthropic/v1/messages - https://taotoken.net/api/anthropic/v1/messages - 200- 200就是成功。如果还是- 400说明补全逻辑没生效回去检查fixMessages有没有被调用、messages是不是数组。再看 Claude Code 实际场景。启动 Claude Code让它做一个必须触发工具调用的任务比如「读取当前目录下的 package.json 并告诉我 name 字段」。这类任务第一轮会返回 tool_use第二轮回填工具结果正好踩中 400 的触发条件。观察代理日志你会看到连续多条 POST每条都返回 200POST /anthropic/v1/messages - https://taotoken.net/api/anthropic/v1/messages - 200 POST /anthropic/v1/messages - https://taotoken.net/api/anthropic/v1/messages - 200对比修复前修复前第二轮开始就是- 400Claude Code 界面弹出API Error: 400 The content[].thinking...。修复后这个报错消失工具调用能连续跑下去。如果你想更直观地对比可以在代理里加一行日志打印补全前后的 assistant 消息数量function fixMessages(data) { if (!data || !Array.isArray(data.messages)) return; let patched 0; for (const msg of data.messages) { if (msg.role ! assistant) continue; if (Array.isArray(msg.content)) { const hasThinking msg.content.some(b b.type thinking); if (!hasThinking) { msg.content.unshift({ type: thinking, thinking: }); patched; } } else { msg.reasoning_content msg.reasoning_content || ; patched; } } if (patched 0) console.log(patched ${patched} assistant message(s)); }跑一轮工具调用日志里出现patched 1 assistant message(s)就说明确实有字段被补回来了——这正是 400 的根因所在。验证通过后建议把代理做成开机自启或后台常驻否则每次开 Claude Code 前都要手动node ds-proxy.js容易忘。Windows 可以用pm2或任务计划程序macOS/Linux 用pm2 start ds-proxy.js --name ds-proxy最省事。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth 对照修 400 的过程中你可能会撞上别的报错。这些报错长得像但根因完全不同混在一起排查会绕远路。下面按真实报错逐条对照。401 Unauthorized。这个跟 thinking mode 无关纯粹是 Key 的问题。常见原因Key 复制时带了空格、Key 已过期、Key 和 Base URL 不是同一个通道。排查方法用 curl 直接打通道绕开代理curl -X POST https://taotoken.net/api/v1/messages \ -H Content-Type: application/json \ -H Authorization: Bearer sk-你的Key \ -H anthropic-version: 2023-06-01 \ -d {model:deepseek-v4,max_tokens:64,messages:[{role:user,content:hi}]}如果这里就 401别往下查代理了先解决 Key。如果这里 200但走代理 401那是代理没把Authorization头透传检查headers里有没有带上req.headers.authorization。local proxy failed / connection refused。Claude Code 报这个说明它连不上127.0.0.1:15722。原因通常是代理没启动或者端口被占。先确认代理窗口还在跑再检查端口# macOS / Linux lsof -i :15722 # Windows netstat -ano | findstr 15722如果端口被别的进程占了改ds-proxy.js里的PORT同时同步改settings.json的 Base URL两边必须一致。reading choices / 流式解析错误。这个报错通常出现在响应流被截断时。代理转发流式响应如果处理不当SSE 的 chunk 会断Claude Code 解析到一半就报reading choices。检查代理里是不是用了Readable.fromWeb(apiRes.body).pipe(res)而不是先把整个 body 读成字符串再发。流式必须边收边转不能缓冲。OAuth / authentication_error。如果你在 Claude Code 里看到 OAuth 相关的报错说明它还在走 Anthropic 官方的登录态没切到 API Key 模式。检查settings.json里ANTHROPIC_API_KEY有没有生效以及有没有残留的 OAuth 配置覆盖了它。有时候ANTHROPIC_AUTH_TOKEN和ANTHROPIC_API_KEY同时存在会打架只留一个。Codex auth.json 相关。如果你同时用 Codex它的鉴权文件在~/.codex/auth.json和 Claude Code 的settings.json是两套。别把两者的 Key 混用。Codex 走的是 OpenAI 兼容协议Base URL 和 Claude Code 不一样配置时分开写。把这几类报错对照清楚你会发现400 是字段问题401 是鉴权问题connection refused 是进程问题reading choices 是流式问题OAuth 是模式问题。定位对了修起来都很快。6. 把通道和代理固定下来长期编码场景的稳定配置修好一次 400 不难难的是让它长期稳定。我自己的做法是把配置固化下来减少每次手动干预。第一代理常驻。用pm2托管崩了自动重启npm install -g pm2 pm2 start ds-proxy.js --name ds-proxy pm2 save pm2 startup这样开机自启不用每次手动node ds-proxy.js。第二Key 和 Base URL 收敛到统一通道。别在多个地方散落不同的 Key出问题时你根本不知道哪个生效了。统一通道的好处是一个 Key 管所有模型切换模型只改 Model ID。控制台里可以随时看用量和调用记录https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite第三如果你主要做长期编码、Agent 类任务调用量大、会话长建议用 Coding Plan额度更划算也省得频繁换 Keyhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite第四接入细节和协议说明看文档遇到新报错先翻文档再动手https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite第五Key 管理在 API Keys 页面建议给不同项目建不同的 Key方便按项目排查和回收https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite第六如果你只是想先验证模型通不通、thinking mode 表现如何用模型对话页面直接试不用配 Claude Codehttps://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite最后说个我踩过的坑代理脚本里的UPSTREAM_BASE千万别写成 DeepSeek 官方地址直连。一是直连容易受网络环境影响二是绕过了统一通道的协议适配thinking 字段补了也可能因为格式不对继续 400。正确链路是Claude Code → 本地代理 → 统一通道 → 模型代理只补字段通道管适配和路由各司其职。配置固化之后日常使用基本不用再管 400。真遇到新报错按第 5 节的对照表定位先分清是字段、鉴权、进程还是流式问题再动手。这套组合我跑了挺长时间工具调用连续几十轮没再炸过 thinking 相关的 400。