Provider abstraction and 9 built-in registrations
@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
- Define the provider abstraction: the
ApiProviderinterface requires anapiidentifier plusstream/streamSimplefunctions, seepackages/ai/src/api-registry.ts:23-27. - Lazy-loading wrapper:
createLazyStream/createLazySimpleStreamreturn a synchronous stream function thatimport()s the concrete module internally beforeforwardStreaming, seepackages/ai/src/providers/register-builtins.ts:159-178. - Register 9 vendors:
registerBuiltInApiProviderscallsregisterApiProviderfor each vendor, seepackages/ai/src/providers/register-builtins.ts:342-396. - Load-and-register: a top-level
registerBuiltInApiProviders()call at the end of the file triggers registration as soon asstream.tsimports it, seepackages/ai/src/providers/register-builtins.ts:403-403. - 9 Api types: the
KnownApiunion type enumerates 9 string literals, seepackages/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
packages/ai/src/types.ts:6-17— the 9KnownApiliterals +Apiallowing extension strings.packages/ai/src/api-registry.ts:23-27— theApiProviderinterface, the triple.packages/ai/src/providers/register-builtins.ts:159-178—createLazyStream: outer stream + dynamic load + forwardStream + error push.packages/ai/src/providers/register-builtins.ts:180-220—createLazySimpleStream, the simple-version equivalent of the same pattern.packages/ai/src/providers/register-builtins.ts:323-340— 9 lazy functions + Bedrock's private lazy.packages/ai/src/providers/register-builtins.ts:342-396— the body ofregisterBuiltInApiProviders.packages/ai/src/providers/register-builtins.ts:398-403—resetApiProvidersand the top-level registration.packages/ai/src/types.ts:155-159— theStreamFunctionsignature, returningAssistantMessageEventStream.
The core of the lazy wrapper — the outer stream returns immediately, and a load failure is converted to an error event via push:
// 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:
// 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
- Lazy load failure:
loadModule().catchwraps the error as an{ type: "error" }event pushed into the outer stream, so the caller'sfor awaitreceives it, seepackages/ai/src/providers/register-builtins.ts:170-174. - List of 9:
anthropic-messages,openai-completions,mistral-conversations,openai-responses,azure-openai-responses,openai-codex-responses,google-generative-ai,google-vertex,bedrock-converse-stream, seepackages/ai/src/providers/register-builtins.ts:343-395. - Bedrock handled separately: due to the weight of the AWS SDK,
streamBedrockLazy/streamSimpleBedrockLazyare not exported and are internal only, seepackages/ai/src/providers/register-builtins.ts:339-340. - Extension registration: third-party extensions can call
registerApiProviderto register their ownapistring; theApitype falls back tostring & {}, seepackages/ai/src/types.ts:17-17.
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.