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
    }
  ]
});
Note

优先使用完整 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,
    })),
  });
}
Tip

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

Warning

在编写自定义流式实现之前,请研究现有的提供者实现。大多数场景下 openai-completionsanthropic-messages 已经足够。

19.10.1 参考实现

19.10.2 Stream Pattern

所有提供者的 streamSimple 遵循相同模式:

  1. 构建请求(headers、body)
  2. 发起 HTTP 请求
  3. 解析 SSE 流
  4. 发出事件(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-ai

19.11.2 使用 Pi 本身测试

在 Pi 对话中直接说”帮我测试 my-provider 提供者”,Pi 会自动检查注册状态、模型列表和基本连通性。