模型与 Provider¶
@earendil-works/pi-ai 把不同模型供应商收敛到统一的 Model + Context + AssistantMessageEventStream。这让 agent loop 不需要关心 OpenAI、Anthropic、Google、Bedrock 等 API 的细节。
本章涉及的类型、registry、lazy provider、compat、OAuth 和 ModelRegistry 行号见源码索引。
关键源码:
packages/ai/src/types.tspackages/ai/src/stream.tspackages/ai/src/api-registry.tspackages/ai/src/providers/register-builtins.tspackages/coding-agent/src/core/model-registry.tspackages/coding-agent/src/core/sdk.ts
统一模型接口¶
Model 描述模型本身:
idnameapiproviderbaseUrlreasoningcontextWindowmaxTokenscostcompat
Context 描述一次请求:
interface Context {
systemPrompt?: string;
messages: Message[];
tools?: Tool[];
}
Message 只包含三类:
UserMessageAssistantMessageToolResultMessage
应用层的自定义消息必须在调用 provider 前转成这些标准消息。
Provider Registry¶
api-registry.ts 维护 api -> provider 映射。
Provider 要实现:
stream(model, context, options)streamSimple(model, context, options)
注册时会包一层校验:如果传入的 model.api 和 provider 声明的 api 不一致,直接报错。这能早发现模型配置错误。
streamSimple()¶
stream.ts 是上层真正调用的入口:
- 根据
model.api从 registry 找 provider。 - 如果 options 没显式
apiKey,尝试从环境变量读取。 - 调用 provider 的
streamSimple()。
completeSimple() 只是对 streamSimple() 的封装:等待 stream result。
Agent loop 只依赖这个统一接口。
AssistantMessageEventStream¶
Provider 的输出不是直接返回完整文本,而是统一事件流:
starttext_start/text_delta/text_endthinking_start/thinking_delta/thinking_endtoolcall_start/toolcall_delta/toolcall_enddoneerror
每个事件都携带 partial 或 final AssistantMessage。这使得 UI、日志、agent state 可以用同一套事件协议处理文本、thinking 和 tool call。
Lazy Provider¶
providers/register-builtins.ts 用 lazy-load 注册内置 provider。第一次请求某个 API 时才动态 import 对应模块,再注册真实 provider。
好处:
- 启动更快。
- 减少不相关 provider 的运行时成本。
- provider 加载失败也能转成统一 assistant error message,而不是让外层流程崩掉。
Provider 兼容层¶
types.ts 中有大量 compat 配置,例如:
- OpenAI completions 是否支持
developerrole。 - tool result 是否需要 name。
- thinking/reasoning 参数格式。
- prompt cache control 格式。
- session affinity header。
- Anthropic eager tool input streaming。
这些字段体现了一个现实:即使都号称 OpenAI-compatible,不同平台也会有细碎差异。Pi 把差异显式建模在 compat 层,而不是散落在 agent loop。
Coding Agent 的模型管理¶
createAgentSession() 会从多个来源决定模型:
- 如果 session 已有消息,优先从 session 中恢复 model。
- 如果恢复失败,用 settings/default provider/default model。
- 如果仍没有,就从可用模型中找初始模型。
- thinking level 会根据模型能力 clamp。
AgentSession 后续支持:
setModel()cycleModel()- scoped models
setThinkingLevel()cycleThinkingLevel()
模型切换会写入 session entry,也会写入 settings 作为默认值。
认证与请求头¶
sdk.ts 里的 streamFn 会:
- 通过
ModelRegistry.getApiKeyAndHeaders(model)获取认证。 - 合并 env、timeout、retry、websocket connect timeout。
- 合并 provider attribution headers。
- 把
sessionId传给 provider,用于支持缓存 affinity。
扩展还可以通过 provider request hook 修改 payload 或观察 response。
设计启发¶
- 模型适配应集中在 provider 层,agent loop 不应该知道 provider 差异。
- 流协议要把 text、thinking、tool call 都作为一等事件。
- OpenAI-compatible 并不等于真正兼容,compat 配置应该显式化。
- 模型选择、认证、provider 请求参数是产品层职责,不是 agent loop 职责。