首页
/ OmniRoute 免费档位(Free Regime)单一事实来源重构:派生集合与编译期完整性约束

OmniRoute 免费档位(Free Regime)单一事实来源重构:派生集合与编译期完整性约束

2026-09-07 21:20:57作者:庞眉杨Will

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.tsFREE_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-uncappedkeyless 预留"可能被计费"的特例之外的分支——比如 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_MONTHLYRECURRING_CREDITONE_TIME_CREDITUNCAPPED 四个集合的来源);
  • 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。三条关键收益:

  1. 一个 discontinued 条目永远不可能被误报为免费——只要把档位标记为 discontinued,它就从 grantsFreeAccess 过滤中被剔除,所有上层派生集合自动同步,无需再逐处手改;
  2. 未来任何新档位"忘记被分类"会在编译期暴露,而不是在运行时静默地"不计入任何总量";
  3. 浏览器与服务器结论一致——该模块同时被 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)对单个拉取到的模型判定免费,满足任一即命中:

  1. 显式标记 model.isFree === true
  2. 模型 id 带 OpenRouter 风格 :free 后缀;
  3. pricing.promptpricing.completion 均为 0
  4. 该 id 出现在 FREE_MODEL_IDS_BY_PROVIDER(即上表派生集合)中(provider 支持别名,会先经 resolveProviderId 归一化)。

这套判定被"仅导入免费模型"的连接选项(Add API Key 弹窗)、模型同步导入过滤器、sortModelsFreeFirst 稳定排序以及 selectModelsForImportimportFreeOnly 过滤共同复用。

无凭证捷径是一条独立的轴:避免与"是否需要 Key"混淆

allowsNoAuthShortcut 只回答路由/计费问题,不回答"此 Provider 需不需要 API Key"。源码在 freeModelCatalog.ts 中特意举了混淆两者代价的例子:blackbox、friendliai、iflytek、sparkdesk 被目录标记为 keyless(意味着不存在任何凭证,请求不可能被计费),但实际没有凭证调用会返回 401。"是否需要凭证"由 providerCredentialRequirement.ts 单独回答,作者在注释里建议两份代码保持分离。keyless 是唯一 allowsNoAuthShortcut === true 的档位——只有目录明确声明"根本不存在凭证"时,合成无凭证路径上的候选者才能跳过实时额度校验。

派生驱动总量统计:steady / credit / one-time / uncapped 的归集

computeFreeModelTotalsfreeModelCatalog.ts)是消费派生集合的总量聚合器,输入默认为随发布的静态基线(opts.entries 缺省即 FREE_MODEL_BUDGETS),支持:

聚合时,模块顶部由 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");

随后 dedupedSumfreeModelCatalog.ts)按 poolKey池去重:同一共享池内取该字段的最大值只计一次,poolKey === null 的模型各自独立计。最终产出 FreeModelTotalsfreeModelCatalog.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_BOOSTSfreeModelCatalog.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).",
}

维护与演进指南

新增一个档位类型

  1. FreeModelFreeTypefreeModelCatalog.ts)加成员;
  2. 由于 FREE_REGIME_TRAITS 使用 satisfies Record<FreeModelFreeType, FreeRegimeTraits>编译期会强制你在表中补齐 grantsFreeAccesstokenBucketallowsNoAuthShortcut 三轴(若 FreeRegimeTraits 未来新增字段,同理所有档位都要补);
  3. 只要 tokenBucket 归类正确,freeTypesInBucket 与总量聚合自动获得该档位——它不可能"静默地不计入任何总量";
  4. 若新档位仍授予免费访问,它自动进入 FREE_BUDGETS,进而进入 PROVIDERS_WITH_FREE_MODELSFREE_MODEL_IDS_BY_PROVIDER

退役一个免费档位

把对应条目的 freeType 改为 discontinued 即可:grantsFreeAccess 翻转为 false,条目立即从 FREE_BUDGETS 派生集合消失,isFreeModel、导入过滤器、auto/* 路由、GET /v1/models 可见性全部同步生效,总量也不再计入它。这条路径被源码注释明确为"供应商把免费档位退役到付费 Key 之后"的标准处理方式(freeModelCatalog.ts)。

数据文件本身:只读产物、人工调研

freeModelCatalog.data.ts 头部注明它是 AUTO-GENERATEDfreeModelCatalog.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 建立编译期穷尽约束。其收益链条环环相扣:

如果你正在维护自己的多 Provider 免费档位目录,这条经验可以直接迁移:把"档位分类"做成穷尽三轴(是否授予、归入哪个数字、能否绕过额度检查)的单一映射表,让所有派生集合只依赖它——编译期替人肉记性把关。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391