第 6 章:LLM 与 Provider —— 用 Canonical IR 吸收协议差异¶
1. Provider 差异不只是 URL¶
不同模型提供商可能在以下维度不同:
- chat completions、responses、Anthropic messages 等消息格式;
- reasoning / thinking 的字段和签名;
- tool call 的 JSON 编码、并行语义和结果形状;
- prompt cache、provider metadata、native item ID;
- streaming event 命名、错误结构、usage 计量;
- 认证方式、headers、重试和 endpoint。
如果 Session 直接拼 provider request,任何新 provider 都会污染 Session、Tool 和 UI。
2. @opencode-ai/llm 的中间表示¶
packages/llm/src/schema/messages.ts 定义了核心 IR:
LLMRequest
├─ model
├─ system: SystemPart[]
├─ messages: Message[]
├─ tools: ToolDefinition[]
├─ toolChoice?
├─ generation?
├─ providerOptions?
├─ responseFormat?
└─ metadata?
Message 的 content 又可以是 text、media、tool-call、tool-result、reasoning。它不是 OpenCode 数据库里的 Session Message,而是“准备发送给模型 / 从模型返回”的协议中间层。
3. 两条模型运行路径¶
当前源码同时保留两条路径:
flowchart LR
A[Session Runner / legacy SessionPrompt] --> B[LLM request preparation]
B --> C{experimental native LLM?}
C -->|supported| D[@opencode-ai/llm native route]
C -->|unsupported / off| E[AI SDK streamText]
D --> F[LLMEvent]
E --> G[LLMAISDK normalizer]
G --> F
F --> H[Event publisher / message projection]
@opencode-ai/llmnative runtime 直接以 canonical request / event 作为边界,适合需要 provider 原生能力的路径;- 默认 AI SDK 路径用
streamText执行,再由LLMAISDK.toLLMEvents等逻辑归一化 fullStream; packages/opencode/src/session/llm.ts负责认证、配置、provider transform、plugin hook、工具 bridge 和运行路径选择。
这不是重复实现,而是迁移中的 runtime seam:native 支持某 provider 时可以返回标准事件,不支持时明确落回 AI SDK。
4. Model Catalog 与选择¶
模型选择不是简单 providerID + modelID 字符串拼接。SessionRunnerModel 会结合:
- Session 选择的 provider / model / variant;
- Agent 默认模型或 small model;
- Catalog 中的上下文窗口、输出限制、capabilities;
- Provider plugin / auth / credential;
- 当前 Location 的配置。
最后才把 provider-neutral 的 generation controls、provider options 和 native continuation metadata 写进 LLMRequest。
5. Native continuation metadata¶
某些 provider 会返回下一轮继续请求所必需的 opaque metadata,例如 reasoning signature 或 provider-hosted item identifier。OpenCode 不把这些字段扁平化成“普通文本”,而是把它们挂在 canonical message/content 上,并在 projection 中保留。
这样做的理由是:一旦把 native metadata 丢掉,下一轮请求可能格式合法但语义无效;而把 provider 私有结构直接扩散到 UI 又会破坏边界。
6. Tool call 的两类执行者¶
模型发出的工具调用可能由 OpenCode 本地执行,也可能由 provider / workflow 模型在服务端执行。canonical ToolCallPart / ToolResultPart 需要用 providerExecuted 等 metadata 区分两类情况。这样 Session Runner 只等待本地 tool fibers,不会误把 provider 已完成的工具再执行一次。
7. 错误为什么要进入 LLMEvent¶
Provider error 如果直接 throw 出 SessionPrompt,系统会丢掉“模型已经输出了什么、哪个 tool 已经完成、usage 是多少”的上下文。归一化成 LLMEvent 后:
- publisher 可以先写入 assistant/provider error;
- Session 可以判断是否是 context overflow 并触发 compact;
- UI 可以得到可读的 provider 错误;
- retry / continuation 可以根据事件状态决策。
8. 新增 Provider 的正确路径¶
从源码和上游 CONTRIBUTING.md 可以抽出稳定流程:
- 优先在模型目录 / provider catalog 中描述模型能力;
- 如果是已有协议,复用对应 route adapter / transform;
- 只在 provider 真的有独特 wire semantics 时新增 adapter;
- 把认证、headers、provider options、错误和 usage 映射集中在 provider 层;
- 验证 canonical events 在 Session / UI 中仍然成立。
本章小结¶
LLM 层做的是协议翻译,不是 Agent orchestration。Session Runner 只生成一个具有 system/history/tools 的 canonical request,并消费统一事件;Provider 层负责把“很多方言”翻译成这个中间表示。