Code Reader
首页
帮助
设计文档
首页
帮助
设计文档
  • Provider 管理和模型加载

Provider 管理和模型加载

OpenCode 的 Provider 管理系统负责加载、配置和管理多个 AI 提供商(如 Anthropic、OpenAI、Google 等),支持环境变量、配置文件、API Key、OAuth 等多种认证方式。

目录

  • 架构概述
  • Provider 状态管理
  • 认证方式
  • 自定义加载器
  • Provider 选择策略
  • 模型初始化
  • Provider 配置

架构概述

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.ts676-953
SDK 实例化packages/opencode/src/provider/provider.ts959-1060
Language Model 获取packages/opencode/src/provider/provider.ts1086-1111
Provider 选择packages/console/app/src/routes/zen/util/handler.ts340-398
自定义加载器packages/opencode/src/provider/provider.ts88-497
小模型选择packages/opencode/src/provider/provider.ts1128-1168
默认模型选择packages/opencode/src/provider/provider.ts1180-1194