不同模型 API 怎样被压到同一接口
从 ProviderStreams、provider factory 和 createProvider 分派,追踪共享消息清洗与供应商适配器如何共同维持统一的 Context 输入和事件流输出。
Pi 所说的统一接口很窄:每个 API 实现模块都交出 stream(model, context, options) 与 streamSimple(model, context, options),返回 AssistantMessageEventStream。它统一的是调用和消费协议,并没有要求 Anthropic Messages、OpenAI Responses 与 Google Generative AI 使用相同 payload。
统一的是形状,不是厂商语义
类型系统也保留了这条缝:已知 Api 会映射到相应的 provider-specific options,自定义 API 字符串才退回通用字段集合。于是统一接口可以被 registry 当作值保存和延迟加载,直接使用具体 adapter 的代码仍能得到更精确的编译期检查。这里追求的是可组合,不是把所有后端能力压成最小公分母。
provider factory 负责把模型列表、base URL、认证策略和 API 实现装在一起。OpenAI 绑定 openAIResponsesApi(),Anthropic 绑定 anthropicMessagesApi() 并同时声明 key 与 OAuth,Google 则绑定 googleGenerativeAIApi()。这些静态绑定决定默认行为;第 7 章的 runtime overlay 可以在此基础上组合或替换 provider。
flowchart LR
accTitle: Provider 归一化的双向过程
accDescr: 通用消息先修复跨模型历史,再编码成厂商请求;原始响应反向映射成统一的 assistant 事件
C["Context + options"] --> T["共享历史归一化"]
T --> D{"model.api"}
D --> OA["OpenAI payload"]
D --> AN["Anthropic payload"]
D --> GG["Google payload"]
OA --> R["供应商原始流"]
AN --> R
GG --> R
R --> E["AssistantMessageEventStream"]
一个 provider 也可能包含多种 API。createProvider() 接受单一 ProviderStreams,也接受按 API 字符串索引的映射;请求时用 model.api 分派。找不到实现不会同步炸出调用栈,而是建立一个错误流。目录中的 api 字段因此是运行时路由键,不只是展示标签。
分派之后,适配器仍拥有大量不可共享的工作:system prompt 放在哪个字段、工具 schema 如何编码、缓存标记挂在哪条消息、推理 token 如何声明、usage 与 stop reason 从哪个终态读取。第 9、10 章看到的 OpenAI 实现只是其中一条路径。上层只依赖 Context 和事件协议,因此换模型时不必改 Agent loop;适配器则必须对自己的 payload 与原始事件负责。
历史先做跨模型清洗
编码 payload 前还有一层容易漏掉的共享清洗。transformMessages() 会为不支持视觉的模型把图片降级成占位文本;跨模型时丢弃不可移植的加密 thinking、把普通 thinking 变成文本、删除私有 thought signature,并规范化工具调用 id。第二遍扫描会跳过错误或中止的 assistant turn,为孤立工具调用补一条明确失败的 synthetic result。这样修的是会话重放约束,不是替 provider 猜答案。
这种转换有意不是无损的。同一模型重放时,带签名的 thinking 可以保留,因为目标 API 能验证它;跨模型后,密文和私有 signature 没有可解释语义,只能删除或退化为普通文本。synthetic tool result 也不会伪造工具输出,它写入 isError: true 和固定缺失提示,只为让后续 provider 看到结构闭合、事实仍诚实的历史。
对应的单元测试用 OpenAI 来源消息切到 Anthropic 目标,断言 thinking 退化、thought signature 消失、长工具 id 被规范化且只为真正缺失的调用补结果。这种纯转换测试比真实跨 provider 对话更适合证明边界:它固定输入输出,不依赖账号、模型漂移或服务端行为。
下面的对照不会加载任何 provider 模块,只读取固定 tag:
repo="${PI_SOURCE_DIR:-/tmp/pi-handbook-qbQTcA}"
for file in openai anthropic google; do
git -C "$repo" show "v0.83.0:packages/ai/src/providers/${file}.ts" |
rg -n 'createProvider|id:|auth:|models:|api:'
done
git -C "$repo" show v0.83.0:packages/ai/src/api/transform-messages.ts |
nl -ba | sed -n '59,76p;92,160p;182,222p'
依赖已安装时,可在 packages/ai 下运行 node ../../node_modules/vitest/dist/cli.js --run test/transform-messages-copilot-openai-to-anthropic.test.ts。测试固定覆盖 thinking 降级、signature 移除和孤立工具结果补齐,不依赖真实 API。统一层解释了响应形状,最后还要处理失败之后是否再发一次、何时改走压缩,以及取消如何打断等待。