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