Claude Code + DeepSeek: role:system 400 错误的诊断与修复
问题现象
升级 Claude Code 到 v2.1.156 后,claude-deepseek 命令直接报 400:
API Error: 400 Failed to deserialize the JSON body into the target type:
messages[1].role: unknown variant `system`, expected `user` or `assistant`
at line 1 column 102921
同一版本的 Claude Code,claude-kimi、claude-mimo、claude-glm 全部正常工作。
第一个假设(错误的)
看到 messages[1].role: unknown variant 'system',第一反应是 Claude Code 新版本引入了 mid-conversation system messages(Anthropic 随 Opus 4.8 推出的新 API 特性),在 messages 数组里插入了 role: "system" 条目。
差异点:DeepSeek 用 ANTHROPIC_API_KEY,其他三家用 ANTHROPIC_AUTH_TOKEN。猜测 Claude Code 根据认证方式选择是否发送 system messages。
验证:写 body sniffer 截获请求
为验证假设,写了一个最小 HTTP 服务器截获 Claude Code 发送的实际请求体:
# body-sniffer.py -- 截获 Claude Code 请求,打印 messages[].role
class H(BaseHTTPRequestHandler):
def do_POST(self):
body = json.loads(self.rfile.read(...))
msgs = body.get("messages", [])
for i, m in enumerate(msgs):
role = m.get("role", "???")
flag = " *** SYSTEM ***" if role == "system" else ""
print(f" [{i}] role={role}{flag}")
# 返回最小合法 Anthropic 响应
...
分别用两种认证方式测试:
# Test 1: ANTHROPIC_API_KEY (DeepSeek 方式)
ANTHROPIC_API_KEY="sk-fake" ANTHROPIC_BASE_URL="http://127.0.0.1:19991" claude -p "hi" --no-session-persistence
# Test 2: ANTHROPIC_AUTH_TOKEN (kimi/mimo 方式)
ANTHROPIC_AUTH_TOKEN="fake" ANTHROPIC_BASE_URL="http://127.0.0.1:19992" claude -p "hi" --no-session-persistence
结果:两种认证方式都发送了 role: "system" 在 messages 数组里。
=== 276403 bytes, 2 messages ===
Top-level 'system' field present: True
system: list of 3 items
[0] role=user content_len=94498
[1] role=system content_len=22014 *** SYSTEM IN MESSAGES ***
第一个假设推翻。认证方式不影响消息格式,Claude Code v2.1.156 对所有 provider 都发送 mid-conversation system messages。
第二个假设:服务端容忍度差异
既然四家 provider 收到的请求格式相同,差异只能在服务端。用 curl 直接向四家发送含 role: "system" 的请求:
body='{
"model": "<model>",
"max_tokens": 10,
"system": "You are helpful.",
"messages": [
{"role": "user", "content": "say ok"},
{"role": "system", "content": "Extra instruction"},
{"role": "user", "content": "say ok again"}
]
}'
结果:
| Provider | 端点 | HTTP 状态 | 行为 |
|---|---|---|---|
| DeepSeek | api.deepseek.com/anthropic | 400 | unknown variant ‘system’ |
| mimo | token-plan-cn.xiaomimimo.com/anthropic | 200 | 正常回复 |
| kimi | api.moonshot.cn/anthropic | 200 | 正常回复 |
| glm | open.bigmodel.cn/api/anthropic | 200 | 正常回复 |
根因确认:DeepSeek 的 Anthropic 兼容端点对 messages 数组的 schema 校验更严格,不接受 role: "system"。其他三家选择了静默忽略或正常处理。
修复方案
既然问题只出在 DeepSeek 一家,写一个最小的本地过滤代理,在请求到达 DeepSeek 之前把 role: "system" 的条目从 messages 数组中移除。
核心过滤逻辑(10 行)
def _filter_body(raw):
try:
data = json.loads(raw)
except (json.JSONDecodeError, UnicodeDecodeError):
return raw # 非 JSON 原样转发
msgs = data.get("messages")
if not isinstance(msgs, list):
return raw
filtered = [m for m in msgs
if not isinstance(m, dict) or m.get("role") != "system"]
if len(filtered) == len(msgs):
return raw # 没有 system message,不碰 body
data["messages"] = filtered
return json.dumps(data, ensure_ascii=False, separators=(",", ":")).encode()
代理架构
遵循已有的 kimi-proxy / zhipu-proxy 模式:
- 监听
127.0.0.1:18890 - 读取请求体 ->
_filter_body()过滤 -> 转发到api.deepseek.com - 认证 header(
x-api-key)原样透传,代理不管理密钥 - 流式响应转发(chunked encoding)
/health端点供 bashrc 探测
与 kimi-proxy 的区别:不需要 key rotation(DeepSeek 单密钥),不需要 429 重试。整个代理约 200 行。
bashrc 集成
_ensure_deepseek_proxy() {
local port="${DEEPSEEK_PROXY_PORT:-18890}"
curl -sf "http://127.0.0.1:${port}/health" >/dev/null 2>&1 && return 0
python3 "$HOME/.local/bin/deepseek-proxy.py" --port "$port" >> "$HOME/.config/deepseek/proxy.log" 2>&1 &
disown $!
local _retries=6
while (( _retries-- )); do
sleep 0.5
curl -sf "http://127.0.0.1:${port}/health" >/dev/null 2>&1 && return 0
done
return 1
}
claude_deepseek() {
# ... 密钥读取 ...
if _ensure_deepseek_proxy; then
env ... ANTHROPIC_BASE_URL="http://127.0.0.1:${port}/anthropic" command claude "$@"
fi
}
首次调用自动启动代理,后续调用复用已运行的进程。aicc deepseek 也自动受益(它直接调用 claude_deepseek 函数)。
验证
代理过滤确认
$ tail ~/.config/deepseek/proxy.log
[deepseek-proxy] -> POST /anthropic/v1/messages?beta=true body=258722B
[deepseek-proxy] stripped 1 system message(s)
[deepseek-proxy] <- first-byte t=2.9s
[deepseek-proxy] done t=8.0s
端到端
$ claude -p "Reply with exactly: DEEPSEEK_PROXY_OK" --no-session-persistence
DEEPSEEK_PROXY_OK
之前同样的调用返回 400,现在正常。
为什么不统一四家代理
评估过把 kimi-proxy / zhipu-proxy / deepseek-proxy 合并成一个通用代理。结论是不值得:
- kimi 需要 21-key rotation + 即时重试
- zhipu 需要 6-key rotation + 指数退避重试
- deepseek 只需要 body 过滤,没有 key rotation
- mimo 完全不需要代理(服务端接受 system messages)
四家的差异维度(认证方式、重试策略、key 管理)太多,抽象后配置项比四个独立脚本加起来还复杂。现有的 kimi/zhipu 代理已经稳定运行,改了反而有回归风险。
总结
| 项目 | 内容 |
|---|---|
| 根因 | Claude Code v2.1.156 发送 mid-conversation role:“system” messages(Opus 4.8 新特性),DeepSeek 端点严格校验拒绝 |
| 影响范围 | 仅 DeepSeek。kimi/mimo/glm 的兼容端点静默接受 |
| 修复 | 本地过滤代理 deepseek-proxy.py,200 行,strip system messages 后转发 |
| 代码 | ~/.local/bin/deepseek-proxy.py + ~/.bashrc 中 _ensure_deepseek_proxy |
这个问题的本质是 Anthropic 在 API 层面引入了新特性(mid-conversation system messages),第三方兼容端点的跟进速度不同。DeepSeek 选择了严格校验,其他三家选择了宽容处理。两种策略都有道理,但对用户来说,一个本地过滤层是最小侵入的解决方案。
更多推荐




所有评论(0)