Skip to content

buildApiHandler: picking a provider by model

源码版本v4.0.10

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: ApiHandler only exposes the createMessage streaming interface and getModel; inside each provider any SDK works, and the upper loop recursivelyMakeClineRequests only consumes ApiStream (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: createHandlerForProvider is 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.maxTokens to pin thinkingBudgetTokens to maxTokens - 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 apiProvider does 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

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:

typescript
// 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 switch reaches default and news up an AnthropicHandler directly (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 into createHandlerForProvider to 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: ...options stuffs 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.
  • apiProvider is undefined: falls into the default branch (default:504), same Anthropic fallback as an unknown provider.
  • Handler does not implement abort: in the interface abort? is optional (abort optional:64); Task must check whether this.api.abort exists 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

See official docs: Cline 文档 · README