ApiProvider レジストリ: provider の統一接入点
api-registry.ts は provider レジストリ (registry) の中枢だ。一つの Map<string, RegisteredApiProvider> で、キーは api 文字列("anthropic-messages"、"openai-responses" など)、値は wrapStream / wrapStreamSimple で包んだ provider。すべての provider――組み込み 9 社も拡張が動的登録したものも――は同じレジストリを通る。stream.ts の resolveApiProvider がこの表を引く。
役割
- provider の格納: モジュール内定数
apiProviderRegistry = new Map<string, RegisteredApiProvider>()。api文字列をキーにする。packages/ai/src/api-registry.ts:35-40参照。 - アサーション被せ:
wrapStream/wrapStreamSimpleは本当の stream を呼ぶ前にmodel.api === apiをアサートし、不一致ならMismatched apiを投げる。packages/ai/src/api-registry.ts:42-64参照。 - 登録/登録解除:
registerApiProviderが表を書き、unregisterApiProviders(sourceId)が source 単位で一括削除、clearApiProvidersが全消去する。packages/ai/src/api-registry.ts:66-98参照。 - 表引き:
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 が真実を握っている。wrapStream は registerApiProvider の時点で provider の api をクロージャに取り込み、アサーションとして閉じ込める。呼び出し点では model.api !== api の一行チェックだけで、型エラーを実行時エラーに変え、スタックトレースが間違った呼び出しを直接指す。
なぜ sourceId を使い、単に delete しないのか? 拡張が登録した provider はプロセスをまたぎ、再起動を跨ぐ可能性がある。拡張には自分が登録した一括だけを消すことを許すべきだ。sourceId は unregisterApiProviders に安定したフィルタキーを与える。組み込み provider の登録には sourceId がつかないので、誤って消されることはない。
主要ファイル
packages/ai/src/api-registry.ts:23-27—ApiProviderインターフェース:api+stream+streamSimple。provider は三つとも必須。packages/ai/src/api-registry.ts:29-38—ApiProviderInternalとRegisteredApiProvider。内部用はsourceIdを持つ。packages/ai/src/api-registry.ts:40-40—apiProviderRegistryの Map インスタンス。packages/ai/src/api-registry.ts:42-52—wrapStream:model.api !== apiのアサーション。packages/ai/src/api-registry.ts:54-64—wrapStreamSimple。同じくアサーション。packages/ai/src/api-registry.ts:66-78—registerApiProvider: 表書き込み + wrap 適用。packages/ai/src/api-registry.ts:80-82—getApiProvider: 表引き。packages/ai/src/api-registry.ts:88-98—unregisterApiProviders/clearApiProviders。テストと拡張のリセット用。
wrapStream のアサーションは事実上 model.api !== api の一行:
// 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 を格納する:
// 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 ファサード を参照。