19 自定义提供商
19.1 自定义提供商概述
通过扩展中的 pi.registerProvider(),可以注册完全自定义的模型提供者。这比 models.json 更强大,适用于需要以下高级功能的场景:
- 自定义认证:OAuth/SSO、设备码流程、企业认证
- 动态模型发现:从远程 API 获取可用模型列表
- 自定义流式 API:实现非标准 LLM API 的流式响应
- 请求过滤和修改:在代理层面修改请求
19.2 示例扩展
参考以下完整的提供者扩展示例:
19.3 快速参考
扩展可以注册两种形式:
19.3.1 完整 Provider(推荐)
使用 createProvider() 创建完整的 pi-ai Provider:
import { createProvider, openAICompletionsApi } from "@earendil-works/pi-ai";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) {
pi.registerProvider(createProvider({
id: "native-local",
name: "Native Local",
baseUrl: "http://localhost:8080/v1",
auth: {
apiKey: {
name: "Local server API key",
async login(interaction) {
return {
type: "api_key",
key: await interaction.prompt({ type: "secret", message: "API key" })
};
},
async resolve({ credential }) {
return credential?.key
? { auth: { apiKey: credential.key }, source: "stored API key" }
: undefined;
}
}
},
models: [],
api: openAICompletionsApi()
}));
}19.3.2 Legacy Provider Config 形式
适用于简单覆盖或新提供者注册:
// 覆盖已有提供者的 baseUrl
pi.registerProvider("anthropic", {
baseUrl: "https://proxy.example.com"
});
// 注册新提供者
pi.registerProvider("my-provider", {
name: "My Provider",
baseUrl: "https://api.example.com",
apiKey: "$MY_API_KEY",
api: "openai-completions",
models: [
{
id: "my-model",
name: "My Model",
reasoning: false,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 4096
}
]
});优先使用完整 Provider 形式,当需要自定义认证、过滤、刷新或流式行为时。Pi 会在注册的原生 Provider 之上叠加 models.json 覆盖。
19.4 覆盖现有提供者
最简单的用法——将现有提供者重定向到代理:
// 所有 Anthropic 请求通过代理
pi.registerProvider("anthropic", {
baseUrl: "https://proxy.example.com"
});
// 添加自定义请求头到 OpenAI
pi.registerProvider("openai", {
headers: {
"X-Custom-Header": "value"
}
});
// 同时修改 baseUrl 和 headers
pi.registerProvider("google", {
baseUrl: "https://ai-gateway.corp.com/google",
headers: {
"X-Corp-Auth": "$CORP_AUTH_TOKEN"
}
});当仅提供 baseUrl 和/或 headers(不含 models)时,该提供者的所有现有模型保留,仅端点变更。
19.5 注册新提供者
19.5.1 静态模型列表
pi.registerProvider("my-llm", {
baseUrl: "https://api.my-llm.com/v1",
apiKey: "$MY_LLM_API_KEY",
api: "openai-completions",
models: [
{
id: "my-llm-large",
name: "My LLM Large",
reasoning: true,
input: ["text", "image"],
cost: {
input: 3.0,
output: 15.0,
cacheRead: 0.3,
cacheWrite: 3.75
},
contextWindow: 200000,
maxTokens: 16384
}
]
});19.5.2 动态模型发现
使用异步扩展工厂从远程端点获取模型列表:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default async function (pi: ExtensionAPI) {
const response = await fetch("http://localhost:1234/v1/models");
const payload = await response.json() as {
data: Array<{
id: string;
name?: string;
context_window?: number;
max_tokens?: number;
}>;
};
pi.registerProvider("local-openai", {
baseUrl: "http://localhost:1234/v1",
apiKey: "$LOCAL_OPENAI_API_KEY",
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,
})),
});
}Pi 会等待异步工厂完成,因此在工厂中注册的提供者在交互式启动和 pi --list-models 时都可用。
19.6 注销提供者
使用 pi.unregisterProvider(name) 移除之前注册的提供者:
// 注册
pi.registerProvider("my-llm", { /* ... */ });
// 移除
pi.unregisterProvider("my-llm");注销会移除该提供者的: - 动态模型 - API 密钥回退 - OAuth 提供者注册 - 自定义流式处理程序注册
被覆盖的内置模型或提供者行为会恢复。初始扩展加载阶段之后的调用立即生效,无需 /reload。
19.7 API 类型
api 字段决定使用的流式实现:
| API | 适用场景 |
|---|---|
anthropic-messages |
Anthropic Claude API 及兼容 |
openai-completions |
OpenAI Chat Completions API 及兼容 |
openai-responses |
OpenAI Responses API |
azure-openai-responses |
Azure OpenAI Responses API |
openai-codex-responses |
OpenAI Codex Responses API |
mistral-conversations |
Mistral SDK Conversations/Chat |
google-generative-ai |
Google Generative AI API |
google-vertex |
Google Vertex AI API |
bedrock-converse-stream |
Amazon Bedrock Converse API |
19.7.1 兼容性配置
模型级别的思考级别和兼容性配置:
models: [{
id: "custom-model",
reasoning: true,
thinkingLevelMap: {
minimal: null,
low: null,
medium: null,
high: "default",
xhigh: null,
max: "max"
},
compat: {
supportsDeveloperRole: false,
supportsReasoningEffort: true,
maxTokensField: "max_tokens",
requiresToolResultName: true,
thinkingFormat: "qwen",
cacheControlFormat: "anthropic"
}
}]19.8 Auth Header
如果提供者期望 Authorization: Bearer <key> 但不使用标准 API,设置 authHeader: true:
pi.registerProvider("custom-api", {
baseUrl: "https://api.example.com",
apiKey: "$MY_API_KEY",
authHeader: true, // 自动添加 Authorization: Bearer header
api: "openai-completions",
models: [...]
});密钥为每个请求解析。显式的请求 Authorization header 优先于生成的值。
19.9 OAuth 支持
添加 OAuth/SSO 认证,集成到 /login 命令:
import type { OAuthCredentials, OAuthLoginCallbacks } from "@earendil-works/pi-ai";
pi.registerProvider("corporate-ai", {
baseUrl: "https://ai.corp.com/v1",
api: "openai-responses",
models: [...],
oauth: {
name: "Corporate AI (SSO)",
async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
const method = await callbacks.onSelect({
message: "Select login method:",
options: [
{ id: "browser", label: "Browser OAuth" },
{ id: "device", label: "Device code" }
]
});
if (!method) throw new Error("Login cancelled");
let code: string;
if (method === "device") {
callbacks.onDeviceCode({
userCode: "ABCD-1234",
verificationUri: "https://sso.corp.com/device",
intervalSeconds: 5,
expiresInSeconds: 900
});
code = await pollDeviceCodeUntilComplete();
} else {
callbacks.onAuth({ url: "https://sso.corp.com/authorize?..." });
code = await callbacks.onPrompt({ message: "Enter SSO code:" });
}
const tokens = await exchangeCodeForTokens(code);
return {
refresh: tokens.refreshToken,
access: tokens.accessToken,
expires: Date.now() + tokens.expiresIn * 1000
};
},
async refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
const tokens = await refreshAccessToken(credentials.refresh);
return {
refresh: tokens.refreshToken ?? credentials.refresh,
access: tokens.accessToken,
expires: Date.now() + tokens.expiresIn * 1000
};
},
getApiKey(credentials: OAuthCredentials): string {
return credentials.access;
}
}
});注册后,用户可通过 /login corporate-ai 进行认证。
19.9.1 OAuthLoginCallbacks
| 方法 | 说明 |
|---|---|
onAuth({ url }) |
在浏览器中打开 URL(用于 OAuth 重定向) |
onDeviceCode({ userCode, verificationUri, ... }) |
显示设备码 |
onProgress?(message) |
显示临时进度 |
onPrompt({ message }) |
提示用户输入 |
onSelect({ message, options }) |
显示交互式选择器 |
19.9.2 OAuthCredentials
凭据持久化在 ~/.pi/agent/auth.json:
interface OAuthCredentials {
refresh: string; // 刷新令牌
access: string; // 访问令牌
expires: number; // 过期时间戳(毫秒)
}19.10 自定义流式 API
对于非标准 API 的提供者,实现 streamSimple。
在编写自定义流式实现之前,请研究现有的提供者实现。大多数场景下 openai-completions 或 anthropic-messages 已经足够。
19.10.1 参考实现
- anthropic.ts — Anthropic Messages API
- mistral.ts — Mistral Conversations API
- openai-completions.ts — OpenAI Chat Completions
- openai-responses.ts — OpenAI Responses API
- google.ts — Google Generative AI
- amazon-bedrock.ts — AWS Bedrock
19.10.2 Stream Pattern
所有提供者的 streamSimple 遵循相同模式:
- 构建请求(headers、body)
- 发起 HTTP 请求
- 解析 SSE 流
- 发出事件(text delta、thinking delta、tool call、usage)
19.10.3 Event Types
| 事件类型 | 说明 |
|---|---|
text_delta |
文本增量 |
thinking_delta |
思考增量 |
tool_call_start |
工具调用开始 |
tool_call_delta |
工具调用增量 |
tool_call_end |
工具调用结束 |
usage |
Token 使用量 |
error |
错误 |
done |
流结束 |
19.10.4 Content Blocks
每个事件可以包含以下内容块类型:
text:文本内容thinking:思考内容tool_use:工具调用请求tool_result:工具调用结果
19.10.5 Usage and Cost
在流结束时报告 token 使用量和费用:
yield {
type: "usage",
input: 1500,
output: 800,
cacheRead: 200,
cacheWrite: 100,
cost: {
input: 0.0075,
output: 0.012,
total: 0.0195
}
};19.10.6 Context Overflow Errors
当请求超出上下文窗口时,返回特定的错误类型:
throw new ContextOverflowError({
type: "context_overflow",
message: "Context length exceeded",
inputTokens: 250000,
contextWindow: 200000
});Pi 会自动触发压缩或显示错误信息。
19.11 测试方法
19.11.1 基本测试
# 测试提供者注册
pi --list-models 2>/dev/null | grep my-provider
# 测试模型选择
pi --model my-provider/my-model "Hello"
# 测试 /login
pi --login corporate-ai19.11.2 使用 Pi 本身测试
在 Pi 对话中直接说”帮我测试 my-provider 提供者”,Pi 会自动检查注册状态、模型列表和基本连通性。