Skip to content

AnthropicHandler:流式解析 Anthropic Messages API

源码版本v4.0.10

職責

AnthropicHandlerApiHandler 介面在 Anthropic Messages API 上的具體實作,也是 buildApiHandlerdefault 分支裡的兜底 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.idname 後存到 lastStartedToolCall,後續 input_json_deltapartial_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).

關鍵檔案

資料流

handler 接到呼叫後先決定走 prompt-cache 還是普通分支,兩條路徑構造不同的 requestBody,然後走 client.messages.createclient.beta.messages.create 拉流:

typescript
// 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 的核心翻譯邏輯:

typescript
// 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:ensureClientoptions.apiKey 為空時直接拋 "Anthropic API key is required"(apiKey required:46),這個錯會冒到 Task 層顯示。
  • SDK 構造失敗:new Anthropic(...) 拋錯會被重新包裝成 "Error creating Anthropic client: ..." 再拋(client wrap:56).
  • 未知模型 id:getModelmodelId 不在 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-01context-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

對照官方資料:Cline 文件 · README