Skip to content

fix(proxy,quota): 解耦客观物理配额耗尽与自适应熔断,增加单请求重试账号排除防反复锤击 - #3845

Merged
jeikl merged 1 commit into
lbjlaq:betafrom
cubelikeplayDaniel:fix/circuit-breaker-quota-rotation
Nov 5, 2026
Merged

jeikl merged 1 commit into
lbjlaq:betafrom
cubelikeplayDaniel:fix/circuit-breaker-quota-rotation

Conversation

@cubelikeplayDaniel

Copy link
Copy Markdown
Collaborator

变更类型

  • 修复(bug / 回归)
  • 功能
  • 文档 / 规范 / 发版基建(不夹带进功能 PR;同类文档改动合并为一个 PR)
  • 是否为大功能更改、修复、更新

基于的分支与版本号

基于 origin/beta (v5.0.3-beta.2,HEAD 39ef36cf)

当前问题、对用户使用体验会产生什么问题或影响

在实测与高频使用场景中,用户报告「一直在 429,网关的熔断为什么没生效?连续 10 次重试都打在同一个已耗尽账号上」:

  1. 硬配额耗尽信号被投机熔断开关旁路吞没:当用户在设置页点击保存时,前端 Settings.tsx 中的初始默认值 circuit_breaker: { enabled: false } 会将熔断器持久化关闭。在 token_manager.rs 的 mark_rate_limited_async_with_mode 与 mark_rate_limited_fast 中,代码直接将 if !config.enabled { return; } 置顶于函数入口,位于 is_quota_depleted_signal、mark_model_quota_depleted 与 fetch_and_lock_with_realtime_quota 之前。导致当 Google 上游返回明确的客观硬配额耗尽信号(Individual quota reached / QUOTA_EXHAUSTED / 携带重置时间戳)时,该信号被彻底丢弃,账号在内存中的配额从未被置为 0%,也未写入限流锁定,调度器完全感知不到该账号已耗尽;
  2. 多轮重试失忆症(Retry Amnesia)导致反复锤击同一账号:在 claude.rs、gemini.rs、openai.rs 处理器的多轮重试循环中,虽然 429 会设置 force_rotate = true,但调用的是无参数的 token_manager.get_token(...),内部传入空的 excluded_accounts = HashSet::new()。P2C 算法在随后所有重试轮次中无从知晓该账号刚刚发生过 429 报错,加之耗尽账号的历史周配额或基础权重可能高于其他备用账号,导致 10 次重试连续全部击中同一个耗尽账号;
  3. 熔断关闭时限流检测旁路:is_rate_limited 在 !config.enabled 时只读取周配额硬快照,旁路了记录于 limits 中的实时 429 到期时间戳锁定。

解决方案

  1. 物理客观配额事实与投机自适应熔断解耦:
    • 将 is_quota_depleted_signal 与 mark_model_quota_depleted 提取到熔断开关检查之前;确立不变量:“账号已被上游告知配额耗尽是客观物理事实,绝不受自适应熔断退避开关的影响”;
    • 一旦上游返回配额耗尽或携带权威重置时间戳,立即将内存中目标模型及其基名(strip_tier_suffix)配额标记为 0%,并精确锁定到期时间;
    • 在 rate_limit.rs 引入 is_quota_exhausted_or_explicit_limited,确保即使熔断器关闭,调度器依然拦截处于 QuotaExhausted 状态的账号。
  2. 重试循环引入 attempted_accounts 排除机制:
    • 将 TokenManager::get_token_filtered 提升为 pub;
    • 在 claude.rs、gemini.rs、openai.rs 重试循环中跟踪当前请求已尝试失败的账号集(attempted_accounts),并在触发账号级故障换号(should_rotate_account == true)时强制排除,确保重试时平滑且确定性地轮换到池中下一个健康账号;全池遍历后立即快速失败,避免漫长无效空转。
  3. 前端状态与基准对齐:
    • 修正 Settings.tsx 中的初始默认状态为 circuit_breaker: { enabled: true, backoff_steps: [60, 300, 1800, 7200] },杜绝保存配置导致熔断器意外关闭。
  4. 单测覆盖与防回归证明:
    • 新增 test_hard_quota_depleted_locks_and_rotates_even_with_circuit_breaker_disabled 单测,覆盖熔断器关闭时上游 429 配额耗尽的准确锁定与轮换逃逸(Red-Green 闭环验证);
    • 新增 test_get_token_filtered_excludes_attempted_accounts_in_retry_loop 单测,验证多轮重试排除机制。

