问题现象

升级 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-kimiclaude-mimoclaude-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 选择了严格校验,其他三家选择了宽容处理。两种策略都有道理,但对用户来说,一个本地过滤层是最小侵入的解决方案。

更多推荐