Skip to main content

Overview

The @mariozechner/pi-ai package provides a unified interface to multiple LLM providers with automatic model discovery, token counting, cost tracking, and cross-provider context handoffs.

Unified API

Single interface for OpenAI, Anthropic, Google, and 12+ other providers

Type Safety

Full TypeScript support with auto-complete for providers and models

Tool Calling

TypeBox schemas with automatic validation and partial JSON streaming

Cross-Provider Handoff

Switch models mid-conversation with automatic context transformation

Installation

npm install @mariozechner/pi-ai

Quick Start

import { getModel, complete, stream } from "@mariozechner/pi-ai";

// Get a model with full auto-complete support
const model = getModel("anthropic", "claude-sonnet-4-20250514");

// Simple completion
const response = await complete(model, {
  systemPrompt: "You are a helpful assistant.",
  messages: [
    { role: "user", content: "Hello!" }
  ]
});

console.log(response.content[0].text);
console.log(`Tokens: ${response.usage.input}/${response.usage.output}`);
console.log(`Cost: $${response.usage.cost.total}`);

Key Features

Supported Providers

All providers support tool calling (function calling) for agentic workflows: API Key Providers:
  • OpenAI (GPT-4o, GPT-5, o1, o3)
  • Anthropic (Claude Sonnet, Opus, Haiku)
  • Google Gemini
  • Amazon Bedrock
  • Mistral AI
  • Groq
  • Cerebras
  • xAI (Grok)
  • OpenRouter
  • Vercel AI Gateway
  • zAI
  • MiniMax
  • Kimi For Coding
  • Hugging Face
OAuth Providers:
  • OpenAI Codex (ChatGPT Plus/Pro, GPT-5.x Codex models)
  • GitHub Copilot
  • Google Gemini CLI (Cloud Code Assist)
  • Google Antigravity (free Gemini, Claude, GPT-OSS)
Cloud Providers:
  • Azure OpenAI (Responses API)
  • Google Vertex AI (with ADC)
Custom:
  • Any OpenAI-compatible API (Ollama, vLLM, LM Studio, etc.)

Tool Calling with TypeBox

Define tools with TypeBox schemas for type-safe validation:
import { Type, Tool, complete } from "@mariozechner/pi-ai";

const tools: Tool[] = [{
  name: "get_weather",
  description: "Get current weather for a location",
  parameters: Type.Object({
    location: Type.String({ description: "City name" }),
    units: Type.Optional(Type.Union([
      Type.Literal("celsius"),
      Type.Literal("fahrenheit")
    ]))
  })
}];

const response = await complete(model, {
  messages: [{ role: "user", content: "What's the weather in London?" }],
  tools
});

// Handle tool calls
for (const block of response.content) {
  if (block.type === "toolCall") {
    console.log(`Tool: ${block.name}`);
    console.log(`Args: ${JSON.stringify(block.arguments)}`);
  }
}

Streaming with Events

Stream responses with granular event types:
import { stream } from "@mariozechner/pi-ai";

const s = stream(model, context);

for await (const event of s) {
  switch (event.type) {
    case "text_delta":
      process.stdout.write(event.delta);
      break;
    case "toolcall_end":
      console.log(`\nTool: ${event.toolCall.name}`);
      break;
    case "thinking_delta":
      // For reasoning models
      process.stdout.write(event.delta);
      break;
    case "done":
      console.log(`\nFinished: ${event.reason}`);
      break;
  }
}

const finalMessage = await s.result();
Event Types:
  • start - Stream begins
  • text_start, text_delta, text_end - Text generation
  • thinking_start, thinking_delta, thinking_end - Reasoning/thinking
  • toolcall_start, toolcall_delta, toolcall_end - Tool calls
  • done - Completion
  • error - Error or abort

Partial Tool Arguments

During streaming, tool arguments are progressively parsed:
for await (const event of s) {
  if (event.type === "toolcall_delta") {
    const toolCall = event.partial.content[event.contentIndex];
    if (toolCall.type === "toolCall" && toolCall.arguments) {
      // Arguments may be incomplete - check before using
      if (toolCall.arguments.filename) {
        console.log(`Writing to: ${toolCall.arguments.filename}`);
      }
    }
  }
}

Reasoning/Thinking Models

Many models support extended thinking capabilities:
import { streamSimple, completeSimple } from "@mariozechner/pi-ai";

const model = getModel("anthropic", "claude-sonnet-4-20250514");
// Also: openai/gpt-5-mini, google/gemini-2.5-flash, xai/grok-code-fast-1

const response = await completeSimple(model, {
  messages: [{ role: "user", content: "Solve: 2x + 5 = 13" }]
}, {
  reasoning: "medium"  // minimal | low | medium | high | xhigh
});

for (const block of response.content) {
  if (block.type === "thinking") {
    console.log("Thinking:", block.thinking);
  } else if (block.type === "text") {
    console.log("Response:", block.text);
  }
}

Cross-Provider Handoffs

Switch models mid-conversation - the library handles context transformation automatically:
import { getModel, complete, Context } from "@mariozechner/pi-ai";

