跳转到内容

Provider 源码解读

模块概述

Provider 是 OpenCode 的 模型抽象层与路由中枢,位于 packages/opencode/src/provider/。在 TypeScript 版本中,它不再直接构建 HTTP 客户端,而是通过 Vercel AI SDKai 包)统一对接 30+ LLM Provider。上层模块(Agent、Session)通过 Provider.getLanguage() 获取标准化的 LanguageModel 对象后调用 streamText / generateText,完全与底层 API 差异解耦。

核心设计选择:

  • Vercel AI SDK 作为统一适配层,避免为每个 Provider 手写 HTTP 协议
  • Effect Schema 品牌类型ProviderIDModelID)在类型层面区分不同实体
  • models.dev 作为模型元数据的外部数据源,运行时动态加载
  • CUSTOM_LOADERS 字典处理各 Provider 的特殊逻辑(Bedrock 区域前缀、Copilot SDK 适配、Vertex 认证等)
  • Instance.state 提供进程级单例缓存,Provider 初始化只执行一次

关键文件

文件职责
src/provider/provider.ts核心命名空间:状态管理、Provider 路由、SDK 工厂、模型查找、CUSTOM_LOADERS
src/provider/models.tsmodels.dev 数据源加载:Zod schema 定义、JSON 解析、定时刷新、快照回退
src/provider/schema.tsEffect Schema 品牌类型 ProviderID / ModelID,含 Zod 桥接
src/provider/transform.ts请求/响应转换:消息规范化、缓存标记、reasoning 变体、temperature/topP 调优、错误格式化
src/provider/auth.tsProvider 认证:OAuth 流程、API Key 管理、Plugin 认证集成
src/provider/error.ts错误分类:上下文溢出检测(12+ Provider 的错误模式)、API 错误解析、可重试判定
src/provider/sdk/copilot/自定义 GitHub Copilot SDK 适配器(chat/ + responses/ 子目录,约 20 文件)

sdk/copilot/ 是唯一不依赖 Vercel AI SDK 官方包的 Provider,因为它需要处理 Copilot 特有的设备认证、token 刷新和 API 兼容性问题。README 中标注为 “temporary package”,后续可能被官方 SDK 替代。

类型体系

品牌类型:ProviderID 与 ModelID

// schema.ts — Effect Schema 品牌类型
const providerIdSchema = Schema.String.pipe(Schema.brand("ProviderID"))
export type ProviderID = typeof providerIdSchema.Type

export const ProviderID = providerIdSchema.pipe(
  withStatics((schema) => ({
    make: (id: string) => schema.makeUnsafe(id),    // 运行时构造
    zod: z.string().pipe(z.custom()),                // Zod 验证桥接
    // 内置常量
    anthropic: schema.makeUnsafe("anthropic"),
    openai: schema.makeUnsafe("openai"),
    githubCopilot: schema.makeUnsafe("github-copilot"),
    amazonBedrock: schema.makeUnsafe("amazon-bedrock"),
    // ...
  })),
)

品牌类型确保 ProviderIDModelID编译期不可互换——你不能把模型 ID 传给期望 Provider ID 的函数。makeUnsafe 用于运行时从字符串构造(如解析用户配置),而常量属性(ProviderID.anthropic)提供类型安全的引用点。

Provider.Model 完整模型定义

