# 提供方目录与 models.dev

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

- 作者：David（道雾轩）
- 专栏：OpenCode 源码解读（https://daiw.net/manual/opencode.md）
- 最后更新：2026-09-15
- 原文：https://daiw.net/manual/opencode/provider-catalog
- 转载与引用：请注明出处并附原文链接（https://daiw.net/about/copyright）

OpenCode 没有在仓库里手写模型清单。“有哪些模型、各有什么能力和价格”交给社区维护的 [models.dev](https://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`，删减后）：

```ts
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` 复制进来：

```mermaid
flowchart TD
  A["models.dev 转成 database"] --> B["插件 provider.models 改写模型表"]
  B --> C["配置 provider 扩充 database"]
  C --> D["环境变量命中：source = env"]
  D --> E["auth.json 的 API key：source = api"]
  E --> F["插件 auth.loader 注入 options"]
  F --> G["内置 custom loader：看 autoload"]
  G --> H["配置再合并：source = config"]
  H --> I["过滤"]
```

- **环境变量**：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/`），查不到才现场安装：

```ts
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`，见[模型调用层](https://daiw.net/manual/opencode/llm-bridge)。

## 配置覆盖与 variants

```json
{
  "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 会话内核](https://daiw.net/manual/opencode/session-v2)。

下一篇：[提供方适配层](https://daiw.net/manual/opencode/provider-transform)——拿到模型之后，`transform.ts` 如何抹平各家在消息、推理参数与工具 schema 上的差异。
