buildApiHandler : choisir un provider selon le modèle
Responsabilités
buildApiHandler est le point d'entrée par lequel Cline traduit « le provider choisi par l'utilisateur dans les réglages » en « une instance d'ApiHandler prête à l'emploi ». La Task l'appelle une fois à l'instanciation et récupère un objet au interface unifiée mais à l'implémentation interne variable selon le provider (buildApiHandler call:585). L'interface ApiHandler est volontairement étroite : createMessage pour lancer une requête en streaming, getModel pour interroger les infos du modèle, plus les deux méthodes optionnelles getApiStreamUsage et abort — seulement quatre méthodes (ApiHandler interface:60). La Task au-dessus n'a donc pas à se soucier de savoir si derrière se trouve Anthropic, OpenRouter, Bedrock ou un Ollama local.
Son autre responsabilité est de sélectionner la configuration selon le mode. Cline modélise les deux modes plan / act (modes) comme deux configurations de modèle distinctes ; buildApiHandler reçoit le mode, puis choisit entre planModeApiProvider et actModeApiProvider, l'id de modèle, le reasoning effort et le thinking budget étant également sélectionnés par mode (buildApiHandler:517). Ainsi, une Task peut utiliser un modèle économique pendant la phase plan, puis basculer sur un modèle plus puissant lors du passage en act.
Motivation de conception
- Interface unifiée, implémentations distinctes :
ApiHandlern'expose que l'interface de streamingcreateMessageetgetModel. À l'intérieur du provider, libre d'utiliser le SDK que l'on veut ; la boucle supérieurerecursivelyMakeClineRequestsne consomme queApiStream(ApiStream:1), indépendamment des différences de protocole. - Deux configurations plan/act : le mode plan privilégie la collecte d'informations et la planification, le mode act agit sur les fichiers ; chacun peut embarquer modèles et thinkingBudget distincts, sans interférence (
planModeApiProvider:520). - Un grand switch plutôt qu'un registre :
createHandlerForProviderest un switch de plus de 30 branches (createHandlerForProvider:83). Cela peut sembler rustique, mais comme les champs à passer à chaque handler diffèrent fortement (Anthropic attend baseUrl, Bedrock toute une configuration AWS, Vertex un projectId), un registre générique imposerait d'écrire beaucoup d'adaptateurs — autant tout lister explicitement. - Plafond de thinkingBudget en filet de sécurité : avant d'entrer dans le switch, on instancie une fois un handler pour récupérer
modelInfo.maxTokenset bornerthinkingBudgetTokensàmaxTokens - 1(thinkingBudget clip:525), évitant qu'un thinking budget trop élevé fasse rejeter la requête par l'API. - Branche default repli sur Anthropic : si
apiProviderne correspond à aucun case, on retombe sur AnthropicHandler (default branch:504), garantissant qu'un id de provider ancien ou en cours de migration ne déclenche pas une erreur immédiate.
Fichiers clés
ApiHandler interface:60— contrat des quatre méthodescreateMessage / getModel / getApiStreamUsage / abort.ApiHandlerModel:67— infos modèle exposées par le handler : seulement id et ModelInfo.ApiProviderInfo:72— paquet plus complet pour la couche de system prompt, avec providerId, mode et customPrompt.createHandlerForProvider:83— switch de dispatch entre id de provider et handler concret.anthropic case:89— branchecase "anthropic", récupère apiKey / baseUrl / thinkingBudgetTokens depuis options.default Anthropic fallback:504— branche de repli, tout provider non reconnu retombe sur Anthropic.buildApiHandler:517— fonction d'entrée publique, applique le plafond thinkingBudget puis délègue àcreateHandlerForProvider.ApiStream:1—AsyncGenerator<ApiStreamChunk>, type de chunk unifié produit par tous les handlers viacreateMessage.ApiStreamChunk:3— quatre variétés de chunk :text/reasoning/usage/tool_calls.LanguageModelChatSelector:4— sélecteur vendor/family/version pour le VSCode LM API.
Flux de données
La logique centrale de buildApiHandler est « choisir le provider par mode → borner thinkingBudget → déléguer au switch pour construire le handler ». Le code est direct :
// apps/vscode/src/core/api/index.ts
export function buildApiHandler(configuration: ApiConfiguration, mode: Mode): ApiHandler {
const { planModeApiProvider, actModeApiProvider, ...options } = configuration
const apiProvider = mode === "plan" ? planModeApiProvider : actModeApiProvider
// Validate thinking budget tokens against model's maxTokens to prevent API errors
try {
const thinkingBudgetTokens = mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens
if (thinkingBudgetTokens && thinkingBudgetTokens > 0) {
const handler = createHandlerForProvider(apiProvider, options, mode)
const modelInfo = handler.getModel().info
if (modelInfo?.maxTokens && modelInfo.maxTokens > 0 && thinkingBudgetTokens > modelInfo.maxTokens) {
const clippedValue = modelInfo.maxTokens - 1
if (mode === "plan") {
options.planModeThinkingBudgetTokens = clippedValue
} else {
options.actModeThinkingBudgetTokens = clippedValue
}
} else {
return handler // don't rebuild unless its necessary
}
}
} catch (error) {
Logger.error("buildApiHandler error:", error)
}
return createHandlerForProvider(apiProvider, options, mode)
}À noter, ce « coût de construction double » : pour obtenir modelInfo.maxTokens et borner la valeur, on instancie d'abord un handler (first construct:527). Si thinkingBudget n'excède pas le plafond, on le réutilise tel quel ; sinon, on modifie options et on reconstruit. Comme AnthropicHandler et consorts initialisent leur client en lazy (ensureClient:44), la construction ne déclenche pas de requête réelle — ce surcoût est donc négligeable.
La Task appelle cette fonction une fois dans son constructeur et stocke le résultat dans this.api ; tous les attemptApiRequest ultérieurs récupèrent le flux via lui (buildApiHandler call:585). Le paramètre mode provient de TaskParams ; la Task ne change pas de handler en interne — un changement de mode reconstruit la Task plutôt que de changer de handler au sein d'une même Task.
Limites et échecs
- id de provider inconnu : le
switchtombe dansdefaultet instancie unAnthropicHandler(default:504), sans lever d'erreur — le code continue jusqu'à ce que l'API Anthropic renvoie un 401. - handler qui lève à la construction : la première construction ne sert qu'à obtenir
modelInfo; en cas d'échec, le try/catch l'avale (catch error:541), puis on rappellecreateHandlerForProvider. Cette seconde fois, si une erreur est levée, elle remonte jusqu'à la Task. - thinkingBudget égal à 0 : on skippe toute la logique de plafonnement et on entre directement dans
createHandlerForProvider(budget check:526), évitant une construction double inutile. - plan / act partagent un même options :
...optionstransmet au handler les deux jeux de champs planMode / actMode ; le handler choisit ensuite en interne la part qui le concerne (destructure:518). Le handler de plan ne dispose pas des champs act — pas d'erreur, simplement des champs superflus dans ses options. apiProviderundefined : tombe dans la branchedefault(default:504), comme un provider inconnu, et replie sur Anthropic.- handler n'implémentant pas
abort: dans l'interface,abort?est optionnel (abort optional:64). À l'annulation, la Task doit vérifier l'existence dethis.api.abortavant de l'appeler.
Résumé
buildApiHandler reste simple : un switch plus une couche de plafonnement thinkingBudget. Sa valeur essentielle est de traduire « la chaîne de provider issue de la configuration utilisateur » en « un objet ApiHandler unifié », rendant la Task supérieure indépendante des différences entre SDK. Une fois cette couche comprise, on peut plonger dans un provider concret pour voir comment le streaming yield des chunks, ou examiner comment la couche de system prompt injecte les informations de mode dans le prompt.
- Comment un provider concret implémente
createMessage:/providers/anthropic - Comment le system prompt choisit une variante selon providerInfo :
/prompts/prompt-builder - Comment le double mode plan / act influe sur le choix du handler :
/prompts/mode
Voir la documentation officielle : documentation Cline · README