19  Custom Providers

19.1 Overview

While models.json configures providers that speak supported APIs, extensions can register providers with fully custom authentication, API implementations, and OAuth flows. This enables integration with any LLM provider, including those with proprietary APIs.

19.2 Quick Reference

19.2.1 Register a Provider

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

export default function (pi: ExtensionAPI) {
  pi.registerProvider("my-provider", {
    baseUrl: "https://api.my-provider.com",
    apiKey: "$MY_PROVIDER_API_KEY",
    api: "openai-completions",
    models: [
      {
        id: "my-model-v1",
        name: "My Model v1",
        reasoning: false,
        input: ["text"],
        cost: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 },
        contextWindow: 64000,
        maxTokens: 4096,
      },
    ],
  });
}

19.2.2 Unregister a Provider

pi.unregisterProvider("my-provider");

19.2.3 Override an Existing Provider

Registering a provider with the same name as a built-in replaces it entirely:

pi.registerProvider("anthropic", {
  baseUrl: "https://my-anthropic-proxy.example.com",
  apiKey: "$PROXY_KEY",
  api: "anthropic",
  models: [
    // Custom model definitions...
  ],
});

19.3 API Types

Providers can use any of the following API types:

19.3.1 "openai-completions"

Standard OpenAI-compatible Chat Completions API. Used by:

  • OpenAI
  • Azure OpenAI
  • Ollama
  • vLLM
  • LM Studio
  • llama.cpp
  • Together AI
  • Groq
  • DeepSeek
  • And many more OpenAI-compatible services
pi.registerProvider("my-ollama", {
  baseUrl: "http://localhost:11434/v1",
  apiKey: "ollama",
  api: "openai-completions",
  models: [/* ... */],
});

19.3.2 "anthropic"

Anthropic Messages API. Used by Anthropic and Anthropic-compatible proxies.

pi.registerProvider("my-anthropic-proxy", {
  baseUrl: "https://my-proxy.example.com",
  apiKey: "$PROXY_KEY",
  api: "anthropic",
  models: [/* ... */],
});

19.3.3 "google"

Google Gemini API.

pi.registerProvider("my-gemini", {
  baseUrl: "https://generativelanguage.googleapis.com",
  apiKey: "$GEMINI_API_KEY",
  api: "google",
  models: [/* ... */],
});

19.3.4 "bedrock"

Amazon Bedrock API.

pi.registerProvider("my-bedrock", {
  baseUrl: "https://bedrock-runtime.us-west-2.amazonaws.com",
  api: "bedrock",
  apiKey: "$AWS_BEARER_TOKEN_BEDROCK",
  models: [/* ... */],
});

19.4 Auth Header

Control how the API key is sent in HTTP headers:

pi.registerProvider("my-provider", {
  baseUrl: "https://api.my-provider.com",
  apiKey: "$MY_API_KEY",
  api: "openai-completions",
  authHeader: "X-API-Key",  // Default: "Authorization"
  authPrefix: "",           // Default: "Bearer "
  models: [/* ... */],
});

19.4.1 Default Behavior

For "openai-completions" API, the key is sent as:

Authorization: Bearer <key>

19.4.2 Custom Header Example

{
  authHeader: "X-Api-Key",
  authPrefix: "",
  // Results in: X-Api-Key: <key>
}

19.5 OAuth Support

Extensions can implement OAuth flows for providers that require them. This is how built-in subscription providers work.

19.5.1 OAuth Flow Example

export default async function (pi: ExtensionAPI) {
  // Register an OAuth-based provider
  pi.registerProvider("my-oauth-provider", {
    baseUrl: "https://api.my-provider.com",
    api: "openai-completions",
    oauth: {
      type: "pkce",
      authUrl: "https://auth.my-provider.com/authorize",
      tokenUrl: "https://auth.my-provider.com/token",
      clientId: "$CLIENT_ID",
      clientSecret: "$CLIENT_SECRET",
      scopes: ["read", "write"],
      redirectUri: "http://localhost:9876/callback",
    },
    models: [/* ... */],
  });
}

19.5.2 Token Storage

