Skip to content

stream/complete: LLM 呼び出しのファサード入口

源码版本v0.73.1

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 なのかは見えない。

役割

この層がやることは四つ:

  1. ディスパッチ: streammodel.api でレジストリから provider を探し、(model, context, options) を渡す。packages/ai/src/stream.ts:25-32 参照。
  2. 同期的なストリーム返却: 下の provider が非同期初期化(遅延読み込み)かどうかにかかわらず、stream は常に同期的に AssistantMessageEventStream を返す。イベントは後から到着した時に push される。packages/ai/src/stream.ts:30-31 参照。
  3. 簡易版入口: streamSimple / completeSimpleSimpleStreamOptions(reasoningthinkingBudgets など UI がよく使うフィールドのみ公開)を受け取り、provider 側で simple options を完全な options に翻訳する。packages/ai/src/stream.ts:43-50 参照。
  4. provider 未登録時のエラー: resolveApiProvider は登録がない場合 No API provider registered for api: <api> を投げる。呼び出し点で黙って失敗することはない。packages/ai/src/stream.ts:17-23 参照。

設計動機

なぜここまで薄い層が必要なのか? 呼び出し側が気にする二つのこと――「どのモデルを呼ぶか」と「ストリーミングか最終結果か」――を provider の具体から切り離すためだ。stream.ts はこの二軸を四つの関数に収め、下流の pi-coding-agentAgentSession、CLI ツール、拡張すべてが同じ入口を使う。具体的な provider モールを直接 import する必要はない。これにより provider 実装は遅延読み込み可能で、差し替え可能で、拡張が新しい api を登録することもできる。呼び出し側のコードは一切変わらない。

complete は非ストリーミングのロジックを別途書かず、stream を再利用して s.result() を取る。ストリーミングのインフラと一発返しが一つのパイプラインを共有し、provider はストリーミング版だけを実装すればよい。

主要ファイル

stream の実装は resolve + 委譲だけで、業務ロジックは一切ない:

typescript
// 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);
}

completestream を再利用して result() を取るだけで、ディスパッチロジックを繰り返さない:

typescript
// 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 StreamOptionsProviderStreamOptions(拡張のカスタムフィールドを含む)を provider が期待する型へ強制キャストする。provider 内部は必要に応じてフィールドを読む。
  • options は省略可能: 三つの関数とも options? の缺省を許す。provider はデフォルト値でフォールバックする。

まとめ

stream.ts は 60 行のディスパッチファサード (facade) で、(model, context, options) をレジストリの provider へルーティングする。stream / complete はフルオプション版、streamSimple / completeSimple は UI 向け部分集合版。レジストリ本体の構造は Provider レジストリ、9 社の組み込み provider の登録は Provider 抽象と組み込み、最終的に返される AssistantMessageEventStreamEventStream 非同期イテレータ を参照。