From 09dd9142f099a97ebdfe9103d2d5e12909e4bf16 Mon Sep 17 00:00:00 2001 From: shevar0 <3235212+shevar0@users.noreply.github.com> Date: Tue, 27 Oct 2026 11:42:59 +0100 Subject: [PATCH 1/8] fix(codex): normalize quota evidence without fabricating healthy windows --- .../0000-codex-quota-signal-normalization.md | 1 + open-sse/executors/codex/quota.ts | 69 ++-- open-sse/services/codexAccount/quota.ts | 5 +- open-sse/services/codexQuotaFetcher.ts | 25 +- open-sse/services/codexQuotaSignals.ts | 235 +++++++++++++ open-sse/services/codexUsageQuotas.ts | 93 ++---- open-sse/services/usage/codex.ts | 3 +- .../codex-quota-signal-normalization.test.ts | 308 ++++++++++++++++++ .../unit/codex-quota-wire-normalizer.test.ts | 203 ++++++++++++ 9 files changed, 830 insertions(+), 112 deletions(-) create mode 100644 changelog.d/fixes/0000-codex-quota-signal-normalization.md create mode 100644 open-sse/services/codexQuotaSignals.ts create mode 100644 tests/unit/codex-quota-signal-normalization.test.ts create mode 100644 tests/unit/codex-quota-wire-normalizer.test.ts diff --git a/changelog.d/fixes/0000-codex-quota-signal-normalization.md b/changelog.d/fixes/0000-codex-quota-signal-normalization.md new file mode 100644 index 000000000000..4361ff1a7c55 --- /dev/null +++ b/changelog.d/fixes/0000-codex-quota-signal-normalization.md @@ -0,0 +1 @@ +- **fix(codex):** Parse official primary/secondary quota headers, keep malformed usage unknown instead of inventing healthy zero-percent windows, validate reset units, and preserve independent Codex/Spark evidence and explicit quota denial. diff --git a/open-sse/executors/codex/quota.ts b/open-sse/executors/codex/quota.ts index 977f794ddee9..ec73e46d9db3 100644 --- a/open-sse/executors/codex/quota.ts +++ b/open-sse/executors/codex/quota.ts @@ -1,4 +1,9 @@ -// Codex quota-snapshot parsing + reset/cooldown scheduling (pure). Verbatim from codex.ts. +// Codex quota-snapshot parsing + reset/cooldown scheduling (pure). +import { + parseCodexFiniteNumber, + parseCodexLegacyReset, + parseCodexOfficialQuotaHeaders, +} from "../../services/codexQuotaSignals.ts"; /** * T03: Parsed quota snapshot from Codex response headers. @@ -22,27 +27,49 @@ export interface CodexQuotaSnapshot { * x-codex-5h-usage / x-codex-5h-limit / x-codex-5h-reset-at * x-codex-7d-usage / x-codex-7d-limit / x-codex-7d-reset-at */ -export function parseCodexQuotaHeaders(headers: Record): CodexQuotaSnapshot | null { - const usage5h = headers["x-codex-5h-usage"] ?? null; - const limit5h = headers["x-codex-5h-limit"] ?? null; - const resetAt5h = headers["x-codex-5h-reset-at"] ?? null; - const usage7d = headers["x-codex-7d-usage"] ?? null; - const limit7d = headers["x-codex-7d-limit"] ?? null; - const resetAt7d = headers["x-codex-7d-reset-at"] ?? null; - - // Return null if none of the quota headers are present (not a quota-aware response) - if (!usage5h && !limit5h && !resetAt5h && !usage7d && !limit7d && !resetAt7d) { - return null; - } - - return { - usage5h: usage5h ? parseFloat(usage5h) : 0, - limit5h: limit5h ? parseFloat(limit5h) : Infinity, - resetAt5h: resetAt5h ?? null, - usage7d: usage7d ? parseFloat(usage7d) : 0, - limit7d: limit7d ? parseFloat(limit7d) : Infinity, - resetAt7d: resetAt7d ?? null, +export function parseCodexQuotaHeaders( + headers: Record, + model?: string | null +): CodexQuotaSnapshot | null { + const lower = Object.fromEntries( + Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]) + ); + const quota: CodexQuotaSnapshot = { + usage5h: 0, + limit5h: Infinity, + resetAt5h: null, + usage7d: 0, + limit7d: Infinity, + resetAt7d: null, }; + const observed = new Set<"5h" | "7d">(); + for (const window of ["5h", "7d"] as const) { + const prefix = `x-codex-${window}-`; + const usage = parseCodexFiniteNumber(lower[`${prefix}usage`]); + const limit = parseCodexFiniteNumber(lower[`${prefix}limit`]); + if (usage === null || usage < 0 || limit === null || limit <= 0) continue; + quota[`usage${window}`] = usage; + quota[`limit${window}`] = limit; + quota[`resetAt${window}`] = parseCodexLegacyReset(lower[`${prefix}reset-at`]); + observed.add(window); + } + const official = parseCodexOfficialQuotaHeaders(lower, model); + for (const window of official?.windows ?? []) { + const seconds = window.durationProvided + ? window.windowSeconds + : window.id === "primary" + ? 18000 + : 604800; + const slot = seconds === 18000 ? "5h" : seconds === 604800 ? "7d" : null; + // This legacy DTO cannot represent arbitrary durations. Keep them in the + // normalized signal rather than inventing a 5h/7d identity for persistence. + if (!slot || observed.has(slot)) continue; + quota[`usage${slot}`] = window.usedPercent; + quota[`limit${slot}`] = 100; + quota[`resetAt${slot}`] = window.resetAt; + observed.add(slot); + } + return observed.size > 0 ? quota : null; } /** diff --git a/open-sse/services/codexAccount/quota.ts b/open-sse/services/codexAccount/quota.ts index 80b15c8f0fb5..cd75032a4b85 100644 --- a/open-sse/services/codexAccount/quota.ts +++ b/open-sse/services/codexAccount/quota.ts @@ -21,10 +21,9 @@ export async function persistCodexChildQuotaResponse(params: { fallbackRateLimitedUntil?: string | null; }): Promise { if (params.model.trim().length === 0) return null; - const quota = parseCodexQuotaHeaders(params.headers); - if (!quota) return null; - const scope = getCodexModelScope(params.model); + const quota = parseCodexQuotaHeaders(params.headers, params.model); + if (!quota) return null; const quotaState = { usage5h: quota.usage5h, limit5h: quota.limit5h, diff --git a/open-sse/services/codexQuotaFetcher.ts b/open-sse/services/codexQuotaFetcher.ts index 528bc8a17677..4b7c98d42583 100644 --- a/open-sse/services/codexQuotaFetcher.ts +++ b/open-sse/services/codexQuotaFetcher.ts @@ -33,6 +33,7 @@ import { throttleQuotaFetch } from "./quotaFetchThrottle.ts"; import { fetchWithConnectionProxy } from "./connectionProxyFetch.ts"; import { getCodexBackendIdentityHeaders } from "../config/codexClient.ts"; import { parseCodexPaidCredits } from "@/lib/providers/codexPaidCredits"; +import { isCodexLimitReached, parseCodexQuotaWindow } from "./codexQuotaSignals.ts"; /** * Stable identifiers for Codex's quota windows. These match the quota keys @@ -299,27 +300,11 @@ function toRecord(value: unknown): Record { : {}; } -function parseWindowReset(window: Record): string | null { - const resetAt = toNumber(window["reset_at"] ?? window["resetAt"], 0); - if (resetAt > 0) { - return new Date(resetAt * 1000).toISOString(); - } - const resetAfterSeconds = toNumber( - window["reset_after_seconds"] ?? window["resetAfterSeconds"], - 0 - ); - if (resetAfterSeconds > 0) { - return new Date(Date.now() + resetAfterSeconds * 1000).toISOString(); - } - return null; -} - function parseCodexWindow( window: Record | null | undefined ): { percentUsed: number; resetAt: string | null } | null { - if (!window || Object.keys(window).length === 0) return null; - const percentUsed = toNumber(window["used_percent"] ?? window["usedPercent"], 0) / 100; - return { percentUsed, resetAt: parseWindowReset(window) }; + const parsed = parseCodexQuotaWindow(window); + return parsed ? { percentUsed: parsed.percentUsed, resetAt: parsed.resetAt } : null; } /** @@ -431,9 +416,7 @@ function parseCodexUsageResponse( const window5h = parsedPrimary ?? { percentUsed: 0, resetAt: null }; const window7d = parsedSecondary ?? { percentUsed: 0, resetAt: null }; const worstPercentUsed = Math.max(window5h.percentUsed, window7d.percentUsed); - const limitReached = Boolean( - selectedRateLimit["limit_reached"] ?? selectedRateLimit["limitReached"] - ); + const limitReached = isCodexLimitReached(selectedRateLimit); const windows: Record = {}; assignCodexWindows(windows, selectedRateLimit, { diff --git a/open-sse/services/codexQuotaSignals.ts b/open-sse/services/codexQuotaSignals.ts new file mode 100644 index 000000000000..d93a23dd2f7a --- /dev/null +++ b/open-sse/services/codexQuotaSignals.ts @@ -0,0 +1,235 @@ +import { z } from "zod"; +import { + getCodexModelScope, + isCodexSparkLimitDescriptor, + type CodexQuotaScope, +} from "../config/codexQuotaScopes.ts"; + +type JsonRecord = Record; +export interface CodexQuotaWindow { + id: "primary" | "secondary"; + /** Fraction, converted exactly once from the wire's 0..100 percentage. */ + percentUsed: number; + /** Validated wire percentage, retained for 0..100 UI/legacy DTOs without float round trips. */ + usedPercent: number; + resetAt: string | null; + windowSeconds: number | null; + durationProvided: boolean; +} +export interface CodexQuotaSignal { + scope: CodexQuotaScope; + limitId: string; + source: "usage" | "headers" | "websocket"; + allowed: boolean | null; + limitReached: boolean | null; + windows: CodexQuotaWindow[]; +} + +// Defensive proxy bounds, not constraints claimed for Codex's signed Rust DTOs. +const MAX_WINDOW_SECONDS = 31 * 86400; +const decimal = /^[+-]?(?:\d{1,16}(?:\.\d{1,8})?|\.\d{1,8})(?:[eE][+-]?\d{1,3})?$/; +const finiteNumber = z.preprocess( + (value) => + typeof value === "string" && value.length <= 64 && decimal.test(value.trim()) + ? Number(value) + : value, + z.number().finite() +); +const percentage = finiteNumber.pipe(z.number().min(0).max(100)); +const duration = finiteNumber.pipe(z.number().int().positive().max(MAX_WINDOW_SECONDS)); + +function record(value: unknown): JsonRecord { + return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {}; +} +function read(value: JsonRecord, snake: string, camel: string): unknown { + return value[snake] ?? value[camel]; +} +export function parseCodexFiniteNumber(value: unknown): number | null { + const parsed = finiteNumber.safeParse(value); + return parsed.success ? parsed.data : null; +} +function resetFromMs(ms: number, now: number): string | null { + return Number.isFinite(ms) && ms > 0 && ms <= now + MAX_WINDOW_SECONDS * 1000 + ? new Date(ms).toISOString() + : null; +} +/** Official reset_at is epoch SECONDS; do not guess milliseconds from its magnitude. */ +export function parseCodexResetSeconds(value: unknown, now = Date.now()): string | null { + const seconds = parseCodexFiniteNumber(value); + return seconds === null ? null : resetFromMs(seconds * 1000, now); +} +/** The older token-count headers carry ISO timestamps, not official epoch seconds. */ +export function parseCodexLegacyReset(value: unknown, now = Date.now()): string | null { + if (typeof value !== "string" || value.length > 64) return null; + return resetFromMs(Date.parse(value), now); +} + +/** Invalid optional timing fields do not discard otherwise valid exhaustion evidence. */ +export function parseCodexQuotaWindow( + value: unknown, + id: CodexQuotaWindow["id"] = "primary", + now = Date.now(), + event = false +): CodexQuotaWindow | null { + const raw = record(value); + const parsed = percentage.safeParse(read(raw, "used_percent", "usedPercent")); + if (!parsed.success) return null; + const secondsValue = + read(raw, "limit_window_seconds", "limitWindowSeconds") ?? + read(raw, "window_seconds", "windowSeconds"); + const minutesValue = read(raw, "window_minutes", "windowMinutes"); + const rawDuration = event + ? (() => { + const minutes = parseCodexFiniteNumber(minutesValue); + return minutes === null ? null : minutes * 60; + })() + : secondsValue; + const parsedDuration = duration.safeParse(rawDuration); + const after = parseCodexFiniteNumber(read(raw, "reset_after_seconds", "resetAfterSeconds")); + const relativeReset = + after !== null && after >= 0 && after <= MAX_WINDOW_SECONDS + ? resetFromMs(now + after * 1000, now) + : null; + return { + id, + percentUsed: parsed.data / 100, + usedPercent: parsed.data, + resetAt: parseCodexResetSeconds(read(raw, "reset_at", "resetAt"), now) ?? relativeReset, + windowSeconds: parsedDuration.success ? parsedDuration.data : null, + durationProvided: event ? minutesValue != null : secondsValue != null, + }; +} + +function boolean(value: unknown): boolean | null { + const parsed = z.boolean().safeParse(value); + return parsed.success ? parsed.data : null; +} +/** Also used by the usage API: a string 'false' is not the boolean true. */ +export function isCodexLimitReached(value: unknown): boolean { + const raw = record(value); + return raw.allowed === false || read(raw, "limit_reached", "limitReached") === true; +} +function signal( + value: unknown, + scope: CodexQuotaScope, + limitId: string, + source: CodexQuotaSignal["source"], + now: number, + event = false +): CodexQuotaSignal | null { + const raw = record(value); + const windows = (["primary", "secondary"] as const).flatMap((id) => { + const window = parseCodexQuotaWindow( + event ? raw[id] : read(raw, `${id}_window`, `${id}Window`), + id, + now, + event + ); + return window ? [window] : []; + }); + const allowed = boolean(raw.allowed); + const reached = boolean(read(raw, "limit_reached", "limitReached")); + if (windows.length === 0 && allowed === null && reached === null) return null; + return { + scope, + limitId, + source, + allowed, + limitReached: allowed === false ? true : reached, + windows, + }; +} +function scopeForLimit(id: string): CodexQuotaScope | null { + if (id === "codex") return "codex"; + return isCodexSparkLimitDescriptor(id) ? "spark" : null; +} + +/** Normalize only the requested known meter. An unknown future meter is not normal Codex. */ +export function parseCodexOfficialQuotaHeaders( + headers: Record, + model?: string | null, + now = Date.now() +): CodexQuotaSignal | null { + const lower = Object.fromEntries( + Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]) + ); + const wanted = getCodexModelScope(model); + const families = new Set( + Object.keys(lower).flatMap((key) => { + const match = /^x-([a-z0-9_-]{1,128})-(?:primary|secondary)-used-percent$/.exec(key); + return match ? [match[1]] : []; + }) + ); + for (const family of families) { + if (scopeForLimit(family) !== wanted) continue; + const raw: JsonRecord = {}; + for (const id of ["primary", "secondary"] as const) { + const prefix = `x-${family}-${id}-`; + raw[id] = { + used_percent: lower[`${prefix}used-percent`], + window_minutes: lower[`${prefix}window-minutes`], + reset_at: lower[`${prefix}reset-at`], + }; + } + const result = signal(raw, wanted, family.replaceAll("-", "_"), "headers", now, true); + if (result) return result; + } + return null; +} + +/** Pure normalization; credits/reset coupons remain separate display/billing evidence. */ +export function parseCodexUsageSignal( + payload: unknown, + model?: string | null, + now = Date.now() +): CodexQuotaSignal | null { + const data = record(payload); + const scope = getCodexModelScope(model); + if (scope === "codex") + return signal(read(data, "rate_limit", "rateLimit"), scope, "codex", "usage", now); + const additional = read(data, "additional_rate_limits", "additionalRateLimits"); + if (!Array.isArray(additional)) return null; + for (const value of additional) { + const entry = record(value); + const descriptors = [ + "limit_id", + "limitId", + "limit_name", + "limitName", + "metered_feature", + "meteredFeature", + "id", + "name", + "model", + ].map((key) => entry[key]); + if (!isCodexSparkLimitDescriptor(...descriptors)) continue; + return signal( + read(entry, "rate_limit", "rateLimit"), + scope, + String(entry.limit_id ?? entry.limitId ?? "spark"), + "usage", + now + ); + } + return null; +} + +const eventSchema = z.object({ + type: z.literal("codex.rate_limits"), + metered_limit_name: z.string().max(128).nullish(), + limit_name: z.string().max(128).nullish(), + rate_limits: z.unknown().optional(), +}); +/** Parser only: transport-side persistence must additionally be owner/revision fenced. */ +export function parseCodexRateLimitEvent( + payload: unknown, + now = Date.now() +): CodexQuotaSignal | null { + const parsed = eventSchema.safeParse(payload); + if (!parsed.success) return null; + const id = (parsed.data.metered_limit_name ?? parsed.data.limit_name ?? "codex") + .trim() + .toLowerCase(); + const scope = scopeForLimit(id); + return scope ? signal(parsed.data.rate_limits, scope, id, "websocket", now, true) : null; +} diff --git a/open-sse/services/codexUsageQuotas.ts b/open-sse/services/codexUsageQuotas.ts index 0d1afd5bdb0c..dd18ba9f5e95 100644 --- a/open-sse/services/codexUsageQuotas.ts +++ b/open-sse/services/codexUsageQuotas.ts @@ -9,6 +9,7 @@ import { isCodexSparkLimitDescriptor, } from "../config/codexQuotaScopes.ts"; import { inferWindowFamilyLabel } from "./quotaWindowLabel.ts"; +import { parseCodexQuotaWindow } from "./codexQuotaSignals.ts"; import { parseCodexPaidCredits, type CodexPaidCredits } from "@/lib/providers/codexPaidCredits"; type JsonRecord = Record; @@ -45,67 +46,21 @@ function toNumber(value: unknown, fallback = 0): number { return fallback; } -function toNullableNumber(value: unknown): number | null { - if (typeof value === "number" && Number.isFinite(value)) return value; - if (typeof value === "string" && value.trim().length > 0) { - const parsed = Number(value); - return Number.isFinite(parsed) ? parsed : null; - } - return null; -} - -function parseResetTime(resetValue: unknown): string | null { - if (!resetValue) return null; - try { - let date: Date | null = null; - if (resetValue instanceof Date) { - date = resetValue; - } else if (typeof resetValue === "number") { - date = new Date(resetValue < 1e12 ? resetValue * 1000 : resetValue); - } else if (typeof resetValue === "string") { - // Numeric strings are Unix timestamps too (seconds or milliseconds). - if (/^\d+$/.test(resetValue)) { - const ts = Number(resetValue); - date = new Date(ts < 1e12 ? ts * 1000 : ts); - } else { - date = new Date(resetValue); - } - } - if (!date || date.getTime() <= 0) return null; - return date.toISOString(); - } catch { - return null; - } -} - -function parseWindowReset(window: unknown): string | null { - const resetAt = toNumber(getFieldValue(window, "reset_at", "resetAt"), 0); - const resetAfterSeconds = toNumber( - getFieldValue(window, "reset_after_seconds", "resetAfterSeconds"), - 0 - ); - if (resetAt > 0) return parseResetTime(resetAt * 1000); - if (resetAfterSeconds > 0) return parseResetTime(Date.now() + resetAfterSeconds * 1000); - return null; -} - -function buildPercentageQuota(window: JsonRecord, displayName?: string): CodexUsageQuota { - const usedPercent = toNumber(getFieldValue(window, "used_percent", "usedPercent"), 0); - return { - used: usedPercent, +function assignPercentageQuota( + quotas: Record, + key: string, + rawWindow: JsonRecord, + displayName?: string +): void { + const window = parseCodexQuotaWindow(rawWindow); + if (!window) return; + quotas[key] = { + used: window.usedPercent, total: 100, - remaining: 100 - usedPercent, - resetAt: parseWindowReset(window), + remaining: 100 - window.usedPercent, + resetAt: window.resetAt, unlimited: false, - windowSeconds: toNullableNumber( - getFieldValue( - window, - "limit_window_seconds", - "limitWindowSeconds", - "window_seconds", - "windowSeconds" - ) - ), + windowSeconds: window.windowSeconds, ...(displayName ? { displayName } : {}), }; } @@ -274,7 +229,7 @@ function appendCodexReserveQuotas(data: JsonRecord, quotas: Record 0) { const primaryLabel = windowDurationLabel(primaryWindow); - quotas.session = buildPercentageQuota( + assignPercentageQuota( + quotas, + "session", primaryWindow, primaryLabel === "Weekly" || primaryLabel === "Monthly" ? primaryLabel : undefined ); @@ -383,7 +340,9 @@ export function buildCodexUsageQuotas(dataValue: unknown): { const secondaryWindow = toRecord(getFieldValue(rateLimit, "secondary_window", "secondaryWindow")); if (Object.keys(secondaryWindow).length > 0) { const secondaryLabel = windowDurationLabel(secondaryWindow); - quotas.weekly = buildPercentageQuota( + assignPercentageQuota( + quotas, + "weekly", secondaryWindow, secondaryLabel === "Session" ? secondaryLabel : undefined ); @@ -411,7 +370,7 @@ export function buildCodexUsageQuotas(dataValue: unknown): { getFieldValue(codeReviewWindow, "used_percent", "usedPercent") !== null || getFieldValue(codeReviewWindow, "remaining_count", "remainingCount") !== null ) { - quotas.code_review = buildPercentageQuota(codeReviewWindow); + assignPercentageQuota(quotas, "code_review", codeReviewWindow); } const codeReviewSecondaryWindow = toRecord( @@ -421,7 +380,7 @@ export function buildCodexUsageQuotas(dataValue: unknown): { getFieldValue(codeReviewSecondaryWindow, "used_percent", "usedPercent") !== null || getFieldValue(codeReviewSecondaryWindow, "remaining_count", "remainingCount") !== null ) { - quotas.code_review_weekly = buildPercentageQuota(codeReviewSecondaryWindow); + assignPercentageQuota(quotas, "code_review_weekly", codeReviewSecondaryWindow); } const spark = findCodexSparkRateLimit(data); @@ -437,14 +396,16 @@ export function buildCodexUsageQuotas(dataValue: unknown): { // a permanent 100% row with a meaningless always-full reset. Once the account // actually uses the spark model the window is no longer latent and appears. if (Object.keys(sparkPrimaryWindow).length > 0 && !isLatentWindow(sparkPrimaryWindow)) { - quotas[CODEX_SPARK_QUOTA_SESSION] = buildPercentageQuota(sparkPrimaryWindow, sparkDisplayName); + assignPercentageQuota(quotas, CODEX_SPARK_QUOTA_SESSION, sparkPrimaryWindow, sparkDisplayName); } const sparkSecondaryWindow = toRecord( getFieldValue(sparkRateLimit, "secondary_window", "secondaryWindow") ); if (Object.keys(sparkSecondaryWindow).length > 0 && !isLatentWindow(sparkSecondaryWindow)) { - quotas[CODEX_SPARK_QUOTA_WEEKLY] = buildPercentageQuota( + assignPercentageQuota( + quotas, + CODEX_SPARK_QUOTA_WEEKLY, sparkSecondaryWindow, `${sparkDisplayName} Weekly` ); diff --git a/open-sse/services/usage/codex.ts b/open-sse/services/usage/codex.ts index 72e58a81f367..3897812e1253 100644 --- a/open-sse/services/usage/codex.ts +++ b/open-sse/services/usage/codex.ts @@ -9,6 +9,7 @@ */ import { buildCodexUsageQuotas } from "../codexUsageQuotas.ts"; +import { isCodexLimitReached } from "../codexQuotaSignals.ts"; import { fetchWithConnectionProxy } from "../connectionProxyFetch.ts"; import { getCodexBackendIdentityHeaders } from "../../config/codexClient.ts"; import { getFieldValue } from "./scalars.ts"; @@ -90,7 +91,7 @@ export async function getCodexUsage( return { plan: String(getFieldValue(data, "plan_type", "planType") || "unknown"), - limitReached: Boolean(getFieldValue(rateLimit, "limit_reached", "limitReached")), + limitReached: isCodexLimitReached(rateLimit), quotas, // Banked reset credits (display-only, eligibility-gated — issue #5199). // Absent for most accounts; never throws when the upstream omits it. diff --git a/tests/unit/codex-quota-signal-normalization.test.ts b/tests/unit/codex-quota-signal-normalization.test.ts new file mode 100644 index 000000000000..112088a3de3d --- /dev/null +++ b/tests/unit/codex-quota-signal-normalization.test.ts @@ -0,0 +1,308 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +const directory = fs.mkdtempSync(path.join(os.tmpdir(), "codex-quota-signals-")); +process.env.DATA_DIR = directory; +process.env.DISABLE_SQLITE_AUTO_BACKUP = "true"; +process.env.OMNIROUTE_QUOTA_FETCH_MIN_INTERVAL_MS = "0"; +const { parseCodexQuotaHeaders, getCodexDualWindowCooldownMs } = + await import("../../open-sse/executors/codex/quota.ts"); +const fetcher = await import("../../open-sse/services/codexQuotaFetcher.ts"); +const { buildCodexUsageQuotas } = await import("../../open-sse/services/codexUsageQuotas.ts"); +const { getCodexUsage, setCodexUsageFetchForTests } = + await import("../../open-sse/services/usage/codex.ts"); +const { persistCodexChildQuotaResponse, persistCodexChildCooldown, getCodexChildCooldown } = + await import("../../open-sse/services/codexAccount/index.ts"); +const providers = await import("../../src/lib/db/providers.ts"); +const { resetDbInstance } = await import("../../src/lib/db/core.ts"); + +type JsonRecord = Record; +const originalFetch = globalThis.fetch; +const sparkModel = "gpt-5.3-codex-spark"; +const connections = new Set(); +const soon = Math.floor((Date.now() + 60_000) / 1000); +const later = Math.floor((Date.now() + 600_000) / 1000); +const iso = (seconds: number) => new Date(seconds * 1000).toISOString(); +const official = (primary = "99", secondary = "20", family = "codex") => ({ + [`x-${family}-primary-used-percent`]: primary, + [`x-${family}-primary-window-minutes`]: "300", + [`x-${family}-primary-reset-at`]: String(soon), + [`x-${family}-secondary-used-percent`]: secondary, + [`x-${family}-secondary-window-minutes`]: "10080", + [`x-${family}-secondary-reset-at`]: String(later), +}); + +async function fetchPayload(payload: unknown, requestedModel = "gpt-5.5") { + const id = `quota-signals-${connections.size}`; + connections.add(id); + globalThis.fetch = async () => Response.json(payload); + return fetcher.fetchCodexQuota(id, { + accessToken: "synthetic-quota-signal-token", + providerSpecificData: { workspaceId: "synthetic-workspace" }, + requestedModel, + }); +} + +test.afterEach(() => { + globalThis.fetch = originalFetch; + setCodexUsageFetchForTests(null); +}); +test.after(() => { + for (const id of connections) fetcher.unregisterCodexConnection(id); + resetDbInstance(); + fs.rmSync(directory, { recursive: true, force: true }); +}); + +test("official primary headers convert percentages once and use epoch seconds", () => { + const result = parseCodexQuotaHeaders(official()); + assert.ok(result); + assert.equal(result.usage5h, 99); + assert.equal(result.limit5h, 100); + assert.equal(result.resetAt5h, iso(soon)); + assert.equal(result.usage7d, 20); + assert.equal(result.limit7d, 100); + assert.equal(result.resetAt7d, iso(later)); + const cooldown = getCodexDualWindowCooldownMs(result); + assert.equal(cooldown.window, "5h"); + assert.ok(cooldown.cooldownMs > 0 && cooldown.cooldownMs <= 60_000); +}); + +test("official secondary exhaustion wins without confusing percent and token counts", () => { + const result = parseCodexQuotaHeaders(official("1", "100")); + assert.ok(result); + assert.equal(getCodexDualWindowCooldownMs(result).window, "7d"); +}); + +test("official fractional and explicit zero percentages are preserved", () => { + const result = parseCodexQuotaHeaders(official("23.75", "0")); + assert.ok(result); + assert.equal(result.usage5h / result.limit5h, 0.2375); + assert.equal(result.usage7d, 0); + assert.equal(result.limit7d, 100); +}); + +test("HTTP header names are case insensitive", () => { + const headers = Object.fromEntries( + Object.entries(official()).map(([k, v]) => [k.toUpperCase(), v]) + ); + assert.equal(parseCodexQuotaHeaders(headers)?.usage5h, 99); +}); + +test("actual duration descriptors, not primary position, select the fixed legacy slots", () => { + const headers = { + ...official("100", "10"), + "x-codex-primary-window-minutes": "10080", + "x-codex-secondary-window-minutes": "300", + }; + const result = parseCodexQuotaHeaders(headers); + assert.ok(result); + assert.equal(result.usage7d, 100); + assert.equal(result.resetAt7d, iso(soon)); + assert.equal(result.usage5h, 10); + assert.equal(result.resetAt5h, iso(later)); +}); + +test("a nonstandard duration is not falsely persisted as a 5h window", () => { + assert.equal( + parseCodexQuotaHeaders({ + "x-codex-primary-used-percent": "100", + "x-codex-primary-window-minutes": "60", + "x-codex-primary-reset-at": String(soon), + }), + null + ); +}); + +test("unknown meter families and credits-only headers are not normal subscription health", () => { + assert.equal(parseCodexQuotaHeaders(official("100", "100", "future-meter")), null); + assert.equal( + parseCodexQuotaHeaders({ + "x-codex-credits-unlimited": "true", + "x-codex-credits-has-credits": "true", + }), + null + ); +}); + +test("legacy token-count headers keep their units", () => { + const result = parseCodexQuotaHeaders({ + "x-codex-5h-usage": "150", + "x-codex-5h-limit": "200", + "x-codex-5h-reset-at": iso(soon), + }); + assert.ok(result); + assert.equal(result.usage5h, 150); + assert.equal(result.limit5h, 200); + assert.equal(result.resetAt5h, iso(soon)); +}); + +for (const usage of ["", "NaN", "98junk", "Infinity", "-1"]) { + test(`malformed legacy usage ${JSON.stringify(usage)} remains unknown`, () => { + assert.equal( + parseCodexQuotaHeaders({ "x-codex-5h-usage": usage, "x-codex-5h-limit": "100" }), + null + ); + }); +} + +test("a legacy reset with no usage/limit evidence is unknown, not zero used", () => { + assert.equal(parseCodexQuotaHeaders({ "x-codex-5h-reset-at": iso(soon) }), null); +}); + +test("impossible legacy reset timestamps cannot create an absurd cooldown", () => { + const result = parseCodexQuotaHeaders({ + "x-codex-5h-usage": "100", + "x-codex-5h-limit": "100", + "x-codex-5h-reset-at": "9999-01-01T00:00:00.000Z", + }); + assert.ok(result); + assert.equal(result.resetAt5h, null); + assert.equal(getCodexDualWindowCooldownMs(result).cooldownMs, 0); +}); + +for (const value of [undefined, null, "", "garbage", "12junk", true, -1, 101, [], {}]) { + test(`malformed usage percentage ${JSON.stringify(value)} is not healthy`, async () => { + const window = { used_percent: value, reset_after_seconds: 60 }; + const payload = { rate_limit: { primary_window: window } }; + assert.equal(await fetchPayload(payload), null); + assert.deepEqual(buildCodexUsageQuotas(payload).quotas, {}); + }); +} + +test("one malformed window cannot fabricate health or hide a valid exhausted sibling", async () => { + const payload = { + rate_limit: { + primary_window: { used_percent: "bad", reset_after_seconds: 60 }, + secondary_window: { used_percent: 100, reset_after_seconds: 600 }, + }, + }; + const result = await fetchPayload(payload); + assert.ok(result); + assert.equal(result.percentUsed, 1); + assert.equal(result.windows?.session, undefined); + assert.equal(result.windows?.weekly.percentUsed, 1); + const display = buildCodexUsageQuotas(payload).quotas; + assert.equal(display.session, undefined); + assert.equal(display.weekly.used, 100); +}); + +test("explicit zero and camelCase compatibility are evidence, not missing data", async () => { + const result = await fetchPayload({ + rateLimit: { primaryWindow: { usedPercent: "0", resetAfterSeconds: 60 } }, + }); + assert.ok(result); + assert.equal(result.percentUsed, 0); + assert.equal(result.windows?.session.percentUsed, 0); +}); + +test("usage allowed=false is an explicit hard stop, even below the percentage threshold", async () => { + const result = await fetchPayload({ + rate_limit: { allowed: false, primary_window: { used_percent: 20 } }, + }); + assert.equal(result?.limitReached, true); +}); + +test("usage API projects explicit allowed=false as a hard stop", async () => { + setCodexUsageFetchForTests(async () => + Response.json({ + rate_limit: { allowed: false, primary_window: { used_percent: 20 } }, + }) + ); + const result = await getCodexUsage("synthetic-usage-token"); + assert.equal("limitReached" in result && result.limitReached, true); +}); + +test("usage string false is not a boolean hard stop", async () => { + const result = await fetchPayload({ + rate_limit: { limit_reached: "false", primary_window: { used_percent: 20 } }, + }); + assert.equal(result?.limitReached, false); +}); + +for (const reset of ["12junk", -1, 1e20, Date.now() + 60_000]) { + test(`usage reset_at is epoch seconds, not milliseconds or parseFloat ${reset}`, async () => { + const payload = { rate_limit: { primary_window: { used_percent: 100, reset_at: reset } } }; + const result = await fetchPayload(payload); + assert.ok(result, "valid exhaustion evidence survives a malformed optional reset"); + assert.equal(result.percentUsed, 1); + assert.equal(result.resetAt, null); + assert.equal(buildCodexUsageQuotas(payload).quotas.session.resetAt, null); + }); +} + +test("paid/unlimited credits and banked reset credits do not erase subscription exhaustion", async () => { + const payload = { + rate_limit: { primary_window: { used_percent: 100, reset_after_seconds: 60 } }, + credits: { has_credits: true, unlimited: true, balance: "42" }, + rate_limit_reset_credits: { available_count: 3 }, + }; + const result = await fetchPayload(payload); + assert.equal(result?.percentUsed, 1); + assert.equal(result?.bankedResetCredits, 3); + assert.equal(buildCodexUsageQuotas(payload).quotas.session.unlimited, false); +}); + +test("normal and Spark usage stay independent when one scope is malformed", async () => { + const payload = { + rate_limit: { primary_window: { used_percent: "bad" } }, + additional_rate_limits: [ + { + limit_id: "codex_bengalfox", + rate_limit: { primary_window: { used_percent: 100, reset_after_seconds: 60 } }, + }, + ], + }; + assert.equal(await fetchPayload(payload), null); + const spark = await fetchPayload(payload, sparkModel); + assert.equal(spark?.percentUsed, 1); + assert.equal(spark?.windows?.session, undefined); + assert.equal(spark?.windows?.gpt_5_3_codex_spark_session.percentUsed, 1); +}); + +test("official headers reach scoped persistence without clearing a sibling cooldown", async () => { + const connection = await providers.createProviderConnection({ + provider: "codex", + authType: "oauth", + name: "signal-consumer-fixture", + accessToken: "synthetic-consumer-token", + providerSpecificData: { unrelated: "keep" }, + }); + const id = String(connection.id); + const sparkUntil = iso(later); + await persistCodexChildCooldown({ + connectionId: id, + model: sparkModel, + rateLimitedUntil: sparkUntil, + }); + const result = await persistCodexChildQuotaResponse({ + connectionId: id, + model: "gpt-5.5", + status: 429, + headers: official(), + }); + assert.ok(result); + assert.equal(result.scope, "codex"); + const current = await providers.getProviderConnectionById(id); + assert.equal(getCodexChildCooldown(current as never, "gpt-5.5"), iso(soon)); + assert.equal(getCodexChildCooldown(current as never, sparkModel), sparkUntil); + assert.equal((current.providerSpecificData as JsonRecord).unrelated, "keep"); +}); + +test("normal HTTP telemetry is not written to the Spark child just because Spark was requested", async () => { + const connection = await providers.createProviderConnection({ + provider: "codex", + authType: "oauth", + name: "signal-spark-fixture", + accessToken: "synthetic-spark-token", + }); + const result = await persistCodexChildQuotaResponse({ + connectionId: String(connection.id), + model: sparkModel, + status: 429, + headers: official(), + }); + assert.equal(result, null); +}); diff --git a/tests/unit/codex-quota-wire-normalizer.test.ts b/tests/unit/codex-quota-wire-normalizer.test.ts new file mode 100644 index 000000000000..6adbfd97077d --- /dev/null +++ b/tests/unit/codex-quota-wire-normalizer.test.ts @@ -0,0 +1,203 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { + parseCodexOfficialQuotaHeaders, + parseCodexQuotaWindow, + parseCodexRateLimitEvent, + parseCodexUsageSignal, +} from "../../open-sse/services/codexQuotaSignals.ts"; + +const now = Date.parse("2026-10-28T00:00:00.000Z"); +const reset = now / 1000 + 120; +const sparkModel = "gpt-5.3-codex-spark"; +const window = { used_percent: 99, reset_at: reset, limit_window_seconds: 18000 }; +const eventWindow = { used_percent: 99, reset_at: reset, window_minutes: 300 }; + +// These are new pure-parser controls, in addition to the existing-entrypoint +// red/green regression. They do not claim live WS persistence/admission coverage. +test("usage, HTTP and WS normalize to the same fraction, duration and epoch reset", () => { + const usage = parseCodexUsageSignal({ rate_limit: { primary_window: window } }, null, now); + const headers = parseCodexOfficialQuotaHeaders( + { + "x-codex-primary-used-percent": "99", + "x-codex-primary-window-minutes": "300", + "x-codex-primary-reset-at": String(reset), + }, + null, + now + ); + const websocket = parseCodexRateLimitEvent( + { type: "codex.rate_limits", rate_limits: { primary: eventWindow } }, + now + ); + assert.ok(usage && headers && websocket); + assert.deepEqual(usage.windows, headers.windows); + assert.deepEqual(usage.windows, websocket.windows); + assert.equal(usage.windows[0].percentUsed, 0.99); + assert.equal(usage.windows[0].usedPercent, 99); + assert.equal(usage.windows[0].windowSeconds, 18000); + assert.equal(usage.windows[0].resetAt, new Date(reset * 1000).toISOString()); +}); + +test("unknown named HTTP and WS meters do not become normal Codex", () => { + assert.equal( + parseCodexOfficialQuotaHeaders({ "x-other-primary-used-percent": "100" }, null, now), + null + ); + assert.equal( + parseCodexRateLimitEvent( + { + type: "codex.rate_limits", + metered_limit_name: "other", + rate_limits: { primary: eventWindow }, + }, + now + ), + null + ); +}); + +test("Spark HTTP meter is selected only for the Spark request scope", () => { + const headers = { + "x-codex-primary-used-percent": "10", + "x-codex-bengalfox-primary-used-percent": "100", + "x-codex-bengalfox-primary-window-minutes": "300", + }; + assert.equal(parseCodexOfficialQuotaHeaders(headers, null, now)?.windows[0].percentUsed, 0.1); + const spark = parseCodexOfficialQuotaHeaders(headers, sparkModel, now); + assert.equal(spark?.scope, "spark"); + assert.equal(spark?.limitId, "codex_bengalfox"); + assert.equal(spark?.windows[0].percentUsed, 1); +}); + +test("named Spark WS events retain scope and real nonstandard window duration", () => { + const spark = parseCodexRateLimitEvent( + { + type: "codex.rate_limits", + metered_limit_name: "codex_bengalfox", + rate_limits: { secondary: { used_percent: 100, window_minutes: 60, reset_at: reset } }, + }, + now + ); + assert.equal(spark?.scope, "spark"); + assert.equal(spark?.windows[0].id, "secondary"); + assert.equal(spark?.windows[0].windowSeconds, 3600); +}); + +test("a bad WS primary does not discard a valid exhausted secondary", () => { + const result = parseCodexRateLimitEvent( + { + type: "codex.rate_limits", + rate_limits: { + primary: { used_percent: "NaN" }, + secondary: { ...eventWindow, used_percent: 100 }, + }, + }, + now + ); + assert.equal(result?.windows.length, 1); + assert.equal(result?.windows[0].id, "secondary"); + assert.equal(result?.windows[0].percentUsed, 1); +}); + +test("credits-only, coupon-only, empty and unrelated WS frames are not subscription health", () => { + for (const payload of [ + {}, + null, + { type: "rate_limits.updated", rate_limits: { primary: eventWindow } }, + { type: "codex.rate_limits", credits: { has_credits: true, unlimited: true, balance: "4" } }, + { type: "codex.rate_limits", rate_limit_reset_credits: { available_count: 9 } }, + ]) + assert.equal(parseCodexRateLimitEvent(payload, now), null); +}); + +test("unlimited paid credits do not mark exhausted subscription telemetry unlimited", () => { + const result = parseCodexRateLimitEvent( + { + type: "codex.rate_limits", + rate_limits: { primary: { used_percent: 100 } }, + credits: { has_credits: true, unlimited: true }, + }, + now + ); + assert.equal(result?.windows[0].percentUsed, 1); + assert.equal(Object.hasOwn(result, "unlimited"), false); +}); + +test("null additional meters do not invalidate normal usage", () => { + const result = parseCodexUsageSignal( + { rate_limit: { primary_window: window }, additional_rate_limits: null }, + null, + now + ); + assert.equal(result?.windows[0].percentUsed, 0.99); +}); + +test("Spark usage never falls back to a normal or unknown additional meter", () => { + assert.equal( + parseCodexUsageSignal( + { + rate_limit: { primary_window: window }, + additional_rate_limits: [{ limit_id: "other", rate_limit: { primary_window: window } }], + }, + sparkModel, + now + ), + null + ); +}); + +test("percentage strings have full decimal validation, not parseFloat prefixes", () => { + for (const used_percent of [null, "", "12oops", true, [], {}, -1, 101, NaN, Infinity]) { + assert.equal(parseCodexQuotaWindow({ used_percent }, "primary", now), null); + } + assert.equal(parseCodexQuotaWindow({ used_percent: "0.5" }, "primary", now)?.percentUsed, 0.005); + assert.equal(parseCodexQuotaWindow({ used_percent: 0 }, "primary", now)?.percentUsed, 0); +}); + +test("bad optional dates and durations keep exhaustion but cannot become enormous cooldowns", () => { + const result = parseCodexQuotaWindow( + { used_percent: 100, reset_at: 1e20, limit_window_seconds: 1e20 }, + "primary", + now + ); + assert.equal(result?.percentUsed, 1); + assert.equal(result?.resetAt, null); + assert.equal(result?.windowSeconds, null); + assert.equal(result?.durationProvided, true); +}); + +test("reset unit conversions are explicit and relative zero means now", () => { + assert.equal( + parseCodexQuotaWindow({ used_percent: 1, reset_after_seconds: 120 }, "primary", now)?.resetAt, + new Date(now + 120000).toISOString() + ); + assert.equal( + parseCodexQuotaWindow({ used_percent: 1, reset_after_seconds: 0 }, "primary", now)?.resetAt, + new Date(now).toISOString() + ); + assert.equal( + parseCodexQuotaWindow({ used_percent: 1, reset_at: now + 120000 }, "primary", now)?.resetAt, + null + ); +}); + +test("31-day proxy scheduling guard rejects absurd relative resets, not valid percentages", () => { + assert.equal( + parseCodexQuotaWindow({ used_percent: 100, reset_after_seconds: 32 * 86400 }, "primary", now) + ?.resetAt, + null + ); + assert.ok( + parseCodexQuotaWindow({ used_percent: 100, reset_after_seconds: 30 * 86400 }, "primary", now) + ?.resetAt + ); +}); + +test("explicit boolean allowance stays separate from unknown quota windows", () => { + const result = parseCodexUsageSignal({ rate_limit: { allowed: false } }, null, now); + assert.equal(result?.allowed, false); + assert.equal(result?.limitReached, true); + assert.deepEqual(result?.windows, []); + assert.equal(parseCodexUsageSignal({ rate_limit: { allowed: "false" } }, null, now), null); +}); From 546efb16ac2b4dd114a92cbf55bc3d5325824587 Mon Sep 17 00:00:00 2001 From: shevar0 <3235212+shevar0@users.noreply.github.com> Date: Tue, 27 Oct 2026 11:43:22 +0100 Subject: [PATCH 2/8] docs: link codex-quota-signal-normalization fix to #18334 --- ...rmalization.md => 18334-codex-quota-signal-normalization.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename changelog.d/fixes/{0000-codex-quota-signal-normalization.md => 18334-codex-quota-signal-normalization.md} (58%) diff --git a/changelog.d/fixes/0000-codex-quota-signal-normalization.md b/changelog.d/fixes/18334-codex-quota-signal-normalization.md similarity index 58% rename from changelog.d/fixes/0000-codex-quota-signal-normalization.md rename to changelog.d/fixes/18334-codex-quota-signal-normalization.md index 4361ff1a7c55..f7ba272708cd 100644 --- a/changelog.d/fixes/0000-codex-quota-signal-normalization.md +++ b/changelog.d/fixes/18334-codex-quota-signal-normalization.md @@ -1 +1 @@ -- **fix(codex):** Parse official primary/secondary quota headers, keep malformed usage unknown instead of inventing healthy zero-percent windows, validate reset units, and preserve independent Codex/Spark evidence and explicit quota denial. +- **fix(codex):** Parse official primary/secondary quota headers, keep malformed usage unknown instead of inventing healthy zero-percent windows, validate reset units, and preserve independent Codex/Spark evidence and explicit quota denial. ([#18334](https://github.com/cheaper-inference/OmniRoute/pull/18334)) — thanks @shevar0 From 52e4696037c633094c1ab1007bc0d97026757092 Mon Sep 17 00:00:00 2001 From: shevar0 <3235212+shevar0@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:57:35 -0300 Subject: [PATCH 3/8] fix(sse): classify Codex quota windows by duration (#18326, #18330) Preserve contributor quota normalization while mapping weekly-only and reversed windows consistently across preflight, usage display and account pools. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --- .../18330-codex-weekly-window-identity.md | 1 + open-sse/executors/codex/quota.ts | 9 +- open-sse/services/codexQuotaFetcher.ts | 82 +++---- open-sse/services/codexQuotaSignals.ts | 20 ++ open-sse/services/codexUsageQuotas.ts | 84 ++++---- .../codex-banked-reset-credits-5199.test.ts | 36 ++-- tests/unit/codex-paid-credits.test.ts | 5 +- tests/unit/codex-quota-fetcher.test.ts | 39 +++- .../codex-quota-signal-normalization.test.ts | 49 ++++- .../codex-quota-window-identity-18330.test.ts | 200 ++++++++++++++++++ .../codex-usage-quotas-latent-window.test.ts | 45 ++-- .../codex-usage-quotas-review-window.test.ts | 6 +- tests/unit/codex-usage-windows.test.ts | 2 +- tests/unit/codex-weekly-routing-18330.test.ts | 132 ++++++++++++ ...headroom-codex-quota-snapshot-6379.test.ts | 4 +- tests/unit/quota-window-label.test.ts | 6 +- 16 files changed, 570 insertions(+), 150 deletions(-) create mode 100644 changelog.d/fixes/18330-codex-weekly-window-identity.md create mode 100644 tests/unit/codex-quota-window-identity-18330.test.ts create mode 100644 tests/unit/codex-weekly-routing-18330.test.ts diff --git a/changelog.d/fixes/18330-codex-weekly-window-identity.md b/changelog.d/fixes/18330-codex-weekly-window-identity.md new file mode 100644 index 000000000000..7c5a61eebd84 --- /dev/null +++ b/changelog.d/fixes/18330-codex-weekly-window-identity.md @@ -0,0 +1 @@ +- fix(providers): classify Codex quota windows by their reported duration so weekly-only Pro quota enforces weekly cutoffs and agrees across usage badges and account pools; preserve unknown durations without inventing 5h availability (#18326, #18330). diff --git a/open-sse/executors/codex/quota.ts b/open-sse/executors/codex/quota.ts index ec73e46d9db3..bb93ac85de0a 100644 --- a/open-sse/executors/codex/quota.ts +++ b/open-sse/executors/codex/quota.ts @@ -1,5 +1,6 @@ // Codex quota-snapshot parsing + reset/cooldown scheduling (pure). import { + classifyCodexQuotaWindow, parseCodexFiniteNumber, parseCodexLegacyReset, parseCodexOfficialQuotaHeaders, @@ -55,12 +56,8 @@ export function parseCodexQuotaHeaders( } const official = parseCodexOfficialQuotaHeaders(lower, model); for (const window of official?.windows ?? []) { - const seconds = window.durationProvided - ? window.windowSeconds - : window.id === "primary" - ? 18000 - : 604800; - const slot = seconds === 18000 ? "5h" : seconds === 604800 ? "7d" : null; + const family = classifyCodexQuotaWindow(window); + const slot = family === "session" ? "5h" : family === "weekly" ? "7d" : null; // This legacy DTO cannot represent arbitrary durations. Keep them in the // normalized signal rather than inventing a 5h/7d identity for persistence. if (!slot || observed.has(slot)) continue; diff --git a/open-sse/services/codexQuotaFetcher.ts b/open-sse/services/codexQuotaFetcher.ts index 4b7c98d42583..c280d82c415e 100644 --- a/open-sse/services/codexQuotaFetcher.ts +++ b/open-sse/services/codexQuotaFetcher.ts @@ -3,9 +3,8 @@ * * Implements QuotaFetcher for the Codex provider (quotaPreflight.ts + quotaMonitor.ts). * - * Codex has TWO independent quota windows: - * - Primary (5h): short-term rate limit, resets every 5 hours - * - Secondary (7d): weekly limit, resets every 7 days + * Codex may report independent 5h and 7d windows in either upstream position. + * Their explicit duration determines identity; primary may be weekly-only. * * We return percentUsed = max(5h%, 7d%) so the system switches accounts when * EITHER window approaches exhaustion (95% threshold). @@ -16,6 +15,7 @@ * Registration: call registerCodexQuotaFetcher() once at server startup. */ +import { parseCodexPaidCredits } from "@/lib/providers/codexPaidCredits"; import { CODEX_SPARK_QUOTA_SESSION, CODEX_SPARK_QUOTA_WEEKLY, @@ -32,8 +32,12 @@ import { registerMonitorFetcher } from "./quotaMonitor.ts"; import { throttleQuotaFetch } from "./quotaFetchThrottle.ts"; import { fetchWithConnectionProxy } from "./connectionProxyFetch.ts"; import { getCodexBackendIdentityHeaders } from "../config/codexClient.ts"; -import { parseCodexPaidCredits } from "@/lib/providers/codexPaidCredits"; -import { isCodexLimitReached, parseCodexQuotaWindow } from "./codexQuotaSignals.ts"; +import { + codexQuotaWindowKey, + isCodexLimitReached, + parseCodexQuotaWindow, + type CodexQuotaWindow, +} from "./codexQuotaSignals.ts"; /** * Stable identifiers for Codex's quota windows. These match the quota keys @@ -43,8 +47,8 @@ import { isCodexLimitReached, parseCodexQuotaWindow } from "./codexQuotaSignals. * Codex fetcher exposes only session + weekly today; the plan-dependent * code_review window is surfaced by the generic path when present. */ -export const CODEX_WINDOW_SESSION = "session"; // primary 5-hour window -export const CODEX_WINDOW_WEEKLY = "weekly"; // secondary 7-day window +export const CODEX_WINDOW_SESSION = "session"; // explicit 5-hour duration +export const CODEX_WINDOW_WEEKLY = "weekly"; // explicit 7-day duration // Codex usage endpoint (same as usage.ts CODEX_CONFIG) const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage"; @@ -52,10 +56,10 @@ const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage"; // Cache TTL — short enough to be reactive, long enough to avoid rate limits const CACHE_TTL_MS = 60_000; // 60 seconds -// Per-account quota window info (richer than QuotaInfo — includes both windows) +// Per-account quota window info; structural slots exist only for known durations. export interface CodexDualWindowQuota extends QuotaInfo { - window5h: { percentUsed: number; resetAt: string | null }; - window7d: { percentUsed: number; resetAt: string | null }; + window5h?: { percentUsed: number; resetAt: string | null }; + window7d?: { percentUsed: number; resetAt: string | null }; limitReached: boolean; /** All known Codex quota windows, including Spark when the upstream exposes it. */ allWindows?: Record; @@ -184,17 +188,13 @@ function getCodexConnectionMeta( return connectionRegistry.get(connectionId) || null; } -function getDominantResetAt(quota: { - window5h: { percentUsed: number; resetAt: string | null }; - window7d: { percentUsed: number; resetAt: string | null }; -}): string | null { - if (quota.window7d.percentUsed > quota.window5h.percentUsed) { - return quota.window7d.resetAt || quota.window5h.resetAt; - } - if (quota.window5h.percentUsed > quota.window7d.percentUsed) { - return quota.window5h.resetAt || quota.window7d.resetAt; - } - return quota.window7d.resetAt || quota.window5h.resetAt; +function getDominantResetAt(windows: CodexQuotaWindow[]): string | null { + const dominant = [...windows].sort( + (a, b) => + b.percentUsed - a.percentUsed || + Number(b.windowSeconds === 604800) - Number(a.windowSeconds === 604800) + ); + return dominant.find((window) => window.resetAt)?.resetAt ?? null; } // ─── Core Fetcher ──────────────────────────────────────────────────────────── @@ -300,13 +300,6 @@ function toRecord(value: unknown): Record { : {}; } -function parseCodexWindow( - window: Record | null | undefined -): { percentUsed: number; resetAt: string | null } | null { - const parsed = parseCodexQuotaWindow(window); - return parsed ? { percentUsed: parsed.percentUsed, resetAt: parsed.resetAt } : null; -} - /** * Codex "banked reset credits" — same eligibility-gated field parsed in * codexUsageQuotas.ts (kept in sync manually; the two parsers read the same @@ -358,13 +351,17 @@ function findSparkRateLimit(data: Record): Record): { - primary: { percentUsed: number; resetAt: string | null } | null; - secondary: { percentUsed: number; resetAt: string | null } | null; + primary: CodexQuotaWindow | null; + secondary: CodexQuotaWindow | null; } { return { - primary: parseCodexWindow(toRecord(rateLimit["primary_window"] ?? rateLimit["primaryWindow"])), - secondary: parseCodexWindow( - toRecord(rateLimit["secondary_window"] ?? rateLimit["secondaryWindow"]) + primary: parseCodexQuotaWindow( + rateLimit["primary_window"] ?? rateLimit["primaryWindow"], + "primary" + ), + secondary: parseCodexQuotaWindow( + rateLimit["secondary_window"] ?? rateLimit["secondaryWindow"], + "secondary" ), }; } @@ -375,8 +372,8 @@ function assignCodexWindows( names: { primary: string; secondary: string } ): void { const { primary, secondary } = getCodexRateLimitWindows(rateLimit); - if (primary) target[names.primary] = primary; - if (secondary) target[names.secondary] = secondary; + if (primary) target[codexQuotaWindowKey(primary, names)] = primary; + if (secondary) target[codexQuotaWindowKey(secondary, names)] = secondary; } function getSelectedCodexRateLimit( @@ -413,9 +410,10 @@ function parseCodexUsageResponse( getCodexRateLimitWindows(selectedRateLimit); if (!parsedPrimary && !parsedSecondary) return null; - const window5h = parsedPrimary ?? { percentUsed: 0, resetAt: null }; - const window7d = parsedSecondary ?? { percentUsed: 0, resetAt: null }; - const worstPercentUsed = Math.max(window5h.percentUsed, window7d.percentUsed); + const measured = [parsedPrimary, parsedSecondary].filter( + (window): window is CodexQuotaWindow => window !== null + ); + const worstPercentUsed = Math.max(...measured.map((window) => window.percentUsed)); const limitReached = isCodexLimitReached(selectedRateLimit); const windows: Record = {}; @@ -438,6 +436,8 @@ function parseCodexUsageResponse( secondary: CODEX_WINDOW_WEEKLY, }); + const window5h = windows[useSparkWindows ? CODEX_SPARK_QUOTA_SESSION : CODEX_WINDOW_SESSION]; + const window7d = windows[useSparkWindows ? CODEX_SPARK_QUOTA_WEEKLY : CODEX_WINDOW_WEEKLY]; const bankedResetCredits = parseBankedResetCredits(obj); const paidCredits = parseCodexPaidCredits(obj.credits); const rateLimitReachedType = parseRateLimitReachedType(obj); @@ -448,7 +448,7 @@ function parseCodexUsageResponse( used: Math.round(worstPercentUsed * 100), total: 100, percentUsed: worstPercentUsed, - resetAt: getDominantResetAt({ window5h, window7d }), + resetAt: getDominantResetAt(measured), // Per-window breakdown for the preflight evaluator. For Spark requests this // intentionally contains ONLY Spark windows, so Spark exhaustion does not // preflight-block normal Codex requests (and vice versa). @@ -485,13 +485,13 @@ export function getCodexQuotaCooldownMs(quota: CodexDualWindowQuota, threshold = const now = Date.now(); // 7d window takes priority (if exhausted, must wait longer) - if (quota.window7d.percentUsed >= threshold && quota.window7d.resetAt) { + if (quota.window7d && quota.window7d.percentUsed >= threshold && quota.window7d.resetAt) { const resetTime = new Date(quota.window7d.resetAt).getTime(); if (resetTime > now) return resetTime - now; } // 5h window - if (quota.window5h.percentUsed >= threshold && quota.window5h.resetAt) { + if (quota.window5h && quota.window5h.percentUsed >= threshold && quota.window5h.resetAt) { const resetTime = new Date(quota.window5h.resetAt).getTime(); if (resetTime > now) return resetTime - now; } diff --git a/open-sse/services/codexQuotaSignals.ts b/open-sse/services/codexQuotaSignals.ts index d93a23dd2f7a..e5d2baeeb15e 100644 --- a/open-sse/services/codexQuotaSignals.ts +++ b/open-sse/services/codexQuotaSignals.ts @@ -233,3 +233,23 @@ export function parseCodexRateLimitEvent( const scope = scopeForLimit(id); return scope ? signal(parsed.data.rate_limits, scope, id, "websocket", now, true) : null; } + +/** Fixed routing slots require an explicit duration, never the upstream position. */ +export function classifyCodexQuotaWindow( + window: Pick +): "session" | "weekly" | null { + if (window.windowSeconds === 18000) return "session"; + if (window.windowSeconds === 604800) return "weekly"; + return null; +} + +/** Preserve unclassified evidence under a neutral key; do not fabricate known quota. */ +export function codexQuotaWindowKey( + window: CodexQuotaWindow, + names: { primary: string; secondary: string } +): string { + const family = classifyCodexQuotaWindow(window); + if (family === "session") return names.primary; + if (family === "weekly") return names.secondary; + return `${names.primary.replace(/session$/, "")}unknown_${window.id}`; +} diff --git a/open-sse/services/codexUsageQuotas.ts b/open-sse/services/codexUsageQuotas.ts index dd18ba9f5e95..98d332181618 100644 --- a/open-sse/services/codexUsageQuotas.ts +++ b/open-sse/services/codexUsageQuotas.ts @@ -1,3 +1,4 @@ +import { parseCodexPaidCredits, type CodexPaidCredits } from "@/lib/providers/codexPaidCredits"; import { CODEX_RESERVE_DISPLAY_NAME, CODEX_RESERVE_QUOTA_SESSION, @@ -9,8 +10,7 @@ import { isCodexSparkLimitDescriptor, } from "../config/codexQuotaScopes.ts"; import { inferWindowFamilyLabel } from "./quotaWindowLabel.ts"; -import { parseCodexQuotaWindow } from "./codexQuotaSignals.ts"; -import { parseCodexPaidCredits, type CodexPaidCredits } from "@/lib/providers/codexPaidCredits"; +import { codexQuotaWindowKey, parseCodexQuotaWindow } from "./codexQuotaSignals.ts"; type JsonRecord = Record; @@ -65,6 +65,31 @@ function assignPercentageQuota( }; } +function assignDurationQuotas( + quotas: Record, + rateLimit: JsonRecord, + names: { primary: string; secondary: string }, + displayName?: string, + skipLatent = false +): void { + for (const id of ["primary", "secondary"] as const) { + const raw = toRecord(getFieldValue(rateLimit, `${id}_window`, `${id}Window`)); + if (skipLatent && isLatentWindow(raw)) continue; + const window = parseCodexQuotaWindow(raw, id); + if (!window) continue; + const key = codexQuotaWindowKey(window, names); + const family = windowDurationLabel(raw); + const label = displayName + ? family === "Weekly" + ? `${displayName} Weekly` + : displayName + : family === "Monthly" + ? family + : undefined; + assignPercentageQuota(quotas, key, raw, label || undefined); + } +} + // A window whose duration is >= this is a weekly (or longer) window; <= the // session threshold is a short rolling ("session") window. ChatGPT reports the // window length in `limit_window_seconds`, so labels can follow the real @@ -322,31 +347,7 @@ export function buildCodexUsageQuotas(dataValue: unknown): { const bankedResetCredits = parseBankedResetCredits(data); const rateLimitReachedType = parseRateLimitReachedType(data); - // The `session`/`weekly` keys carry routing semantics (combo quota scoring, - // preflight, cooldowns) and stay position-based. Only the display label is - // corrected from the real window duration, so a `primary_window` that is - // actually a 7-day window shows "Weekly" instead of "Session". - const primaryWindow = toRecord(getFieldValue(rateLimit, "primary_window", "primaryWindow")); - if (Object.keys(primaryWindow).length > 0) { - const primaryLabel = windowDurationLabel(primaryWindow); - assignPercentageQuota( - quotas, - "session", - primaryWindow, - primaryLabel === "Weekly" || primaryLabel === "Monthly" ? primaryLabel : undefined - ); - } - - const secondaryWindow = toRecord(getFieldValue(rateLimit, "secondary_window", "secondaryWindow")); - if (Object.keys(secondaryWindow).length > 0) { - const secondaryLabel = windowDurationLabel(secondaryWindow); - assignPercentageQuota( - quotas, - "weekly", - secondaryWindow, - secondaryLabel === "Session" ? secondaryLabel : undefined - ); - } + assignDurationQuotas(quotas, rateLimit, { primary: "session", secondary: "weekly" }); // Resolve the code-review rate limit block. ChatGPT Codex exposes the same // information under two different shapes depending on the plan tier @@ -389,27 +390,16 @@ export function buildCodexUsageQuotas(dataValue: unknown): { // reports (e.g. as the spark model version bumps) rather than a hardcoded // constant; fall back to the constant when the field is absent. const sparkDisplayName = spark.limitName || CODEX_SPARK_DISPLAY_NAME; - const sparkPrimaryWindow = toRecord( - getFieldValue(sparkRateLimit, "primary_window", "primaryWindow") - ); - // Skip latent (never-used, full-window) spark ceilings so they don't render as - // a permanent 100% row with a meaningless always-full reset. Once the account - // actually uses the spark model the window is no longer latent and appears. - if (Object.keys(sparkPrimaryWindow).length > 0 && !isLatentWindow(sparkPrimaryWindow)) { - assignPercentageQuota(quotas, CODEX_SPARK_QUOTA_SESSION, sparkPrimaryWindow, sparkDisplayName); - } - - const sparkSecondaryWindow = toRecord( - getFieldValue(sparkRateLimit, "secondary_window", "secondaryWindow") + assignDurationQuotas( + quotas, + sparkRateLimit, + { + primary: CODEX_SPARK_QUOTA_SESSION, + secondary: CODEX_SPARK_QUOTA_WEEKLY, + }, + sparkDisplayName, + true ); - if (Object.keys(sparkSecondaryWindow).length > 0 && !isLatentWindow(sparkSecondaryWindow)) { - assignPercentageQuota( - quotas, - CODEX_SPARK_QUOTA_WEEKLY, - sparkSecondaryWindow, - `${sparkDisplayName} Weekly` - ); - } appendCodexReserveQuotas(data, quotas); diff --git a/tests/unit/codex-banked-reset-credits-5199.test.ts b/tests/unit/codex-banked-reset-credits-5199.test.ts index b831956bfda5..5bd15089d398 100644 --- a/tests/unit/codex-banked-reset-credits-5199.test.ts +++ b/tests/unit/codex-banked-reset-credits-5199.test.ts @@ -41,8 +41,8 @@ test.afterEach(() => { test("buildCodexUsageQuotas surfaces bankedResetCredits when present", () => { const { quotas, bankedResetCredits, rateLimitReachedType } = buildCodexUsageQuotas({ rate_limit: { - primary_window: { used_percent: 10 }, - secondary_window: { used_percent: 20 }, + primary_window: { limit_window_seconds: 18000, used_percent: 10 }, + secondary_window: { limit_window_seconds: 604800, used_percent: 20 }, }, rate_limit_reset_credits: { available_count: 3 }, rate_limit_reached_type: { type: "secondary_window" }, @@ -57,7 +57,7 @@ test("buildCodexUsageQuotas surfaces bankedResetCredits when present", () => { test("buildCodexUsageQuotas tolerates camelCase field shape", () => { const { bankedResetCredits, rateLimitReachedType } = buildCodexUsageQuotas({ - rateLimit: { primaryWindow: { usedPercent: 5 } }, + rateLimit: { primaryWindow: { limitWindowSeconds: 18000, usedPercent: 5 } }, rateLimitResetCredits: { availableCount: 7 }, rateLimitReachedType: "primary_window", }); @@ -69,8 +69,8 @@ test("buildCodexUsageQuotas tolerates camelCase field shape", () => { test("buildCodexUsageQuotas leaves bankedResetCredits undefined when absent (fail-open)", () => { const result = buildCodexUsageQuotas({ rate_limit: { - primary_window: { used_percent: 10 }, - secondary_window: { used_percent: 20 }, + primary_window: { limit_window_seconds: 18000, used_percent: 10 }, + secondary_window: { limit_window_seconds: 604800, used_percent: 20 }, }, }); @@ -84,7 +84,7 @@ test("buildCodexUsageQuotas leaves bankedResetCredits undefined when absent (fai test("buildCodexUsageQuotas never throws on a garbage rate_limit_reset_credits shape", () => { assert.doesNotThrow(() => { const result = buildCodexUsageQuotas({ - rate_limit: { primary_window: { used_percent: 1 } }, + rate_limit: { primary_window: { limit_window_seconds: 18000, used_percent: 1 } }, rate_limit_reset_credits: "not-an-object", rate_limit_reached_type: 12345, }); @@ -101,8 +101,8 @@ test("getCodexUsage threads bankedResetCredits through additively", async () => JSON.stringify({ plan_type: "plus", rate_limit: { - primary_window: { used_percent: 15 }, - secondary_window: { used_percent: 25 }, + primary_window: { limit_window_seconds: 18000, used_percent: 15 }, + secondary_window: { limit_window_seconds: 604800, used_percent: 25 }, }, rate_limit_reset_credits: { available_count: 2 }, }), @@ -123,8 +123,8 @@ test("getCodexUsage omits bankedResetCredits and stays intact when absent", asyn JSON.stringify({ plan_type: "plus", rate_limit: { - primary_window: { used_percent: 15 }, - secondary_window: { used_percent: 25 }, + primary_window: { limit_window_seconds: 18000, used_percent: 15 }, + secondary_window: { limit_window_seconds: 604800, used_percent: 25 }, }, }), { status: 200, headers: { "content-type": "application/json" } } @@ -146,8 +146,16 @@ test("fetchCodexQuota surfaces bankedResetCredits from the dual-window parser", new Response( JSON.stringify({ rate_limit: { - primary_window: { used_percent: 70, reset_after_seconds: 45 }, - secondary_window: { used_percent: 20, reset_after_seconds: 300 }, + primary_window: { + limit_window_seconds: 18000, + used_percent: 70, + reset_after_seconds: 45, + }, + secondary_window: { + limit_window_seconds: 604800, + used_percent: 20, + reset_after_seconds: 300, + }, }, rate_limit_reset_credits: { available_count: 4 }, rate_limit_reached_type: { type: "primary_window" }, @@ -180,8 +188,8 @@ test("fetchCodexQuota omits bankedResetCredits when the payload does not have it new Response( JSON.stringify({ rate_limit: { - primary_window: { used_percent: 30 }, - secondary_window: { used_percent: 10 }, + primary_window: { limit_window_seconds: 18000, used_percent: 30 }, + secondary_window: { limit_window_seconds: 604800, used_percent: 10 }, }, }), { status: 200, headers: { "content-type": "application/json" } } diff --git a/tests/unit/codex-paid-credits.test.ts b/tests/unit/codex-paid-credits.test.ts index 881291feb4da..622b064aa9f8 100644 --- a/tests/unit/codex-paid-credits.test.ts +++ b/tests/unit/codex-paid-credits.test.ts @@ -30,7 +30,10 @@ const credits = { hasCredits: true, unlimited: false, overageLimitReached: false test("Codex paid credits preserve Business availability without inventing a balance", () => { const parsed = buildCodexUsageQuotas({ credits: { has_credits: true, unlimited: false, overage_limit_reached: false, balance: null }, - rate_limit: { limit_reached: true, primary_window: { used_percent: 100 } }, + rate_limit: { + limit_reached: true, + primary_window: { used_percent: 100, limit_window_seconds: 18000 }, + }, rate_limit_reset_credits: { available_count: 3 }, }); assert.deepEqual(parsed.paidCredits, credits); diff --git a/tests/unit/codex-quota-fetcher.test.ts b/tests/unit/codex-quota-fetcher.test.ts index 0b2a218670d7..08a7d06b9b20 100644 --- a/tests/unit/codex-quota-fetcher.test.ts +++ b/tests/unit/codex-quota-fetcher.test.ts @@ -33,7 +33,10 @@ test("fetchCodexQuota returns null when no registered credentials exist", async test("fetchCodexQuota exposes available paid credits without changing exhausted subscription windows", async () => { globalThis.fetch = async () => Response.json({ - rate_limit: { limit_reached: true, primary_window: { used_percent: 100 } }, + rate_limit: { + limit_reached: true, + primary_window: { limit_window_seconds: 18000, used_percent: 100 }, + }, credits: { has_credits: true, unlimited: false, overage_limit_reached: false, balance: null }, }); const connectionId = "codex-paid-credits-fetch"; @@ -61,10 +64,12 @@ test("fetchCodexQuota can read credentials directly from the provided connection JSON.stringify({ rate_limit: { primary_window: { + limit_window_seconds: 18000, used_percent: 70, reset_after_seconds: 45, }, secondary_window: { + limit_window_seconds: 604800, used_percent: 20, reset_after_seconds: 300, }, @@ -108,10 +113,12 @@ test("fetchCodexQuota parses dual-window usage, forwards workspace headers, and JSON.stringify({ rate_limit: { primary_window: { + limit_window_seconds: 18000, used_percent: 80, reset_after_seconds: 60, }, secondary_window: { + limit_window_seconds: 604800, used_percent: 40, reset_at: Math.floor((Date.now() + 3600_000) / 1000), }, @@ -152,8 +159,16 @@ test("fetchCodexQuota evaluates normal and Spark windows independently by reques return new Response( JSON.stringify({ rate_limit: { - primary_window: { used_percent: 20, reset_after_seconds: 60 }, - secondary_window: { used_percent: 30, reset_after_seconds: 120 }, + primary_window: { + limit_window_seconds: 18000, + used_percent: 20, + reset_after_seconds: 60, + }, + secondary_window: { + limit_window_seconds: 604800, + used_percent: 30, + reset_after_seconds: 120, + }, }, additional_rate_limits: [ { @@ -161,8 +176,16 @@ test("fetchCodexQuota evaluates normal and Spark windows independently by reques limit_name: "GPT-5.3-Codex-Spark", metered_feature: "gpt_5_3_codex_spark", rate_limit: { - primary_window: { used_percent: 100, reset_after_seconds: 300 }, - secondary_window: { used_percent: 40, reset_after_seconds: 600 }, + primary_window: { + limit_window_seconds: 18000, + used_percent: 100, + reset_after_seconds: 300, + }, + secondary_window: { + limit_window_seconds: 604800, + used_percent: 40, + reset_after_seconds: 600, + }, }, }, ], @@ -255,7 +278,11 @@ test("registerCodexQuotaFetcher exposes Codex quota to preflight and monitor flo new Response( JSON.stringify({ rate_limit: { - primary_window: { used_percent: 100, reset_after_seconds: 90 }, + primary_window: { + limit_window_seconds: 18000, + used_percent: 100, + reset_after_seconds: 90, + }, }, }), { diff --git a/tests/unit/codex-quota-signal-normalization.test.ts b/tests/unit/codex-quota-signal-normalization.test.ts index 112088a3de3d..274716f310df 100644 --- a/tests/unit/codex-quota-signal-normalization.test.ts +++ b/tests/unit/codex-quota-signal-normalization.test.ts @@ -165,7 +165,7 @@ test("impossible legacy reset timestamps cannot create an absurd cooldown", () = for (const value of [undefined, null, "", "garbage", "12junk", true, -1, 101, [], {}]) { test(`malformed usage percentage ${JSON.stringify(value)} is not healthy`, async () => { - const window = { used_percent: value, reset_after_seconds: 60 }; + const window = { limit_window_seconds: 18000, used_percent: value, reset_after_seconds: 60 }; const payload = { rate_limit: { primary_window: window } }; assert.equal(await fetchPayload(payload), null); assert.deepEqual(buildCodexUsageQuotas(payload).quotas, {}); @@ -175,8 +175,12 @@ for (const value of [undefined, null, "", "garbage", "12junk", true, -1, 101, [] test("one malformed window cannot fabricate health or hide a valid exhausted sibling", async () => { const payload = { rate_limit: { - primary_window: { used_percent: "bad", reset_after_seconds: 60 }, - secondary_window: { used_percent: 100, reset_after_seconds: 600 }, + primary_window: { limit_window_seconds: 18000, used_percent: "bad", reset_after_seconds: 60 }, + secondary_window: { + limit_window_seconds: 604800, + used_percent: 100, + reset_after_seconds: 600, + }, }, }; const result = await fetchPayload(payload); @@ -191,7 +195,9 @@ test("one malformed window cannot fabricate health or hide a valid exhausted sib test("explicit zero and camelCase compatibility are evidence, not missing data", async () => { const result = await fetchPayload({ - rateLimit: { primaryWindow: { usedPercent: "0", resetAfterSeconds: 60 } }, + rateLimit: { + primaryWindow: { limitWindowSeconds: 18000, usedPercent: "0", resetAfterSeconds: 60 }, + }, }); assert.ok(result); assert.equal(result.percentUsed, 0); @@ -200,7 +206,10 @@ test("explicit zero and camelCase compatibility are evidence, not missing data", test("usage allowed=false is an explicit hard stop, even below the percentage threshold", async () => { const result = await fetchPayload({ - rate_limit: { allowed: false, primary_window: { used_percent: 20 } }, + rate_limit: { + allowed: false, + primary_window: { limit_window_seconds: 18000, used_percent: 20 }, + }, }); assert.equal(result?.limitReached, true); }); @@ -208,7 +217,10 @@ test("usage allowed=false is an explicit hard stop, even below the percentage th test("usage API projects explicit allowed=false as a hard stop", async () => { setCodexUsageFetchForTests(async () => Response.json({ - rate_limit: { allowed: false, primary_window: { used_percent: 20 } }, + rate_limit: { + allowed: false, + primary_window: { limit_window_seconds: 18000, used_percent: 20 }, + }, }) ); const result = await getCodexUsage("synthetic-usage-token"); @@ -217,14 +229,21 @@ test("usage API projects explicit allowed=false as a hard stop", async () => { test("usage string false is not a boolean hard stop", async () => { const result = await fetchPayload({ - rate_limit: { limit_reached: "false", primary_window: { used_percent: 20 } }, + rate_limit: { + limit_reached: "false", + primary_window: { limit_window_seconds: 18000, used_percent: 20 }, + }, }); assert.equal(result?.limitReached, false); }); for (const reset of ["12junk", -1, 1e20, Date.now() + 60_000]) { test(`usage reset_at is epoch seconds, not milliseconds or parseFloat ${reset}`, async () => { - const payload = { rate_limit: { primary_window: { used_percent: 100, reset_at: reset } } }; + const payload = { + rate_limit: { + primary_window: { limit_window_seconds: 18000, used_percent: 100, reset_at: reset }, + }, + }; const result = await fetchPayload(payload); assert.ok(result, "valid exhaustion evidence survives a malformed optional reset"); assert.equal(result.percentUsed, 1); @@ -235,7 +254,9 @@ for (const reset of ["12junk", -1, 1e20, Date.now() + 60_000]) { test("paid/unlimited credits and banked reset credits do not erase subscription exhaustion", async () => { const payload = { - rate_limit: { primary_window: { used_percent: 100, reset_after_seconds: 60 } }, + rate_limit: { + primary_window: { limit_window_seconds: 18000, used_percent: 100, reset_after_seconds: 60 }, + }, credits: { has_credits: true, unlimited: true, balance: "42" }, rate_limit_reset_credits: { available_count: 3 }, }; @@ -247,11 +268,17 @@ test("paid/unlimited credits and banked reset credits do not erase subscription test("normal and Spark usage stay independent when one scope is malformed", async () => { const payload = { - rate_limit: { primary_window: { used_percent: "bad" } }, + rate_limit: { primary_window: { limit_window_seconds: 18000, used_percent: "bad" } }, additional_rate_limits: [ { limit_id: "codex_bengalfox", - rate_limit: { primary_window: { used_percent: 100, reset_after_seconds: 60 } }, + rate_limit: { + primary_window: { + limit_window_seconds: 18000, + used_percent: 100, + reset_after_seconds: 60, + }, + }, }, ], }; diff --git a/tests/unit/codex-quota-window-identity-18330.test.ts b/tests/unit/codex-quota-window-identity-18330.test.ts new file mode 100644 index 000000000000..9cbccc4280a7 --- /dev/null +++ b/tests/unit/codex-quota-window-identity-18330.test.ts @@ -0,0 +1,200 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +const isolatedHome = fs.mkdtempSync(path.join(os.tmpdir(), "codex-weekly-18330-")); +process.env.HOME = isolatedHome; +process.env.DATA_DIR = path.join(isolatedHome, "data"); +process.env.DISABLE_SQLITE_AUTO_BACKUP = "true"; +process.env.OMNIROUTE_QUOTA_FETCH_MIN_INTERVAL_MS = "0"; + +const { fetchCodexQuota, unregisterCodexConnection } = + await import("../../open-sse/services/codexQuotaFetcher.ts"); +const { evaluateQuotaCutoff, preflightQuota, registerQuotaFetcher } = + await import("../../open-sse/services/quotaPreflight.ts"); +const { buildCodexUsageQuotas } = await import("../../open-sse/services/codexUsageQuotas.ts"); +const { projectCodexAccountPool } = await import("../../open-sse/services/codexAccount/index.ts"); +const { markQuotaHealthy } = await import("../../src/domain/quotaCacheState.ts"); +const { resetDbInstance } = await import("../../src/lib/db/core.ts"); +const originalFetch = globalThis.fetch; +const ids = new Set(); +const thresholds = { + resolveMinRemainingPercent: (name: string | null) => (name === "weekly" ? 10 : 0), + isConfiguredCutoff: (name: string | null) => name === "weekly", +}; +const window = (used: number, seconds?: number) => ({ + used_percent: used, + ...(seconds ? { limit_window_seconds: seconds } : {}), + reset_after_seconds: 600, +}); +async function readQuota(rate_limit: unknown, requestedModel?: string) { + const id = `identity-${ids.size}`; + ids.add(id); + globalThis.fetch = async () => Response.json({ rate_limit }); + const connection = { accessToken: "offline-fixture", providerSpecificData: {}, requestedModel }; + const quota = await fetchCodexQuota(id, connection); + assert.ok(quota); + return { id, quota, connection }; +} +test.afterEach(() => { + globalThis.fetch = originalFetch; +}); +test.after(() => { + for (const id of ids) unregisterCodexConnection(id); + resetDbInstance(); + fs.rmSync(isolatedHome, { recursive: true, force: true }); +}); + +for (const used of [94, 90, 85]) { + test(`weekly-only primary ${used}% enforces weekly cutoff after recent success`, async () => { + const { id, quota } = await readQuota({ primary_window: window(used, 604800) }); + markQuotaHealthy(id); + assert.equal( + evaluateQuotaCutoff(quota, thresholds, { provider: "codex", connectionId: id }).proceed, + used < 90 + ); + assert.equal(quota.windows?.weekly?.percentUsed, used / 100); + assert.equal(quota.windows?.session, undefined); + assert.equal(quota.window5h, undefined); + assert.equal(quota.window7d?.percentUsed, used / 100); + }); +} +test("reversed windows preserve their duration identity", async () => { + const { quota } = await readQuota({ + primary_window: window(94, 604800), + secondary_window: window(20, 18000), + }); + assert.equal(quota.windows?.weekly?.percentUsed, 0.94); + assert.equal(quota.windows?.session?.percentUsed, 0.2); + assert.equal(quota.window5h?.percentUsed, 0.2); + assert.equal(quota.window7d?.percentUsed, 0.94); +}); +for (const duration of [undefined, 3600]) { + test(`unknown or unsupported duration ${duration} does not fabricate fixed windows`, async () => { + const { quota } = await readQuota({ primary_window: window(100, duration) }); + assert.equal(quota.window5h, undefined); + assert.equal(quota.window7d, undefined); + assert.equal(quota.windows?.session, undefined); + assert.equal(quota.windows?.weekly, undefined); + assert.equal(quota.percentUsed, 1, "unknown duration keeps valid exhaustion evidence"); + }); +} +test("weekly usage badge and account pool agree without a synthetic 5h row", () => { + const { quotas } = buildCodexUsageQuotas({ rate_limit: { primary_window: window(94, 604800) } }); + assert.equal(quotas.weekly?.used, 94); + assert.equal(quotas.session, undefined); + const pool = projectCodexAccountPool( + { id: "offline-pool", provider: "codex", providerSpecificData: {} }, + Date.now(), + { quotas, fetchedAt: new Date().toISOString() } + ); + const child = pool.children.find((child) => child.key.scope === "codex"); + assert.equal(child?.quota.windows["7d"]?.usedPercentage, 94); + assert.equal(child?.quota.windows["5h"], null); +}); +for (const enabled of [false, true]) { + test(`explicit weekly cutoff is evaluated when preflight runs with legacy flag ${enabled}`, async () => { + const { id, quota, connection } = await readQuota({ primary_window: window(94, 604800) }); + registerQuotaFetcher("codex", async () => quota); + const result = await preflightQuota( + "codex", + id, + { ...connection, providerSpecificData: { quotaPreflightEnabled: enabled } }, + thresholds + ); + assert.equal(result.proceed, false); + assert.equal(result.windowName, "weekly"); + }); +} + +const { parseCodexQuotaHeaders } = await import("../../open-sse/executors/codex/quota.ts"); +for (const minutes of [undefined, "60", "bad"]) { + test(`official headers with duration ${minutes} do not fabricate 5h/7d`, () => { + const headers: Record = { "x-codex-primary-used-percent": "94" }; + if (minutes !== undefined) headers["x-codex-primary-window-minutes"] = minutes; + assert.equal(parseCodexQuotaHeaders(headers), null); + }); +} +test("alternate duration keys preserve weekly classification and Spark scope", async () => { + globalThis.fetch = async () => + Response.json({ + rateLimit: { primaryWindow: { usedPercent: 20, windowSeconds: 18000 } }, + additionalRateLimits: [ + { + limitId: "codex_bengalfox", + rateLimit: { + primaryWindow: { usedPercent: 94, windowSeconds: "604800", resetAfterSeconds: 600 }, + }, + }, + ], + }); + const id = "identity-spark-alias"; + ids.add(id); + const quota = await fetchCodexQuota(id, { + accessToken: "offline", + requestedModel: "gpt-5.3-codex-spark", + }); + assert.equal(quota?.windows?.gpt_5_3_codex_spark_weekly?.percentUsed, 0.94); + assert.equal(quota?.windows?.gpt_5_3_codex_spark_session, undefined); + assert.equal(quota?.windows?.session, undefined); + const display = buildCodexUsageQuotas({ + additionalRateLimits: [ + { + limitId: "codex_bengalfox", + rateLimit: { primaryWindow: { usedPercent: 94, windowSeconds: "604800" } }, + }, + ], + }).quotas; + assert.equal(display.gpt_5_3_codex_spark_weekly?.used, 94); +}); + +for (const duration of [undefined, 3600]) { + test(`unknown usage duration ${duration} cannot hydrate the pool 5h or weekly slots`, () => { + const { quotas } = buildCodexUsageQuotas({ + rate_limit: { primary_window: window(94, duration) }, + }); + assert.equal(quotas.session, undefined); + assert.equal(quotas.weekly, undefined); + assert.equal(quotas.unknown_primary.used, 94); + const pool = projectCodexAccountPool( + { id: "offline-unknown", provider: "codex", providerSpecificData: {} }, + Date.now(), + { quotas, fetchedAt: new Date().toISOString() } + ); + for (const child of pool.children) { + assert.equal(child.quota.windows["5h"], null); + assert.equal(child.quota.windows["7d"], null); + } + }); +} +test("equal usage keeps the weekly dominant reset for reversed windows", async () => { + const resetWeekly = Math.floor(Date.now() / 1000) + 5000; + const { quota } = await readQuota({ + primary_window: { ...window(90, 604800), reset_at: resetWeekly }, + secondary_window: window(90, 18000), + }); + assert.equal(quota.resetAt, new Date(resetWeekly * 1000).toISOString()); +}); + +test("public cooldown and scoring consumers tolerate absent fixed windows", async () => { + const { getCodexQuotaCooldownMs } = await import("../../open-sse/services/codexQuotaFetcher.ts"); + const { resolveQuotaWindowByName, resolveResetAwareConfig } = + await import("../../open-sse/services/combo/quotaScoring.ts"); + const { scoreQuotaPace } = await import("../../open-sse/services/combo/quotaPaceScoring.ts"); + const { quota } = await readQuota({ primary_window: window(100) }); + assert.equal( + getCodexQuotaCooldownMs(quota), + 0, + "no fixed-window cooldown is invented for an unknown duration" + ); + assert.equal(resolveQuotaWindowByName(quota, "session"), null); + assert.equal(resolveQuotaWindowByName(quota, "weekly"), null); + assert.equal(scoreQuotaPace(quota, resolveResetAwareConfig({}), Date.now()).score, 0); + assert.equal( + evaluateQuotaCutoff(quota).proceed, + false, + "known exhaustion still reaches generic cutoff fallback" + ); +}); diff --git a/tests/unit/codex-usage-quotas-latent-window.test.ts b/tests/unit/codex-usage-quotas-latent-window.test.ts index 78a5c1b56f2f..0c6402d7bf4d 100644 --- a/tests/unit/codex-usage-quotas-latent-window.test.ts +++ b/tests/unit/codex-usage-quotas-latent-window.test.ts @@ -12,15 +12,14 @@ * Separately, the window display label must follow the real window duration * (`limit_window_seconds`) rather than assuming primary=session / secondary= * weekly by position — so a 7-day primary_window is labeled "Weekly", not - * "Session". The internal `session`/`weekly` keys (routing semantics) are - * unchanged; only the display label is corrected. + * "Session". Routing keys now follow the same duration identity (#18326). */ import test from "node:test"; import assert from "node:assert/strict"; import { buildCodexUsageQuotas } from "../../open-sse/services/codexUsageQuotas.ts"; import { - CODEX_SPARK_QUOTA_SESSION, + CODEX_SPARK_QUOTA_WEEKLY, CODEX_SPARK_DISPLAY_NAME, } from "../../open-sse/config/codexQuotaScopes.ts"; @@ -47,8 +46,8 @@ test("omits a latent, never-used spark window (used 0% + full-window reset)", () }, ], }); - assert.equal(quotas[CODEX_SPARK_QUOTA_SESSION], undefined, "latent spark window must be hidden"); - assert.ok(quotas.session, "the main session window is still present"); + assert.equal(quotas[CODEX_SPARK_QUOTA_WEEKLY], undefined, "latent spark window must be hidden"); + assert.ok(quotas.weekly, "the main weekly window is still present"); }); test("includes the spark window once it has actually been used", () => { @@ -70,8 +69,8 @@ test("includes the spark window once it has actually been used", () => { }, ], }); - assert.ok(quotas[CODEX_SPARK_QUOTA_SESSION], "used spark window must be shown"); - assert.equal(quotas[CODEX_SPARK_QUOTA_SESSION].used, 12); + assert.ok(quotas[CODEX_SPARK_QUOTA_WEEKLY], "used spark window must be shown"); + assert.equal(quotas[CODEX_SPARK_QUOTA_WEEKLY].used, 12); }); test("spark display name comes from the payload limit_name, falling back to the constant", () => { @@ -81,12 +80,20 @@ test("spark display name comes from the payload limit_name, falling back to the limit_name: "GPT-9.9-Codex-Nova", metered_feature: "codex_bengalfox", rate_limit: { - primary_window: { used_percent: 5, limit_window_seconds: WEEK, reset_after_seconds: 1000 }, + primary_window: { + used_percent: 5, + limit_window_seconds: WEEK, + reset_after_seconds: 1000, + }, }, }, ], - }).quotas[CODEX_SPARK_QUOTA_SESSION]; - assert.equal(withName?.displayName, "GPT-9.9-Codex-Nova", "uses payload limit_name"); + }).quotas[CODEX_SPARK_QUOTA_WEEKLY]; + assert.equal( + withName?.displayName, + "GPT-9.9-Codex-Nova Weekly", + "uses payload limit_name and duration" + ); const withoutName = buildCodexUsageQuotas({ additional_rate_limits: [ @@ -94,12 +101,20 @@ test("spark display name comes from the payload limit_name, falling back to the metered_feature: "codex_bengalfox", model: "codex-spark", rate_limit: { - primary_window: { used_percent: 5, limit_window_seconds: WEEK, reset_after_seconds: 1000 }, + primary_window: { + used_percent: 5, + limit_window_seconds: WEEK, + reset_after_seconds: 1000, + }, }, }, ], - }).quotas[CODEX_SPARK_QUOTA_SESSION]; - assert.equal(withoutName?.displayName, CODEX_SPARK_DISPLAY_NAME, "falls back to constant"); + }).quotas[CODEX_SPARK_QUOTA_WEEKLY]; + assert.equal( + withoutName?.displayName, + `${CODEX_SPARK_DISPLAY_NAME} Weekly`, + "falls back to constant" + ); }); test("labels a 7-day primary_window as Weekly, not Session", () => { @@ -109,8 +124,8 @@ test("labels a 7-day primary_window as Weekly, not Session", () => { secondary_window: null, }, }); - assert.ok(quotas.session, "primary window is keyed 'session' (routing semantics unchanged)"); - assert.equal(quotas.session.displayName, "Weekly", "but displayed as Weekly by real duration"); + assert.ok(quotas.weekly, "weekly primary carries the weekly routing key"); + assert.equal(quotas.session, undefined, "weekly primary never fabricates a session"); }); test("leaves a genuine 5h session window with the default label", () => { diff --git a/tests/unit/codex-usage-quotas-review-window.test.ts b/tests/unit/codex-usage-quotas-review-window.test.ts index 27d796a248a7..3bdc275d54fc 100644 --- a/tests/unit/codex-usage-quotas-review-window.test.ts +++ b/tests/unit/codex-usage-quotas-review-window.test.ts @@ -53,7 +53,7 @@ test("buildCodexUsageQuotas surfaces the secondary code_review window", () => { test("buildCodexUsageQuotas reads review windows from additional_rate_limits", () => { const { quotas } = buildCodexUsageQuotas({ rate_limit: { - primary_window: { used_percent: 10 }, + primary_window: { used_percent: 10, limit_window_seconds: 18000 }, }, additional_rate_limits: [ { @@ -76,8 +76,8 @@ test("buildCodexUsageQuotas reads review windows from additional_rate_limits", ( test("buildCodexUsageQuotas leaves review windows undefined when payload is silent", () => { const { quotas } = buildCodexUsageQuotas({ rate_limit: { - primary_window: { used_percent: 10 }, - secondary_window: { used_percent: 20 }, + primary_window: { used_percent: 10, limit_window_seconds: 18000 }, + secondary_window: { used_percent: 20, limit_window_seconds: 604800 }, }, }); diff --git a/tests/unit/codex-usage-windows.test.ts b/tests/unit/codex-usage-windows.test.ts index e45c51c56224..3106fdd2b9c2 100644 --- a/tests/unit/codex-usage-windows.test.ts +++ b/tests/unit/codex-usage-windows.test.ts @@ -47,6 +47,6 @@ describe("Codex usage windows", () => { }); assert.equal(quotas.session.windowSeconds, 18_000); - assert.equal(quotas.weekly.windowSeconds, null); + assert.equal(quotas.unknown_secondary.windowSeconds, null); }); }); diff --git a/tests/unit/codex-weekly-routing-18330.test.ts b/tests/unit/codex-weekly-routing-18330.test.ts new file mode 100644 index 000000000000..a6e10e88f996 --- /dev/null +++ b/tests/unit/codex-weekly-routing-18330.test.ts @@ -0,0 +1,132 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +const isolatedHome = fs.mkdtempSync(path.join(os.tmpdir(), "codex-weekly-18330-")); +process.env.HOME = isolatedHome; +process.env.DATA_DIR = path.join(isolatedHome, "data"); +process.env.DISABLE_SQLITE_AUTO_BACKUP = "true"; +process.env.OMNIROUTE_QUOTA_FETCH_MIN_INTERVAL_MS = "0"; + +const core = await import("../../src/lib/db/core.ts"); +const providers = await import("../../src/lib/db/providers.ts"); +const settings = await import("../../src/lib/db/settings.ts"); +const auth = await import("../../src/sse/services/auth.ts"); +const cache = await import("../../src/domain/quotaCache.ts"); +const { fetchCodexQuota, unregisterCodexConnection } = + await import("../../open-sse/services/codexQuotaFetcher.ts"); +const { registerQuotaFetcher } = await import("../../open-sse/services/quotaPreflight.ts"); +const originalFetch = globalThis.fetch; +const connections: string[] = []; +const usage = new Map(); +const calls: string[] = []; + +async function seed(used: number, priority: number, extra: Record = {}) { + const connection = await providers.createProviderConnection({ + provider: "codex", + authType: "oauth", + isActive: true, + testStatus: "active", + name: `weekly-${used}-${priority}`, + accessToken: `fixture-${used}-${priority}`, + priority, + quotaWindowThresholds: { weekly: 10 }, + providerSpecificData: { + codexLimitPolicy: { use5h: false, useWeekly: true }, + allowPaidCredits: false, + ...extra, + }, + }); + connections.push(connection.id); + usage.set(connection.id, used); + cache.markQuotaHealthy(connection.id); + return connection; +} +test.beforeEach(async () => { + for (const id of connections.splice(0)) { + unregisterCodexConnection(id); + await providers.deleteProviderConnection(id); + } + cache.__clearForTests(); + calls.length = 0; + registerQuotaFetcher("codex", async (id, connection) => { + calls.push(id); + globalThis.fetch = async () => + Response.json({ + rate_limit: { + primary_window: { + used_percent: usage.get(id), + limit_window_seconds: 604800, + reset_after_seconds: 600, + }, + }, + credits: { has_credits: true, unlimited: false, balance: "20" }, + }); + return fetchCodexQuota(id, connection); + }); +}); +test.after(() => { + globalThis.fetch = originalFetch; + for (const id of connections) unregisterCodexConnection(id); + core.resetDbInstance(); + fs.rmSync(isolatedHome, { recursive: true, force: true }); +}); + +for (const enabled of [false, true]) { + test(`routing honors per-account weekly cutoff with global preflight ${enabled}`, async () => { + await settings.updateSettings({ + fallbackStrategy: "priority", + resilienceSettings: { quotaPreflight: { enabled } }, + }); + const low = await seed(94, 1); + const healthy = await seed(85, 2); + const selected = await auth.getProviderCredentialsWithQuotaPreflight( + "codex", + null, + null, + "gpt-5.5" + ); + assert.equal(selected?.connectionId, healthy.id); + assert.deepEqual(calls, [low.id, healthy.id]); + selected?.releaseOAuthSession?.(); + }); +} +test("all accounts at or below the weekly minimum return no eligible account", async () => { + const low = await seed(94, 1); + const boundary = await seed(90, 2); + const selected = await auth.getProviderCredentialsWithQuotaPreflight( + "codex", + null, + null, + "gpt-5.5" + ); + assert.equal(selected?.allRateLimited, true); + assert.equal(selected?.connectionId, undefined); + assert.deepEqual(calls, [low.id, boundary.id]); +}); +test("explicit connection preflight opt-out keeps its documented bypass", async () => { + const low = await seed(94, 1, { quotaPreflightEnabled: false }); + const selected = await auth.getProviderCredentialsWithQuotaPreflight( + "codex", + null, + null, + "gpt-5.5" + ); + assert.equal(selected?.connectionId, low.id); + assert.deepEqual(calls, []); + selected?.releaseOAuthSession?.(); +}); +test("explicit paid-credit consent can use credits below subscription cutoff", async () => { + const paid = await seed(94, 1, { allowPaidCredits: true }); + const selected = await auth.getProviderCredentialsWithQuotaPreflight( + "codex", + null, + null, + "gpt-5.5" + ); + assert.equal(selected?.connectionId, paid.id); + assert.deepEqual(calls, [paid.id]); + selected?.releaseOAuthSession?.(); +}); diff --git a/tests/unit/headroom-codex-quota-snapshot-6379.test.ts b/tests/unit/headroom-codex-quota-snapshot-6379.test.ts index 85ac9fa922e0..588b00ad0ce6 100644 --- a/tests/unit/headroom-codex-quota-snapshot-6379.test.ts +++ b/tests/unit/headroom-codex-quota-snapshot-6379.test.ts @@ -83,8 +83,8 @@ function target(connectionId: string) { function usageResponse(primaryUsedPercent: number, secondaryUsedPercent: number) { return { rate_limit: { - primary_window: { used_percent: primaryUsedPercent }, - secondary_window: { used_percent: secondaryUsedPercent }, + primary_window: { limit_window_seconds: 18000, used_percent: primaryUsedPercent }, + secondary_window: { limit_window_seconds: 604800, used_percent: secondaryUsedPercent }, }, }; } diff --git a/tests/unit/quota-window-label.test.ts b/tests/unit/quota-window-label.test.ts index 83b7b8e7405f..2d9e657946ab 100644 --- a/tests/unit/quota-window-label.test.ts +++ b/tests/unit/quota-window-label.test.ts @@ -47,9 +47,9 @@ test("buildCodexUsageQuotas labels a 30-day primary window Monthly, not Weekly", }, }, }); - assert.ok(quotas.session); - assert.equal(quotas.session.displayName, "Monthly"); - assert.equal(quotas.session.windowSeconds, 2_592_000); + assert.ok(quotas.unknown_primary); + assert.equal(quotas.unknown_primary.displayName, "Monthly"); + assert.equal(quotas.unknown_primary.windowSeconds, 2_592_000); }); test("AUTH reasons match usage duration labels for a 30-day Codex session window", () => { From 7f55b1435610a17d5848d5cbff07d194593cd4c4 Mon Sep 17 00:00:00 2001 From: shevar0 <3235212+shevar0@users.noreply.github.com> Date: Tue, 27 Oct 2026 14:46:15 -0300 Subject: [PATCH 4/8] fix(sse): honor Codex quota tracking across routing gates (#18330) Share the session and weekly toggle policy between live preflight, cached cutoffs and final quota exhaustion checks without discarding unknown windows. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --- .../fixes/18330-codex-dual-window-policy.md | 1 + open-sse/services/quotaPreflight.ts | 9 +- src/domain/quotaCache.ts | 16 +- src/lib/providers/codexQuotaWindowPolicy.ts | 56 +++++ src/sse/services/auth.ts | 39 +-- .../codex-dual-toggle-policy-18330.test.ts | 231 ++++++++++++++++++ .../codex-dual-toggle-routing-18330.test.ts | 206 ++++++++++++++++ 7 files changed, 513 insertions(+), 45 deletions(-) create mode 100644 changelog.d/fixes/18330-codex-dual-window-policy.md create mode 100644 src/lib/providers/codexQuotaWindowPolicy.ts create mode 100644 tests/unit/codex-dual-toggle-policy-18330.test.ts create mode 100644 tests/unit/codex-dual-toggle-routing-18330.test.ts diff --git a/changelog.d/fixes/18330-codex-dual-window-policy.md b/changelog.d/fixes/18330-codex-dual-window-policy.md new file mode 100644 index 000000000000..e2f40fa8f2e2 --- /dev/null +++ b/changelog.d/fixes/18330-codex-dual-window-policy.md @@ -0,0 +1 @@ +- Honor Codex session and weekly tracking toggles consistently in cached routing and live quota preflight, including explicit cutoffs, while retaining unknown-window exhaustion and paid-credit controls (#18330). diff --git a/open-sse/services/quotaPreflight.ts b/open-sse/services/quotaPreflight.ts index 7301ad4dd66a..235990f2632d 100644 --- a/open-sse/services/quotaPreflight.ts +++ b/open-sse/services/quotaPreflight.ts @@ -21,6 +21,7 @@ import { isCompatibleProviderConnectionId } from "@/shared/utils/compatibleProviderId"; import { isFeatureFlagEnabled } from "@/shared/utils/featureFlags"; import { isClaudeExtraUsageAllowed } from "@/lib/providers/claudeExtraUsage"; +import { filterCodexQuotaWindows } from "@/lib/providers/codexQuotaWindowPolicy"; import { isCodexQuotaFilteringDisabled } from "@/lib/providers/codexQuotaFiltering"; // #14359 — import the leaf, NOT "@/domain/quotaCache": quotaCache → usage.ts → usage/openrouter.ts → // openrouterQuotaFetcher.ts → this file, so importing quotaCache here closes an ESM init cycle @@ -250,6 +251,9 @@ function windowsForScope( windows: NonNullable, scope?: QuotaCutoffScope ): NonNullable { + if (scope?.provider === "codex") { + return filterCodexQuotaWindows(windows, scope.providerSpecificData); + } if (!scope?.requestedModel || !isAntigravityQuotaProvider(scope.provider ?? null)) { return windows; } @@ -413,7 +417,10 @@ function windowedQuotaCutoffResult( if (isAntigravityQuotaProvider(scope?.provider ?? null) && scope?.requestedModel) { return { proceed: true, quotaPercent: quota.percentUsed }; } - if (quota.limitReached === true) return limitReachedResult(quota); + // An aggregate signal cannot re-enable a Codex window explicitly removed by its toggle. + if (quota.limitReached === true && !(scope?.provider === "codex" && scopedWindows !== windows)) { + return limitReachedResult(quota); + } return { proceed: true, quotaPercent: quota.percentUsed }; } diff --git a/src/domain/quotaCache.ts b/src/domain/quotaCache.ts index 0d1e5e753054..29d8b2d55c28 100644 --- a/src/domain/quotaCache.ts +++ b/src/domain/quotaCache.ts @@ -40,6 +40,7 @@ import { } from "@omniroute/open-sse/services/codexAccount/index.ts"; import { selectAntigravityQuotaWindowNames } from "@omniroute/open-sse/services/antigravityQuotaFamily.ts"; import { isClaudeExtraUsageAllowed } from "@/lib/providers/claudeExtraUsage"; +import { filterCodexQuotaWindows } from "@/lib/providers/codexQuotaWindowPolicy"; import { isCodexQuotaFilteringDisabled } from "@/lib/providers/codexQuotaFiltering"; import { resolveProviderId } from "@/shared/constants/providers"; import { @@ -433,13 +434,16 @@ export function hydrateCodexQuotaCacheForRequest( function isCodexQuotaExhausted( connectionId: string, entry: QuotaCacheEntry, - requestedModel: string | null + requestedModel: string | null, + providerSpecificData: unknown ): boolean { - if (!requestedModel) return entry.exhausted; - const quotaNames = Object.keys(entry.quotas || {}); - if (quotaNames.length === 0) return entry.exhausted; + if (Object.keys(entry.quotas).length === 0) return entry.exhausted; + const windows = filterCodexQuotaWindows(entry.quotas, providerSpecificData); + if (!requestedModel && windows === entry.quotas) return entry.exhausted; const filterWindow = getCodexQuotaWindowFilterForModel(requestedModel); - const scopedWindowNames = quotaNames.filter((windowName) => filterWindow?.(windowName)); + const scopedWindowNames = Object.keys(windows).filter( + (name) => !filterWindow || filterWindow(name) + ); return ( scopedWindowNames.length > 0 && scopedWindowNames.every( @@ -707,7 +711,7 @@ export function isQuotaExhaustedForRequest( } if (provider === "codex") { - return isCodexQuotaExhausted(connectionId, entry, requestedModel); + return isCodexQuotaExhausted(connectionId, entry, requestedModel, providerSpecificData); } if (resolveProviderId(provider) === "claude") { diff --git a/src/lib/providers/codexQuotaWindowPolicy.ts b/src/lib/providers/codexQuotaWindowPolicy.ts new file mode 100644 index 000000000000..cabc7650a6cc --- /dev/null +++ b/src/lib/providers/codexQuotaWindowPolicy.ts @@ -0,0 +1,56 @@ +import { toCodexBaseQuotaWindowName } from "@omniroute/open-sse/config/codexQuotaScopes.ts"; + +function asRecord(value: unknown): Record { + return value && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : {}; +} + +function getCodexLimitPolicy(providerSpecificData: unknown) { + const policy = asRecord(asRecord(providerSpecificData).codexLimitPolicy); + return { use5h: policy.use5h !== false, useWeekly: policy.useWeekly !== false }; +} + +function normalizeCodexWindowName(windowName: string): string { + const normalized = windowName.trim().toLowerCase(); + if (["session (5h)", "5h", "five_hour"].includes(normalized)) return "session"; + if (["weekly (7d)", "7d", "seven_day"].includes(normalized)) return "weekly"; + return toCodexBaseQuotaWindowName(normalized) ?? normalized; +} + +function isWindowEnabled(windowName: string, policy: ReturnType) { + const normalized = normalizeCodexWindowName(windowName); + if (normalized === "session") return policy.use5h; + if (normalized === "weekly") return policy.useWeekly; + return true; +} + +/** Shared tracking policy: disabled known windows stay disabled even with explicit cutoffs. */ +export function applyCodexWindowPolicy( + rawWindows: string[], + providerSpecificData: unknown +): string[] { + const policy = getCodexLimitPolicy(providerSpecificData); + const windows = rawWindows + .map(normalizeCodexWindowName) + .filter(Boolean) + .filter((name) => isWindowEnabled(name, policy)); + if (policy.use5h) windows.push("session"); + if (policy.useWeekly) windows.push("weekly"); + return [...new Set(windows)]; +} + +/** + * Filter measured windows without renaming Spark keys or guessing unknown durations. + * Preserve the input reference when nothing was excluded: the preflight uses that + * distinction to retain unattributed upstream exhaustion unless a known window was disabled. + */ +export function filterCodexQuotaWindows( + windows: Record, + providerSpecificData: unknown +): Record { + const policy = getCodexLimitPolicy(providerSpecificData); + const entries = Object.entries(windows); + const selected = entries.filter(([name]) => isWindowEnabled(name, policy)); + return selected.length === entries.length ? windows : Object.fromEntries(selected); +} diff --git a/src/sse/services/auth.ts b/src/sse/services/auth.ts index 39687d0690f3..5601843ad61f 100644 --- a/src/sse/services/auth.ts +++ b/src/sse/services/auth.ts @@ -25,6 +25,7 @@ import { touchConnectionLastUsed, clearConnectionErrorIfUnchanged, } from "@/lib/db/providers"; +import { applyCodexWindowPolicy } from "@/lib/providers/codexQuotaWindowPolicy"; import { getDbInstance } from "@/lib/db/core"; import { getRecentEgressIpForConnection, EGRESS_IP_LOOKUP_WINDOW_MS } from "@/lib/db/proxyLogs"; import { validateApiKey } from "@/lib/db/apiKeys"; @@ -256,16 +257,6 @@ function toStringOrNull(value: unknown): string | null { function toBooleanOrDefault(value: unknown, fallback: boolean): boolean { return typeof value === "boolean" ? value : fallback; } -function getCodexLimitPolicy(providerSpecificData: JsonRecord): { - use5h: boolean; - useWeekly: boolean; -} { - const policy = asRecord(providerSpecificData.codexLimitPolicy); - return { - use5h: toBooleanOrDefault(policy.use5h, true), - useWeekly: toBooleanOrDefault(policy.useWeekly, true), - }; -} interface QuotaLimitPolicy { enabled: boolean; thresholdPercent: number; @@ -295,34 +286,6 @@ function normalizeWindowName(windowName: unknown): string | null { 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 toCodexBaseQuotaWindowName(normalized); -} -function applyCodexWindowPolicy(rawWindows: string[], providerSpecificData: JsonRecord): string[] { - const codexPolicy = getCodexLimitPolicy(providerSpecificData); - 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 uniqueWindows(windows); -} function normalizeStatus(value: string | null): string { return (value || "").trim().toLowerCase(); } diff --git a/tests/unit/codex-dual-toggle-policy-18330.test.ts b/tests/unit/codex-dual-toggle-policy-18330.test.ts new file mode 100644 index 000000000000..c52071de78c7 --- /dev/null +++ b/tests/unit/codex-dual-toggle-policy-18330.test.ts @@ -0,0 +1,231 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +const home = fs.mkdtempSync(path.join(os.tmpdir(), "codex-dual-toggle-")); +process.env.HOME = home; +process.env.DATA_DIR = path.join(home, "data"); +process.env.DISABLE_SQLITE_AUTO_BACKUP = "true"; +const { evaluateQuotaCutoff, preflightQuota, registerQuotaFetcher } = + await import("../../open-sse/services/quotaPreflight.ts"); +const { markQuotaHealthy, unmarkQuotaHealthy } = + await import("../../src/domain/quotaCacheState.ts"); +const { resolveQuotaLimitPolicy } = await import("../../src/sse/services/auth.ts"); +const core = await import("../../src/lib/db/core.ts"); +const resetAt = new Date(Date.now() + 600_000).toISOString(); +const thresholds = { resolveMinRemainingPercent: () => 10, isConfiguredCutoff: () => true }; +function quota(windows: Record, limitReached = false) { + return { + used: 100, + total: 100, + percentUsed: Math.max(...Object.values(windows)), + limitReached, + windows: Object.fromEntries( + Object.entries(windows).map(([name, percentUsed]) => [name, { percentUsed, resetAt }]) + ), + }; +} +function scope(use5h: boolean, useWeekly: boolean, extra: Record = {}) { + return { + provider: "codex", + requestedModel: "gpt-5.5", + providerSpecificData: { + codexLimitPolicy: { use5h, useWeekly }, + allowPaidCredits: false, + ...extra, + }, + }; +} +test.after(() => { + core.resetDbInstance(); + fs.rmSync(home, { recursive: true, force: true }); +}); + +test("dual5h100 weekly85 with tracking5hOFF weeklyON should remain eligible", () => { + assert.equal( + evaluateQuotaCutoff(quota({ session: 1, weekly: 0.85 }), thresholds, scope(false, true)) + .proceed, + true + ); +}); +test("disabled window cannot return through aggregate limitReached", () => { + assert.equal( + evaluateQuotaCutoff(quota({ session: 1, weekly: 0.85 }, true), thresholds, scope(false, true)) + .proceed, + true + ); +}); +for (const use5h of [false, true]) { + for (const useWeekly of [false, true]) { + for (const [session, weekly] of [ + [1, 0.85], + [0.85, 1], + [1, 1], + [0.85, 0.85], + ]) { + test(`explicit cutoffs respect toggles ${use5h}/${useWeekly} at used ${session}/${weekly}`, () => { + const actual = evaluateQuotaCutoff( + quota({ session, weekly }), + thresholds, + scope(use5h, useWeekly) + ); + assert.equal(actual.proceed, !((use5h && session >= 0.9) || (useWeekly && weekly >= 0.9))); + }); + } + } +} +for (const healthy of [false, true]) { + for (const [used, allowed] of [ + [0.89, true], + [0.9, false], + [0.94, false], + ] as const) { + test(`weekly cutoff boundary ${used}, recent success ${healthy}`, () => { + unmarkQuotaHealthy("dual-healthy"); + if (healthy) markQuotaHealthy("dual-healthy"); + const actual = evaluateQuotaCutoff(quota({ session: 1, weekly: used }), thresholds, { + ...scope(false, true), + connectionId: "dual-healthy", + }); + assert.equal(actual.proceed, allowed); + }); + } +} +test("both toggles off do not suppress exhausted unknown or custom feature windows", () => { + for (const key of [ + "unknown_primary", + "gpt_5_3_codex_spark_unknown_primary", + "code_review", + "monthly", + ]) { + const actual = evaluateQuotaCutoff( + quota({ session: 1, weekly: 1, [key]: 1 }, true), + thresholds, + scope(false, false) + ); + assert.equal(actual.proceed, false); + assert.equal(actual.windowName, key); + } +}); +test("unattributed exhaustion remains enforced without removed windows", () => { + for (const windows of [{ weekly: 0.85 }, { unknown_primary: 0.85 }]) { + assert.equal( + evaluateQuotaCutoff(quota(windows, true), thresholds, scope(false, true)).proceed, + false + ); + } + assert.equal( + evaluateQuotaCutoff( + { used: 0, total: 100, percentUsed: 0, limitReached: true }, + thresholds, + scope(false, false) + ).proceed, + false + ); + assert.equal( + evaluateQuotaCutoff({ used: 100, total: 100, percentUsed: 1 }, thresholds, scope(false, false)) + .proceed, + false + ); +}); +test("both disabled known windows do not fall back to aggregate exhaustion", () => { + assert.equal( + evaluateQuotaCutoff(quota({ session: 1, weekly: 1 }, true), thresholds, scope(false, false)) + .proceed, + true + ); +}); +test("Spark windows honor the same toggles without dropping their identity", () => { + const spark = { ...scope(false, true), requestedModel: "gpt-5.3-codex-spark" }; + assert.equal( + evaluateQuotaCutoff( + quota({ gpt_5_3_codex_spark_session: 1, gpt_5_3_codex_spark_weekly: 0.85 }, true), + thresholds, + spark + ).proceed, + true + ); + const blocked = evaluateQuotaCutoff( + quota({ gpt_5_3_codex_spark_session: 0.85, gpt_5_3_codex_spark_weekly: 1 }), + thresholds, + spark + ); + assert.equal(blocked.proceed, false); + assert.equal(blocked.windowName, "gpt_5_3_codex_spark_weekly"); +}); +test("only explicit false disables tracking, malformed policy stays default-on", () => { + for (const codexLimitPolicy of [null, [], "false", { use5h: "false", useWeekly: 0 }]) { + assert.equal( + evaluateQuotaCutoff( + quota({ session: 1 }), + thresholds, + scope(true, true, { codexLimitPolicy }) + ).proceed, + false + ); + } +}); +test("cached policy preserves custom windows and excludes disabled aliases", () => { + const policy = resolveQuotaLimitPolicy("codex", { + codexLimitPolicy: { use5h: false, useWeekly: true }, + limitPolicy: { + windows: ["Session (5h)", "5h", "gpt_5_3_codex_spark_session", "unknown_primary", "monthly"], + }, + }); + assert.deepEqual(policy.windows, ["unknown_primary", "monthly", "weekly"]); +}); +test("other providers do not inherit Codex toggles", () => { + assert.equal( + evaluateQuotaCutoff(quota({ session: 1 }), thresholds, { + ...scope(false, false), + provider: "claude", + }).proceed, + false + ); +}); +test("paid credit consent and full opt-out retain their existing precedence", () => { + const observed = { + ...quota({ session: 1, weekly: 1 }), + paidCredits: { hasCredits: true, unlimited: false, overageLimitReached: false, balance: 20 }, + }; + assert.equal(evaluateQuotaCutoff(observed, thresholds, scope(true, true)).proceed, false); + assert.equal( + evaluateQuotaCutoff(observed, thresholds, scope(true, true, { allowPaidCredits: true })) + .proceed, + true + ); + assert.equal( + evaluateQuotaCutoff(observed, thresholds, { + ...scope(true, true, { allowPaidCredits: true }), + requestedModel: "gpt-5.3-codex-spark", + }).proceed, + false + ); + assert.equal( + evaluateQuotaCutoff( + observed, + thresholds, + scope(true, true, { quotaPreflightEnabled: false, limitPolicy: { enabled: false } }) + ).proceed, + true + ); +}); +test("preflight warning and cutoff both ignore disabled windows", async () => { + registerQuotaFetcher("codex", async () => quota({ session: 1, weekly: 0.4 }, true)); + const warnings: unknown[][] = []; + const original = console.warn; + console.warn = (...args: unknown[]) => { + warnings.push(args); + }; + try { + assert.equal( + (await preflightQuota("codex", "dual-warning", scope(false, true), thresholds)).proceed, + true + ); + assert.deepEqual(warnings, []); + } finally { + console.warn = original; + } +}); diff --git a/tests/unit/codex-dual-toggle-routing-18330.test.ts b/tests/unit/codex-dual-toggle-routing-18330.test.ts new file mode 100644 index 000000000000..aca674913612 --- /dev/null +++ b/tests/unit/codex-dual-toggle-routing-18330.test.ts @@ -0,0 +1,206 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +const isolatedHome = fs.mkdtempSync(path.join(os.tmpdir(), "codex-dual-routing-18330-")); +process.env.HOME = isolatedHome; +process.env.DATA_DIR = path.join(isolatedHome, "data"); +process.env.DISABLE_SQLITE_AUTO_BACKUP = "true"; +process.env.OMNIROUTE_QUOTA_FETCH_MIN_INTERVAL_MS = "0"; + +const core = await import("../../src/lib/db/core.ts"); +const providers = await import("../../src/lib/db/providers.ts"); +const settings = await import("../../src/lib/db/settings.ts"); +const auth = await import("../../src/sse/services/auth.ts"); +const cache = await import("../../src/domain/quotaCache.ts"); +const { fetchCodexQuota, unregisterCodexConnection } = + await import("../../open-sse/services/codexQuotaFetcher.ts"); +const { registerQuotaFetcher } = await import("../../open-sse/services/quotaPreflight.ts"); +const originalFetch = globalThis.fetch; +const connections: string[] = []; +const usage = new Map(); +const calls: string[] = []; + +async function seed( + session: number, + weekly: number, + priority: number, + use5h: boolean, + useWeekly: boolean +) { + const connection = await providers.createProviderConnection({ + provider: "codex", + authType: "oauth", + isActive: true, + testStatus: "active", + name: `dual-${session}-${weekly}-${priority}`, + accessToken: `fixture-${priority}`, + priority, + quotaWindowThresholds: { session: 10, weekly: 10 }, + providerSpecificData: { + codexLimitPolicy: { use5h, useWeekly }, + allowPaidCredits: false, + limitPolicy: { enabled: true, thresholdPercent: 90 }, + }, + }); + connections.push(connection.id); + usage.set(connection.id, { session, weekly }); + cache.setQuotaCache(connection.id, "codex", { + session: { + remainingPercentage: 100 - session, + resetAt: new Date(Date.now() + 600_000).toISOString(), + }, + weekly: { + remainingPercentage: 100 - weekly, + resetAt: new Date(Date.now() + 600_000).toISOString(), + }, + }); + return connection; +} +test.beforeEach(async () => { + for (const id of connections.splice(0)) { + unregisterCodexConnection(id); + await providers.deleteProviderConnection(id); + } + cache.__clearForTests(); + calls.length = 0; + registerQuotaFetcher("codex", async (id, connection) => { + calls.push(id); + globalThis.fetch = async () => + Response.json({ + rate_limit: { + limit_reached: true, + primary_window: { + used_percent: usage.get(id)?.session, + limit_window_seconds: 18000, + reset_after_seconds: 600, + }, + secondary_window: { + used_percent: usage.get(id)?.weekly, + limit_window_seconds: 604800, + reset_after_seconds: 600, + }, + }, + credits: { has_credits: true, unlimited: false, balance: "20" }, + }); + return fetchCodexQuota(id, connection); + }); +}); +test.after(() => { + globalThis.fetch = originalFetch; + for (const id of connections) unregisterCodexConnection(id); + core.resetDbInstance(); + fs.rmSync(isolatedHome, { recursive: true, force: true }); +}); + +for (const enabled of [false, true]) { + for (const use5h of [false, true]) { + test(`routing respects disabled ${use5h ? "weekly" : "session"} with global preflight ${enabled}`, async () => { + await settings.updateSettings({ + fallbackStrategy: "priority", + resilienceSettings: { quotaPreflight: { enabled } }, + }); + const selectedAccount = await seed(use5h ? 85 : 100, use5h ? 100 : 85, 1, use5h, !use5h); + await seed(85, 85, 2, true, true); + const selected = await auth.getProviderCredentialsWithQuotaPreflight( + "codex", + null, + null, + "gpt-5.5" + ); + assert.equal(selected?.connectionId, selectedAccount.id); + assert.deepEqual(calls, [selectedAccount.id]); + selected?.releaseOAuthSession?.(); + }); + } +} +test("cached selection skips an enabled exhausted window but accepts disabled exhaustion", async () => { + const low = await seed(100, 94, 1, false, true); + const healthy = await seed(100, 85, 2, false, true); + assert.equal(auth.evaluateQuotaLimitPolicy("codex", low).blocked, true); + assert.equal(auth.evaluateQuotaLimitPolicy("codex", healthy).blocked, false); + const selected = await auth.getProviderCredentialsWithQuotaPreflight( + "codex", + null, + null, + "gpt-5.5" + ); + assert.equal(selected?.connectionId, healthy.id); + assert.deepEqual(calls, [healthy.id]); + selected?.releaseOAuthSession?.(); +}); +test("both toggles disabled preserve eligibility despite explicit cutoffs and aggregate flag", async () => { + const account = await seed(100, 100, 1, false, false); + const selected = await auth.getProviderCredentialsWithQuotaPreflight( + "codex", + null, + null, + "gpt-5.5" + ); + assert.equal(selected?.connectionId, account.id); + assert.deepEqual(calls, [account.id]); + selected?.releaseOAuthSession?.(); +}); +test("all accounts below enabled weekly cutoff are excluded", async () => { + await seed(100, 94, 1, false, true); + await seed(100, 90, 2, false, true); + const selected = await auth.getProviderCredentialsWithQuotaPreflight( + "codex", + null, + null, + "gpt-5.5" + ); + assert.equal(selected?.allRateLimited, true); + assert.equal(selected?.connectionId, undefined); +}); + +test("cached observations remain intact for independent policy decisions", () => { + const id = "dual-shared-observation"; + const resetAt = new Date(Date.now() + 600_000).toISOString(); + cache.setQuotaCache(id, "codex", { + session: { remainingPercentage: 0, resetAt }, + weekly: { remainingPercentage: 0, resetAt }, + }); + const disabled = { codexLimitPolicy: { use5h: false, useWeekly: false } }; + const enabled = { codexLimitPolicy: { use5h: true, useWeekly: true } }; + for (const model of ["gpt-5.5", null]) { + assert.equal(cache.isQuotaExhaustedForRequest(id, "codex", model, disabled), false); + assert.equal(cache.isQuotaExhaustedForRequest(id, "codex", model, enabled), true); + assert.equal(cache.isQuotaExhaustedForRequest(id, "codex", model, disabled), false); + } + assert.equal(cache.getQuotaCache(id)?.quotas.session.remainingPercentage, 0); +}); +test("unknown cached exhaustion and an unattributed upstream 429 survive disabled toggles", () => { + const disabled = { codexLimitPolicy: { use5h: false, useWeekly: false } }; + const resetAt = new Date(Date.now() + 600_000).toISOString(); + cache.setQuotaCache("dual-unknown", "codex", { + session: { remainingPercentage: 0, resetAt }, + unknown_primary: { remainingPercentage: 0, resetAt }, + }); + assert.equal( + cache.isQuotaExhaustedForRequest("dual-unknown", "codex", "gpt-5.5", disabled), + true + ); + cache.markAccountExhaustedFrom429("dual-no-windows", "codex"); + assert.equal( + cache.isQuotaExhaustedForRequest("dual-no-windows", "codex", "gpt-5.5", disabled), + true + ); +}); +test("cached Spark identity remains separate while respecting toggles", () => { + const resetAt = new Date(Date.now() + 600_000).toISOString(); + cache.setQuotaCache("dual-spark", "codex", { + session: { remainingPercentage: 0, resetAt }, + weekly: { remainingPercentage: 0, resetAt }, + gpt_5_3_codex_spark_session: { remainingPercentage: 0, resetAt }, + gpt_5_3_codex_spark_weekly: { remainingPercentage: 15, resetAt }, + }); + const policy = { codexLimitPolicy: { use5h: false, useWeekly: true } }; + assert.equal( + cache.isQuotaExhaustedForRequest("dual-spark", "codex", "gpt-5.3-codex-spark", policy), + false + ); + assert.equal(cache.isQuotaExhaustedForRequest("dual-spark", "codex", "gpt-5.5", policy), true); +}); From d5696d28b2d5ca82ba99a430852a04d13c9259c9 Mon Sep 17 00:00:00 2001 From: shevar0 <3235212+shevar0@users.noreply.github.com> Date: Thu, 29 Oct 2026 10:50:57 +0100 Subject: [PATCH 5/8] test(sse): link Codex weekly regressions to reported issues --- .../codex-pro-weekly-window-18326.test.ts | 180 +++++++++++++++++ tests/unit/codex-weekly-cutoff-18330.test.ts | 187 ++++++++++++++++++ 2 files changed, 367 insertions(+) create mode 100644 tests/unit/codex-pro-weekly-window-18326.test.ts create mode 100644 tests/unit/codex-weekly-cutoff-18330.test.ts diff --git a/tests/unit/codex-pro-weekly-window-18326.test.ts b/tests/unit/codex-pro-weekly-window-18326.test.ts new file mode 100644 index 000000000000..b6700991591d --- /dev/null +++ b/tests/unit/codex-pro-weekly-window-18326.test.ts @@ -0,0 +1,180 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +// Refs #18326. Synthetic validation-plan fixtures, not a captured reporter response. +const home = fs.mkdtempSync(path.join(os.tmpdir(), "codex-issue-18326-")); +process.env.HOME = home; +process.env.DATA_DIR = path.join(home, "data"); +process.env.DISABLE_SQLITE_AUTO_BACKUP = "true"; +process.env.OMNIROUTE_QUOTA_FETCH_MIN_INTERVAL_MS = "0"; + +const { fetchCodexQuota, unregisterCodexConnection } = + await import("../../open-sse/services/codexQuotaFetcher.ts"); +const { getCodexUsage } = await import("../../open-sse/services/usage/codex.ts"); +const { buildCodexUsageQuotas } = await import("../../open-sse/services/codexUsageQuotas.ts"); +const { projectCodexAccountPool } = await import("../../open-sse/services/codexAccount/index.ts"); +const { resetDbInstance } = await import("../../src/lib/db/core.ts"); +const ids = new Set(); +const resetSeconds = Math.floor(Date.now() / 1000) + 600; +const resetAt = new Date(resetSeconds * 1000).toISOString(); +const window = (used: number, seconds?: number) => ({ + used_percent: used, + ...(seconds === undefined ? {} : { limit_window_seconds: seconds }), + reset_at: resetSeconds, +}); +const connection = { + id: "issue-18326-pool", + provider: "codex", + providerSpecificData: { + codexLimitPolicy: { use5h: false, useWeekly: true }, + allowPaidCredits: false, + }, +}; + +test.after(() => { + for (const id of ids) unregisterCodexConnection(id); + resetDbInstance(); + fs.rmSync(home, { recursive: true, force: true }); +}); + +for (const fixture of [ + { + name: "Pro weekly-only primary", + body: { plan_type: "pro", rate_limit: { primary_window: window(94, 604800) } }, + session: null, + weekly: 94, + }, + { + name: "Pro weekly-only camelCase", + body: { + planType: "pro", + rateLimit: { + primaryWindow: { usedPercent: 94, windowSeconds: "604800", resetAt: resetSeconds }, + }, + }, + session: null, + weekly: 94, + }, + { + name: "Plus dual-window control", + body: { + plan_type: "plus", + rate_limit: { + primary_window: window(20, 18000), + secondary_window: window(94, 604800), + }, + }, + session: 20, + weekly: 94, + }, + { + name: "Pro reversed dual-window control", + body: { + plan_type: "pro", + rate_limit: { + primary_window: window(94, 604800), + secondary_window: window(20, 18000), + }, + }, + session: 20, + weekly: 94, + }, +]) { + test(`#18326 ${fixture.name}: fetcher, usage badge and account pool agree`, async (t) => { + let requests = 0; + t.mock.method(globalThis, "fetch", async (input: string) => { + assert.equal(input, "https://chatgpt.com/backend-api/wham/usage"); + requests += 1; + return Response.json(fixture.body); + }); + const id = `issue-18326-${ids.size}`; + ids.add(id); + const routing = await fetchCodexQuota(id, { + accessToken: "synthetic-offline-token", + providerSpecificData: connection.providerSpecificData, + }); + const usage = await getCodexUsage("synthetic-offline-token", connection.providerSpecificData); + assert.ok(routing); + assert.ok("quotas" in usage && usage.quotas); + assert.equal(requests, 2, "both public fetch paths exercised the synthetic response"); + assert.equal(usage.plan, routing.plan); + const pool = projectCodexAccountPool(connection, Date.now(), { + quotas: usage.quotas, + fetchedAt: new Date().toISOString(), + }); + const normal = pool.children.find((child) => child.key.scope === "codex"); + assert.ok(normal); + for (const [key, slot, expected] of [ + ["session", "5h", fixture.session], + ["weekly", "7d", fixture.weekly], + ] as const) { + const badge = usage.quotas[key]; + const projected = normal.quota.windows[slot]; + if (expected === null) { + assert.equal(badge, undefined, "missing 5h quota must not become a usage badge"); + assert.equal(projected, null, "missing 5h quota must remain absent in the pool"); + assert.equal(routing.windows?.[key], undefined); + assert.equal(routing.window5h, undefined); + } else { + assert.equal(badge?.used, expected); + assert.equal(badge?.total, 100); + assert.equal(badge?.resetAt, resetAt); + assert.equal(projected?.usedPercentage, expected); + assert.equal(projected?.resetAt, badge.resetAt); + assert.equal(routing.windows?.[key]?.percentUsed, expected / 100); + } + } + }); +} + +for (const seconds of [undefined, 3600]) { + test(`#18326 duration ${seconds}: Pro plan alone cannot manufacture a 5h or 7d slot`, async (t) => { + const body = { plan_type: "pro", rate_limit: { primary_window: window(94, seconds) } }; + t.mock.method(globalThis, "fetch", async () => Response.json(body)); + const id = `issue-18326-${ids.size}`; + ids.add(id); + const routing = await fetchCodexQuota(id, { accessToken: "synthetic-offline-token" }); + assert.ok(routing); + assert.equal(routing.percentUsed, 0.94, "unknown duration must retain measured usage"); + assert.equal(routing.window5h, undefined); + assert.equal(routing.window7d, undefined); + const { quotas } = buildCodexUsageQuotas(body); + assert.equal(quotas.unknown_primary?.used, 94); + assert.equal(quotas.session, undefined); + assert.equal(quotas.weekly, undefined); + const pool = projectCodexAccountPool(connection, Date.now(), { + quotas, + fetchedAt: new Date().toISOString(), + }); + for (const child of pool.children) { + assert.equal(child.quota.windows["5h"], null); + assert.equal(child.quota.windows["7d"], null); + } + }); +} + +test("#18326 Spark weekly evidence does not replace the normal account's weekly usage", () => { + const { quotas } = buildCodexUsageQuotas({ + plan_type: "pro", + rate_limit: { primary_window: window(12, 604800) }, + additional_rate_limits: [ + { + limit_id: "codex_bengalfox", + rate_limit: { primary_window: window(94, 604800) }, + }, + ], + }); + const pool = projectCodexAccountPool(connection, Date.now(), { + quotas, + fetchedAt: new Date().toISOString(), + }); + const normal = pool.children.find((child) => child.key.scope === "codex"); + const spark = pool.children.find((child) => child.key.scope === "spark"); + assert.equal(normal?.quota.windows["7d"]?.usedPercentage, 12); + assert.equal(spark?.quota.windows["7d"]?.usedPercentage, 94); + assert.equal(normal?.quota.windows["5h"], null); + assert.equal(spark?.quota.windows["5h"], null); +}); diff --git a/tests/unit/codex-weekly-cutoff-18330.test.ts b/tests/unit/codex-weekly-cutoff-18330.test.ts new file mode 100644 index 000000000000..ebd792fc7f01 --- /dev/null +++ b/tests/unit/codex-weekly-cutoff-18330.test.ts @@ -0,0 +1,187 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +// Refs #18330: 94% used, 6% remaining, saved 10% weekly cutoff, 5h/paid credits off. +const home = fs.mkdtempSync(path.join(os.tmpdir(), "codex-issue-18330-")); +process.env.HOME = home; +process.env.DATA_DIR = path.join(home, "data"); +process.env.DISABLE_SQLITE_AUTO_BACKUP = "true"; +process.env.OMNIROUTE_QUOTA_FETCH_MIN_INTERVAL_MS = "0"; + +const core = await import("../../src/lib/db/core.ts"); +const providers = await import("../../src/lib/db/providers.ts"); +const settings = await import("../../src/lib/db/settings.ts"); +const auth = await import("../../src/sse/services/auth.ts"); +const cache = await import("../../src/domain/quotaCache.ts"); +const { fetchCodexQuota, unregisterCodexConnection } = + await import("../../open-sse/services/codexQuotaFetcher.ts"); +const { registerQuotaFetcher } = await import("../../open-sse/services/quotaPreflight.ts"); +const connections: string[] = []; +const usage = new Map(); +const calls: string[] = []; + +async function seed( + weeklyUsed: number, + priority: number, + options: { + cachedUsed?: number; + cutoff?: number; + providerSpecificData?: Record; + } = {} +) { + const token = `synthetic-issue-18330-${connections.length}`; + const connection = await providers.createProviderConnection({ + provider: "codex", + authType: "oauth", + isActive: true, + testStatus: "active", + name: `issue-18330-${priority}`, + accessToken: token, + priority, + quotaWindowThresholds: { weekly: options.cutoff ?? 10 }, + providerSpecificData: { + codexLimitPolicy: { use5h: false, useWeekly: true }, + allowPaidCredits: false, + ...options.providerSpecificData, + }, + }); + connections.push(connection.id); + usage.set(token, weeklyUsed); + if (options.cachedUsed !== undefined) { + cache.setQuotaCache(connection.id, "codex", { + weekly: { + remainingPercentage: 100 - options.cachedUsed, + resetAt: new Date(Date.now() + 600_000).toISOString(), + }, + }); + } + cache.markQuotaHealthy(connection.id); + const saved = await providers.getProviderConnectionById(connection.id); + assert.deepEqual(saved?.quotaWindowThresholds, { weekly: options.cutoff ?? 10 }); + return connection; +} + +async function select() { + const selected = await auth.getProviderCredentialsWithQuotaPreflight( + "codex", + null, + null, + "gpt-5.5" + ); + selected?.releaseOAuthSession?.(); + return selected; +} + +test.beforeEach(async (t) => { + for (const id of connections.splice(0)) { + unregisterCodexConnection(id); + await providers.deleteProviderConnection(id); + } + usage.clear(); + calls.length = 0; + cache.__clearForTests(); + t.mock.method(globalThis, "fetch", async (input: string, init: RequestInit) => { + assert.equal(input, "https://chatgpt.com/backend-api/wham/usage"); + const token = new Headers(init.headers).get("authorization")?.replace(/^Bearer /, ""); + assert.ok(token && usage.has(token), "only a seeded offline account may be queried"); + return Response.json({ + plan_type: "pro", + rate_limit: { + primary_window: { + used_percent: usage.get(token), + limit_window_seconds: 604800, + reset_after_seconds: 600, + }, + }, + credits: { has_credits: true, unlimited: false, balance: "20" }, + }); + }); + registerQuotaFetcher("codex", async (id, connection) => { + calls.push(id); + return fetchCodexQuota(id, connection); + }); + await settings.updateSettings({ + fallbackStrategy: "priority", + resilienceSettings: { quotaPreflight: { enabled: false } }, + }); +}); + +test.after(() => { + for (const id of connections) unregisterCodexConnection(id); + core.resetDbInstance(); + fs.rmSync(home, { recursive: true, force: true }); +}); + +for (const enabled of [false, true]) { + for (const weeklyUsed of [94, 90, 85]) { + test(`#18330 live ${weeklyUsed}% used overrides old healthy evidence; global preflight ${enabled}`, async () => { + await settings.updateSettings({ + resilienceSettings: { quotaPreflight: { enabled } }, + }); + const account = await seed(weeklyUsed, 1, { cachedUsed: 50 }); + const fallback = weeklyUsed >= 90 ? await seed(20, 2) : null; + const selected = await select(); + assert.equal(selected?.connectionId, fallback?.id ?? account.id); + assert.deepEqual(calls, fallback ? [account.id, fallback.id] : [account.id]); + assert.deepEqual( + (await providers.getProviderConnectionById(account.id))?.quotaWindowThresholds, + { weekly: 10 }, + "quota selection must not rewrite the saved cutoff" + ); + }); + } + for (const strategy of ["priority", "round-robin", "p2c"]) { + test(`#18330 cached weekly policy filters before ${strategy} selection; global preflight ${enabled}`, async () => { + await settings.updateSettings({ + fallbackStrategy: strategy, + resilienceSettings: { quotaPreflight: { enabled } }, + }); + const providerSpecificData = { + limitPolicy: { enabled: true, thresholdPercent: 90, windows: ["weekly"] }, + }; + const low = await seed(94, 1, { cachedUsed: 94, providerSpecificData }); + const healthy = await seed(85, 2, { cachedUsed: 85, providerSpecificData }); + assert.equal(auth.evaluateQuotaLimitPolicy("codex", low).blocked, true); + assert.equal(auth.evaluateQuotaLimitPolicy("codex", healthy).blocked, false); + const selected = await select(); + assert.equal(selected?.connectionId, healthy.id); + assert.deepEqual(calls, [healthy.id], "filtered accounts must not be probed or selected"); + }); + } + test(`#18330 no eligible accounts returns a retry result, not the below-cutoff account; global ${enabled}`, async () => { + await settings.updateSettings({ + resilienceSettings: { quotaPreflight: { enabled } }, + }); + const low = await seed(94, 1); + const boundary = await seed(90, 2); + const selected = await select(); + assert.equal(selected?.allRateLimited, true); + assert.equal(selected?.connectionId, undefined); + assert.deepEqual(calls, [low.id, boundary.id]); + }); + test(`#18330 saving a stricter weekly cutoff affects the next selection; global ${enabled}`, async () => { + await settings.updateSettings({ + resilienceSettings: { quotaPreflight: { enabled } }, + }); + const account = await seed(94, 1, { cachedUsed: 50, cutoff: 5 }); + assert.equal((await select())?.connectionId, account.id, "6% remains above a 5% cutoff"); + await providers.updateProviderConnection(account.id, { quotaWindowThresholds: { weekly: 10 } }); + const fallback = await seed(85, 2); + assert.equal((await select())?.connectionId, fallback.id, "saved 10% cutoff takes effect"); + assert.deepEqual(calls, [account.id, account.id, fallback.id]); + }); +} + +for (const providerSpecificData of [ + { quotaPreflightEnabled: false, allowPaidCredits: false }, + { quotaPreflightEnabled: true, allowPaidCredits: true }, +]) { + test(`#18330 explicit ${providerSpecificData.allowPaidCredits ? "paid-credit consent" : "preflight opt-out"} remains intentional`, async () => { + const account = await seed(94, 1, { providerSpecificData }); + assert.equal((await select())?.connectionId, account.id); + assert.deepEqual(calls, providerSpecificData.allowPaidCredits ? [account.id] : []); + }); +} From 665b27904fb7cfd517f1f91ed2ba30895972450d Mon Sep 17 00:00:00 2001 From: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Date: Wed, 4 Nov 2026 01:39:45 -0300 Subject: [PATCH 6/8] fix(codex): keep cached windows in their real slot and align duration-less fixtures (#18334) - cachedQuotaConversion: replay the cache key identity (session/weekly, Spark included) or the persisted windowSeconds as limit_window_seconds when rebuilding the wire payload, so the duration-based parser no longer parks cached windows under unknown_primary/unknown_secondary (availability blockers regressed). - codexQuotaParser: publish only {percentUsed, resetAt} for each window, as the declared type says, instead of leaking the internal window object. - quotaCache: providerSpecificData is optional on isCodexQuotaExhausted so callers that only know the connection id keep compiling and keep the default policy. - Codex usage fixtures now carry limit_window_seconds like the real wham/usage DTO (identity requires an explicit duration); the single-window Pro fixture asserts its 604800s window is weekly (#18326). No assertion was removed or relaxed. - stryker.conf.json: list this PR's 18330 tests in tap.testFiles. --- open-sse/services/codexQuotaParser.ts | 10 +++++-- src/domain/quotaCache.ts | 3 +- src/lib/usage/cachedQuotaConversion.ts | 30 +++++++++++++++---- stryker.conf.json | 5 ++++ tests/unit/chat-context-relay.test.ts | 2 ++ tests/unit/codex-reset-credits.test.ts | 18 +++++------ tests/unit/combo-strategies.test.ts | 2 ++ .../combo/quota-weighted-pace-plan.test.ts | 6 ++-- .../quota-availability-codex-cache.test.ts | 12 ++++++-- tests/unit/usage-service-hardening.test.ts | 4 +++ 10 files changed, 70 insertions(+), 22 deletions(-) diff --git a/open-sse/services/codexQuotaParser.ts b/open-sse/services/codexQuotaParser.ts index 9983083ef56a..edebe9e95421 100644 --- a/open-sse/services/codexQuotaParser.ts +++ b/open-sse/services/codexQuotaParser.ts @@ -88,8 +88,14 @@ function assignCodexWindows( names: { primary: string; secondary: string } ): void { const { primary, secondary } = getCodexRateLimitWindows(rateLimit); - if (primary) target[codexQuotaWindowKey(primary, names)] = primary; - if (secondary) target[codexQuotaWindowKey(secondary, names)] = secondary; + // Publish only the declared {percentUsed, resetAt} shape; identity/duration stay internal. + for (const window of [primary, secondary]) { + if (!window) continue; + target[codexQuotaWindowKey(window, names)] = { + percentUsed: window.percentUsed, + resetAt: window.resetAt, + }; + } } function getSelectedCodexRateLimit( normalRateLimit: Record, diff --git a/src/domain/quotaCache.ts b/src/domain/quotaCache.ts index d65e0db90ae1..0dc198781abe 100644 --- a/src/domain/quotaCache.ts +++ b/src/domain/quotaCache.ts @@ -443,7 +443,8 @@ function isCodexQuotaExhausted( connectionId: string, entry: QuotaCacheEntry, requestedModel: string | null, - providerSpecificData: unknown + // Optional: callers without the connection's settings keep the default (all windows tracked). + providerSpecificData?: unknown ): boolean { if (Object.keys(entry.quotas).length === 0) return entry.exhausted; const windows = filterCodexQuotaWindows(entry.quotas, providerSpecificData); diff --git a/src/lib/usage/cachedQuotaConversion.ts b/src/lib/usage/cachedQuotaConversion.ts index 8e3b6f8c4b66..2e4e082f9e32 100644 --- a/src/lib/usage/cachedQuotaConversion.ts +++ b/src/lib/usage/cachedQuotaConversion.ts @@ -23,14 +23,32 @@ import { } from "@omniroute/open-sse/config/codexQuotaScopes.ts"; import type { ProviderLimitsCacheEntry } from "@/lib/db/providerLimits"; -/** Convert the cached percentage/count fields with routing's existing converter. */ -function codexWindow(value: unknown) { +const CODEX_SESSION_SECONDS = 18000; +const CODEX_WEEKLY_SECONDS = 604800; + +/** + * Rebuild one wire-shaped window from a cached quota row. The cache key (`session` / `weekly`, + * Spark variants included) is the identity the normalized parser already assigned from the + * measured duration, so it is replayed as the duration here; a persisted `windowSeconds` + * wins when present. Without it the parser would see a duration-less window and park the + * evidence under a neutral `unknown_*` key instead of the account's real session/weekly slot. + * Percent/count fields use routing's existing converter. + */ +function codexWindow(value: unknown, keyedSeconds: number) { const window = convertUsageToQuotaInfo({ quotas: { window: value } }, { provider: "codex" }) ?.windows?.window; if (!window) return undefined; + const persisted = + value && typeof value === "object" + ? (value as { windowSeconds?: unknown }).windowSeconds + : null; return { used_percent: window.percentUsed * 100, reset_at: window.resetAt ? Date.parse(window.resetAt) / 1000 : undefined, + limit_window_seconds: + typeof persisted === "number" && Number.isFinite(persisted) && persisted > 0 + ? persisted + : keyedSeconds, }; } export function convertCachedUsageToQuotaInfo( @@ -59,16 +77,16 @@ export function convertCachedUsageToQuotaInfo( rate_limit_reached_type: usage.rateLimitReachedType, rate_limit: { limit_reached: usage.limitReached ?? false, - primary_window: codexWindow(quotas.session), - secondary_window: codexWindow(quotas.weekly), + primary_window: codexWindow(quotas.session, CODEX_SESSION_SECONDS), + secondary_window: codexWindow(quotas.weekly, CODEX_WEEKLY_SECONDS), }, additional_rate_limits: [ { limit_name: CODEX_SPARK_DISPLAY_NAME, rate_limit: { limit_reached: usage.sparkLimitReached ?? false, - primary_window: codexWindow(quotas[CODEX_SPARK_QUOTA_SESSION]), - secondary_window: codexWindow(quotas[CODEX_SPARK_QUOTA_WEEKLY]), + primary_window: codexWindow(quotas[CODEX_SPARK_QUOTA_SESSION], CODEX_SESSION_SECONDS), + secondary_window: codexWindow(quotas[CODEX_SPARK_QUOTA_WEEKLY], CODEX_WEEKLY_SECONDS), }, }, ], diff --git a/stryker.conf.json b/stryker.conf.json index b5abd93f9f7a..0f056501a521 100644 --- a/stryker.conf.json +++ b/stryker.conf.json @@ -38,6 +38,11 @@ "tap": { "testFiles": [ "tests/unit/provider-safety-refusal-not-a-ban.test.ts", + "tests/unit/codex-dual-toggle-policy-18330.test.ts", + "tests/unit/codex-dual-toggle-routing-18330.test.ts", + "tests/unit/codex-weekly-cutoff-18330.test.ts", + "tests/unit/codex-weekly-routing-18330.test.ts", + "tests/unit/codex-quota-window-identity-18330.test.ts", "tests/unit/muse-session-ownership.test.ts", "tests/unit/muse-empty-response-retry.test.ts", "tests/unit/deepseek-api-flash-vision.test.ts", diff --git a/tests/unit/chat-context-relay.test.ts b/tests/unit/chat-context-relay.test.ts index e9c766a6996f..93daf66e561b 100644 --- a/tests/unit/chat-context-relay.test.ts +++ b/tests/unit/chat-context-relay.test.ts @@ -62,10 +62,12 @@ function buildQuotaResponse(usedPercent, resetAfterSeconds = 3600) { rate_limit: { primary_window: { used_percent: usedPercent, + limit_window_seconds: 18000, reset_after_seconds: resetAfterSeconds, }, secondary_window: { used_percent: 0, + limit_window_seconds: 604800, reset_after_seconds: 86400, }, }, diff --git a/tests/unit/codex-reset-credits.test.ts b/tests/unit/codex-reset-credits.test.ts index 79ea9e1dc0c4..bebd9d3fae3c 100644 --- a/tests/unit/codex-reset-credits.test.ts +++ b/tests/unit/codex-reset-credits.test.ts @@ -99,8 +99,8 @@ function mockResetThenUsage(opts: { opts.usageBody ?? { plan_type: "plus", rate_limit: { - primary_window: { used_percent: 0 }, - secondary_window: { used_percent: 0 }, + primary_window: { used_percent: 0, limit_window_seconds: 18000 }, + secondary_window: { used_percent: 0, limit_window_seconds: 604800 }, }, } ), @@ -152,8 +152,8 @@ test("consumeCodexResetCredit fetches a credit id, posts it, then refreshes usag JSON.stringify({ plan_type: "plus", rate_limit: { - primary_window: { used_percent: 0 }, - secondary_window: { used_percent: 40 }, + primary_window: { used_percent: 0, limit_window_seconds: 18000 }, + secondary_window: { used_percent: 40, limit_window_seconds: 604800 }, }, rate_limit_reset_credits: { available_count: 1 }, }), @@ -198,7 +198,7 @@ test("consumeCodexResetCredit accepts alreadyRedeemed as success", async () => { if (String(url).includes("/backend-api/wham/usage")) { return new Response( JSON.stringify({ - rate_limit: { primary_window: { used_percent: 5 } }, + rate_limit: { primary_window: { used_percent: 5, limit_window_seconds: 18000 } }, }), { status: 200, headers: { "content-type": "application/json" } } ); @@ -463,8 +463,8 @@ test("#12951 consumeCodexResetCredit alreadyRedeemed still needs healthy usage t usageBody: { plan_type: "plus", rate_limit: { - primary_window: { used_percent: 100 }, - secondary_window: { used_percent: 100 }, + primary_window: { used_percent: 100, limit_window_seconds: 18000 }, + secondary_window: { used_percent: 100, limit_window_seconds: 604800 }, }, }, }); @@ -560,8 +560,8 @@ test("#4305 consumeCodexResetCredit keeps model lockouts while refreshed usage i usageBody: { plan_type: "plus", rate_limit: { - primary_window: { used_percent: 100 }, - secondary_window: { used_percent: 100 }, + primary_window: { used_percent: 100, limit_window_seconds: 18000 }, + secondary_window: { used_percent: 100, limit_window_seconds: 604800 }, }, }, }); diff --git a/tests/unit/combo-strategies.test.ts b/tests/unit/combo-strategies.test.ts index ef8e270f2c4c..a8038900365b 100644 --- a/tests/unit/combo-strategies.test.ts +++ b/tests/unit/combo-strategies.test.ts @@ -118,10 +118,12 @@ function codexQuota({ rate_limit: { primary_window: { used_percent: used5h, + limit_window_seconds: 18000, reset_after_seconds: reset5hSeconds, }, secondary_window: { used_percent: used7d, + limit_window_seconds: 604800, reset_after_seconds: reset7dSeconds, }, }, diff --git a/tests/unit/combo/quota-weighted-pace-plan.test.ts b/tests/unit/combo/quota-weighted-pace-plan.test.ts index e5cc6504a842..e899e1886587 100644 --- a/tests/unit/combo/quota-weighted-pace-plan.test.ts +++ b/tests/unit/combo/quota-weighted-pace-plan.test.ts @@ -260,8 +260,10 @@ test("live Codex plan: fetch preserves trimmed plan_type and planType, omits inv test("live Codex single-window Pro: real fetch and pace draw use 0.6/100 times ten", async () => { const pro = await liveQuota(usagePayload({ plan_type: "pro" })); - assert.deepEqual(pro.window5h, { percentUsed: 0.4, resetAt: resetAt(100) }); - assert.deepEqual(pro.window7d, { percentUsed: 0, resetAt: null }); + // The lone window reports a 604800s duration: it is the weekly window (#18326), so no + // 5h slot is fabricated and no empty weekly placeholder is invented next to it. + assert.equal(pro.window5h, undefined); + assert.deepEqual(pro.window7d, { percentUsed: 0.4, resetAt: resetAt(100) }); const targets = await seedTargets([quotaAt("plus"), pro]); // Plus owns 1/11, not 1/2; straddling that threshold proves the real projection's weight. assert.deepEqual(await draw(targets, 1 / 11 - 1e-8), [0, 1]); diff --git a/tests/unit/quota-availability-codex-cache.test.ts b/tests/unit/quota-availability-codex-cache.test.ts index bfff2d40045e..9c180df62720 100644 --- a/tests/unit/quota-availability-codex-cache.test.ts +++ b/tests/unit/quota-availability-codex-cache.test.ts @@ -27,8 +27,16 @@ test("Codex upstream limit flag survives cache writes and server reconstruction" const wire = { rate_limit: { limit_reached: true, - primary_window: { used_percent: 20, reset_at: (now + 3600000) / 1000 }, - secondary_window: { used_percent: 20, reset_at: (now + 144 * 3600000) / 1000 }, + primary_window: { + used_percent: 20, + limit_window_seconds: 18000, + reset_at: (now + 3600000) / 1000, + }, + secondary_window: { + used_percent: 20, + limit_window_seconds: 604800, + reset_at: (now + 144 * 3600000) / 1000, + }, }, rate_limit_reached_type: "weekly", }; diff --git a/tests/unit/usage-service-hardening.test.ts b/tests/unit/usage-service-hardening.test.ts index 344fa30cbf1f..17d5d1fad49f 100644 --- a/tests/unit/usage-service-hardening.test.ts +++ b/tests/unit/usage-service-hardening.test.ts @@ -639,10 +639,12 @@ test("usage service covers Codex, Kiro and Kimi usage parsing and error branches limit_reached: false, primary_window: { used_percent: 25, + limit_window_seconds: 18000, reset_after_seconds: 30, }, secondary_window: { used_percent: 50, + limit_window_seconds: 604800, reset_at: Math.floor(Date.now() / 1000) + 120, }, }, @@ -661,10 +663,12 @@ test("usage service covers Codex, Kiro and Kimi usage parsing and error branches rate_limit: { primary_window: { used_percent: 90, + limit_window_seconds: 18000, reset_after_seconds: 60, }, secondary_window: { used_percent: 20, + limit_window_seconds: 604800, reset_after_seconds: 600, }, }, From 26ae0124b4f9b65756582f9106b465313f6dd26d Mon Sep 17 00:00:00 2001 From: shevar0 <3235212+shevar0@users.noreply.github.com> Date: Wed, 4 Nov 2026 15:22:17 +0100 Subject: [PATCH 7/8] fix(sse): retain neutral Codex quota evidence through cache --- src/lib/usage/cachedQuotaConversion.ts | 27 ++++++-- stryker.conf.json | 5 ++ tests/unit/codex-reset-credits.test.ts | 8 +-- tests/unit/combo-strategies.test.ts | 2 + .../combo/quota-weighted-pace-plan.test.ts | 6 +- .../quota-availability-codex-cache.test.ts | 62 ++++++++++++++++++- tests/unit/usage-service-hardening.test.ts | 4 ++ 7 files changed, 101 insertions(+), 13 deletions(-) diff --git a/src/lib/usage/cachedQuotaConversion.ts b/src/lib/usage/cachedQuotaConversion.ts index 8e3b6f8c4b66..bf0842e323db 100644 --- a/src/lib/usage/cachedQuotaConversion.ts +++ b/src/lib/usage/cachedQuotaConversion.ts @@ -24,13 +24,32 @@ import { import type { ProviderLimitsCacheEntry } from "@/lib/db/providerLimits"; /** Convert the cached percentage/count fields with routing's existing converter. */ -function codexWindow(value: unknown) { +function codexWindow(value: unknown, canonicalDuration?: number) { const window = convertUsageToQuotaInfo({ quotas: { window: value } }, { provider: "codex" }) ?.windows?.window; if (!window) return undefined; + const measured = value && typeof value === "object" ? (value as Record) : {}; return { used_percent: window.percentUsed * 100, reset_at: window.resetAt ? Date.parse(window.resetAt) / 1000 : undefined, + // Canonical cache keys carry a known duration; neutral keys retain measured evidence only. + limit_window_seconds: canonicalDuration ?? measured.windowSeconds, + }; +} + +function codexCachedWindows( + quotas: Record, + sessionKey: string, + weeklyKey: string +) { + const neutralPrefix = sessionKey.replace(/session$/, "unknown_"); + const known = [ + codexWindow(quotas[sessionKey], 18000), + codexWindow(quotas[weeklyKey], 604800), + ].filter((window) => window !== undefined); + return { + primary_window: codexWindow(quotas[`${neutralPrefix}primary`]) ?? known.shift(), + secondary_window: codexWindow(quotas[`${neutralPrefix}secondary`]) ?? known.shift(), }; } export function convertCachedUsageToQuotaInfo( @@ -59,16 +78,14 @@ export function convertCachedUsageToQuotaInfo( rate_limit_reached_type: usage.rateLimitReachedType, rate_limit: { limit_reached: usage.limitReached ?? false, - primary_window: codexWindow(quotas.session), - secondary_window: codexWindow(quotas.weekly), + ...codexCachedWindows(quotas, "session", "weekly"), }, additional_rate_limits: [ { limit_name: CODEX_SPARK_DISPLAY_NAME, rate_limit: { limit_reached: usage.sparkLimitReached ?? false, - primary_window: codexWindow(quotas[CODEX_SPARK_QUOTA_SESSION]), - secondary_window: codexWindow(quotas[CODEX_SPARK_QUOTA_WEEKLY]), + ...codexCachedWindows(quotas, CODEX_SPARK_QUOTA_SESSION, CODEX_SPARK_QUOTA_WEEKLY), }, }, ], diff --git a/stryker.conf.json b/stryker.conf.json index 5602120a0d8d..726c516eac10 100644 --- a/stryker.conf.json +++ b/stryker.conf.json @@ -98,6 +98,11 @@ "tests/unit/check-error-helper-call-scope.test.ts", "tests/unit/check-error-helper-false-positives-15159.test.ts", "tests/unit/codex-quota-optout.test.ts", + "tests/unit/codex-dual-toggle-policy-18330.test.ts", + "tests/unit/codex-dual-toggle-routing-18330.test.ts", + "tests/unit/codex-weekly-cutoff-18330.test.ts", + "tests/unit/codex-weekly-routing-18330.test.ts", + "tests/unit/codex-quota-window-identity-18330.test.ts", "tests/unit/codex-passive-refresh-gate.test.ts", "tests/unit/compression/default-combo-lossy-policy.test.ts", "tests/unit/compression/output-styles-anthropic-placement.test.ts", diff --git a/tests/unit/codex-reset-credits.test.ts b/tests/unit/codex-reset-credits.test.ts index 79ea9e1dc0c4..afdcbaa5f3a6 100644 --- a/tests/unit/codex-reset-credits.test.ts +++ b/tests/unit/codex-reset-credits.test.ts @@ -99,8 +99,8 @@ function mockResetThenUsage(opts: { opts.usageBody ?? { plan_type: "plus", rate_limit: { - primary_window: { used_percent: 0 }, - secondary_window: { used_percent: 0 }, + primary_window: { used_percent: 0, limit_window_seconds: 18000 }, + secondary_window: { used_percent: 0, limit_window_seconds: 604800 }, }, } ), @@ -152,8 +152,8 @@ test("consumeCodexResetCredit fetches a credit id, posts it, then refreshes usag JSON.stringify({ plan_type: "plus", rate_limit: { - primary_window: { used_percent: 0 }, - secondary_window: { used_percent: 40 }, + primary_window: { used_percent: 0, limit_window_seconds: 18000 }, + secondary_window: { used_percent: 40, limit_window_seconds: 604800 }, }, rate_limit_reset_credits: { available_count: 1 }, }), diff --git a/tests/unit/combo-strategies.test.ts b/tests/unit/combo-strategies.test.ts index ef8e270f2c4c..3ead8d054322 100644 --- a/tests/unit/combo-strategies.test.ts +++ b/tests/unit/combo-strategies.test.ts @@ -117,10 +117,12 @@ function codexQuota({ return { rate_limit: { primary_window: { + limit_window_seconds: 18000, used_percent: used5h, reset_after_seconds: reset5hSeconds, }, secondary_window: { + limit_window_seconds: 604800, used_percent: used7d, reset_after_seconds: reset7dSeconds, }, diff --git a/tests/unit/combo/quota-weighted-pace-plan.test.ts b/tests/unit/combo/quota-weighted-pace-plan.test.ts index e5cc6504a842..ec576cce365f 100644 --- a/tests/unit/combo/quota-weighted-pace-plan.test.ts +++ b/tests/unit/combo/quota-weighted-pace-plan.test.ts @@ -260,8 +260,10 @@ test("live Codex plan: fetch preserves trimmed plan_type and planType, omits inv test("live Codex single-window Pro: real fetch and pace draw use 0.6/100 times ten", async () => { const pro = await liveQuota(usagePayload({ plan_type: "pro" })); - assert.deepEqual(pro.window5h, { percentUsed: 0.4, resetAt: resetAt(100) }); - assert.deepEqual(pro.window7d, { percentUsed: 0, resetAt: null }); + assert.equal(pro.window5h, undefined); + assert.equal(pro.window7d?.percentUsed, 0.4); + assert.equal(pro.window7d?.resetAt, resetAt(100)); + assert.deepEqual(Object.keys(pro.windows ?? {}), ["weekly"]); const targets = await seedTargets([quotaAt("plus"), pro]); // Plus owns 1/11, not 1/2; straddling that threshold proves the real projection's weight. assert.deepEqual(await draw(targets, 1 / 11 - 1e-8), [0, 1]); diff --git a/tests/unit/quota-availability-codex-cache.test.ts b/tests/unit/quota-availability-codex-cache.test.ts index bfff2d40045e..13fbc203a2d7 100644 --- a/tests/unit/quota-availability-codex-cache.test.ts +++ b/tests/unit/quota-availability-codex-cache.test.ts @@ -27,8 +27,16 @@ test("Codex upstream limit flag survives cache writes and server reconstruction" const wire = { rate_limit: { limit_reached: true, - primary_window: { used_percent: 20, reset_at: (now + 3600000) / 1000 }, - secondary_window: { used_percent: 20, reset_at: (now + 144 * 3600000) / 1000 }, + primary_window: { + used_percent: 20, + reset_at: (now + 3600000) / 1000, + limit_window_seconds: 18000, + }, + secondary_window: { + used_percent: 20, + reset_at: (now + 144 * 3600000) / 1000, + limit_window_seconds: 604800, + }, }, rate_limit_reached_type: "weekly", }; @@ -100,6 +108,54 @@ test("Codex upstream limit flag survives cache writes and server reconstruction" } }); +for (const measuredWeekly of [false, true]) { + test(`Codex neutral cached windows keep exhaustion evidence without invented slots, weekly ${measuredWeekly}`, () => { + const cached = { + quotas: { + unknown_primary: { + used: 100, + total: 100, + windowSeconds: null, + }, + ...(measuredWeekly + ? { weekly: { used: 20, total: 100, windowSeconds: 604800 } } + : { unknown_secondary: { used: 20, total: 100, windowSeconds: 3600 } }), + }, + }; + const quota = convertCachedUsageToQuotaInfo("codex", cached, "gpt-5.4"); + assert.ok(quota); + assert.equal(quota.percentUsed, 1); + assert.equal(quota.window5h, undefined); + assert.equal(quota.window7d?.percentUsed, measuredWeekly ? 0.2 : undefined); + assert.equal(Object.keys(quota.windows ?? {}).length, 2); + assert.equal(quota.windows?.unknown_primary.percentUsed, 1); + }); +} + +test("Codex neutral Spark cache evidence remains separate from the ordinary scope", () => { + const cached = { + quotas: { + session: { used: 20, total: 100 }, + gpt_5_3_codex_spark_unknown_secondary: { + used: 100, + total: 100, + windowSeconds: 3600, + }, + gpt_5_3_codex_spark_session: { used: 30, total: 100 }, + }, + }; + const ordinary = convertCachedUsageToQuotaInfo("codex", cached, "gpt-5.4"); + const spark = convertCachedUsageToQuotaInfo("codex", cached, "gpt-5.3-codex-spark"); + assert.ok(ordinary); + assert.ok(spark); + assert.equal(ordinary.percentUsed, 0.2); + assert.equal(spark.percentUsed, 1); + assert.equal(spark.window5h?.percentUsed, 0.3); + assert.equal(spark.window7d, undefined); + assert.equal(evaluateQuotaCutoff(ordinary, policy, "codex").proceed, true); + assert.equal(evaluateQuotaCutoff(spark, policy, "codex").proceed, false); +}); + test("Codex legacy cache entries without a limit flag retain their existing eligibility", () => { const now = Date.now(); const usage = toProviderLimitsCacheEntry( @@ -112,6 +168,8 @@ test("Codex legacy cache entries without a limit flag retain their existing elig ); const quota = convertCachedUsageToQuotaInfo("codex", usage, "gpt-5.4"); assert.equal(quota?.limitReached, false); + assert.equal(quota?.window5h?.percentUsed, 0.2); + assert.equal(quota?.window7d, undefined); assert.equal( buildConnectionQuotaAvailability( { id: "synthetic-codex-legacy", provider: "codex" }, diff --git a/tests/unit/usage-service-hardening.test.ts b/tests/unit/usage-service-hardening.test.ts index 344fa30cbf1f..17d5d1fad49f 100644 --- a/tests/unit/usage-service-hardening.test.ts +++ b/tests/unit/usage-service-hardening.test.ts @@ -639,10 +639,12 @@ test("usage service covers Codex, Kiro and Kimi usage parsing and error branches limit_reached: false, primary_window: { used_percent: 25, + limit_window_seconds: 18000, reset_after_seconds: 30, }, secondary_window: { used_percent: 50, + limit_window_seconds: 604800, reset_at: Math.floor(Date.now() / 1000) + 120, }, }, @@ -661,10 +663,12 @@ test("usage service covers Codex, Kiro and Kimi usage parsing and error branches rate_limit: { primary_window: { used_percent: 90, + limit_window_seconds: 18000, reset_after_seconds: 60, }, secondary_window: { used_percent: 20, + limit_window_seconds: 604800, reset_after_seconds: 600, }, }, From 816934b9bb30ed548402767f1a5e0783e31f3318 Mon Sep 17 00:00:00 2001 From: shevar0 <3235212+shevar0@users.noreply.github.com> Date: Wed, 4 Nov 2026 12:52:49 -0300 Subject: [PATCH 8/8] =?UTF-8?q?refactor(sse):=20move=20codexQuotaWindowPol?= =?UTF-8?q?icy=20under=20open-sse=20to=20avoid=20a=20new=20open-sse?= =?UTF-8?q?=E2=86=92src=20edge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit quotaPreflight.ts (open-sse) imported the policy from src/lib/providers, adding an open-sse→src boundary edge (773→774, at the ratchet ceiling). The module only depends on open-sse config, so it now lives in open-sse/services; src consumers import it via @omniroute/open-sse. Boundary back to 773. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --- .../providers => open-sse/services}/codexQuotaWindowPolicy.ts | 2 +- open-sse/services/quotaPreflight.ts | 2 +- src/domain/quotaCache.ts | 2 +- src/sse/services/quotaLimitPolicy.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename {src/lib/providers => open-sse/services}/codexQuotaWindowPolicy.ts (96%) diff --git a/src/lib/providers/codexQuotaWindowPolicy.ts b/open-sse/services/codexQuotaWindowPolicy.ts similarity index 96% rename from src/lib/providers/codexQuotaWindowPolicy.ts rename to open-sse/services/codexQuotaWindowPolicy.ts index cabc7650a6cc..2c8c4689cdb0 100644 --- a/src/lib/providers/codexQuotaWindowPolicy.ts +++ b/open-sse/services/codexQuotaWindowPolicy.ts @@ -1,4 +1,4 @@ -import { toCodexBaseQuotaWindowName } from "@omniroute/open-sse/config/codexQuotaScopes.ts"; +import { toCodexBaseQuotaWindowName } from "../config/codexQuotaScopes.ts"; function asRecord(value: unknown): Record { return value && typeof value === "object" && !Array.isArray(value) diff --git a/open-sse/services/quotaPreflight.ts b/open-sse/services/quotaPreflight.ts index f327f579afd4..3f7712997073 100644 --- a/open-sse/services/quotaPreflight.ts +++ b/open-sse/services/quotaPreflight.ts @@ -21,7 +21,6 @@ import { isCompatibleProviderConnectionId } from "@/shared/utils/compatibleProviderId"; import { isFeatureFlagEnabled } from "@/shared/utils/featureFlags"; import { isClaudeExtraUsageAllowed } from "@/lib/providers/claudeExtraUsage"; -import { filterCodexQuotaWindows } from "@/lib/providers/codexQuotaWindowPolicy"; import { isCodexQuotaFilteringDisabled } from "@/lib/providers/codexQuotaFiltering"; // #14359 — import the leaf, NOT "@/domain/quotaCache": quotaCache → usage.ts → usage/openrouter.ts → // openrouterQuotaFetcher.ts → this file, so importing quotaCache here closes an ESM init cycle @@ -32,6 +31,7 @@ import { isCodexPaidCreditsEnabled, type CodexPaidCredits, } from "@/lib/providers/codexPaidCredits"; +import { filterCodexQuotaWindows } from "./codexQuotaWindowPolicy.ts"; import { fetchNewApiAggregatorQuota, invalidateNewApiAggregatorQuotaCache, diff --git a/src/domain/quotaCache.ts b/src/domain/quotaCache.ts index dd6f07352402..dffbfee5b590 100644 --- a/src/domain/quotaCache.ts +++ b/src/domain/quotaCache.ts @@ -39,7 +39,7 @@ import { } from "@omniroute/open-sse/services/codexAccount/index.ts"; import { selectAntigravityQuotaWindowNames } from "@omniroute/open-sse/services/antigravityQuotaFamily.ts"; import { isClaudeExtraUsageAllowed } from "@/lib/providers/claudeExtraUsage"; -import { filterCodexQuotaWindows } from "@/lib/providers/codexQuotaWindowPolicy"; +import { filterCodexQuotaWindows } from "@omniroute/open-sse/services/codexQuotaWindowPolicy.ts"; import { isCodexQuotaFilteringDisabled } from "@/lib/providers/codexQuotaFiltering"; import { resolveProviderId } from "@/shared/constants/providers"; import { diff --git a/src/sse/services/quotaLimitPolicy.ts b/src/sse/services/quotaLimitPolicy.ts index 0ba693be50b3..7738e0467fbc 100644 --- a/src/sse/services/quotaLimitPolicy.ts +++ b/src/sse/services/quotaLimitPolicy.ts @@ -1,6 +1,6 @@ import { DEFAULT_QUOTA_THRESHOLD_PERCENT, getQuotaWindowStatus } from "@/domain/quotaCache"; import { defersQuotaCutoff } from "@/lib/providers/quotaCutoffOptIns"; -import { applyCodexWindowPolicy } from "@/lib/providers/codexQuotaWindowPolicy"; +import { applyCodexWindowPolicy } from "@omniroute/open-sse/services/codexQuotaWindowPolicy.ts"; import { toNumber } from "@/shared/utils/numeric"; import { toCodexScopedQuotaWindowName } from "@omniroute/open-sse/config/codexQuotaScopes.ts"; import { cooldownUntilMs } from "@omniroute/open-sse/services/accountFallback.ts";