diff --git a/docs-site/src/content/docs/guides/web-dashboard.md b/docs-site/src/content/docs/guides/web-dashboard.md index 6a57e78fd38..cff84de6197 100644 --- a/docs-site/src/content/docs/guides/web-dashboard.md +++ b/docs-site/src/content/docs/guides/web-dashboard.md @@ -72,6 +72,9 @@ The **Codex Auth** page manages the native ChatGPT/Codex route: and a strictly lower-usage eligible account exists. - New sessions can choose the lowest-usage eligible account. Paid plans score the hottest known 5h, weekly, or 30d window; Go/Free plans use the 30d window only. +- When WHAM supplies `limit_window_seconds`, Codex Auth classifies a primary window of at least 28 + days as 30d instead of assuming every primary window is weekly. Responses without a duration keep + the legacy weekly interpretation. - **Refresh quotas** re-reads account usage immediately so routing and the account cards use the same values. - Pool request logs use opaque labels such as `p3fa91c`, never account emails. diff --git a/docs-site/src/content/docs/ko/guides/web-dashboard.md b/docs-site/src/content/docs/ko/guides/web-dashboard.md index 1f8a371fd80..a4510bda6e4 100644 --- a/docs-site/src/content/docs/ko/guides/web-dashboard.md +++ b/docs-site/src/content/docs/ko/guides/web-dashboard.md @@ -71,6 +71,8 @@ Dashboard의 **Sub-agent delegation** 선택기는 `injectionModel`과 선택적 정상 계정이 있으면 그 계정으로 다시 묶일 수 있습니다. - 새 세션은 사용량이 가장 낮은 정상 계정을 고를 수 있습니다. 유료 플랜은 알려진 5시간, 주간, 30일 창 중 가장 높은 사용률로 점수를 매기고, Go/Free 플랜은 30일 창만 사용합니다. +- WHAM이 `limit_window_seconds`를 제공하면 Codex Auth는 28일 이상인 primary window를 주간이 아닌 + 30일 창으로 분류합니다. 기간이 없는 기존 응답은 이전과 동일하게 주간 창으로 해석합니다. - **Refresh quotas**는 계정 사용량을 즉시 다시 읽어 라우팅과 화면의 계정 카드가 같은 값을 보게 합니다. - 풀 요청 로그에는 이메일 대신 `p3fa91c` 같은 불투명한 라벨을 사용합니다. diff --git a/src/codex/quota.ts b/src/codex/quota.ts index ee0bedd3f27..6d402797bab 100644 --- a/src/codex/quota.ts +++ b/src/codex/quota.ts @@ -11,15 +11,23 @@ export type WhamUsageResponse = { email?: string | null; plan_type?: string | null; rate_limit?: { - primary_window?: { used_percent?: number; reset_at?: number }; - secondary_window?: { used_percent?: number; reset_at?: number }; - tertiary_window?: { used_percent?: number; reset_at?: number }; + primary_window?: WhamUsageWindow; + secondary_window?: WhamUsageWindow; + tertiary_window?: WhamUsageWindow; }; rate_limit_reset_credits?: { available_count: number; } | null; }; +type WhamUsageWindow = { + used_percent?: number; + reset_at?: number; + limit_window_seconds?: number; +}; + +const MONTHLY_WINDOW_MIN_SECONDS = 28 * 24 * 60 * 60; + const accountQuota = new Map(); export const CODEX_UNKNOWN_USAGE_SCORE = 100; @@ -49,6 +57,13 @@ function hasKnownQuotaValue(quota: Omit): boole .some(value => typeof value === "number" && Number.isFinite(value)); } +function isExplicitMonthlyWindow(window: WhamUsageWindow | undefined): boolean { + const seconds = window?.limit_window_seconds; + return typeof seconds === "number" + && Number.isFinite(seconds) + && seconds >= MONTHLY_WINDOW_MIN_SECONDS; +} + export function updateAccountQuota( accountId: string, weekly: unknown, @@ -110,16 +125,30 @@ export function parseUsageQuota(data: WhamUsageResponse): Omit = {}; const thirtyDayOnly = data.plan_type?.trim().toLowerCase() === "go" || data.plan_type?.trim().toLowerCase() === "free"; - // primary_window was the 5h window; it now carries weekly data for GPT plans. - // secondary_window is the legacy weekly source; prefer primary when present. - const primaryPercent = normalizeUsagePercent(data.rate_limit.primary_window?.used_percent); - const secondaryPercent = normalizeUsagePercent(data.rate_limit.secondary_window?.used_percent); - const weeklyPercent = primaryPercent ?? secondaryPercent; - const monthlyPercent = normalizeUsagePercent(data.rate_limit.tertiary_window?.used_percent); - const primaryResetAt = normalizeResetAt(data.rate_limit.primary_window?.reset_at); - const secondaryResetAt = normalizeResetAt(data.rate_limit.secondary_window?.reset_at); - const weeklyResetAt = primaryPercent !== undefined ? primaryResetAt : secondaryResetAt; - const monthlyResetAt = normalizeResetAt(data.rate_limit.tertiary_window?.reset_at); + const primaryWindow = data.rate_limit.primary_window; + const secondaryWindow = data.rate_limit.secondary_window; + const tertiaryWindow = data.rate_limit.tertiary_window; + const primaryPercent = normalizeUsagePercent(primaryWindow?.used_percent); + const secondaryPercent = normalizeUsagePercent(secondaryWindow?.used_percent); + const tertiaryPercent = normalizeUsagePercent(tertiaryWindow?.used_percent); + const primaryResetAt = normalizeResetAt(primaryWindow?.reset_at); + const secondaryResetAt = normalizeResetAt(secondaryWindow?.reset_at); + const tertiaryResetAt = normalizeResetAt(tertiaryWindow?.reset_at); + const primaryIsMonthly = isExplicitMonthlyWindow(primaryWindow); + + // [Decision Log] + // - 목적과 의도: distinguish weekly and roughly monthly WHAM primary windows without plan-name guesses. + // - 기존 구현 및 제약 조건: primary meant weekly, and older responses omit limit_window_seconds. + // - 검토한 주요 대안: exact-duration matching, plan-specific mapping, and a duration lower bound. + // - 선택한 방식: only an explicit primary duration of at least 28 days changes it to monthly. + // - 다른 대안 대신 이 방식을 선택한 이유: it accepts calendar-month variance and preserves legacy payloads. + // - 장점, 단점 및 영향: Team monthly quotas classify correctly; unknown durations remain weekly by design. + const weeklyPercent = primaryIsMonthly ? secondaryPercent : primaryPercent ?? secondaryPercent; + const weeklyResetAt = primaryIsMonthly + ? secondaryResetAt + : primaryPercent !== undefined ? primaryResetAt : secondaryResetAt; + const monthlyPercent = primaryIsMonthly ? primaryPercent ?? tertiaryPercent : tertiaryPercent; + const monthlyResetAt = primaryIsMonthly && primaryPercent !== undefined ? primaryResetAt : tertiaryResetAt; if (thirtyDayOnly) { if (monthlyPercent !== undefined) { quota.monthlyPercent = monthlyPercent; diff --git a/tests/codex-routing.test.ts b/tests/codex-routing.test.ts index a7994d5f598..c8deffbf675 100644 --- a/tests/codex-routing.test.ts +++ b/tests/codex-routing.test.ts @@ -571,6 +571,46 @@ describe("codex routing", () => { }); }); + test("WHAM primary window uses its explicit duration to distinguish weekly and monthly quotas", () => { + expect(parseUsageQuota({ + plan_type: "team", + rate_limit: { + primary_window: { used_percent: 20, reset_at: 2, limit_window_seconds: 604_800 }, + }, + })).toEqual({ weeklyPercent: 20, weeklyResetAt: 2 }); + + expect(parseUsageQuota({ + plan_type: "team", + rate_limit: { + primary_window: { used_percent: 39, reset_at: 3, limit_window_seconds: 2_628_000 }, + }, + })).toEqual({ monthlyPercent: 39, monthlyResetAt: 3 }); + }); + + test("WHAM monthly primary preserves a legacy secondary weekly window", () => { + expect(parseUsageQuota({ + plan_type: "team", + rate_limit: { + primary_window: { used_percent: 39, reset_at: 30, limit_window_seconds: 2_628_000 }, + secondary_window: { used_percent: 12, reset_at: 7, limit_window_seconds: 604_800 }, + }, + })).toEqual({ + weeklyPercent: 12, + weeklyResetAt: 7, + monthlyPercent: 39, + monthlyResetAt: 30, + }); + }); + + test("WHAM primary window without duration keeps the legacy weekly fallback", () => { + expect(parseUsageQuota({ + plan_type: "team", + rate_limit: { + primary_window: { used_percent: 6, reset_at: 7 }, + }, + })).toEqual({ weeklyPercent: 6, weeklyResetAt: 7 }); + }); + test("WHAM parser returns null when no valid quota window is present", () => { expect(parseUsageQuota({ rate_limit: {} })).toBeNull(); expect(parseUsageQuota({