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 を取得して上限クリップを行うため、先に handler を一つ new する(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