OmniRoute 弹性(Resilience)体系详解:Provider 熔断、连接冷却与模型 Lockout 的分层容错设计
【免费下载链接】OmniRouteNever stop coding. Free MIT AI gateway: one endpoint, 352 providers (150+ free), 1200+ models Kimi, Claude, GPT, Gemini, GLM, DeepSeek, MiniMax. Works with Claude Code, Codex, Cursor, OpenCode, Cline & Copilot. Quota-aware auto-fallback, RTK+Caveman compression saves 15-95% tokens, MCP/A2A, Desktop/PWA. Built by 550+ contributors项目地址: https://gitcode.com/GitHub_Trending/om/OmniRoute
OmniRoute 用三套相互独立又彼此联动的弹性机制(Provider 级熔断、单连接冷却、单模型 Lockout),加上 quota-share 并发控制与请求队列准入控制,构建了一条从"整个 provider"到"单个模型"的完整故障隔离链路。本文以仓库内的RESILIENCE_GUIDE官方文档为主线,结合 circuitBreaker.ts、accountFallback.ts、resilience settings 等源码逐一展开:读完你将理解每层机制的作用范围、默认参数、触发条件与懒恢复(lazy recovery)路径,并掌握用 Dashboard 设置项、REST API 与混沌测试来验证和调试路由行为的完整方法。
图示来源:resilience-3layers.mmd。三层机制的作用范围从大到小依次为:provider → 连接/账号 → 模型。
调试路由异常行为时的第一原则是把这三层分开看:它们各有独立的状态存储、触发阈值与恢复路径,混在一起排查会得出错误结论。下文按"范围从大到小"逐层解析。
1. Provider Circuit Breaker:整 provider 级熔断
作用范围:整个 provider(如glm、openai、anthropic)。目标:当一个 provider 在上游/服务层面反复失败时,停止向其发送流量,避免拖垮整个路由管道。
1.1 状态机与实现位置
核心实现是 src/shared/utils/circuitBreaker.ts 中的CircuitBreaker类,接入点在 src/sse/handlers/chatHelpers.ts 与 src/sse/handlers/chat.ts,账号级包装逻辑在 open-sse/services/accountFallback.ts。
四个状态(源码中以STATE常量定义):
| 状态 | 含义 |
|---|---|
CLOSED | 正常放行 |
DEGRADED | 流量仍放行,但已记录到偏高失败率,随时可能 OPEN |
OPEN | 熔断打开,combo 路由会跳过该 provider |
HALF_OPEN | reset 超时已到期,只允许有限的探测(probe)请求 |
从源码结构看,_onFailure()中维护了完整迁移逻辑:CLOSED → DEGRADED(失败数达到degradationThreshold)、DEGRADED/CLOSED → OPEN(达到failureThreshold)、HALF_OPEN → OPEN(探测失败,且openCycleCount递增)。每次迁移都会写入transitionHistory(最多保留 20 条)供诊断。
1.2 可配置的默认阈值
文档给出的默认值可在 open-sse/config/constants.ts 的PROVIDER_PROFILES中得到印证,并且全部支持OMNIROUTE_CIRCUIT_BREAKER_*环境变量覆盖(Dashboard → Settings → Resilience 亦可配置):
| Profile | 进入 DEGRADED | 熔断(OPEN) | Reset timeout |
|---|---|---|---|
| OAuth | 5 次失败 | 8 次失败 | 60s |
| API-key | 7 次失败 | 12 次失败 | 30s |
| Local | 派生值 | 2 次失败 | 15s |
- OAuth:
circuitBreakerThreshold=8、circuitBreakerReset=60000、degradationThreshold=5; - API-key:
circuitBreakerThreshold=12、circuitBreakerReset=30000、degradationThreshold=7; - Local:
circuitBreakerThreshold=2、circuitBreakerReset=15000。注意源码注释指出 local profile尚未完全接入getProviderProfile()弹性层(为未来 local provider node 预留),Dashboard 的 Resilience 设置页也暂未暴露该 profile。
degradationThreshold控制何时进入DEGRADED;failureThreshold控制何时 OPEN 并被路由跳过。若未显式指定,CircuitBreaker类内部默认degradationThreshold取failureThreshold的 60%(Math.ceil(failureThreshold * 60 / 100))。
此外源码中还有一组"自适应退避"参数:连续多次OPEN → HALF_OPEN → OPEN后,有效 reset 超时会按2^(cycle-escalationCount)指数放大,上限为maxBackoffMultiplier(类默认 16x,OAuth profile 覆盖为 8x,API-key 为 4x),对应OMNIROUTE_PROVIDER_BREAKER_*_MAX_BACKOFF_MULTIPLIER等环境变量。
1.3 触发条件(trip codes)
只有 provider 级状态码[408, 500, 502, 503, 504]才应触发熔断。账号级错误(绝大多数 401/403/429)不在此列——它们属于 connection cooldown 或 lockout 的管辖范围。
源码中还有一组精心的"排除"谓词,防止把本地/单模型问题误判为整 provider 故障:
isLocalStreamLifecycleError():识别 Codex WebSocket→SSE 桥的Controller is already closed、客户端中断(AbortError/Client disconnected)等本地流生命周期错误;isLocalExecutionError():识别ENOENT/EACCES/EPIPE、"command not found" 等本地宿主执行错误(典型如本地 CLI provider 缺二进制);isModelCapacityOverloadError():识别 Anthropic 529/Overloaded这类单模型容量过载(同一账号的其他模型仍可服务)。
这些谓词通过CircuitBreaker构造参数的isFailure选项接入,使"provider 级 5xx 真故障"才计入熔断计数。
1.4 懒恢复(lazy recovery)
OPEN到期后无需后台定时器:getStatus()、canExecute()、getRetryAfterMs()三个读入口都会先调用_refreshOpenState(),若Date.now() - lastFailureTime >= 有效冷却就自动迁移到HALF_OPEN并持久化。execute()在OPEN状态抛出CircuitBreakerOpenError(携带retryAfterMs),HALF_OPEN 状态下只有halfOpenAllowed > 0才放行探测请求。
1.5 状态持久化与运维接口
- 状态 API:
GET /api/monitoring/health可查各 breaker 状态(getAllCircuitBreakerStatuses()还会先回填 DB 中已持久化但不在内存 registry 的条目)。 - 重置 API:
POST /api/resilience/reset调resetAllCircuitBreakers()。 - 持久化:每次状态变化经
saveCircuitBreakerState()写入 DB(文档中对应domain_circuit_breakers表),重启后_restoreFromDb()恢复。全局 registry 有 500 个 breaker 的上限(MAX_REGISTRY_SIZE),超限优先淘汰"零失败的冷 CLOSED"实例;另有 5 分钟一次的清扫定时器移除长期空闲的 CLOSED 实例。
2. Connection Cooldown:单连接/单账号级冷却
作用范围:provider 的某一个连接/账号/密钥。目标:只跳过那个坏掉的关键,同一 provider 的其他连接继续服务。
2.1 实现与字段
- 标记不可用:src/sse/services/auth.ts 的
markAccountUnavailable(); - 凭据选择(自动跳过冷却中的账号):同文件的
getProviderCredentials*系列; - 冷却时长计算:open-sse/services/accountFallback.ts 的
checkFallbackError(); - 设置:src/lib/resilience/settings.ts(
connectionCooldown段)。
每个连接会维护以下字段:
rateLimitedUntil— 冷却到期时间戳;testStatus: "unavailable"— 不可用标记;lastError、lastErrorType、errorCode— 诊断信息;backoffLevel— 指数退避计数器。
2.2 默认冷却值与退避公式
从 resilience settings 的DEFAULT_RESILIENCE_SETTINGS.connectionCooldown与PROVIDER_PROFILES可确认:
- OAuth 基线冷却:
transientCooldown = 5000ms(5s,会话型 token 快速恢复); - API-key 基线冷却:
transientCooldown = 3000ms(3s,API 配额按已知周期重置,退避上限更低:maxBackoffLevel=5vs OAuth 的 8); - API-key 429:优先采用上游
Retry-After/reset 头或可解析的 reset 文本(rateLimitCooldown: 0即"尊重 retry-after"),没有提示时才用默认值; - 退避公式:
baseCooldownMs * 2 ** failureIndex。
防惊群(anti-thundering-herd):对并发失败,代码避免把同一连接的 cooldown 重复累加过长、或对backoffLevel重复递增——多个请求同时失败时只应产生一次状态推进。
2.3 终态(不是冷却)
banned(由 banned-keyword/封号检测设置,见 BAN_DETECTION.md)、expired、credits_exhausted是终态:持续到凭据变更或操作员手动重置。规则:不要用瞬时的 cooldown 态去覆盖终态。
懒恢复:rateLimitedUntil过期后连接重新变得可选;一次成功调用后clearAccountError()清掉所有错误字段。
2.4 Session Affinity(#7274)
作用范围:任意 provider 的单个客户端会话(请求头X-Session-Id/x-codex-session-id/x-omniroute-session),固定到同一个连接。目的:让多轮 agent(Claude Code、aider、自建 agent)跨请求保持在同一账号上,减少跨账号的上下文丢失,以及 per-account 有状态 provider 上反复出现的 cold-start 429。
实现链路(全部在源码中可查):
- TTL 解析:src/sse/services/sessionAffinityPin.ts 的
resolveSessionAffinityTtlMs(); - 选路/建 pin:同文件
selectSessionAffinityConnection(); - 头提取(通用、provider 无关):src/sse/services/auth.ts 的
extractSessionAffinityKey(); - 持久化 pin 表:
sessionAccountAffinity(src/lib/db/sessionAccountAffinity.ts); - 设置项:
sessionAffinityTtlMs(全局 TTL,毫秒;0表示禁用),经迁移124_generic_session_affinity_ttl.sql从旧的 Codex 专属codexSessionAffinityTtlMs改名迁移而来——之前配置的 Codex TTL 值被保留为新全局默认值。
#7274 修复前,resolveSessionAffinityTtlMs()对codex之外的所有 provider 硬编码返回0,导致 TTL 设置(以及会话头)在任何其他 provider 上都不生效,尽管 pin 机制与头提取本身早已是 provider 无关的。修复移除了该 early-return:只要全局 TTL 设置为正值,就对所有provider 统一生效。
一个容易误解的点:这三个 session-affinity 头永远不会被转发到上游——executor 是从零构建上游请求头的,而不是透传客户端头,因此它纯粹是内部关联标识。
3. Model Lockout:provider + connection + model 三元组级锁定
作用范围:provider + connection + model三元组。目的:当只有某一个模型不可用或被限流时,不牵连整个连接。
典型场景:
- per-model 配额 provider 对某模型返回 429;
- 本地 provider 对某个缺失模型返回 404;
- provider 特有的 model/模式权限故障(如 Grok 的某些模式)。
核心实现在 open-sse/services/accountFallback.ts:lockModel()、clearModelLock()、getAllModelLockouts(),以及决策函数hasPerModelQuota()——只有被识别为"按模型计配额"的 provider(如 antigravity、codex、gemini、github、passthrough/compatible/local 类型)才走模型级锁定,其余走连接级冷却。
3.1 Model Cooldowns Dashboard(v3.8.0)
只读卡片列出当前活跃 lockout(provider、connection、model、reason、expiresAt),操作员可从卡片手动重新启用模型。组件位于 ModelCooldownsCard.tsx/dashboard/runtime/components/ModelCooldownsCard.tsx)。
REST API:
GET /api/resilience/model-cooldowns— 列出活跃 lockout(实现见 route.ts);DELETE /api/resilience/model-cooldowns— 手动重新启用。Body:{provider, connection, model},需要 management 权限。
3.2 Lockout 设置卡 + success-decay 恢复(v3.8.23)
Model lockout 已从"总是开启、硬编码行为"升级为可选(opt-in)、完全可配置的功能,自带设置卡与自愈恢复路径。设置卡 ModelLockoutCard.tsx/dashboard/settings/components/ModelLockoutCard.tsx)(Settings → Model Lockout)与 3.1 中只读展示活跃状态的ModelCooldownsCard是两张不同的卡:前者配置参数,后者只列状态。
默认值定义在 src/lib/resilience/modelLockoutSettings.ts 的DEFAULT_MODEL_LOCKOUT_SETTINGS,源码同时给出了各字段的取值钳制(clamp):
| Setting | 默认值 | 含义 | 源码钳制范围 |
|---|---|---|---|
enabled | false | 主开关——默认关闭 | 布尔 |
errorCodes | [403, 404, 429, 502, 503, 504] | 计入"模型级失败"的上游状态码 | 每项 100–599 |
baseCooldownMs | 120_000(120s) | 首次失败的初始锁定时长 | 5s – 600s(测试进程可为 0) |
maxCooldownMs | 1_800_000(30min) | 退避升级后的上限 | 5s – 3600s,且强制≥ baseCooldownMs |
maxBackoffSteps | 10 | 指数退避最大步数 | 0 – 20 |
useExponentialBackoff | true | 重复失败是否指数升级冷却 | 布尔 |
设置经常规 settings store 持久化并由 resilience settings 校验;只有设置是持久的,活跃的 lockout 状态是内存态(per-process 的Map<ModelLockoutEntry>,键为provider:connectionId:model),进程重启即丢失。
3.3 success-decay:不靠定时器也能量化的恢复
恢复不只依赖定时器到期。源码中recordModelLockoutFailure()在升级窗口(resetAfterMs + 上次冷却)内递增failureCount并指数升级冷却;而 combo 命中成功时,open-sse/services/combo.ts 会调用decayModelFailureCount(),把存量的failureCount减半(Math.floor(failureCount / 2)),减到 0 时整个 lockout 条目被删除。也就是说:一个在锁定期内恢复健康的模型会在定时器到期前停止升级并被清场。两条恢复路径(success-decay 与定时器过期)任何一条都能重新启用模型。
另有两条值得注意的边界逻辑(均见recordModelLockoutFailure()源码):
quota_exhausted(日配额耗尽):冷却直接设到次日 00:00(getMsUntilTomorrow()),绕过指数退避——这就是文档 2.4 节"combo 等待从不等待quota_exhausted"的底层原因;- 权威上游 reset 提示(
Retry-After/X-RateLimit-Reset头、google.rpc.RetryInfo)可不受maxCooldownMs钳制,因为"上游明确告诉我们等到何时"必须被精确尊重;而来自 JSON 正文或自然语言解析出的 reset 文本仍受maxCooldownMs约束。
4. Quota-Share 并发控制(v3.8.36)
订阅制账号(GLM、MiniMax 等)通常只接受约 1–3 个并发请求,超发即 429 加冷却。这在quota-sharecombo(qtSd/…,多个 API key 共享一个上游账号)中尤为尖锐。仓库用三层保护共享账号:
4.1 Per-connection 并发上限(max_concurrent)
每个 provider 连接可声明max_concurrent(provider_connections表字段,可在连接弹窗/API/DB 设置),留空即无限制。它是驱动下面序列化层级的唯一旋钮,建议按账号真实并发能力设置(如 GLM ~1、MiniMax ~2)。
4.2 Quota-share 请求序列化
当 quota-share 派生命中一个设置了正数max_concurrent的连接时,发往该账号的并发请求会被 per-connection 信号量(键qsconn:<connectionId>)串行化:超出的请求排队等待而不是把账号打爆。该机制是fail-open的——队列饱和或超时会"无 slot 放行",绝不拒绝本可发送的请求。开关在 Settings → Resilience → "Quota-share per-connection concurrency",对应resilienceSettings.quotaShareConcurrencyLimit.enabled(resilience settings 中默认true,仅作 kill-switch)。未设max_concurrent时行为不变。
注意:quota-share 路由闸门
selectQuotaShareTarget(DRR + P2C)本身也是 fail-open,它只在触顶时降低该连接的优先级;单连接池时它无法硬性限流,真正挡住洪峰的是上面的信号量。
4.3 Combo cooldown-aware retry
对每一种已启用的 combo 策略:当一个请求即将因为一个短暂的瞬态冷却而 429 时,OmniRoute 会等待冷却结束并重新派生,而不是直接回 429——这覆盖了 Gemini 系 TPM/RPM 窗口(实测 ~60s retry-after)落在多模型 combo 上的情况(例如 2 模型 combo 的两个目标同时触发 per-model 限流)。受resilienceSettings.comboCooldownWait约束(Settings → Resilience),源码默认值:enabled: true、maxWaitMs: 90_000(单次等待上限 90s)、maxAttempts: 5、budgetMs: 300_000(总预算 5 分钟)。永不等待quota_exhausted(锁到午夜)以及 auth/not-found 类原因。
5. 请求队列准入控制(v3.8.49 · issue #6593)
作用范围:per-provider+connection 的本地速率限制队列(open-sse/services/rateLimitManager.ts,基于 Bottleneck 库),位于上述三层机制之下的"最后一道"本地排队层。
5.1maxWaitMs默认值从 120s 降到 15s
resilienceSettings.requestQueue.maxWaitMs限制请求在本地队列中可等待多久后被拒绝(错误码RATE_LIMIT_QUEUE_TIMEOUT,#4165)。工厂默认从 120000ms 降到15000ms,让饱和队列快速失败(fail-fast),而不是让调用方干等两分钟。可用环境变量RATE_LIMIT_MAX_WAIT_MS或 Dashboard(Settings → Resilience,UI 上限 1–30000ms)覆盖。resilience settings 中同时可见另一个独立的旋钮executionMaxWaitMs(默认 600000ms)——那是针对"已开始执行"的请求的兜底(部分非增量式网关会在首字节前缓冲整段生成),与排队预算刻意分开。
5.2maxQueueDepth:可选的准入上限(opt-in)
resilienceSettings.requestQueue.maxQueueDepth限制单个 provider+connection 队列中"已排队、尚未发出"的请求数量。当队列已达到maxQueueDepth时,新请求在到达limiter.schedule()之前就被快速拒绝,返回带类型的错误code: "RATE_LIMIT_QUEUE_FULL"——因此拒绝是廉价的,发生在该请求的 prompt-compression / translation 等后续工作开始之前。默认0= 禁用(保持原来的无界队列行为);合法范围 0–100000。可用RATE_LIMIT_MAX_QUEUE_DEPTH环境变量或 Dashboard/API patch 覆盖。
准入检查被抽成纯函数 admission.ts 的checkQueueAdmission(queuedCount, maxQueueDepth, identity):maxQueueDepth <= 0或队列有容量时返回null放行,否则返回带RATE_LIMIT_QUEUE_FULL品牌(WeakMap provenance 标记,供健康检查与路由决策识别"这是本地队列拒绝而非上游拒绝")的 429 错误。抽成纯函数意味着无需启动真实的 Bottleneck limiter 即可单测。
RFC(#6593)还提出过
bypassCompressionOnRateLimit标志,但未实现:本仓库 open-sse/services/compression/ 管道压的是出站 LLM 请求的 prompt/context(在chatCore.ts的resolveCompressionSettings/selectCompressionStrategy附近),并非对合成 429 body 做 HTTP 响应压缩——没有对应的代码路径。且 prompt-compression 目前运行在withRateLimit()之前,为在 queue-full 拒绝时跳过它而重排管道顺序,属于超出该 issue 范围的独立改动;出于"收益是否值得顺序调整风险"的考量被有意留作 follow-up。
6. 其他弹性能力
- 19 种路由策略:priority、weighted、round-robin、context-relay、fill-first、p2c、random、least-used、cost-optimized、reset-aware、reset-window、headroom、strict-random、auto、lkgp、context-optimized、cache-optimized、fusion、pipeline——详见 AUTO-COMBO.md。
- Reset-aware routing(v3.8.0):按配额重置时间优先排序连接。
- Background mode degradation:Responses API 的
background: true降级为同步并附带警告。 - Dynamic tool limit detection:命中 tool 数量上限时自动退回(回退)到不受该限制的 provider。
- Emergency fallback:由
OMNIROUTE_EMERGENCY_FALLBACK环境变量控制;操作员可在 Feature Flags 页面不重启覆盖。
7. 调试清单
来自官方文档的五条排障规则,覆盖了绝大多数"路由行为异常"工单:
- 所有 provider 关键都被跳过→ 同时检查 circuit breaker 状态和每个连接的
rateLimitedUntil/testStatus(两层都可能独立阻塞)。 - provider 在 reset 窗口后仍被排除→ 排查代码是否读了原始
state字段而不是getStatus()/canExecute()——只有后者会触发懒恢复。 - 只有一个关键坏、其他应可用→ 优先用 connection cooldown,而不是整个 provider 的 circuit breaker。
- 只有一个模型坏→ 优先用 model lockout,而不是连接级 cooldown。
- 状态应自愈却没有→ 检查未来的时间戳(时钟漂移)以及刷新过期状态的读路径是否走到了
_refreshOpenState()/cleanupModelLockKey这类懒恢复逻辑;永久性状态(终态)必须靠手动变更。
8. TLS 指纹与 Stealth
Provider 级的 TLS 指纹伪装(JA3/JA4、CCH、obfuscation)不属于弹性状态机,独立文档化——参见 STEALTH_GUIDE.md。
9. 弹性测试矩阵(Phase 8 · Block C)
除逻辑单测外,有三个测试在真实压力/故障下压 runtime(全部为 integration/nightly 级,不阻塞 PR):
| 测试 | 内容 | 运行方式 |
|---|---|---|
| Chaos | 假上游 node 注入真实 latency/reset/timeout/503;验证 circuit breaker 能打开并恢复,且checkFallbackError将 503 归类为可恢复 fallback | npm run test:chaos(即RUN_CHAOS_INT=1下跑 resilience-chaos.test.ts,脚本定义于 package.json) |
| Heap-growth | 每个createSSEStream约 500 并发流,--expose-gc下运行;堆增长超过上限即失败(OOM 防护,#3069) | npm run test:heap(heap-growth.test.ts) |
| k6 soak | 对/api/monitoring/health的持续负载,检查 p95/错误率阈值(k6-soak.js) | k6 run tests/load/k6-soak.js(nightly) |
三者由 .github/workflows/nightly-resilience.yml 编排(cron + 手动 dispatch)。默认的test:integration中,chaos 与 heap 测试会自动跳过(未设RUN_CHAOS_INT/--expose-gc时)。
10. 延伸阅读
- ARCHITECTURE.md — 系统架构与内部机制总览;
- USER_GUIDE.md — provider、combo、CLI 集成;
- AUTO-COMBO.md — 多因子评分与 mode packs;
- BAN_DETECTION.md — 封号/禁词检测与
banned终态来源; - STEALTH_GUIDE.md — TLS 指纹与隐身。
适用前提:本文参数与行为以当前仓库源码为准(文档版本 v3.8.40,lastUpdated 2026-06-28)。注意 model lockout 默认关闭(需手动开启),maxQueueDepth默认禁用,而 quota-share 并发限制默认开启;本地 provider 的 breaker profile 尚未完全接入弹性层。
【免费下载链接】OmniRouteNever stop coding. Free MIT AI gateway: one endpoint, 352 providers (150+ free), 1200+ models Kimi, Claude, GPT, Gemini, GLM, DeepSeek, MiniMax. Works with Claude Code, Codex, Cursor, OpenCode, Cline & Copilot. Quota-aware auto-fallback, RTK+Caveman compression saves 15-95% tokens, MCP/A2A, Desktop/PWA. Built by 550+ contributors项目地址: https://gitcode.com/GitHub_Trending/om/OmniRoute
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考