拆解 9Router 的自动路由:配额、成本、可用性三个指标如何决定请求去向
【免费下载链接】9routerUnlimited FREE AI coding. Connect Claude Code, Codex, Cursor, Cline, Copilot, Antigravity to FREE Claude/GPT/Gemini via 40+ providers. Auto-fallback, RTK -40% tokens, never hit limits.项目地址: https://gitcode.com/GitHub_Trending/9r/9router
当 Claude Code、Codex、Cursor、Cline 这些 AI 编码工具同时指向一个本地网关,而网关背后挂着 40+ 提供方、数百个模型、既有订阅账户又有免费额度时,"这个请求该发给谁"就成了一个远比"转发"复杂的问题。9Router 在 README 里把答案压缩成一句话:Subscription → Cheap → Free,zero downtime(订阅 → 便宜 → 免费,零停机)。但这句口号背后的决策机制,是一个由配额、成本、可用性三个指标交织而成的路由系统。本文直接进入源码,拆解请求在 9Router 内部被"决定去向"的完整链路。
一、路由的第一步:先弄清请求需要什么,而不是先问去哪
路由的入口在 src/sse/handlers/chat.js。handleChat拿到请求后做的第一件事不是查模型表,而是调用detectRequiredCapabilities(body)扫描请求本身:
const requiredCapabilities = detectRequiredCapabilities(body);这个函数定义在 open-sse/services/combo.js,它会逐条扫描当前用户回合(trailingUserItems,即最后一条 assistant 消息之后的所有消息)里的内容块:OpenAI 的image_url、Claude 的image、Gemini 的inlineData.mimeType、Responses API 的input_image、Ollama 的images数组,乃至字符串消息里内嵌的data:image/URI——全都被归入四类硬能力:vision / pdf / audioInput / videoInput。
关键设计在于能力被分成两档:
// Hard capabilities = input modalities; missing one drops request data (e.g. image // stripped). Must be prioritized. Soft (e.g. search) only degrades a feature. const HARD_CAPS = new Set(["vision", "pdf", "audioInput", "videoInput"]);硬能力缺失意味着请求数据会被直接丢弃(图片被剥离、PDF 无法解析),所以路由必须优先满足;而软能力(如 search)只是降级一个功能。这一"先摸清请求、再决定路由"的顺序,是整个自动路由体系的逻辑地基——没有它,后续一切配额和成本权衡都无从谈起。
二、成本指标:不是打分,而是用户声明的有序列表
9Router 的成本决策没有隐藏的权重表。成本优先级是由用户在创建 combo(模型组合)时用列表顺序显式声明的。README 中的三层回落(Smart 3-Tier Fallback)是标准范式:
Combo: "my-coding-stack" 1. cc/claude-opus-4-6 (your subscription) 2. glm/glm-4.7 (cheap backup, $0.6/1M) 3. if/kimi-k2-thinking (free fallback)订阅在最前、便宜中间、免费兜底。这套"用户声明顺序"再叠加两种执行策略:fallback(按序尝试,失败才向后走)或round-robin(轮转均衡)。轮转逻辑在getRotatedModels(open-sse/services/combo.js)中实现,且带一个stickyLimit参数控制"每个模型连续吃几个请求才切换",默认值为 1(src/lib/db/repos/settingsRepo.js 中comboStickyRoundRobinLimit: 1),而账户层面的轮转粘性默认是 3。
值得注意的是,能力排序并不会推翻用户声明的成本顺序——reorderByCapabilities明确声明"稳定排序,永不丢弃模型"(fallback 完整性保留),它只是把能满足请求能力的模型按三层 tier 浮到前面:
- Tier 0:满足全部硬能力 + 全部软能力;
- Tier 1:满足全部硬能力;
- Tier 2:其余。
当请求带图而列表头部的订阅模型不支持 vision 时,一个支持 vision 的免费模型会被浮到首位——成本顺序让位于能力约束,但原列表作为 fallback 链完整保留。这是"成本"与"可用性"第一次发生冲突时给出的裁决规则。
三、配额指标:规则引擎式的错误分类与指数退避
请求失败后是否换人,不是看"谁分数高",而是走一套显式规则引擎。核心在 open-sse/services/accountFallback.js 的checkFallbackError与 open-sse/config/errorConfig.js 的ERROR_RULES:
export const ERROR_RULES = [ // --- Text-based rules (checked first, order = priority) --- { provider: "codex", text: "model is not supported when using codex with a chatgpt account", cooldownMs: MAX_RATE_LIMIT_COOLDOWN_MS }, { text: "no credentials", cooldownMs: COOLDOWN.long }, { text: "request not allowed", cooldownMs: COOLDOWN.short }, { text: "improperly formed request", cooldownMs: COOLDOWN.long }, { text: "rate limit", backoff: true }, { text: "too many requests", backoff: true }, { text: "quota exceeded", backoff: true }, { text: "capacity", backoff: true }, { text: "overloaded", backoff: true }, // --- Status-based rules (fallback when text doesn't match) --- { status: 401, cooldownMs: COOLDOWN.long }, { status: 402, cooldownMs: COOLDOWN.long }, { status: 403, cooldownMs: COOLDOWN.long }, { status: 404, cooldownMs: COOLDOWN.long }, { status: 429, backoff: true }, ];注意匹配顺序:文本规则优先于状态码规则。rate limit、quota exceeded、capacity、overloaded这些配额类错误触发指数退避(base 2s,逐级翻倍,最高 5 分钟,15 级封顶);401/402/403/404 固定冷却 2 分钟;no credentials同样 2 分钟;未匹配的瞬时错误统一 30 秒冷却。
这套规则引擎还有一个耐人寻味的细节:请求本身导致的 4xx(如上下文溢出、参数不支持)被刻意排除在降级之外——status >= 400 && status < 500且非 401/402/403/429 时直接shouldFallback: false,把上游错误原样交还调用方。注释写得很直白:如果给这类错误也冷却账户,就会把"请求本身有问题"伪装成"账户被限流",让所有后续请求都背负一份错误的副本。这是典型的"错误归因"设计:只有证明是账户/配额问题的错误才消耗配额冷却预算。
四、可用性指标:模型锁、实时配额缓存与熔断
配额冷却最终落到持久化的"模型锁"上。markAccountUnavailable(src/sse/services/auth.js)会给指定连接的modelLock_${model}字段写入一个冷却到期时间戳,之后getProviderCredentials在选账户前会先过滤掉所有被锁的连接。锁定粒度是"模型"而非"账户"——同一个账户上的其他模型仍可被路由命中。
可用性指标在这里出现了三种精细化的信号源:
- provider 精确重置时间:Codex 的
resets_at、GitHub 402 的"下个自然月 UTC 零点"(githubMonthlyResetMs直接计算到月初)都会被直接采用;Antigravity 的配额 API 给出的resetAt甚至不做 30 分钟截断,因为它是精确的模型级重置时间。 - 实时配额缓存预过滤:Antigravity 走 src/sse/services/antigravityQuota.js 的 RAM 缓存,账户选择前先看
remainingPercentage <= 0 && resetAt > now,命中就直接跳过该连接,不浪费一次上游请求。缓存带 30 秒刷新门限防抖。 - 熔断器:Google 配额 API 偶尔会乐观上报"有配额",但生成接口仍持续 429。为此实现了 strike 计数器——同一连接+模型在 60 秒窗口内连续 3 次 429,就把该组合缓存封锁 15 分钟,防止对上游的 retry-storm。
而恢复路径同样干脆:clearAccountError在请求成功时清掉当前模型的锁、顺手清理所有已过期的锁,并把backoffLevel归零。一次成功即宣告该路径恢复健康,这与指数退避形成闭环。
五、切换失败时的降级策略:三层兜底
当所有候选都失败时,9Router 不是简单抛错,而是分层降级:
第一层:瞬时错误等待。在handleComboChat(open-sse/services/combo.js)的失败循环里,503/502/504 这类瞬时错误会先等待 cooldown(上限 5 秒)再切下一个模型,给"短暂过载"的提供方一个恢复窗口,而不是立刻跳过——注释明确这是修复"combo 在瞬时 503 上直接穿透"的补丁。
第二层:能力适配池(Capacity Adapter)。open-sse/services/capacityAdapter.js 为每个硬能力维护一个兜底模型池(vision/pdf/audioInput/videoInput),默认兜底是免费模型oc/mimo-v2.6-flash-free。当原始列表没有任何模型能满足请求能力时,适配池模型被前置为优先候选。更关键的是stripHistoryForContext:切到上下文窗口较小的适配模型时,会按 80% 窗口预算从中间裁掉历史消息(保留头部指令与携带媒体的尾部用户回合),让降级不至于因上下文溢出而失败。
第三层:全挂时的统一出口。所有模型失败后返回 503(而非 406),并带上所有候选里最早的 retryAfter,让客户端能按真实恢复时间重试。注释解释了原因:406 暗示请求本身非法,但这里是提供方不可用——503 语义准确且可重试。
另外还有一条独立的 Fusion 策略(comboStrategy: "fusion"),它把多个模型并行组成评审团,再由 judge 模型综合成一份答案,实现了另一种"降级":0 份面板答案返回 503,只有 1 份成功就直接透传,绝不在合成上强行凑数。
六、所以:规则引擎,不是打分排序
回到最初的疑问——9Router 的路由决策是"规则引擎"还是"打分排序"?源码给出的答案是清晰的:规则引擎 + 有序候选列表 + 离散分层。
- 成本不产生分数,而是用户组合里的一条有序链(订阅 → 便宜 → 免费),配合轮转策略做请求级均衡;
- 配额不产生分数,而是
ERROR_RULES的文本/状态码顺序匹配,输出"是否降级 + 冷却多久"的二元裁决; - 可用性不产生分数,而是"模型锁过滤 + 实时配额缓存预过滤 + 熔断封锁"的布尔化健康状态。
三个指标的分工可以用一句话概括:配额决定"能不能去",成本决定"优先去哪",可用性决定"失败后去哪"。能力约束(vision/pdf 等)作为不可逾越的硬边界在最外层过滤,用户声明的成本顺序在边界内保持稳定,而错误分类引擎在每一跳失败时重新校准下一跳。这套设计把"便宜"和"不断供"这两个看似冲突的目标,拆解成了可以独立观测、独立配置的机制——这也是 9Router 敢在 README 里承诺"never stop coding,never hit limits"的工程底气。
【免费下载链接】9routerUnlimited FREE AI coding. Connect Claude Code, Codex, Cursor, Cline, Copilot, Antigravity to FREE Claude/GPT/Gemini via 40+ providers. Auto-fallback, RTK -40% tokens, never hit limits.项目地址: https://gitcode.com/GitHub_Trending/9r/9router
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考