OpenCode 调用大模型

OpenCode 调用大模型

1. 概览

本文档详细介绍 OpenCode 如何与大模型进行交互,涵盖调用链路、参数配置、SDK 使用和多平台适配。

核心调用链路

processor.process() → LLM.stream() → streamText() → Provider.getLanguage() → 模型 API

关键组件

组件 位置 职责
LLM.stream src/session/llm.ts 统一的大模型调用入口,整合配置、认证、工具定义
streamText Vercel AI SDK 流式调用大模型,返回统一格式的事件流
ProviderTransform src/provider/transform.ts 多平台模型适配(消息格式、缓存、推理配置等)

文档结构

  1. 大模型调用流程:从调用链到参数详解,再到 streamText 的使用
  2. SessionProcessor:Agent 定义行为配置,Processor 处理流式响应

2. 大模型调用流程

2.1 调用链

processor.process()
    ↓
LLM.stream()
    ↓
streamText() (来自 ai 库)
    ↓
Provider.getLanguage()
    ↓
模型 API 调用

2.2 LLM.stream 函数

位置: src/session/llm.ts:46-260

export async function stream(input: StreamInput) {
  const [language, cfg, provider, auth] = await Promise.all([
    Provider.getLanguage(input.model),
    Config.get(),
    Provider.getProvider(input.model.providerID),
    Auth.get(input.model.providerID),
  ])

  // 构建系统提示词
  const system = []
  system.push([
    ...(input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(input.model)),
    ...input.system,
    ...(input.user.system ? [input.user.system] : []),
  ].filter((x) => x).join("\n"))

  // 合并各种配置选项
  const options = pipe(
    base,
    mergeDeep(input.model.options),
    mergeDeep(input.agent.options),
    mergeDeep(variant),
  )

  // 获取工具列表
  const tools = await resolveTools(input)

  // 调用 Vercel AI SDK
  return streamText({
    temperature: params.temperature,
    topP: params.topP,
    tools,
    toolChoice: input.toolChoice,
    maxOutputTokens,
    abortSignal: input.abort,
    messages: [
      ...system.map((x) => ({ role: "system", content: x })),
      ...input.messages,
    ],
    model: wrapLanguageModel({
      model: language,
      middleware: [/*...*/],
    }),
    // ...
  })
}

2.3 streamText 参数详解

位置: src/session/llm.ts:176-259

2.3.1 完整参数列表

return streamText({
  onError(error) { ... },
  async experimental_repairToolCall(failed) { ... },
  temperature: params.temperature,
  topP: params.topP,
  topK: params.topK,
  providerOptions: ProviderTransform.providerOptions(input.model, params.options),
  activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
  tools,
  toolChoice: input.toolChoice,
  maxOutputTokens,
  abortSignal: input.abort,
  headers: { ... },
  maxRetries: input.retries ?? 0,
  messages: [
    ...system.map((x) => ({ role: "system", content: x })),
    ...input.messages,
  ],
  model: wrapLanguageModel({ model: language, middleware: [...] }),
  experimental_telemetry: { ... },
})

2.3.2 参数说明总览

参数 类型 用途
onError 回调函数 流式请求出错时的处理函数,记录错误日志
experimental_repairToolCall 回调函数 工具调用失败时的修复逻辑
temperature number (0-2) 控制输出随机性,值越高越随机
topP number (0-1) nucleus sampling,限制候选 token 的累积概率
topK number top-k sampling,限制候选 token 的数量
providerOptions object 提供商特定选项(如 Anthropic 缓存配置)
activeTools string[] 当前会话中允许被自动调用的工具列表
tools object 所有可用工具的定义
toolChoice "auto" | "required" | "none" 工具选择策略
maxOutputTokens number 模型输出的最大 token 数
abortSignal AbortSignal 用于中止请求的信号
headers object 自定义 HTTP 请求头
maxRetries number 请求失败时的最大重试次数
messages ModelMessage[] 对话消息历史
model LanguageModel AI SDK 的语言模型实例
experimental_telemetry object OpenTelemetry 遥测配置

2.3.3 关键参数详解

1. temperature / topP / topK(生成控制)

temperature: input.agent.temperature ?? ProviderTransform.temperature(input.model),
topP: input.agent.topP ?? ProviderTransform.topP(input.model),
topK: ProviderTransform.topK(input.model),
参数 范围 说明
temperature 0-2 越高越有创意,0 = 贪婪解码(最确定)
topP 0-1 0.9 表示只考虑累积概率前 90% 的 token
topK 正整数 只考虑概率最高的 K 个 token

2. tools / activeTools / toolChoice(工具调用)

tools,                                                    // 所有可用工具定义
activeTools: Object.keys(tools).filter((x) => x !== "invalid"),  // 允许自动调用的
toolChoice: input.toolChoice,                             // 工具选择策略
参数 说明
tools 工具定义对象,包含 name、description、inputSchema、execute
activeTools 哪些工具可以被模型自动选择调用
toolChoice "auto":模型自己决定;"required":必须调用;"none":禁止调用

3. experimental_repairToolCall(工具修复)

async experimental_repairToolCall(failed) {
  const lower = failed.toolCall.toolName.toLowerCase()
  if (lower !== failed.toolCall.toolName && tools[lower]) {
    // 修复大小写问题:模型调用 "Read" → 修复为 "read"
    return { ...failed.toolCall, toolName: lower }
  }
  // 无法修复,标记为无效工具
  return { ...failed.toolCall, toolName: "invalid" }
}

用途:当模型调用工具名大小写不匹配时,自动修复或标记为无效。

4. model + middleware(模型包装)

model: wrapLanguageModel({
  model: language,
  middleware: [{
    async transformParams(args) {
      if (args.type === "stream") {
        // 在发送前转换消息格式
        args.params.prompt = ProviderTransform.message(args.params.prompt, input.model, options)
      }
      return args.params
    },
  }],
})

三个关键概念