OAuth tokens are stored in ~/.pi/agent/auth.json alongside API keys. Tokens are automatically refreshed when expired.

19.5.3 Interactive OAuth

For interactive OAuth flows that require opening a browser:

import { open } from "node:fs/promises";

pi.on("project_trust", async (_event, ctx) => {
  // Start OAuth flow
  const authUrl = buildAuthUrl();
  await ctx.ui.notify("Opening browser for authentication...", "info");
  
  // Open browser
  const { spawn } = await import("node:child_process");
  spawn("open", [authUrl], { detached: true });
  
  // Wait for callback
  const code = await waitForCallback();
  
  // Exchange code for token
  const token = await exchangeCode(code);
  
  // Store credential
  // ...
});

19.6 Custom Streaming API

For providers with non-standard streaming protocols, implement a custom API adapter:

interface CustomProviderConfig {
  baseUrl: string;
  apiKey: string;
  models: Array<{
    id: string;
    name: string;
    contextWindow: number;
    maxTokens: number;
  }>;
}

pi.registerProvider("custom-streaming", {
  baseUrl: "https://api.custom.com",
  apiKey: "$CUSTOM_API_KEY",
  api: "openai-completions",  // Base API
  models: [/* ... */],
  
  // Custom request transformer
  transformRequest(request) {
    // Modify the request before sending
    return {
      ...request,
      custom_field: "value",
    };
  },
  
  // Custom response parser
  transformResponse(response) {
    // Parse non-standard response format
    return {
      content: response.choices[0]?.message?.content || "",
      usage: {
        inputTokens: response.usage?.prompt_tokens || 0,
        outputTokens: response.usage?.completion_tokens || 0,
      },
    };
  },
});

19.7 Dynamic Model Discovery

Fetch available models from the provider at startup:

export default async function (pi: ExtensionAPI) {
  // Fetch models from local server
  const response = await fetch("http://localhost:1234/v1/models");
  const payload = await response.json();
  
  pi.registerProvider("local-models", {
    baseUrl: "http://localhost:1234/v1",
    apiKey: "local",
    api: "openai-completions",
    models: payload.data.map((model) => ({
      id: model.id,
      name: model.name ?? model.id,
      reasoning: false,
      input: ["text"],
      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
      contextWindow: model.context_window ?? 128000,
      maxTokens: model.max_tokens ?? 4096,
    })),
  });
}
Note

Async factory functions are awaited before startup continues. This means dynamically discovered models are available during normal startup and to pi --list-models.

19.8 Testing Custom Providers

19.8.1 Verify Registration

pi --verbose

Look for your provider in the startup output.

19.8.2 List Models

pi --list-models

Your custom models should appear in the list.

19.8.3 Test Connection

pi --model my-provider/my-model -p "Hello, are you working?"

19.8.4 Debug API Calls

Use verbose mode to see API requests and responses:

pi --model my-provider/my-model --verbose -p "Test"

19.9 Complete Example: Enterprise Proxy

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

export default async function (pi: ExtensionAPI) {
  // Fetch token from enterprise secret manager
  const tokenResponse = await fetch("https://secrets.internal/token", {
    headers: { "X-Vault-Token": process.env.VAULT_TOKEN! },
  });
  const { token } = await tokenResponse.json();
  
  pi.registerProvider("enterprise-llm", {
    baseUrl: "https://llm.internal/api/v1",
    apiKey: token,
    api: "openai-completions",
    authHeader: "X-Enterprise-Key",
    authPrefix: "",
    headers: {
      "X-Org-ID": "engineering",
      "X-Project": "pi-coding",
    },
    models: [
      {
        id: "enterprise-coder",
        name: "Enterprise Coder Model",
        reasoning: true,
        input: ["text"],
        cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
        contextWindow: 128000,
        maxTokens: 8192,
        thinkingLevelMap: {
          off: "none",
          low: "brief",
          medium: "standard",
          high: "detailed",
          xhigh: "thorough",
          max: "exhaustive",
        },
      },
    ],
  });
}
Tip

Custom providers registered via extensions are first-class citizens. They work with /model, --list-models, scoped models, and all other Pi features just like built-in providers.