buildApiHandler: Provider-Auswahl anhand des Modells
Verantwortung
buildApiHandler ist der Einstieg, mit dem Cline „den vom Benutzer in den Einstellungen gewählten Provider" in „eine lauffähige ApiHandler-Instanz" übersetzt. Task ruft die Funktion bei der Instanziierung einmal auf und erhält ein Objekt, das nach außen einheitlich, intern aber pro Provider ist (buildApiHandler call:585). Die Schnittstelle ApiHandler ist bewusst schmal: Nur createMessage zum streamenden Senden von Anfragen, getModel zum Abfragen von Modellinformationen sowie die optionalen getApiStreamUsage und abort – vier Methoden insgesamt (ApiHandler interface:60). Die obere Task-Schicht muss daher nicht wissen, ob dahinter Anthropic, OpenRouter, Bedrock oder ein lokales Ollama steht.
Eine zweite Aufgabe besteht darin, die Konfiguration anhand des Modus (mode) zu wählen. Cline hält plan und act als zwei unabhängige Modellkonfigurationen vor; buildApiHandler nimmt den mode entgegen und wählt aus planModeApiProvider oder actModeApiProvider aus. Auch Modell-ID, reasoning effort und thinking budget werden pro Modus bezogen (buildApiHandler:517). So kann ein Task in der plan-Phase ein günstiges Modell verwenden und beim Wechsel in die act-Phase auf ein stärkeres wechseln.
Entwurfsmotivation
- Einheitliche Schnittstelle, einzelne Implementierungen:
ApiHandlerexposes nur die streamendecreateMessage-Schnittstelle undgetModel. Intern darf jeder Provider eine beliebige SDK verwenden; die obere SchleiferecursivelyMakeClineRequestskonsumiert ausschließlichApiStream(ApiStream:1) und bekommt von Protokollunterschieden nichts mit. - plan/act als zwei Konfigurationen: plan sammelt Informationen und plant, act ändert Dateien. Beide Modi können unterschiedliche Modelle und thinkingBudget-Werte tragen, ohne einander zu beeinflussen (
planModeApiProvider:520). - Großer switch statt Registry:
createHandlerForProviderist ein switch mit über 30 Zweigen (createHandlerForProvider:83). Das wirkt altbacken, doch die zu übergebenden Felder pro Zweig unterscheiden sich massiv (Anthropic benötigt baseUrl, Bedrock viele AWS-Felder, Vertex projectId) – eine Registry würde viele generische Adapter erfordern, die direkte Auflistung ist klarer. - Grenze für thinkingBudget: Vor dem switch wird einmalig ein Handler konstruiert, um an
modelInfo.maxTokensthinkingBudgetTokensaufmaxTokens - 1zu begrenzen (thinkingBudget clip:525). So wird vermieden, dass ein zu hoch gesetzter thinking budget einen API-Fehler auslöst. - default-Fallback auf Anthropic: Trifft
apiProviderauf keinen case, fällt der Code auf AnthropicHandler zurück (default branch:504), sodass alte Konfigurationen oder provider-IDs während einer Migration keinen Fehler werfen.
Schlüsseldateien
ApiHandler interface:60— Vertrag über die vier MethodencreateMessage / getModel / getApiStreamUsage / abort.ApiHandlerModel:67— Modellinformationen, die der Handler nach außen gibt; nur id und ModelInfo.ApiProviderInfo:72— Vollständigeres Paket für die System-Prompt-Schicht, mit providerId, mode und customPrompt.createHandlerForProvider:83— switch, der die provider-id auf den konkreten Handler abbildet.anthropic case:89—case "anthropic", zieht apiKey / baseUrl / thinkingBudgetTokens aus options.default Anthropic fallback:504— Fallback-Zweig; jeder nicht erkannte Provider fällt auf Anthropic zurück.buildApiHandler:517— Öffentliche Einstiegsfunktion; nach der thinkingBudget-Begrenzung Delegation ancreateHandlerForProvider.ApiStream:1—AsyncGenerator<ApiStreamChunk>; allecreateMessage-Implementierungen geben diesen einheitlichen Chunk-Typ zurück.ApiStreamChunk:3— Vier Chunk-Varianten:text/reasoning/usage/tool_calls.LanguageModelChatSelector:4— Selector (vendor/family/version) für die VSCode LM API.
Datenfluss
Die Kernlogik von buildApiHandler lautet „mode wählen → thinkingBudget begrenzen → switch konstruieren". Der Code ist direkt:
// 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)
}Achtung: Es gibt eine kleine Kostenstelle der „doppelten Konstruktion". Um an modelInfo.maxTokens zu gelangen, wird ein Handler vorab erzeugt (first construct:527). Liegt thinkingBudget innerhalb der Grenze, wird dieser Handler direkt zurückgegeben, andernfalls werden options angepasst und neu konstruiert. Da in Implementierungen wie AnthropicHandler der Client lazy geladen wird (ensureClient:44), löst die Konstruktion selbst keine Anfrage aus – der Aufwand ist vernachlässigbar.
Task ruft die Funktion in seinem Konstruktor einmal auf und speichert das Ergebnis in this.api. Alle späteren attemptApiRequest-Aufrufe holen sich über ihn den Stream (buildApiHandler call:585). Der Parameter mode stammt aus TaskParams; Task selbst tauscht den Handler nicht aus – ein Moduswechsel bedeutet, dass Task neu aufgebaut wird, kein Handler-Tausch innerhalb desselben Task.
Grenzen und Fehler
- Unbekannte Provider-ID: Der
switchlandet imdefault-Zweig und instanziiert direkt einenAnthropicHandler(default:504); es wird kein Fehler geworfen, die Ausführung läuft weiter, bis Anthropic irgendwann mit 401 antwortet. - Fehler bei Handler-Konstruktion: Die erste Konstruktion dient nur dem Abruf von
modelInfo; schlägt sie fehl, wird sie vom try/catch geschluckt (catch error:541). Der anschließendecreateHandlerForProvider-Aufruf versucht es erneut – wirft dieser, bläst der Fehler bis zur Task-Schicht durch. - thinkingBudget gleich 0: Die gesamte Begrenzungslogik wird übersprungen und direkt in
createHandlerForProvidergegangen (budget check:526), um die doppelte Konstruktion zu vermeiden. - plan / act teilen sich options:
...optionsübergibt sowohl planMode- als auch actMode-Felder an den Handler, der intern seinen Teil auswählt (destructure:518). Ein plan-Handler erhält zwar die act-Felder nicht, was aber zu keinen Fehlern führt – lediglich redundante Felder liegen in den handler options. apiProviderist undefined: Fällt in dendefault-Zweig (default:504) und wird wie ein unbekannter Provider auf Anthropic zurückgeführt.- Handler implementiert
abortnicht: In der Schnittstelle istabort?optional (abort optional:64); Task muss vor dem Abbruch prüfen, obthis.api.abortüberhaupt existiert.
Zusammenfassung
buildApiHandler selbst ist schlicht: ein switch plus eine Schicht thinkingBudget-Begrenzung. Der Kernwert liegt darin, die „Provider-Zeichenkette aus der Benutzerkonfiguration" in ein „einheitliches ApiHandler-Objekt" zu übersetzen, sodass die obere Task-Schicht die Unterschiede der einzelnen SDKs ignorieren kann. Wer diese Schicht verstanden hat, kann in einen konkreten Provider einsteigen und sehen, wie der Stream Chunks liefert, oder in der System-Prompt-Schicht nachvollziehen, wie die mode-Information in den Prompt einfließt.
- Wie ein konkreter Provider
createMessageumsetzt:/providers/anthropic - Wie der System-Prompt anhand von providerInfo Varianten wählt:
/prompts/prompt-builder - Wie plan / act als zwei Modi die Handler-Wahl beeinflussen:
/prompts/mode
Siehe offizielle Dokumentation: Cline-Dokumentation · README