概念 含义
language LanguageModelV2 实例,封装了如何调用特定模型 API
wrapLanguageModel AI SDK 函数,包装语言模型,添加中间件功能
middleware 中间件数组,在请求发送前后对参数进行转换

language 是什么?

languageLanguageModelV2 类型,代表一个可调用的大模型实例。

// llm.ts:60
const [language, cfg, provider, auth] = await Promise.all([
  Provider.getLanguage(input.model),  // ← 获取语言模型实例
  // ...
])

// provider.ts:1171-1196
export async function getLanguage(model: Model): Promise<LanguageModelV2> {
  const sdk = await getSDK(model)  // 获取 SDK(如 @ai-sdk/anthropic)

  // 创建语言模型实例
  const language = sdk.languageModel(model.api.id)  // 如 anthropic("claude-3-opus")
  return language
}

language 的本质

  • 对于 Anthropic:anthropic("claude-3-opus") 返回的对象
  • 对于 OpenAI:openai("gpt-4") 返回的对象
  • 它封装了如何调用特定模型 API 的所有逻辑

wrapLanguageModel 的用途

wrapLanguageModel 是 AI SDK 提供的函数,用于包装语言模型,添加中间件功能:

  • 不改变原始语言模型的能力
  • 在调用前后插入自定义逻辑(中间件)
  • 类似于 Express.js 的中间件概念

middleware 的工作流程

streamText() 调用
    ↓
wrapLanguageModel 拦截
    ↓
middleware.transformParams() 执行
    ↓
修改 args.params(如消息格式转换)
    ↓
返回转换后的参数
    ↓
实际调用模型 API

为什么要用 middleware?

不同模型提供商有不同的消息格式要求。ProviderTransform.message 会:

  • 调整消息格式
  • 添加特定提供商需要的字段
  • 处理特殊情况(如缓存标记)

举例

// 原始消息
{ role: "user", content: "hello" }

// 转换后(针对特定提供商)
{ role: "user", content: [{ type: "text", text: "hello" }], cache_control: { ... } }

类比

  • language = 真实的电话
  • wrapLanguageModel = 电话适配器
  • middleware = 适配器中的信号转换器

5. abortSignal(请求中止)

abortSignal: input.abort,

用途:用户取消请求时(Ctrl+C),通过 AbortSignal 通知 AI SDK 中止流式请求。

6. headers(请求头)

headers: {
  // 1. OpenCode 内部提供商
  ...(input.model.providerID.startsWith("opencode") ? {
    "x-opencode-project": Instance.project.id,
    "x-opencode-session": input.sessionID,
    "x-opencode-request": input.user.id,
    "x-opencode-client": Flag.OPENCODE_CLIENT,
  } :
  // 2. 其他提供商(非 Anthropic)
  input.model.providerID !== "anthropic" ? {
    "User-Agent": `opencode/${Installation.VERSION}`,
  } :
  // 3. Anthropic 官方:不添加额外头
  undefined),

  // 4. 模型配置中的自定义头
  ...input.model.headers,

  // 5. 插件注入的头
  ...headers,
}

headers 的五个来源

来源 说明 添加条件
OpenCode 内部头 项目/会话/请求标识 providerID 以 "opencode" 开头
User-Agent 客户端标识 非 OpenCode 且非 Anthropic
模型配置头 用户自定义头 配置文件中定义
插件注入头 插件动态添加 插件触发时

1. OpenCode 内部提供商请求头

请求头 用途
x-opencode-project 项目 ID 标识当前项目,用于服务端识别和计费
x-opencode-session 会话 ID 标识当前会话,用于追踪和日志关联
x-opencode-request 用户消息 ID 标识当前请求,用于追踪单次请求
x-opencode-client 客户端类型 标识客户端(CLI、VSCode 扩展等)

用途:OpenCode 代理服务器需要这些信息做请求追踪、项目统计、计费管理。

2. User-Agent(第三方提供商)

"User-Agent": `opencode/${Installation.VERSION}`  // 如 "opencode/1.2.3"

用途:让第三方 API(OpenAI、Google 等)知道请求来自 OpenCode,用于统计和问题排查。

3. Anthropic 官方提供商

不添加任何额外的 User-Agent 头,因为 Anthropic SDK 已经有自己的 User-Agent,避免冲突。

4. 模型配置中的自定义头

用户可以在配置文件中为特定模型定义自定义请求头:

{
  "models": {
    "my-custom-model": {
      "providerID": "openai",
      "modelID": "gpt-4",
      "headers": {
        "X-Custom-Header": "custom-value",
        "X-API-Version": "v2"
      }
    }
  }
}

5. 插件注入的头

通过 Plugin.trigger("chat.headers", ...) 由插件动态注入:

// llm.ts:137-149
const { headers } = await Plugin.trigger(
  "chat.headers",
  { sessionID, agent, model, provider, message },
  { headers: {} },  // 默认空对象,插件可以添加
)

用途:插件可以动态添加认证信息、追踪 ID、A/B 测试标记等。

7. experimental_telemetry(遥测)

experimental_telemetry: {
  isEnabled: cfg.experimental?.openTelemetry,
  metadata: {
    userId: cfg.username ?? "unknown",
    sessionId: input.sessionID,
  },
},

用途:OpenTelemetry 遥测配置,用于追踪和监控 API 调用。

2.4 streamText 详解

streamTextVercel AI SDK 的核心函数,用于流式调用大模型 API

2.4.1 streamText 返回值

返回 StreamTextResult 对象:

interface StreamTextResult {
  // 流式事件(统一格式)- 立即可用
  fullStream: AsyncIterable<StreamPart>

  // 完整文本(Promise,流结束后可用)
  text: Promise<string>

  // Token 使用量(Promise,流结束后可用)
  usage: Promise<Usage>

  // 工具调用列表(Promise,流结束后可用)
  toolCalls: Promise<ToolCall[]>

  // 工具结果列表(Promise,流结束后可用)
  toolResults: Promise<ToolResult[]>

  // 是否有工具调用(Promise)
  hasToolCalls: Promise<boolean>

