fix(usage): 修正 OpenAI 5h 用量百分比语义
This commit is contained in:
parent
f18451e56f
commit
b65dde634b
@ -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, 42.0, repo.updatedExtra["codex_5h_used_percent"])
|
||||
require.Equal(t, 58.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", "100")
|
||||
resp.Header.Set("x-codex-secondary-used-percent", "0")
|
||||
resp.Header.Set("x-codex-secondary-reset-after-seconds", "18000")
|
||||
resp.Header.Set("x-codex-secondary-window-minutes", "300")
|
||||
|
||||
|
||||
@ -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", "100")
|
||||
headers.Set("x-codex-secondary-used-percent", "0")
|
||||
headers.Set("x-codex-secondary-reset-after-seconds", "18000")
|
||||
headers.Set("x-codex-secondary-window-minutes", "300")
|
||||
|
||||
@ -92,6 +92,33 @@ 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()
|
||||
|
||||
|
||||
@ -126,6 +126,19 @@ 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.
|
||||
@ -184,7 +197,7 @@ func (s *OpenAICodexUsageSnapshot) Normalize() *NormalizedCodexLimits {
|
||||
|
||||
// Assign values
|
||||
if use5hFromPrimary {
|
||||
result.Used5hPercent = s.PrimaryUsedPercent
|
||||
result.Used5hPercent = normalizeCodexFiveHourUsedPercent(s.PrimaryUsedPercent)
|
||||
result.Reset5hSeconds = s.PrimaryResetAfterSeconds
|
||||
result.Window5hMinutes = s.PrimaryWindowMinutes
|
||||
result.Used7dPercent = s.SecondaryUsedPercent
|
||||
@ -194,7 +207,7 @@ func (s *OpenAICodexUsageSnapshot) Normalize() *NormalizedCodexLimits {
|
||||
result.Used7dPercent = s.PrimaryUsedPercent
|
||||
result.Reset7dSeconds = s.PrimaryResetAfterSeconds
|
||||
result.Window7dMinutes = s.PrimaryWindowMinutes
|
||||
result.Used5hPercent = s.SecondaryUsedPercent
|
||||
result.Used5hPercent = normalizeCodexFiveHourUsedPercent(s.SecondaryUsedPercent)
|
||||
result.Reset5hSeconds = s.SecondaryResetAfterSeconds
|
||||
result.Window5hMinutes = s.SecondaryWindowMinutes
|
||||
}
|
||||
|
||||
@ -104,6 +104,40 @@ 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
|
||||
|
||||
@ -1774,7 +1774,7 @@ func TestOpenAIUpdateCodexUsageSnapshotFromHeaders(t *testing.T) {
|
||||
|
||||
select {
|
||||
case updates := <-repo.updateExtraCalls:
|
||||
require.Equal(t, 12.0, updates["codex_5h_used_percent"])
|
||||
require.Equal(t, 88.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"])
|
||||
|
||||
@ -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", "100")
|
||||
headers.Set("x-codex-secondary-used-percent", "0")
|
||||
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", "100") // This is 5h
|
||||
headers.Set("x-codex-primary-used-percent", "0") // This is 5h remaining%
|
||||
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", "100")
|
||||
headers.Set("x-codex-secondary-used-percent", "0")
|
||||
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
|
||||
sUsed := 3.0
|
||||
sRemaining := 3.0
|
||||
sReset := 17369
|
||||
sWindow := 300
|
||||
|
||||
@ -232,7 +232,7 @@ func TestNormalizedCodexLimits(t *testing.T) {
|
||||
PrimaryUsedPercent: &pUsed,
|
||||
PrimaryResetAfterSeconds: &pReset,
|
||||
PrimaryWindowMinutes: &pWindow,
|
||||
SecondaryUsedPercent: &sUsed,
|
||||
SecondaryUsedPercent: &sRemaining,
|
||||
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 != 3.0 {
|
||||
t.Errorf("expected Used5hPercent=3, got %v", normalized.Used5hPercent)
|
||||
if normalized.Used5hPercent == nil || *normalized.Used5hPercent != 97.0 {
|
||||
t.Errorf("expected Used5hPercent=97, 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
|
||||
sUsed := 60.0
|
||||
sRemaining := 60.0
|
||||
sReset := 3000
|
||||
|
||||
snapshot := &OpenAICodexUsageSnapshot{
|
||||
SecondaryUsedPercent: &sUsed,
|
||||
SecondaryUsedPercent: &sRemaining,
|
||||
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 != 60.0 {
|
||||
t.Errorf("expected Used5hPercent=60, got %v", normalized.Used5hPercent)
|
||||
if normalized.Used5hPercent == nil || *normalized.Used5hPercent != 40.0 {
|
||||
t.Errorf("expected Used5hPercent=40, 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
|
||||
sUsed := 50.0
|
||||
sRemaining := 30.0
|
||||
sReset := 10000
|
||||
|
||||
snapshot := &OpenAICodexUsageSnapshot{
|
||||
PrimaryUsedPercent: &pUsed,
|
||||
PrimaryResetAfterSeconds: &pReset,
|
||||
SecondaryUsedPercent: &sUsed,
|
||||
SecondaryUsedPercent: &sRemaining,
|
||||
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 != 50.0 {
|
||||
t.Errorf("expected Used5hPercent=50, got %v", normalized.Used5hPercent)
|
||||
if normalized.Used5hPercent == nil || *normalized.Used5hPercent != 70.0 {
|
||||
t.Errorf("expected Used5hPercent=70, 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: 3
|
||||
// codex_5h_used_percent: 97 (from upstream 3% remaining)
|
||||
// codex_5h_reset_after_seconds: 17369 (约4.8小时后重置)
|
||||
|
||||
svc := &RateLimitService{}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user