Skip to content

Chapter 2 · pi-ai Quickstart: Unified LLM API ​

Chapter goals: Master pi-ai's core abstraction — the Models collection — learn to register providers with createModels() / setProvider(), and make your first streaming call with streamSimple.

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:

text
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 ​

typescript
// 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 factory

Bundle 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:

typescript
// 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:

typescript
// 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);
}
APISuitable ScenarioReturns
streamSimpleUI real-time output, need to observe intermediate eventsAsync event iterator + .result()
completeSimpleBackground tasks, simple Q&AFinal AssistantMessage

2.5 The Value of a Unified Interface ​

With the same business code, switching models only requires changing one lookup parameter:

typescript
// 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 Models collection is a router: register on demand with setProvider() or use builtinModels() for a full registration;
  • streamSimple initiates streaming calls with unified options; text_delta is the incremental text event;
  • completeSimple is 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 ​

  1. Ask the same question with both streamSimple and completeSimple, and compare the terminal output shape and perceived latency.
  2. Register two different providers (e.g. Anthropic + DeepSeek), ask "1+1" once with the same context for each, and verify zero business code changes.
  3. Print all fields of finalMessage.usage and 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.