buildApiHandler: picking a provider by model
Responsibilities
buildApiHandler is Cline's entry point that translates "which provider the user picked in settings" into "a runnable ApiHandler instance". Task calls it once during instantiation and gets back an object with a uniform exterior but vendor-specific internals (buildApiHandler call:585). The ApiHandler interface is deliberately narrow — only createMessage for streaming requests, getModel for model info, and optional getApiStreamUsage and abort — four methods (ApiHandler interface:60), so the upper Task does not care whether the backend is Anthropic, OpenRouter, Bedrock, or local Ollama.
Its other responsibility is selecting configuration by "mode". Cline exposes plan / act modes as two independent model configurations; after receiving mode, buildApiHandler picks from planModeApiProvider or actModeApiProvider, and the model id, reasoning effort, and thinking budget are also taken per mode (buildApiHandler:517); this way a Task can use a cheaper model during the plan phase and switch to a stronger one for the act phase.
Design motivation
- Unified interface, vendor-specific implementations:
ApiHandleronly exposes thecreateMessagestreaming interface andgetModel; inside each provider any SDK works, and the upper looprecursivelyMakeClineRequestsonly consumesApiStream(ApiStream:1), unaware of protocol differences. - plan/act dual configuration: plan mode leans toward information gathering and planning, act mode actually mutates files; the two can hang different models and different thinkingBudgets without interfering (
planModeApiProvider:520). - A big switch rather than a registry:
createHandlerForProvideris a 30+ branch switch (createHandlerForProvider:83); it looks crude, but the fields each branch passes when constructing the corresponding handler differ wildly (Anthropic needs baseUrl, Bedrock needs a pile of AWS config, Vertex needs projectId), and a registry would force a bunch of generic adapters — listing them out directly is clearer. - thinkingBudget cap fallback: before entering the switch, it builds a handler once and uses the returned
modelInfo.maxTokensto pinthinkingBudgetTokenstomaxTokens - 1(thinkingBudget clip:525), preventing users from setting a thinking budget above the model's limit and triggering an outright API error. - default branch falls back to Anthropic: when
apiProviderdoes not match any case, it falls back to AnthropicHandler (default branch:504), so legacy configs or in-migration provider ids do not throw outright.
Key files
ApiHandler interface:60— the contract of four methods:createMessage / getModel / getApiStreamUsage / abort.ApiHandlerModel:67— model info the handler exposes; only id and ModelInfo.ApiProviderInfo:72— a fuller package for the system-prompt layer, with providerId, mode, customPrompt.createHandlerForProvider:83— the switch dispatching from provider id to concrete handler.anthropic case:89— thecase "anthropic"branch; takes apiKey / baseUrl / thinkingBudgetTokens from options.default Anthropic fallback:504— the fallback branch; any unrecognized provider falls back to Anthropic.buildApiHandler:517— the external entry function; clips thinkingBudget and then delegates tocreateHandlerForProvider.ApiStream:1—AsyncGenerator<ApiStreamChunk>; every handler'screateMessageyields this unified chunk type.ApiStreamChunk:3— four chunks:text/reasoning/usage/tool_calls.LanguageModelChatSelector:4— the vendor/family/version selector used by the VSCode LM API.
Data flow
The core logic of buildApiHandler is "pick provider by mode → clip thinkingBudget → delegate to the switch to construct the handler"; the code is straightforward:
// 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)
}Note the small cost of "constructing twice": to obtain modelInfo.maxTokens for clipping, it news up a handler once (first construct:527); if thinkingBudget does not exceed the limit it returns the handler directly, otherwise it mutates options and reconstructs. In implementations like AnthropicHandler the client is lazy-loaded (ensureClient:44), so construction itself does not actually fire a request and the overhead is negligible.
Task calls it once in its constructor and stores the result in this.api; all subsequent attemptApiRequest calls retrieve the stream through it (buildApiHandler call:585). The mode parameter comes from TaskParams; Task does not switch handlers internally — switching mode rebuilds the Task rather than swapping handlers within the same Task.
Boundaries and failure modes
- Unknown provider id: the
switchreachesdefaultand news up anAnthropicHandlerdirectly (default:504); no throw, it continues until the Anthropic API returns 401. - Handler construction throws: the first construction is only to obtain
modelInfo; failures are swallowed by try/catch (catch error:541), and execution continues intocreateHandlerForProviderto retry — if it throws this time, the error propagates to the Task layer. - thinkingBudget equals 0: skip the whole clipping logic and go straight into
createHandlerForProvider(budget check:526), avoiding a pointless double construction. - plan / act share one options:
...optionsstuffs both planMode and actMode fields into the handler; the handler internally picks its own part by mode (destructure:518). The plan handler not getting act fields is not an error — the extra fields just sit unused in the handler options. apiProvideris undefined: falls into thedefaultbranch (default:504), same Anthropic fallback as an unknown provider.- Handler does not implement
abort: in the interfaceabort?is optional (abort optional:64); Task must check whetherthis.api.abortexists before calling it during cancellation.
Summary
buildApiHandler itself is simple: a switch plus a thinkingBudget clipping fallback. Its core value is translating "the provider string in the user config" into "a unified ApiHandler object", so the upper Task does not need to care about vendor SDK differences. Once you understand this layer, you can dig into a specific provider to see how the stream emits chunks, or look at the system-prompt layer to see how mode info is stuffed into the prompt.
- How a specific provider implements
createMessage:/providers/anthropic - How the system prompt picks a variant based on providerInfo:
/prompts/prompt-builder - How plan / act dual mode affects handler selection:
/prompts/mode