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 用这些字段算成本和上下文占用。工具调用的 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