ApiProvider registry: the unified entry point for providers
api-registry.ts is the hub of the provider registry — a Map<string, RegisteredApiProvider>, keyed by the api string (e.g. "anthropic-messages", "openai-responses"), with values that are providers wrapped in wrapStream / wrapStreamSimple. Every provider — the 9 built-ins and any extension-registered ones — goes through the same registry. stream.ts's resolveApiProvider just looks up this table.
Responsibilities
- Store providers: a module-level constant
apiProviderRegistry = new Map<string, RegisteredApiProvider>(), keyed by theapistring. Seepackages/ai/src/api-registry.ts:35-40. - Wrap with an assertion:
wrapStream/wrapStreamSimpleassertmodel.api === apibefore invoking the real stream, throwingMismatched apiotherwise. Seepackages/ai/src/api-registry.ts:42-64. - Register / unregister:
registerApiProviderwrites to the table,unregisterApiProviders(sourceId)removes by source in bulk, andclearApiProviderswipes it. Seepackages/ai/src/api-registry.ts:66-98. - Lookup:
getApiProvider(api)returnsApiProviderInternal | undefined, called bystream.ts. Seepackages/ai/src/api-registry.ts:80-82.
Design motivation
Why wrap every provider with wrapStream? Because the type system cannot prevent "the caller holds a Model<"openai-responses"> but passes it to streamAnthropic" — TS's Model<TApi> is generic, and model.api at runtime is the source of truth. wrapStream closes over the provider's api at registerApiProvider time, so the check at the call site is a single model.api !== api, turning a type error into a runtime error whose stack points straight at the mismatched call.
Why use sourceId instead of a direct delete? Extension-registered providers may outlive a process or a restart, and an extension should only be allowed to clean up the batch it registered itself. sourceId gives unregisterApiProviders a stable filter key. Built-in providers are registered without a sourceId, so they cannot be removed by mistake.
Key files
packages/ai/src/api-registry.ts:23-27— theApiProviderinterface:api+stream+streamSimple; providers must supply all three.packages/ai/src/api-registry.ts:29-38—ApiProviderInternalandRegisteredApiProvider, carryingsourceIdinternally.packages/ai/src/api-registry.ts:40-40— theapiProviderRegistryMap instance.packages/ai/src/api-registry.ts:42-52—wrapStream: themodel.api !== apiassertion.packages/ai/src/api-registry.ts:54-64—wrapStreamSimple, with the same assertion.packages/ai/src/api-registry.ts:66-78—registerApiProvider: writes the table + applies the wrap.packages/ai/src/api-registry.ts:80-82—getApiProvider: table lookup.packages/ai/src/api-registry.ts:88-98—unregisterApiProviders/clearApiProviders, used by tests and extension resets.
The assertion inside wrapStream is a single 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);
};
}At registration both functions are wrapped, storing the full 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,
});Data flow
Provider registration and lookup are two separate phases:
Boundaries and failure modes
- api mismatch:
wrapStreamasserts before calling the real stream, seepackages/ai/src/api-registry.ts:47-49. Common when the caller holds the wrongModelinstance (e.g. passing an OpenAI model to an Anthropic provider). - Duplicate registration:
apiProviderRegistry.setoverwrites directly; the last registered wins. Extensions can override built-in providers. - Missing sourceId:
unregisterApiProviders(sourceId)will not touch built-in providers that have no sourceId, seepackages/ai/src/api-registry.ts:89-92. getApiProviders: returns an array of all providers, used by UI to list "available providers".- Test reset:
clearApiProvidersfollowed byregisterBuiltInApiProviders(), seepackages/ai/src/providers/register-builtins.ts:398-401.
Summary
api-registry.ts is a Map plus two wrap functions that turn type mismatches from a runtime mystery into an explicit error. The provider's api + stream + streamSimple triple is defined in Provider abstraction and built-ins; how a specific provider implements stream is covered in Anthropic SSE implementation. The dispatch logic of the streaming facade is in stream/complete facade.