Merge pull request #2993 from ghostg00/fix/openai-5h-used-percent-direct

fix(usage): revert OpenAI 5h used_percent inversion (#2918 regression)
This commit is contained in:
Wesley Liddick 2026-06-04 16:02:52 +08:00 committed by GitHub
commit f1aa589646
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 22 additions and 96 deletions

View File

@ -132,7 +132,7 @@ func TestAccountTestService_OpenAISuccessPersistsSnapshotFromHeaders(t *testing.
require.Len(t, upstream.requests, 1)
require.Equal(t, HTTPUpstreamProfileOpenAI, HTTPUpstreamProfileFromContext(upstream.requests[0].Context()))
require.NotEmpty(t, repo.updatedExtra)
require.Equal(t, 58.0, repo.updatedExtra["codex_5h_used_percent"])
require.Equal(t, 42.0, repo.updatedExtra["codex_5h_used_percent"])
require.Equal(t, 88.0, repo.updatedExtra["codex_7d_used_percent"])
require.Contains(t, recorder.Body.String(), "test_complete")
}
@ -170,7 +170,7 @@ func TestAccountTestService_OpenAI429PersistsSnapshotAndRateLimitState(t *testin
resp.Header.Set("x-codex-primary-used-percent", "100")
resp.Header.Set("x-codex-primary-reset-after-seconds", "604800")
resp.Header.Set("x-codex-primary-window-minutes", "10080")
resp.Header.Set("x-codex-secondary-used-percent", "0")
resp.Header.Set("x-codex-secondary-used-percent", "100")
resp.Header.Set("x-codex-secondary-reset-after-seconds", "18000")
resp.Header.Set("x-codex-secondary-window-minutes", "300")

View File

@ -73,7 +73,7 @@ func TestExtractOpenAICodexProbeUpdatesAccepts429WithCodexHeaders(t *testing.T)
headers.Set("x-codex-primary-used-percent", "100")
headers.Set("x-codex-primary-reset-after-seconds", "604800")
headers.Set("x-codex-primary-window-minutes", "10080")
headers.Set("x-codex-secondary-used-percent", "0")
headers.Set("x-codex-secondary-used-percent", "100")
headers.Set("x-codex-secondary-reset-after-seconds", "18000")
headers.Set("x-codex-secondary-window-minutes", "300")
@ -92,33 +92,6 @@ func TestExtractOpenAICodexProbeUpdatesAccepts429WithCodexHeaders(t *testing.T)
}
}
func TestBuildCodexUsageProgressFromExtra_UsesCanonicalUsedPercent(t *testing.T) {
t.Parallel()
now := time.Date(2026, 5, 30, 7, 4, 9, 0, time.UTC)
extra := map[string]any{
"codex_5h_used_percent": 94.0,
"codex_5h_reset_at": now.Add(2 * time.Hour).Format(time.RFC3339),
"codex_7d_used_percent": 93.0,
"codex_7d_reset_at": now.Add(5 * 24 * time.Hour).Format(time.RFC3339),
}
fiveHour := buildCodexUsageProgressFromExtra(extra, "5h", now)
if fiveHour == nil {
t.Fatal("expected non-nil 5h progress")
}
if fiveHour.Utilization != 94.0 {
t.Fatalf("5h Utilization = %v, want 94", fiveHour.Utilization)
}
sevenDay := buildCodexUsageProgressFromExtra(extra, "7d", now)
if sevenDay == nil {
t.Fatal("expected non-nil 7d progress")
}
if sevenDay.Utilization != 93.0 {
t.Fatalf("7d Utilization = %v, want 93", sevenDay.Utilization)
}
}
func TestAccountUsageService_PersistOpenAICodexProbeSnapshotOnlyUpdatesExtra(t *testing.T) {
t.Parallel()

View File

@ -126,19 +126,6 @@ type NormalizedCodexLimits struct {
Window7dMinutes *int
}
func normalizeCodexFiveHourUsedPercent(raw *float64) *float64 {
if raw == nil {
return nil
}
// OpenAI's 5h Codex quota header is remaining%, despite the upstream header
// name saying "used"; the canonical codex_5h_used_percent field stores used%.
used := 100 - *raw
if used < 0 {
used = 0
}
return &used
}
// Normalize converts primary/secondary fields to canonical 5h/7d fields.
// Strategy: Compare window_minutes to determine which is 5h vs 7d.
// Returns nil if snapshot is nil or has no useful data.
@ -197,7 +184,7 @@ func (s *OpenAICodexUsageSnapshot) Normalize() *NormalizedCodexLimits {
// Assign values
if use5hFromPrimary {
result.Used5hPercent = normalizeCodexFiveHourUsedPercent(s.PrimaryUsedPercent)
result.Used5hPercent = s.PrimaryUsedPercent
result.Reset5hSeconds = s.PrimaryResetAfterSeconds
result.Window5hMinutes = s.PrimaryWindowMinutes
result.Used7dPercent = s.SecondaryUsedPercent
@ -207,7 +194,7 @@ func (s *OpenAICodexUsageSnapshot) Normalize() *NormalizedCodexLimits {
result.Used7dPercent = s.PrimaryUsedPercent
result.Reset7dSeconds = s.PrimaryResetAfterSeconds
result.Window7dMinutes = s.PrimaryWindowMinutes
result.Used5hPercent = normalizeCodexFiveHourUsedPercent(s.SecondaryUsedPercent)
result.Used5hPercent = s.SecondaryUsedPercent
result.Reset5hSeconds = s.SecondaryResetAfterSeconds
result.Window5hMinutes = s.SecondaryWindowMinutes
}

View File

@ -104,40 +104,6 @@ func TestBuildCodexUsageExtraUpdates_UsesSnapshotUpdatedAt(t *testing.T) {
}
}
func TestBuildCodexUsageExtraUpdates_NormalizesFiveHourRemainingToUsedPercent(t *testing.T) {
primaryUsed := 93.0
primaryReset := 86400
primaryWindow := 10080
secondaryRemaining := 6.0
secondaryReset := 3600
secondaryWindow := 300
snapshot := &OpenAICodexUsageSnapshot{
PrimaryUsedPercent: &primaryUsed,
PrimaryResetAfterSeconds: &primaryReset,
PrimaryWindowMinutes: &primaryWindow,
SecondaryUsedPercent: &secondaryRemaining,
SecondaryResetAfterSeconds: &secondaryReset,
SecondaryWindowMinutes: &secondaryWindow,
UpdatedAt: "2026-05-30T07:04:09Z",
}
updates := buildCodexUsageExtraUpdates(snapshot, time.Time{})
if updates == nil {
t.Fatal("expected non-nil updates")
}
if got := updates["codex_secondary_used_percent"]; got != 6.0 {
t.Fatalf("codex_secondary_used_percent = %v, want raw upstream value 6", got)
}
if got := updates["codex_5h_used_percent"]; got != 94.0 {
t.Fatalf("codex_5h_used_percent = %v, want 94", got)
}
if got := updates["codex_7d_used_percent"]; got != 93.0 {
t.Fatalf("codex_7d_used_percent = %v, want 93", got)
}
}
func TestBuildCodexUsageExtraUpdates_FallbackToNowWhenUpdatedAtInvalid(t *testing.T) {
primaryUsed := 15.0
primaryReset := 30

View File

@ -1774,7 +1774,7 @@ func TestOpenAIUpdateCodexUsageSnapshotFromHeaders(t *testing.T) {
select {
case updates := <-repo.updateExtraCalls:
require.Equal(t, 88.0, updates["codex_5h_used_percent"])
require.Equal(t, 12.0, updates["codex_5h_used_percent"])
require.Equal(t, 34.0, updates["codex_7d_used_percent"])
require.Equal(t, 600, updates["codex_5h_reset_after_seconds"])
require.Equal(t, 86400, updates["codex_7d_reset_after_seconds"])

View File

@ -51,7 +51,7 @@ func TestCalculateOpenAI429ResetTime_5hExhausted(t *testing.T) {
headers.Set("x-codex-primary-used-percent", "50")
headers.Set("x-codex-primary-reset-after-seconds", "500000")
headers.Set("x-codex-primary-window-minutes", "10080") // 7 days
headers.Set("x-codex-secondary-used-percent", "0")
headers.Set("x-codex-secondary-used-percent", "100")
headers.Set("x-codex-secondary-reset-after-seconds", "3600") // 1 hour
headers.Set("x-codex-secondary-window-minutes", "300") // 5 hours
@ -122,7 +122,7 @@ func TestCalculateOpenAI429ResetTime_ReversedWindowOrder(t *testing.T) {
// Test when OpenAI sends primary as 5h and secondary as 7d (reversed)
headers := http.Header{}
headers.Set("x-codex-primary-used-percent", "0") // This is 5h remaining%
headers.Set("x-codex-primary-used-percent", "100") // This is 5h
headers.Set("x-codex-primary-reset-after-seconds", "3600") // 1 hour
headers.Set("x-codex-primary-window-minutes", "300") // 5 hours - smaller!
headers.Set("x-codex-secondary-used-percent", "50")
@ -180,7 +180,7 @@ func TestHandle429_OpenAIPersistsCodexSnapshotImmediately(t *testing.T) {
headers.Set("x-codex-primary-used-percent", "100")
headers.Set("x-codex-primary-reset-after-seconds", "604800")
headers.Set("x-codex-primary-window-minutes", "10080")
headers.Set("x-codex-secondary-used-percent", "0")
headers.Set("x-codex-secondary-used-percent", "100")
headers.Set("x-codex-secondary-reset-after-seconds", "18000")
headers.Set("x-codex-secondary-window-minutes", "300")
@ -224,7 +224,7 @@ func TestNormalizedCodexLimits(t *testing.T) {
pUsed := 100.0
pReset := 384607
pWindow := 10080
sRemaining := 3.0
sUsed := 3.0
sReset := 17369
sWindow := 300
@ -232,7 +232,7 @@ func TestNormalizedCodexLimits(t *testing.T) {
PrimaryUsedPercent: &pUsed,
PrimaryResetAfterSeconds: &pReset,
PrimaryWindowMinutes: &pWindow,
SecondaryUsedPercent: &sRemaining,
SecondaryUsedPercent: &sUsed,
SecondaryResetAfterSeconds: &sReset,
SecondaryWindowMinutes: &sWindow,
}
@ -249,8 +249,8 @@ func TestNormalizedCodexLimits(t *testing.T) {
if normalized.Reset7dSeconds == nil || *normalized.Reset7dSeconds != 384607 {
t.Errorf("expected Reset7dSeconds=384607, got %v", normalized.Reset7dSeconds)
}
if normalized.Used5hPercent == nil || *normalized.Used5hPercent != 97.0 {
t.Errorf("expected Used5hPercent=97, got %v", normalized.Used5hPercent)
if normalized.Used5hPercent == nil || *normalized.Used5hPercent != 3.0 {
t.Errorf("expected Used5hPercent=3, got %v", normalized.Used5hPercent)
}
if normalized.Reset5hSeconds == nil || *normalized.Reset5hSeconds != 17369 {
t.Errorf("expected Reset5hSeconds=17369, got %v", normalized.Reset5hSeconds)
@ -338,11 +338,11 @@ func TestRateLimitService_HandleUpstreamError_403FallsBackToRawBody(t *testing.T
func TestNormalizedCodexLimits_OnlySecondaryData(t *testing.T) {
// Test when only secondary has data, no window_minutes
sRemaining := 60.0
sUsed := 60.0
sReset := 3000
snapshot := &OpenAICodexUsageSnapshot{
SecondaryUsedPercent: &sRemaining,
SecondaryUsedPercent: &sUsed,
SecondaryResetAfterSeconds: &sReset,
// No window_minutes, no primary data
}
@ -354,8 +354,8 @@ func TestNormalizedCodexLimits_OnlySecondaryData(t *testing.T) {
// Legacy assumption: primary=7d, secondary=5h
// So secondary goes to 5h
if normalized.Used5hPercent == nil || *normalized.Used5hPercent != 40.0 {
t.Errorf("expected Used5hPercent=40, got %v", normalized.Used5hPercent)
if normalized.Used5hPercent == nil || *normalized.Used5hPercent != 60.0 {
t.Errorf("expected Used5hPercent=60, got %v", normalized.Used5hPercent)
}
if normalized.Reset5hSeconds == nil || *normalized.Reset5hSeconds != 3000 {
t.Errorf("expected Reset5hSeconds=3000, got %v", normalized.Reset5hSeconds)
@ -370,13 +370,13 @@ func TestNormalizedCodexLimits_BothDataNoWindowMinutes(t *testing.T) {
// Test when both have data but no window_minutes
pUsed := 100.0
pReset := 400000
sRemaining := 30.0
sUsed := 50.0
sReset := 10000
snapshot := &OpenAICodexUsageSnapshot{
PrimaryUsedPercent: &pUsed,
PrimaryResetAfterSeconds: &pReset,
SecondaryUsedPercent: &sRemaining,
SecondaryUsedPercent: &sUsed,
SecondaryResetAfterSeconds: &sReset,
// No window_minutes
}
@ -393,8 +393,8 @@ func TestNormalizedCodexLimits_BothDataNoWindowMinutes(t *testing.T) {
if normalized.Reset7dSeconds == nil || *normalized.Reset7dSeconds != 400000 {
t.Errorf("expected Reset7dSeconds=400000, got %v", normalized.Reset7dSeconds)
}
if normalized.Used5hPercent == nil || *normalized.Used5hPercent != 70.0 {
t.Errorf("expected Used5hPercent=70, got %v", normalized.Used5hPercent)
if normalized.Used5hPercent == nil || *normalized.Used5hPercent != 50.0 {
t.Errorf("expected Used5hPercent=50, got %v", normalized.Used5hPercent)
}
if normalized.Reset5hSeconds == nil || *normalized.Reset5hSeconds != 10000 {
t.Errorf("expected Reset5hSeconds=10000, got %v", normalized.Reset5hSeconds)
@ -425,7 +425,7 @@ func TestCalculateOpenAI429ResetTime_UserProvidedScenario(t *testing.T) {
// This is the exact scenario from the user:
// codex_7d_used_percent: 100
// codex_7d_reset_after_seconds: 384607 (约4.5天后重置)
// codex_5h_used_percent: 97 (from upstream 3% remaining)
// codex_5h_used_percent: 3
// codex_5h_reset_after_seconds: 17369 (约4.8小时后重置)
svc := &RateLimitService{}