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 门面。