// provider.ts — Zod schema
export const Model = z.object({
  id: ModelID.zod,           // 品牌类型标识
  providerID: ProviderID.zod,
  api: z.object({
    id: z.string(),          // 发给 API 的实际模型名
    url: z.string(),         // API base URL
    npm: z.string(),         // Vercel AI SDK 包名(如 "@ai-sdk/anthropic")
  }),
  name: z.string(),          // 人类可读名称
  family: z.string().optional(),  // 模型家族(如 "claude", "gpt")
  capabilities: z.object({
    temperature: z.boolean(),
    reasoning: z.boolean(),       // 是否支持思考/推理模式
    attachment: z.boolean(),      // 是否支持附件
    toolcall: z.boolean(),        // 是否支持工具调用
    input: z.object({
      text: z.boolean(), audio: z.boolean(),
      image: z.boolean(), video: z.boolean(), pdf: z.boolean(),
    }),
    output: z.object({
      text: z.boolean(), audio: z.boolean(),
      image: z.boolean(), video: z.boolean(), pdf: z.boolean(),
    }),
    interleaved: z.union([       // 交错思考模式
      z.boolean(),
      z.object({ field: z.enum(["reasoning_content", "reasoning_details"]) }),
    ]),
  }),
  cost: z.object({
    input: z.number(),      // $/M tokens(输入)
    output: z.number(),     // $/M tokens(输出)
    cache: z.object({
      read: z.number(),     // 缓存读取单价
      write: z.number(),    // 缓存写入单价
    }),
    experimentalOver200K: z.object({ /* ... */ }).optional(),  // 超 200K 定价
  }),
  limit: z.object({
    context: z.number(),    // 上下文窗口
    input: z.number().optional(),
    output: z.number(),     // 最大输出 token
  }),
  status: z.enum(["alpha", "beta", "deprecated", "active"]),
  options: z.record(z.string(), z.any()),  // Provider 特定选项
  headers: z.record(z.string(), z.string()), // 自定义请求头
  release_date: z.string(),
  variants: z.record(/* reasoning effort 变体 */).optional(),
})

Model 不仅描述模型能力,还包含完整的成本信息reasoning 变体配置variants 字段由 ProviderTransform.variants() 根据模型和 SDK 包名自动生成(如 Anthropic 的 high/max thinking budget,OpenAI 的 none/minimal/low/medium/high/xhigh reasoning effort)。

Provider.Info

export const Info = z.object({
  id: ProviderID.zod,
  name: z.string(),
  source: z.enum(["env", "config", "custom", "api"]),  // 来源追踪
  env: z.string().array(),      // 环境变量名列表(如 ["ANTHROPIC_API_KEY"])
  key: z.string().optional(),   // 已解析的 API Key
  options: z.record(z.string(), z.any()),  // Provider 级选项
  models: z.record(z.string(), Model),     // 该 Provider 下的所有模型
})

source 字段追踪 Provider 凭据的来源优先级:env(环境变量) > api(Auth 存储) > config(配置文件) > custom(Plugin/Loader)。

核心流程

Provider 状态初始化

Instance.state() 创建进程级单例,按以下顺序组装可用 Provider:

1. Config.get()           → 读取用户配置
2. ModelsDev.get()        → 加载 models.dev 数据(缓存 → 快照 → 网络获取)
3. fromModelsDevProvider() → 转换为 Provider.Info 数据库
4. 环境变量扫描           → 匹配 env 字段,设置 source: "env"
5. Auth.all()             → 读取持久化 API Key / OAuth token
6. Plugin.list()          → 处理 Plugin 提供的认证(如 Copilot 设备认证)
7. CUSTOM_LOADERS         → 执行各 Provider 的自定义逻辑
8. Config 合并            → 覆盖模型定义、选项、白名单/黑名单
9. 过滤                   → 移除 disabled、alpha(非实验模式)、deprecated、空模型列表的 Provider

初始化结果缓存在内存中,后续调用 Provider.list() / Provider.getModel() 直接读取缓存。

SDK 工厂与 30+ Provider 路由

getSDK() 函数负责为给定模型创建 Vercel AI SDK Provider 实例:

// 内置 Provider 映射:20 个官方 SDK 包
const BUNDLED_PROVIDERS: Record<string, (...args: any[]) => SDK> = {
  "@ai-sdk/anthropic": createAnthropic,
  "@ai-sdk/openai": createOpenAI,
  "@ai-sdk/google": createGoogleGenerativeAI,
  "@ai-sdk/amazon-bedrock": createAmazonBedrock,
  "@ai-sdk/azure": createAzure,
  "@openrouter/ai-sdk-provider": createOpenRouter,
  "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,  // 自定义适配器
  // ... 还有 Groq, Mistral, DeepInfra, Cerebras, Cohere, Together, Perplexity, Vercel, GitLab 等
}

路由逻辑:

  1. BUNDLED_PROVIDERS[model.api.npm]——命中则直接调用工厂函数
  2. 未命中则通过 BunProc.install() 动态安装 npm 包,再 import 加载
  3. 每个实例按 { providerID, npm, options } 的哈希值缓存,相同配置复用

getLanguage() 进一步将 SDK 实例转换为 LanguageModel

// 如果该 Provider 有自定义 modelLoader,用它;否则调用 sdk.languageModel()
const language = s.modelLoaders[model.providerID]
  ? await s.modelLoaders[model.providerID](sdk, model.api.id, provider.options)
  : sdk.languageModel(model.api.id)

模型发现:models.dev 数据源

models.ts 实现了三级回退的数据加载策略:

优先级:
1. 本地缓存文件(~/.cache/opencode/models.json)
2. 构建时快照(models-snapshot.ts,打包在二进制中)
3. 远程获取(https://models.dev/api.json)

启动后每 60 分钟后台刷新一次(setInterval + unref(),不阻塞进程退出)。模型数据通过 ModelsDev.Model Zod schema 验证后转换为 Provider.Model,确保结构一致。

自定义 Provider 加载器(CUSTOM_LOADERS)

CUSTOM_LOADERS 字典为特殊 Provider 提供定制逻辑,每个加载器返回 { autoload, getModel?, options?, vars? }

Provider特殊逻辑
anthropic添加 beta header(claude-code-20250219, interleaved-thinking
openai使用 Responses API(sdk.responses(modelID))而非 Chat
github-copilot根据 model ID 选择 responses()chat()
amazon-bedrockAWS 凭证链、区域推断、跨区域推理前缀(us./eu./apac.
google-vertexGoogle ADC 认证、项目/位置解析、自定义 fetch 注入
azure动态 baseURL 模板替换、resourceName 变量注入
gitlabAgentic Chat API、自定义 User-Agent 和 feature flags
cloudflare-ai-gateway动态加载 ai-gateway-provider 包,Unified API 格式路由
opencode检测 API Key 可用性,无 Key 时只保留免费模型

Copilot 自定义 SDK 适配器

provider/sdk/copilot/ 目录包含独立的 Copilot SDK 适配器,目录结构:

sdk/copilot/
├── index.ts                  # createOpenaiCompatible 工厂函数
├── copilot-provider.ts       # Provider 实现
├── openai-compatible-error.ts # 错误处理
├── chat/                     # Chat Completions API 适配
└── responses/                # Responses API 适配

这个适配器不依赖 @ai-sdk/openai,而是自行实现了 Copilot 特有的设备认证流程(OAuth 设备码流)、token 刷新和 API 兼容性处理。README 中标注为 “temporary”——当 Vercel AI SDK 提供官方 Copilot 支持后将被移除。

自定义 fetch 注入

每个 SDK 实例创建时注入自定义 fetch,提供三个关键能力:

  1. SSE 超时包装wrapSSE):为每个 chunk 读取设置超时,默认 5 分钟,防止 Provider 端静默断开导致连接永久挂起
  2. OpenAI 请求体优化:删除 input 数组中的 id 字段,减少不必要的网络传输
  3. 信号合并:将原始 signal + chunkAbort + timeout 通过 AbortSignal.any() 合并为统一的中断信号
// 信号合并示意
const combined = AbortSignal.any([
  originalSignal,     // 用户取消
  chunkAbort,         // chunk 级超时
  timeoutSignal,      // 全局超时
])

这种分层信号设计确保了:用户主动取消立即生效、单个 chunk 超时触发重连、全局超时作为最终保底。

ProviderAuth OAuth 流程

ProviderAuth 命名空间处理两种认证类型的完整生命周期:

API Key 认证key 类型):

  • 从环境变量或配置文件获取 API Key
  • 直接设置 provider.key,标记 source: "env"source: "config"

OAuth 认证refresh 类型):

  • authorize 阶段:从 Plugin hooks 获取 auth 配置(如 Copilot 的设备码流 URL)
  • callback 阶段:支持 auto(自动回调)和 code(手动输入授权码)两种回调方式
  • 认证完成后将 refresh token 持久化,后续通过自动刷新维持会话
