Skip to content

AnthropicHandler : analyser en streaming l'Anthropic Messages API

源码版本v4.0.10

Responsabilités

AnthropicHandler est l'implémentation concrète de l'interface ApiHandler pour l'Anthropic Messages API, et également le handler de repli de la branche default dans buildApiHandler (default Anthropic:504). Il assemble systemPrompt / messages / tools reçus de la couche supérieure en une requête SDK Anthropic, puis, à mesure qu'il consomme le flux, traduit les BetaRawMessageStreamEvent natifs en ApiStreamChunk unifié pour Cline (ApiStreamChunk:3), de sorte que la Task au-dessus n'a pas à connaître les détails du protocole Anthropic.

Il prend également en charge trois choses spécifiques à Anthropic : d'abord le prompt cache, en posant un point de rupture cache_control: ephemeral à la fin du system prompt (cache_control:128) ; ensuite l'extended thinking, en décidant selon que le modèle supporte le reasoning d'inclure ou non le champ thinking, et de fixer budget_tokens (thinkingConfig:107) ; enfin les deux suffixes beta fast mode et 1m context, identifiés via les marqueurs :fast / :1m en fin d'id de modèle, qui basculent vers client.beta.messages.create avec les headers beta (fast mode detection:70).

Motivation de conception

  • client lazy-loaded : à la construction, le handler ne crée pas le client SDK Anthropic ; il attend le premier createMessage pour le construire dans ensureClient (ensureClient:44). Ainsi, buildApiHandler peut instancier un handler en avance pour borner thinkingBudget sans déclencher de requête réelle.
  • traduction des chunks de flux : les événements natifs du flux Anthropic sont au nombre de six — message_start / message_delta / content_block_start / content_block_delta / content_block_stop / message_stop — ; Cline ne s'intéresse qu'à ceux qui se mappent en text / reasoning / tool_calls / usage (for await chunk:184), les autres (message_stop) sont simplement ignorés.
  • assemblage incrémental des paramètres d'appel d'outil : content_block_start récupère tool_use.id et name, les stocke dans lastStartedToolCall, puis les input_json_delta suivants yieldent un par un le partial_json (input_json_delta:276), la couche supérieure étant responsable de réassembler le JSON complet.
  • thinking et temperature mutuellement exclusifs : quand thinking est activé, temperature doit être mis à undefined (temperature thinking:123), selon une contrainte officielle d'Anthropic.
  • position du point de rupture du prompt cache : cache_control est posé à la fin du system plutôt qu'à la fin des tools, car tools ne change pas mais le contenu du system est volumineux — placer le point de rupture en fin de system maximise le taux de hit (system cache_control:124).
  • décorateur de retry : createMessage porte un @withRetry() ; seuls 429 et RetriableError sont retentés, par défaut trois essais avec backoff exponentiel (withRetry:63).
  • adaptive thinking : Claude Opus 4.5+ n'utilise plus l'extended thinking budgétisé, mais passe à type: "adaptive" avec un champ effort optionnel ; le handler détecte l'id de modèle et emprunte des branches différentes (adaptive thinking:100).

Fichiers clés

Flux de données

À l'appel, le handler décide d'abord entre la branche prompt-cache et la branche ordinaire, chacune construisant un requestBody différent, puis appelle client.messages.create ou client.beta.messages.create pour obtenir le flux :

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 */)
}

Le point clé de cette branche est que cache_control est placé sur le dernier élément du tableau system (cache_control:128). Anthropic met en cache tout le préfixe jusqu'à ce point de rupture ; toute requête ultérieure partageant ce préfixe pourra lire directement le cache, réduisant notablement le coût en tokens d'entrée du system prompt. tool_choice vaut {type:"any"} quand thinking est désactivé (tool_choice any:140), signifiant « si des outils sont présents, l'un doit être appelé » ; mais avec thinking activé, ce forçage déclenche une erreur d'incompatibilité côté Anthropic, donc on le laisse undefined et le modèle décide lui-même.

Une fois le flux obtenu, on entre dans la boucle for await qui traduit les chunks. Ci-dessous, le cœur de la traduction pour 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 ressort d'un coup avec les quatre champs usage input_tokens / output_tokens / cache_creation / cache_read (message_start:186) ; ensuite message_delta donne incrémentalement les output_tokens. La Task utilise ces champs pour calculer le coût et l'occupation du contexte. Les paramètres JSON d'un appel d'outil sont yieldés en fragments de chaîne partial_json, et c'est parseAssistantMessageV2 en amont qui les réassemble avant JSON.parse.

Limites et échecs

  • clé API absente : ensureClient lève directement "Anthropic API key is required" si options.apiKey est vide (apiKey required:46). L'erreur remonte jusqu'à la Task pour affichage.
  • échec de construction du SDK : si new Anthropic(...) lève, l'erreur est rewrappée en "Error creating Anthropic client: ..." puis propagée (client wrap:56).
  • id de modèle inconnu : getModel replie sur anthropicDefaultModelId lorsque modelId n'est pas dans la table anthropicModels (fallback:311), sans faire planter la couche supérieure.
  • 429 rate limit : le décorateur @withRetry() lit les headers retry-after / x-ratelimit-reset pour calculer le délai ; trois essais par défaut, et au troisième échec l'erreur originale est propagée (withRetry:29).
  • blocs redacted thinking : quand content_block_start reçoit redacted_thinking, le handler yield un reasoning placeholder et renvoie tel quel le data chiffré (redacted_thinking:219), car les requêtes suivantes attendent à Anthropic ce bloc renvoyé à l'identique.
  • saut de ligne entre plusieurs blocs text : content_block_start yield un \n avant tout deuxième bloc text ou ultérieur (text block newline:237), pour éviter que les blocs ne se touchent.
  • fast mode + 1m activés ensemble : si l'id de modèle porte à la fois :fast et :1m, le tableau de headers beta embarque fast-mode-2026-02-01 et context-1m-2025-08-07 simultanément (fastModeBetas:76).

Résumé

AnthropicHandler est l'implémentation de provider la plus « complète » de Cline : elle gère à la fois le point de rupture de prompt cache, l'extended thinking / adaptive thinking, les deux chemins beta fast mode et 1m context, ainsi que toutes les traductions des événements natifs du flux en ApiStreamChunk unifié. Une fois ce handler compris, OpenAI / Gemini / Bedrock et les autres handlers suivent tous la même recette — construire la requête, consommer le flux, traduire les chunks — seuls les champs du protocole changent. Pour la façon dont le system prompt consomme les champs comme cache_creation_input_tokens, voir la couche PromptBuilder.

  • Entrée de sélection des providers : /providers/api-handler
  • Comment le system prompt est assemblé : /prompts/prompt-builder
  • Comment les outils deviennent des Anthropic Tool : /prompts/toolset

Voir la documentation officielle : documentation Cline · README