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,
})),
});
}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 --verboseLook for your provider in the startup output.
19.8.2 List Models
pi --list-modelsYour 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",
},
},
],
});
}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.