stream/complete: LLM 呼び出しのファサード入口
stream.ts は @mariozechner/pi-ai の外部に対する最も薄い一層だ。4 つの関数 stream / complete / streamSimple / completeSimple に、内部用の resolveApiProvider が一つ。SSE の解析もしない、リクエストボディも構築しない、リトライも扱わない。やることは (model, context, options) の三つ組を model.api に従って対応する provider へルーティングすることだけだ。「呼び出し側と provider 実装の間にあるディスパッチ局」と考えてよい。呼び出し側が見るのは統一された戻り値 AssistantMessageEventStream だけで、下が Anthropic の SSE なのか OpenAI Responses なのか Bedrock Converse なのかは見えない。
役割
この層がやることは四つ:
- ディスパッチ:
streamはmodel.apiでレジストリから provider を探し、(model, context, options)を渡す。packages/ai/src/stream.ts:25-32参照。 - 同期的なストリーム返却: 下の provider が非同期初期化(遅延読み込み)かどうかにかかわらず、
streamは常に同期的にAssistantMessageEventStreamを返す。イベントは後から到着した時に push される。packages/ai/src/stream.ts:30-31参照。 - 簡易版入口:
streamSimple/completeSimpleはSimpleStreamOptions(reasoning、thinkingBudgetsなど UI がよく使うフィールドのみ公開)を受け取り、provider 側で simple options を完全な options に翻訳する。packages/ai/src/stream.ts:43-50参照。 - provider 未登録時のエラー:
resolveApiProviderは登録がない場合No API provider registered for api: <api>を投げる。呼び出し点で黙って失敗することはない。packages/ai/src/stream.ts:17-23参照。
設計動機
なぜここまで薄い層が必要なのか? 呼び出し側が気にする二つのこと――「どのモデルを呼ぶか」と「ストリーミングか最終結果か」――を provider の具体から切り離すためだ。stream.ts はこの二軸を四つの関数に収め、下流の pi-coding-agent の AgentSession、CLI ツール、拡張すべてが同じ入口を使う。具体的な provider モールを直接 import する必要はない。これにより provider 実装は遅延読み込み可能で、差し替え可能で、拡張が新しい api を登録することもできる。呼び出し側のコードは一切変わらない。
complete は非ストリーミングのロジックを別途書かず、stream を再利用して s.result() を取る。ストリーミングのインフラと一発返しが一つのパイプラインを共有し、provider はストリーミング版だけを実装すればよい。
主要ファイル
packages/ai/src/stream.ts:1-3— 先頭のimport "./providers/register-builtins.js"が 9 社の組み込み provider の自己登録をトリガーする。getApiProviderがレジストリから取り出す。packages/ai/src/stream.ts:17-23—resolveApiProvider。provider がない時にエラーを投げる。packages/ai/src/stream.ts:25-32—streamのフルオプション版。packages/ai/src/stream.ts:34-41—completeはstream(...).result()に等しい。packages/ai/src/stream.ts:43-50—streamSimpleはprovider.streamSimpleに委譲。packages/ai/src/stream.ts:52-59—completeSimpleはstreamSimple(...).result()に等しい。
stream の実装は resolve + 委譲だけで、業務ロジックは一切ない:
// packages/ai/src/stream.ts:25-32
export function stream<TApi extends Api>(
model: Model<TApi>,
context: Context,
options?: ProviderStreamOptions,
): AssistantMessageEventStream {
const provider = resolveApiProvider(model.api);
return provider.stream(model, context, options as StreamOptions);
}complete は stream を再利用して result() を取るだけで、ディスパッチロジックを繰り返さない:
// packages/ai/src/stream.ts:34-41
export async function complete<TApi extends Api>(
model: Model<TApi>,
context: Context,
options?: ProviderStreamOptions,
): Promise<AssistantMessage> {
const s = stream(model, context, options);
return s.result();
}データフロー
stream(model, ctx, opts) のディスパッチ連鎖:
境界と失敗
- provider 未登録:
resolveApiProviderは即座にエラーを投げ、空ストリームを返さない。packages/ai/src/stream.ts:19-21参照。拡張が動的登録した provider が未登録の場合、呼び出し側はすぐ分かる。 - 遅延読み込み provider: 具体的な provider モジュールは
createLazyStreamで包まれている。streamは外側のストリームを同期的に返し、モジュールの読み込み完了後にforwardStreamで内側へ繋ぐ。packages/ai/src/providers/register-builtins.ts:159-178参照。stream自身は遅延読み込みを意識しない。 - 型アサーション:
options as StreamOptionsがProviderStreamOptions(拡張のカスタムフィールドを含む)を provider が期待する型へ強制キャストする。provider 内部は必要に応じてフィールドを読む。 optionsは省略可能: 三つの関数ともoptions?の缺省を許す。provider はデフォルト値でフォールバックする。
まとめ
stream.ts は 60 行のディスパッチファサード (facade) で、(model, context, options) をレジストリの provider へルーティングする。stream / complete はフルオプション版、streamSimple / completeSimple は UI 向け部分集合版。レジストリ本体の構造は Provider レジストリ、9 社の組み込み provider の登録は Provider 抽象と組み込み、最終的に返される AssistantMessageEventStream は EventStream 非同期イテレータ を参照。