一份对 api.deepseek.com/anthropic 兼容层逐字段拆解的技术笔记,基于本地 .claude.jsonsettings.json 的真实配置还原。


一、起点:Claude Code 认为自己在对谁说话?

先看 ~/.claude/settings.json 里的三个关键环境变量:

"ANTHROPIC_BASE_URL":  "https://api.deepseek.com/anthropic",
"ANTHROPIC_AUTH_TOKEN": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"ANTHROPIC_MODEL":     "DeepSeek-V4-pro[1M]"

这三行完成了一件事:Claude Code 以为自己连的是 Anthropic 官方服务器,实际上所有请求都被路由到了 DeepSeek 的 Anthropic 兼容端点。

从 Claude Code 的视角看,它只是在用标准的 Anthropic Messages API 跟一个叫 DeepSeek-V4-pro[1M] 的"Anthropic 模型"对话。它完全不知道自己被骗了——这正是兼容层设计的目标:让客户端零修改就能换个后端。

请求的物理路径是:

POST https://api.deepseek.com/anthropic/v1/messages
Authorization: Bearer sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json

二、请求被发出前:Claude Code 本地组装了什么

在用户按下回车、到真正发出 HTTP 请求之间,Claude Code 在本地把以下内容拼成了一个完整的 Anthropic Messages API 请求体:

组装内容 来源
System Prompt(角色定义、工具列表、行为规则) Claude Code 内置模板 + .claude/ 下所有 skills/agents/plugins 的指令合并
工具定义(Read / Write / Bash / Grep / Agent / Workflow …) Claude Code 内置
MCP 工具定义(mingpan / word / ppt / figma) .mcp.json + .claude.json project 级 MCP 配置
记忆文件 MEMORY.md 索引的 .claude/projects/.../memory/*.md
对话历史 当前 session 完整 transcript
当前用户输入 刚刚输入的文字

这个请求体完全遵循 Anthropic Messages API 规范,典型结构如下:

{
  "model": "DeepSeek-V4-pro[1M]",
  "system": [
    { "type": "text", "text": "You are an interactive agent...", "cache_control": { "type": "ephemeral" } },
    { "type": "text", "text": "…大量工具定义…" }
  ],
  "messages": [
    { "role": "user", "content": [{ "type": "text", "text": "帮我改一下 main.py" }] },
    {
      "role": "assistant",
      "content": [
        { "type": "text", "text": "让我先读取文件。" },
        { "type": "tool_use", "id": "toolu_001", "name": "Read", "input": { "file_path": "/path/to/main.py" } }
      ]
    },
    {
      "role": "user",
      "content": [
        { "type": "tool_result", "tool_use_id": "toolu_001", "content": "print('hello world')" }
      ]
    }
  ],
  "tools": [
    { "name": "Read", "description": "Read a file", "input_schema": { "type": "object", "properties": {…}, "required": ["file_path"] } }
  ],
  "max_tokens": 16000,
  "stop_sequences": ["\n\nHuman:", "\n\nAssistant:"],
  "thinking": { "type": "enabled", "budget_tokens": 4000 }
}

请求发出去了。接下来就是 api.deepseek.com/anthropic 要做的事。


三、请求侧:Anthropic → DeepSeek 原生格式,逐字段转换

DeepSeek 自己的模型不是吃 Anthropic 协议的——它走的是内部原生协议,结构上更接近业界主流的 Chat API 风格(跟 OpenAI 的格式同属一个流派)。所以 api.deepseek.com/anthropic 的第一件事,就是把这套 Anthropic 请求翻译成 DeepSeek 内部协议能懂的东西。

关于下文中的"DeepSeek 协议":DeepSeek /anthropic 端点的内部实现没有开源——它可能直接适配到模型推理管线,也可能中间经过一层 OpenAI 兼容网关。下面用"内部协议 / 内部格式"来指代 Anthropic 请求被翻译到的目标格式,不假设中间是否绕了 OpenAI 格式这一跳。好在这种 Anthropic → 主流 Chat API 的结构性差异是固定的,不管目标格式具体是哪一套,转换的方向和逻辑都一致。

下面把每个字段的转换拆开看。

3.1 System Prompt:从顶层字段变成 messages[0]

Anthropic 的 System Prompt 是一个顶层数组,独立于对话消息:

"system": [
  { "type": "text", "text": "You are an interactive agent…", "cache_control": { "type": "ephemeral" } },
  { "type": "text", "text": "…几百行的工具定义…" }
]

兼容层最可能的处理(类比业内主流 Chat API 的结构推断):

system 数组里的所有 text 块拼接成一个字符串
    │
    ├── 有 cache_control 标记的块 → cache_control 标记大概率被丢弃或内部映射(DeepSeek 没有原样实现此协议)
    ├── 普通文本块 → 拼入字符串
    │
    ▼
插到 messages 数组的最前面:
    { "role": "system", "content": "You are an interactive agent…\n\n…几百行的工具定义…" }

关于 cache_control 的存疑:Anthropic 的 ephemeral cache 机制允许客户端标记"这段内容短期内不会变,帮我缓存"。DeepSeek 没有公开同样的接口,兼容层大概率会丢掉这些标记。但这不代表完全没有缓存——DeepSeek V4 可能内部有自己的 context caching 实现,兼容层也许会在内部把标记了 cache_control 的块用自己的方式处理。这些都不开源,外部无法验证。

.claude.jsonlastTotalCacheReadInputTokens: ~550k 这个数字,大概率是兼容层在响应里回填的一个值——它需要让 Claude Code 认为 cache 在正常工作,避免 Claude Code 的缓存逻辑出现异常。但也不排除 DeepSeek 确实有内部缓存命中统计、通过兼容层如实上报的可能。


3.2 消息结构:从嵌套数组平摊成扁平的 role/content

这是最复杂的转换。Anthropic 的消息结构对 DeepSeek 内部协议来说是"反常规"的。以下转换方式基于 Anthropic 与主流 Chat API 的结构性差异做的合理推断——兼容层的实际实现可能有所不同,但结构转换的大方向是一致的。

普通文本消息
// Anthropic — content 永远是一个数组
{ "role": "user", "content": [{ "type": "text", "text": "Hello" }] }
​
//        ↓ 兼容层转换 ↓
​
// DeepSeek — 单 block 直接降维成字符串
{ "role": "user", "content": "Hello" }

如果 content 里有多段文本(比如 Claude Code 在 user 消息里同时附带了上下文说明),就拼接:

// Anthropic
{ "role": "user", "content": [
    { "type": "text", "text": "下面是一段代码:" },
    { "type": "text", "text": "def foo(): pass" }
]}
​
// ↓↓
​
// DeepSeek
{ "role": "user", "content": "下面是一段代码:\ndef foo(): pass" }
工具调用(Assistant 说"我要用工具")

这是结构差异最大的地方。基于业内主流 Chat API 的格式推断,转换大致如下:

// Anthropic — tool_use 嵌在 content 数组里,和文本是兄弟关系
{
  "role": "assistant",
  "content": [
    { "type": "text",    "text": "让我读一下这个文件" },
    { "type": "tool_use", "id": "toolu_001", "name": "Read", "input": { "file_path": "/tmp/test.py" } }
  ]
}
​
//        ↓ 兼容层转换(推断) ↓
​
// DeepSeek 内部协议 — tool_calls 大概率是消息的独立字段
{
  "role": "assistant",
  "content": "让我读一下这个文件",
  "tool_calls": [
    {
      "id": "toolu_001",
      "type": "function",
      "function": {
        "name": "Read",
        "arguments": "{\"file_path\": \"/tmp/test.py\"}"   // ← JSON 对象 → JSON 字符串!
      }
    }
  ]
}

注意这里的一个关键转换:Anthropic 的 input 字段是一个 JSON 对象,而内部协议中的 arguments 是一个 JSON 字符串。兼容层必须在这里做 JSON.stringify(input)。反过来,在响应侧做 JSON.parse(arguments)

这个转换虽然看起来简单,但有隐性风险:

  • 如果 input 里包含特殊字符(换行符、Unicode、emoji),stringify/parse 可能出问题

  • 如果 input 的值本身是一个复杂的嵌套对象,转成字符串再转回来,有可能引入微妙的格式差异(比如 key 的顺序、数字精度)

工具结果(User 说"这是工具返回的结果")

接下来这个角色映射是推断出来的——类比主流 Chat API 的格式,tool_result 最自然的对应就是独立的 tool 角色:

// Anthropic — tool_result 是一种特殊的 user 消息
{
  "role": "user",
  "content": [
    { "type": "tool_result", "tool_use_id": "toolu_001", "content": "print('hello')" }
  ]
}
​
//        ↓ 兼容层转换(推断) ↓
​
// DeepSeek — 很可能映射到独立的 tool 角色(不是 user!)
{
  "role": "tool",
  "tool_call_id": "toolu_001",
  "content": "print('hello')"
}

这个转换要求兼容层在遍历消息时跟踪语义变化:当看到 role: "user" + type: "tool_result",它不能只是简单映射字段名,而是要把整条消息的角色从 user 改成 tool。如果有多条连续的 tool_result 消息(Claude Code 并行调用多个工具时就会这样),每条都要独立映射,并且保持 tool_call_id 的对应关系不变。

有一个细微但重要的差异

Anthropic 协议允许一条 assistant 消息里同时包含文本和多个 tool_use

{
  "role": "assistant",
  "content": [
    { "type": "text",      "text": "我需要同时读取两个文件" },
    { "type": "tool_use",  "id": "t1", "name": "Read", "input": {…} },
    { "type": "tool_use",   "id": "t2", "name": "Read", "input": {…} }
  ]
}

这转换后就是一条带有多个 tool_calls 的 assistant 消息。反过来,也可能出现 Claude Code 分两条 assistant 消息各发一个 tool_use 的情况。兼容层需要保留这种"一条消息里几个 tool call"的对应关系,不能错误地把它们拆散或者错误合并。

图片/多模态内容
// Anthropic
{ "type": "image", "source": { "type": "base64", "media_type": "image/png", "data": "iVBORw0KGgo…" } }
​
//        ↓ 兼容层转换(推断) ↓
​
// DeepSeek 内部协议 — 大概率拼成 data URL
{ "type": "image_url", "image_url": { "url": "data:image/png;base64,iVBORw0KGgo…" } }

3.3 工具定义:从 input_schema 到 parameters

// Anthropic
{
  "name": "Read",
  "description": "Reads a file from the local filesystem.",
  "input_schema": {
    "type": "object",
    "properties": {
      "file_path": { "type": "string", "description": "The absolute path to the file" }
    },
    "required": ["file_path"]
  }
}
​
//        ↓ 兼容层转换 ↓
​
// DeepSeek 内部协议
{
  "type": "function",
  "function": {
    "name": "Read",
    "description": "Reads a file from the local filesystem.",
    "parameters": {
      "type": "object",
      "properties": {
        "file_path": { "type": "string", "description": "The absolute path to the file" }
      },
      "required": ["file_path"]
    }
  }
}

三个变化(基于主流 Chat API 格式推断):

  1. 外加一层 { "type": "function", "function": {...} } 包装

  2. input_schemaparameters(仅仅改名)

  3. 如果 Anthropic 的 tool 定义里有 cache_control,大概率被丢弃或内部映射

Claude Code 每次请求发的工具数量可能非常大——特别是有多个 MCP server 的时候。像上面截图的配置里挂了 mingpan、word、powerpoint、figma,工具总数轻松过百。这些定义每次都原封不动地塞在请求里,兼容层要对每条逐个做这种结构包装。不过兼容层也可能采用更聪明的策略——比如对工具定义做一次转换后缓存,只在工具列表变化时重新处理。具体实现方式取决于兼容层的设计。


3.4 Thinking / Extended Thinking:黑盒映射

Anthropic 的 extended thinking 是一个独立参数:

"thinking": { "type": "enabled", "budget_tokens": 4000 }

DeepSeek V4 Pro 确实有原生的推理/思维链能力,但它的 API 参数格式没有公开发布。兼容层在这里是厂商特定映射——外部无从得知这个 budget_tokens: 4000 具体被转化成了 DeepSeek 的什么内部参数。

在 settings.json 里配了 ANTHROPIC_REASONING_MODEL: "DeepSeek-V4-pro[1M]",这是告诉 Claude Code"这个模型支持 thinking"。如果这里配成一个不支持 thinking 的模型名,Claude Code 根本不会发 thinking 参数,兼容层也不用处理。


3.5 模型名解析

// Claude Code 发出
"model": "DeepSeek-V4-pro[1M]"
​
// 兼容层解析
// "DeepSeek-V4-pro" → 模型本体
// "[1M]"            → 开关标记:启用 1M 上下文窗口

[1M] 后辍是 DeepSeek 兼容层自己约定的标记语法,不是 Anthropic 协议的内容。兼容层把它解析成一个 flag,在调 DeepSeek 原生 API 时设置对应的上下文长度参数。

完整的模型配置:

"ANTHROPIC_DEFAULT_FABLE_MODEL":      "DeepSeek-V4-pro[1M]",   // ← 1M 上下文
"ANTHROPIC_DEFAULT_FABLE_MODEL_NAME": "DeepSeek-V4-pro",
"ANTHROPIC_DEFAULT_OPUS_MODEL":       "DeepSeek-V4-pro[1M]",   // ← 1M 上下文
"ANTHROPIC_DEFAULT_OPUS_MODEL_NAME":  "DeepSeek-V4-pro",
"ANTHROPIC_DEFAULT_SONNET_MODEL":     "DeepSeek-V4-pro[1M]",   // ← 1M 上下文
"ANTHROPIC_DEFAULT_SONNET_MODEL_NAME":"DeepSeek-V4-pro",
"ANTHROPIC_DEFAULT_HAIKU_MODEL":      "DeepSeek-V4-pro",       // ← 默认上下文
"ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME": "DeepSeek-V4-pro",

Claude Code 内部有"模型路由"机制——它根据任务复杂度自动选模型:

  • 简单的一次性问答 → 走 Haiku(上面配置:DeepSeek V4 Pro,无 1M)

  • 中等复杂度任务 → 走 Sonnet(上面配置:DeepSeek V4 Pro,有 1M)

  • 复杂多步骤任务 → 走 Opus 或 Fable(上面配置:DeepSeek V4 Pro,有 1M)

但是,到了兼容层,这些不同的模型名最重要也最可确认的区别是 [1M] 后缀带来的上下文窗口大小差异。至于 [1M] 版和无后缀版本是否指向同一模型的不同参数配置(如上下文长度、推理算力配额),还是完全同一套推理管线,外部无从确认。Claude Code 以为自己调了四个不同能力的模型,实际后端行为取决于 DeepSeek 的模型路由策略。


3.6 其他参数的映射(推断)

Anthropic DeepSeek 说明(基于类比推断)
max_tokens max_tokens 大概率原值传递
temperature temperature 大概率原值传递(两边的合法范围不同,兼容层可能负责 clamp)
top_p top_p 大概率原值传递
top_k 大概率丢弃或内部映射。DeepSeek 公开 API 无 top_k 对应字段
stop_sequences stop 很可能仅改名,值不变
metadata.user_id 可能映射到 user 大概率丢弃;也可能被兼容层留作日志

top_k 的映射问题在绝大多数场景下没有实际影响——Anthropic 官方也建议不要同时使用 top_ktop_p,Claude Code 通常不设这个参数。但如果通过自定义 hook 注入了 top_k,它很可能在兼容层被丢弃。


四、聊天模板(Chat Template):从 JSON 到纯文本

第三节聊完了兼容层如何把 Anthropic JSON 转成内部协议 JSON。但内部协议 JSON 仍然不是模型最终消费的东西。在真正喂给推理引擎之前,还有一道关键工序——这一步往往被忽略,但它决定了模型到底"看"到了什么:

内部协议 JSON → Chat Template 渲染 → 纯文本字符串 → Tokenizer → token ID 序列 → 模型

4.1 模型收到的不是 JSON,是一串 token

不管兼容层产出的内部 JSON 长什么样,模型最终拿到的不是结构化数据,而是一个长整型数组:[1, 2675, 389, 337, ...]。这个数组里的每个数字是一个 token ID,指向模型的词表(vocabulary)中的某个元素。模型在这个数组上做自注意力运算,通过 token 之间的位置关系和 special token 的边界标记来"理解"上下文——它对 JSON 的结构、字段名、类型标记完全无感知

4.2 什么是 Chat Template

Chat Template 是挂在 tokenizer 上的一个字符串模板(通常用 Jinja2 语法),它定义了:

  • system prompt 用什么标记包裹、放在什么位置

  • user / assistant / tool 消息分别用什么标记分隔

  • 工具定义和工具调用怎么嵌入

  • 是否需要在对话末尾追加一个"生成提示"(generation prompt)来触发 assistant 回复

不同模型的 Chat Template 完全不同。DeepSeek V4 的模板没有官方文档,但我们可以参考 DeepSeek V3 的开源 Chat Template 来推测方向。以下是一个推演示例(标记语法只是示意,V4 的实际标记一定不同):

假设兼容层产出的内部 JSON 是:

system:  "You are an interactive agent with tools...\n\n## Tools\n..."  
messages:
  [0] { role: "system", content: "<拼好的 system>" }
  [1] { role: "user",    content: "帮我读取 main.py" }
  [2] { role: "assistant", content: "让我来读取。", tool_calls: [{...Read...}] }
  [3] { role: "tool",      content: "print('hello world')", tool_call_id: "t1" }

Chat Template 把它渲染成一段纯文本:

<|begin▁of▁sentence|>
You are an interactive agent with tools...
​
## Tools
- Read: Reads a file from the local filesystem.
  Parameters: {"file_path": {"type": "string", ...}}
- Bash: Runs a shell command.
  Parameters: {"command": {"type": "string", ...}}
​
<tools_available>
以上工具可选调用,每次可调用 0~N 个。
</tools_available>
​
<solver>帮我读取 main.py</solver>
<assistant>让我来读取。</assistant>
<tool_call>
{"name": "Read", "arguments": {"file_path": "/tmp/main.py"}}
</tool_call>
<tool_response>
print('hello world')
</tool_response>
<assistant>

注意:上面用到的 <|begin▁of▁sentence|><solver><tool_call><tool_response> 等标记,来自 DeepSeek V3 的开源 Chat Template,仅作示意。DeepSeek V4 Pro 是新一代架构,它的 special token 设计很可能完全不同于 V3——这里展示的是渲染的逻辑而非精确的标记语法。

4.3 Tokenizer:从文本到数字

渲染出的纯文本再经过 tokenizer 分词:

"<|begin▁of▁sentence|>"  →  [1]
"You"                     →  [2675]
" are"                    →  [389]
" an"                     →  [337]
" interactive"            →  [12345, 678]
" agent"                  →  [23456]
...
"Read"                    →  [34567]
...
"<solver>"               →  [某 special token ID]
"帮我读取"                →  [45678, 12345]
" main"                   →  [9876]
".py"                     →  [8765]
...
"<assistant>"            →  [某 special token ID]
"让我来读取。"             →  [56789, 23456, 7890]
...
"<tool_call>"            →  [某 special token ID]
"{"                       →  [123]
"\"name\""                →  [456, 789]
": "                      →  [234, 345]
"\"Read\""                →  [567, 890]
...
"{"                       →  [123]
"\"file_path\""           →  [456, 789, 234]
": "                      →  [234, 345]
"\"/tmp/main.py\""        →  [567, 890, 123, 456]
...
"</tool_response>"       →  [某 special token ID]
"<assistant>"            →  [某 special token ID]

最终得到一个长整型序列:

[1, 2675, 389, 337, 12345, 678, 23456, ..., 34567, ..., 
 45678, 12345, 9876, 8765, ..., 56789, 23456, 7890, ..., 
 123, 456, 789, ..., 567, 890, 123, 456, ...]

这才是模型推理引擎真正接收的输入。 没有 role 字段,没有 content block 类型,没有 JSON 的花括号结构——只有一个 token ID 向量。

4.4 这对兼容层的转换质量意味着什么

这个流程揭示了一个重要的现实:在 Anthropic 协议层精心构造的结构,到了模型那里已经几乎不剩什么了。

  • Anthropic 的 content block 类型(texttool_usetool_resultimage)到了模型的 token 序列里,全部坍缩成了被 special token 包裹的文本段。模型区分它们只靠 special token 的 embedding 和上下文位置。

  • 如果兼容层的 Chat Template 处理工具调用的 special token 和 Anthropic 原生模型的训练模板不同,模型对"工具调用""工具结果"这些概念的理解可能会有微妙偏差。

  • 如果 tokenizer 对 JSON 特殊字符({}[]":,)的分词策略不同,同一个 tool use 的 arguments 在两个模型那里会产生完全不同的 token 序列。这直接影响模型对 JSON 结构的"直觉"——因为模型在训练时学到的 JSON 模式是和特定分词方式绑定的。


五、DeepSeek V4 Pro 推理

token ID 序列进入 Transformer 推理引擎。模型逐 token 生成输出——预测下一个最可能的 token,把它追加到序列末尾,然后基于新的序列再预测下一个,如此往复直到生成结束标记。

接着,推理引擎的输出经过反向流程:

token ID 序列 → Detokenizer → 纯文本 → Chat Template 反向解析 → 内部协议 JSON

然后才交给兼容层做 Anthropic 格式的反向翻译。


六、响应侧:DeepSeek 原生 → Anthropic 格式,逐字段反转

6.1 普通文本响应

// DeepSeek 返回(内部格式,结构接近业界主流 Chat API)
{
  "id": "chatcmpl-abc123",
  "object": "chat.completion",
  "choices": [{
    "index": 0,
    "message": {
      "role": "assistant",
      "content": "文件内容是 print('hello world')"
    },
    "finish_reason": "stop"
  }],
  "usage": {
    "prompt_tokens": 5000,
    "completion_tokens": 100,
    "total_tokens": 5100
  }
}
​
//        ↓ 兼容层转换 ↓
​
// Anthropic 格式
{
  "id": "msg_abc123",
  "type": "message",
  "role": "assistant",
  "model": "DeepSeek-V4-pro[1M]",   // ← 回填用户请求的模型名
  "content": [
    { "type": "text", "text": "文件内容是 print('hello world')" }
  ],
  "stop_reason": "end_turn",         // ← stop → end_turn
  "stop_sequence": null,
  "usage": {
    "input_tokens": 5000,
    "output_tokens": 100
  }
}

几个转换要点:

  • content 必须是数组:哪怕只有一段纯文本,也要包在 [{ "type": "text", "text": "..." }] 里。这是 Anthropic 协议的硬性要求——text 必须作为 content block 存在。

  • model 回填原始请求的模型名:不能用 DeepSeek 内部模型名(比如 deepseek-v4-pro-internal),必须用 Claude Code 请求时传的那个名字 DeepSeek-V4-pro[1M]。否则 Claude Code 的模型统计和模型路由会乱。

  • finish_reason 映射

DeepSeek Anthropic 含义
"stop" "end_turn" 模型自然终止
"tool_calls" "tool_use" 模型要求调用工具
"length" "max_tokens" 达到 max_tokens 上限被截断

6.2 Tool Call 响应

// DeepSeek 返回
{
  "choices": [{
    "message": {
      "role": "assistant",
      "content": "让我读取这个文件",
      "tool_calls": [{
        "id": "call_xyz",
        "type": "function",
        "function": {
          "name": "Read",
          "arguments": "{\"file_path\": \"/tmp/test.py\"}"    // ← JSON 字符串
        }
      }]
    },
    "finish_reason": "tool_calls"
  }]
}
​
//        ↓ 兼容层转换 ↓
​
// Anthropic 格式
{
  "role": "assistant",
  "content": [
    { "type": "text",     "text": "让我读取这个文件" },
    {
      "type": "tool_use",
      "id": "call_xyz",
      "name": "Read",
      "input": { "file_path": "/tmp/test.py" }               // ← JSON.parse(arguments)
    }
  ],
  "stop_reason": "tool_use"
}

这里完成了请求侧的反向操作:arguments(JSON 字符串)→ JSON.parse()input(JSON 对象)。

一个容易被忽略的坑:DeepSeek 有时会在 arguments 里返回不符合 JSON 语法的内容。比如参数值里有未转义的换行符、或者模型输出了不完整的 JSON(因为 max_tokens 截断)。兼容层必须处理这些异常:

  • 截断的 JSON → 尝试补全,补不全就丢弃这个 tool_call,或者交给客户端自己兜底

  • 非法 JSON → 同样需要容错逻辑

  • 空 arguments → {}

6.3 Thinking / 推理内容

DeepSeek V4 Pro 在推理过程中会产生内部思考:

// DeepSeek 可能返回
{
  "choices": [{
    "message": {
      "reasoning_content": "用户想改 main.py,我应该先用 Read 看一下文件内容,然后再判断改哪里…",
      "content": "让我先读取文件。"
    }
  }]
}
​
//        ↓ 兼容层转换 ↓
​
// Anthropic thinking 格式
{
  "content": [
    { "type": "thinking", "thinking": "用户想改 main.py,我应该先用 Read 看一下文件内容…" },
    { "type": "text",     "text":     "让我先读取文件。" }
  ]
}

这个转换对 Claude Code 的用户体验有直接影响:Anthropic 协议的 thinking 块在 Claude Code 界面上是折叠或灰度展示的,相当于模型的"内心独白"。如果兼容层没有正确映射这个字段,推理内容就会暴露在正文里,或者直接丢失。

从配置里 ANTHROPIC_REASONING_MODEL 的设置来看,这种用法是期望使用 thinking 功能的。兼容层在这里的处理质量直接影响输出效果。

6.4 Token Usage 统计

// DeepSeek 原样返回
"usage": {
  "prompt_tokens": 5000,
  "completion_tokens": 100,
  "total_tokens": 5100
}
​
//        ↓ 兼容层转换 ↓
​
// Anthropic 格式(多了 cache 相关字段)
"usage": {
  "input_tokens": 5000,
  "output_tokens": 100,
  "cache_read_input_tokens": ~550000,     // ← 这个数字从哪来?
  "cache_creation_input_tokens": 0
}

.claude.json 记录的上次请求 cache_read_input_tokens: ~550k 非常醒目。几个可能的解释(按可能性排序):

  • 兼容层在 Anthropic 响应里回填了一个估算值或固定值,让 Claude Code 认为 cache 在正常工作——维持接口兼容性

  • DeepSeek V4 内部有 context caching 实现,兼容层维护了一个计数器追踪命中情况,如实上报

  • 这个数字是其他内部指标(如 system prompt 的 token 数)映射过来的,并非真正的 cache 命中统计

没有开源代码,哪一种成立无法确认。

6.5 Streaming 响应(SSE 事件映射)

如果 Claude Code 用了 streaming(stream: true),兼容层还要做 Streaming SSE 事件的实时翻译:

DeepSeek SSE Event(内部格式)                  Anthropic SSE Event
─────────────────────────────────────────────────────────────────────
(首个 chunk)                ──────────→    event: message_start
                                            data: {"type":"message_start","message":{"id":"...","model":"...",...}}
​
data: {"choices":[{"delta":   ──────────→  event: content_block_start
  {"content":"Hello"}}]}                    data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
​
                                            event: content_block_delta
                                            data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}
​
data: {"choices":[{"delta":   ──────────→  event: content_block_start
  {"tool_calls":[{"index":0,               data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"...","name":"Read","input":{}}}
   "id":"call_x",
   "function":{"name":"Read"}}]}}]}
​
data: {"choices":[{"delta":   ──────────→  event: content_block_delta
  {"tool_calls":[{"index":0,               data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"file"}}
   "function":{"arguments":"{\"file"}}]}}]}
​
​
data: {"choices":[{"delta":{}, ──────────→ event: content_block_delta
  "finish_reason":"stop"}}]                 data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"_path\":\"/tmp/test.py\"}"}}
​
                                            event: message_delta
                                            data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null}}
​
[DONE]                       ──────────→  event: message_stop
                                            data: {"type":"message_stop"}

Streaming 转换的难点在于:

  • Tool call 参数是增量 JSON 片段。兼容层必须维护一个 StringBuilder,把零散的 partial_json 拼成完整片段后逐块输出

  • 事件顺序必须正确:必须先发 content_block_start 再发 content_block_delta,不能乱序

  • 心跳:Anthropic 的 SSE 实现有 ping 心跳机制(具体间隔由服务端控制)。兼容层如果要完整模拟 Anthropic 服务器的行为,也需要模拟这个机制,否则 Claude Code 可能误判连接断开


七、特殊配置:全模型路由到一个后端

值得拿出来单说的一点是模型路由配置。Claude Code 内置了"模型智能路由"——它根据任务复杂度自动在 Haiku / Sonnet / Opus / Fable 之间选择。但 settings.json 把四个层级全部指到了 DeepSeek:

"ANTHROPIC_DEFAULT_HAIKU_MODEL":  "DeepSeek-V4-pro",       // 无 1M
"ANTHROPIC_DEFAULT_SONNET_MODEL": "DeepSeek-V4-pro[1M]",   // 有 1M
"ANTHROPIC_DEFAULT_OPUS_MODEL":   "DeepSeek-V4-pro[1M]",   // 有 1M
"ANTHROPIC_DEFAULT_FABLE_MODEL":  "DeepSeek-V4-pro[1M]",   // 有 1M

这让 Claude Code 在决定"这个任务很复杂,我得用 Opus"时,实际发的还是同一个 DeepSeek V4 Pro。这是一个合理的做法——如果只有一个 API 后端,没必要让 Claude Code 因为模型不匹配而拒绝某些类型的请求。

唯一有意义的区分是 Haiku 走了无 [1M] 版本。这也许意味着"简单任务不需要 1M 上下文,省一点"——但从 DeepSeek 的计费方式看,这个区分可能更多是为了让 Claude Code 内部的路由逻辑不报错。


八、完整链路时序图

用户输入文字并回车
        │
        ▼
┌─ Claude Code 客户端 ──────────────────────────────────────────┐
│                                                                │
│  1. 读取 MEMORY.md + 关联记忆文件                               │
│  2. 合并 system prompt (内置 + skills + CLAUDE.md)             │
│  3. 收集 tools 定义 (内置 + MCP servers)                       │
│  4. 组装完整的 Anthropic Messages API 请求体                    │
│                                                                │
└──┬─────────────────────────────────────────────────────────────┘
   │  HTTPS POST to https://api.deepseek.com/anthropic/v1/messages
   │  Authorization: Bearer sk-... (DeepSeek API Key)
   ▼
┌─ api.deepseek.com/anthropic ───────────────────────────────────┐
│                                                                │
│  ★ 请求侧:Anthropic → 内部协议 ★                              │
│                                                                │
│  □ system 数组 → 拼接字符串, cache_control 大概率丢弃或内部映射, 插到 messages 最前    │
│  □ content[{text}] → content: "string" (去数组化,推断)                   │
│  □ content[{tool_use}] → tool_calls[], input(对象)→arguments(JSON字符串,推断) │
│  □ content[{tool_result}] → role 从 user 改为 tool(推断)                  │
│  □ content[{image}] → type:image_url, base64 → data URL(推断)            │
│  □ tools[].input_schema → tools[].function.parameters(推断)               │
│  □ thinking → DeepSeek 内部 reasoning 参数(黑盒)                          │
│  □ model="xxx[1M]" → 解析 [1M] flag, 映射到内部模型名                       │
│  □ stop_sequences → stop(推断), top_k → 大概率丢弃或内部映射                │
│  □ metadata.user_id → 大概率丢弃或内部映射                                 │
│                                                                │
│  输出:内部协议 JSON                                             │
│                                                                │
└──┬─────────────────────────────────────────────────────────────┘
   │
   ▼
┌─ DeepSeek 推理管线 ───────────────────────────────────────────┐
│                                                                │
│  ★ Chat Template 渲染(JSON → 纯文本)★                        │
│  □ 按模型模板将 role/tool_calls/arguments 用 special token 包裹│
│  □ system prompt 和工具定义嵌入指定位置                         │
│  □ 末尾追加 generation prompt 标记触发 assistant 回复          │
│                                                                │
│  输出:纯文本字符串                                              │
│  "<|begin▁of▁sentence|>You are...<solver>帮我读取...<tool_call>..."│
│       │                                                        │
│       ▼                                                        │
│  ★ Tokenizer(纯文本 → token ID 序列)★                        │
│  □ 每个词/标记映射到词表中的 token ID                           │
│  □ special token 有独立 ID(如 <solver> → ID 某值)             │
│                                                                │
│  输出:长整型数组                                               │
│  [1, 2675, 389, 337, 12345, ...]                               │
│       │                                                        │
│       ▼                                                        │
│  ★ Transformer 推理引擎 ★                                      │
│  □ 自注意力在 token 序列上运算                                  │
│  □ 逐 token 预测下一个最可能的 token                            │
│  □ 直到生成结束标记(<eos> 或 stop_reason 触发)                │
│                                                                │
│  输出:token ID 序列                                            │
│       │                                                        │
│       ▼                                                        │
│  ★ Detokenizer + 反向解析 ★                                    │
│  □ token ID → 文本 → 内部协议 JSON(Chat Template 反向)       │
│                                                                │
│  输出:内部协议 JSON                                             │
│                                                                │
└──┬─────────────────────────────────────────────────────────────┘
   │
   ▼
┌─ api.deepseek.com/anthropic ───────────────────────────────────┐
│                                                                │
│  ★ 响应侧:内部协议 → Anthropic ★                              │
│                                                                │
│  □ choices[].message.content → content[{type:"text", text:...}](推断)    │
│  □ choices[].message.tool_calls[] → content[{type:"tool_use", ...}](推断) │
│  □ tool_calls[].function.arguments → JSON.parse() → input(推断)          │
│  □ finish_reason: stop→end_turn, tool_calls→tool_use, length→max_tokens(推断)│
│  □ reasoning_content → content[{type:"thinking", thinking:...}](黑盒)     │
│  □ model → 回填请求时的 "DeepSeek-V4-pro[1M]"                              │
│  □ usage → input_tokens/output_tokens + 回填或估算 cache_read_input_tokens │
│  □ 如果是 stream: DeepSeek SSE → Anthropic SSE 事件映射(推断)           │
│                                                                │
└──┬─────────────────────────────────────────────────────────────┘
   │  HTTPS Response(标准 Anthropic Messages API 格式)
   ▼
┌─ Claude Code 客户端 ──────────────────────────────────────────┐
│                                                                │
│  1. 解析 Anthropic 格式响应                                    │
│  2. type:"text" → 显示给用户                                   │
│  3. type:"tool_use" → 执行对应工具                             │
│  4. type:"thinking" → 在界面折叠展示                           │
│  5. usage → 写入 .claude.json 的 lastModelUsage 统计          │
│  6. 如果执行了工具,结果返回,进入下一轮对话                    │
│                                                                │
└────────────────────────────────────────────────────────────────┘
        │
        ▼
    用户看到回复
    (如果回复是 tool_use,则本轮结束后 Claude Code 自动进入下一轮循环)

九、一些值得关注的点

9.1 Token 统计的准确性

.claude.json 的数据示例:

"lastTotalInputTokens": ~68000,
"lastTotalOutputTokens": ~16000,
"lastTotalCacheReadInputTokens": ~550000

cache_read_input_tokens 高达 550k,远超实际 input tokens 的 68k。这说明缓存统计的口径和实际输入很可能是不同维度的——缓存数字可能包含了历史上被缓存的 system prompt 部分,也可能是兼容层基于某个固定公式估算的结果,而非真实的当次请求缓存命中。

9.2 转换层的性能开销

lastAPIDuration: ~217000ms(约 3.6 分钟),这个耗时包含:

  • 兼容层的格式转换耗时(通常 <100ms,可忽略)

  • 网络传输(国内到 DeepSeek 服务器)

  • DeepSeek V4 Pro 的推理耗时(占绝对大头)

在 1M 上下文窗口 + 大量工具定义 + thinking 开启的情况下,3.6 分钟的推理时间并不离谱。

9.3 有哪些东西在转换过程中"丢了"

在转换中可能丢失的 影响(推测)
cache_control 标记 缓存控制精度大概率不如 Anthropic 原生,实际效果取决于 DeepSeek 内部实现
top_k 如果通过 hook 设置了这个参数,它大概率不会生效
metadata.user_id 大概率不影响功能,但 DeepSeek 侧的用户维度统计可能会丢失
精确的 cache token 统计 观察到的 cache 数字不一定反映真实命中情况

9.4 有哪些转换是"容易出 bug 的"

按风险从高到低排:

  1. Tool call arguments 的 JSON 序列化/反序列化:嵌套对象、特殊字符、截断的 JSON,是兼容层最脆弱的环节

  2. Chat Template 的 special token 对齐:DeepSeek V4 的 Chat Template 如果把 tool_call / tool_response / thinking 用不同于 Anthropic 训练数据的 special token 包裹,模型对这些结构的"理解"会偏离预期

  3. Thinkinking ↔ reasoning_content 映射:两个厂商的推理格式都是半公开的,对齐程度直接影响 Claude Code 的 thinking 折叠功能;此外 tokenizer 对推理内容的分词方式是否与 Anthropic 一致也是未知

  4. Streaming 事件的顺序和完整性:如果 DeepSeek 的 SSE 事件格式有版本变化,兼容层需要跟进

  5. 多 tool_use 并行时的消息映射:多个 tool 的 id 对应关系一旦错乱,Claude Code 会匹配不上工具结果


十、一句话总结

api.deepseek.com/anthropic 本质上是一个双向协议翻译器:它在 DeepSeek V4 Pro 上面模拟了一套完整的 Anthropic Messages API。Claude Code 全程以为自己在跟 Anthropic 服务器对话——它发出的每一个字段都被翻译成 DeepSeek 能懂的格式,返回的每一个字符又被翻译回 Anthropic 格式。settings.json 里用一个 DeepSeek API Key 和五个 ANTHROPIC_DEFAULT_*_MODEL 配置,让这套翻译器在每次对话中默默地工作了几百次(numStartups: ~380 记录了)。

对于使用者来说,最重要的是理解几件事:

  1. 观察到的 cache_read_input_tokens 统计,数据来源不透明——它可能是真实的缓存命中、兼容层的估算值、或者只是维持接口兼容性的占位数字。不要像用 Anthropic 原生 API 一样把这些数字当精确指标。

  2. thinking 功能在 DeepSeek V4 上有真实的推理能力支撑,但格式映射是兼容层做的,对齐程度可能不如 Anthropic 原生。

  3. 所有模型名映射到同一族 DeepSeek V4 Pro,意味着 Claude Code 的"模型路由"实际上不再影响模型能力——唯一有意义的参数是 [1M] 后缀带来的上下文窗口大小差异。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