Provider 规则、模型目录与选项映射
ZCode 用一份随包分发、可远程更新的规则集描述账号型 Provider、第三方模板和每个模型的能力,再叠上个人 BYOK 配置与账号权益两层;“推理强度”“输出上限”两个统一选项则由一门受限的 CEL 小语言翻译成各家请求体字段。
ZCode 不在代码里写死“有哪些模型、每个模型能做什么”。这些事实放在仓库根的 config/provider/zcode-builtin.json:一份 6212 行、约 18 万字节的规则集,随客户端打包,运行时还会从 ZCode 平台拉取新版本。它回答三个问题:有哪些 Provider(账号型套餐与第三方模板),每个模型有什么能力(上下文窗口、多模态、工具调用),以及“推理强度”“输出上限”这两个统一选项在不同协议下该写成哪些请求字段。
这一篇讲规则集本身、它怎样与个人配置和账号状态叠成一份 Registry、模型选择怎样解析与持久化,以及选项映射这门小语言。规则解析出的事实怎样变成一次模型请求,留给下一篇模型适配层;账号登录与各档套餐见账号、Coding Plan 与闲时计划。
| 位置 | 职责 |
|---|---|
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)。
几条叠加规则值得记住:
- 覆盖语义统一:稀疏层里缺省的字段继承下层,
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):
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 与闲时计划。一条账号型规则的主体长这样(zcode-builtin.json:819):
"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 依赖 models.dev、MiniMax Code 内置 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):
~/.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,内容为虚构):
{
"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 与闲时计划。 - 两种模型覆盖:
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):
{
"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):
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)。
下一篇:模型适配层——Registry 里的 Provider 与模型事实,怎样经由 Vercel AI SDK 变成一次真正的流式请求。