Merge pull request #7624 from wucm667/fix/issue-7620-future-quota-reset

fix(openai): retain quota pause until known future reset
This commit is contained in:
Wesley Liddick
2026-09-28 09:54:52 +08:00
committed by GitHub
4 changed files with 77 additions and 10 deletions
@@ -271,13 +271,19 @@ func openAIThresholdCandidate(extra map[string]any, window string, now time.Time
if !ok {
return nil
}
if openAIQuotaWindowReset(extra, window, now) || openAICodexSnapshotStaleForPause(extra, now) {
if openAIQuotaWindowReset(extra, window, now) || (openAICodexSnapshotStaleForPause(extra, now) && !openAIQuotaWindowResetPending(extra, window, now)) {
return nil
}
until := parseSchedulingResetAt(extra[resetAtKey])
if until == nil {
if resetAt, ok := openAICodexWindowResetAt(extra, window); ok {
until = &resetAt
}
}
return &accountSchedulingThresholdCandidate{
window: window,
usedPercent: schedulingPercentValue(usedPercent),
until: parseSchedulingResetAt(extra[resetAtKey]),
until: until,
}
}
@@ -172,7 +172,7 @@ func TestEvaluateAccountSchedulingThreshold_OpenAISkipsStaleSnapshot(t *testing.
Extra: map[string]any{
"codex_usage_updated_at": now.Add(-2 * time.Hour).Format(time.RFC3339),
"codex_5h_used_percent": 100.0,
"codex_5h_reset_at": now.Add(3 * time.Hour).Format(time.RFC3339),
"codex_5h_reset_at": "invalid",
},
}
@@ -181,6 +181,63 @@ func TestEvaluateAccountSchedulingThreshold_OpenAISkipsStaleSnapshot(t *testing.
require.False(t, decision.ShouldPause)
}
func TestOpenAIThresholdCandidate_StaleSnapshotFutureReset(t *testing.T) {
now := time.Date(2026, 6, 3, 12, 0, 0, 0, time.UTC)
for _, tc := range []struct {
name string
reset map[string]any
want bool
}{
{"absolute future", map[string]any{"codex_5h_reset_at": now.Add(time.Hour).Format(time.RFC3339)}, true},
{"relative future", map[string]any{"codex_5h_reset_after_seconds": 4 * 3600}, true},
{"missing", nil, false},
{"invalid", map[string]any{"codex_5h_reset_at": "invalid"}, false},
{"past", map[string]any{"codex_5h_reset_at": now.Add(-time.Minute).Format(time.RFC3339)}, false},
} {
t.Run(tc.name, func(t *testing.T) {
extra := map[string]any{"codex_usage_updated_at": now.Add(-3 * time.Hour).Format(time.RFC3339), "codex_5h_used_percent": 99.0}
for key, value := range tc.reset {
extra[key] = value
}
candidate := openAIThresholdCandidate(extra, "5h", now)
require.Equal(t, tc.want, candidate != nil)
if tc.want {
decision := EvaluateAccountSchedulingThreshold(&Account{Platform: PlatformOpenAI, Extra: extra}, map[string]int{PlatformOpenAI: 95}, now)
require.True(t, decision.ShouldPause)
require.NotNil(t, decision.Until)
require.True(t, now.Before(*decision.Until))
}
})
}
}
func TestResolveOpenAIQuotaUtilization_StaleSnapshotFutureReset(t *testing.T) {
now := time.Date(2026, 6, 3, 12, 0, 0, 0, time.UTC)
for _, tc := range []struct {
name string
reset map[string]any
want bool
}{
{"absolute future", map[string]any{"codex_5h_reset_at": now.Add(time.Hour).Format(time.RFC3339)}, true},
{"relative future", map[string]any{"codex_5h_reset_after_seconds": 4 * 3600}, true},
{"missing", nil, false},
{"invalid", map[string]any{"codex_5h_reset_at": "invalid"}, false},
{"past", map[string]any{"codex_5h_reset_at": now.Add(-time.Minute).Format(time.RFC3339)}, false},
} {
t.Run(tc.name, func(t *testing.T) {
extra := map[string]any{"codex_usage_updated_at": now.Add(-3 * time.Hour).Format(time.RFC3339), "codex_5h_used_percent": 99.0}
for key, value := range tc.reset {
extra[key] = value
}
utilization, ok := resolveOpenAIQuotaUtilization(extra, "5h", now)
require.Equal(t, tc.want, ok)
if ok {
require.Equal(t, 0.99, utilization)
}
})
}
}
func TestEvaluateAccountSchedulingThreshold_OpenAISkipsResetWindow(t *testing.T) {
t.Parallel()
@@ -1906,7 +1906,7 @@ func TestOpenAIGatewayService_SelectAccountForModelWithExclusions_FreshUsageWind
// inversion) gets excluded from scheduling, and a paused account never receives traffic to
// refresh its snapshot. When the snapshot is stale (codex_usage_updated_at older than the
// staleness bound) the account must be allowed a request so it can self-heal from the real
// response headers — independent of the window's reset time.
// response headers when no valid future window reset is known.
func TestOpenAIGatewayService_SelectAccountForModelWithExclusions_StaleUsageSnapshotSkipsPause_Issue2994(t *testing.T) {
ctx := context.Background()
primary := Account{
@@ -1920,8 +1920,7 @@ func TestOpenAIGatewayService_SelectAccountForModelWithExclusions_StaleUsageSnap
Extra: map[string]any{
"codex_5h_used_percent": 99.0,
"auto_pause_5h_threshold": 0.95,
// Window has NOT reset yet, so the reset guard stays inactive.
"codex_5h_reset_at": time.Now().Add(time.Hour).Format(time.RFC3339),
// No reset time is known, so stale snapshot self-heal applies.
// Snapshot is stale: older than openAICodexAutoPauseStaleAfter (2h).
"codex_usage_updated_at": time.Now().Add(-3 * time.Hour).Format(time.RFC3339),
},
@@ -662,8 +662,8 @@ func resolveAccountExtraNumber(extra map[string]any, keys ...string) (float64, b
// resolveOpenAIQuotaUtilization returns the current utilization ratio (0..1) for the
// given Codex usage window. ok=false means there is no usable signal to pause on:
// either no snapshot exists, or the window has already rolled over so the cached
// percentage is stale. The stale guard matters because a paused account stops
// either no snapshot exists, the window has rolled over, or an old snapshot has
// no known future reset. The stale guard matters because a paused account stops
// receiving requests, so its snapshot is never refreshed from upstream headers —
// without this check an old used_percent would keep the account paused forever even
// after the real window reset.
@@ -675,14 +675,19 @@ func resolveOpenAIQuotaUtilization(extra map[string]any, window string, now time
if openAIQuotaWindowReset(extra, window, now) {
return 0, false
}
// 快照过于陈旧(账号长期未收到流量刷新)时,不再据此暂停。放行后下一次响应头
// 快照过于陈旧且没有明确的未来重置时间时,不再据此暂停。放行后下一次响应头
// 会刷新快照实现自愈,避免账号在错误/过期的 used% 上被永久跳过(issue #2994)。
if openAICodexSnapshotStaleForPause(extra, now) {
if openAICodexSnapshotStaleForPause(extra, now) && !openAIQuotaWindowResetPending(extra, window, now) {
return 0, false
}
return usedPercent / 100, true
}
func openAIQuotaWindowResetPending(extra map[string]any, window string, now time.Time) bool {
resetAt, ok := openAICodexWindowResetAt(extra, window)
return ok && now.Before(resetAt)
}
// openAICodexSnapshotStaleForPause reports whether the Codex usage snapshot is stale
// enough that it should no longer keep an account auto-paused. It anchors on
// codex_usage_updated_at (always written by buildCodexUsageExtraUpdates). A missing or