Skip to content

Commit 78c9dd8

Browse files
committed
fix(openai): preserve Codex 5h used percent semantics
1 parent aa69e39 commit 78c9dd8

6 files changed

Lines changed: 35 additions & 31 deletions

backend/internal/service/account_test_service_openai_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ func TestAccountTestService_OpenAISuccessPersistsSnapshotFromHeaders(t *testing.
132132
require.Len(t, upstream.requests, 1)
133133
require.Equal(t, HTTPUpstreamProfileOpenAI, HTTPUpstreamProfileFromContext(upstream.requests[0].Context()))
134134
require.NotEmpty(t, repo.updatedExtra)
135-
require.Equal(t, 58.0, repo.updatedExtra["codex_5h_used_percent"])
135+
require.Equal(t, 42.0, repo.updatedExtra["codex_5h_used_percent"])
136136
require.Equal(t, 88.0, repo.updatedExtra["codex_7d_used_percent"])
137137
require.Contains(t, recorder.Body.String(), "test_complete")
138138
}
@@ -170,7 +170,7 @@ func TestAccountTestService_OpenAI429PersistsSnapshotAndRateLimitState(t *testin
170170
resp.Header.Set("x-codex-primary-used-percent", "100")
171171
resp.Header.Set("x-codex-primary-reset-after-seconds", "604800")
172172
resp.Header.Set("x-codex-primary-window-minutes", "10080")
173-
resp.Header.Set("x-codex-secondary-used-percent", "0")
173+
resp.Header.Set("x-codex-secondary-used-percent", "100")
174174
resp.Header.Set("x-codex-secondary-reset-after-seconds", "18000")
175175
resp.Header.Set("x-codex-secondary-window-minutes", "300")
176176

backend/internal/service/account_usage_service_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func TestExtractOpenAICodexProbeUpdatesAccepts429WithCodexHeaders(t *testing.T)
7373
headers.Set("x-codex-primary-used-percent", "100")
7474
headers.Set("x-codex-primary-reset-after-seconds", "604800")
7575
headers.Set("x-codex-primary-window-minutes", "10080")
76-
headers.Set("x-codex-secondary-used-percent", "0")
76+
headers.Set("x-codex-secondary-used-percent", "100")
7777
headers.Set("x-codex-secondary-reset-after-seconds", "18000")
7878
headers.Set("x-codex-secondary-window-minutes", "300")
7979

@@ -96,7 +96,7 @@ func TestBuildCodexUsageProgressFromExtra_UsesCanonicalUsedPercent(t *testing.T)
9696
t.Parallel()
9797
now := time.Date(2026, 5, 30, 7, 4, 9, 0, time.UTC)
9898
extra := map[string]any{
99-
"codex_5h_used_percent": 94.0,
99+
"codex_5h_used_percent": 6.0,
100100
"codex_5h_reset_at": now.Add(2 * time.Hour).Format(time.RFC3339),
101101
"codex_7d_used_percent": 93.0,
102102
"codex_7d_reset_at": now.Add(5 * 24 * time.Hour).Format(time.RFC3339),
@@ -106,8 +106,8 @@ func TestBuildCodexUsageProgressFromExtra_UsesCanonicalUsedPercent(t *testing.T)
106106
if fiveHour == nil {
107107
t.Fatal("expected non-nil 5h progress")
108108
}
109-
if fiveHour.Utilization != 94.0 {
110-
t.Fatalf("5h Utilization = %v, want 94", fiveHour.Utilization)
109+
if fiveHour.Utilization != 6.0 {
110+
t.Fatalf("5h Utilization = %v, want 6", fiveHour.Utilization)
111111
}
112112

113113
sevenDay := buildCodexUsageProgressFromExtra(extra, "7d", now)

backend/internal/service/openai_gateway_service.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,16 @@ func normalizeCodexFiveHourUsedPercent(raw *float64) *float64 {
130130
if raw == nil {
131131
return nil
132132
}
133-
// OpenAI's 5h Codex quota header is remaining%, despite the upstream header
134-
// name saying "used"; the canonical codex_5h_used_percent field stores used%.
135-
used := 100 - *raw
133+
// OpenAI's Codex 5h quota header is already used%, despite the header name
134+
// historically being treated as remaining% in this codebase. Keep the canonical
135+
// codex_5h_used_percent field aligned with the upstream used percentage.
136+
used := *raw
136137
if used < 0 {
137138
used = 0
138139
}
140+
if used > 100 {
141+
used = 100
142+
}
139143
return &used
140144
}
141145

backend/internal/service/openai_gateway_service_codex_snapshot_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,19 +104,19 @@ func TestBuildCodexUsageExtraUpdates_UsesSnapshotUpdatedAt(t *testing.T) {
104104
}
105105
}
106106

107-
func TestBuildCodexUsageExtraUpdates_NormalizesFiveHourRemainingToUsedPercent(t *testing.T) {
107+
func TestBuildCodexUsageExtraUpdates_NormalizesFiveHourUsedPercent(t *testing.T) {
108108
primaryUsed := 93.0
109109
primaryReset := 86400
110110
primaryWindow := 10080
111-
secondaryRemaining := 6.0
111+
secondaryUsed := 6.0
112112
secondaryReset := 3600
113113
secondaryWindow := 300
114114

115115
snapshot := &OpenAICodexUsageSnapshot{
116116
PrimaryUsedPercent: &primaryUsed,
117117
PrimaryResetAfterSeconds: &primaryReset,
118118
PrimaryWindowMinutes: &primaryWindow,
119-
SecondaryUsedPercent: &secondaryRemaining,
119+
SecondaryUsedPercent: &secondaryUsed,
120120
SecondaryResetAfterSeconds: &secondaryReset,
121121
SecondaryWindowMinutes: &secondaryWindow,
122122
UpdatedAt: "2026-05-30T07:04:09Z",
@@ -130,8 +130,8 @@ func TestBuildCodexUsageExtraUpdates_NormalizesFiveHourRemainingToUsedPercent(t
130130
if got := updates["codex_secondary_used_percent"]; got != 6.0 {
131131
t.Fatalf("codex_secondary_used_percent = %v, want raw upstream value 6", got)
132132
}
133-
if got := updates["codex_5h_used_percent"]; got != 94.0 {
134-
t.Fatalf("codex_5h_used_percent = %v, want 94", got)
133+
if got := updates["codex_5h_used_percent"]; got != 6.0 {
134+
t.Fatalf("codex_5h_used_percent = %v, want 6", got)
135135
}
136136
if got := updates["codex_7d_used_percent"]; got != 93.0 {
137137
t.Fatalf("codex_7d_used_percent = %v, want 93", got)

backend/internal/service/openai_gateway_service_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1774,7 +1774,7 @@ func TestOpenAIUpdateCodexUsageSnapshotFromHeaders(t *testing.T) {
17741774

17751775
select {
17761776
case updates := <-repo.updateExtraCalls:
1777-
require.Equal(t, 88.0, updates["codex_5h_used_percent"])
1777+
require.Equal(t, 12.0, updates["codex_5h_used_percent"])
17781778
require.Equal(t, 34.0, updates["codex_7d_used_percent"])
17791779
require.Equal(t, 600, updates["codex_5h_reset_after_seconds"])
17801780
require.Equal(t, 86400, updates["codex_7d_reset_after_seconds"])

backend/internal/service/ratelimit_service_openai_test.go

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func TestCalculateOpenAI429ResetTime_5hExhausted(t *testing.T) {
5151
headers.Set("x-codex-primary-used-percent", "50")
5252
headers.Set("x-codex-primary-reset-after-seconds", "500000")
5353
headers.Set("x-codex-primary-window-minutes", "10080") // 7 days
54-
headers.Set("x-codex-secondary-used-percent", "0")
54+
headers.Set("x-codex-secondary-used-percent", "100")
5555
headers.Set("x-codex-secondary-reset-after-seconds", "3600") // 1 hour
5656
headers.Set("x-codex-secondary-window-minutes", "300") // 5 hours
5757

@@ -122,7 +122,7 @@ func TestCalculateOpenAI429ResetTime_ReversedWindowOrder(t *testing.T) {
122122

123123
// Test when OpenAI sends primary as 5h and secondary as 7d (reversed)
124124
headers := http.Header{}
125-
headers.Set("x-codex-primary-used-percent", "0") // This is 5h remaining%
125+
headers.Set("x-codex-primary-used-percent", "100") // This is 5h used%
126126
headers.Set("x-codex-primary-reset-after-seconds", "3600") // 1 hour
127127
headers.Set("x-codex-primary-window-minutes", "300") // 5 hours - smaller!
128128
headers.Set("x-codex-secondary-used-percent", "50")
@@ -180,7 +180,7 @@ func TestHandle429_OpenAIPersistsCodexSnapshotImmediately(t *testing.T) {
180180
headers.Set("x-codex-primary-used-percent", "100")
181181
headers.Set("x-codex-primary-reset-after-seconds", "604800")
182182
headers.Set("x-codex-primary-window-minutes", "10080")
183-
headers.Set("x-codex-secondary-used-percent", "0")
183+
headers.Set("x-codex-secondary-used-percent", "100")
184184
headers.Set("x-codex-secondary-reset-after-seconds", "18000")
185185
headers.Set("x-codex-secondary-window-minutes", "300")
186186

@@ -224,15 +224,15 @@ func TestNormalizedCodexLimits(t *testing.T) {
224224
pUsed := 100.0
225225
pReset := 384607
226226
pWindow := 10080
227-
sRemaining := 3.0
227+
sUsed := 3.0
228228
sReset := 17369
229229
sWindow := 300
230230

231231
snapshot := &OpenAICodexUsageSnapshot{
232232
PrimaryUsedPercent: &pUsed,
233233
PrimaryResetAfterSeconds: &pReset,
234234
PrimaryWindowMinutes: &pWindow,
235-
SecondaryUsedPercent: &sRemaining,
235+
SecondaryUsedPercent: &sUsed,
236236
SecondaryResetAfterSeconds: &sReset,
237237
SecondaryWindowMinutes: &sWindow,
238238
}
@@ -249,8 +249,8 @@ func TestNormalizedCodexLimits(t *testing.T) {
249249
if normalized.Reset7dSeconds == nil || *normalized.Reset7dSeconds != 384607 {
250250
t.Errorf("expected Reset7dSeconds=384607, got %v", normalized.Reset7dSeconds)
251251
}
252-
if normalized.Used5hPercent == nil || *normalized.Used5hPercent != 97.0 {
253-
t.Errorf("expected Used5hPercent=97, got %v", normalized.Used5hPercent)
252+
if normalized.Used5hPercent == nil || *normalized.Used5hPercent != 3.0 {
253+
t.Errorf("expected Used5hPercent=3, got %v", normalized.Used5hPercent)
254254
}
255255
if normalized.Reset5hSeconds == nil || *normalized.Reset5hSeconds != 17369 {
256256
t.Errorf("expected Reset5hSeconds=17369, got %v", normalized.Reset5hSeconds)
@@ -338,11 +338,11 @@ func TestRateLimitService_HandleUpstreamError_403FallsBackToRawBody(t *testing.T
338338

339339
func TestNormalizedCodexLimits_OnlySecondaryData(t *testing.T) {
340340
// Test when only secondary has data, no window_minutes
341-
sRemaining := 60.0
341+
sUsed := 60.0
342342
sReset := 3000
343343

344344
snapshot := &OpenAICodexUsageSnapshot{
345-
SecondaryUsedPercent: &sRemaining,
345+
SecondaryUsedPercent: &sUsed,
346346
SecondaryResetAfterSeconds: &sReset,
347347
// No window_minutes, no primary data
348348
}
@@ -354,8 +354,8 @@ func TestNormalizedCodexLimits_OnlySecondaryData(t *testing.T) {
354354

355355
// Legacy assumption: primary=7d, secondary=5h
356356
// So secondary goes to 5h
357-
if normalized.Used5hPercent == nil || *normalized.Used5hPercent != 40.0 {
358-
t.Errorf("expected Used5hPercent=40, got %v", normalized.Used5hPercent)
357+
if normalized.Used5hPercent == nil || *normalized.Used5hPercent != 60.0 {
358+
t.Errorf("expected Used5hPercent=60, got %v", normalized.Used5hPercent)
359359
}
360360
if normalized.Reset5hSeconds == nil || *normalized.Reset5hSeconds != 3000 {
361361
t.Errorf("expected Reset5hSeconds=3000, got %v", normalized.Reset5hSeconds)
@@ -370,13 +370,13 @@ func TestNormalizedCodexLimits_BothDataNoWindowMinutes(t *testing.T) {
370370
// Test when both have data but no window_minutes
371371
pUsed := 100.0
372372
pReset := 400000
373-
sRemaining := 30.0
373+
sUsed := 30.0
374374
sReset := 10000
375375

376376
snapshot := &OpenAICodexUsageSnapshot{
377377
PrimaryUsedPercent: &pUsed,
378378
PrimaryResetAfterSeconds: &pReset,
379-
SecondaryUsedPercent: &sRemaining,
379+
SecondaryUsedPercent: &sUsed,
380380
SecondaryResetAfterSeconds: &sReset,
381381
// No window_minutes
382382
}
@@ -393,8 +393,8 @@ func TestNormalizedCodexLimits_BothDataNoWindowMinutes(t *testing.T) {
393393
if normalized.Reset7dSeconds == nil || *normalized.Reset7dSeconds != 400000 {
394394
t.Errorf("expected Reset7dSeconds=400000, got %v", normalized.Reset7dSeconds)
395395
}
396-
if normalized.Used5hPercent == nil || *normalized.Used5hPercent != 70.0 {
397-
t.Errorf("expected Used5hPercent=70, got %v", normalized.Used5hPercent)
396+
if normalized.Used5hPercent == nil || *normalized.Used5hPercent != 30.0 {
397+
t.Errorf("expected Used5hPercent=30, got %v", normalized.Used5hPercent)
398398
}
399399
if normalized.Reset5hSeconds == nil || *normalized.Reset5hSeconds != 10000 {
400400
t.Errorf("expected Reset5hSeconds=10000, got %v", normalized.Reset5hSeconds)
@@ -425,7 +425,7 @@ func TestCalculateOpenAI429ResetTime_UserProvidedScenario(t *testing.T) {
425425
// This is the exact scenario from the user:
426426
// codex_7d_used_percent: 100
427427
// codex_7d_reset_after_seconds: 384607 (约4.5天后重置)
428-
// codex_5h_used_percent: 97 (from upstream 3% remaining)
428+
// codex_5h_used_percent: 3 (from upstream 3% used)
429429
// codex_5h_reset_after_seconds: 17369 (约4.8小时后重置)
430430

431431
svc := &RateLimitService{}

0 commit comments

Comments
 (0)