模型调用层:AI SDK 与原生双轨
LLM.stream 如何组装请求、参数与请求头,默认的 Vercel AI SDK 链路与可选的 @opencode-ai/llm 原生链路如何分流,又如何汇合成同一种 LLMEvent 流。
处理器只认一种输入:LLMEvent 流。把会话数据变成这条流的,是 packages/opencode/src/session/llm.ts 的 LLM.Service,以及它身后 session/llm/ 目录里的三个适配器。
边界
session/llm/AGENTS.md 开宗明义:
../llm.tsis the opencode session LLM service. It owns opencode concerns: auth, config, model/provider resolution, plugins, permissions, telemetry headers, and runtime selection.
| 文件 | 职责 |
|---|---|
llm.ts | 会话侧编排:认证、配置、插件、权限、请求头、运行时选择 |
llm/request.ts | 组装 system、合并参数、筛选工具、生成请求头 |
llm/ai-sdk.ts | 把 AI SDK 的 fullStream 事件转成 LLMEvent(默认路径) |
llm/native-request.ts | 把会话数据转换成 @opencode-ai/llm 的 LLMRequest,不负责执行 |
llm/native-runtime.ts | 原生路径的准入判断与工具桥接,交给 LLMClient 发送 |
stream:一次调用一个作用域
llm.ts:357:
const stream: Interface["stream"] = (input) =>
Stream.scoped(
Stream.unwrap(
Effect.gen(function* () {
const ctrl = yield* Effect.acquireRelease(
Effect.sync(() => new AbortController()),
(ctrl) => Effect.sync(() => ctrl.abort()),
)
const result = yield* run({ ...input, abort: ctrl.signal })
if (result.type === "native") return result.stream
// Adapter seam: both runtimes expose the same LLMEvent stream. Native
// already returns one; AI SDK streams are converted here.
const state = LLMAISDK.adapterState()
return Stream.fromAsyncIterable(result.result.fullStream, (e) =>
e instanceof Error ? e : new Error(String(e)),
).pipe(
Stream.mapEffect((event) => LLMAISDK.toLLMEvents(state, event)),
Stream.flatMap((events) => Stream.fromIterable(events)),
)
}),
),
)AbortController 的生命周期绑定在流的作用域上:作用域一关闭(用户中断,或处理器提前停止消费),abort() 就会执行,底层 HTTP 请求随之取消。适配器状态也是每次调用新建一份,互不串扰。
run 与 prepare
run(llm.ts:85)先并行取语言模型实例、配置、提供方信息与认证,再调用 LLMRequestPrep.prepare(llm/request.ts:56)组装请求:
- system:拼装规则见系统提示词。OpenAI 走 OAuth 时,system 改放进
options.instructions,不再作为 system 消息发送。 - 提供方参数:按“
ProviderTransform的默认值 → 模型配置 → agent 配置 → 当前 variant”的顺序深度合并;小模型(如生成标题)改用smallOptions。 - 采样参数:
temperature、topP、topK、maxOutputTokens先取 agent 设置或按模型给出的默认值,再交给插件钩子chat.params修改;请求头同理经过chat.headers。 - 工具:按权限规则与 user 消息的
tools开关过滤后按名字排序。OpenAI、Azure 等 Responses 系提供方给每个工具加strict: false,注释说这是与 Codex 保持一致,让不满足结构化输出约束的 MCP 与动态 schema 也能注册;Copilot 在回放历史工具调用但当前没有工具时,会补一个声明为“永远不要调用”的_noop工具。 - 请求头:OpenCode 自家提供方带
x-opencode-project、x-opencode-session、x-opencode-request、x-opencode-client;其他提供方带x-session-affinity与X-Session-Id;子代理会话再加x-parent-session-id;User-Agent统一为 opencode 加版本号。
之后 run 处理两个特例——GitLab Workflow 模型的工具调用被接回 OpenCode 的工具系统执行,开启 experimental.openTelemetry 时给每个 span 打上 session.id——然后选择运行时。ProviderTransform 本身的规则见提供方适配层。
默认路径:AI SDK
streamText 的调用(llm.ts:280)里几个参数值得注意:
| 参数 | 设置 | 说明 |
|---|---|---|
maxRetries | input.retries ?? 0 | 重试交给会话层的 SessionRetry;主循环不传此值,生成标题时传 2 |
experimental_repairToolCall | 工具名大小写不符就改成小写,否则转给 invalid 工具 | 失败原因作为 invalid 的输入回报给模型 |
activeTools | 排除 invalid | 不向模型主动暴露兜底工具 |
includeRawChunks | 仅 GitHub Copilot 开启 | 注释说 Copilot 的计费数据只出现在原始分块里 |
model | 用 wrapLanguageModel 包一层中间件 | 发送前调用 ProviderTransform.message 按提供方修整消息 |
llm/ai-sdk.ts 的 toLLMEvents 再把 fullStream 逐个映射成 LLMEvent:start-step 变 step-start,finish-step 变 step-finish,文本、推理、工具输入、调用、结果、错误各有对应,abort、source、file 等被丢弃;switch 末尾用 never 做穷尽检查,AI SDK 新增事件类型时编译期就会报错。归一化的细节包括:
- 结束原因不在
FinishReason枚举里就记为unknown; - 推理 token 取自
outputTokenDetails,缓存读写取自inputTokenDetails; - 提供方没给文本或推理块 ID 时,按序合成
text-0、reasoning-0这样的 ID; - 原始结束原因为
network_error时转成可重试的ResponseStreamError; - 从原始分块里抽出 Copilot 的计费数值挂到 step-finish 上,注释称这是临时桥接,等 Copilot 迁到原生运行时再移走。
可选路径:原生 @opencode-ai/llm
llm.ts:224:
// Runtime seam: native is an opt-in adapter over @opencode-ai/llm. It
// either returns a ready LLMEvent stream or a concrete fallback reason.
if (flags.experimentalNativeLlm) {开关是 OPENCODE_EXPERIMENTAL_NATIVE_LLM。llm/AGENTS.md 称总开关 OPENCODE_EXPERIMENTAL 也能打开它,但在 packages/opencode/src/effect/runtime-flags.ts:54 里它是独立的布尔值,并不跟随总开关。
LLMNativeRuntime.status 的准入条件:提供方是 openai、anthropic 或以 opencode 开头;模型包是 @ai-sdk/openai、@ai-sdk/openai-compatible 或 @ai-sdk/anthropic;OAuth 只放行带自定义 fetch 的 OpenAI;必须配置了 API key。不满足就返回具体原因,llm.ts 记下 llm.native_unsupported_reason 后回退到 AI SDK。判定逐请求进行,同一会话里可以有的调用走原生、有的回退。
通过准入后,native-request.ts 把 AI SDK 形状的消息转换成 LLMRequest,并按包名选路由门面(OpenAI.responses、Anthropic.model、OpenAICompatible 等);它是适配层里唯一允许构造 @opencode-ai/llm 请求对象的文件。提供方参数则原样透传,注释的说法是:
Both sides intentionally use OpenAI's official wire field names, so this is identity, not translation
工具执行依然归 OpenCode:native-runtime.ts 把每个 AI SDK 工具包成原生 Tool,其 execute 回调原来的实现;流里出现本地 tool-call 时交给 ToolRuntime.dispatch 执行,结果先进队列,等提供方的流结束后接到流尾。两条路径最终吐出的是同一种 LLMEvent,处理器无从分辨也无需分辨。@opencode-ai/llm 包本身见packages/llm:模型协议层。
下一篇:快照与撤销——基于影子 git 仓库的文件快照,以及 revert 与 unrevert 的实现。