# Provider 规则、模型目录与选项映射

> ZCode 用一份随包分发、可远程更新的规则集描述账号型 Provider、第三方模板和每个模型的能力，再叠上个人 BYOK 配置与账号权益两层；“推理强度”“输出上限”两个统一选项则由一门受限的 CEL 小语言翻译成各家请求体字段。

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

ZCode 不在代码里写死“有哪些模型、每个模型能做什么”。这些事实放在仓库根的 `config/provider/zcode-builtin.json`：一份 6212 行、约 18 万字节的规则集，随客户端打包，运行时还会从 ZCode 平台拉取新版本。它回答三个问题：有哪些 Provider（账号型套餐与第三方模板），每个模型有什么能力（上下文窗口、多模态、工具调用），以及“推理强度”“输出上限”这两个统一选项在不同协议下该写成哪些请求字段。

这一篇讲规则集本身、它怎样与个人配置和账号状态叠成一份 Registry、模型选择怎样解析与持久化，以及选项映射这门小语言。规则解析出的事实怎样变成一次模型请求，留给下一篇[模型适配层](https://daiw.org/manual/zcode/model-adapters)；账号登录与各档套餐见[账号、Coding Plan 与闲时计划](https://daiw.org/manual/zcode/accounts-plans)。

| 位置 | 职责 |
| --- | --- |
| `config/provider/zcode-builtin.json` | 内置规则集，代码里称为 ZCode Built-in |
| `packages/provider` | 纯领域层：zod schema、规则叠加、三层解析、Registry、模型选择（4827 行） |
| `packages/provider-node` | Node 侧 IO：随包基线、缓存、远程同步、个人配置文件（1895 行） |
| `packages/model-option-map` | 选项映射小语言（867 行） |
| `packages/shared/src/model-*.ts` 等 | 跨层共享的数据契约 |
| `scripts/builtin-provider-config.mjs` | 构建期校验并暂存规则集 |

## 三层配置叠成一份 Registry

运行时的 Provider 事实来自三层：Built-in 规则集、个人配置文件 `provider_config.json`、账号服务给出的权益与连接状态。`ProviderConfigResolver` 把它们叠起来，只把完整、可执行的 Provider 与模型发布进 `ProviderRegistry`（`packages/provider/src/resolver.ts:182`）。

```mermaid
flowchart LR
  B["随包 zcode-builtin.json"] --> S["Built-in Source<br/>取 revision 较新者"]
  C["Active 缓存<br/>CDN 下载"] --> S
  P["provider_config.json<br/>个人 BYOK"] --> CS["ProviderConfigService"]
  S --> CS
  CS --> R["ProviderConfigResolver"]
  A["账号层<br/>权益与当前连接"] --> R
  R --> G["ProviderRegistry"]
  G --> M["模型选择与 ModelFactory"]
```

几条叠加规则值得记住：

- **覆盖语义统一**：稀疏层里缺省的字段继承下层，`null` 表示清空，嵌套配置递归覆盖，其余值（包括数组）整体替换（`packages/provider/src/config-overlay.ts:26`、`30`）。
- **账号层只能改内置的账号型 Provider**：不在 Built-in 里的 ID 会被丢掉（`resolver.ts:184`）；它能写的只有权益 `entitled` 和 Start Plan 的模型清单（`packages/provider/src/account-provider-resolution.ts:106`）。反过来，个人配置不许给 `account:` 前缀的 Provider 声明访问方式（`packages/provider/src/config/rule-data-schema.ts:116`）。
- **模板在最底**：带 `templateId` 的 Provider 先铺模板配置，再叠自己的覆盖（`resolver.ts:192`、`201`）。
- **进 Registry 的门槛**：Provider 配置要能通过完整 schema 校验，模型要 `enabled`，账号型 Provider 还要有权益且不是“非当前连接”（`resolver.ts:249`、`253`）；`visibility: "hidden"` 的 Provider 模型可执行、但不出现在选择器里（`resolver.ts:280`）。
- **排序**：`zai-family`、`bigmodel-family` 两组账号型 Provider 永远在最前，其后是其他内置与个人 Provider，个人可以用 `providerOrder` 调整后者（`resolver.ts:355`）。

## 规则集：一个文件，七类规则

文件头只有三个字段：`schemaVersion` 为 1，`revision` 为 30，其余都在 `config` 里（`config/provider/zcode-builtin.json:2`、`3`）。用脚本统计各组条数：

| 规则组 | 条数 | 匹配键 | 作用 |
| --- | --- | --- | --- |
| `providerConfigRules.templateRules` | 20 | `templateId` | 第三方模板：访问方式、API 类型与地址、内置模型清单、图标 |
| `providerConfigRules.providerRules` | 8 | `providerId` | 固定的账号型 Provider，ID 都以 `account:` 开头 |
| `modelConfigRules.modelRules` | 84 | `modelMatch` 正则 | 按模型 ID 给能力与选项规格 |
| `modelConfigRules.modelApiRules` | 72 | 另加 `apiTypeMatch` | 同一模型在不同协议下的差异，主要是选项映射 |
| `modelConfigRules.providerSiteRules` | 52 | 另加 `baseUrlMatch` | 同一模型在某个站点上的差异 |
| `modelConfigRules.templateModelRules` | 244 | `templateId` 加 `modelId` | 模板里每个模型默认开不开 |
| `modelConfigRules.builtinProviderModelRules` | 26 | `providerId` 加 `modelId` | 账号型 Provider 的模型启用 |

五组模型规则解析时按固定顺序拼成一条链：`model`、`model-api`、`provider-site`、`template-model`、`provider-model`，个人配置的精确规则排在最后（`packages/provider/src/config/schema.ts:78`、`packages/provider/src/config/model-config.ts:379`）。`resolve` 从空配置出发逐条覆盖（`config/model-config.ts:387`）：

```ts
  resolve(input: ModelConfigRuleResolutionInput): ModelConfig {
    let result = ModelConfig.empty();
    const baseUrl = input.baseUrl == null ? undefined : normalizeBaseURLForRuleMatch(input.baseUrl);
    for (const rule of this.#rules) {
      if (isExactModelRule(rule)) {
        if (rule.providerId !== input.providerId || rule.modelId !== input.modelId) continue;
  // ...
      if (rule.type === "template-model") {
        if (rule.templateId === input.templateId && rule.modelId === input.modelId)
          result = result.overlay(rule.config);
        continue;
      }
      // 只放宽推荐规则匹配，不改真实请求里的模型 ID。
      if (!matchesRule(rule.modelMatch, input.modelId, true)) continue;
      if (
        (rule.type === "model-api" || rule.type === "provider-site") &&
        rule.apiTypeMatch !== undefined
      ) {
        if (input.apiType == null || !matchesRule(rule.apiTypeMatch, input.apiType)) continue;
      }
      if (
        rule.type === "provider-site" &&
        (baseUrl === undefined || !matchesRule(rule.baseUrlMatch, baseUrl))
      )
        continue;
      result = result.overlay(rule.config);
    }
    return result;
  }
```

正则一律按 `^(?:pattern)$` 整串匹配，写入时就校验能否编译（`rule-data-schema.ts:14`）；`modelMatch` 忽略大小写，协议与地址则区分大小写，地址匹配前会规范化主机大小写、默认端口和尾部斜杠（`config/model-config.ts:544`、`548`）。

链条的起点是一条 `.*` 默认规则：启用、上下文 200000、只收文本、支持工具调用、输出上限 32000、推理档位只有 `disabled` 与 `enabled` 且映射为空对象（`zcode-builtin.json:867`）。以个人 Coding Plan 上的 GLM-5.3 为例，后面依次命中：`glm-5` 家族规则（上下文 200000、输出 64000，`zcode-builtin.json:918`），`glm-5.3` 专属规则（上下文 1000000、档位改为 `low`、`high`、`max`、输出 128000，`zcode-builtin.json:978`），三条 `anthropic-messages` 协议规则逐步把推理映射改写成 GLM-5.3 的形态（`zcode-builtin.json:2594`），站点规则再为 `api.z.ai` 打开图片、视频输入与原生 WebSearch（`zcode-builtin.json:4012`、`4092`），最后是一条精确启用规则。模型规则本身把 GLM-5.3 的图片输入写成 `false`，站点规则才把它打开；前端注释说明这是套餐端的服务端桥接，因此界面刻意不给它显示视觉徽标（`packages/ui/src/lib/modelVisionBadge.ts:3`）。

## 账号型 Provider 与模板

8 条 `providerRules` 是 z.ai 与 bigmodel 两个家族各四个账号型 Provider，访问方式都是 `zhipu-account`，协议都是 `anthropic-messages`：

| 家族 | 个人 Coding Plan | 团队 Coding Plan | Start Plan | 闲时（Idle plan） |
| --- | --- | --- | --- | --- |
| z.ai（`zai-family`） | `https://api.z.ai/api/anthropic` | 同左 | `https://zcode.z.ai/api/v1/zcode-plan/anthropic` | `https://zcode.z.ai/api/v1/off-peak/anthropic`，隐藏 |
| bigmodel（`bigmodel-family`） | `https://open.bigmodel.cn/api/anthropic` | 同左 | 与 z.ai 相同 | 与 z.ai 相同，隐藏 |

各档套餐的模型、权益与凭据见[账号、Coding Plan 与闲时计划](https://daiw.org/manual/zcode/accounts-plans)。一条账号型规则的主体长这样（`zcode-builtin.json:819`）：

```json
          "providerId": "account:zai-offpeak-idle-plan",
          "providerName": "Z.AI Idle plan",
          "config": {
            "group": "zai-family",
            "builtinModelIds": ["GLM-5.3", "GLM-5.3-Flash"],
            "visibility": "hidden",
            "access": {
              "type": "zhipu-account",
              "mode": "off-peak",
              "accountType": "zai"
            },
            "api": {
              "type": "anthropic-messages",
              "baseUrl": "https://zcode.z.ai/api/v1/off-peak/anthropic"
            },
```

`mode` 只有 `start-plan`、`individual-coding-plan`、`team-coding-plan`、`off-peak` 四种（`packages/provider/src/config/provider-data-schema.ts:14`）。

20 个模板是用户新建 Provider 时的起点，覆盖三种 API 类型 `anthropic-messages`、`openai-chat-completions`、`openai-responses`（`provider-data-schema.ts:4`）。下表的“默认启用”按 `templateModelRules` 统计，其余模型列在模板里但默认关闭：

| 模板 | API 类型 | 模型（默认启用/总数） |
| --- | --- | --- |
| `zai-api`、`bigmodel-api`（Z.ai、BigModel Coding Plan） | anthropic-messages | 2/2 |
| `zai-standard-api`、`bigmodel-standard-api`（Z.ai、BigModel API） | openai-chat-completions | 2/24 |
| `moonshot-kimi`、`minimax`、`deepseek`、`xiaomi-mimo` | anthropic-messages | 3/6、3/8、2/2、2/2 |
| `qwen-alibaba-model-studio-cn`、`-intl`（阿里云百炼中国、国际） | anthropic-messages、openai-chat-completions | 2/13、2/14 |
| `openai`、`xai` | openai-responses | 4/10、2/3 |
| `anthropic` | anthropic-messages | 5/5 |
| `openrouter` | anthropic-messages | 11/59 |
| `opencode-go-chat`、`-messages`、`-responses`（OpenCode Go） | 三种各一 | 8/15、3/8、2/2 |
| `opencode-zen-chat`、`-messages`、`-responses`（OpenCode Zen） | 三种各一 | 10/14、5/15、2/14 |

模板的访问方式几乎都是普通 `api-key`，只有两个 Coding Plan 模板是 `zhipu-coding-plan-api-key`（`provider-data-schema.ts:32`）。新建模板实例时，ID 取模板 ID 的小写短横线形式，冲突时加 `-2`、`-3` 后缀；不带模板的自定义 Provider 从 `new-provider` 起名（`packages/provider/src/config-service.ts:688`）。与 [OpenCode](https://daiw.org/manual/opencode/provider-catalog) 依赖 models.dev、[MiniMax Code](https://daiw.org/manual/minimax-code/model-system) 内置 models.dev 快照不同，ZCode 的模型目录完全由这份自维护的规则集给出。

## 随包分发与远程更新

**构建期**。`loadBuiltinProviderConfig` 读取规则集（可用 `ZCODE_BUILTIN_PROVIDER_CONFIG_FILE` 换源），借 tsx 加载运行时同一个 `decodeZCodeBuiltinRelease` 做完整校验，再原样写进产物目录（`scripts/builtin-provider-config.mjs:43`、`70`）；构建环境 `ZCODE_ENV` 只能是 `test` 或 `production`（`builtin-provider-config.mjs:35`）。

**启动时**。CLI 在需要 Provider 的命令前准备路径：SEA 单文件里的资源键是 `zcode-provider/zcode-builtin.json`，释放到 `~/.zcode/v2/runtime/provider/bundled/zcode-builtin.json`；普通安装则找入口旁的 `provider/zcode-builtin.json` 或仓库里的原文件（`apps/zcode-cli/packages/cli/src/provider-runtime-env.ts:137`、`149`）。可更新的副本叫 Active 缓存，路径按平台、App 版本和 ZCode 控制面地址隔离（`packages/provider-node/src/zcode-builtin-cache-paths.ts:24`）：

```text
~/.zcode/v2/runtime/provider/<平台>/<App 版本>/endpoint-<地址 sha256 前 32 位>/zcode-builtin.json
```

随包基线与 Active 缓存并存时取 `revision` 大的；两者 `revision` 相同但内容不同，视为缓存损坏，以随包版本为准并重写缓存（`packages/provider-node/src/zcode-builtin-provider-config-source.ts:180`）。

**远程同步**。控制面地址取 `ZCODE_BASE_URL` 或 `ZCODE_ENDPOINT_ORIGIN`，缺省 `https://zcode.z.ai`（`packages/shared/src/zcodeEndpoint.ts:3`、`142`）。客户端先请求 `GET /api/v1/client/configs`（带 `app_version` 与 `platform`），从 `data.configs.builtin_provider_config_json` 拿到 CDN 地址，再下载整份规则集（`packages/provider-node/src/zcode-builtin-download.ts:50`、`54`）。

- 配置运行时每 60 秒检查一次（`packages/provider-node/src/provider-config-runtime.ts:120`），真正下载受控制文件节流：成功后 1 小时内不再下载，失败按 60 秒起翻倍退避、最长 1 小时；多个进程靠控制文件上的 30 秒租约合并刷新，网络请求期间不持有文件锁（`packages/provider-node/src/zcode-builtin-remote-synchronizer.ts:48`、`96`、`151`）。
- 两段请求共用 20 秒预算，正文上限 10000000 字节，请求不带凭据、不跟随重定向，CDN 地址必须是不带用户名密码的 https（`zcode-builtin-download.ts:14`、`43`、`79`、`103`）。
- 应用时 `revision` 更小记为 `stale`，相同且内容一致为 `unchanged`，相同却内容不同直接报错，更大才写入（`zcode-builtin-provider-config-source.ts:64`）；含已下线的 `builtin:zapi` 的整份 Release 会被拒绝（`packages/provider-node/src/zcode-builtin-release.ts:43`）。

README 把 `ZCODE_BUILTIN_PROVIDER_CONFIG_FILE` 描述为“本地 Provider 配置文件路径”（`README.md:132`）。从代码看它指的是 Built-in 规则集，不是个人配置；在独立运行的 CLI 里，效果还取决于是否同时设置了 `ZCODE_PERSONAL_PROVIDER_CONFIG_FILE`：两个都设时直接使用、不做远程同步；只设前者时它只是随包基线，Active 缓存里 `revision` 更大的远程版本仍会胜出（`provider-runtime-env.ts:58`、`86`，`apps/zcode-cli/packages/bootstrap/src/app/process-provider-registry-runtime.ts:59`）。

## 个人 Provider：provider_config.json

BYOK 配置、模型覆盖和默认模型选择都在同一个文件里：`~/.zcode/v2/provider_config.json`（`provider-runtime-env.ts:76`），数据基目录可用 `ZCODE_DATA_BASE_DIR` 改，文件路径可用 `ZCODE_PERSONAL_PROVIDER_CONFIG_FILE` 指定，但它必须与 Built-in 路径变量同时出现（`packages/provider-node/src/runtime-paths.ts:21`）。顶层结构由 `packages/provider-node/src/provider-config-file-codec.ts:18` 定义，下面是一个示意（字段按 schema，内容为虚构）：

```json
{
  "schemaVersion": 1,
  "config": {
    "providerOrder": ["deepseek"],
    "providerConfigRules": {
      "providerRules": [{
        "providerId": "deepseek", "templateId": "deepseek", "providerName": "DeepSeek",
        "config": {
          "group": "standard-personal",
          "access": { "type": "api-key", "apiKey": "<API Key>" },
          "personalModelIds": [], "modelOrder": []
        }
      }]
    },
    "modelConfigRules": { "providerModelRules": [], "manualProviderModelRules": [] },
    "defaultModelSelection": {
      "providerId": "deepseek", "modelId": "deepseek-v4-pro",
      "options": { "reasoningLevel": "max" }
    }
  }
}
```

- **API Key 明文存放**：Key 是 `access.apiKey` 字段（`provider-data-schema.ts:30`），文件以 `0600` 权限原子写入，没有另做加密（`packages/provider-node/src/personal-provider-config-repository.ts:148`，`packages/shared/src/node/privateFilePersistence.ts:100`）。账号凭据另存一处并逐项加密，见[账号、Coding Plan 与闲时计划](https://daiw.org/manual/zcode/accounts-plans)。
- **两种模型覆盖**：`providerModelRules` 是“推荐配置加稀疏覆盖”，`manualProviderModelRules` 是手动模式，必须给齐产品开放的可编辑叶子（上下文、结构化输出、原生搜索、会话中途 system、图片视频 PDF、推理档位与映射、输出上限），同一 Provider 与模型不能两种都声明（`packages/provider/src/config/manual-model-config.ts:6`，`rule-data-schema.ts:64`）。
- **读写**：写入在文件锁内先规范化、再严格校验整份结果；多进程靠每秒一次的轮询发现外部修改（`personal-provider-config-repository.ts:48`、`143`）。文件损坏时原样保留，本进程以空的个人层降级运行，等用户修复（`personal-provider-config-repository.ts:159`）。

## 模型选择：解析与持久化

一次模型选择就是 `providerId`、`modelId` 加可选的 `options.reasoningLevel`（`packages/shared/src/model-selection.ts:4`）。TUI 里 `/model <provider/model>` 切模型，可以写成 `provider/model$level` 一并指定档位（`src/model-selection.ts:31`、`43`）；`/effort <level>`（别名 `/variant`）只改档位，`/effort list` 列出可选值（`apps/zcode-cli/packages/cli/src/command-center/handlers/effort.ts:6`）。两者成功后都把当前选择写回 `defaultModelSelection`，写失败只追加一条警告，不影响本会话已切换的模型（`apps/zcode-cli/packages/cli/src/command-center/model-selection.ts:4`）。

档位值来自有效模型配置的 `reasoningLevel.values`，约定按强度从低到高排列（`packages/shared/src/model-config.ts:23`）。用户主动选模型、没指定档位时取最高一档（`apps/zcode-cli/packages/bootstrap/src/app/provider-registry-selection.ts:196`）；标题生成、记忆抽取、网页内容处理这类辅助调用固定取最低一档，输出不超过 5000 token（`apps/zcode-cli/packages/core/src/model/auxiliary-model-options.ts:3`）。

新会话的初始选择由 `resolveInitialModelSelection` 决定：配置的默认值可选、且档位合法才用；否则按 Registry 顺序找第一个可见模型并补最高档（`packages/provider/src/model-selection-config.ts:28`、`58`）。`zcode login` 写下的默认值只有 `providerId` 与 `modelId`、不带档位（`apps/zcode-cli/packages/bootstrap/src/auth-login.ts:355`），按这段逻辑会走 Registry 兜底；账号型 Provider 排在 Registry 最前，所以通常还是登录时那个模型，只是档位成了最高一档。恢复旧会话时不套用这套初始推荐（`apps/zcode-cli/packages/bootstrap/src/app/runtime-config.ts:230`）。

已有选择在执行前还要解析一次“有效选择”（`packages/provider/src/effective-model-selection.ts:17`）：

- 指向个人或团队 Coding Plan 的选择，会被换成当前账号唯一处于 `current` 的那个 Coding Plan Provider，没有或不止一个就报 `account-connection-unavailable`（`effective-model-selection.ts:29`）。Start Plan 按普通 Provider 处理，不参与这种换算（`packages/provider-node/src/model-selection-facade.ts:17`）。
- 隐藏 Provider 只对闲时 Provider 放行（`effective-model-selection.ts:44`）。
- 旧版本保存的档位 `off`、`nothink` 会按一份精确匹配原规则的改名清单换成 `disabled`；个人配置改过这个模型的档位时不做换算（`packages/provider-node/src/legacy-reasoning-level.ts:18`，`packages/provider-node/src/legacy-reasoning-level-renames.ts:2`）。旧的 `builtin:` 前缀 Provider ID 另有一次性迁移（`packages/shared/src/legacy-model-provider-identity.ts:22`）。

## model-option-map：一门受限的 CEL

规则里的 `reasoningLevel.map` 与 `maxOutputTokens.map` 是表达式字符串，求值结果是要合并进请求体的 JSON 补丁。代码把它叫 Restricted CEL，在 schema 校验时就编译一遍（`packages/shared/src/model-config.ts:5`），Model 绑定时再编译成可复用的程序，请求时只代入本轮冻结的值（`packages/model-option-map/src/option-maps.ts:19`）。

| 组件 | 做什么 |
| --- | --- |
| tokenizer | 标识符、单双引号字符串、JSON 安全的数字，运算符 `&&`、`!=`、`>=` 等与 `+ - * / % ! < >`（`packages/model-option-map/src/tokenizer.ts:18`） |
| parser | 优先级由低到高：三元条件、逻辑或、逻辑与、相等、比较、加减、乘除、一元运算（`packages/model-option-map/src/parser.ts:79`） |
| evaluator | 强类型求值：条件必须是布尔值，算术只接受数字，结果必须 JSON 安全（`packages/model-option-map/src/evaluator.ts:147`、`161`） |
| compiler | 按“变量名加源码”缓存，并要求顶层（含三元的两个分支）是对象（`packages/model-option-map/src/compiler.ts:80`） |
| merge-patch | 按顺序把补丁合并进请求体，并检查两个选项是否写了同一路径（`packages/model-option-map/src/merge-patch.ts:13`） |

它刻意很小：唯一可用的变量就是选项本身，在 `reasoningLevel` 的映射里引用 `maxOutputTokens` 会报未知标识符；没有成员访问和函数调用；对象键必须是字符串字面量且不能重复（`parser.ts:61`、`159`、`167`、`198`）。一条真实的规则，`anthropic-messages` 协议下所有模型的默认映射（`zcode-builtin.json:2494`）：

```json
        {
          "modelMatch": ".*",
          "apiTypeMatch": "anthropic-messages",
          "config": {
            "optionSpecs": {
              "reasoningLevel": {
                "map": "reasoningLevel == \"disabled\"\n  ? {\n      \"thinking\": {\n        \"type\": \"disabled\"\n      }\n    }\n  : {\n      \"thinking\": {\n        \"type\": \"adaptive\"\n      },\n      \"output_config\": {\n        \"effort\": reasoningLevel == \"enabled\" ? \"high\" : reasoningLevel\n      }\n    }"
              },
              "maxOutputTokens": {
                "map": "{'max_tokens': maxOutputTokens}"
              }
            }
          }
        },
```

把 `packages/model-option-map` 复制到临时目录，用规则集里的真实表达式求值，得到：

| 规则 | 输入 | 补丁 |
| --- | --- | --- |
| anthropic 默认 | `disabled` | `{"thinking":{"type":"disabled"}}` |
| anthropic 默认 | `enabled` | `{"thinking":{"type":"adaptive"},"output_config":{"effort":"high"}}` |
| anthropic 默认 | `max` | `{"thinking":{"type":"adaptive"},"output_config":{"effort":"max"}}` |
| GLM-5.3 在 anthropic 下 | `low` | `{"thinking":{"type":"enabled"},"output_config":{"effort":"low"}}` |
| chat-completions 默认 | `high` | `{"thinking":{"type":"enabled"},"enable_thinking":true,"reasoning_effort":"high","reasoning":{"effort":"high"}}` |
| responses 默认 | `disabled` | `{"reasoning":{"effort":"none"}}` |
| anthropic 输出上限 | `128000` | `{"max_tokens":128000}` |

chat-completions 的默认映射一次写四个字段，覆盖几家兼容服务各自的思考开关方言（`zcode-builtin.json:2514`）；具体模型或站点再用后面的规则收窄。整个文件里一共只有 25 种不同的映射表达式。合并时两个选项的补丁按顺序叠到 SDK 生成的请求体上，并记下每个补丁写过的路径（`merge-patch.ts:19`）：

```ts
  for (const namedPatch of patches) {
    const paths = collectWrittenPaths(namedPatch.patch);
    for (const path of paths) {
      const conflict = ownedPaths.find((owned) => pathsOverlap(owned.path, path));
      if (conflict) {
        throw new ModelOptionMapError(
          `Model option maps write conflicting JSON path ${formatPath(path)}: ${conflict.option} and ${namedPatch.option}`,
        );
      }
      ownedPaths.push({ option: namedPatch.option, path });
    }
    result = mergeObject(result, namedPatch.patch);
  }
  return result;
}
```

补丁里的 `null` 删除字段，对象递归合并，其余值覆盖（`merge-patch.ts:63`）；所以 `max_tokens` 这类 SDK 已经写过的字段会被映射结果改写，而两个选项互相踩路径则直接报错。这个合并发生在适配层拦截的 `fetch` 里，请求体不是 JSON 文本时宁可失败也不静默跳过（`apps/zcode-cli/packages/adapters/src/model/model-option-map-fetch.ts:38`）。

下一篇：[模型适配层](https://daiw.org/manual/zcode/model-adapters)——Registry 里的 Provider 与模型事实，怎样经由 Vercel AI SDK 变成一次真正的流式请求。