提交清单(组内独立、可单独回退)

提交 作用 可否单独 revert
538d2255 fix(proxy,quota): 解耦客观物理配额耗尽与自适应熔断,增加单请求重试账号排除防反复锤击 是

每项提交引起的变化和影响面

  • 消除 429 报错后无法熔断、连续 10 次反复锤击同一账号的异常现象;
  • 保证当某账号额度用尽时,立即无感切至号池中可用的健康账号。

自检(与 CI 相同的命令)

  • cd src-tauri && cargo fmt -- --check
  • cd src-tauri && cargo clippy --all-targets --all-features
  • npm run build
  • cargo test test_hard_quota_depleted 通过 (1 passed)
  • cargo test test_get_token_filtered_excludes 通过 (1 passed)

署名

cubelikeplayDaniel

Ref #3838, #3840

🤖 Generated with Claude Code

1. 客观物理配额与投机熔断解耦:
   - 修复 mark_rate_limited_async_with_mode 与 fast 将 !config.enabled 置顶导致客观零配额/到期时间戳信号被丢弃的缺陷;
   - 无论自适应熔断开关是否启用,一旦上游返回 429 配额耗尽,坚定将目标模型及基名配额标记为 0% 并锁定到期时间;
   - 熔断关闭时 is_rate_limited 正确路由至 is_quota_exhausted_or_explicit_limited。
2. 重试循环账号排除与强制轮换:
   - 将 TokenManager::get_token_filtered 暴露为 pub,支持 excluded_accounts;
   - 在 claude、gemini、openai 请求处理器的重试循环中引入 attempted_accounts,发生账号级故障换号时排除当前请求已尝试账号,消除 10 次重试反复锤击同一耗尽账号的调度失忆症。
3. 前端配置对齐与单测覆盖:
   - 修正 Settings.tsx 中 circuit_breaker 初始状态漂移;
   - 补充 test_hard_quota_depleted_locks_and_rotates_even_with_circuit_breaker_disabled 与 test_get_token_filtered_excludes_attempted_accounts_in_retry_loop 端到端测试。

Ref lbjlaq#3838, lbjlaq#3840

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jeikl
jeikl merged commit ad12524 into lbjlaq:beta Nov 5, 2026
7 checks passed
@jeikl

jeikl commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator

已合并 ✅

感谢 @cubelikeplayDaniel 的这份修复。分析得很扎实,尤其是把「物理配额事实」和「自适应熔断」这两个不同性质的信号区分开的思路,是本轮最有价值的架构洞察。

已 squash 合并进 beta(ad125244)。

这份 PR 补上了我们遗漏的三个真实缺陷

我们上一轮(41714953 / 8952121a)主要解决的是「如何正确识别和锁定配额耗尽」,但没有覆盖"锁定之后,熔断关闭的场景下是否真的生效"。逐条核实如下:

缺陷 我们是否修复 本 PR
熔断关闭时硬配额信号被 if !config.enabled { return; } 丢弃 ❌ 未覆盖 ✅ 物理事实优先
重试循环不排除已失败账号(get_token 传空 excluded_accounts) ❌ 未覆盖 ✅ attempted_accounts
is_rate_limited 在熔断关闭时旁路实时 429 锁定 ❌ 未覆盖 ✅ is_quota_exhausted_or_explicit_limited
前端 circuit_breaker 默认 enabled: false,保存配置即关闭熔断 ❌ 未覆盖 ✅ 改为 true

结论:不重复,是互补。 我们解决了"识别",PR 解决了"生效"。

合并前的兼容性验证

因为两边改动了同一批文件(token_manager.rs / rate_limit.rs / 三个 handler),我们做了三方合并测试与语义验证:

1. 代码合并

git apply --check        → 9 文件失败(我们的 rustfmt 改了行格式)
git apply --3way --check → 全部 cleanly applied

无真实冲突,只是上下文行号漂移。

2. 判据复用关系

本 PR 直接复用了我们新增的 rate_limit::is_quota_depleted_signal(见 token_manager.rs 里的 is_quota_depleted = is_quota_depleted_signal(...) || reason == QuotaExhausted),没有重写第二套判据,这点做得很好。

3. 四条关键路径的等价逻辑推演(全部 PASS)

场景 预期 结果
argon 桶耗尽 + 熔断关闭 标记 0% + 慢速刷新 ✅
argon 桶耗尽 + 熔断开启 标记 0% + 慢速刷新 ✅
TPM 限流 + 熔断关闭 不标记、不刷新(行为与旧版一致) ✅ 关键边界,无误伤
check quota + 熔断关闭 触发 1.5% 木桶 ✅ 我们的入口仍有效

