ApiProvider 註冊表:provider 的統一接入點
api-registry.ts 是 provider 註冊表的中樞——一個 Map<string, RegisteredApiProvider>,key 是 api 字串(如 "anthropic-messages"、"openai-responses"),value 是包了 wrapStream / wrapStreamSimple 的 provider。所有 provider——內建 9 家、擴充動態註冊的——都從同一個註冊表裡走。stream.ts 的 resolveApiProvider 就是查這張表。
職責
- 存 provider:模組內常數
apiProviderRegistry = new Map<string, RegisteredApiProvider>(),以api字串為 key。見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—apiProviderRegistryMap 實例。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 兩個函式,store 完整的 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」時用。- 測試 reset:
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 門面。