Provider 源码解读
模块概述
Provider 是 OpenCode 的 模型抽象层与路由中枢,位于 packages/opencode/src/provider/。在 TypeScript 版本中,它不再直接构建 HTTP 客户端,而是通过 Vercel AI SDK(ai 包)统一对接 30+ LLM Provider。上层模块(Agent、Session)通过 Provider.getLanguage() 获取标准化的 LanguageModel 对象后调用 streamText / generateText,完全与底层 API 差异解耦。
核心设计选择:
- Vercel AI SDK 作为统一适配层,避免为每个 Provider 手写 HTTP 协议
- Effect Schema 品牌类型(
ProviderID、ModelID)在类型层面区分不同实体 - models.dev 作为模型元数据的外部数据源,运行时动态加载
CUSTOM_LOADERS字典处理各 Provider 的特殊逻辑(Bedrock 区域前缀、Copilot SDK 适配、Vertex 认证等)Instance.state提供进程级单例缓存,Provider 初始化只执行一次
关键文件
| 文件 | 职责 |
|---|---|
src/provider/provider.ts | 核心命名空间:状态管理、Provider 路由、SDK 工厂、模型查找、CUSTOM_LOADERS |
src/provider/models.ts | models.dev 数据源加载:Zod schema 定义、JSON 解析、定时刷新、快照回退 |
src/provider/schema.ts | Effect Schema 品牌类型 ProviderID / ModelID,含 Zod 桥接 |
src/provider/transform.ts | 请求/响应转换:消息规范化、缓存标记、reasoning 变体、temperature/topP 调优、错误格式化 |
src/provider/auth.ts | Provider 认证: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"),
// ...
})),
)
品牌类型确保 ProviderID 和 ModelID 在编译期不可互换——你不能把模型 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 等
}
路由逻辑:
- 查
BUNDLED_PROVIDERS[model.api.npm]——命中则直接调用工厂函数 - 未命中则通过
BunProc.install()动态安装 npm 包,再import加载 - 每个实例按
{ 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-bedrock | AWS 凭证链、区域推断、跨区域推理前缀(us./eu./apac.) |
google-vertex | Google ADC 认证、项目/位置解析、自定义 fetch 注入 |
azure | 动态 baseURL 模板替换、resourceName 变量注入 |
gitlab | Agentic 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,提供三个关键能力:
- SSE 超时包装(
wrapSSE):为每个 chunk 读取设置超时,默认 5 分钟,防止 Provider 端静默断开导致连接永久挂起 - OpenAI 请求体优化:删除 input 数组中的
id字段,减少不必要的网络传输 - 信号合并:将原始
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 初始化完成后,模型列表经过多层过滤:
- autoload 检查:
CUSTOM_LOADERS中autoload: false的 Provider 被跳过(如无 API Key 的 Provider) - disable 标记:
disable: true的模型直接跳过 - 免费模型回退:无 API Key 时只保留标记为免费的模型(如
opencodeProvider 的免费模型) - 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() 按顺序执行四个转换步骤:unsupportedParts → normalizeMessages → applyCaching → providerOptions 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.npm 和 model.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 错误,含statusCode、isRetryable、responseBody
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)
}
模型切换时(用户选择不同模型),调用链为:
Provider.getModel(newProviderID, newModelID)— 查找模型信息Provider.getLanguage(model)— 获取或创建 LanguageModel 实例getSDK(model)— 获取或创建 SDK Provider 实例- 实例按
{ providerID, npm, options }哈希缓存,相同配置复用
defaultModel() 函数按以下优先级选择默认模型:
- 配置文件中的
model字段 - 最近使用的模型(
model.json中的recent列表) - 按优先级排序(
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 而非直接 HTTP | 30+ 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_providers、config.enabled_providers - Auth:
Auth.get(providerID)读取持久化的 API Key / OAuth token;ProviderAuth命名空间处理 OAuth 授权流程 - Plugin:
Plugin.list()获取注册的认证方法(如 Copilot 的设备认证),通过plugin.auth.loader注入 Provider 选项 - Instance:
Instance.state()提供 Provider 状态的单例管理,与项目生命周期绑定 - Transform:Agent 在调用
streamText前,通过ProviderTransform.message()规范化消息、注入缓存标记 - Error:Agent 通过
ProviderError.parseAPICallError()将 Vercel AI SDK 的APICallError转换为结构化的ParsedAPICallError