0%

Claude Code SDK Note

想看看 CC 使用的 SDK。基于对 Claude Code v2.1.88 源码 src/services/api/src/query.ts 的阅读。


一、SDK

CC 用的就是 @anthropic-ai/sdk,我们普通人想用就用 npm install @anthropic-ai/sdk,文档在开始使用 Claude - Claude API Docs

当然,源码中 package.json"dependencies": {}。SDK 在 Bun 打包时已捆进 claude.exe,没有运行时的 npm 依赖。


二、支持四种后端

getAnthropicClient() 根据环境变量动态返回不同的客户端实例,除了默认后端是 api.anthropic.com,改了环境变量还能用 AWS 的 AWS Bedrock、Google 的 GCP Vertex AI、Azure 的 Microsoft Foundry。不过对应的 SDK 也变为 @anthropic-ai/bedrock-sdk 这样的。

在运行时按需动态加载。

1
2
3
if (CLAUDE_CODE_USE_BEDROCK) {
const { AnthropicBedrock } = await import('@anthropic-ai/bedrock-sdk')
}

三、调用链

这个结构图直接让 CC 帮忙画了:

image-20260418045057949

queryModelasync function* 异步生成器,用 for await 遍历 SSE 流,每收到一个 event 就立刻 yield 给上层渲染。


四、SDK 发出的 JSON

如果要看一下底层,paramsFromContext() 构建的完整请求体大致是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"model": "claude-sonnet-4-6",
"system": [
{
"type": "text",
"text": "You are Claude Code, Anthropic's official CLI...\n# auto memory\n...",
"cache_control": { "type": "ephemeral" }
}
],
"messages": [
{ "role": "user", "content": [{ "type": "text", "text": "创建 helloworld.py" }] }
],
"tools": [
{ "name": "Write", "description": "...", "input_schema": { ... } },
{ "name": "Read", "description": "...", "input_schema": { ... } }
],
"thinking": { "type": "adaptive" },
"max_tokens": 32000,
"stream": true,
"betas": ["interleaved-thinking-2025-05-14", "..."]
}

cache_control 做 prompt caching。Claude API 文档说了目前 type 只有 ephemeral,默认 5 分钟。

thinking 支持 Adaptive thinking,也可以 {"type": "enabled", "budget_tokens": N}extended thinking

betas 就是 beta-features,如例子中的 extended thinking。


五、LLM 实际得到的输入

API 服务器收到请求 JSON 后,会应用 Anthropic 内部的 Chat Template,把 JSON 结构展平成 token 序列再输入模型。

模型感知到系统提示的文本、用户消息的文本、工具描述的文本。

但 Anthropic 可能是处于安全考量,未公开 Chat Template 的具体格式。

不过可以从模型卡片文档中管中窥豹:

image-20260418051936878

但依然不知道具体的 Human/Assistant 是什么样的 Chat Template


六、模型的流式输出格式

响应是 Server-Sent Events(SSE)流,每条 event 是一个小 JSON。CC 用claude.ts 里的 for await (const part of stream) 遍历。

message_start 是消息的开始,含初始 usage 统计。

content_block_start 是一个 content block 开始,type = thinking/text/tool_use

content_block_delta 是该 block 的增量内容,type = thinking_delta/text_delta/input_json_delta

content_block_stop 是该 block 的结束。

message_delta 包含 stop_reason、最终 token 计数,message_stop 是整条消息结束。

例如,如果我要求 CC 创建一个 helloworld.py,其返回的 stream 里的 event 顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
content_block_start   {index:0, type:"thinking"}
content_block_delta {delta:{type:"thinking_delta", thinking:"用户要创建文件..."}}
content_block_delta {delta:{type:"thinking_delta", thinking:"用 Write 工具..."}}
content_block_stop {index:0}

content_block_start {index:1, type:"tool_use", id:"toolu_abc", name:"Write", input:""}
content_block_delta {delta:{type:"input_json_delta", partial_json:"{\"file_path\":"}}
content_block_delta {delta:{type:"input_json_delta", partial_json:"\"helloworld.py\","}}
content_block_delta {delta:{type:"input_json_delta", partial_json:"\"content\":\"print('Hello, World!')\""}}
content_block_stop {index:1}

message_delta {delta:{stop_reason:"tool_use"}, usage:{output_tokens:87}}
message_stop

tool_use 的 input 是逐字符流式输出的 JSON 字符串片段,CC 把所有 partial_json 拼接起来,等 content_block_stop 之后再一次性 parse。这样做是为了 “avoid O(n²) partial JSON parsing”。


七、消耗 API 调用

每次 HTTP 请求 = 1 次 API 调用。

stop_reason: "tool_use" 后模型主动停下等工具结果,之后 Claude Code 发送一次附上 tool_result 的新请求。


八、thinking 内容与生命周期

请求里 "thinking": {"type": "adaptive"} 表示模型自行决定是否思考,简单问题可能跳过。

官方文档Claude 4 系列通过 Messages API 返回的 thinking 内容是摘要,不是原始 CoT,防蒸馏。原始 CoT token 被计费但对外不可见。

摘要由 API 服务端的 Haiku 模型生成。

大致情况如图:

context-window-thinking

redact-thinking beta

beta 生效时:API 跳过 Haiku 摘要器,返回 type: "redacted_thinking" block:

1
{ "type": "redacted_thinking", "data": "EqQBCgIYAhIM..." }

Claude Code 渲染为 ✻ Thinking…

beta 未生效时,源码注释(utils/betas.ts:264):”the summary is only used for ctrl+o display, which interactive users rarely open”,终端显示 ∴ Thinking… + Haiku 摘要。、

当然,在按 Ctrl+O 之前,摘要就已经生成好了。Haiku 摘要在 API 响应期间(流式输出时)就在服务端运行。Ctrl+O 触发的只是UI 视图切换。

安全触发的 redacted_thinking

触发原因:安全系统主动标记了 thinking 内容。


九、工具调用的 token 成本

tool_use 和 tool_result 都进入 messages 历史,后续每次请求原样携带。例如,Read 大文件的 tool_result 是全文加行号,但是如果之前读过了且时间戳未改动就是 "File unchanged since last read...",让 Claude 知道之前的上下文里有。

1
2
3
4
5
6
7
8
{
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": "toolu_abc",
"content": "File updated successfully."
}]
}

Reference