首页
/ OpenCode 下一代 LLM 库设计深度解读:`@opencode-ai/ai` 的 API 分层、Run/Turn 语义与干净断裂迁移

OpenCode 下一代 LLM 库设计深度解读:`@opencode-ai/ai` 的 API 分层、Run/Turn 语义与干净断裂迁移

2026-09-06 12:51:38作者:冯爽妲Honey

本文基于 OpenCode 仓库中的 DESIGN.md 设计稿展开,完整解读即将取代私有包 @opencode-ai/llm 的新 API 方案 @opencode-ai/ai:四层渐进式披露的 API 结构、Model RunProvider Turn 的语义分离、可移植数据与本地执行行为的边界,以及从现有 @opencode-ai/llm 到目标 API 的干净断裂迁移路线。读完本文,你可以准确把握新库的调用方式(generate/stream/generateTurn/streamTurn)、默认行为(20 轮上限、auto 缓存、保守重试等)、与当前实现的逐条差异,并理解其对 Effect 生态的适配方式。需要说明:DESIGN.md 自述为“讨论稿”(Discussion draft),包名与精确 TypeScript 签名在实现前是示意性的,但领域边界与默认值是刻意确定的设计决策

一、状态与定位:一次不留兼容别名的大重写

设计稿明确给出的元信息如下(继承自文档 Status 一节):

  • 拟议包名@opencode-ai/ai(注意:不是 @opencode-ai/llm
  • 首批稳定域LLM
  • 发布姿态:pre-1.0,但具有“稳定核心”意图
  • 迁移姿态:干净断裂(clean break),不保留任何兼容别名
  • 主要受众:使用 Effect 的通用 TypeScript 开发者
  • 次要受众:OpenCode 及其他持久化(durable)Agent 运行时

包名特意留出扩展空间,未来可能承载 embeddings、images、speech 等域——但设计稿明确警告:这些域不属于本设计,也不应被硬塞进 LLM 的 run/turn 模型。

对照当前仓库,@opencode-ai/llm 是已实现并私有分发的包:package.json 显示其版本为 1.18.29"private": true,子路径导出覆盖 ./providers/openai./protocols/anthropic-messages 等十余个入口。设计稿中对“Current API”的引用(如强制 LLM.request({ model, ... })LLM.generate 只跑一轮、providerOptions: { openai: ... } 嵌套键)与当前 README.mdsrc/llm.ts 的实现一致——也就是说,新设计是对这套真实代码的重写蓝图,而非空中楼阁。

二、设计目标与非目标

八条设计目标(Goals):

  1. 让一次有用的模型调用需要的代码极少;
  2. 默认行为足够好,使大多数调用者无需配置;
  3. 允许高级调用者检查、转换或替换每一个关键阶段;
  4. 把 provider 怪癖关在 provider 与 protocol 边界之后;
  5. 为持久化运行时保留“单个 provider turn”这一显式原语;
  6. 把可序列化请求数据与进程本地执行行为分离;
  7. 让不支持的组合在本地失败,并给出有用的类型化错误;
  8. 保持 Effect 原生,但不把包专属的服务供给(service provisioning)塞进每个调用点。

非目标(Non-goals)同样重要,划清了本库不做的事:全局 provider/model 注册表、持久化 Agent 编排、会话历史所有权、权限处理、成本计费保证、运行时的模型目录网络请求、与现有私有 API 的兼容、以及现在去设计 embeddings/图像/语音。

三、四条设计原则

3.1 渐进式披露(Progressive disclosure)

API 分为四层,正常文档只教第一层:

  1. 运行一个模型LLM.generateLLM.stream
  2. 控制单个 provider turnLLM.generateTurnLLM.streamTurn
  3. 定制执行:模型默认值、调用选项、hooks、provider 配置;
  4. 编写 provider:实验性的 provider 定义与 protocol。

3.2 值优于注册表(Values over registries)

Provider 定义、已配置 provider、模型、protocol、工具、hooks 全部是不可变值。导入一个 provider 不会向任何全局注册表登记任何东西。

3.3 可移植数据,本地行为

Request、消息、工具定义、事件、用量与结果投影是带 schema 的纯不可变数据(可序列化);而配置好的模型、可执行工具、hooks、provider 定义可以包含函数与 Effect 依赖(requirements),不可序列化。这条原则贯穿全文,是理解 Tool.definitionTool.make 拆分的关键。

3.4 强默认值,显式覆盖

默认值让常见调用正确,且不掩盖行为来源;覆盖按文档化的顺序组合,永远不需要 patch 已安装的依赖。

四、领域模型:九个核心概念

概念 职责
Provider Definition 不可变、声明式的 provider 集成描述;拥有模型选择、选项 schema、目录修正、protocols 与 provider 级 hooks;属于实验性编写 API
Configured Provider provider 定义绑定部署关注点(凭据、endpoint、transport、provider 头)。configure(...) 刻意只管部署,不建立隐藏的生成默认值
Model 从已配置 provider 选出的进程本地可执行模型值:身份、能力、价格元数据、provider 特定选项类型、可复用的请求行为默认值、隐藏执行行为。普通用户不需要学习当前的 Route 复合概念——protocol、endpoint、auth、transport、hooks 全部藏在 Model 背后
Request 可移植、模型无关的调用输入:系统指令、消息、工具定义、生成控制、输出意图、缓存策略、元数据。它不含已配置模型、可执行工具处理器或 hooks
Provider Turn 恰好一次对模型 provider 的请求及其归一化响应;不执行本地工具、不继续对话
Model Run 完整交互:一次或多次 provider turn;run 会执行本地工具、追加其结果,直到模型完成或停止条件命中
TurnResult 恰好一次 provider turn 的结果
GenerateResult 完整 model run 的结果:保留所有 turn、工具活动、聚合用量与估算成本,同时暴露最终输出的快捷字段
Protocol provider 线格式契约:把可移植请求降阶(lower)为 provider 原生 body,把 provider 原生流事件升阶(raise)为归一化 turn 事件。Protocol 公开、可复用、完全可检查、不可变可打补丁,但整体编写 API 是实验性的

对照当前实现,Model 隐藏 Route 的主张正是对现有四件套(Protocol / Endpoint / Auth / Framing 组合成 Route.make,见 AGENTS.md 的 Routes 一节与 src/route/client.tsRoute 接口)的封装升级:新设计把“选模型”这一步收敛为 OpenAI.model("gpt-4.1-mini") 一个动作。

五、Happy Path:新 API 与当前 API 的正面比较

Effect 写法(提案)

import { Effect } from "effect"
import { LLM } from "@opencode-ai/ai"
import { OpenAI } from "@opencode-ai/ai/providers/openai"

// 环境凭据是 provider 默认值。无需 LLMClient 层:
// Effect 直接暴露标准运行时依赖。
const model = OpenAI.model("gpt-4.1-mini")

const program = Effect.gen(function* () {
  const result = yield* LLM.generate({
    model,
    system: "You are concise.",
    prompt: "Explain Effect in one sentence.",
  })

  // `generate` 总是返回 GenerateResult,即使 run 只有一轮。
  console.log(result.text)
  console.log(result.turns.length) // 1
  console.log(result.usage)
  console.log(result.cost) // 估算成本;若任一 turn 无定价则为 undefined
})

当前 API(仓库现状),对应 README.md 中的示例:

// 当前 API:请求里携带可执行的 model/route 值
const request = LLM.request({
  model: OpenAI.configure({ apiKey }).responses("gpt-4o-mini"),
  prompt: "Say hello.",
})
// 当前 API:名字叫 generate,但实际只执行一次 provider turn
const response = yield* LLM.generate(request)
// 且执行还需要 LLMClient.layer 与 RequestExecutor 服务

当前实现里 LLMClient.generate 的定义可从 src/route/client.ts 找到,LLM 命名空间则被 src/llm.ts 导出为 LLM.request/LLM.generate/LLM.updateRequest 等薄构造函数(该文件注释自述“request constructors and convenience helpers”)。设计稿的改动主张是:移除强制的请求对象构造、移除包专属运行时供给、让 generate 名副其实地表示一次完整 run

六、Provider 与模型选择:三层分离

6.1 环境默认值

import { OpenAI } from "@opencode-ai/ai/providers/openai"

// Open 字符串对 models.dev 生成快照中的 ID 提供自动补全,
// 同时继续接受新发布与微调模型 ID
const model = OpenAI.model("gpt-4.1-mini")

6.2 部署配置(configure 只管部署)

const openai = OpenAI.configure({
  apiKey,
  baseURL: "https://gateway.example.com/openai/v1",
  headers: { "x-tenant": "acme" },
})

const model = openai.model("gpt-4.1-mini")

configure(...) 拥有且仅拥有:凭据与认证、Base URL 与部署位置、transport 选择、provider/部署头、其他 provider 特定连接设置。它不拥有 temperature、maxTokens、缓存策略、重试策略、工具、输出 schema、系统指令。

6.3 可复用的模型默认值

const model = OpenAI.model("gpt-4.1-mini", {
  generation: { temperature: 0.2, maxTokens: 2_000 },
  cache: "auto",
  provider: { store: false },
})

第二个参数可以默认请求行为,但不能默认 prompt/历史或可执行工具;调用级值覆盖模型默认值。

provider 特定选项由具体模型推断类型,不再需要 { openai: ... } 嵌套键:

yield* LLM.generate({
  model: OpenAI.model("gpt-4.1-mini"),
  prompt: "Hello",
  provider: { store: false }, // OpenAI 特定选项直接内联、有自动补全
})

动态在 provider 之间做选择的代码,必须先对 model 做类型收窄(narrow)才能使用 provider 特定选项;可移植的生成控制则不需要收窄即可使用。对照当前实现,README.md 的“Provider options”一节展示了被取代的形态:providerOptions: { <provider>: {...} } 这种按 provider 键分桶的选项袋。

七、请求(Requests):内联、可复用与对话历史

内联输入

const result = yield* LLM.generate({
  model,
  system: "You are concise.",
  prompt: "Summarize this pull request.",
  generation: { maxTokens: 500 },
})

可复用的可移植请求

const request = LLM.request({
  system: "You are concise.",
  prompt: "Summarize this pull request.",
  generation: { maxTokens: 500 },
})

// 执行时才绑定进程本地的执行行为
const result = yield* LLM.generate({ model, request })

LLM.request(...) 返回的是纯不可变对象,派生新请求直接用普通对象展开:

const longer = {
  ...request,
  generation: { ...request.generation, maxTokens: 1_000 },
}

没有 LLM.updateRequest(...) 助手,也没有 request Schema 类——这是对当前 src/llm.ts 中真实存在的 updateRequest 的明确废弃。

对话历史

import { Message } from "@opencode-ai/ai"

const request = LLM.request({
  system: "You are concise.",
  messages: [
    Message.user("What is Effect?"),
    Message.assistant("A TypeScript library for typed functional effects."),
    Message.user("Why would I use it?"),
  ],
})

system 与时序消息(chronological messages)分开保留,因为它是初始特权指令;而时间线上的 system 消息表示“历史某一点的指令变更”——当前实现里这一语义已有对应设计:LLMRequest.system 是初始特权 prompt,Message.system(...) 是历史内的时序算子更新,其他路由会将其降级为 <system-update> 包裹的普通用户文本(见 AGENTS.md “Chronological System Updates”一节)。

八、完整 Run:自动本地工具循环

这是新设计对调用者体验改动最大的部分。

import { Effect, Schema } from "effect"
import { LLM, Tool } from "@opencode-ai/ai"

const tools = {
  getWeather: Tool.make({
    description: "Get current weather for a city.",
    parameters: Schema.Struct({ city: Schema.String }),
    success: Schema.Struct({ forecast: Schema.String }),

    // 工具的服务依赖与类型化错误流入 LLM.generate 的
    // Effect environment/error 模型,而不是被抹掉
    execute: ({ city }) => Weather.get(city),

    // 预期的领域失败必须有显式的、模型可见的表现形式
    formatError: (error) => ({
      type: "text",
      text: `Weather lookup failed: ${error.message}`,
    }),
  }),
}

const result = yield* LLM.generate({ model, prompt: "What is the weather in London?", tools })

// 运行时负责:宣告定义、分派调用、记录结果、自动继续 provider turn
console.log(result.text)
console.log(result.turns)
console.log(result.toolExecutions)

默认停止条件等价于 stopWhen: StopWhen.turnCount(20),与 Vercel AI SDK ToolLoopAgent 的默认值一致;达到上限是成功的 stopReason: "max-turns" 结果,而不是 Effect 失败。

自定义停止只接受一个谓词,组合通过显式组合器表达:

const result = yield* LLM.generate({
  model, prompt, tools,
  stopWhen: StopWhen.any(StopWhen.turnCount(8), StopWhen.hasToolCall("finalize")),
})

成功 run 的停止原因是闭集:

type RunStopReason = "completed" | "max-turns" | "stop-condition"

工具并发:同一 turn 发出的独立工具调用以有界、可配置的并发度并发执行;结果按确定性的发出顺序追加。运行时不推断工具调用间的依赖——有依赖的调用必须由模型在分离的 turn 中请求。工具可声明可选超时,但整个 run 的超时的仍然生效。

当前 API 的对照(设计稿“Current API”小节):今天是调用者手工桥接每一层——LLM.requestTool.toDefinitions、收集事件流找 toolCall、检查 providerExecuted、手工 ToolRuntime.dispatchLLM.updateRequest 追加 assistant/tool 消息、再自行调用 provider 重复循环。这条流程与 AGENTS.md “Tool dispatch”一节的描述完全吻合(其中也强调当前包每次 generate/stream 只跑一个 provider turn,且分派器“不流式、不续轮”)。新设计中,该显式流程仍可通过 turn API 复现,但不再是唯一的工具体验。

九、单个 Provider Turn:持久化运行时的显式原语

OpenCode 这类 durable runtime 需要自己拥有持久化、工具结算与续轮,因此使用显式 turn API:

const result = yield* LLM.generateTurn({
  model,
  request,
  // 仅定义。generateTurn 永不分派本地处理器
  tools: {
    getWeather: Tool.definition({
      description: "Get current weather for a city.",
      parameters: WeatherInput,
    }),
  },
})

// 持久化 TurnResult,并在下一轮前做应用自有的工具结算
for (const call of result.toolCalls) {
  // 应用自有的分派与持久化
}

generateTurn/streamTurn 恰好发起一次 provider 请求,永不执行本地工具、永不自动继续。设计稿特别强调这个分离是“承重结构”(load-bearing):

  • generate / stream:完整 Model Run
  • generateTurn / streamTurn:一次 Provider Turn

并且明确要求:OpenCode 应迁移到 generateTurn / streamTurn,保留其持久化的 prompt 准入、持久化、权限、工具结算与续轮边界;不应使用自动 run API 来做 Session 编排。这与 AGENTS.md 列出的仓库内集成点一致:packages/opencode/src/session/llm.ts 决定走 AI SDK 还是原生 route 运行时,native-runtime.ts 正是以“原始 LLMClient.stream(request) + 一个 provider turn 的工具调用桥接”为工作模式——即新设计中 turn API 的精确落点。

十、可移植工具定义与托管工具

可移植请求中只能声明可序列化的定义,可执行处理器在 run 时绑定:

const request = LLM.request({
  prompt: "What is the weather in London?",
  tools: {
    getWeather: Tool.definition({
      description: "Get current weather for a city.",
      parameters: WeatherInput,
    }),
  },
})

const result = yield* LLM.generate({
  model,
  request,
  tools: {
    getWeather: Tool.make({
      description: "Get current weather for a city.",
      parameters: WeatherInput,
      success: WeatherOutput,
      execute: getWeather,
      formatError,
    }),
  },
})

定义与处理器按记录键匹配。首次 provider 调用前,运行时校验每个本地定义都有兼容的可执行绑定;缺失或不兼容会以类型化的工具绑定错误失败。

provider 托管工具是另一种类型化值,调用者无需再检查 providerExecuted 布尔量:

const result = yield* LLM.generate({
  model: OpenAI.model("gpt-4.1"),
  prompt: "Find today's relevant announcements.",
  tools: { search: OpenAI.tool.webSearch({ searchContextSize: "medium" }) },
})

托管工具不假装拥有本地处理器。对照现状,AGENTS.md 详细记载了当前的 providerExecuted 检查机制(Anthropic web_search/code_execution/web_fetch、OpenAI Responses 一系列 *_call 托管工具直接穿透运行时)——新设计用不同的构造器取代了这个布尔检查。

十一、流式:RunEvent 与 TurnEvent 两套事件代数

LLM.stream 返回 Effect Stream<RunEvent, LLMError, Requirements>。Run 事件显式暴露编排边界:

const program = LLM.stream({ model, prompt, tools }).pipe(
  Stream.tap((event) =>
    Effect.sync(() => {
      switch (event.type) {
        case "run-start":
        case "turn-start":
          break
        case "turn-event":
          // 归一化的 text、reasoning、tool-call、usage、finish 事件
          if (event.event.type === "text-delta") {
            process.stdout.write(event.event.text)
          }
          break
        case "tool-start":
        case "tool-finish":
        case "turn-finish":
          break
        case "run-finish":
          // 内含与 LLM.generate 相同完整的 GenerateResult
          console.log(event.result.usage)
          break
      }
    }),
  ),
  Stream.runDrain,
)

事件标签拼写是待定的实现细节,但代数是确定的

  • RunEvent 联合:run/turn/tool 三层生命周期;
  • TurnEvent 联合:归一化的 provider 输出;
  • streamTurn 只发出 TurnEvent
  • 终结性 run 事件携带完整 GenerateResult

外部取消仍是 Effect 中断(interruption),不会伪造一个 interrupted 的“成功”结果。

十二、结构化输出:一个选项而非一个操作

const Weather = Schema.Struct({
  city: Schema.String,
  forecast: Schema.String,
  highCelsius: Schema.Number,
})

const result = yield* LLM.generate({
  model,
  prompt: "Give me today's weather for London.",
  output: Weather,
})

result.output.city // 由 Weather 推断类型

策略由模型声明与 protocol 选择“最佳可靠策略”:

  1. 支持且可靠时,用 provider 原生结构化输出;
  2. 需要时,强制工具输出作为兼容回退;
  3. 两者皆无时,在网络执行之前以类型化的“不支持能力”失败。

高级调用者可在精确 provider 语义重要时覆盖策略。而当前实现的做法见 src/llm.tsLLM.generateObject 恒定强制一个名为 generate_object 的合成工具并 ToolChoice.named 它——设计稿正是为了“不再永久编码一种跨 provider 的变通方案”而将其统一为 output 选项。

十三、模型目录:models.dev 快照与五级优先链

models.dev发布时(release-time)来源,提供模型 ID 建议、能力与模态、上下文/输出限制、价格及其他元数据。包随附一份生成、带版本的快照;正常执行不做任何目录网络请求。

provider 定义可用协议级知识修正生成元数据,优先级为:

models.dev 快照
  < provider 定义修正
  < provider 配置覆盖
  < 模型选择覆盖
  < 调用覆盖

未知模型 ID 只继承所选 protocol 保证的能力基线;除非调用者显式覆盖模型声明,不支持的请求能力会在网络执行之前失败。这一条与“能力错配 = 本地类型化失败”的目标直接呼应。

十四、用量与成本

GenerateResult 跨所有 turn 聚合归一化用量(含 provider 报告的 cache read/write):

result.usage.inputTokens
result.usage.outputTokens
result.usage.cacheReadInputTokens
result.usage.cacheWriteInputTokens

result.cost?.total
result.cost?.currency // e.g. "USD"

成本是估算而非计费保证:若任一 turn 无可靠定价,整个 run 的成本就是“不可用”,而不是部分值或静默归零。每 turn 元数据应保留所使用的目录/定价身份,以便解释估算。

十五、缓存:auto 默认值及其经济学

新设计中提示缓存默认 "auto":库在支持显式缓存的协议上放置协议感知的缓存边界,在 provider 隐式缓存的场景下线上不做任何事;cache: "none" 为显式退出,细粒度缓存策略保留为高级选项。

当前实现中这一机制已有完整落地,可直接作为佐证(README.md “Caching”一节):

  • "auto" 放置三个断点:最后一个工具定义、最后一个 system part、最新用户消息;
  • 最后用户消息边界是承重细节:工具循环里单个用户 turn 会展开为多次 assistant/tool 往返并共享前缀,在该边界缓存让每个 turn 内的 API 调用都能命中;
  • 经济学依据:Anthropic 的 5 分钟缓存写是 1.25× 基准价、读是 0.1×,5 分钟内一次复用即回本;低于最低可缓存 token 阈值的一次性补全会在线上静默 no-op;
  • provider 行为:Anthropic Messages 最多发 3 个 cache_control 标记(4 断点上限),Bedrock Converse 最多 3 个 cachePoint,OpenAI Chat/Responses 与 Gemini 为隐式缓存 no-op;
  • 归一化缓存用量回读进 usage.cacheReadInputTokens / cacheWriteInputTokens

这些正是新设计“协议感知缓存边界”在现库中的对应物。

十六、重试、超时与取消

重试(刻意保守的默认策略):

  • 只重试有界的瞬态传输失败与限流失败;
  • 只在可观察输出之前重试;
  • 工具执行等副作用发生后绝不静默重试;
  • 每次调用可覆盖或禁用;
  • 重试配置仅限调用作用域:provider 与 model 配置不会静默继承自定义重试策略。

超时

yield* LLM.generate({
  model, prompt,
  timeout: "2 minutes",     // 整个 run(含工具)
  turnTimeout: "30 seconds", // 每个 provider turn
  tools,
})

Duration 输入的具体拼写遵循 Effect 约定;单个工具也可声明可选超时。

取消:Effect API 用 fiber 中断;Promise API 用 AbortSignal 并以可识别的 abort 错误拒绝;取消不是成功的 run 停止原因。

十七、Hooks:五个命名阶段与作用域组合

稳定的高层 hooks 存在于五个命名阶段:

  1. 规范化请求(canonical request)
  2. provider 原生 body
  3. 准备好的 transport 请求
  4. 归一化事件
  5. 错误

Hooks 是 Effectful 的:可以转换阶段值或以类型化错误失败;不得暗中短路执行、合成响应、重试或重定向控制流:

const model = OpenAI.model("gpt-4.1", {
  hooks: {
    request: (request) =>
      Effect.succeed({ ...request, metadata: { ...request.metadata, tenant: "acme" } }),
    body: (body, context) => auditBody(body, context),
    transport: (request) => signInternalGatewayRequest(request),
    event: (event) => redactProviderMetadata(event),
    error: (error) => classifyInternalError(error),
  },
})

作用域按以下顺序组合:

provider 定义 hooks -> 模型 hooks -> 调用 hooks

每个 hook 看到的是上一个 hook 的输出;替换需要显式的定义级 patch,而不是“最后写入者获胜”的意外语义。provider 定义 hooks 由 provider 集成方编写,不经过 Provider.configure(...)——后者保持只管部署。

十八、HTTP 逃生舱与请求定制阶梯

请求定制阶梯从稳定到激进依次为:

  1. 可移植生成控制(generation controls)
  2. 模型类型化的 provider 选项
  3. 稳定的分阶段 hooks
  4. 可序列化的 HTTP/body 覆盖层
  5. 实验性的 provider 定义或 protocol 打补丁
yield* LLM.generate({
  model, prompt,
  http: {
    headers: { "x-experimental": "1" },
    query: { debug: "true" },
    body: { newlyReleasedProviderField: true },
  },
})

原始覆盖层(raw overlays)是刻意为“provider 特性先于库的类型化选项上线”准备的最后手段。当前实现中 http: { body, headers, query } 已是第三级逃生舱(README.md),新设计在其上再叠两级更受控的路径。

十九、Provider 原生元数据与错误模型

providerMetadata:归一化的 message/content/event 联合保持闭集且穷尽;未知或 provider 要求的往返数据放在调用者可写的 providerMetadata 中:

const assistant = Message.assistant([
  {
    type: "reasoning",
    text: "...",
    providerMetadata: { openai: { /* 回放或续轮所需的 provider 不透明数据 */ } },
  },
])

protocol 校验自己消费的 metadata;该字段是逃生舱,不是可移植的语义保证。

错误模型:Effect 错误通道是一个带标签的领域联合,而不是一个包了嵌套 reason 的 LLMError。示意分类:

type LLMError =
  | AuthenticationError
  | InvalidRequestError
  | UnsupportedCapabilityError
  | ToolBindingError
  | TransportError
  | ProviderResponseError
  | InvalidProviderOutputError
  | HookError

每个错误保留相关的 provider/model/turn/stage 上下文及其底层 cause(可得时)。预期工具错误保留自己的类型化错误通道:Tool.make 要求在预期错误变为模型可见的工具结果之前显式映射;映射过的失败让模型自愈,缺陷与中断则使整个 run 失败。

二十、可观测性:默认只记元数据

核心库为模型 run、provider turn、provider 请求、重试、工具执行发出 Effect 原生 span 与 metric。默认遥测只记元数据:provider 与模型身份、耗时、token/缓存用量、估算成本可得性、finish 与停止原因、重试次数、工具名。prompt、模型输出、工具参数与工具结果默认永不记录;显式 hooks 或遥测配置才可选择性开启内容捕获。

二十一、Promise API 与 Schema 子路径

Promise 包装放在独立子路径,使根路径保持无歧义的 Effect 优先:

import { LLM } from "@opencode-ai/ai/promise"
import { OpenAI } from "@opencode-ai/ai/providers/openai"

const result = await LLM.generate({
  model: OpenAI.model("gpt-4.1-mini"),
  prompt: "Explain Effect in one sentence.",
  signal: abortController.signal,
})

流式返回 AsyncIterable<RunEvent>

for await (const event of LLM.stream({ model, prompt, signal })) {
  if (event.type === "turn-event" && event.event.type === "text-delta") {
    process.stdout.write(event.event.text)
  }
}

顶层 Promise 函数使用内置服务的默认运行时;自定义 Effect 服务依赖则用配置化 client:

const client = LLM.makeClient({ layer: Layer.mergeAll(WeatherLive, AuditLive) })
const result = await client.generate({ model, prompt, tools })

Promise API 镜像 Effect 语义,不发明不同的 run、错误、停止或取消行为。

Schema 放在专属命名空间,不淹没根导出:

import { LLMSchema } from "@opencode-ai/ai/schema"

const request = yield* Schema.decodeUnknown(LLMSchema.Request)(input)

Schema 只覆盖可序列化的领域值:请求与消息、可移植工具定义、turn/run 事件、可序列化结果投影、用量与成本估算、可序列化的带标签错误、provider 元数据容器。配置好的模型、可执行工具、hooks、provider 定义与 protocol 是进程本地行为,不配假的可序列化 schema

二十二、Provider 编写:声明式定义与协议打补丁

Provider 编写公开但实验性。声明式 provider 定义:

import { Provider, Protocol } from "@opencode-ai/ai/provider"

export const ExampleAI = Provider.define({
  id: "example",
  options: ExampleProviderOptions,
  configure: configureExampleDeployment,
  protocols: { responses: ExampleResponses },
  models: ({ deployment, catalog }) => ({
    model: (id, defaults) =>
      Provider.model({
        id, deployment,
        protocol: ExampleResponses,
        metadata: catalog.model(id),
        defaults,
      }),
  }),
  catalog: generatedExampleCatalog,
  corrections: exampleCatalogCorrections,
  hooks: exampleProviderHooks,
})

确切 builder 字段待实现设计,但必须保持:一个声明式不可变对象、推断 provider 选项类型、支持 .with(...) 打补丁、不向全局注册。内建 provider 导出其不可变定义供高级 fork:

import { OpenAI } from "@opencode-ai/ai/providers/openai"

const PatchedOpenAI = OpenAI.definition.with({
  protocols: {
    responses: OpenAI.protocols.responses.with({
      body: { fromRequest: patchResponsesBody }, // 显式不可变阶段补丁
    }),
  },
})

Protocol 暴露全部原生类型与阶段:provider 原生请求 body 与 schema、transport 帧类型、provider 原生事件与 schema、解析器状态、请求降阶、事件步进、终态检测与最终冲刷。每个阶段都不可变可打补丁——这刻意比“启发创建本包的 AI SDK 集成”更开放:

const PatchedResponses = OpenAIResponses.with({
  body: {
    fromRequest: (request) =>
      OpenAIResponses.body.fromRequest(request).pipe(
        Effect.map((body) => ({ ...body, custom_field: true })),
      ),
  },
  stream: { step: patchResponsesStep },
})

因为 provider 线格式变化频繁,这些类型与打补丁 API 被明确标记为实验性,不享受高层 API 的兼容承诺。从源码结构看,当前实现中 src/route/client.tsRoute 接口已经具备 with: (patch) => Route 打补丁能力,protocol 模块也确实导出 body schema 与 stream.step 状态机(AGENTS.md 的 Folder layout 一节列有 protocols/openai-chat.ts 等文件),新设计是把这些既有接缝正式提升为公开编写 API。

二十三、包表面(Package Surface)

示意导出布局:

@opencode-ai/ai
  LLM
  Message
  Tool
  StopWhen
  稳定领域类型

@opencode-ai/ai/promise
  Promise/AsyncIterable LLM 门面

@opencode-ai/ai/schema
  可序列化领域 schema

@opencode-ai/ai/provider
  实验性 Provider 与 Protocol 编写 API

@opencode-ai/ai/providers/openai
@opencode-ai/ai/providers/anthropic
@opencode-ai/ai/providers/google
...

provider 通过独立子路径导入;根路径不导出所有 provider,也不存在“首选的全 provider barrel”。这延续了现包 package.json 中按 provider/protocol 逐一声明 exports 的格局(如 ./providers/openai./protocols/gemini)。

二十四、默认值总表

关注点 默认值
LLM.generate 语义 完整 Model Run
LLM.generateTurn 语义 恰好一次 Provider Turn
最大轮数 20
达到轮数上限的结果 成功的 max-turns 结果
工具执行 run 中自动
工具并发 并发、有界、结果顺序确定
提示缓存 auto
重试 保守,仅输出前瞬态失败
结构化输出 能力选择的原生或工具策略
能力错配 网络执行前类型化失败
未知模型能力 保守的 protocol 基线
遥测内容 仅元数据
成本 估算聚合值或不可用
取消 中断/拒绝,永非成功完成

二十五、干净断裂迁移:现状到提案的逐条映射

当前(@opencode-ai/llm 提案(@opencode-ai/ai
强制 LLM.request({ model, ... }) 内联调用或无模型的可移植请求
LLM.generate 表示一轮 LLM.generate 表示完整 run
LLMClient.generate/stream 单轮改用 LLM.generateTurn/streamTurn
LLMClient.layer 要求 直接暴露标准 Effect requirements
公开的 Route 心智模型 藏在可执行 Model 背后
Provider.make 结构化助手 实验性声明式 Provider.define
Schema 类作为规范值 纯不可变值 + schema 子路径
LLM.updateRequest 对象展开
常规调用中的 Tool.toDefinitions 命名的可执行工具记录
手工 ToolRuntime.dispatch 循环 run 自动分派;编排走显式 turn API
providerOptions: { openai: ... } 模型类型化的 provider: ...
generateObject generate 上的类型化 output 选项
单一 provider 输出事件联合 分离的 TurnEventRunEvent 联合
providerExecuted 分派检查 独立的托管工具构造器
单一包装 LLMError 带标签的领域错误联合

以上每一行“当前”列都能在仓库中找到实证:LLM.updateRequestLLM.generateObjectsrc/llm.tsToolRuntime.dispatchsrc/tool-runtime.ts(经 src/index.ts 导出),providerExecuted 检查流程记录在 AGENTS.md 的 Tool dispatch 一节,LLMClient 服务层在 src/route/client.ts

迁移落点明确:OpenCode 的 Session 编排应迁移到 generateTurn/streamTurn,保留其持久化 prompt 准入、持久化、权限、工具结算与续轮边界,不应用自动 run API 做 Session 编排。

二十六、遗留的实现级问题

设计稿最后列出十项不重开主要设计的待定实现细节,供实现尖峰(implementation spike)解决:

  1. RunEvent/TurnEvent 的确切标签名与载荷;
  2. GenerateResult 的 text/reasoning/output/messages 快捷字段确切形态;
  3. 强推断所需 Provider 定义的确切 TypeScript 形状;
  4. protocol .with(...) 打补丁语法与替换语义;
  5. Duration 输入的确切字段与命名;
  6. models.dev 生成流水线与修正文件格式;
  7. 成本表示与小数算术策略;
  8. 默认重试调度与有界工具并发的具体数值;
  9. 请求级可序列化 HTTP 覆盖层是否进入稳定 schema;
  10. 哪些带标签错误可序列化、哪些仅限进程本地。

二十七、总结:为什么这套设计值得被关注

回到仓库证据看,这套设计解决的问题非常具体:当前 @opencode-ai/llm 里“名字叫 generate 却只跑一轮”的语义陷阱、LLMClient.layer/RequestExecutor 的运行时供给负担、Route 四件套的心智成本、providerOptions 的嵌套键与 providerExecuted 的布尔检查,都是真实存在于 src/llm.tssrc/route/client.tsAGENTS.md 中的现状。新设计用三个核心抽象收敛了它们:Model 吞掉路由细节、RunTurn 的语义分离同时服务通用开发者与 durable runtime、可移植数据 vs 本地行为 的边界让请求可以放心序列化。配合 20 轮上限、auto 缓存、保守重试、元数据级遥测与“能力错配本地失败”这组强默认值,一次典型调用可以压缩到十来行代码;而 hooks、protocol 打补丁与 provider 编写 API 则为深度定制保留了完整的上升通道。对 OpenCode 自身而言,这套设计也提前钉死了 Session 层与 LLM 层的职责分界——turn API 就是那条边界线。

适用前提提示:本文所有新 API 细节(@opencode-ai/ai 的导入路径、Tool.make/StopWhen 等签名)来自 DESIGN.md 的讨论稿,属实现前的示意性契约,以最终发布为准;文中“当前 API”部分均以仓库现有代码为准并已给出对应文件路径。

登录后查看全文
热门项目推荐
相关项目推荐