Merge pull request #441 from rexname/fix/issue-440-direct-api-fallback

fix(codex): block weekly-exhausted accounts in direct API fallback
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-03-18 08:40:19 -03:00
committed by GitHub
3 changed files with 110 additions and 6 deletions

View File

@@ -101,6 +101,45 @@ function clampPercent(value: number): number {
return Math.max(0, Math.min(100, value));
}
function normalizeWindowKey(value: unknown): string {
if (typeof value !== "string") return "";
return value
.toLowerCase()
.replace(/[^a-z0-9]+/g, " ")
.trim();
}
function resolveQuotaWindow(
quotas: Record<string, QuotaInfo>,
windowName: string
): QuotaInfo | null {
const direct = quotas[windowName];
if (direct) return direct;
const normalizedTarget = normalizeWindowKey(windowName);
if (!normalizedTarget) return null;
const prefixMatches: Array<{ key: string; quota: QuotaInfo }> = [];
for (const [key, quota] of Object.entries(quotas)) {
const normalizedKey = normalizeWindowKey(key);
if (!normalizedKey) continue;
if (normalizedKey === normalizedTarget) return quota;
// Support canonical selection of generic windows from labeled windows,
// e.g. "weekly" from "weekly (7d)" or "session" from "session (5h)".
if (normalizedKey.startsWith(`${normalizedTarget} `)) {
prefixMatches.push({ key, quota });
}
}
// Deterministic fallback: choose the lexicographically first matching key.
if (prefixMatches.length > 0) {
prefixMatches.sort((a, b) => a.key.localeCompare(b.key));
return prefixMatches[0].quota;
}
return null;
}
function earliestResetAt(quotas: Record<string, QuotaInfo>): string | null {
let earliest: string | null = null;
let earliestMs = Infinity;
@@ -201,7 +240,7 @@ export function getQuotaWindowStatus(
const now = Date.now();
const window = entry.quotas[windowName];
const window = resolveQuotaWindow(entry.quotas, windowName);
if (!window) return null;
const remainingPercentage = clampPercent(window.remainingPercentage);

View File

@@ -132,12 +132,38 @@ function normalizeWindowName(windowName: unknown): string | null {
return normalized.length > 0 ? normalized : null;
}
function getLegacyCodexWindows(providerSpecificData: JsonRecord): string[] {
function uniqueWindows(windows: string[]): string[] {
return [...new Set(windows)];
}
function normalizeCodexWindowName(windowName: unknown): string | null {
if (typeof windowName !== "string") return null;
const normalized = windowName.trim().toLowerCase();
if (normalized === "session (5h)" || normalized === "5h" || normalized === "five_hour") {
return "session";
}
if (normalized === "weekly (7d)" || normalized === "7d" || normalized === "seven_day") {
return "weekly";
}
return normalized;
}
function applyCodexWindowPolicy(rawWindows: string[], providerSpecificData: JsonRecord): string[] {
const codexPolicy = getCodexLimitPolicy(providerSpecificData);
const windows: string[] = [];
const normalizedRaw = rawWindows.map(normalizeCodexWindowName).filter(Boolean) as string[];
// Preserve explicitly configured custom windows, but enforce canonical Codex windows
// from toggles so weekly exhaustion is never skipped when useWeekly=true.
let windows = [...normalizedRaw];
windows = windows.filter((windowName) => {
if (windowName === "session") return codexPolicy.use5h;
if (windowName === "weekly") return codexPolicy.useWeekly;
return true;
});
if (codexPolicy.use5h) windows.push("session");
if (codexPolicy.useWeekly) windows.push("weekly");
return windows;
return uniqueWindows(windows);
}
export function resolveQuotaLimitPolicy(
@@ -149,8 +175,7 @@ export function resolveQuotaLimitPolicy(
const windows = rawWindows.map(normalizeWindowName).filter(Boolean) as string[];
if (provider === "codex") {
const fallbackWindows = getLegacyCodexWindows(providerSpecificData);
const defaultWindows = windows.length > 0 ? windows : fallbackWindows;
const defaultWindows = applyCodexWindowPolicy(windows, providerSpecificData);
const enabled = toBooleanOrDefault(rawPolicy.enabled, defaultWindows.length > 0);
return {

View File

@@ -21,6 +21,26 @@ test("resolveQuotaLimitPolicy keeps codex legacy defaults when generic policy is
assert.equal(policy.thresholdPercent, 90);
});
test("resolveQuotaLimitPolicy enforces codex weekly window when weekly toggle is enabled", () => {
const policy = auth.resolveQuotaLimitPolicy("codex", {
codexLimitPolicy: { use5h: true, useWeekly: true },
limitPolicy: { enabled: true, windows: ["session"] },
});
assert.equal(policy.enabled, true);
assert.deepEqual(policy.windows.sort(), ["session", "weekly"]);
});
test("resolveQuotaLimitPolicy removes codex weekly window when weekly toggle is disabled", () => {
const policy = auth.resolveQuotaLimitPolicy("codex", {
codexLimitPolicy: { use5h: true, useWeekly: false },
limitPolicy: { enabled: true, windows: ["session", "weekly"] },
});
assert.equal(policy.enabled, true);
assert.deepEqual(policy.windows, ["session"]);
});
test("resolveQuotaLimitPolicy disables non-codex policy by default", () => {
const policy = auth.resolveQuotaLimitPolicy("openai", {});
assert.equal(policy.enabled, false);
@@ -60,6 +80,26 @@ test("evaluateQuotaLimitPolicy blocks when configured window reaches threshold",
assert.equal(result.resetAt, resetAt);
});
test("evaluateQuotaLimitPolicy matches canonical weekly window against labeled cache keys", () => {
const resetAt = new Date(Date.now() + 60_000).toISOString();
quotaCache.setQuotaCache("conn-policy-weekly-label", "codex", {
"weekly (7d)": { remainingPercentage: 0, resetAt },
});
const result = auth.evaluateQuotaLimitPolicy(
"codex",
buildConnection("conn-policy-weekly-label", {
codexLimitPolicy: { use5h: true, useWeekly: true },
limitPolicy: { enabled: true, windows: ["weekly"] },
})
);
assert.equal(result.blocked, true);
assert.equal(result.reasons.length, 1);
assert.match(result.reasons[0], /weekly usage/i);
assert.equal(result.resetAt, resetAt);
});
test("evaluateQuotaLimitPolicy does not block when no quota data exists", () => {
const result = auth.evaluateQuotaLimitPolicy(
"openai",