数据截至 (上游 commit 59a71b235dad)
pi-ai:统一多-provider LLM 层
30 秒导读: pi-ai 是 Pi 项目的最底层基座——一个 TypeScript 库,把 40 多家大模型 供应商(Anthropic、OpenAI、Google、Bedrock、Mistral、xAI、DeepSeek、各种网关……)、 8 种互不兼容的 HTTP/WebSocket 线缆协议,统一成一个函数调用:给它一个
model和一段 对话Context,它返回一个标准化的流式事件流。上层的 agent 循环(见 02-agent-loop) 因此完全不用关心底下是谁、说的是哪种"方言"。
1. 这是什么(零基础也能懂)
一句话定义: pi-ai 是一个"大模型万能翻译插座"——你按同一种方式插进去,它负责把请求 翻译成每家供应商各自的电压和插脚。
解决什么问题 / 给谁用。 假设你在写一个编码 agent,想让用户既能用 Claude、也能用 GPT、 还能用本地跑的 Llama。三家的 HTTP 接口完全不一样:
- Anthropic 用
/v1/messages,thinking(思考)是一种独立的 content block。 - OpenAI 有两套接口:老的
/chat/completions和新的/responses,reasoning 的表达方式又不同。 - Google 用
generateContent,消息角色叫model不叫assistant。 - Amazon Bedrock 走的是 AWS SigV4 签名的 Converse Stream,连认证方式都不一样。
如果每加一家供应商,你的 agent 主循环就得改一次,那是灾难。pi-ai 就是把这堆差异吸收进一层, 让上层只面对一个稳定接口。
它能做什么(功能):
- 规范化的会话数据模型:一套
Context/Message/AssistantMessage,所有供应商共用。 - 每种线缆协议一个适配器:把规范模型 ↔ 该协议的 JSON 来回翻译。
- 统一的流式接口:
stream()/streamSimple()返回同一种AssistantMessageEventStream。 - 内置的模型目录(40+ provider、数百个模型),带价格、上下文窗口、能力标记。
- 认证解析:OAuth token、API key、AWS profile、环境变量……自动挑对的那个。
- 成本 / token 统计、重试分类、上下文溢出检测等横切工具。
用起来什么样。 最小调用(示意,真实 API 见下文):
// 示意,非源码 —— 展示"上层眼里 pi-ai 有多简单"
import { builtinModels } from "@earendil-works/pi-ai/providers/all";
const models = builtinModels(); // 装好所有内置 provider
const model = models.getModel("anthropic", "claude-opus-4-7");
// 同一个调用,换成 openai / google / bedrock 只需改上面两行
const stream = models.streamSimple(model, {
systemPrompt: "You are helpful.",
messages: [{ role: "user", content: "你好", timestamp: Date.now() }],
});
for await (const event of stream) {
if (event.type === "text_delta") process.stdout.write(event.delta);
}
const final = await stream.result(); // 完整 AssistantMessage
一句话直觉 / 类比。 把 pi-ai 想成旅行万能转换插头:你的电器(agent 循环)只有一种插头, 墙上的插座(供应商)有几十种。转换插头内部对每种墙插各有一套铜片布局(适配器),但对你露出的 永远是同一个孔。
2. 顶层全景(它大概怎么转)
pi-ai 的整个价值,浓缩成一句话:"规范模型"进,"规范事件流"出,中间用一层可替换的适配器 兑换成某家供应商的方言。 全景如下(从左到右是一次请求的数据流):
上层(agent 循环)
│ 给出: Model + Context(规范模型)
▼
┌───────────────────────────────────────────────────────┐
│ Models 集合 (models.ts) │
│ · 按 model.provider 找到 Provider │
│ · 解析认证 (auth/): OAuth? api-key? AWS profile? │
│ · 把 apiKey/headers/baseUrl 注入 options │
└───────────────────────────────────────────────────────┘
│ 委托给拥有该 model 的 Provider
▼
┌───────────────────────────────────────────────────────┐
│ Provider (providers/anthropic.ts …) │
│ · 绑定到某个 "API 格式" (= wire 协议适配器) │
│ · 按 model.api 分发到对应适配器 │
└───────────────────────────────────────────────────────┘
│ model.api = "anthropic-messages"
▼
┌────────────────────────────────────── ─────────────────┐
│ API 适配器 (api/anthropic-messages.ts …) │
│ 规范 Context ──► 该协议 JSON (buildParams) │
│ 供应商 SSE 分片 ──► 规范事件 (convertMessages 反向) │
└───────────────────────────────────────────────────────┘
│ 逐块推送
▼
AssistantMessageEventStream(统一事件流)──► 回到上层
部件一句话职责:
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
| 规范数据模型 | 定义所有供应商共用的会话类型 | packages/ai/src/types.ts |
| API 适配器 | 每种 wire 协议一个,做规范↔方言的双向翻译 | packages/ai/src/api/*.ts |
| Provider | 把"某家供应商"绑定到"某个 API 格式" | packages/ai/src/providers/*.ts |
| Models 集合 | 注册 provider、解析认证、按 model 分发请求 | packages/ai/src/models.ts |
| api-registry | 旧版全局 stream() 的 api→实现 注册表 | packages/ai/src/compat.ts |
| 事件流 | 统一的异步可迭代 + 最终结果 promise | packages/ai/src/utils/event-stream.ts |
| 模型目录 | 生成的静态目录(价格/窗口/能力) | packages/ai/src/models.generated.ts |
| 认证 | 凭据存储 + OAuth/api-key 解析 | packages/ai/src/auth/*.ts |
主线走一遍(高层,不进代码):
- 上层拿一个
Model(描述"我要用哪家的哪个模型")和一个Context(系统提示 + 消息 + 工具)。 Models.streamSimple(model, context)按model.provider找到对应Provider,先解析认证。Provider看model.api(如"anthropic-messages"),分发给对应的 API 适配器。- 适配器把规范
Context翻译成该协议的请求 JSON,发出去,再把回来的 SSE 分片翻译回规范事件。 - 上层拿到的永远是同一种
AssistantMessageEventStream——不管底下是谁。
两条平行的入口。 pi-ai 有两套并存的分发路径:新路径是面向对象的
Models/Provider(models.ts),旧路径是全局函数stream()+ api-registry(compat.ts, 文件头自称"临时兼容入口")。本章两者都讲,因为它们共享同一批适配器和同一套数据模型。