Quota Sharing Engine (中文 (简体))
配额共享引擎在共享同一连接的多个 API 密钥之间,公平分配提供者基于时间的配额(例如 Codex 的 5 小时窗口、Kimi 的 1500 次请求/小时)。
它解决的问题: OmniRoute 使用许多 API 密钥代理访问同一个上游提供者账户。如果没有共享逻辑,密钥 A 的突发请求可能会耗尽提供者在当前小时内的配额,导致密钥 B 和 C 在窗口重置前一直被阻止。该引擎通过以下方式防止这种情况:
- 按维度(%、请求数、令牌数、$)跟踪每个密钥的滚动用量。
- 应用工作保持型公平份额算法:只要全局池尚未饱和,密钥就可以借用闲置份额。
- 在请求到达上游执行器之前,于热路径(
chatCore.ts)中强制执行计算结果。
算法:工作保持型公平份额
Section titled “算法:工作保持型公平份额”实现在 src/lib/quota/fairShare.ts 中。
| 条件 | 模式 | 行为 |
|---|---|---|
globalUsedPercent < saturationThreshold |
宽松 | 密钥最多可借用全局限制减去总消耗量后的剩余额度 |
globalUsedPercent >= saturationThreshold |
严格 | 严格执行各密钥的公平份额 |
默认值为 saturationThreshold = 0.5(环境变量 QUOTA_SATURATION_THRESHOLD)。
对于池中的每个活跃维度,引擎会计算:
fairShareAllowed = poolLimit × (allocationWeight / 100)consumed = 此密钥当前的滚动值(来自 QuotaStore.peek)remaining = fairShareAllowed - consumed然后:
policy = hard:如果consumed > fairShareAllowed且处于严格模式 → 阻止。policy = soft:如果consumed > fairShareAllowed且处于严格模式 → 惩罚(降低其在组合中的优先级;绝不硬性阻止)。policy = burst:只要全局仍有余量,无论公平份额如何都允许使用。
分配中的 capValue + capUnit 是独立于模式或策略的硬性上限。任何维度中,只要 consumed >= capValue,就始终会阻止请求。
如果池中的任意维度会阻止请求,则该请求会被阻止。各维度彼此独立——5h% 耗尽不会影响 weekly% 维度。
在宽松模式下,某个分配未被完全使用的密钥可以使用其他密钥未分配份额中的剩余额度。公式为:
maxAllowed = globalLimit - consumedByOtherKeys其中,consumedByOtherKeys = consumedTotal - consumedByThisKey。全局上限(该维度的池 limit)始终是硬性上限。
滑动窗口计数器
Section titled “滑动窗口计数器”实现在 src/lib/quota/sqliteQuotaStore.ts 和 redisQuotaStore.ts 中。
每个 (apiKeyId, dimensionKey) 有两个桶:
curr:当前桶(floor(nowMs / windowMs))prev:前一个桶(curr - 1)
有效滚动值:
effectiveBucketIndex = floor(nowMs / windowMs)bucketStartMs = effectiveBucketIndex × windowMselapsed = nowMs - bucketStartMsweight = 1 - elapsed / windowMs
effective = prev × weight + curr精度:约 99% 准确。误差最多为窗口大小的 1%,出现在桶之间的边界处(这是双桶近似法固有的误差)。
SQLite 驱动:按 (apiKeyId | dimensionKey) 键使用内存互斥锁,以防止读取-修改-写入竞争。该模式与 src/sse/services/auth.ts 中防止惊群效应的实现一致。
Redis 驱动:使用 Lua EVAL 脚本进行原子递增——作为单条 Redis 命令执行。
SQLite(默认,零安装)
Section titled “SQLite(默认,零安装)”- 表:
quota_consumption(请参阅迁移文件073_quota_pools.sql/074_quota_consumption.sql)。 - 最适合单实例部署。
- 所有持久化数据都存储在现有的 OmniRoute SQLite 数据库中(
DATA_DIR/storage.sqlite)。
Redis(可选,多实例)
Section titled “Redis(可选,多实例)”- 需要
ioredisnpm 包。 - 计数器存储在 Redis 中;元数据(池/分配)仍存储在 SQLite 中。
- 最适合必须共享计数器的多副本部署。
切换驱动程序
Section titled “切换驱动程序”通过设置界面(/dashboard/settings → 配额存储),或通过环境变量:
QUOTA_STORE_DRIVER=redisQUOTA_STORE_REDIS_URL=redis://localhost:6379数据库设置的优先级高于环境变量。如果 driver=redis 但未提供 URL,或未安装
ioredis,工厂将回退到 SQLite 并记录警告。
驱动程序选择顺序:
- 数据库设置
quotaStore.driver - 环境变量
QUOTA_STORE_DRIVER - 默认值:
sqlite
一个池可以具有多个维度。每个维度相互独立:
QuotaDimension { unit: "percent" | "requests" | "tokens" | "usd", window: "5h" | "hourly" | "daily" | "weekly" | "monthly", limit: number, // 此维度的全局池上限}示例:Codex 方案(5 小时百分比 + 每周百分比):
[ { "unit": "percent", "window": "5h", "limit": 100 }, { "unit": "percent", "window": "weekly", "limit": 100 }]请求必须满足所有维度才能获准通过。
实现在 src/lib/quota/planResolver.ts 中。
优先级(从高到低):
- 手动数据库覆盖 —
provider_plans表,按connectionId设置。 - 已知目录 —
src/lib/quota/planRegistry.ts(仅包含数据)。 - 空方案 — 没有维度,需要手动配置。
| 提供者 | 维度 |
|---|---|
codex |
percent/5h/100, percent/weekly/100 |
glm |
tokens/5h(limit=0,未知),tokens/weekly |
minimax |
tokens/5h, tokens/weekly |
bailian |
percent/5h/100, percent/weekly/100, percent/monthly/100 |
kimi |
requests/hourly/1500 |
alibaba |
requests/monthly/90000 |
openai, anthropic |
无默认值 — 需要手动配置 |
PRE 钩子(open-sse/handlers/chatCore.ts)
Section titled “PRE 钩子(open-sse/handlers/chatCore.ts)”在上游执行器之前、身份验证和策略检查之后运行:
resolveComboTargets / handleSingleModel → enforceQuotaShare(apiKeyId, connectionId, provider, estimatedCost) → 每个维度调用 getQuotaStore().peek() → fairShare.decideFairShare() → 如果阻止 → 返回 429(buildErrorBody,硬性规则 #12) → 如果允许 + 降低优先级 → 在候选项上设置 quotaSoftPenalty=true → executor.execute()故障开放:如果 enforceQuotaShare 抛出异常,则允许请求通过,
并记录 pino.warn 日志。这可防止配额引擎错误阻止所有
流量。
POST 钩子(记录消耗)
Section titled “POST 钩子(记录消耗)”成功响应后:
执行器返回成功 → spendRecorder.recordConsumption(apiKeyId, connectionId, provider, actualCost) → 每个维度调用 getQuotaStore().consume() → 故障开放:错误记录为 pino.warn,绝不传播给客户端漂移说明:如果响应后 consume 失败,滚动计数器会少计。
来自提供者的饱和度信号(例如 anthropic-ratelimit-unified-5h-utilization)
将在下一个请求中修正全局估算值。
组合软惩罚(open-sse/services/combo.ts)
Section titled “组合软惩罚(open-sse/services/combo.ts)”当 decision.deprioritize === true 时:
if (candidate.quotaSoftPenalty) { score *= QUOTA_SOFT_DEPRIORITIZE_FACTOR; // 默认值为 0.7}该惩罚会在所有其他评分因子之后应用。它会降低自动组合选择已饱和密钥的 概率,但不会硬性阻止该密钥。
UI 操作指南
Section titled “UI 操作指南”/dashboard/costs/quota-share — 主池页面
Section titled “/dashboard/costs/quota-share — 主池页面”组件(均位于 src/app/(dashboard)/dashboard/costs/quota-share/):
| 组件 | 用途 |
|---|---|
QuotaConceptCard |
向新用户说明配额共享的介绍卡片 |
CreatePoolModal |
创建新的配额池(连接 + 名称 + 初始分配) |
PoolCard |
各配额池摘要:名称、连接、分配数量 |
DimensionBar |
各维度堆叠条形图:每个键的份额 + 全局使用量 |
AllocationTable |
包含已消耗量、公平份额、欠额/盈余和借用标志的表格 |
BurnRateChart |
EMA 消耗速率折线图(通过 dynamic() 延迟加载 Recharts) |
EditAllocationsModal |
编辑配额池的分配权重、上限和策略 |
页面钩子:
usePools— 每 30 秒获取一次GET /api/quota/pools。usePoolUsage— 按需获取GET /api/quota/pools/[id]/usage。useLocalStoragePoolMigration— 挂载时运行一次,用于迁移旧版 LS 数据。
/dashboard/costs/quota-share/plans — 提供者套餐配置
Section titled “/dashboard/costs/quota-share/plans — 提供者套餐配置”ProviderPlanConfigClient.tsx:用于选择提供者、查看解析后的套餐(从目录自动获取或手动覆盖)以及编辑维度的下拉菜单。- 更改将写入
PUT /api/quota/plans/[connectionId]。 - 删除后将恢复为目录中的套餐或空套餐。
| 变量 | 默认值 | 说明 |
|---|---|---|
QUOTA_STORE_DRIVER |
sqlite |
要使用的驱动程序:sqlite 或 redis |
QUOTA_STORE_REDIS_URL |
(空) | Redis URL,例如 redis://localhost:6379 |
QUOTA_SATURATION_THRESHOLD |
0.5 |
0..1;>= threshold 时启用严格模式 |
QUOTA_SOFT_DEPRIORITIZE_FACTOR |
0.7 |
0..1;软策略组合得分的乘数 |
QUOTA_CONSUMPTION_RETENTION_DAYS |
14 |
GC 删除旧 quota_consumption 存储桶之前的保留天数 |
数据库设置(quotaStore.*)会覆盖环境变量。
已配置 Redis 但无法连接
Section titled “已配置 Redis 但无法连接”检查是否已安装 ioredis(npm ls ioredis),并确认 QUOTA_STORE_REDIS_URL 可访问。连接失败时,工厂会回退到 SQLite(以 warn 级别记录)。
peek 返回过时结果/失效时放行
Section titled “peek 返回过时结果/失效时放行”如果 peek 抛出异常,enforceQuotaShare 会将结果视为“允许”(失效时放行)。检查 pino 日志中的 quota:enforce 和 quota:factory 条目以确定根本原因。
消耗计数器偏移
Section titled “消耗计数器偏移”如果提供者的实际使用量与计数器不同,这是预期现象——双桶滑动窗口在窗口边界处约有 1% 的误差,并且 consume 是响应后即发即弃的操作。饱和度信号(saturationSignals.ts)会以 30 秒 TTL 读取提供者的实际利用率,并相应调整 globalUsedPercent。
配额池的消耗速率显示“无数据”
Section titled “配额池的消耗速率显示“无数据””computeBurnRate 至少需要 2 个历史样本。此前未调用过 consume 的新配额池将显示 tokensPerSecond: 0 和 timeToExhaustionMs: null。
从 localStorage 迁移
Section titled “从 localStorage 迁移”首次加载 /dashboard/costs/quota-share 时,hook useLocalStoragePoolMigration
会检查:
localStorage.getItem("omniroute:quota-share:pools")非空。GET /api/quota/pools返回[](DB 为空)。
如果两个条件均为真,则会将每个旧版池批量提交到 POST /api/quota/pools,
然后移除 localStorage 键。该迁移具有幂等性:条件 2 可防止
重复迁移。
内部策略分类
Section titled “内部策略分类”quota-share 是一种仅供内部使用的路由策略(位于
src/shared/constants/routingStrategies.ts 中的 INTERNAL_ROUTING_STRATEGY_VALUES)。
它仅由系统生成的 qtSd/ 池组合使用,并被特意排除在 ROUTING_STRATEGY_VALUES
之外,因此永远不会在 UI 或 API 中作为用户可选选项出现。
配额共享引擎提供两层自动化测试覆盖:
| 测试套件 | 命令 | 覆盖内容 |
|---|---|---|
| 单元测试(29 项测试) | node --import tsx/esm --test tests/unit/quota-share-strategy.test.ts |
DRR 调度器、饱和度门控、并发上限、fairShare 计算、积压队列 |
| 集成矩阵 | npm run test:combo:matrix |
通过真实组合流水线进行端到端路由决策;通过实时接入点(registerQuotaFetcher、setLKGP、__setHeadroomSaturationFetcherForTests)验证 DRR 公平性和饱和度降优先级 |
集成矩阵会在 CI 中与全部 19 种公共策略一同运行。单元测试套件 可以独立运行。
DB 架构摘要
Section titled “DB 架构摘要”迁移 078、079 和 085 新增了三个表:
quota_pools+quota_allocations— 池定义及每个键的分配。quota_consumption— 每个(apiKeyId, dimensionKey)的滚动双桶计数器。provider_plans— 手动提供者套餐覆盖配置(每个 connectionId 的维度 JSON)。
所有表均通过幂等的 CREATE TABLE IF NOT EXISTS 迁移添加。
HagiCode
HagiCode 是一套智能体编码工作台:结构化工作流、多 Agent 并行执行与 Hero Dungeon 视图,把想法变成真正交付的软件。
让想法更快变成好用的软件,让智能编码更聪明、更高效,也更有趣。

- Smart结构化工作流将意图转化为从想法到交付的可执行路径。
- Efficient多 Agent 工作流让调研、实现与审阅并行推进。
- FunHero Dungeon 让长时间编码协作更直观、更有参与感。