// 认证类型判断
key → api 类型 auth     // 直接使用 API Key
refresh → oauth 类型 auth  // 使用 OAuth 刷新令牌

模型过滤与黑白名单

Provider 初始化完成后,模型列表经过多层过滤:

  1. autoload 检查CUSTOM_LOADERSautoload: false 的 Provider 被跳过(如无 API Key 的 Provider)
  2. disable 标记disable: true 的模型直接跳过
  3. 免费模型回退:无 API Key 时只保留标记为免费的模型(如 opencode Provider 的免费模型)
  4. SDK 实例缓存:按 { providerID, npm, options } 哈希值缓存,相同配置复用同一实例

LiteLLM 代理兼容性

OpenCode 检测 LiteLLM 代理并自动适配消息格式:

  • 检测响应头或请求路径中的 LiteLLM 标识
  • 当消息历史包含 tool calls 但当前消息无工具定义时,注入 dummy tool
  • 这是由于 LiteLLM 在消息包含 tool_call 角色但请求不携带 tools 时会报错

这种兼容性处理确保了 OpenCode 可以透明地通过 LiteLLM 代理访问各种模型。

请求转换层

ProviderTransform 命名空间处理消息在发送前的 Provider 特定转换。整个转换管道在 wrapLanguageModel 中间件中被调用:

wrapLanguageModel 中间件

LLM.stream() 在调用 streamText() 前,通过 wrapLanguageModel() 注入消息转换中间件:

model: wrapLanguageModel({
  model: language,
  middleware: [{
    specificationVersion: "v3" as const,
    async transformParams(args) {
      if (args.type === "stream") {
        args.params.prompt = ProviderTransform.message(args.params.prompt, input.model, options)
      }
      return args.params
    },
  }],
})

ProviderTransform.message() 按顺序执行四个转换步骤:unsupportedPartsnormalizeMessagesapplyCachingproviderOptions key remap。这种中间件设计使得转换逻辑与 SDK 调用逻辑完全解耦。

消息规范化(normalizeMessages)

  • Anthropic:过滤空内容消息(API 会拒绝空字符串)
  • Claude 模型:tool call ID 中的非字母数字字符替换为 _
  • Mistral:tool call ID 规范化为恰好 9 位字母数字,工具消息后不能紧跟用户消息(自动插入 “Done.” 助手消息)
  • 交错思考模型:将 reasoning part 从 content 移到 providerOptions.openaiCompatible.reasoning_content

缓存标记(applyCaching)

为支持 Prompt Caching 的 Provider 自动添加缓存标记。缓存策略有明确的规则:

  • system 前 2 条 + 最后 2 条消息添加缓存标记
  • 各 Provider 使用不同的格式:
// Anthropic
{ cacheControl: { type: "ephemeral" } }

// Amazon Bedrock
{ cachePoint: { type: "default" } }

只对 Anthropic / OpenRouter / Bedrock 等已知支持 Prompt Caching 的 Provider 生效,其他 Provider 静默跳过。

Reasoning 变体生成(variants)

