Skip to content

Provider abstraction and 9 built-in registrations

源码版本v0.73.1

@mariozechner/pi-ai defines a provider with just three fields: api, stream, streamSimple. register-builtins.ts uses registerApiProvider to register implementations from 9 vendors into the table. Registration does not import the concrete provider module directly — instead it wraps a dynamic import in createLazyStream / createLazySimpleStream, so the SDK is only loaded on first call, keeping both module size and startup time unaffected.

Responsibilities

  1. Define the provider abstraction: the ApiProvider interface requires an api identifier plus stream / streamSimple functions, see packages/ai/src/api-registry.ts:23-27.
  2. Lazy-loading wrapper: createLazyStream / createLazySimpleStream return a synchronous stream function that import()s the concrete module internally before forwardStreaming, see packages/ai/src/providers/register-builtins.ts:159-178.
  3. Register 9 vendors: registerBuiltInApiProviders calls registerApiProvider for each vendor, see packages/ai/src/providers/register-builtins.ts:342-396.
  4. Load-and-register: a top-level registerBuiltInApiProviders() call at the end of the file triggers registration as soon as stream.ts imports it, see packages/ai/src/providers/register-builtins.ts:403-403.
  5. 9 Api types: the KnownApi union type enumerates 9 string literals, see packages/ai/src/types.ts:6-17.

Design motivation

Why lazy-load every provider? Because some SDKs are heavy — Bedrock pulls in the entire AWS SDK @aws-sdk/client-bedrock-runtime, and OpenAI Responses drags in a pile of dependencies. With a top-level static import, even a user who only uses Anthropic would have to load every SDK. createLazyStream turns the first call into a dynamic import, so startup only parses the few dozen lines of register-builtins.ts itself; whichever provider is actually used is the one that gets loaded.

Why is registerBuiltInApiProviders called at the top level at the end of the file, rather than exported for the caller to decide? Because stream.ts does import "./providers/register-builtins.js" at the top, a side-effect import — as soon as anyone imports stream from @mariozechner/pi-ai, registration happens. Callers get zero-config usage. resetApiProviders is provided for test scenarios to reset state.

Key files

The core of the lazy wrapper — the outer stream returns immediately, and a load failure is converted to an error event via push:

typescript
// packages/ai/src/providers/register-builtins.ts:159-178
function createLazyStream<TApi extends Api, TOptions extends StreamOptions, TSimpleOptions extends SimpleStreamOptions>(
	loadModule: () => Promise<LazyProviderModule<TApi, TOptions, TSimpleOptions>>,
): StreamFunction<TApi, TOptions> {
	return (model, context, options) => {
		const outer = new AssistantMessageEventStream();
		loadModule()
			.then((module) => {
				const inner = module.stream(model, context, options);
				forwardStream(outer, inner);
			})
			.catch((error) => {
				const message = createLazyLoadErrorMessage(model, error);
				outer.push({ type: "error", reason: "error", error: message });
				outer.end(message);
			});
		return outer;
	};
}

The 9 registrations are simply registerApiProvider calls, one api literal per vendor:

typescript
// packages/ai/src/providers/register-builtins.ts:343-347
	registerApiProvider({
		api: "anthropic-messages",
		stream: streamAnthropic,
		streamSimple: streamSimpleAnthropic,
	});

Data flow

Provider registration and first call are two phases:

Boundaries and failure modes

Summary

The provider abstraction is the api + stream + streamSimple triple, and all 9 built-ins go through createLazyStream for lazy loading. For the facade dispatch logic, see stream/complete facade; for the registry data structure, see ApiProvider registry; for how a specific vendor parses SSE, see Anthropic SSE implementation.