OpenCode 下一代 LLM 库设计深度解读:`@opencode-ai/ai` 的 API 分层、Run/Turn 语义与干净断裂迁移
本文基于 OpenCode 仓库中的 DESIGN.md 设计稿展开,完整解读即将取代私有包 @opencode-ai/llm 的新 API 方案 @opencode-ai/ai:四层渐进式披露的 API 结构、Model Run 与 Provider 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.md 和 src/llm.ts 的实现一致——也就是说,新设计是对这套真实代码的重写蓝图,而非空中楼阁。
二、设计目标与非目标
八条设计目标(Goals):
- 让一次有用的模型调用需要的代码极少;
- 默认行为足够好,使大多数调用者无需配置;
- 允许高级调用者检查、转换或替换每一个关键阶段;
- 把 provider 怪癖关在 provider 与 protocol 边界之后;
- 为持久化运行时保留“单个 provider turn”这一显式原语;
- 把可序列化请求数据与进程本地执行行为分离;
- 让不支持的组合在本地失败,并给出有用的类型化错误;
- 保持 Effect 原生,但不把包专属的服务供给(service provisioning)塞进每个调用点。
非目标(Non-goals)同样重要,划清了本库不做的事:全局 provider/model 注册表、持久化 Agent 编排、会话历史所有权、权限处理、成本计费保证、运行时的模型目录网络请求、与现有私有 API 的兼容、以及现在去设计 embeddings/图像/语音。
三、四条设计原则
3.1 渐进式披露(Progressive disclosure)
API 分为四层,正常文档只教第一层:
- 运行一个模型:
LLM.generate或LLM.stream; - 控制单个 provider turn:
LLM.generateTurn或LLM.streamTurn; - 定制执行:模型默认值、调用选项、hooks、provider 配置;
- 编写 provider:实验性的 provider 定义与 protocol。
3.2 值优于注册表(Values over registries)
Provider 定义、已配置 provider、模型、protocol、工具、hooks 全部是不可变值。导入一个 provider 不会向任何全局注册表登记任何东西。
3.3 可移植数据,本地行为
Request、消息、工具定义、事件、用量与结果投影是带 schema 的纯不可变数据(可序列化);而配置好的模型、可执行工具、hooks、provider 定义可以包含函数与 Effect 依赖(requirements),不可序列化。这条原则贯穿全文,是理解 Tool.definition 与 Tool.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.ts 中 Route 接口)的封装升级:新设计把“选模型”这一步收敛为 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.request 加 Tool.toDefinitions、收集事件流找 toolCall、检查 providerExecuted、手工 ToolRuntime.dispatch、LLM.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 RungenerateTurn/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 选择“最佳可靠策略”:
- 支持且可靠时,用 provider 原生结构化输出;
- 需要时,强制工具输出作为兼容回退;
- 两者皆无时,在网络执行之前以类型化的“不支持能力”失败。
高级调用者可在精确 provider 语义重要时覆盖策略。而当前实现的做法见 src/llm.ts:LLM.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 存在于五个命名阶段:
- 规范化请求(canonical request)
- provider 原生 body
- 准备好的 transport 请求
- 归一化事件
- 错误
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 逃生舱与请求定制阶梯
请求定制阶梯从稳定到激进依次为:
- 可移植生成控制(generation controls)
- 模型类型化的
provider选项 - 稳定的分阶段 hooks
- 可序列化的 HTTP/body 覆盖层
- 实验性的 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.ts 的 Route 接口已经具备 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 输出事件联合 | 分离的 TurnEvent 与 RunEvent 联合 |
providerExecuted 分派检查 |
独立的托管工具构造器 |
单一包装 LLMError |
带标签的领域错误联合 |
以上每一行“当前”列都能在仓库中找到实证:LLM.updateRequest 与 LLM.generateObject 在 src/llm.ts,ToolRuntime.dispatch 在 src/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)解决:
RunEvent/TurnEvent的确切标签名与载荷;GenerateResult的 text/reasoning/output/messages 快捷字段确切形态;- 强推断所需 Provider 定义的确切 TypeScript 形状;
- protocol
.with(...)打补丁语法与替换语义; - Duration 输入的确切字段与命名;
- models.dev 生成流水线与修正文件格式;
- 成本表示与小数算术策略;
- 默认重试调度与有界工具并发的具体数值;
- 请求级可序列化 HTTP 覆盖层是否进入稳定 schema;
- 哪些带标签错误可序列化、哪些仅限进程本地。
二十七、总结:为什么这套设计值得被关注
回到仓库证据看,这套设计解决的问题非常具体:当前 @opencode-ai/llm 里“名字叫 generate 却只跑一轮”的语义陷阱、LLMClient.layer/RequestExecutor 的运行时供给负担、Route 四件套的心智成本、providerOptions 的嵌套键与 providerExecuted 的布尔检查,都是真实存在于 src/llm.ts、src/route/client.ts 与 AGENTS.md 中的现状。新设计用三个核心抽象收敛了它们:Model 吞掉路由细节、Run 与 Turn 的语义分离同时服务通用开发者与 durable runtime、可移植数据 vs 本地行为 的边界让请求可以放心序列化。配合 20 轮上限、auto 缓存、保守重试、元数据级遥测与“能力错配本地失败”这组强默认值,一次典型调用可以压缩到十来行代码;而 hooks、protocol 打补丁与 provider 编写 API 则为深度定制保留了完整的上升通道。对 OpenCode 自身而言,这套设计也提前钉死了 Session 层与 LLM 层的职责分界——turn API 就是那条边界线。
适用前提提示:本文所有新 API 细节(
@opencode-ai/ai的导入路径、Tool.make/StopWhen等签名)来自 DESIGN.md 的讨论稿,属实现前的示意性契约,以最终发布为准;文中“当前 API”部分均以仓库现有代码为准并已给出对应文件路径。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00