提供方目录与 models.dev

models.dev 目录从哪来、怎么刷新,Provider 状态如何由目录、环境变量、凭据、插件与配置层层叠加,以及 getLanguage 如何拿到 AI SDK 模型。

作者 David更新于 第 19 篇(共 38 篇)

OpenCode 没有在仓库里手写模型清单。“有哪些模型、各有什么能力和价格”交给社区维护的 models.dev 目录,OpenCode 只回答另一个问题:此刻在这个项目里,哪些提供方真正可用。前者是目录,后者是 Provider 状态。现役实现在 packages/opencode/src/provider/provider.ts,数据源在 packages/core/src/models-dev.ts。

先看怎么用

场景做法
列出模型opencode models [provider],--verbose 看元数据,--refresh 强制刷新目录
默认模型配置 model 与 small_model,格式为 provider/model
会话中切换TUI 里用 /models 选模型,ctrl+t 轮换推理档位(variant)
限定提供方disabled_providers 为黑名单,enabled_providers 为白名单
离线或内网OPENCODE_DISABLE_MODELS_FETCH、OPENCODE_MODELS_PATH、OPENCODE_MODELS_URL

models.dev:磁盘、快照、网络三级

ModelsDev.Service 的 get() 求值一次后永久缓存,首次求值按顺序找数据(packages/core/src/models-dev.ts:217,删减后):

const populate = Effect.gen(function* () {
  const fromDisk = yield* loadFromDisk
  if (fromDisk) return fromDisk
  const snapshot = yield* loadSnapshot
  if (snapshot) return snapshot
  if (Flag.OPENCODE_DISABLE_MODELS_FETCH) return {}
  // Flock is cross-process: concurrent opencode CLIs can race on this cache file.
  // ...
}).pipe(Effect.withSpan("ModelsDev.populate"), Effect.orDie)

磁盘缓存是 ~/.cache/opencode/models.json(或 OPENCODE_MODELS_PATH 指定的文件),读不出来就删掉;快照由 packages/opencode/script/build.ts 通过 Bun 的 define 把 OPENCODE_MODELS_DEV 编进二进制,发布版首次启动不必等网络;两级都落空才联网拉取,10 秒超时、失败重试两次,写入时持有跨进程文件锁。

后台纤程在启动时先调一次 refresh(),之后每 60 分钟一次:缓存 5 分钟内刷新过就跳过,否则下载、让内存缓存失效并发布 Refreshed 事件;opencode models --refresh 与 opencode auth login 会强制刷新。运行时默认源是 https://models.opencode.ai,packages/opencode/script/generate.ts 生成快照时则默认拉 https://models.dev。

Provider 状态:一层层叠上去

状态按项目实例(InstanceState)构建,入口在 provider.ts:1400。models.dev 先被转成完整目录 database;另一份 providers 起初为空,只有“有理由启用”的提供方才会经 mergeProvider() 从 database 复制进来:

图表加载中…
  • 环境变量:models.dev 为每家列出 env(如 ANTHROPIC_API_KEY),任一有值即启用。
  • 配置即启用:出现在配置 provider 里的提供方会以 source: config 进入 providers,哪怕没有密钥。
  • custom loader:custom()(provider.ts:174 起)按提供方 ID 返回 autoload、getModel、vars、options 等钩子。autoload 决定“没有别的理由时要不要主动启用”:amazon-bedrock 发现 AWS profile、访问密钥、Bearer token 等凭据就启用;opencode(OpenCode Zen)没有密钥时删掉收费模型、把 apiKey 设为 "public",让免费模型开箱即用。getModel 决定“调 SDK 的哪个工厂方法”:openai、xai 固定走 sdk.responses();github-copilot 默认让 GPT-5 及以上(gpt-5-mini 除外)走 Responses。

最后过滤:enabled_providers、disabled_providers 不放行的整家删除;deprecated 模型总被删除,alpha 模型要设 OPENCODE_ENABLE_EXPERIMENTAL_MODELS=true 才保留;提供方级的 whitelist、blacklist 按模型 ID 过滤;删空了的提供方一并移除。

默认模型与 small model

