buildApiHandler:按模型選提供者
職責
buildApiHandler 是 Cline 把「使用者在設定裡選了哪個提供者 (provider)」翻譯成「一個能跑的 ApiHandler 實例」的入口。Task 實例化時調它一次,拿回一個對外統一、內部卻各家的物件 (buildApiHandler call:585)。ApiHandler 這個介面定義得很窄,只有 createMessage 流式發請求、getModel 問模型資訊、可選的 getApiStreamUsage 和 abort 四個方法 (ApiHandler interface:60),所以上層 Task 不用關心背後是 Anthropic、OpenRouter、Bedrock 還是本地 Ollama。
它的另一個職責是按「模式 (mode)」選設定。Cline 把 plan / act 兩種模式做成兩套獨立的模型設定,buildApiHandler 收到 mode 後,從 planModeApiProvider 或 actModeApiProvider 裡挑一個,模型 id、reasoning effort、thinking budget 也都按模式各自取 (buildApiHandler:517);這樣一個 Task 可以在 plan 階段用便宜的模型,切到 act 階段再換成更強的模型。
設計動機
- 統一介面,各家實作:
ApiHandler只暴露createMessage流式介面和getModel,provider 內部用哪家 SDK 都行,上層迴圈recursivelyMakeClineRequests只消費ApiStream(ApiStream:1),不感知協議差異。 - plan/act 雙套設定:plan 模式偏資訊收集和規劃,act 模式才動檔案,兩套可以掛不同模型、不同 thinkingBudget,互不干擾 (
planModeApiProvider:520). - 大 switch 而不是註冊表:
createHandlerForProvider是一個 30+ 分支的 switch (createHandlerForProvider:83),看起來土,但每個分支構造對應 handler 時要傳的欄位差異巨大(Anthropic 要 baseUrl、Bedrock 要一堆 AWS 設定、Vertex 要 projectId),註冊表反而要寫一堆泛型適配,不如直接列出來清楚。 - thinkingBudget 限額兜底:進 switch 之前先建一次 handler,用拿到的
modelInfo.maxTokens把thinkingBudgetTokens卡到maxTokens - 1(thinkingBudget clip:525),避免使用者把 thinking budget 設超過模型上限導致 API 直接報錯。 - default 分支兜底 Anthropic:
apiProvider沒匹配上任何 case 時回落到 AnthropicHandler (default branch:504),保證舊設定或遷移中的 provider id 不會直接拋錯。
關鍵檔案
ApiHandler interface:60—createMessage / getModel / getApiStreamUsage / abort四個方法的契約。ApiHandlerModel:67— handler 對外暴露的模型資訊,只有 id 和 ModelInfo。ApiProviderInfo:72— 給系統提示層用的更全的包,帶 providerId、mode、customPrompt。createHandlerForProvider:83— provider id 到具體 handler 的 switch 分發。anthropic case:89—case "anthropic"分支,從 options 取 apiKey / baseUrl / thinkingBudgetTokens。default Anthropic fallback:504— 兜底分支,任何未識別 provider 都回落到 Anthropic。buildApiHandler:517— 對外的入口函式,處理 thinkingBudget 限額後再委託到createHandlerForProvider。ApiStream:1—AsyncGenerator<ApiStreamChunk>,所有 handler 的createMessage都吐這個統一 chunk 型別。ApiStreamChunk:3— 四種 chunk:text/reasoning/usage/tool_calls。LanguageModelChatSelector:4— VSCode LM API 用的 vendor/family/version 選擇器。
資料流
buildApiHandler 的核心邏輯是「按 mode 選 provider → 限額 thinkingBudget → 委託 switch 構造 handler」,程式碼很直白:
// 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)
}注意這裡有個「構造兩次」的小代價:為了拿到 modelInfo.maxTokens 做限額,它會先 new 一個 handler 出來(first construct:527),如果 thinkingBudget 沒超就直接復用回傳,超了就改 options 重新構造一次。AnthropicHandler 等實作裡 client 是懶載入的(ensureClient:44),構造本身不真正發請求,所以這個開銷可以忽略。
Task 在自己的建構函式裡就這樣調一次,把結果存到 this.api,後續所有 attemptApiRequest 都透過它拿流(buildApiHandler call:585)。mode 參數來自 TaskParams,Task 內部不切換 handler —— 切 mode 會重建 Task,不是在同一個 Task 裡換 handler。
邊界與失敗
- 未知 provider id:
switch走到default直接 new 一個AnthropicHandler(default:504),不拋錯,繼續往下跑直到 Anthropic API 報 401。 - handler 構造拋例外:首次構造只為拿
modelInfo,失敗被 try/catch 吞掉(catch error:541),隨後再走createHandlerForProvider重試,這回拋了就拋到 Task 層。 - thinkingBudget 等於 0:skip 整段限額邏輯,直接進
createHandlerForProvider(budget check:526),避免無謂構造兩次。 - plan / act 共用一份 options:
...options把兩套 planMode / actMode 欄位都塞給 handler,handler 內部再按 mode 選自己那部分(destructure:518),plan handler 拿不到 act 欄位不會出錯,只是多餘欄位在 handler options 裡。 apiProvider是 undefined:落到default分支(default:504),和未知 provider 一樣兜底 Anthropic。- handler 不實作
abort:介面裡abort?是可選的(abort optional:64),Task 取消時呼叫前要先判this.api.abort是否存在。
小結
buildApiHandler 本身很簡單:一個 switch 加一層 thinkingBudget 限額兜底,核心價值是把「使用者設定裡的 provider 字串」翻譯成「統一的 ApiHandler 物件」,讓上層 Task 不用關心各家 SDK 的差異。理解了這一層,接下來就可以鑽進具體 provider 看流式怎麼吐 chunk,或者看系統提示層怎麼把 mode 資訊塞進 prompt。
- 具體一個 provider 怎麼實作
createMessage:/providers/anthropic - 系統提示怎麼根據 providerInfo 選變體:
/prompts/prompt-builder - plan / act 雙模式怎麼影響 handler 選擇:
/prompts/mode