深色模式
01 · pi-ai — 统一多 provider LLM API
包路径:
packages/ai(registry 名@earendil-works/pi-ai) 定位:把 OpenAI / Anthropic / Google / Bedrock / OpenRouter / 国内厂商等差异抹平成一套接口。整个 Pi 最底层、最独立、最值得先读。
1. 设计原则(来自 src/index.ts 注释)
index.ts 明确区分了四类导出边界:
- core(无副作用):类型、
models.ts、models-store.ts、providers/faux.ts、session-resources.ts、工具函数。 - provider 工厂:放在
@earendil-works/pi-ai/providers/*(如providers/anthropic.ts)。 - API 实现:放在
@earendil-works/pi-ai/api/*(如api/anthropic-messages.ts)。 - 旧全局 API:放在
@earendil-works/pi-ai/compat。
学习提示:
index.ts本身不引入生成目录、不引入 OAuth、不引入 compat。想看"干净的核心"只读index.ts+models.ts即可。
2. 目录结构与关键文件
| 目录 / 文件 | 规模 | 作用 |
|---|---|---|
src/index.ts | 48 行 | 对外导出入口(先看) |
src/models.ts | ~46k | createModels() 注册表、Model 类型、setProvider / getModel |
src/models.generated.ts | 自动生成 | 模型目录,勿手改;改 scripts/generate-models.ts 后重新生成 |
src/model-catalog.ts | — | 模型目录的加载/覆盖逻辑 |
src/providers/all.ts | 8k | 汇总所有 provider(看完整厂商清单的入口) |
src/providers/<vendor>.ts | 94 个 | 每个厂商一个 provider 工厂(*.models.ts 是该厂商的模型清单) |
src/api/<vendor>-*.ts | 43 个 | 各厂商请求/响应格式与流式实现;*.lazy.ts 为懒加载分包 |
src/api/transform-messages.ts | 7.6k | 跨 provider 消息归一化的核心(重点) |
src/auth/credential-store.ts | — | 凭据存储 |
src/auth/oauth/* | 11 个 | 各厂商 OAuth(anthropic / openai / github-copilot / openrouter / xai ...) |
src/utils/* | 26 个 | retry / event-stream / overflow / transcript / provider-retry 等 |
3. 核心抽象
Model<TApi>:一个可调用模型,含provider/api/id/reasoning/input(能力,如是否支持image)/contextWindow/cost等。Api:协议族标识(openai-responses、anthropic-messages、google-generative-ai ...)。Provider:给定配置产出Model并知道如何调用。StreamFn(来自pi-agent-core概念):(model, context, options) => EventStream;真正发请求的地方在api/*。Models注册表(models.ts):setProvider(provider)注册,getModel(provider, id)取模型,调用方只认Model不认厂商。
4. 一次请求的生命周期
调用方(agent)
→ streamFn(model, llmContext, {apiKey, signal})
→ 按 model.api 路由到 src/api/<vendor>-*.ts 的实现
→ 构造厂商 HTTP 请求(SSE / WebSocket)
→ 逐块解析为统一事件:start / text_delta / thinking_delta / toolcall_delta / done / error
→ 返回 EventStream<AssistantMessage>1
2
3
4
5
6
2
3
4
5
6
- 厂商差异全部收敛在
src/api/与src/providers/。新增一个厂商 = 加providers/<x>.ts+providers/<x>.models.ts+api/<x>-*.ts,并在providers/all.ts注册。 *.lazy.ts用动态 import 把不常用 provider 拆成独立 chunk,减小主包体积。
5. 统一 API 的关键:transform-messages.ts
这是"统一多 provider"的真正难点所在。一个历史 Message[] 可能混有不同厂商、不同模型产出的消息,发给新模型前必须归一化(transformMessages):
- null/undefined content 填 []:兼容手搓历史与旧 session 文件。
- 图片降级:若目标
model.input不含image,把 user / toolResult 里的 image 块替换成占位文本。 - thinking 块处理:
- 加密 redacted thinking 仅对同模型有效,跨模型丢弃;
- 同模型且带签名(replay 需要)保留;空 thinking 丢弃,否则转纯文本。
- tool call id 归一化:OpenAI Responses 的 id 可能 450+ 字符含
|,Anthropic 要求^[a-zA-Z0-9_-]+$(≤64)。提供normalizeToolCallId回调重映射,并建立toolCallIdMap。 - 孤儿 tool call 补合成结果:跨消息的 tool call 若没有对应 toolResult,插入
toolResult: "No result provided", isError:true,满足 API 约束;system 消息在 tool call 与结果之间会被"挂起"后补发,避免重复结果。
这是读
pi-ai最该精读的一个文件——它暴露了多 provider 真实存在的坑。
6. 模型目录与代码生成
src/models.generated.ts由packages/ai/scripts/generate-models.ts生成,禁止手改。- 改模型元数据要改脚本 → 重新生成 → 连同 diff 一并提交(AGENTS.md 明确规定)。
nix/model-catalog.json是 Nix 离线构建时用的目录快照。
7. 阅读清单(建议顺序)
src/index.ts— 看清导出边界。src/models.ts—createModels/Model/setProvider/getModel。src/providers/all.ts— 全厂商清单。- 选一个厂商串一遍:
providers/anthropic.ts→providers/anthropic.models.ts→api/anthropic-messages.ts。 src/api/transform-messages.ts— 跨 provider 归一化(重点)。src/auth/credential-store.ts+src/auth/oauth/anthropic.ts— 凭据与 OAuth 流程。
8. 自测
- 给
claude-sonnet的一条带图片的消息,发给不支持图片的模型时transformMessages做了什么? - 为什么
models.generated.ts不能直接编辑?改它要走什么路径? - 新增一个自托管的 OpenAI 兼容端点,需要动哪几个文件?