  // 请求 ID
  requestId: Promise<string>
}

2.4.2 ⭐️ fullStream 事件类型

fullStream 是统一格式的事件流,OpenCode 处理的事件类型:

事件类型 含义 OpenCode 处理
start 流开始 设置会话状态为 "busy"
text-start 文本块开始 创建文本 part
text-delta 文本增量 增量更新文本到数据库
text-end 文本块结束 完成文本 part
reasoning-start 推理块开始(如 Claude thinking) 创建推理 part
reasoning-delta 推理增量 增量更新推理文本
reasoning-end 推理块结束 完成推理 part
tool-input-start 工具输入开始 创建工具 part(pending 状态)
tool-input-delta 工具输入增量 -
tool-input-end 工具输入结束 -
tool-call 工具调用完成 执行工具,更新状态为 running
tool-result 工具执行结果 保存工具执行结果
finish-step 步骤完成 保存 usage、finishReason
error 错误 处理错误,记录日志

Skill、Agent、Tool 调用的事件对应

在 OpenCode 中,Skill 和 Agent 的调用本质上都是 Tool 调用,因此全部对应 tool-* 系列事件:

┌─────────────────────────────────────────────────────────────────┐
│                      LLM 决策调用                               │
└─────────────────────────────────────────────────────────────────┘
                              │
        ┌─────────────────────┼─────────────────────┐
        ▼                     ▼                     ▼
   调用 "skill" tool     调用 "task" tool      调用其他 tool
   (加载 skill 内容)     (启动 subagent)      (执行普通工具)
        │                     │                     │
        └─────────────────────┴─────────────────────┘
                              │
                              ▼
                    全部触发 tool-* 事件

事件与调用类型的对应关系

事件类型 Skill 调用 Agent 调用 普通 Tool 调用
tool-input-start toolName = "skill" toolName = "task" toolName = "read"/"bash"/...
tool-input-delta 参数增量流入 参数增量流入 参数增量流入
tool-input-end 参数解析完成 参数解析完成 参数解析完成
tool-call 执行 skill 工具 执行 task 工具 执行对应工具
tool-result 返回 skill 内容 返回 subagent 结果 返回工具执行结果
tool-error skill 加载失败 subagent 执行失败 工具执行失败

调用类型总结

调用类型 触发方式 对应的 Tool Name 本质
Skill LLM 匹配 skill description "skill" 加载预定义的 prompt 模板
Agent LLM 匹配 agent description "task" 创建子 session 运行子 agent
Tool LLM 匹配 tool description "read", "bash", "edit" 执行具体操作

示例:Skill 调用流程

用户: "使用 commit skill"
         │
         ▼
LLM 决定调用 skill tool
         │
         ▼
fullStream 事件:
  tool-input-start  → toolName="skill", 创建 pending 状态的 part
  tool-input-delta  → 参数 {"name": "commit"} 增量流入
  tool-call         → 执行 SkillTool.execute(), 状态变为 running
                      ↓
                 加载 SKILL.md 内容
                      ↓
  tool-result       → 返回 skill 内容, 状态变为 completed
                      output: "<skill_content>...</skill_content>"

示例:Agent (Subagent) 调用流程

用户: "探索 API 端点"
         │
         ▼
LLM 决定调用 task tool
         │
         ▼
fullStream 事件:
  tool-input-start  → toolName="task", 创建 pending 状态的 part
  tool-input-delta  → 参数增量流入
  tool-call         → 执行 TaskTool.execute(), 状态变为 running
                      ↓
                 创建子 Session
                 启动 explore agent
                 子 agent 运行 (有自己的 fullStream)
                      ↓
  tool-result       → 返回 subagent 结果, 状态变为 completed
                      output: "<task_result>探索结果...</task_result>"

关键点:从 fullStream 事件的角度看,skill、agent、tool 的调用完全相同,都是 tool-* 系列事件。区别只在于 toolName 字段的值和执行的具体逻辑。

2.4.2.1 事件流转机制

完整流转架构

事件从大模型 API 到 OpenCode 处理的完整链路:

┌─────────────────────────────────────────────────────────────────────────────┐
│                           大模型 API (如 Anthropic)                          │
│                                                                             │
│  返回 SSE 流:                                                               │
│  event: content_block_start                                                 │
│  data: {"type": "tool_use", "name": "skill", "id": "toolu_01..."}          │
│                                                                             │
│  event: content_block_delta                                                 │
│  data: {"delta": {"type": "input_json_delta", "partial_json": "{\"n"}}     │
│                                                                             │
│  event: content_block_delta                                                 │
│  data: {"delta": {"type": "input_json_delta", "partial_json": "ame\"}}"}}  │
└─────────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                    @ai-sdk/anthropic (Provider SDK)                         │
│                                                                             │
│  将原始 SSE 转换为 AI SDK 统一格式:                                          │
│  { type: "tool-input-start", toolName: "skill", id: "toolu_01..." }        │
│  { type: "tool-input-delta", delta: "{\"n" }                                │
│  { type: "tool-input-delta", delta: "ame\"}}" }                            │
│  { type: "tool-input-end" }                                                 │
└─────────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                         ai 包 (streamText 内部)                              │
│                                                                             │
│  1. 接收统一格式事件                                                         │
│  2. 累积 tool-input-delta 中的 JSON 片段                                    │
│  3. 解析完整参数:{"name": "commit"}                                         │
│  4. 查找 tools["skill"].execute 函数                                        │
│  5. 调用 execute(args, context)                                             │
│  6. 等待执行完成                                                             │
│                                                                             │
│  发出事件:                                                                   │
│  { type: "tool-call", toolCallId: "...", input: {"name": "commit"} }       │
│  { type: "tool-result", toolCallId: "...", output: {...} }                 │
└─────────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                         fullStream (统一事件流)                              │
│                                                                             │
│  for await (const event of stream.fullStream) {                            │
│    // event.type: "tool-input-start" | "tool-call" | "tool-result" | ...   │
│  }                                                                          │
└─────────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                    OpenCode processor.ts (事件处理)                          │
│                                                                             │
│  switch (value.type) {                                                      │
│    case "tool-input-start": → 创建 pending 状态的 ToolPart                  │
│    case "tool-call":        → 更新为 running 状态                           │
│    case "tool-result":      → 更新为 completed 状态,保存结果               │
│  }                                                                          │
└─────────────────────────────────────────────────────────────────────────────┘