根据 model.api.npmmodel.id 自动生成 reasoning effort 变体。每个 Provider 的变体参数不同:

  • Anthropic{ thinking: { type: "enabled", budgetTokens } }(high: 16K, max: 32K)
  • OpenAI{ reasoningEffort, reasoningSummary: "auto" }(none 到 xhigh)
  • Google{ thinkingConfig: { thinkingBudget, includeThoughts } }
  • Bedrock:Anthropic 模型用 reasoningConfig.budgetTokens,Nova 用 maxReasoningEffort

模型特定调参

// transform.ts — temperature 根据 model ID 自动调整
function temperature(model: Provider.Model) {
  if (id.includes("qwen"))   return 0.55
  if (id.includes("claude")) return undefined  // 使用默认值
  if (id.includes("gemini")) return 1.0
  if (id.includes("kimi-k2")) return id.includes("thinking") ? 1.0 : 0.6
  return undefined
}

错误处理

上下文溢出检测

error.ts 维护了覆盖 12+ Provider 的溢出模式匹配列表:

const OVERFLOW_PATTERNS = [
  /prompt is too long/i,                      // Anthropic
  /input is too long for requested model/i,   // Amazon Bedrock
  /exceeds the context window/i,              // OpenAI
  /input token count.*exceeds the maximum/i,  // Google Gemini
  /maximum prompt length is \d+/i,            // xAI Grok
  /exceeds the limit of \d+/i,                // GitHub Copilot
  /context[_ ]length[_ ]exceeded/i,           // 通用回退
  // ... 更多
]

错误被分类为两种类型:

  • context_overflow:上下文超出窗口限制,需要截断输入
  • api_error:通用 API 错误,含 statusCodeisRetryableresponseBody

OpenAI Provider 有特殊的可重试逻辑:HTTP 404 也被视为可重试(某些模型在首次请求时返回 404 但实际可用)。

自定义错误类型

// provider.ts
ModelNotFoundError  — 包含 providerID、modelID 和 fuzzysort 模糊匹配建议
InitError           — SDK 初始化失败(包安装/加载错误)

ProviderError.parseAPICallError() 是上层(Agent/Session)使用的统一错误解析入口。

SSE 超时保护

wrapSSE() 函数为 SSE 流添加逐 chunk 超时(默认 5 分钟),防止网络断开时连接永远挂起:

function wrapSSE(res: Response, ms: number, ctl: AbortController) {
  // 为每个 chunk 读取设置超时
  // 超时后 abort 连接 + 取消 reader
}

状态管理与 Provider 热切换

Provider 状态通过 Instance.state() 管理——这是一个惰性单例工厂,首次调用时初始化,后续调用返回缓存结果。状态包含:

{
  providers: Record<string, Provider.Info>,     // 所有可用 Provider
  models: Map<string, LanguageModel>,            // 已实例化的 LanguageModel 缓存
  sdk: Map<string, SDK>,                         // 已实例化的 SDK Provider 缓存
  modelLoaders: Record<string, CustomModelLoader>, // 自定义模型加载器
  varsLoaders: Record<string, CustomVarsLoader>,   // 变量注入器(如 Azure resourceName)
}

模型切换时(用户选择不同模型),调用链为:

  1. Provider.getModel(newProviderID, newModelID) — 查找模型信息
  2. Provider.getLanguage(model) — 获取或创建 LanguageModel 实例
  3. getSDK(model) — 获取或创建 SDK Provider 实例
  4. 实例按 { providerID, npm, options } 哈希缓存,相同配置复用

defaultModel() 函数按以下优先级选择默认模型:

  1. 配置文件中的 model 字段
  2. 最近使用的模型(model.json 中的 recent 列表)
  3. 按优先级排序(gpt-5 > claude-sonnet-4 > gemini-3-pro)选择第一个可用模型

调用链示例

用户选择模型到流式响应

用户选择 "anthropic/claude-sonnet-4"


Provider.getModel("anthropic", "claude-sonnet-4")
    │  ← 查 state.providers.anthropic.models["claude-sonnet-4"]
    │  ← 未找到则 fuzzysort 模糊匹配并抛 ModelNotFoundError

