跳转至

第 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/llm native 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 可以抽出稳定流程:

  1. 优先在模型目录 / provider catalog 中描述模型能力;
  2. 如果是已有协议,复用对应 route adapter / transform;
  3. 只在 provider 真的有独特 wire semantics 时新增 adapter;
  4. 把认证、headers、provider options、错误和 usage 映射集中在 provider 层;
  5. 验证 canonical events 在 Session / UI 中仍然成立。

本章小结

LLM 层做的是协议翻译,不是 Agent orchestration。Session Runner 只生成一个具有 system/history/tools 的 canonical request,并消费统一事件;Provider 层负责把“很多方言”翻译成这个中间表示。

源码锚点