Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs-site/src/content/docs/guides/web-dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/ko/guides/web-dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 같은 불투명한 라벨을 사용합니다.

Expand Down
55 changes: 42 additions & 13 deletions src/codex/quota.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, StoredAccountQuota>();

export const CODEX_UNKNOWN_USAGE_SCORE = 100;
Expand Down Expand Up @@ -49,6 +57,13 @@ function hasKnownQuotaValue(quota: Omit<StoredAccountQuota, "updatedAt">): 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,
Expand Down Expand Up @@ -110,16 +125,30 @@ export function parseUsageQuota(data: WhamUsageResponse): Omit<StoredAccountQuot

const quota: Omit<StoredAccountQuota, "updatedAt"> = {};
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;
Expand Down
40 changes: 40 additions & 0 deletions tests/codex-routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Loading