其中第三条是我们最关心的:非配额耗尽场景在熔断关闭时行为完全不变,没有引入回归。

4. 构建门禁

  • cargo fmt --check → 0 diff
  • cargo clippy --all-targets --all-features → 无 error
  • cargo check --tests → 通过
  • npm run build → 通过

一处值得肯定的实现细节

mark_model_quota_depleted 里补充了 strip_tier_suffix 基名归零:

let base_name = strip_tier_suffix(&m_lower);
if base_name != m_lower {
    token.model_quotas.insert(base_name, 0);
}

我们的原版只置了三处 key(norm / 原名 / lower),你这个补充让 gemini-X-flash-high 耗尽时,裸名 gemini-X-flash 也会被标记——更贴合实际调度中裸模型名的查询路径。


再次感谢。这类"客观事实 vs 自适应策略"的解耦思路,后续如果在其他信号上还有类似问题,欢迎继续提。

jeikl added a commit that referenced this pull request Nov 5, 2026
…3845, Ref #3838, #3840)

发布 beta.5,包含 PR #3845 的熔断解耦与重试去重修复。

- 版本号同步至 5.0.3-beta.5(package.json / Cargo.toml / Cargo.lock / tauri.conf.json / Cask / Settings.tsx)
- CHANGELOG.md 与 CHANGELOG_EN.md 补充本次更新:
  - 解耦客观物理配额耗尽与自适应熔断(PR #3845, Thanks to @cubelikeplayDaniel)
  - 重试循环引入 attempted_accounts 排除机制,杜绝反复锤击同一耗尽账号
  - 熔断关闭时不再旁路实时 429 锁定
  - 修正前端 circuit_breaker 默认值为 enabled: true
- beta.4 的 Release 因 GitHub API 瞬时 500 失败(publish-release 步骤,其余构建作业均成功),
  已删除悬空 tag,直接以 beta.5 交付

Co-Authored-By: JeikCode <code@jeikcode.top>
jeikl added a commit that referenced this pull request Nov 6, 2026
发布 v5.0.3 正式版,将 5.0.3 预发布线(beta.0 ~ beta.5)转正,功能与 beta 线完全一致。

- 配额归因治理:网关内部限流判据收敛至 policy::is_internal_gateway_exhaustion 单一权威源(根除占全量 5xx 92.6% 的 503 归因污染),新增 403 封禁专用归因 account_forbidden (Fixes #3838)
- 配额耗尽与自适应熔断彻底解耦,重试循环引入 attempted_accounts 账号排除,配额过滤前置于优先级收敛以消除 0 配额高优账号饥饿全池死锁 (Fixes #3841, PR #3845)
- 蹬额度模式重构为最高调度优先级竞争,保住 Prompt Cache 并自动外溢并发流量 (PR #3840)
- 超时契约解除 135s/160s 硬截断,首字 180s / 多模态 270s / 全局预算统一下沉 pipeline;Codex / Legacy 流式收敛至统一生命周期 (Fixes #3816, #3821)
- 自定义映射确立最高优先级、旧版模型硬下线与先映射后判定、号池自适应暗路由与 X-Upstream-Model 统一收敛 (Fixes #2065, Ref #3764)
- 多模态坏图 400 就地自愈、保鲜滑窗默认开启、Responses 日志脱敏与视觉 Token 估算 (Fixes #3758, #3824, #3756)
- 提示词清洗分轨保护用户正文、CTF 策略剥离与 Gzip 入站解压 (Fixes #3764, #3781)
- GCP Vertex AI 企业账号、IDE 2.6.0+ 三级双写切号、账号删除一致性、Web/Docker OAuth 与更新设置修复 (Fixes #3823, #3829, #3806, #3802, #3754)
- Linux 远程桌面保活、macOS Mini 视窗主线程调度、界面缩放;签名缓存写入放大治理与 Token 双视角 + TTFT 统计 (Fixes #3752, #3757, #3744, PR #3765, Fixes #3810)
- 回填 beta 分支缺失的 v5.0.0 正式版日志条目,保持 main 与 beta 内容一致

Co-Authored-By: JeikCode <code@jeikcode.top>
cubelikeplayDaniel added a commit to cubelikeplayDaniel/Antigravity-Manager that referenced this pull request Nov 6, 2026
lbjlaq#3845, lbjlaq#3852)

1. [P0-1 Weekly 硬配额优先契约]
   - 在 `ProxyToken` 引入 `model_quota_windows: HashMap<String, Vec<QuotaWindowStatus>>`,保存各配额桶详细窗口快照;
   - 重构 `is_model_quota_depleted_expired`:当账号周硬配额耗尽 (0%) 且未越过周重置时,严禁仅因短期 5h 重置误触发自愈;
2. [P0-2 模型族重置时钟隔离]
   - 提取 `resolve_model_reset_time` 与 `resolve_model_group_key`,按 Argon / 3P / Gemini 独立维护重置时间映射;
   - 当模型族映射非空时,未匹配模型严禁回退至全局最早重置时间,根除跨模型重置时间污染;
3. [P1-1 ~ P1-3 状态同步与多桶收敛]
   - 修正 `sync_zero_quota_circuit_breaker` 中的 `all_buckets_positive`,缺失剩余比例时不默认判为正额度;
   - `load_single_account` 与 `fetch_and_lock_with_realtime_quota` 全状态同步 `model_reset_times`、`model_weekly_reset_times` 与 `model_quota_windows`;
   - 多桶同窗口取最早截止时间戳 (`.min(ts)`),动态从 `quota.models` 提取模型名称;
4. [P1-4 准入资格与权重排序解耦]
   - 拆分 `has_candidate_quota_admission`(布尔准入资格)与 `get_candidate_quota`(排序权重计算);
   - 自愈到期账号赋予保守正向 sentinel (20),参与调度竞争的同时绝不抢占健康高配额 (80%-100%) 账号;
5. [P1-5 & P1-6 精准解限与确定性单测]
   - 账号重载与正配额快照恢复时调用 `reconcile_all_quota_recovery_for_account`,仅清除 `QuotaExhausted` 并保留 429 `RateLimitExceeded`;
   - 增加 `test_weekly_hard_quota_depleted_blocks_self_healing_until_weekly_reset`、`test_model_family_isolation_prevents_argon_reset_contaminating_gemini` 等全套确定性单元测试。

Co-Authored-By: @jeikl <jeikl@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cubelikeplayDaniel added a commit to cubelikeplayDaniel/Antigravity-Manager that referenced this pull request Nov 6, 2026
…lbjlaq#3840, lbjlaq#3845, lbjlaq#3852, lbjlaq#3854, lbjlaq#3857)

发布 v5.0.4-beta.1,包含:
- 分级配额契约与自愈架构重构:落实周硬配额绝对优先契约、模型族隔离与安全自愈 (PR lbjlaq#3852, Fixes lbjlaq#3840, lbjlaq#3845)
- Docker 容器健康检查超时修复与 Tini Init 进程泄漏治理 (PR lbjlaq#3854, Fixes lbjlaq#3850)
- 上传带宽语义与预算对齐,根治大请求体 15s 误杀 (PR lbjlaq#3857, Ref lbjlaq#3763, Ref lbjlaq#3821)
- 账号封禁识别与无配额门禁卡死修复 (commit 03c83b1, Ref lbjlaq#3525, lbjlaq#3630, lbjlaq#3631, lbjlaq#3840)
- 版本号全量同步至 5.0.4-beta.1(package.json / package-lock.json / Cargo.toml / Cargo.lock / tauri.conf.json / Cask / Settings.tsx)

Co-Authored-By: @jeikl <jeikl@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cubelikeplayDaniel added a commit to cubelikeplayDaniel/Antigravity-Manager that referenced this pull request Nov 6, 2026
…lbjlaq#3840, lbjlaq#3845, lbjlaq#3852, lbjlaq#3854, lbjlaq#3857, lbjlaq#3864, lbjlaq#3866)

发布 v5.0.4-beta.1,包含:
- 分级配额契约与自愈架构重构:落实周硬配额绝对优先契约、模型族隔离与安全自愈 (PR lbjlaq#3852, Fixes lbjlaq#3840, lbjlaq#3845)
- Docker 容器健康检查超时修复与 Tini Init 进程泄漏治理 (PR lbjlaq#3854, Fixes lbjlaq#3850)
- 上传带宽语义与预算对齐,根治大请求体 15s 误杀 (PR lbjlaq#3857, Ref lbjlaq#3763, Ref lbjlaq#3821)
- 账号封禁识别与无配额门禁卡死修复 (commit 03c83b1, Ref lbjlaq#3525, lbjlaq#3630, lbjlaq#3631, lbjlaq#3840)
- 文本内嵌 Data URL 视觉自愈与超大 Token 计费治理 (PR lbjlaq#3864, Ref lbjlaq#3830)
- Responses 协议工具调用与回执相邻性对齐:根治 Claude 400 Prefill 报错 (PR lbjlaq#3866)
- 版本号全量同步至 5.0.4-beta.1(package.json / package-lock.json / Cargo.toml / Cargo.lock / tauri.conf.json / Cask / Settings.tsx)

Co-Authored-By: @jeikl <jeikl@users.noreply.github.com>
Co-Authored-By: @mclinyeshi <mclinyeshi@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cubelikeplayDaniel added a commit to cubelikeplayDaniel/Antigravity-Manager that referenced this pull request Nov 6, 2026
lbjlaq#3845, lbjlaq#3852)

1. [P0-1 Weekly 硬配额优先契约]
   - 在 `ProxyToken` 引入 `model_quota_windows: HashMap<String, Vec<QuotaWindowStatus>>`,保存各配额桶详细窗口快照;
   - 重构 `is_model_quota_depleted_expired`:当账号周硬配额耗尽 (0%) 且未越过周重置时,严禁仅因短期 5h 重置误触发自愈;
2. [P0-2 模型族重置时钟隔离]
   - 提取 `resolve_model_reset_time` 与 `resolve_model_group_key`,按 Argon / 3P / Gemini 独立维护重置时间映射;
   - 当模型族映射非空时,未匹配模型严禁回退至全局最早重置时间,根除跨模型重置时间污染;
3. [P1-1 ~ P1-3 状态同步与多桶收敛]
   - 修正 `sync_zero_quota_circuit_breaker` 中的 `all_buckets_positive`,缺失剩余比例时不默认判为正额度;
   - `load_single_account` 与 `fetch_and_lock_with_realtime_quota` 全状态同步 `model_reset_times`、`model_weekly_reset_times` 与 `model_quota_windows`;
   - 多桶同窗口取最早截止时间戳 (`.min(ts)`),动态从 `quota.models` 提取模型名称;
4. [P1-4 准入资格与权重排序解耦]
   - 拆分 `has_candidate_quota_admission`(布尔准入资格)与 `get_candidate_quota`(排序权重计算);
   - 自愈到期账号赋予保守正向 sentinel (20),参与调度竞争的同时绝不抢占健康高配额 (80%-100%) 账号;
5. [P1-5 & P1-6 精准解限与确定性单测]
   - 账号重载与正配额快照恢复时调用 `reconcile_all_quota_recovery_for_account`,仅清除 `QuotaExhausted` 并保留 429 `RateLimitExceeded`;
   - 增加 `test_weekly_hard_quota_depleted_blocks_self_healing_until_weekly_reset`、`test_model_family_isolation_prevents_argon_reset_contaminating_gemini` 等全套确定性单元测试。

Co-Authored-By: @jeikl <jeikl@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cubelikeplayDaniel added a commit to cubelikeplayDaniel/Antigravity-Manager that referenced this pull request Nov 6, 2026
…lbjlaq#3840, lbjlaq#3845, lbjlaq#3852, lbjlaq#3854, lbjlaq#3857, lbjlaq#3864, lbjlaq#3866, lbjlaq#3867)

发布 v5.0.4-beta.1,包含:
- 分级配额契约与自愈架构重构:落实周硬配额绝对优先契约、模型族隔离与安全自愈 (PR lbjlaq#3852, Fixes lbjlaq#3840, lbjlaq#3845)
- Docker 容器健康检查超时修复与 Tini Init 进程泄漏治理 (PR lbjlaq#3854, Fixes lbjlaq#3850)
- 上传带宽语义与预算对齐,根治大请求体 15s 误杀 (PR lbjlaq#3857, Ref lbjlaq#3763, Ref lbjlaq#3821)
- 账号封禁识别与无配额门禁卡死修复 (commit 03c83b1, Ref lbjlaq#3525, lbjlaq#3630, lbjlaq#3631, lbjlaq#3840)
- 文本内嵌 Data URL 视觉自愈与超大 Token 计费治理 (PR lbjlaq#3864, Ref lbjlaq#3830)
- Responses 协议工具调用与回执相邻性对齐:根治 Claude 400 与 Gemini 缓存跳变 (PR lbjlaq#3866, PR lbjlaq#3867)
- 版本号全量同步至 5.0.4-beta.1(package.json / package-lock.json / Cargo.toml / Cargo.lock / tauri.conf.json / Cask / Settings.tsx)

Co-Authored-By: @jeikl <jeikl@users.noreply.github.com>
Co-Authored-By: @mclinyeshi <mclinyeshi@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants