diff --git a/backend/internal/service/openai_gateway_service.go b/backend/internal/service/openai_gateway_service.go index 98399e51..ffd61a92 100644 --- a/backend/internal/service/openai_gateway_service.go +++ b/backend/internal/service/openai_gateway_service.go @@ -827,6 +827,112 @@ func (s *OpenAIGatewayService) updateCodexUsageSnapshot(ctx context.Context, acc } updates["codex_usage_updated_at"] = snapshot.UpdatedAt + // Normalize to canonical 5h/7d fields based on window_minutes + // This fixes the issue where OpenAI's primary/secondary naming is reversed + // Strategy: Compare the two windows and assign the smaller one to 5h, larger one to 7d + + // IMPORTANT: We can only reliably determine window type from window_minutes field + // The reset_after_seconds is remaining time, not window size, so it cannot be used for comparison + + var primaryWindowMins, secondaryWindowMins int + var hasPrimaryWindow, hasSecondaryWindow bool + + // Only use window_minutes for reliable window size comparison + if snapshot.PrimaryWindowMinutes != nil { + primaryWindowMins = *snapshot.PrimaryWindowMinutes + hasPrimaryWindow = true + } + + if snapshot.SecondaryWindowMinutes != nil { + secondaryWindowMins = *snapshot.SecondaryWindowMinutes + hasSecondaryWindow = true + } + + // Determine which is 5h and which is 7d + var use5hFromPrimary, use7dFromPrimary bool + var use5hFromSecondary, use7dFromSecondary bool + + if hasPrimaryWindow && hasSecondaryWindow { + // Both window sizes known: compare and assign smaller to 5h, larger to 7d + if primaryWindowMins < secondaryWindowMins { + use5hFromPrimary = true + use7dFromSecondary = true + } else { + use5hFromSecondary = true + use7dFromPrimary = true + } + } else if hasPrimaryWindow { + // Only primary window size known: classify by absolute threshold + if primaryWindowMins <= 360 { + use5hFromPrimary = true + } else { + use7dFromPrimary = true + } + } else if hasSecondaryWindow { + // Only secondary window size known: classify by absolute threshold + if secondaryWindowMins <= 360 { + use5hFromSecondary = true + } else { + use7dFromSecondary = true + } + } else { + // No window_minutes available: cannot reliably determine window types + // Fall back to legacy assumption (may be incorrect) + // Assume primary=7d, secondary=5h based on historical observation + if snapshot.SecondaryUsedPercent != nil || snapshot.SecondaryResetAfterSeconds != nil || snapshot.SecondaryWindowMinutes != nil { + use5hFromSecondary = true + } + if snapshot.PrimaryUsedPercent != nil || snapshot.PrimaryResetAfterSeconds != nil || snapshot.PrimaryWindowMinutes != nil { + use7dFromPrimary = true + } + } + + // Write canonical 5h fields + if use5hFromPrimary { + if snapshot.PrimaryUsedPercent != nil { + updates["codex_5h_used_percent"] = *snapshot.PrimaryUsedPercent + } + if snapshot.PrimaryResetAfterSeconds != nil { + updates["codex_5h_reset_after_seconds"] = *snapshot.PrimaryResetAfterSeconds + } + if snapshot.PrimaryWindowMinutes != nil { + updates["codex_5h_window_minutes"] = *snapshot.PrimaryWindowMinutes + } + } else if use5hFromSecondary { + if snapshot.SecondaryUsedPercent != nil { + updates["codex_5h_used_percent"] = *snapshot.SecondaryUsedPercent + } + if snapshot.SecondaryResetAfterSeconds != nil { + updates["codex_5h_reset_after_seconds"] = *snapshot.SecondaryResetAfterSeconds + } + if snapshot.SecondaryWindowMinutes != nil { + updates["codex_5h_window_minutes"] = *snapshot.SecondaryWindowMinutes + } + } + + // Write canonical 7d fields + if use7dFromPrimary { + if snapshot.PrimaryUsedPercent != nil { + updates["codex_7d_used_percent"] = *snapshot.PrimaryUsedPercent + } + if snapshot.PrimaryResetAfterSeconds != nil { + updates["codex_7d_reset_after_seconds"] = *snapshot.PrimaryResetAfterSeconds + } + if snapshot.PrimaryWindowMinutes != nil { + updates["codex_7d_window_minutes"] = *snapshot.PrimaryWindowMinutes + } + } else if use7dFromSecondary { + if snapshot.SecondaryUsedPercent != nil { + updates["codex_7d_used_percent"] = *snapshot.SecondaryUsedPercent + } + if snapshot.SecondaryResetAfterSeconds != nil { + updates["codex_7d_reset_after_seconds"] = *snapshot.SecondaryResetAfterSeconds + } + if snapshot.SecondaryWindowMinutes != nil { + updates["codex_7d_window_minutes"] = *snapshot.SecondaryWindowMinutes + } + } + // Update account's Extra field asynchronously go func() { updateCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) diff --git a/frontend/src/components/account/AccountUsageCell.vue b/frontend/src/components/account/AccountUsageCell.vue index 6b91d29f..57e21fbf 100644 --- a/frontend/src/components/account/AccountUsageCell.vue +++ b/frontend/src/components/account/AccountUsageCell.vue @@ -69,21 +69,21 @@