关键:streamText 内部的工具执行

streamText 不仅仅是转发事件,它会在内部自动执行工具

// ai 包内部逻辑(简化伪代码)

async function* streamText({ tools, ... }) {
  // 1. 调用 provider SDK 获取原始流
  const providerStream = await languageModel.doStream({ ... })

  let toolCallAccumulator = {}  // 累积工具参数

  for await (const event of providerStream) {
    // 2. 转发原始事件
    yield event

    // 3. 累积工具参数
    if (event.type === "tool-input-delta") {
      toolCallAccumulator[event.id] += event.delta
    }

    // 4. 参数完整后执行工具
    if (event.type === "tool-input-end") {
      const input = JSON.parse(toolCallAccumulator[event.id])

      // 发出 tool-call 事件
      yield { type: "tool-call", toolCallId: event.id, input }

      // 执行工具!!!
      const tool = tools[event.toolName]
      const result = await tool.execute(input, context)

      // 发出 tool-result 事件
      yield { type: "tool-result", toolCallId: event.id, output: result }
    }
  }
}

实际执行时序

时间线 ──────────────────────────────────────────────────────────────────────▶

大模型 API:
  [content_block_start] → [delta: {"na] → [delta: me":"com] → [delta: mit"}]
         │                      │                │                  │
         ▼                      ▼                ▼                  ▼
@ai-sdk/anthropic:
  [tool-input-start] → [tool-input-delta] → [tool-input-delta] → [tool-input-delta]
                                                     │
                                                     ▼ (参数累积完成)
                                              [tool-input-end]
                                                     │
         ┌───────────────────────────────────────────┘
         ▼
ai 包 streamText:
         │
         │  解析完整参数: {"name": "commit"}
         ▼
  [tool-call] ──────────────────────────────────────────────────────────────
         │
         │  调用 tools["skill"].execute({"name": "commit"}, context)
         │       │
         │       ▼
         │  ┌─────────────────────────────────────┐
         │  │  SkillTool.execute() 执行中...       │
         │  │  1. Permission 检查                  │
         │  │  2. 加载 SKILL.md 文件               │
         │  │  3. 返回 skill 内容                  │
         │  └─────────────────────────────────────┘
         │       │
         │       ▼
         ▼
  [tool-result] ────────────────────────────────────────────────────────────
         │
         │  output: { title: "Loaded skill: commit",
         │            output: "<skill_content>...</skill_content>" }
         ▼
OpenCode processor:
         │
         ▼  更新数据库中的 ToolPart 状态
2.4.2.2 异常情况处理

事件流的正常与异常路径

┌─────────────────────────────────────────────────────────────────────────────┐
│                            工具调用事件流                                      │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  tool-input-start  →  创建 ToolPart (status: pending)                        │
│         │                                                                   │
│         ▼                                                                   │
│  tool-input-delta  →  (忽略,AI SDK 内部累积)                                 │
│         │                                                                   │
│         ▼                                                                   │
│  tool-call         →  更新 ToolPart (status: running)                        │
│         │                                                                   │
│         ├─────────────────────────────────────────┐                         │
│         │                                         │                         │
│         ▼                                         ▼                         │
│  ┌──────────────┐                         ┌──────────────┐                  │
│  │  执行成功      │                         │  执行失败     │                  │
│  └──────────────┘                         └──────────────┘                  │
│         │                                         │                         │
│         ▼                                         ▼                         │
│  tool-result      →  status: completed    tool-error  →  status: error      │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

异常场景及处理

场景 事件流 ToolPart 最终状态 处理方式
工具执行异常 tool-calltool-error error 错误信息返回给模型,模型可以重试或换方案
用户取消 tool-calltool-error error 流终止,模型收到 "aborted" 信息
网络中断 流异常终止,catch 捕获 error ("Tool execution aborted") 清理阶段标记未完成 parts
超时 流异常终止 error 同上

processor.ts 中的异常处理代码

// 1. 工具执行失败 → tool-error 事件
case "tool-error": {
  const match = toolcalls[value.toolCallId]
  if (match && match.state.status === "running") {
    await Session.updatePart({
      ...match,
      state: {
        status: "error",              // ← 状态设为 error
        error: value.error.toString(), // ← 错误信息
      },
    })
  }

  // 权限被拒绝时,标记为 blocked
  if (value.error instanceof PermissionNext.RejectedError) {
    blocked = shouldBreak
  }
  break
}

// 2. 流异常终止 → catch 捕获
} catch (e: any) {
  const retry = SessionRetry.retryable(error)
  if (retry !== undefined) {
    attempt++
    await SessionRetry.sleep(delay, input.abort)
    continue  // 重试
  }

  // 不可重试,记录错误
  input.assistantMessage.error = error
}

// 3. 流结束后 → 清理未完成的 tool parts
const p = await MessageV2.parts(input.assistantMessage.id)
for (const part of p) {
  if (part.type === "tool" &&
      part.state.status !== "completed" &&
      part.state.status !== "error") {
    await Session.updatePart({
      ...part,
      state: {
        status: "error",
        error: "Tool execution aborted",  // ← 标记为中断
      },
    })
  }
}

状态一致性保证

┌─────────────────────────────────────────────────────────────────────────────┐
│                          异常情况处理                                        │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  流中断 (网络错误/用户取消/超时)                                             │
│         │                                                                   │
│         ▼                                                                   │
│  catch (e) → 判断是否可重试                                                 │
│         │                                                                   │
│         ├─ 可重试 → 等待延迟 → 重新发起请求                                  │
│         │                                                                   │
│         └─ 不可重试 → 流结束                                                │
│                          │                                                  │
│                          ▼                                                  │
│                   清理阶段                                                  │
│                          │                                                  │
│                          ▼                                                  │
│                   遍历所有 parts:                                           │
│                   if (tool && status 不是 completed/error) {               │
│                     → 更新为 status: "error"                                │
│                     → error: "Tool execution aborted"                      │
│                   }                                                         │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

