AnthropicHandler:流式解析 Anthropic Messages API
职责
AnthropicHandler 是 ApiHandler 接口在 Anthropic Messages API 上的具体实现,也是 buildApiHandler 在 default 分支里的兜底 handler (default Anthropic:504). 它把上层传来的 systemPrompt / messages / tools 拼成 Anthropic SDK 的请求,然后边收流边把原生的 BetaRawMessageStreamEvent 翻译成 Cline 统一的 ApiStreamChunk(ApiStreamChunk:3),让上层 Task 不用感知 Anthropic 协议细节。
它额外负责三件 Anthropic 特有的事:一是 prompt cache,在系统提示末尾打 cache_control: ephemeral 断点(cache_control:128);二是 extended thinking,按模型是否支持 reasoning 决定要不要带 thinking 字段、budget_tokens 给多少(thinkingConfig:107);三是 fast mode 和 1m context 这两个 beta 后缀,通过模型 id 末尾的 :fast / :1m 标记识别,再切到 client.beta.messages.create 带 beta header 的请求路径(fast mode detection:70)。
设计动机
- 懒加载 client:handler 构造时不创建 Anthropic SDK 客户端,等第一次
createMessage才在ensureClient里建(ensureClient:44),这样buildApiHandler为了限额 thinkingBudget 提前 new 一个 handler 也不会真发请求。 - 流式 chunk 翻译:Anthropic 原生流事件是
message_start / message_delta / content_block_start / content_block_delta / content_block_stop / message_stop六种,Cline 只关心其中能映射成text / reasoning / tool_calls / usage的部分(for await chunk:184),其他(message_stop)直接 skip。 - 工具调用增量拼参数:
content_block_start拿到tool_use.id和name后存到lastStartedToolCall,后续input_json_delta把partial_json一段段yield出去(input_json_delta:276),上层负责拼成完整 JSON。 - thinking 与 temperature 互斥:开 thinking 时
temperature必须置undefined(temperature thinking:123),这是 Anthropic 官方限制。 - prompt cache 断点位置:
cache_control打在 system 末尾而不是 tools 末尾,因为 tools 不变但 system 内容多,断点放 system 末尾命中率最高(system cache_control:124)。 - 重试装饰器:
createMessage上挂了@withRetry(),429 /RetriableError才重试,默认 3 次指数退避(withRetry:63)。 - adaptive thinking:Claude Opus 4.5+ 不再用 budgeted extended thinking,改成
type: "adaptive"加可选 effort 字段,handler 里检测模型 id 走不同分支(adaptive thinking:100).
关键文件
AnthropicHandler class:36— 实现ApiHandler,只持options和懒加载的client。ensureClient:44— 第一次发请求时才建new Anthropic(...),带 baseURL 和自定义 headers。createMessage with @withRetry:63— 主流式入口,async *生成器。useFastMode detection:70— 从模型 id 末尾:fast切到 beta API。budget_tokens:93— 从 options 取 thinking budget,0 表示关 thinking。thinkingConfig:107— adaptive 模型走{type:"adaptive"},其他走{type:"enabled", budget_tokens}。supportsPromptCache branch:114— 有 cache 时给 system 打cache_control,无 cache 走 else 分支。stream loop:183—for await chunk of stream把原生事件翻译成统一 chunk。tool_use block start:227— 拿到 tool_use id/name 存进lastStartedToolCall。getModel:305— 从anthropicModels表里查 ModelInfo,找不到回落到anthropicDefaultModelId。
数据流
handler 接到调用后先决定走 prompt-cache 还是普通分支,两条路径构造不同的 requestBody,然后走 client.messages.create 或 client.beta.messages.create 拉流:
// apps/vscode/src/core/api/providers/anthropic.ts
if (model.info.supportsPromptCache) {
const anthropicMessages = sanitizeAnthropicMessages(messages, true)
const requestBody: AnthropicMessageCreateParamsStreaming & Record<string, unknown> = {
model: modelId,
thinking: thinkingConfig,
max_tokens: model.info.maxTokens || 8192,
temperature: isAdaptiveThinkingModel ? undefined : reasoningOn ? undefined : 0,
system: [
{
text: systemPrompt,
type: "text",
cache_control: { type: "ephemeral" },
},
], // setting cache breakpoint for system prompt so new tasks can reuse it
messages: anthropicMessages,
stream: true,
tools: nativeToolsOn ? tools : undefined,
tool_choice: nativeToolsOn && !thinkingEnabled ? { type: "any" } : undefined,
}
// ...
stream = useFastMode
? await createFastModeMessage(requestBody)
: await client.messages.create(requestBody, /* options */)
}这条分支的关键在 cache_control 打在 system 数组最后一项上(cache_control:128),Anthropic 会把到这个断点为止的前缀缓存住,下一次同一前缀的请求能直接命中读缓存,显著降低系统提示的输入 token 成本。tool_choice 在 thinking 关掉时强制 {type:"any"}(tool_choice any:140),意思是「有工具就必须用」,但开 thinking 时这个强制会触发 Anthropic 的不兼容错误,所以那时只能 undefined 让模型自己决定。
流拿到后进 for await 翻译 chunk。下面这段是 text / reasoning 的核心翻译逻辑:
// apps/vscode/src/core/api/providers/anthropic.ts
for await (const chunk of stream) {
switch (chunk?.type) {
case "message_start":
{
const usage = chunk.message.usage
yield {
type: "usage",
inputTokens: usage.input_tokens || 0,
outputTokens: usage.output_tokens || 0,
cacheWriteTokens: usage.cache_creation_input_tokens || undefined,
cacheReadTokens: usage.cache_read_input_tokens || undefined,
}
}
break
// ...
case "content_block_delta":
switch (chunk.delta.type) {
case "text_delta":
yield { type: "text", text: chunk.delta.text }
break
case "input_json_delta":
if (lastStartedToolCall.id && lastStartedToolCall.name && chunk.delta.partial_json) {
yield {
type: "tool_calls",
tool_call: {
...lastStartedToolCall,
function: { ...lastStartedToolCall, id: lastStartedToolCall.id, name: lastStartedToolCall.name, arguments: chunk.delta.partial_json },
},
}
}
break
}
break
}
}message_start 一次性带出 input_tokens / output_tokens / cache_creation / cache_read 四个 usage 字段(message_start:186),后续 message_delta 还会增量给 output_tokens,Task 用这些字段算成本和上下文占用。工具调用的 JSON 参数以 partial_json 字符串片段的形式一段段 yield 出去,上层 parseAssistantMessageV2 自己负责把片段拼起来再 JSON.parse。
边界与失败
- 缺 API key:
ensureClient在options.apiKey为空时直接抛"Anthropic API key is required"(apiKey required:46),这个错会冒到 Task 层显示。 - SDK 构造失败:
new Anthropic(...)抛错会被重新包装成"Error creating Anthropic client: ..."再抛(client wrap:56). - 未知模型 id:
getModel在modelId不在anthropicModels表里时回落到anthropicDefaultModelId(fallback:311),不让上层崩。 - 429 限流:
@withRetry()装饰器读retry-after / x-ratelimit-reset头算延迟,默认三次重试,第三次失败原错误抛出(withRetry:29). - redacted thinking 块:
content_block_start收到redacted_thinking时 yield 一条 placeholder reasoning,并把加密的data原样带回去(redacted_thinking:219),因为后续请求 Anthropic 期望原样回传。 - 多 text block 之间插换行:
content_block_start收到第二个及以后的 text block 时先 yield 一个\n(text block newline:237),避免块之间贴在一起。 - fast mode + 1m 同时开:模型 id 同时带
:fast和:1m时,beta header 数组同时挂fast-mode-2026-02-01和context-1m-2025-08-07(fastModeBetas:76).
小结
AnthropicHandler 是 Cline 里最「全功能」的 provider 实现:它同时管着 prompt cache 断点、extended thinking / adaptive thinking、fast mode 和 1m context 两个 beta 路径,以及把原生流事件翻译成统一 ApiStreamChunk 的所有映射。看懂它再去看 OpenAI / Gemini / Bedrock 等其他 handler,基本都是同一套套路 —— 构造请求、拉流、翻译 chunk —— 只是各家协议字段不一样。系统提示怎么消费 cache_creation_input_tokens 等字段,看 PromptBuilder 那一层。
- 提供者选择入口:
/providers/api-handler - 系统提示如何拼装:
/prompts/prompt-builder - 工具如何转成 Anthropic Tool 定义:
/prompts/toolset