0-1. 学习目标与一句话
Provider / LLM 层是 OpenCode 的模型网关:接收 agent loop 准备好的 messages/tools/model,合并 system prompt、参数、headers,调用 AI SDK 或 native runtime,然后把 provider stream 翻译成统一 LLMEvent。
来源:packages/opencode/src/session/llm.ts:39-60、packages/opencode/src/session/llm.ts:402-493、packages/opencode/src/session/llm/ai-sdk.ts:61-236。
2-5. 它的位置与最小源码路径
源码路径卡片
session/llm.ts:39-60:LLM stream 数据合同。session/llm.ts:99-188:解析 language/config/provider/auth,拼 system 和参数。session/llm.ts:204-225、512-518:过滤 tools。session/llm.ts:330-467:headers、native runtime、AI SDKstreamText。session/llm/ai-sdk.ts:61-236:AI SDK event 到LLMEvent。provider/provider.ts:1508-1703:SDK 动态加载和 language model 缓存。provider/transform.ts:429-474:provider-specific message transform。
6. 用户输入到 agent 行动的整体链路
SessionPrompt.runLoop
-> ModelMessage[] + tools
-> LLM.stream(StreamInput)
-> Provider.getLanguage(model)
-> ProviderTransform.message(...)
-> streamText({ messages, tools, toolChoice, headers })
-> fullStream events
-> LLMAISDK.toLLMEvents
-> SessionProcessor.process
-> message parts and next round
Java 类比
LLM.Service 像 LlmGateway;Provider.Service 像 ProviderRegistry + ClientFactory;ProviderTransform 像 provider-specific HttpMessageConverter;LLMAISDK 像事件 adapter。
7. 核心源码逐段讲解
LLM stream 输入
export type StreamInput = {
user: MessageV2.User
sessionID: string
model: Provider.Model
agent: Agent.Info
permission?: Permission.Ruleset
system: string[]
messages: ModelMessage[]
tools: Record<string, Tool>
retries?: number
toolChoice?: "auto" | "required" | "none"
}
路径:packages/opencode/src/session/llm.ts:39-52。
并发解析 provider 运行所需信息
const [language, cfg, item, info] = yield* Effect.all(
[
provider.getLanguage(input.model),
config.get(),
provider.getProvider(input.model.providerID),
auth.get(input.model.providerID),
],
{ concurrency: "unbounded" },
)
路径:packages/opencode/src/session/llm.ts:99-107。
拼 system prompt
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"),
)
路径:packages/opencode/src/session/llm.ts:112-124。
工具过滤
function resolveTools(input: Pick<StreamInput, "tools" | "agent" | "permission" | "user">) {
const disabled = Permission.disabled(
Object.keys(input.tools),
Permission.merge(input.agent.permission, input.permission ?? []),
)
return Record.filter(input.tools, (_, k) => input.user.tools?.[k] !== false && !disabled.has(k))
}
路径:packages/opencode/src/session/llm.ts:512-518。即使上游准备了工具,LLM 调用前还会按权限和用户开关过滤。
AI SDK streamText
result: streamText({
experimental_repairToolCall(failed) {
const lower = failed.toolCall.toolName.toLowerCase()
if (lower !== failed.toolCall.toolName && sortedTools[lower]) {
return { ...failed.toolCall, toolName: lower }
}
return {
...failed.toolCall,
input: JSON.stringify({ tool: failed.toolCall.toolName, error: failed.error.message }),
toolName: "invalid",
}
},
tools: sortedTools,
toolChoice: input.toolChoice,
abortSignal: input.abort,
headers: requestHeaders,
messages,
model: wrapLanguageModel({ model: language, middleware: [...] }),
})
路径:packages/opencode/src/session/llm.ts:402-467。
Provider SDK 加载
const existing = s.sdk.get(key)
if (existing) return existing
const bundledLoader = BUNDLED_PROVIDERS[model.api.npm]
if (bundledLoader) {
const factory = await bundledLoader()
const loaded = factory({
name: model.providerID,
...options,
})
s.sdk.set(key, loaded)
return loaded as SDK
}
const mod = await import(importSpec)
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
路径:packages/opencode/src/provider/provider.ts:1553-1640。
ProviderTransform.message
export function message(msgs: ModelMessage[], model: Provider.Model, options: Record<string, unknown>) {
msgs = unsupportedParts(msgs, model)
msgs = normalizeMessages(msgs, model, options)
if (model.providerID === "anthropic" || model.api.id.includes("claude")) {
msgs = applyCaching(msgs, model)
}
const key = sdkKey(model.api.npm)
if (key && key !== model.providerID) {
...
}
return msgs
}
路径:packages/opencode/src/provider/transform.ts:429-474。
AI SDK event adapter
case "tool-call":
return Effect.sync(() => {
state.toolNames[event.toolCallId] = event.toolName
return [
LLMEvent.toolCall({
id: event.toolCallId,
name: event.toolName,
input: event.input,
}),
]
})
路径:packages/opencode/src/session/llm/ai-sdk.ts:191-203。
8. 关键 TypeScript 语法复习
Pick<StreamInput, "tools" | "agent">:从大类型挑字段,类似小 DTO。Record<string, Tool>:类似 JavaMap<String, Tool>。"auto" | "required" | "none":字符串字面量 union,类似 enum。parentSessionID?: string:可选属性,运行时可能是undefined。...(condition ? [item] : []):条件数组 spread。await import(importSpec):动态加载 provider SDK。"ai-sdk" as const:收窄 literal type,方便 discriminated union。
9-10. 架构思想与模块协作
- Gateway:
LLM.Service是 agent loop 到模型的统一出口。 - Adapter:
LLMAISDK.toLLMEvents转换第三方 SDK 事件。 - Factory + cache:
Provider.getLanguage创建并缓存 language model。 - Hook:
chat.params、chat.headers、system transform 修改请求。 - Compatibility layer:
ProviderTransform.message处理各 provider 的格式差异。 - Policy filter:
resolveTools在调用模型前按权限禁用工具。
11. mini agent 对应代码
async function* streamLlm(input) {
const client = await providerRegistry.getClient(input.model)
const request = providerTransform.toRequest({
system: input.system,
messages: input.messages,
tools: input.tools,
})
for await (const event of client.stream(request, { signal: input.signal })) {
yield aiSdkAdapter.toInternalEvent(event)
}
}
12. 费曼复述区
Provider.getLanguage解决什么问题?- 为什么内部 message 不能原样发给所有 provider?
- 为什么 AI SDK event 要变成 OpenCode 自己的
LLMEvent? - 为什么 LLM 层还要过滤 tools?
13-15. 练习与自测
- 入门:把
StreamInput字段按 session/model/prompt/tool/control 分组。 - 进阶:读
ProviderTransform.message,列出它做的转换。 - 源码追踪:从
LLM.stream追到streamText.fullStream再追到LLMAISDK.toLLMEvents。 - 小实现:模拟 provider stream,并把 provider event 转成内部
LlmEvent。 - 自测:tool name 大小写错了,哪段代码尝试修复?