Provider.getLanguage(model)
    │  ← 查 modelLoaders["anthropic"]
    │  ← 有自定义 loader?调用 loader(sdk, model.api.id, options)
    │  ← 否则 sdk.languageModel(model.api.id)

getSDK(model)
    │  ← 计算哈希 key = hash({ providerID, npm: "@ai-sdk/anthropic", options })
    │  ← 缓存命中?直接返回
    │  ← 缓存未命中:BUNDLED_PROVIDERS["@ai-sdk/anthropic"] → createAnthropic(options)
    │  ← 注入自定义 fetch(SSE 超时 + 请求体优化)

Agent 调用 streamText({ model: languageModel, messages, tools })
    │  ← Vercel AI SDK 处理实际的 HTTP 请求和 SSE 解析

流式响应返回给上层

模型切换时的状态更新

用户从 claude-sonnet-4 切换到 gpt-5


parseModel("openai/gpt-5") → { providerID: "openai", modelID: "gpt-5" }


Provider.getModel("openai", "gpt-5")
    │  ← state.providers["openai"] 存在?
    │  ← state.providers["openai"].models["gpt-5"] 存在?

Provider.getLanguage(model)
    │  ← getSDK: BUNDLED_PROVIDERS["@ai-sdk/openai"] → createOpenAI(options)
    │  ← modelLoaders["openai"]: sdk.responses("gpt-5")  (Responses API)

新的 LanguageModel 实例缓存到 state.models

设计取舍

决策理由
Vercel AI SDK 而非直接 HTTP30+ Provider 的协议差异由 SDK 统一处理,避免维护大量 HTTP 客户端代码。OpenCode 专注业务逻辑(消息转换、缓存、错误分类)
Effect Schema 品牌类型编译期防止 ProviderID / ModelID / string 混用,运行时零开销(品牌类型擦除为 string)
models.dev 外部数据源模型信息(定价、窗口、能力)由社区维护,OpenCode 不需硬编码。三级回退保证离线可用
CUSTOM_LOADERS 字典而非子类继承每个 Provider 的差异逻辑(认证方式、API 选择、区域处理)差异太大,继承体系无法优雅表达。字典 + 函数更灵活
自定义 Copilot SDK 适配器Copilot 的设备认证和 token 刷新流程与标准 OAuth 差异过大,官方 SDK 不支持,需要独立实现
SSE 逐 chunk 超时防止 Provider 端静默断开导致连接永久挂起。默认 5 分钟,可通过 chunkTimeout 选项调整
fuzzysort 模糊匹配建议模型 ID 经常拼写错误或名称变更,模糊匹配给出 3 个建议替代硬报错
BunProc 动态安装未打包的 SDK社区 Provider(如 @mymediset/sap-ai-provider)不必打进主包,按需安装

与其他模块的关系

  • Agent:通过 Provider.getLanguage() 获取 LanguageModel,传给 Vercel AI SDK 的 streamText() / generateText() 驱动对话循环
  • Config:Provider 初始化时读取 config.model(默认模型)、config.provider(Provider 配置覆盖)、config.disabled_providersconfig.enabled_providers
  • AuthAuth.get(providerID) 读取持久化的 API Key / OAuth token;ProviderAuth 命名空间处理 OAuth 授权流程
  • PluginPlugin.list() 获取注册的认证方法(如 Copilot 的设备认证),通过 plugin.auth.loader 注入 Provider 选项
  • InstanceInstance.state() 提供 Provider 状态的单例管理,与项目生命周期绑定
  • Transform:Agent 在调用 streamText 前,通过 ProviderTransform.message() 规范化消息、注入缓存标记
  • Error:Agent 通过 ProviderError.parseAPICallError() 将 Vercel AI SDK 的 APICallError 转换为结构化的 ParsedAPICallError