Chapter 2 · pi-ai Quickstart: Unified LLM API
Chapter goals: Master pi-ai's core abstraction — the
Modelscollection — learn to register providers withcreateModels()/setProvider(), and make your first streaming call withstreamSimple.
2.1 Core Mental Model: The Models Collection
Everything in pi-ai revolves around one concept: the Models collection. It is a "router" — it holds several providers, and each request is forwarded to the provider that owns the given model ID:
models.complete(model, context)
│
▼
┌──────────────────┐ ┌──────────────────┐
│ anthropicProvider│ │ openaiProvider │ …other registered providers
│ (auth+catalog+streaming) │ │ (auth+catalog+streaming) │
└──────────────────┘ └──────────────────┘A Provider is a runtime unit that is responsible for three things: model catalog (what models exist), authentication (how the API Key is resolved), and streaming behavior (how to call the wire API).
2.2 Two Ways to Create a Collection
// Approach 1: Register on demand (recommended for production, smaller bundle)
import { createModels } from "@earendil-works/pi-ai";
import { anthropicProvider } from "@earendil-works/pi-ai/providers/anthropic";
import { deepseekProvider } from "@earendil-works/pi-ai/providers/deepseek";
const models = createModels();
// Each factory sub-path only introduces that vendor's catalog and lazy-loaded SDK
models.setProvider(anthropicProvider());
models.setProvider(deepseekProvider());
// Approach 2: One-shot registration of all built-in providers (most convenient for prototyping)
import { builtinModels } from "@earendil-works/pi-ai/providers/all";
const all = builtinModels(); // equivalent to calling setProvider for every factoryBundle size
providers/all pulls in all built-in catalogs and SDK wrappers. For size-sensitive applications (especially on the browser side), use approach 1 to register on demand.
2.3 streamSimple: The Simplest Streaming Call
streamSimple provides cross-provider unified simplified options (such as reasoning: 'medium') and is the default choice for Agent scenarios:
// Complete minimal streaming example
import { createModels } from "@earendil-works/pi-ai";
import { anthropicProvider } from "@earendil-works/pi-ai/providers/anthropic";
const models = createModels();
models.setProvider(anthropicProvider());
const model = models.getModel("anthropic", "claude-sonnet-4-6")!;
// Context is plain data: system prompt + messages array + tools list
const context = {
systemPrompt: "You are a concise assistant.",
messages: [
// Every message must carry a timestamp (milliseconds)
{ role: "user", content: "Explain what a closure is", timestamp: Date.now() },
],
};
// Initiate the streaming request; auth is resolved automatically from ANTHROPIC_API_KEY by the provider
const stream = models.streamSimple(model, context);
for await (const event of stream) {
switch (event.type) {
case "text_delta":
// Incremental text: write directly to the terminal for a typewriter effect
process.stdout.write(event.delta);
break;
case "thinking_delta":
// Thinking increment from reasoning-capable models (if reasoning is enabled)
process.stdout.write(`🤔 ${event.delta}`);
break;
case "done":
// Completion reason: stop / length / toolUse / error / aborted
console.log("\n[Done]", event.reason);
break;
}
}
// result() returns the final complete message (including usage/billing info)
const final = await stream.result();
console.log(`Input ${final.usage.input} tokens, output ${final.usage.output} tokens`);
console.log(`Cost $${final.usage.cost.total.toFixed(4)}`);2.4 completeSimple: When Streaming Is Not Needed
If you only need the final answer (e.g. for batch processing tasks), completeSimple does it in one line:
// Non-streaming: wait for the complete response before returning
const response = await models.completeSimple(model, context);
// The response content is an array of content blocks: text / thinking / toolCall
for (const block of response.content) {
if (block.type === "text") console.log(block.text);
}| API | Suitable Scenario | Returns |
|---|---|---|
streamSimple | UI real-time output, need to observe intermediate events | Async event iterator + .result() |
completeSimple | Background tasks, simple Q&A | Final AssistantMessage |
2.5 The Value of a Unified Interface
With the same business code, switching models only requires changing one lookup parameter:
// The same context can be fed to any registered provider's model
const gpt = all.getModel("openai", "gpt-5-mini")!;
const ds = all.getModel("deepseek", "deepseek-chat")!;
// Anthropic's response
await models.completeSimple(model, context);
// DeepSeek's response — zero business code changes
await models.completeSimple(ds, context);Message format, tool protocol, and error semantics are all flattened by pi-ai. This is the foundation that makes Chapter 16's "cross-provider Handoffs" work seamlessly.
2.6 Chapter Summary
- The
Modelscollection is a router: register on demand withsetProvider()or usebuiltinModels()for a full registration; streamSimpleinitiates streaming calls with unified options;text_deltais the incremental text event;completeSimpleis used when streaming is not needed;- All responses uniformly carry
usage(token count and cost); switching models requires no business code changes.
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. What three things is a Provider responsible for in pi-ai?
2. What is the difference between `streamSimple` and `stream`?
3. Why is using provider factories for on-demand registration recommended over providers/all in production?
4. What field must every message in a Context object carry?
🛠️ Hands-on Practice
- Ask the same question with both
streamSimpleandcompleteSimple, and compare the terminal output shape and perceived latency. - Register two different providers (e.g. Anthropic + DeepSeek), ask "1+1" once with the same context for each, and verify zero business code changes.
- Print all fields of
finalMessage.usageand calculate the actual cost per million tokens for this conversation.
With the unified API mastered, move on to Chapter 3 to dig deeper into the model catalog and provider queries.