AnthropicHandler : analyser en streaming l'Anthropic Messages API
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
createMessagepour le construire dansensureClient(ensureClient:44). Ainsi,buildApiHandlerpeut 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 entext / 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_startrécupèretool_use.idetname, les stocke danslastStartedToolCall, puis lesinput_json_deltasuivants yieldent un par un lepartial_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é,
temperaturedoit être mis àundefined(temperature thinking:123), selon une contrainte officielle d'Anthropic. - position du point de rupture du prompt cache :
cache_controlest 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 :
createMessageporte un@withRetry(); seuls 429 etRetriableErrorsont 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
AnthropicHandler class:36— implémenteApiHandler; ne détient queoptionset unclientlazy-loaded.ensureClient:44— ne construitnew Anthropic(...)qu'au premier appel, avec baseURL et headers personnalisés.createMessage with @withRetry:63— entrée principale du flux, générateurasync *.useFastMode detection:70— détecte:fasten fin d'id de modèle pour basculer vers l'API beta.budget_tokens:93— récupère le thinking budget depuis options ; 0 désactive thinking.thinkingConfig:107— modèles adaptive →{type:"adaptive"}, autres →{type:"enabled", budget_tokens}.supportsPromptCache branch:114— si cache présent, on posecache_controlsur le system ; sinon branche else.stream loop:183—for await chunk of streamtraduit les événements natifs en chunks unifiés.tool_use block start:227— récupère tool_use id/name et les stocke danslastStartedToolCall.getModel:305— lit ModelInfo depuis la tableanthropicModels, à défaut repli suranthropicDefaultModelId.
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 :
// 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 :
// 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 :
ensureClientlève directement"Anthropic API key is required"sioptions.apiKeyest 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 :
getModelreplie suranthropicDefaultModelIdlorsquemodelIdn'est pas dans la tableanthropicModels(fallback:311), sans faire planter la couche supérieure. - 429 rate limit : le décorateur
@withRetry()lit les headersretry-after / x-ratelimit-resetpour 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_startreçoitredacted_thinking, le handler yield un reasoning placeholder et renvoie tel quel ledatachiffré (redacted_thinking:219), car les requêtes suivantes attendent à Anthropic ce bloc renvoyé à l'identique. - saut de ligne entre plusieurs blocs text :
content_block_startyield un\navant 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
:fastet:1m, le tableau de headers beta embarquefast-mode-2026-02-01etcontext-1m-2025-08-07simultané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