关键点

  1. AI SDK 保证:要么返回 tool-result,要么返回 tool-error,不会卡在中间
  2. 兜底机制:流结束后,代码会检查并清理所有未完成的 tool parts
  3. 状态一致性:无论成功、失败还是中断,ToolPart 最终都会有一个确定的状态(completederror

2.4.3 streamText 的工作流程

┌─────────────────────────────────────────────────────────────┐
│                     streamText() 调用                        │
├─────────────────────────────────────────────────────────────┤
│  1. 参数处理                                                  │
│     - 合并 system + messages                                 │
│     - 准备工具定义                                            │
│     - 设置生成参数(temperature 等)                           │
├─────────────────────────────────────────────────────────────┤
│  2. 调用 Provider SDK                                        │
│     - 选择对应的 SDK (@ai-sdk/anthropic 等)                   │
│     - 构建请求体(适配提供商格式)                               │
│     - 发起 HTTP 流式请求                                      │
├─────────────────────────────────────────────────────────────┤
│  3. 接收流式响应                                              │
│     - 解析 SSE/流式数据                                       │
│     - 转换为统一格式事件                                       │
│     - 通过 fullStream 暴露                                   │
├─────────────────────────────────────────────────────────────┤
│  4. 返回 StreamTextResult                                    │
│     - fullStream: 立即可用                                    │
│     - text/usage/toolCalls: Promise(流结束后 resolve)       │
└─────────────────────────────────────────────────────────────┘

2.4.4 OpenCode 如何使用 streamText

位置src/session/processor.ts:53-55

const stream = await LLM.stream(streamInput)

for await (const value of stream.fullStream) {
  // 实时处理每个事件
  switch (value.type) {
    case "text-delta":
      // 增量更新文本到数据库
      await Session.updatePartDelta({
        sessionID, messageID, partID,
        field: "text",
        delta: value.text,
      })
    case "tool-call":
      // 执行工具调用,更新状态到数据库
    // ...
  }
}

2.4.5 streamText vs generateText

函数 特点 适用场景
streamText 流式返回,实时获取内容 对话、实时显示、长响应
generateText 等待完成后返回完整结果 单次任务、不需要实时

2.4.6 为什么选择 streamText?

  1. 实时反馈:用户可以立即看到模型生成的内容
  2. 更好的用户体验:不需要等待完整响应
  3. 支持长响应:可以处理长时间生成的内容
  4. 支持中断:用户可以随时取消(通过 abortSignal)
  5. 工具调用流式化:可以看到工具调用的进度

2.5 多平台模型适配

OpenCode 通过 ProviderTransformAI SDK 架构 适配多个平台的模型。

2.5.1 适配架构

┌──────────────────────────────────────────────────────────────────────────┐
│                           OpenCode 统一接口                               │
└──────────────────────────────────────────────────────────────────────────┘
                                    ↓
┌──────────────────────────────────────────────────────────────────────────┐
│                          1. 模型元数据适配                                │
│  - 模型 ID、上下文限制、能力声明、价格                                     │
└──────────────────────────────────────────────────────────────────────────┘
                                    ↓
┌──────────────────────────────────────────────────────────────────────────┐
│                          2. 认证适配                                      │
│  - API Key 格式、认证方式、请求头                                         │
└──────────────────────────────────────────────────────────────────────────┘
                                    ↓
┌──────────────────────────────────────────────────────────────────────────┐
│                          3. 请求适配                                      │
│  - 请求体格式、消息格式、工具定义、生成参数、端点 URL                       │
└──────────────────────────────────────────────────────────────────────────┘
                                    ↓
┌──────────────────────────────────────────────────────────────────────────┐
│                          4. 响应适配                                      │
│  - 响应格式、流式格式、错误码、Token 统计、finish_reason                   │
└──────────────────────────────────────────────────────────────────────────┘
                                    ↓
┌──────────────────────────────────────────────────────────────────────────┐
│                          5. 功能适配                                      │
│  - 工具调用、推理/thinking、缓存、多模态                                   │
└──────────────────────────────────────────────────────────────────────────┘
                                    ↓
┌──────────────────────────────────────────────────────────────────────────┐
│                          6. 行为适配                                      │
│  - 默认参数、错误处理、重试策略、限流                                      │
└──────────────────────────────────────────────────────────────────────────┘

2.5.2 模型元数据适配

位置src/provider/provider.ts:717-737,数据来源:models.dev

适配项 说明 示例差异
模型 ID 不同提供商的命名规则 claude-3-opus vs gpt-4-turbo
上下文限制 输入/输出 token 限制 Claude 200K vs GPT-4 128K
输入类型 支持的输入格式 文本、图像、音频、视频、PDF
输出类型 支持的输出格式 文本、图像
功能支持 工具调用、推理、缓存 toolcall: true/false
温度支持 是否支持 temperature capabilities.temperature
推理支持 是否支持 thinking capabilities.reasoning
// 模型能力定义
capabilities: {
  temperature: boolean,
  reasoning: boolean,
  attachment: boolean,
  toolcall: boolean,
  input: { text, audio, image, video, pdf },
  output: { text, audio, image, video, pdf },
  interleaved: { field: string } | false,
}

2.5.3 认证适配

适配项 说明 示例差异
API Key 格式 Key 的格式和长度 Anthropic sk-ant-... vs OpenAI sk-...
认证方式 Bearer token / OAuth / 自定义 GitHub Copilot 使用 OAuth
认证头位置 Header 名 Authorization vs X-API-Key
额外认证 需要的额外 header anthropic-version

2.5.4 请求适配

消息格式适配src/provider/transform.ts:47-172):

