stream/complete: the facade entry point for LLM calls
stream.ts is the thinnest layer that @mariozechner/pi-ai exposes — four functions stream / complete / streamSimple / completeSimple, plus an internal resolveApiProvider. It does not parse SSE, build request bodies, or handle retries. It just routes the (model, context, options) triple to the right provider based on model.api. Think of it as a dispatch office between callers and provider implementations: callers only see the unified return type AssistantMessageEventStream, and never see whether underneath is Anthropic SSE, OpenAI Responses, or Bedrock Converse.
Responsibilities
This layer does four things:
- Dispatch:
streamlooks up the provider in the registry bymodel.apiand hands off(model, context, options). Seepackages/ai/src/stream.ts:25-32. - Synchronous stream return: regardless of whether the underlying provider initializes asynchronously (lazy loading),
streamsynchronously returns anAssistantMessageEventStreamand pushes events as they arrive. Seepackages/ai/src/stream.ts:30-31. - Simplified entry points:
streamSimple/completeSimpletakeSimpleStreamOptions(exposing only the UI-common fields likereasoning,thinkingBudgets), and the provider translates simple options into full options itself. Seepackages/ai/src/stream.ts:43-50. - Missing provider error:
resolveApiProviderthrowsNo API provider registered for api: <api>when nothing is registered, instead of failing silently at the call site. Seepackages/ai/src/stream.ts:17-23.
Design motivation
Why have such a thin layer? Because the two things callers care about — "which model to call" and "streaming vs. final result" — should be decoupled from the specific provider. stream.ts collapses these two dimensions into four functions. Downstream consumers like pi-coding-agent's AgentSession, the CLI tool, and extensions all use the same entry points without importing concrete provider modules directly. That way provider implementations can be lazily loaded, swapped, or extended with new api registrations, without changing caller code.
complete does not implement a separate non-streaming path; it reuses stream and then calls s.result() — streaming infrastructure and one-shot returns share one pipeline, and providers only need to implement the streaming version.
Key files
packages/ai/src/stream.ts:1-3— at the top,import "./providers/register-builtins.js"triggers self-registration of the 9 built-in providers;getApiProviderreads from the registry.packages/ai/src/stream.ts:17-23—resolveApiProvider, throws when no provider is registered.packages/ai/src/stream.ts:25-32— the full-optionsstream.packages/ai/src/stream.ts:34-41—completeis equivalent tostream(...).result().packages/ai/src/stream.ts:43-50—streamSimpledelegates toprovider.streamSimple.packages/ai/src/stream.ts:52-59—completeSimpleis equivalent tostreamSimple(...).result().
The entire implementation of stream is resolve + hand-off, with no business logic:
// packages/ai/src/stream.ts:25-32
export function stream<TApi extends Api>(
model: Model<TApi>,
context: Context,
options?: ProviderStreamOptions,
): AssistantMessageEventStream {
const provider = resolveApiProvider(model.api);
return provider.stream(model, context, options as StreamOptions);
}complete reuses stream and then takes result(), without duplicating dispatch logic:
// packages/ai/src/stream.ts:34-41
export async function complete<TApi extends Api>(
model: Model<TApi>,
context: Context,
options?: ProviderStreamOptions,
): Promise<AssistantMessage> {
const s = stream(model, context, options);
return s.result();
}Data flow
Dispatch chain of stream(model, ctx, opts):
Boundaries and failure modes
- Missing provider:
resolveApiProviderthrows immediately rather than returning an empty stream, seepackages/ai/src/stream.ts:19-21. When an extension's dynamically registered provider is missing, the caller finds out right away. - Lazy-loaded providers: concrete provider modules are wrapped with
createLazyStream.streamsynchronously returns an outer stream, and once the module has loaded itforwardStreams to the inner one, seepackages/ai/src/providers/register-builtins.ts:159-178.streamitself is unaware of the lazy loading. - Type assertion:
options as StreamOptionscastsProviderStreamOptions(which may carry extension-specific fields) to the type the provider expects; the provider reads fields as needed. - Optional
options: all three functions allowoptions?to be omitted; providers fall back to defaults.
Summary
stream.ts is a 60-line dispatch facade that routes (model, context, options) to the provider in the registry. stream / complete are the full-options versions, and streamSimple / completeSimple are UI-friendly subset versions. For the registry itself, see ApiProvider registry; for how the 9 built-in providers register, see Provider abstraction and built-ins; for the AssistantMessageEventStream that gets returned, see EventStream async iterator.