Skip to content

fix: allow account failover on exhausted paid quota#739

Closed
HSJ-BanFan wants to merge 1 commit into
chenyme:mainfrom
HSJ-BanFan:fix/payment-required-account-failover
Closed

fix: allow account failover on exhausted paid quota#739
HSJ-BanFan wants to merge 1 commit into
chenyme:mainfrom
HSJ-BanFan:fix/payment-required-account-failover

Conversation

@HSJ-BanFan

Copy link
Copy Markdown

问题

当 Grok Build 账号额度耗尽时,上游可能返回:

HTTP/1.1 402 Payment Required
X-Should-Retry: false

响应体包含:

{
  "code": "personal-team-blocked:spending-limit",
  "error": "You have run out of credits or need a Grok subscription."
}

网关原先将 X-Should-Retry: false 解释为禁止整个请求继续重试,因此会直接将 402 返回给客户端,不会尝试账号池中的其他可用账号。

这会导致单个账号额度耗尽阻断整个请求,即使账号池中仍有额度正常且支持当前模型的账号。

根因

X-Should-Retry 描述的是是否应该重试当前上游账号,但现有 isRetryableResponse 将它作为账号池级别的全局重试否决条件。

对于 personal-team-blocked:spending-limit,重试同一个账号没有意义,但切换到其他账号仍然可以成功处理请求。

此外,当上游已经明确确认额度耗尽,而本地 Billing 快照没有可靠的账期结束时间时,现有逻辑无法通过 MarkPaidQuotaExhausted 将该账号移出路由池。账号可能很快再次被选中,并继续影响其他会话。

修改内容

  • 402 Payment Required 保留跨账号故障转移能力,即使响应包含 X-Should-Retry: false
  • 普通 4034295xx 响应仍继续遵守上游的 X-Should-Retry: false,避免扩大重试范围。
  • 当上游明确返回付费额度耗尽时:
    • Billing 包含可靠账期结束时间,则等到账期结束后进行恢复探测。
    • Billing 快照不完整或无法确定账期结束时间,则默认等待 24 小时后进行恢复探测。
  • 将额度耗尽账号从现有 sticky session 中解除,并立即失效候选账号缓存。
  • 如果额度恢复状态无法持久化,则使用账号冷却机制作为兜底,避免账号立即重新进入路由池。

行为说明

场景 修改前 修改后
402 + spending-limit + X-Should-Retry:false 直接返回 402 标记当前账号额度耗尽并尝试下一个账号
Billing 有可靠账期结束时间 等到账期结束后探测 行为不变
Billing 无可靠账期结束时间 账号可能很快重新进入路由池 24 小时后进行恢复探测
普通 403/429/5xx + X-Should-Retry:false 不重试 行为不变
故障转移成功 sticky session 更新为新的可用账号

兼容性

  • 不修改下游 API 协议。
  • 不增加配置项。
  • 不需要数据库迁移。
  • 不改变正常账号的选择顺序。
  • 不改变普通 4034295xx 的上游重试语义。
  • 仅影响明确属于账号额度耗尽的 402 响应。

测试

新增回归测试覆盖:

  • 普通响应继续遵守 X-Should-Retry: false
  • 402 不会被该响应头阻止跨账号故障转移。
  • 第一个账号返回 personal-team-blocked:spending-limit 后,请求由第二个账号成功处理。
  • 故障转移后 sticky session 绑定到第二个账号。
  • 额度耗尽账号进入恢复等待状态。
  • Billing 快照缺少可靠账期结束时间时,使用 24 小时恢复探测。

已执行:

cd backend
go test ./internal/application/gateway -count=1
go vet ./internal/application/gateway

git diff --check

相关 gateway 测试、vet 和 diff 检查均通过。

@zhangruyide

Copy link
Copy Markdown

感谢修复,这个方向是正确的。我在 v3.0.7 基础上验证了该提交,gatewaygo testgo vet 均通过。

不过当前实现对 Free 账号存在一个建议在合并前处理的语义问题:

MarkPaidQuotaExhaustedbilling == nilbilling.IsPaid() == false 时,仍会写入:

Kind: account.QuotaRecoveryKindPaid

newQuotaView 只有在 Billing 被识别为 paid 时才展示 paid recovery;Free 账号分支只识别 QuotaRecoveryKindFree

因此,当 Free Build 账号返回 402 personal-team-blocked:spending-limit 时:

  • 路由层会正确排除该账号;
  • 但管理页仍可能显示账号状态正常;
  • 到期后执行的是 paid Billing probe,而不是 Free recovery probe;
  • Billing 中可能存在的 UsagePeriodEnd 和响应的 Retry-After 没有得到利用。

建议:

  1. 为 Build 402 增加独立的 MarkBuildCreditsExhausted
  2. billing != nil && billing.IsPaid() 时写入 QuotaRecoveryKindPaid;Free/未知账号写入 QuotaRecoveryKindFree
  3. 恢复时间优先使用未来的 UsagePeriodEnd,其次使用 Retry-After,最后使用 24 小时兜底。
  4. 增加 Free 账号 402 后 quota.status == waitingReset 的测试。
  5. 当前 isRetryableResponse 会让所有 Provider 的任何 402 忽略 X-Should-Retry:false,建议考虑收窄到 Build 402,或明确这是有意扩大范围。

账号级 402 跨账号故障转移以及持久化失败时使用 cooldown 兜底的整体设计是合理的;主要需要补齐 Free/paid recovery 类型与管理页状态的一致性。

@HSJ-BanFan

Copy link
Copy Markdown
Author

感谢修复,这个方向是正确的。我在v3.0.7基础上验证了该提交,gatewaygo testgo vet均通过。

当前实现对免费账号存在一个建议在合并前处理的语义问题:

MarkPaidQuotaExhausted此时billing == nilbilling.IsPaid() == false仍会写入:

Kind: account.QuotaRecoveryKindPaid

newQuotaView只有在计费被识别为付费时才显示付费恢复;免费账号分支只识别QuotaRecoveryKindFree

因此,当 Free Build 账号返回402 personal-team-blocked:spending-limit时:

  • 路由层会正确排除该账号;
  • 但管理页仍可能显示账号状态正常;
  • 近期执行的是付费计费探针,而不是免费恢复探针;
  • 计费中可能存在的UsagePeriodEnd和响应的Retry-After未得到利用。

建议:

  1. 为Build 402增加独立的MarkBuildCreditsExhausted
  2. billing != nil && billing.IsPaid()时写入QuotaRecoveryKindPaid;免费/未知账号写入QuotaRecoveryKindFree
  3. 恢复时间优先使用未来的UsagePeriodEnd,其次使用Retry-After,最后使用24小时兜底。
  4. 增加免费账号 402 后quota.status == waitingReset的测试。
  5. 当前,isRetryableResponse所有提供商的任何 402 都可以忽略不计X-Should-Retry:false,考虑建议收窄至 Build 402,或明确这是认知扩大范围。

账号级 402 跨账号故障转移以及持久化失败时使用冷却兜底的整体设计是合理的;主要需要补齐免费/付费恢复类型与管理页状态的一致性。

好勒

@chenyme chenyme closed this Jul 22, 2026
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.

3 participants