适配项 说明 OpenCode 处理
空消息过滤 Anthropic 拒绝空消息 过滤 content === "" 的消息
toolCallId 格式 ID 字符限制 Claude: a-zA-Z0-9_-,Mistral: 9位字母数字
推理内容位置 thinking 放在哪里 reasoning_content vs reasoning 字段
消息顺序限制 tool 消息后不能跟 user Mistral 需要插入 "Done." 消息
// 不同模型的 toolCallId 适配
if (model.api.id.includes("claude")) {
  toolCallId = toolCallId.replace(/[^a-zA-Z0-9_-]/g, "_")
}
if (model.api.id.includes("mistral")) {
  toolCallId = toolCallId.replace(/[^a-zA-Z0-9]/g, "").substring(0, 9).padEnd(9, "0")
}

生成参数适配src/provider/transform.ts:292-327):

模型 temperature topP topK
qwen 0.55 1 -
claude - - -
gemini 1.0 0.95 64
minimax - 0.95 40

2.5.5 响应适配

错误响应适配src/provider/error.ts:8-39):

// 不同提供商的溢出错误模式
const OVERFLOW_PATTERNS = [
  /prompt is too long/i,                     // Anthropic
  /exceeds the context window/i,             // OpenAI
  /input token count.*exceeds the maximum/i, // Google
  /maximum context length is \d+ tokens/i,   // OpenRouter, DeepSeek
  /context_window_exceeds_limit/i,           // MiniMax
  /exceeded model token limit/i,             // Kimi, Moonshot
]

finish_reason 适配

提供商 正常结束 工具调用 长度限制
Anthropic end_turn tool_use max_tokens
OpenAI stop tool_calls length
统一格式 stop tool-calls length

2.5.6 功能适配

推理/Thinking 配置src/provider/transform.ts:332-811):

export function variants(model: Provider.Model) {
  switch (model.api.npm) {
    // OpenAI
    case "@ai-sdk/openai":
      return {
        low: { reasoningEffort: "low", reasoningSummary: "auto" },
        high: { reasoningEffort: "high", reasoningSummary: "auto" },
      }

    // Anthropic
    case "@ai-sdk/anthropic":
      return {
        high: { thinking: { type: "enabled", budgetTokens: 16000 } },
        max: { thinking: { type: "enabled", budgetTokens: 31999 } },
      }

    // AWS Bedrock
    case "@ai-sdk/amazon-bedrock":
      return {
        high: { reasoningConfig: { type: "enabled", budgetTokens: 16000 } },
        max: { reasoningConfig: { type: "enabled", budgetTokens: 31999 } },
      }

    // Google
    case "@ai-sdk/google":
      return {
        high: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } },
        max: { thinkingConfig: { includeThoughts: true, thinkingBudget: 24576 } },
      }

    // Groq
    case "@ai-sdk/groq":
      return {
        low: { includeThoughts: true, thinkingLevel: "low" },
        high: { includeThoughts: true, thinkingLevel: "high" },
      }
  }
}

缓存配置src/provider/transform.ts:174-212):

const providerOptions = {
  anthropic: { cacheControl: { type: "ephemeral" } },
  bedrock: { cachePoint: { type: "default" } },
  openaiCompatible: { cache_control: { type: "ephemeral" } },
  copilot: { copilot_cache_control: { type: "ephemeral" } },
}

// 放置位置也不同
const useMessageLevelOptions = model.providerID === "anthropic" || model.providerID.includes("bedrock")
const shouldUseContentOptions = !useMessageLevelOptions && Array.isArray(msg.content)

if (shouldUseContentOptions) {
  // 内容级别(OpenRouter 等)
  lastContent.providerOptions = mergeDeep(lastContent.providerOptions, providerOptions)
} else {
  // 消息级别(Anthropic 等)
  msg.providerOptions = mergeDeep(msg.providerOptions, providerOptions)
}

2.5.7 其他重要适配

providerOptions 键名映射src/provider/transform.ts:24-45, 267-287):

// 不同 SDK 期望的键名
function sdkKey(npm: string) {
  switch (npm) {
    case "@ai-sdk/github-copilot": return "copilot"
    case "@ai-sdk/openai": return "openai"
    case "@ai-sdk/amazon-bedrock": return "bedrock"
    case "@ai-sdk/anthropic": return "anthropic"
    case "@ai-sdk/google": return "google"
    case "@ai-sdk/gateway": return "gateway"
    case "@openrouter/ai-sdk-provider": return "openrouter"
  }
}

// 重映射键名:存储的 providerID → SDK 期望的 key
if (key !== model.providerID) {
  msg.providerOptions[key] = msg.providerOptions[model.providerID]
  delete msg.providerOptions[model.providerID]
}

不支持的输入类型处理src/provider/transform.ts:214-250):

// 检查模型是否支持某种输入类型
const mime = part.type === "image" ? part.image.toString().split(";")[0].replace("data:", "") : part.mediaType
const modality = mimeToModality(mime)  // image/audio/video/pdf
if (!model.capabilities.input[modality]) {
  return {
    type: "text",
    text: `ERROR: Cannot read ${name} (this model does not support ${modality} input).`,
  }
}

提供商特定选项src/provider/transform.ts:660-776):

export function options(input: { model, sessionID, providerOptions }) {
  const result = {}

  // OpenAI: 默认禁用存储
  if (model.providerID === "openai" || model.api.npm === "@ai-sdk/openai") {
    result["store"] = false
  }

  // Anthropic: 启用 token 高效工具使用
  if (model.api.npm === "@ai-sdk/anthropic") {
    result["tokenEfficientTools"] = true
  }

  // Gateway: 自动缓存
  if (model.api.npm === "@ai-sdk/gateway") {
    result["gateway"] = { caching: "auto" }
  }

  return result
}

JSON Schema 适配src/provider/transform.ts:858-929):

