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 用這些欄位算成本和 context 佔用。工具呼叫的 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