模型调用层:AI SDK 与原生双轨

LLM.stream 如何组装请求、参数与请求头,默认的 Vercel AI SDK 链路与可选的 @opencode-ai/llm 原生链路如何分流,又如何汇合成同一种 LLMEvent 流。

作者 David更新于 第 11 篇(共 38 篇)

处理器只认一种输入:LLMEvent 流。把会话数据变成这条流的,是 packages/opencode/src/session/llm.ts 的 LLM.Service,以及它身后 session/llm/ 目录里的三个适配器。

边界

session/llm/AGENTS.md 开宗明义:

../llm.ts is 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)里几个参数值得注意:

参数设置说明
maxRetriesinput.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 的实现。

本页目录