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-60packages/opencode/src/session/llm.ts:402-493packages/opencode/src/session/llm/ai-sdk.ts:61-236

2-5. 它的位置与最小源码路径

源码路径卡片

  1. session/llm.ts:39-60:LLM stream 数据合同。
  2. session/llm.ts:99-188:解析 language/config/provider/auth,拼 system 和参数。
  3. session/llm.ts:204-225512-518:过滤 tools。
  4. session/llm.ts:330-467:headers、native runtime、AI SDK streamText
  5. session/llm/ai-sdk.ts:61-236:AI SDK event 到 LLMEvent
  6. provider/provider.ts:1508-1703:SDK 动态加载和 language model 缓存。
  7. 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.ServiceLlmGatewayProvider.ServiceProviderRegistry + ClientFactoryProviderTransform 像 provider-specific HttpMessageConverterLLMAISDK 像事件 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>:类似 Java Map<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.paramschat.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. 费曼复述区

  1. Provider.getLanguage 解决什么问题?
  2. 为什么内部 message 不能原样发给所有 provider?
  3. 为什么 AI SDK event 要变成 OpenCode 自己的 LLMEvent
  4. 为什么 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 大小写错了,哪段代码尝试修复?

16. 下一步阅读建议

继续读 “权限、审批、安全边界”。Provider 章说明模型如何拿到工具;权限章说明模型即使拿到工具,也不能绕过 runtime 边界。