// Google/Gemini: 整数枚举转字符串枚举
if (model.providerID === "google" || model.api.id.includes("gemini")) {
  const sanitizeGemini = (obj) => {
    if (obj.enum && Array.isArray(obj.enum)) {
      obj.enum = obj.enum.map((v) => String(v))  // 整数转字符串
      if (obj.type === "integer") obj.type = "string"
    }
    // 非对象类型不能有 properties/required(Gemini 拒绝)
    if (obj.type && obj.type !== "object") {
      delete obj.properties
      delete obj.required
    }
    return obj
  }
  schema = sanitizeGemini(schema)
}

2.5.8 适配点总表

层级 适配项 代码位置
模型元数据 ID、限制、能力 provider.ts:717-737
消息格式 空消息、toolCallId、顺序 transform.ts:47-172
缓存 格式、位置 transform.ts:174-212
输入类型 不支持的类型处理 transform.ts:214-250
键名映射 providerID → SDK key transform.ts:24-45, 267-287
生成参数 temperature/topP/topK transform.ts:292-327
推理配置 thinking/reasoning 格式 transform.ts:332-811
提供商选项 store、caching 等 transform.ts:660-776
JSON Schema 整数枚举、字段过滤 transform.ts:858-929
错误响应 溢出检测、错误消息 error.ts:8-80
请求头 User-Agent、认证头 llm.ts:212-227
请求体 JSON 结构 @ai-sdk/* 包内部
响应体 格式转换 @ai-sdk/* 包内部
适配项总计 70+

3. 构建用户和系统提示词

详见OpenCode 构建提示词

4. SessionProcessor (processor.ts)

位置: src/session/processor.ts

Processor 负责处理与大模型的交互:

export function create(input: {
  assistantMessage: MessageV2.Assistant
  sessionID: string
  model: Provider.Model
  abort: AbortSignal
}) {
  const toolcalls: Record<string, MessageV2.ToolPart> = {}

  return {
    get message() { return input.assistantMessage },

    partFromToolCall(toolCallID: string) {
      return toolcalls[toolCallID]
    },

    async process(streamInput: LLM.StreamInput) {
      // 调用 LLM.stream
      const stream = await LLM.stream(streamInput)

      // 处理流式响应
      for await (const value of stream.fullStream) {
        switch (value.type) {
          case "text-start":
          case "text-delta":
          case "text-end":
          case "tool-call":
          case "tool-result":
          // ...
        }
      }

      return "continue" | "stop" | "compact"
    }
  }
}

4.1 processor.process 执行流程图

位置: src/session/processor.ts:45-417

flowchart TD subgraph Init["初始化"] A[process 入口] --> B[needsCompaction = false] B --> C[获取 shouldBreak 配置] end subgraph MainLoop["主循环 while(true)"] C --> D[LLM.stream 调用大模型] D --> E[for await value of fullStream] E --> F{value.type} F -->|start| G[设置状态 busy] F -->|reasoning-start| H[创建 reasoning part] F -->|reasoning-delta| I[更新 reasoning 增量] F -->|reasoning-end| J[完成 reasoning part] F -->|text-start| K[创建 text part] F -->|text-delta| L[更新 text 增量] F -->|text-end| M[完成 text part] F -->|tool-input-start| N[创建 tool part<br/>status: pending] F -->|tool-call| O[更新 tool part<br/>status: running<br/>检测 doom loop] F -->|tool-result| P[更新 tool part<br/>status: completed] F -->|tool-error| Q[更新 tool part<br/>status: error<br/>检查是否 blocked] F -->|error| R[抛出错误] F -->|start-step| S[Snapshot.track] F -->|finish-step| T[更新 tokens/cost<br/>检查 isOverflow] F -->|finish| U[流结束] G --> V{needsCompaction?} H --> V I --> V J --> V K --> V L --> V M --> V N --> V O --> V P --> V Q --> V S --> V T --> V U --> V V -->|是| W[break 退出循环] V -->|否| E end subgraph ErrorHandling["错误处理"] R --> X{可重试?} X -->|是| Y[attempt++<br/>等待延迟<br/>continue] Y --> D X -->|否| Z[设置 error<br/>发布 Error 事件] end subgraph Cleanup["清理阶段"] W --> AA[处理未完成的 tool parts] Z --> AA AA --> AB[更新 message.completed] AB --> AC{返回值判断} end subgraph Return["返回值"] AC -->|needsCompaction| AD[return 'compact'] AC -->|blocked| AE[return 'stop'] AC -->|error| AF[return 'stop'] AC -->|正常| AG[return 'continue'] end style A fill:#e1f5fe style D fill:#fff3e0 style R fill:#ffebee style AD fill:#e8f5e9 style AE fill:#ffebee style AF fill:#ffebee style AG fill:#e8f5e9

文字版流程图:

┌─────────────────────────────────────────────────────────────────────────────┐
│                              【初始化】                                       │
├─────────────────────────────────────────────────────────────────────────────┤
│  process(streamInput)                                                        │
│      │                                                                       │
│      ▼                                                                       │
│  needsCompaction = false                                                     │
│  shouldBreak = 配置项                                                         │
│      │                                                                       │
│      ▼                                                                       │
│  ┌───────────────────────────────────────┐                                   │
│  │         while (true)                  │ ◄─────────────────────────┐      │
│  └───────────────────────────────────────┘                           │      │
│      │                                                               │      │
│      ▼                                                               │      │
│  LLM.stream(streamInput)  ─────────────────► 大模型 API              │      │
│      │                                                               │      │
│      ▼                                                               │      │
│  ┌───────────────────────────────────────┐                           │      │
│  │  for await (value of fullStream)      │ ◄─────────┐               │      │
│  └───────────────────────────────────────┘           │               │      │
│      │                                               │               │      │
│      ▼                                               │               │      │
│  ┌───────────────────────────────────────────────────┤               │      │
│  │              switch (value.type)                  │               │      │
│  └───────────────────────────────────────────────────┤               │      │
│      │                                               │               │      │
│      ├─ start          → 设置状态 busy               │               │      │
│      ├─ reasoning-*    → 处理推理内容                │               │      │
│      ├─ text-*         → 处理文本内容                │               │      │
│      ├─ tool-input-*   → 处理工具输入                │               │      │
│      ├─ tool-call      → 执行工具(检测 doom loop)    │               │      │
│      ├─ tool-result    → 更新工具结果                │               │      │
│      ├─ tool-error     → 更新错误状态(可能 blocked)  │               │      │
│      ├─ start-step     → 跟踪快照                    │               │      │
│      ├─ finish-step    → 更新 tokens/cost            │               │      │
│      │                    检查 isOverflow            │               │      │
│      ├─ finish         → 流结束                      │               │      │
│      └─ error          → 抛出错误 ───────────────────┼──► 异常处理   │      │
│                                                      │               │      │
│      ▼                                               │               │      │
│  ┌───────────────────────┐                           │               │      │
│  │  needsCompaction ?    │──是──► break ─────────────┼──► 清理阶段   │      │
│  └───────────────────────┘                           │               │      │
│      │ 否                                            │               │      │
│      └───────────────────────────────────────────────┘               │      │
│                                                                      │      │
│  异常处理:                                                            │      │
│  ┌───────────────────────┐                                           │      │
│  │  可重试错误 ?          │──是──► 等待延迟 ─────────────────────────┘      │
│  └───────────────────────┘                                                │
│      │ 否                                                                 │
│      ▼                                                                    │
│  设置 error → 发布事件 → 清理阶段                                          │
│                                                                           │
└─────────────────────────────────────────────────────────────────────────────┘
                                     │
                                     ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                              【清理阶段】                                    │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  1. 处理未完成的 tool parts (status 设为 error)                              │
│  2. 更新 message.time.completed                                             │
│  3. Session.updateMessage 保存消息                                          │
│                                                                             │
│  ┌───────────────────────────────────────────────────────────────┐         │
│  │                      返回值判断                                │         │
│  ├───────────────────────────────────────────────────────────────┤         │
│  │  needsCompaction ? ──是──► return "compact"                   │         │
│  │  blocked ?         ──是──► return "stop"                      │         │
│  │  error ?           ──是──► return "stop"                      │         │
│  │  正常              ──►   return "continue"                    │         │
│  └───────────────────────────────────────────────────────────────┘         │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

4.2 fullStream 的事件类型

stream.fullStream 中的 value.typeAI SDK 格式化后的统一结构,不是大模型 API 直接返回的原始格式。

value.type 的可能值

事件类型 含义
start 流开始
reasoning-start 推理开始(如 Claude 的 thinking)
reasoning-delta 推理内容增量
reasoning-end 推理结束
text-start 文本开始
text-delta 文本增量
text-end 文本结束
tool-call 工具调用
tool-result 工具结果
finish-step 步骤完成(包含 usage、finishReason)
error 错误

4.3 AI SDK 的架构和转换

不同模型 API 返回的原始格式不同,AI SDK 将它们统一转换为标准格式:

AI SDK 架构

┌─────────────────────────────────────────────────────────────┐
│                     OpenCode 代码                            │
│  stream.fullStream → value.type = "text-delta", "tool-call" │
└─────────────────────────────────────────────────────────────┘
                              ↑ 统一格式
┌─────────────────────────────────────────────────────────────┐
│                    ai 包 (核心 SDK)                          │
│  streamText() → 返回 StreamTextResult.fullStream            │
└─────────────────────────────────────────────────────────────┘
                              ↑ 调用 doStream()
┌─────────────────────────────────────────────────────────────┐
│                 @ai-sdk/provider (接口定义)                  │
│  LanguageModelV2.doStream() → 返回统一格式的 Stream          │
└─────────────────────────────────────────────────────────────┘
                              ↑ 实现
┌──────────────────┬──────────────────┬───────────────────────┐
│ @ai-sdk/anthropic│ @ai-sdk/openai   │ @ai-sdk/google        │
│                  │                  │                       │
│ Anthropic API    │ OpenAI API       │ Google API            │
│ 原始响应         │ 原始响应         │ 原始响应              │
│      ↓           │      ↓           │      ↓                │
│ 转换为统一格式   │ 转换为统一格式   │ 转换为统一格式        │
└──────────────────┴──────────────────┴───────────────────────┘

不同提供商的原始事件对比

提供商 原始事件类型
Anthropic content_block_start, content_block_delta, message_stop
OpenAI response.created, response.output_item.added
Google generateContentResponse

转换示例@ai-sdk/anthropic 内部实现,伪代码):

// @ai-sdk/anthropic 内部实现
async *doStream({ prompt }) {
  // 1. 调用 Anthropic API
  const response = await anthropic.messages.stream({
    model: "claude-3-opus",
    messages: prompt,
  })

  // 2. 转换为统一格式
  for await (const event of response) {
    switch (event.type) {
      case "content_block_start":
        yield { type: "text-start", id: event.index }
        break
      case "content_block_delta":
        yield { type: "text-delta", delta: event.delta.text }
        break
      case "message_stop":
        yield { type: "finish-step", finishReason: "stop" }
        break
    }
  }
}

项目中的依赖

package.json:
├── ai                      # 核心 SDK,提供 streamText()
├── @ai-sdk/provider        # 接口定义
├── @ai-sdk/anthropic       # Anthropic → 统一格式
├── @ai-sdk/openai          # OpenAI → 统一格式
├── @ai-sdk/google          # Google → 统一格式
├── @ai-sdk/amazon-bedrock  # Bedrock → 统一格式
└── ... 其他 provider

各层职责

层级 职责
ai 提供 streamText()fullStream 接口
@ai-sdk/provider 定义统一的事件类型(text-deltatool-call 等)
@ai-sdk/anthropic 将特定 API 响应转换为统一格式

Processor 的作用:

  1. 管理助手消息和工具调用
  2. 调用 LLM.stream() 与大模型通信
  3. 处理流式响应并更新数据库
  4. 返回处理结果状态
posted @ 2026-02-19 21:55  不歪歪  阅读(157)  评论(0)    收藏  举报