Skip to content

Abstracción de provider y registro de 9 built-ins

源码版本v0.73.1

@mariozechner/pi-ai define al provider con sólo tres campos: api, stream, streamSimple. register-builtins.ts registra las implementaciones de 9 proveedores usando registerApiProvider. Al registrar, no se importa directamente el módulo del provider concreto, sino que se envuelve con createLazyStream / createLazySimpleStream para hacer un dynamic import: la primera invocación carga el SDK, así ni el tamaño del módulo ni el tiempo de arranque se ven afectados.

Responsabilidades

  1. Definir la abstracción Provider: la interfaz ApiProvider exige el identificador api + dos funciones stream / streamSimple. Ver packages/ai/src/api-registry.ts:23-27.
  2. Envoltorio lazy loading: createLazyStream / createLazySimpleStream devuelven una función de stream síncrona que internamente hace import() del módulo concreto y luego forwardStream. Ver packages/ai/src/providers/register-builtins.ts:159-178.
  3. Registrar 9 providers: registerBuiltInApiProviders invoca registerApiProvider para cada uno. Ver packages/ai/src/providers/register-builtins.ts:342-396.
  4. Cargar el módulo es registrar: al final del archivo se llama a registerBuiltInApiProviders() al nivel superior, de modo que basta con que stream.ts lo importe para dispararlo. Ver packages/ai/src/providers/register-builtins.ts:403-403.
  5. 9 tipos Api: el tipo unión KnownApi enumera 9 literales. Ver packages/ai/src/types.ts:6-17.

Motivación de diseño

¿Por qué todos los providers son lazy-loaded? Porque algunos SDK son pesados: Bedrock arrastra todo el @aws-sdk/client-bedrock-runtime de AWS, OpenAI Responses se trae un montón de dependencias. Si se importaran estáticamente al nivel superior, aunque el usuario sólo usara Anthropic, tendría que cargar todos los SDK. createLazyStream convierte la primera invocación en un dynamic import; al arrancar sólo se parsean las pocas líneas de register-builtins.ts, y cada provider se carga sólo cuando se usa.

¿Por qué registerBuiltInApiProviders se llama al final del archivo en lugar de exportarse para que el llamador decida? Porque stream.ts arriba hace import "./providers/register-builtins.js", un import de efectos secundarios: en cuanto alguien importa stream desde @mariozechner/pi-ai, el registro ocurre. El llamador no necesita configuración. resetApiProviders queda para resetear en tests.

Archivos clave

El núcleo del envoltorio lazy: el outer stream se devuelve enseguida; los errores de carga también se traducen en eventos de error vía 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;
	};
}

El registro de los 9 providers es un registerApiProvider por cada uno, cada uno con su literal api:

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

Flujo de datos

El registro y la primera invocación, en dos tramos:

Límites y fallos

Resumen

La abstracción de provider = el tríptico api + stream + streamSimple; los 9 built-ins pasan todos por createLazyStream para lazy loading. La lógica de despacho de la fachada en fachada stream/complete, la estructura de datos del registro en registro de provider, y el parseo SSE de un provider concreto en implementación SSE de Anthropic.