const context: Context = {
  messages: []
};

// Start with Claude
const claude = getModel("anthropic", "claude-sonnet-4-20250514");
context.messages.push({ role: "user", content: "What is 25 * 18?" });
const claudeResponse = await complete(claude, context, { thinkingEnabled: true });
context.messages.push(claudeResponse);

// Switch to GPT-5 - Claude's thinking becomes <thinking> tagged text
const gpt5 = getModel("openai", "gpt-5-mini");
context.messages.push({ role: "user", content: "Is that correct?" });
const gptResponse = await complete(gpt5, context);
context.messages.push(gptResponse);

// Switch to Gemini
const gemini = getModel("google", "gemini-2.5-flash");
context.messages.push({ role: "user", content: "What was the original question?" });
const geminiResponse = await complete(gemini, context);

Image Support

Vision-capable models can process images:
import { readFileSync } from "fs";
import { getModel, complete } from "@mariozechner/pi-ai";

const model = getModel("openai", "gpt-4o");

if (model.input.includes("image")) {
  const imageBuffer = readFileSync("screenshot.png");
  const base64Image = imageBuffer.toString("base64");

  const response = await complete(model, {
    messages: [{
      role: "user",
      content: [
        { type: "text", text: "What's in this image?" },
        { type: "image", data: base64Image, mimeType: "image/png" }
      ]
    }]
  });
}

Token Counting and Costs

Every response includes detailed usage information:
const response = await complete(model, context);

console.log(response.usage);
// {
//   input: 150,
//   output: 75,
//   cacheRead: 0,
//   cacheWrite: 100,
//   cost: {
//     input: 0.00045,
//     output: 0.001125,
//     cacheRead: 0,
//     cacheWrite: 0.000375,
//     total: 0.00195
//   }
// }

Custom Models

Define custom models for local servers or proxies:
import { Model, stream } from "@mariozechner/pi-ai";

const ollamaModel: Model<"openai-completions"> = {
  id: "llama-3.1-8b",
  name: "Llama 3.1 8B (Ollama)",
  api: "openai-completions",
  provider: "ollama",
  baseUrl: "http://localhost:11434/v1",
  reasoning: false,
  input: ["text"],
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
  contextWindow: 128000,
  maxTokens: 32000
};

await stream(ollamaModel, context, { apiKey: "dummy" });

API Reference

Core Functions

// Get models
function getProviders(): string[];
function getModels(provider: string): Model[];
function getModel<P extends KnownProvider>(provider: P, modelId: string): Model;

// Completion (non-streaming)
function complete<A extends KnownApi>(
  model: Model<A>,
  context: Context,
  options?: StreamOptions
): Promise<AssistantMessage>;

function completeSimple<A extends KnownApi>(
  model: Model<A>,
  context: SimpleContext,
  options?: SimpleStreamOptions
): Promise<AssistantMessage>;

// Streaming
function stream<A extends KnownApi>(
  model: Model<A>,
  context: Context,
  options?: StreamOptions
): AssistantMessageEventStream;

function streamSimple<A extends KnownApi>(
  model: Model<A>,
  context: SimpleContext,
  options?: SimpleStreamOptions
): AssistantMessageEventStream;

// Tool validation
function validateToolCall(
  tools: Tool[],
  toolCall: { name: string; arguments: any }
): any;

Types

interface Context {
  systemPrompt?: string;
  messages: Message[];
  tools?: Tool[];
}

interface Message {
  role: "user" | "assistant" | "toolResult";
  content: ContentBlock[] | string;
  // ... additional fields
}

interface Tool {
  name: string;
  description: string;
  parameters: TSchema;  // TypeBox schema
}

interface AssistantMessage {
  role: "assistant";
  content: ContentBlock[];
  stopReason: "stop" | "length" | "toolUse" | "error" | "aborted";
  usage: Usage;
  timestamp: number;
}
For complete type definitions, see Stream and Complete API Reference.

OAuth Authentication

For providers requiring OAuth (Codex, Copilot, Gemini CLI, Antigravity):
# CLI login
npx @mariozechner/pi-ai login anthropic
npx @mariozechner/pi-ai login github-copilot
// Programmatic OAuth
import { loginGitHubCopilot, getOAuthApiKey } from "@mariozechner/pi-ai";

const credentials = await loginGitHubCopilot({
  onAuth: (url) => console.log(`Open: ${url}`),
  onProgress: (msg) => console.log(msg)
});

// Store credentials securely
const auth = { "github-copilot": { type: "oauth", ...credentials } };

// Use in requests
const result = await getOAuthApiKey("github-copilot", auth);
if (result) {
  await complete(model, context, { apiKey: result.apiKey });
}

Environment Variables

Provider API keys can be set via environment variables:
export ANTHROPIC_API_KEY=sk-ant-...
export OPENAI_API_KEY=sk-...
export GEMINI_API_KEY=...
export GROQ_API_KEY=...
export XAI_API_KEY=...
# ... and more
See Installation Guide for the complete list.

Build docs developers (and LLMs) love