Skip to content

ApiProvider レジストリ: provider の統一接入点

源码版本v0.73.1

api-registry.ts は provider レジストリ (registry) の中枢だ。一つの Map<string, RegisteredApiProvider> で、キーは api 文字列("anthropic-messages""openai-responses" など)、値は wrapStream / wrapStreamSimple で包んだ provider。すべての provider――組み込み 9 社も拡張が動的登録したものも――は同じレジストリを通る。stream.tsresolveApiProvider がこの表を引く。

役割

  1. provider の格納: モジュール内定数 apiProviderRegistry = new Map<string, RegisteredApiProvider>()api 文字列をキーにする。packages/ai/src/api-registry.ts:35-40 参照。
  2. アサーション被せ: wrapStream / wrapStreamSimple は本当の stream を呼ぶ前に model.api === api をアサートし、不一致なら Mismatched api を投げる。packages/ai/src/api-registry.ts:42-64 参照。
  3. 登録/登録解除: registerApiProvider が表を書き、unregisterApiProviders(sourceId) が source 単位で一括削除、clearApiProviders が全消去する。packages/ai/src/api-registry.ts:66-98 参照。
  4. 表引き: getApiProvider(api)ApiProviderInternal | undefined を返し、stream.ts が呼ぶ。packages/ai/src/api-registry.ts:80-82 参照。

設計動機

なぜ provider の外に wrapStream を被せるのか? 型システムでは「呼び出し側が Model<"openai-responses"> を持っていながら streamAnthropic に渡す」といった間違いを防げないからだ。TS の Model<TApi> はジェネリックで、実行時の model.api が真実を握っている。wrapStreamregisterApiProvider の時点で provider の api をクロージャに取り込み、アサーションとして閉じ込める。呼び出し点では model.api !== api の一行チェックだけで、型エラーを実行時エラーに変え、スタックトレースが間違った呼び出しを直接指す。

なぜ sourceId を使い、単に delete しないのか? 拡張が登録した provider はプロセスをまたぎ、再起動を跨ぐ可能性がある。拡張には自分が登録した一括だけを消すことを許すべきだ。sourceIdunregisterApiProviders に安定したフィルタキーを与える。組み込み provider の登録には sourceId がつかないので、誤って消されることはない。

主要ファイル

wrapStream のアサーションは事実上 model.api !== api の一行:

typescript
// packages/ai/src/api-registry.ts:42-52
function wrapStream<TApi extends Api, TOptions extends StreamOptions>(
	api: TApi,
	stream: StreamFunction<TApi, TOptions>,
): ApiStreamFunction {
	return (model, context, options) => {
		if (model.api !== api) {
			throw new Error(`Mismatched api: ${model.api} expected ${api}`);
		}
		return stream(model as Model<TApi>, context, options as TOptions);
	};
}

登録時に二つの関数を同時に wrap し、完全な internal provider を格納する:

typescript
// packages/ai/src/api-registry.ts:70-77
apiProviderRegistry.set(provider.api, {
	provider: {
		api: provider.api,
		stream: wrapStream(provider.api, provider.stream),
		streamSimple: wrapStreamSimple(provider.api, provider.streamSimple),
	},
	sourceId,
});

データフロー

provider の登録と表引きの二段:

境界と失敗

  • api 不一致: wrapStream は本当の stream を呼ぶ前にアサートする。packages/ai/src/api-registry.ts:47-49 参照。呼び出し側が間違った Model インスタンス(OpenAI のモデルを Anthropic provider に渡すなど)を持つ時に多い。
  • 重複登録: apiProviderRegistry.set はそのまま上書きし、後から登録した方が勝つ。拡張は組み込み provider を上書きできる。
  • sourceId 欠如: unregisterApiProviders(sourceId) は sourceId のない組み込み provider を触らない。packages/ai/src/api-registry.ts:89-92 参照。
  • getApiProviders: 全 provider の配列を返す。UI で「利用可能 provider」を列挙する時に使う。
  • テストのリセット: clearApiProviders の後に registerBuiltInApiProviders() を呼ぶ。packages/ai/src/providers/register-builtins.ts:398-401 参照。

まとめ

api-registry.ts は一つの Map + 二つの wrap 関数で、型不一致という実行時の謎を明確なエラーに変える。Provider の api + stream + streamSimple 三点セットの定義は Provider 抽象と組み込み、ある一家が具体的に stream をどう実装するかは Anthropic SSE 実装 を参照。ファサードのディスパッチロジックは stream/complete ファサード を参照。