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 |
多平台模型适配(消息格式、缓存、推理配置等) |
文档结构
- 大模型调用流程:从调用链到参数详解,再到 streamText 的使用
- 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 是什么?
language 是 LanguageModelV2 类型,代表一个可调用的大模型实例。
// 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 详解
streamText 是 Vercel 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-call → tool-error |
error |
错误信息返回给模型,模型可以重试或换方案 |
| 用户取消 | tool-call → tool-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" │
│ } │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
关键点:
- AI SDK 保证:要么返回
tool-result,要么返回tool-error,不会卡在中间 - 兜底机制:流结束后,代码会检查并清理所有未完成的 tool parts
- 状态一致性:无论成功、失败还是中断,ToolPart 最终都会有一个确定的状态(
completed或error)
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?
- 实时反馈:用户可以立即看到模型生成的内容
- 更好的用户体验:不需要等待完整响应
- 支持长响应:可以处理长时间生成的内容
- 支持中断:用户可以随时取消(通过 abortSignal)
- 工具调用流式化:可以看到工具调用的进度
2.5 多平台模型适配
OpenCode 通过 ProviderTransform 和 AI 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. 构建用户和系统提示词
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
文字版流程图:
┌─────────────────────────────────────────────────────────────────────────────┐
│ 【初始化】 │
├─────────────────────────────────────────────────────────────────────────────┤
│ 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.type 是 AI 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 |
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-delta、tool-call 等) |
@ai-sdk/anthropic 等 |
将特定 API 响应转换为统一格式 |
Processor 的作用:
- 管理助手消息和工具调用
- 调用
LLM.stream()与大模型通信 - 处理流式响应并更新数据库
- 返回处理结果状态

浙公网安备 33010602011771号