OmniRoute 免费档位(Free Regime)单一事实来源重构:派生集合与编译期完整性约束
OmniRoute 是一个面向多 Agent(Claude Code、Codex、OpenCode、Cline、Copilot 等)的免费 MIT 网关,内置 352 个 Provider、1200+ 模型,并维护一份经过人工调研与对抗验证的免费档位目录。本文以 changelog.d/maintenance/11537-free-regime-derived-sets.md 记录的 #11537 重构为主线,讲解 OmniRoute 如何把"哪些免费档位计入哪张汇总表、哪些允许无凭证捷径"这些原本靠人肉重复的回答,收敛到单一权威表
FREE_REGIME_TRAITS,并让上层派生集合全部从它推导而来。读完本文,你将理解免费档位freeType的完整分类体系、总量统计(steady/credit/one-time/uncapped)的归集规则,以及"新增一个档位类型必须把每个问题都回答一遍才能通过编译"这一约束背后的实现原理。
问题背景:被各处手写复制的"答案"
OmniRoute 的免费档位目录(open-sse/config/freeModelCatalog.data.ts)记录了数百条免费模型条目,每条都带一个 freeType 字段(FreeModelFreeType),声明它属于哪一种免费档位。在 #11537 之前,代码库里有多个地方要回答"这个档位是否仍授予免费访问""它的额度计入哪张汇总表""它是否允许走无凭证捷径"这三个问题:
- 判断某条目是否仍免费(决定导入、路由、模型列表可见性);
- 聚合"稳态每月可用免费 token"、"叠加循环额度"、"首月真实额度"等总量指标;
- 决定某个档位是否可以从合成无凭证路径直达、跳过实时额度校验。
过去这些答案是通过手工重复的集合、硬编码分支来维持的,存在真实风险:新增一个 freeType 时,维护者可能忘记更新某一处判断,从而让新档位"静默地"不计入任何总量、或者被错误地当作免费。
#11537 的解法是引入一张单一事实来源表 FREE_REGIME_TRAITS,把三个问题统一收录;原本手写重复的集合改为从这张表派生(derived)。由于派生代码要求表对 FreeModelFreeType 的每一个成员都穷尽地给出回答,此后新增任何 freeType 都会触发编译错误——直到它在每条轴上都有明确归类,否则无法编译通过。
单一事实来源:FREE_REGIME_TRAITS 与三条轴
核心表位于 open-sse/config/freeModelCatalog.ts,它定义了档位与"代码库会向档位提出的每个问题"之间的映射(源码注释称之为 FreeRegimeTraits,三条轴):
| 轴 | 字段 | 类型 | 含义 |
|---|---|---|---|
| 是否仍免费 | grantsFreeAccess |
boolean |
该档位是否仍授予免费访问(无付费前提下可否路由至此) |
| 计入哪张总量 | tokenBucket |
FreeRegimeTokenBucket |
该档位的额度归属哪一类汇总数字 |
| 可否无凭证捷径 | allowsNoAuthShortcut |
boolean |
经合成无凭证路径到达该档位时,能否跳过实时额度校验 |
FreeModelFreeType:档位的七种形态
目录条目通过 freeType 声明档位,完整取值(见 freeModelCatalog.ts):
recurring-daily—— 按日重置的循环免费额度(如 api-airforce、bazaarlink 等);recurring-monthly—— 按月重置的循环免费额度;recurring-credit—— 每月/周期补充的信用额度(credit 型循环赠送);recurring-uncapped—— 永久免费但上游未公布 token 上限(仅限速/并发限制);one-time-initial—— 一次性注册/初始赠送额度(通常只覆盖首月);keyless—— 该目录条目判定不存在任何凭证(详见下文的关键区分);discontinued—— 厂商把免费档位退役到付费 Key 之后,用于"记录已不免费"的档位。
FreeRegimeTokenBucket:总量的五类桶
汇总时每个档位的额度必须归属恰好一个桶(freeModelCatalog.ts):
steady-monthly—— 计入稳态循环头条数字(被汇总进 headline);recurring-credit—— 会补充的循环信用额度,紧挨稳态数字单独展示;one-time-credit—— 注册赠送额度,仅计入首月;uncapped—— 真实可用但无公布上限,只列出、绝不求和(避免把"限速额度×24/7"虚增进头条);none—— 不授予任何额度(即discontinued),因此不喂给任何数字。
FREE_REGIME_TRAITS 表体(#11537 引入的权威答案)
以下为源码 freeModelCatalog.ts 中 FREE_REGIME_TRAITS 的完整映射:
| freeType | grantsFreeAccess | tokenBucket | allowsNoAuthShortcut |
|---|---|---|---|
recurring-daily |
true |
steady-monthly |
false |
recurring-monthly |
true |
steady-monthly |
false |
recurring-credit |
true |
recurring-credit |
false |
recurring-uncapped |
true |
uncapped |
false |
one-time-initial |
true |
one-time-credit |
false |
keyless |
true |
steady-monthly |
true |
discontinued |
false |
none |
false |
注意表中没有为 recurring-uncapped 或 keyless 预留"可能被计费"的特例之外的分支——比如 recurring-uncapped 之所以只列不求和,正是因为它落在 uncapped 桶,而在聚合函数 dedupedSum 里只有 steady-monthly 的条目才进入稳态和。
编译期穷尽约束:satisfies Record<...>
表体声明为:
export const FREE_REGIME_TRAITS = {
"recurring-daily": { grantsFreeAccess: true, tokenBucket: "steady-monthly", allowsNoAuthShortcut: false },
// ...其余档位
} satisfies Record<FreeModelFreeType, FreeRegimeTraits>;
satisfies Record<FreeModelFreeType, FreeRegimeTraits> 的效果是:只要 FreeModelFreeType 增加一个新成员(例如未来的 sponsored-trial),这张对象字面量就缺少对应 key 而无法通过类型检查;同样,若 FreeRegimeTraits 增加新字段,则每个档位都必须补齐该字段才能编译。这正是 changelog 中"a new freeType no longer compiles until it has answered every question"(新增 freeType 在回答完每一个问题之前无法编译)的源码级含义。注释将其概括为 "Exhaustive by construction"(构造即穷尽)。
表体之上暴露了三组查询函数,供全库调用(freeModelCatalog.ts):
grantsFreeAccess(freeType)—— 读取grantsFreeAccess轴;freeTypesInBucket(bucket)—— 按tokenBucket轴反查出属于某桶的全部档位,返回Set<FreeModelFreeType>(这是总量聚合里STEADY_MONTHLY、RECURRING_CREDIT、ONE_TIME_CREDIT、UNCAPPED四个集合的来源);allowsNoAuthShortcut(freeType)—— 读取allowsNoAuthShortcut轴。
从表派生的集合:删除手写重复
派生体现在 src/shared/utils/freeModels.ts。此前"哪些 Provider 拥有免费模型""某 Provider 有哪些免费模型 id"这些集合需要手工维持,现在全部从 FREE_MODEL_BUDGETS 过滤 grantsFreeAccess 后派生:
/** Catalogued entries whose regime still grants free access. */
const FREE_BUDGETS = FREE_MODEL_BUDGETS.filter((m) => grantsFreeAccess(m.freeType));
/** Provider ids that have at least one documented free model. */
export const PROVIDERS_WITH_FREE_MODELS: Set<string> = new Set(FREE_BUDGETS.map((m) => m.provider));
const FREE_MODEL_IDS_BY_PROVIDER: Map<string, Set<string>> = (() => { /* ...按 provider 聚合 modelId... */ })();
对应源码见 freeModels.ts。三条关键收益:
- 一个
discontinued条目永远不可能被误报为免费——只要把档位标记为discontinued,它就从grantsFreeAccess过滤中被剔除,所有上层派生集合自动同步,无需再逐处手改; - 未来任何新档位"忘记被分类"会在编译期暴露,而不是在运行时静默地"不计入任何总量";
- 浏览器与服务器结论一致——该模块同时被 6 个
"use client"组件与服务器逻辑导入,决策路径只读这份随发布产物一起分发的静态目录,可离线复现、无需播种数据库即可做单元测试。
"免费判定"的两套来源:展示与决策被刻意分离
freeModels.ts 的模块头注释(freeModels.ts)说明了为什么"是否免费"由两套来源分别回答:
- 计数/展示(免费 token 总量、仪表盘):可以使用"已解析目录"(随发布基线 + Radar feed 叠加,见
getRadarCatalog),数字是信息性的,有 feed 时可以变好; - 决策(该 Provider/模型是否免费、是否导入、
auto/*是否路由到它、是否出现在GET /v1/models):只读随发布目录FREE_MODEL_BUDGETS加本地启发式,绝不触达 Radar 缓存。否则导入弹窗的预览与实际点击导入时的结果会不一致,且 DB 读取会把 SQLite 驱动拖进客户端包。
这也与 changelog.d/fixes/11550-free-tier-summary-catalog-source.md 记录的修复一脉相承:总量汇总标注其数据来源(静态基线 vs 雷达叠加),避免把"展示来源"误当成"决策来源"。
单模型免费的三条本地启发式
在目录条目之外,isFreeModel(provider, model)(freeModels.ts)对单个拉取到的模型判定免费,满足任一即命中:
- 显式标记
model.isFree === true; - 模型 id 带 OpenRouter 风格
:free后缀; pricing.prompt与pricing.completion均为 0;- 该 id 出现在
FREE_MODEL_IDS_BY_PROVIDER(即上表派生集合)中(provider 支持别名,会先经resolveProviderId归一化)。
这套判定被"仅导入免费模型"的连接选项(Add API Key 弹窗)、模型同步导入过滤器、sortModelsFreeFirst 稳定排序以及 selectModelsForImport 的 importFreeOnly 过滤共同复用。
无凭证捷径是一条独立的轴:避免与"是否需要 Key"混淆
allowsNoAuthShortcut 只回答路由/计费问题,不回答"此 Provider 需不需要 API Key"。源码在 freeModelCatalog.ts 中特意举了混淆两者代价的例子:blackbox、friendliai、iflytek、sparkdesk 被目录标记为 keyless(意味着不存在任何凭证,请求不可能被计费),但实际没有凭证调用会返回 401。"是否需要凭证"由 providerCredentialRequirement.ts 单独回答,作者在注释里建议两份代码保持分离。keyless 是唯一 allowsNoAuthShortcut === true 的档位——只有目录明确声明"根本不存在凭证"时,合成无凭证路径上的候选者才能跳过实时额度校验。
派生驱动总量统计:steady / credit / one-time / uncapped 的归集
computeFreeModelTotals(freeModelCatalog.ts)是消费派生集合的总量聚合器,输入默认为随发布的静态基线(opts.entries 缺省即 FREE_MODEL_BUDGETS),支持:
excludeTosAvoid:剔除tos === "avoid"的条目;entries:传入已解析的新鲜目录(如 Radar 叠加结果),其中enabled: false的条目与缺席等价(对应 changelog.d/maintenance/11550-free-tier-summary-catalog-source.md 附近的"汇总来源"话题)。
聚合时,模块顶部由 freeTypesInBucket 派生出四个档位集合(freeModelCatalog.ts):
const STEADY_MONTHLY = freeTypesInBucket("steady-monthly");
const RECURRING_CREDIT = freeTypesInBucket("recurring-credit");
const ONE_TIME_CREDIT = freeTypesInBucket("one-time-credit");
const UNCAPPED = freeTypesInBucket("uncapped");
随后 dedupedSum(freeModelCatalog.ts)按 poolKey 做池去重:同一共享池内取该字段的最大值只计一次,poolKey === null 的模型各自独立计。最终产出 FreeModelTotals(freeModelCatalog.ts):
| 字段 | 组成 |
|---|---|
steadyRecurringTokens |
池去重后的循环每月 token,头条"稳态"数字 |
steadyWithRecurringCreditsTokens |
稳态 + 循环信用(如每月赠送 credit 的套餐) |
firstMonthRealisticTokens |
稳态 + 循环 + 一次性注册额度(仅首月) |
boostMonthlyTokens |
一次性小额充值解锁的额外每月额度,单独上报、不进稳态头条 |
uncappedProviders |
永久免费但无公布上限的 Provider,列出、不求和的去重排序数组 |
modelCount / poolCount / perModel / headline |
模型数、池数、按月度额度降序的明细、格式化摘要 |
聚合中 dedupedSum 只对落入 STEADY_MONTHLY 的条目使用 monthlyTokens 字段,recurring-credit / one-time-credit 的条目改用 creditTokens——也就是总量的每个分量完全由派生集合决定,维护者不需要记得"哪种档位该进哪列"。
头条之外的"充值解锁"数字:FREE_TIER_BOOSTS
一次性小额存款永久提高循环免费额度的场景(如 OpenRouter 充值 $10 后免费池从 50 提升到 1000 req/day)被单独登记在 FREE_TIER_BOOSTS(freeModelCatalog.ts),且只在对应池仍有存活循环档位时才计入 boostMonthlyTokens——确保它永远不与稳态头条混淆。示例:
"openrouter-free": {
provider: "openrouter",
boostMonthlyTokens: 24_000_000,
note: "A one-time $10 lifetime top-up raises the free pool from 50 to 1000 requests/day (~24M tokens/month).",
}
维护与演进指南
新增一个档位类型
- 在
FreeModelFreeType(freeModelCatalog.ts)加成员; - 由于
FREE_REGIME_TRAITS使用satisfies Record<FreeModelFreeType, FreeRegimeTraits>,编译期会强制你在表中补齐grantsFreeAccess、tokenBucket、allowsNoAuthShortcut三轴(若FreeRegimeTraits未来新增字段,同理所有档位都要补); - 只要
tokenBucket归类正确,freeTypesInBucket与总量聚合自动获得该档位——它不可能"静默地不计入任何总量"; - 若新档位仍授予免费访问,它自动进入
FREE_BUDGETS,进而进入PROVIDERS_WITH_FREE_MODELS与FREE_MODEL_IDS_BY_PROVIDER。
退役一个免费档位
把对应条目的 freeType 改为 discontinued 即可:grantsFreeAccess 翻转为 false,条目立即从 FREE_BUDGETS 派生集合消失,isFreeModel、导入过滤器、auto/* 路由、GET /v1/models 可见性全部同步生效,总量也不再计入它。这条路径被源码注释明确为"供应商把免费档位退役到付费 Key 之后"的标准处理方式(freeModelCatalog.ts)。
数据文件本身:只读产物、人工调研
freeModelCatalog.data.ts 头部注明它是 AUTO-GENERATED(freeModelCatalog.data.ts):由"50-agent 网页调研 + 对抗性验证"的原始数据经补丁生成器刷新,"请勿手改,重新运行补丁生成器以刷新"。目录用 FREE_CATALOG_CURATED_AT 字面量(当前值 2026-08-30)而非文件 mtime 记录最近人工核对日期——因为独立构建会重写文件时间戳,会把几个月前的目录误报为"今天更新"。这意味着:任何数据字段的修改都应回到生成链,而 FREE_REGIME_TRAITS 这类结构性分类则在手写的源码表里集中演进——这正是 #11537 "让集合从表派生"重构要维持的清晰边界。
小结
#11537 把"免费档位 → 是否免费 / 计入哪张总量 / 可否无凭证捷径"三组本被各调用点手写复制的答案,收敛为 open-sse/config/freeModelCatalog.ts 中的权威表 FREE_REGIME_TRAITS,并用 satisfies Record 建立编译期穷尽约束。其收益链条环环相扣:
- 正确性:
discontinued条目不再可能被误判免费(src/shared/utils/freeModels.ts 全部集合由grantsFreeAccess过滤派生); - 可维护性:新增档位类型时,漏分类即编译失败,杜绝"静默不计入总量";
- 一致性:浏览器与服务器、预览与落地导入共用同一决策路径,总量展示与决策来源分离(详见 docs/getting-started/FREE-TIERS-GUIDE.md 与 docs/routing/STRICT_ZERO_COST.md 对零成本路由规则的延伸约束)。
如果你正在维护自己的多 Provider 免费档位目录,这条经验可以直接迁移:把"档位分类"做成穷尽三轴(是否授予、归入哪个数字、能否绕过额度检查)的单一映射表,让所有派生集合只依赖它——编译期替人肉记性把关。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python08
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00