Provider 管理和模型加载
OpenCode 的 Provider 管理系统负责加载、配置和管理多个 AI 提供商(如 Anthropic、OpenAI、Google 等),支持环境变量、配置文件、API Key、OAuth 等多种认证方式。
目录
架构概述
Provider 生命周期
Provider 状态管理
核心状态结构
// packages/opencode/src/provider/provider.ts:676-953
const state = Instance.state(async () => {
const config = await Config.get()
const modelsDev = await ModelsDev.get()
const database = mapValues(modelsDev, fromModelsDevProvider)
const providers: { [providerID: string]: Info } = {}
const languages = new Map<string, LanguageModelV2>()
const modelLoaders: {
[providerID: string]: CustomModelLoader
} = {}
const sdk = new Map<number, SDK>()
return {
models: languages,
providers,
sdk,
modelLoaders,
}
})
状态初始化流程
// 1. 从 models.dev 加载基础 Provider 数据
const modelsDev = await ModelsDev.get()
const database = mapValues(modelsDev, fromModelsDevProvider)
// 2. 扩展 GitHub Copilot Enterprise
if (database["github-copilot"]) {
const githubCopilot = database["github-copilot"]
database["github-copilot-enterprise"] = {
...githubCopilot,
id: "github-copilot-enterprise",
name: "GitHub Copilot Enterprise",
models: mapValues(githubCopilot.models, (model) => ({
...model,
providerID: "github-copilot-enterprise",
})),
}
}
// 3. 合并配置文件中的 Provider 定义
for (const [providerID, provider] of configProviders) {
// 合并 models、options、env 等
mergeProvider(providerID, provider)
}
// 4. 加载环境变量认证
const env = Env.all()
for (const [providerID, provider] of Object.entries(database)) {
const apiKey = provider.env.map((item) => env[item]).find(Boolean)
if (!apiKey) continue
mergeProvider(providerID, {
source: "env",
key: provider.env.length === 1 ? apiKey : undefined,
})
}
// 5. 加载 API Key 认证
for (const [providerID, provider] of Object.entries(await Auth.all())) {
if (provider.type === "api") {
mergeProvider(providerID, {
source: "api",
key: provider.key,
})
}
}
// 6. 执行自定义加载器
for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
const result = await fn(database[providerID])
if (result && (result.autoload || providers[providerID])) {
if (result.getModel) modelLoaders[providerID] = result.getModel
mergeProvider(providerID, {
source: "custom",
options: result.options,
})
}
}
// 7. 应用配置覆盖
for (const [providerID, provider] of configProviders) {
mergeProvider(providerID, { source: "config" })
}
// 8. 过禁用 Provider 和模型
for (const [providerID, provider] of Object.entries(providers)) {
// 检查 disabled_providers
// 检查 enabled_providers
// 删除 deprecated 模型
// 删除 alpha 模型(如果未启用实验功能)
// 删除用户禁用的模型
}
认证方式
1. 环境变量
// packages/opencode/src/provider/provider.ts:812-822
const env = Env.all()
for (const [providerID, provider] of Object.entries(database)) {
if (disabled.has(providerID)) continue
const apiKey = provider.env.map((item) => env[item]).find(Boolean)
if (!apiKey) continue
mergeProvider(providerID, {
source: "env",
key: provider.env.length === 1 ? apiKey : undefined,
})
}
示例:
export ANTHROPIC_API_KEY=sk-ant-...
export OPENAI_API_KEY=sk-...
export GOOGLE_API_KEY=...
2. API Key 存储在配置文件
// packages/opencode/src/provider/provider.ts:824-833
for (const [providerID, provider] of Object.entries(await Auth.all())) {
if (disabled.has(providerID)) continue
if (provider.type === "api") {
mergeProvider(providerID, {
source: "api",
key: provider.key,
})
}
}
存储位置:~/.opencode/auth.json
{
"anthropic": {
"type": "api",
"key": "sk-ant-..."
},
"openai": {
"type": "api",
"key": "sk-..."
}
}
3. OAuth 认证
// packages/opencode/src/auth/index.ts:9-18
export const Oauth = z
.object({
type: z.literal("oauth"),
refresh: z.string(),
access: z.string(),
expires: z.number(),
accountId: z.string().optional(),
enterpriseUrl: z.string.googleional(),
})
.meta({ ref: "OAuth" })
使用示例:
opencode provider anthropic oauth authorize 0
# 返回授权 URL,用户在浏览器中完成授权
4. 配置文件
{
"provider": {
"anthropic": {
"options": {
"apiKey": "sk-ant-...",
"headers": {
"anthropic-version": "2023-06-01"
}
}
}
}
}
5. 插件认证
// packages/opencode/src/provider/provider.ts:835-880
for (const plugin of await Plugin.list()) {
if (!plugin.auth) continue
const providerID = plugin.auth.provider
const auth = await Auth.get(providerID)
if (!auth) continue
if (!plugin.auth.loader) continue
const options = await plugin.auth.loader(() => Auth.get(providerID) as any, database[providerID])
mergeProvider(providerID, {
source: "custom",
options: options,
})
}
自定义加载器
加载器接口
type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
type CustomLoader = (provider: Info) => Promise<{
autoload: boolean
getModel?: CustomModelLoader
options?: Record<string, any>
}>
Opencode Provider 加载器
// packages/opencode/src/provider/provider.ts:100-121
async opencode(input) {
const hasKey = await (async () => {
const env = Env.all()
if (input.env.some((item) => env[item])) return true
if (await Auth.get(input.id)) return true
const config = await Config.get()
if (config.provider?.["opencode"]?.options?.apiKey) return true
return false
})()
if (!hasKey) {
// 匿名访问:删除有成本的模型
for (const [key, value] of Object.entries(input.models)) {
if (value.cost.input === 0) continue
delete input.models[key]
}
return {
autoload: Object.keys(input.models).length > 0,
options: { apiKey: "public" },
}
}
return {
autoload: true,
options: {},
}
}
Anthropic Provider 加载器
// packages/opencode/src/provider/provider.ts:89-99
async anthropic() {
return {
autoload: false,
options: {
headers: {
"anthropic-beta":
"claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
},
},
}
}
Amazon Bedrock Provider 加载器
// packages/opencode/src/provider/provider.ts:178-318
"amazon-bedrock": async () => {
const config = await Config.get()
const providerConfig = config.provider?.["amazon-bedrock"]
const auth = await Auth.get("amazon-bedrock")
// 区域配置优先级:config file > env var > default
const configRegion = providerConfig?.options?.region
const envRegion = Env.get("AWS_REGION")
const defaultRegion = configRegion ?? envRegion ?? "us-east-1"
// Bearer Token 处理
const awsBearerToken = iife(() => {
const envToken = Env.get("AWS_BEARER_TOKEN_BEDROCK")
if (envToken) return envToken
if (auth?.type === "api") {
Env.set("AWS_BEARER_TOKEN_BEDROCK", auth.key)
return auth.key
}
return undefined
})
// AWS Credential Chain(如果没有 bearer token)
const providerOptions: AmazonBedrockProviderSettings = {
region: defaultRegion,
}
if (!awsBearerToken) {
const { fromNodeProviderChain } = await import("@aws-sdk/credential-providers")
const credentialProviderOptions = profile ? { profile } : {}
providerOptions.credentialProvider = fromNodeProviderChain(credentialProviderOptions)
}
return {
autoload: true,
options: providerOptions,
getModel: async (sdk: any, modelID: string, options?: Record<string, any>) => {
// 添加区域前缀(us., eu., apac., jp., au.)
if (!modelID.startsWith("global.") && !modelID.startsWith("jp.")) {
const regionPrefix = region.split("-")[0]
// 检查模型是否需要前缀
if (modelRequiresPrefix(modelID, region)) {
modelID = `${regionPrefix}.${modelID}`
}
}
return sdk.languageModel(modelID)
},
}
}
Provider 选择策略
权重随机选择
// packages/opencode/src/provider/provider.ts:340-398
function selectProvider(
reqModel: string,
zenData: ZenData,
authInfo: AuthInfo,
modelInfo: ModelInfo,
sessionId: string,
isTrial: boolean,
retry: RetryOptions,
stickyProvider: string | undefined,
) {
const modelProvider = (() => {
// 1. BYOK 优先
if (authInfo?.provider?.credentials) {
return modelInfo.providers.find((provider) => provider.id === modelInfo.byokProvider)
}
// 2. 试用用户使用试用 provider
if (isTrial) {
return modelInfo.providers.find((provider) => provider.id === modelInfo.trial!.provider)
}
// 3. 会话粘性:使用上次选择的 provider
if (stickyProvider) {
const provider = modelInfo.providers.find((provider) => provider.id === stickyProvider)
if (provider) return provider
}
// 4. 重试次数达到上限:使用回退 provider
if (retry.retryCount === MAX_RETRIES) {
return modelInfo.providers.find((provider) => provider.id === modelInfo.fallbackProvider)
}
// 5. 基于权重的随机选择
const providers = modelInfo.providers
.filter((provider) => !provider.disabled)
.filter((provider) => !retry.excludeProviders.includes(provider.id))
.flatMap((provider) => Array<typeof provider>(provider.weight ?? 1).fill(provider))
// 使用 session ID 的哈希来选择 provider(确保同一 session 选择相同 provider)
let h = 0
const l = sessionId.length
for (let i = l - 4; i < l; i++) {
h = (h * 31 + sessionId.charCodeAt(i)) | 0 // 32-bit int
}
const index = (h >>> 0) % providers.length
return providers[index || 0]
})()
return {
...modelProvider,
...zenData.providers[modelProvider.id],
// 应用 provider 特定的 helper(anthropicHelper, googleHelper 等)
...applyProviderHelper(modelProvider),
}
}
选择优先级
1. BYOK (authInfo.provider?.credentials)
↓ (不存在)
2. 试用 Provider (isTrial && modelInfo.trial)
↓ (不存在)
3. Sticky Provider (来自会话缓存)
↓ (不存在)
4. Fallback Provider (重试次数达到上限)
↓ (不存在)
5. 加权随机选择(基于 session ID 哈希)
模型初始化
SDK 实例化
// packages/opencode/src/provider/provider.ts:959-1060
async function getSDK(model: Model) {
const s = await state()
const provider = s.providers[model.providerID]
const options = { ...provider.options }
// 1. 设置 baseURL
if (!options["baseURL"]) options["baseURL"] = model.api.url
// 2. 设置 API Key
if (options["apiKey"] === undefined && provider.key) {
options["apiKey"] = provider.key
}
// 3. 合并模型特定的 headers
if (model.headers) {
options["headers"] = {
...options["headers"],
...model.headers,
}
}
// 4. 使用哈希缓存 SDK 实例(避免重复初始化)
const key = Bun.hash.xxHash32(JSON.stringify({ npm: model.api.npm, options }))
const existing = s.sdk.get(key)
if (existing) return existing
// 5. 自定义 fetch(超时处理)
options["fetch"] = async (input: any, init?: BunFetchRequestInit) => {
const fetchFn = customFetch ?? fetch
const opts = init ?? {}
// 添加超时
if (options["timeout"] !== undefined && options["timeout"] !== null) {
const signals: AbortSignal[] = []
if (opts.signal) signals.push(opts.signal)
if (options["timeout"] !== false) {
signals.push(AbortSignal.timeout(options["timeout"]))
}
const combined = signals.length > 1 ? AbortSignal.any(signals) : signals[0]
opts.signal = combined
}
return fetchFn(input, { ...opts, timeout: false })
}
// 6. 加载并实例化 Provider
const bundledKey = model.providerID === "google-vertex-anthropic" ? "@ai-sdk/google-vertex/anthropic" : model.api.npm
const bundledFn = BUNDLED_PROVIDERS[bundledKey]
if (bundledFn) {
const loaded = bundledFn({
name: model.providerID,
...options,
})
s.sdk.set(key, loaded)
return loaded as SDK
}
// 7. 动态安装 npm 包
let installedPath: string
if (!model.api.npm.startsWith("file://")) {
installedPath = await BunProc.install(model.api.npm, "latest")
} else {
installedPath = model.api.npm
}
const mod = await import(installedPath)
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
const loaded = fn({
name: model.providerID,
...options,
})
s.sdk.set(key, loaded)
return loaded as SDK
}
Language Model 实例化
// packages/opencode/src/provider/provider.ts:1086-1111
export async function getLanguage(model: Model): Promise<LanguageModelV2> {
const s = await state()
const key = `${model.providerID}/${model.id}`
// 缓存 Language Model 实例
if (s.models.has(key)) return s.models.get(key)!
const provider = s.providers[model.providerID]
const sdk = { await getSDK(model) }
try {
// 使用自定义模型加载器(如果存在)
const language = s.modelLoaders[model.providerID]
? await s.modelLoaders[model.providerID](sdk, model.api.id, provider.options)
: sdk.languageModel(model.api.id)
s.models.set(key, language)
return language
} catch (e) {
if (e instanceof NoSuchModelError)
throw new ModelNotFoundError(
{
modelID: model.id,
providerID: model.providerID,
},
{ cause: e },
)
throw e
}
}
Provider 配置
默认模型选择
// packages/opencode/src/provider/provider.ts:1180-1194
export async function defaultModel() {
const cfg = await Config.get()
// 1. 优先使用配置文件中的默认模型
if (cfg.model) return parseModel(cfg.model)
// 2. 选择第一个可用的 Provider
const provider = await list()
.then((val) => Object.values(val))
.then((x) => x.find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.id)))
if (!provider) throw new Error("no providers found")
// 3. 按优先级排序模型
const [model] = sort(Object.values(provider.models))
if (!model) throw new Error("no models found")
return {
providerID: provider.id,
modelID: model.id,
}
}
模型优先级
// packages/opencode/src/provider/provider.ts:1170
const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
export function sort(models: Model[]) {
return sortBy(
models,
// 1. 按优先级排序
[(model) => priority.findIndex((filter) => model.id.includes(filter)), "desc"],
// 2. 非 "latest" 模型优先
[(model) => (model.id.includes("latest") ? 0 : 1), "asc"],
// 3. 按名称排序(降序)
[(model) => model.id, "desc"],
)
}
小模型选择
(用于工具调用、总结等轻量级任务)
// packages/opencode/src/provider/provider.ts:1128-1168
export async function getSmallModel(providerID: string) {
const cfg = await Config.get()
// 1. 优先使用配置文件中的小模型
if (cfg.small_model) {
const parsed = parseModel(cfg.small_model)
return getModel(parsed.providerID, parsed.modelID)
}
const provider = await state().then((state) => state.providers[providerID])
if (provider) {
let priority = [
"claude-haiku-4-5",
"claude-haiku-4.5",
"3-5-haiku",
"3.5-haiku",
"gemini-3-flash",
"gemini-2.5-flash",
"gpt-5-nano",
]
// 特定 Provider 的优先级
if (providerID.startsWith("opencode")) {
priority = ["gpt-5-nano"]
}
if (providerID.startsWith("github-copilot")) {
priority = ["gpt-5-mini", "claude-haiku-4.5", ...priority]
}
// 查找第一个可用的优先模型
for (const item of priority) {
for (const model of Object.keys(provider.models)) {
if (model.includes(item)) return getModel(providerID, model)
}
}
}
// 2. 回退到 opencode 的 gpt-5-nano
const opencodeProvider = await state().then((state) => state.providers["opencode"])
if (opencodeProvider && opencodeProvider.models["gpt-5-nano"]) {
return getModel("opencode", "gpt-5-nano")
}
return undefined
}
相关文档
- 计费和认证系统 - 认证和计费流程
- 匿名访问和免费试用 - 匿名访问机制
- Provider/Model - 模型配置和数据结构
代码位置
| 功能 | 文件路径 | 行号 |
|---|---|---|
| Provider 状态管理 | packages/opencode/src/provider/provider.ts | 676-953 |
| SDK 实例化 | packages/opencode/src/provider/provider.ts | 959-1060 |
| Language Model 获取 | packages/opencode/src/provider/provider.ts | 1086-1111 |
| Provider 选择 | packages/console/app/src/routes/zen/util/handler.ts | 340-398 |
| 自定义加载器 | packages/opencode/src/provider/provider.ts | 88-497 |
| 小模型选择 | packages/opencode/src/provider/provider.ts | 1128-1168 |
| 默认模型选择 | packages/opencode/src/provider/provider.ts | 1180-1194 |