defaultModel()(provider.ts:2008)依次尝试:配置的 model;~/.local/state/opencode/model.json 的 recent 列表(TUI 切模型时写入)里第一个仍可用的;最后取第一个可用提供方,用 sort() 取第一。sort() 对关键词数组 ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"] 取 findIndex 后降序排列,越靠后的关键词优先级越高,不命中的排最后。

getSmallModel() 服务于生成会话标题这类轻量调用(session/prompt.ts:220):先看配置 small_model,再问插件钩子 experimental.provider.small_model,最后按家族优先级挑最新的一个,默认顺序是 gemini-flash、gpt-nano、claude-haiku;都找不到就退回主模型。

getLanguage:从 Model 到 LanguageModelV3

getLanguage(model)(provider.ts:1896)以 providerID/modelID 为键缓存,未命中时由 resolveSDK() 拿 SDK 实例,再调 custom loader 的 getModel,没有就用 sdk.languageModel(api.id)。resolveSDK() 先合并提供方 options、模型 headers、apiKey 与 baseURL(URL 里的 ${VAR} 占位用 vars loader 的值和环境变量替换),再包一层 fetch,给响应头与 SSE 分块间隔各加 300 秒超时(headerTimeout、chunkTimeout,设 false 关闭)。最后加载工厂:先查 BUNDLED_PROVIDERS 里 24 个随二进制打包的工厂(其中 @ai-sdk/github-copilot 这个键指向 OpenCode 自带的 packages/core/src/github-copilot/),查不到才现场安装:

const installedPath = await (async () => {
  if (model.api.npm.startsWith("file://")) {
    return model.api.npm
  }
  const item = await Npm.add(model.api.npm)
  if (!item.entrypoint) throw new Error(`Package ${model.api.npm} has no import entrypoint`)
  return item.entrypoint
})()
// ...
const mod = await import(importSpec)

const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]

(provider.ts:1842,删减后)

Npm.add()(packages/core/src/npm.ts:115)用 @npmcli/arborist 把包装进 ~/.cache/opencode/packages/ 下的独立目录,设 ignoreScripts: true,装过就复用;加载后取第一个以 create 开头的导出当工厂。因此任何遵循 AI SDK 约定的第三方包,配置里写上 npm 就能接入,本地开发还能用 file:// 路径。拿到的模型如何进入 streamText,见模型调用层。

配置覆盖与 variants

{
  "model": "anthropic/claude-sonnet-4-5",
  "provider": {
    "anthropic": {
      "models": { "claude-sonnet-4-5": { "variants": { "max": { "disabled": true } } } }
    },
    "local": {
      "npm": "@ai-sdk/openai-compatible",
      "options": { "baseURL": "http://localhost:8000/v1" },
      "models": { "qwen3-coder": { "limit": { "context": 131072, "output": 16384 } } }
    }
  }
}

覆盖已有提供方时,options 与目录深合并,模型字段逐项覆盖,没写的回落到目录值;新提供方写 npm、options.baseURL 与 models 即可,npm 缺省为 @ai-sdk/openai-compatible。variants 是“档位名 → 选项片段”的表:优先由 models.dev 的 reasoning_options 生成,否则用 ProviderTransform.variants() 的手写规则,再与配置的 variants 深合并,disabled: true 删掉一档。

V2 对照:可重放的 Catalog

迁移中的 packages/core/src/catalog.ts 把“启动时一次性叠加”换成“可重放的变换”:插件用 ctx.catalog.transform() 注册回调,按注册顺序作用于草稿,全部跑完后在 finalize 里执行 provider.use 策略(将取代 enabled_providers、disabled_providers)。models.dev 刷新、凭据变化或插件卸载都只需重放一遍。注册顺序中与目录相关的部分(packages/core/src/plugin/internal.ts:108)是 ModelsDevPlugin → 34 个提供方插件 → ConfigExternalPlugin → ConfigProviderPlugin → VariantPlugin,配置因而能覆盖目录与内置插件写入的值。注意 specs/v2/provider-model.md 里的 endpoint、options、enabled 等字段已被代码中的 api、request、disabled 取代,以代码为准;V2 会话如何选模型见 V2 会话内核。

下一篇:提供方适配层——拿到模型之后,transform.ts 如何抹平各家在消息、推理参数与工具 schema 上的差异。

本页目录