Skip to content

ApiProvider registry: the unified entry point for providers

源码版本v0.73.1

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

  1. Store providers: a module-level constant apiProviderRegistry = new Map<string, RegisteredApiProvider>(), keyed by the api string. See packages/ai/src/api-registry.ts:35-40.
  2. Wrap with an assertion: wrapStream / wrapStreamSimple assert model.api === api before invoking the real stream, throwing Mismatched api otherwise. See packages/ai/src/api-registry.ts:42-64.
  3. Register / unregister: registerApiProvider writes to the table, unregisterApiProviders(sourceId) removes by source in bulk, and clearApiProviders wipes it. See packages/ai/src/api-registry.ts:66-98.
  4. Lookup: getApiProvider(api) returns ApiProviderInternal | undefined, called by stream.ts. See packages/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

The assertion inside wrapStream is a single 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);
	};
}

At registration both functions are wrapped, storing the full 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,
});

Data flow

Provider registration and lookup are two separate phases:

Boundaries and failure modes

  • api mismatch: wrapStream asserts before calling the real stream, see packages/ai/src/api-registry.ts:47-49. Common when the caller holds the wrong Model instance (e.g. passing an OpenAI model to an Anthropic provider).
  • Duplicate registration: apiProviderRegistry.set overwrites 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, see packages/ai/src/api-registry.ts:89-92.
  • getApiProviders: returns an array of all providers, used by UI to list "available providers".
  • Test reset: clearApiProviders followed by registerBuiltInApiProviders(), see packages/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.