第五章:Provider 适配器内部 -- 以 Anthropic 为例
对应源文件:
packages/ai/src/providers/anthropic.ts
一个 provider 适配器要做什么
每个 provider 适配器本质上是一个"翻译器",需要完成两个方向的翻译:
发送方向:pi-ai 统一类型 → provider 原生 API 格式
接收方向:provider 原生响应 → pi-ai 统一事件序列
具体来说,一个适配器需要实现以下步骤:
第一步:消息转换(Outbound)
把 pi-ai 的 Context(systemPrompt + messages + tools)转换为 provider 特定的请求格式。
例如,Anthropic API 要求:
- system prompt 单独放在顶层
system字段 - messages 数组只包含 user 和 assistant 消息
- tool results 嵌套在 user 消息内部
- thinking 块需要特殊的 content block 格式
- 工具定义放在顶层
tools数组
而 OpenAI API 要求:
- system prompt 是 messages 数组中 role 为
system(或developer)的消息 - tool results 是独立的
role: "tool"消息 - 工具定义放在顶层
tools数组,schema 包在function.parameters里
每个适配器都有大量的"格式转换"代码来处理这些差异。
第二步:发送 HTTP 请求
大多数 LLM API 使用 SSE(Server-Sent Events) 协议进行流式传输。SSE 是 HTTP 之上的一种简单协议:
HTTP/1.1 200 OK
Content-Type: text/event-stream
event: message_start
data: {"type":"message_start","message":{...}}
event: content_block_delta
data: {"type":"content_block_delta","delta":{"text":"你好"}}
event: content_block_delta
data: {"type":"content_block_delta","delta":{"text":"世界"}}
event: message_stop
data: {"type":"message_stop"}
每行以 data: 开头,后面是一个 JSON 对象。事件之间用空行分隔。
第三步:解析响应(Inbound)
把 provider 返回的原生事件翻译成 pi-ai 的统一 AssistantMessageEvent 序列。
例如,Anthropic 返回 content_block_delta 类型包含 {"text": "你好"} 时,适配器需要把它翻译成 pi-ai 的 text_delta 事件。
第四步:计算使用量
从响应中提取 token 使用信息(input tokens、output tokens、cache tokens),结合 Model 的定价信息,计算出费用。
Provider 适配器的共同模式
虽然每个 provider 的 API 格式不同,但适配器的代码结构都遵循类似的模式:
导出两个函数:
streamXxx() -- 接受 provider 特定选项
streamSimpleXxx() -- 接受统一简化选项,内部映射为特定选项
内部流程:
1. 转换消息(统一格式 → 原生格式)
2. 构建请求体
3. 发送流式 HTTP 请求
4. 逐块解析响应
5. 生成统一事件序列
6. push 到 EventStream
7. 最终生成 AssistantMessage 并以 done/error 事件结束
streamSimple 如何映射到 stream
streamSimple 是给调用者用的"简化版"。它内部做的事情是把统一的 reasoning 参数映射为各 provider 的特有参数:
| 统一参数 | Anthropic 映射 | OpenAI 映射 | Google 映射 |
|---|---|---|---|
reasoning: "off" | thinkingEnabled: false | 不传 reasoningEffort | thinking.enabled: false |
reasoning: "medium" | thinkingEnabled: true, thinkingBudgetTokens: 8192 | reasoningEffort: "medium" | thinking.budgetTokens: 8192 |
reasoning: "high" | thinkingEnabled: true, thinkingBudgetTokens: 16384 | reasoningEffort: "high" | thinking.budgetTokens: 16384 |
每个 provider 对"思考"的控制方式不同:
- Anthropic:通过 token 预算(
thinkingBudgetTokens)控制思考长度 - OpenAI:通过
reasoningEffort字符串级别控制 - Google:通过
thinking.budgetTokens控制
streamSimple 把这些差异隐藏起来了。
兼容性处理(Compat)
Model 类型有一个可选的 compat 字段,用于处理各种 API 兼容性差异。这在 OpenAI 兼容 API 中尤其复杂。
为什么需要 compat
很多 provider(xAI、Groq、DeepSeek、OpenRouter 等)声称"兼容 OpenAI API",但实际上总有一些微小差异:
- 有的不支持
store字段 - 有的不支持
developer角色(只接受system) - 有的不支持在流式响应中返回 usage 信息
- 有的 max tokens 字段叫
max_tokens而不是max_completion_tokens - 有的 thinking 参数格式完全不同
compat 字段让每个模型可以声明自己的 API 有哪些"偏差",适配器会据此调整请求格式。
例如:
compat: {
supportsStore: false, // 不支持 store
supportsDeveloperRole: false, // 不支持 developer 角色
thinkingFormat: "deepseek", // 使用 DeepSeek 特有的思考格式
requiresToolResultName: true, // 工具结果必须带 name 字段
}
这避免了为每个"稍有不同"的 provider 写一个全新的适配器。相反,一个 openai-completions 适配器通过 compat 配置就能适配十几个不同的兼容 provider。
Faux Provider -- 测试利器
packages/ai/src/providers/faux.ts 是一个特殊的"假 provider",用于测试:
- 不发送任何网络请求
- 你预设好 AI 应该返回什么,它就返回什么
- 完整模拟流式事件序列(包括逐 token 的 delta 事件)
- 可以控制输出速度(
tokensPerSecond)
const reg = registerFauxProvider();
reg.setResponses([
fauxAssistantMessage([
fauxThinking("我在思考..."),
fauxText("这是回答"),
fauxToolCall("read", { path: "foo.ts" })
], { stopReason: "toolUse" })
]);
const model = reg.getModel();
for await (const event of stream(model, context)) {
// 会收到完整的事件序列,就像真的 provider 一样
}
这对于测试 Agent 逻辑很有价值:你可以精确控制 LLM 的"行为",验证你的 Agent 是否正确处理了各种场景,而不需要花真钱调用真实的 LLM API。
小结
Provider 适配器的职责:
统一世界 原生世界
┌─────────────────┐ ┌─────────────────┐
│ Context │ ─── 转换 ───► │ Anthropic 请求 │
│ (systemPrompt, │ │ (system, messages│
│ messages, │ │ tools, ...) │
│ tools) │ │ │
└─────────────────┘ └──────┬────────────┘
│ HTTP SSE
▼
┌─────────────────┐ ┌─────────────────┐
│ AssistantMessage │ ◄── 翻译 ─── │ Anthropic 响应 │
│ Event 序列 │ │ (SSE events) │
│ (text_delta, │ │ │
│ toolcall_end, │ │ │
│ done) │ │ │
└─────────────────┘ └─────────────────┘
核心技巧:
- 懒加载:首次使用时才加载 provider 模块
- Compat 配置:用配置而非代码处理 API 差异
- streamSimple 映射:统一参数到各 provider 特有参数
- 消息转换:处理跨 provider 的格式差异(思考块、工具 ID、图片)