Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
09dd914
fix(codex): normalize quota evidence without fabricating healthy windows
shevar0 Oct 27, 2026
546efb1
docs: link codex-quota-signal-normalization fix to #18334
shevar0 Oct 27, 2026
52e4696
fix(sse): classify Codex quota windows by duration (#18326, #18330)
shevar0 Oct 27, 2026
7f55b14
fix(sse): honor Codex quota tracking across routing gates (#18330)
shevar0 Oct 27, 2026
cdf7c24
fix(sse): preserve Codex quota fixes through release refactors
shevar0 Oct 27, 2026
d5696d2
test(sse): link Codex weekly regressions to reported issues
shevar0 Oct 29, 2026
54199c8
Merge branch 'release/v3.8.53' into fix/codex-quota-signal-normalization
diegosouzapw Nov 3, 2026
02946d8
Merge branch 'release/v3.8.53' into fix/codex-quota-signal-normalization
diegosouzapw Nov 3, 2026
4c600f1
Merge remote-tracking branch 'origin/release/v3.8.53' into HEAD
diegosouzapw Nov 4, 2026
665b279
fix(codex): keep cached windows in their real slot and align duration…
diegosouzapw Nov 4, 2026
2f6484c
fix(sse): align codex display labels with window identity
shevar0 Nov 4, 2026
edfb85d
Merge remote-tracking branch 'origin/release/v3.8.53' into HEAD
diegosouzapw Nov 4, 2026
ff0407f
fix(sse): reconcile normalized Codex quota evidence
shevar0 Nov 4, 2026
4f7790f
fix(sse): reconcile normalized Codex quota evidence
shevar0 Nov 4, 2026
a893eec
Merge commit 'refs/rel53-live/18334' into HEAD
diegosouzapw Nov 4, 2026
26ae012
fix(sse): retain neutral Codex quota evidence through cache
shevar0 Nov 4, 2026
f9a74d1
chore: merge release/v3.8.53 into fix/codex-quota-signal-normalization
shevar0 Nov 4, 2026
816934b
refactor(sse): move codexQuotaWindowPolicy under open-sse to avoid a …
shevar0 Nov 4, 2026
1741a90
Merge commit 'refs/rel53-live/18334' into HEAD
diegosouzapw Nov 4, 2026
c1b1235
Merge release/v3.8.53 into #18334 (v3.8.53 train: land exactly the va…
diegosouzapw Nov 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/fixes/18330-codex-dual-window-policy.md
Original file line number Diff line number Diff line change
@@ -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).
1 change: 1 addition & 0 deletions changelog.d/fixes/18330-codex-weekly-window-identity.md
Original file line number Diff line number Diff line change
@@ -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).
Original file line number Diff line number Diff line change
@@ -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. ([#18334](https://github.com/cheaper-inference/OmniRoute/pull/18334)) — thanks @shevar0
66 changes: 45 additions & 21 deletions open-sse/executors/codex/quota.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
// Codex quota-snapshot parsing + reset/cooldown scheduling (pure). Verbatim from codex.ts.
// Codex quota-snapshot parsing + reset/cooldown scheduling (pure).
import {
classifyCodexQuotaWindow,
parseCodexFiniteNumber,
parseCodexLegacyReset,
parseCodexOfficialQuotaHeaders,
} from "../../services/codexQuotaSignals.ts";

/**
* T03: Parsed quota snapshot from Codex response headers.
Expand All @@ -22,27 +28,45 @@ 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<string, string>): 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<string, string>,
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 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;
quota[`usage${slot}`] = window.usedPercent;
quota[`limit${slot}`] = 100;
quota[`resetAt${slot}`] = window.resetAt;
observed.add(slot);
}
return observed.size > 0 ? quota : null;
}

/**
Expand Down
5 changes: 2 additions & 3 deletions open-sse/services/codexAccount/quota.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@ export async function persistCodexChildQuotaResponse(params: {
fallbackRateLimitedUntil?: string | null;
}): Promise<PersistCodexChildQuotaResult | null> {
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,
Expand Down
24 changes: 7 additions & 17 deletions open-sse/services/codexQuotaFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -42,25 +41,16 @@ export {
CODEX_WINDOW_WEEKLY,
} from "./codexQuotaParser.ts";

/**
* Stable identifiers for Codex's quota windows. These match the quota keys
* surfaced by `getCodexUsage` (in usage.ts) and rendered by the dashboard,
* so per-window thresholds set in the UI line up with the keys persisted
* in `provider_connections.quota_window_thresholds_json`. The dedicated
* Codex fetcher exposes only session + weekly today; the plan-dependent
* code_review window is surfaced by the generic path when present.
*/

// Codex usage endpoint (same as usage.ts CODEX_CONFIG)
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<string, { percentUsed: number; resetAt: string | null }>;
Expand Down Expand Up @@ -293,13 +283,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;
}
Expand Down
75 changes: 40 additions & 35 deletions open-sse/services/codexQuotaParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import {
} from "../config/codexQuotaScopes.ts";
import { parseCodexPaidCredits } from "@/lib/providers/codexPaidCredits";
import type { CodexDualWindowQuota } from "./codexQuotaFetcher.ts";
import {
codexQuotaWindowKey,
isCodexLimitReached,
parseCodexQuotaWindow,
type CodexQuotaWindow,
} from "./codexQuotaSignals.ts";
export const CODEX_WINDOW_SESSION = "session";
export const CODEX_WINDOW_WEEKLY = "weekly";

Expand All @@ -22,21 +28,6 @@ function toRecord(value: unknown): Record<string, unknown> {
? (value as Record<string, unknown>)
: {};
}
function parseWindowReset(window: Record<string, unknown>): string | null {
const resetAt = parseCodexNumber(window["reset_at"] ?? window["resetAt"], 0);
if (resetAt > 0) return new Date(resetAt * 1000).toISOString();
const resetAfterSeconds = parseCodexNumber(
window["reset_after_seconds"] ?? window["resetAfterSeconds"],
0
);
if (resetAfterSeconds > 0) return new Date(Date.now() + resetAfterSeconds * 1000).toISOString();
return null;
}
function parseCodexWindow(window: Record<string, unknown> | null | undefined) {
if (!window || Object.keys(window).length === 0) return null;
const percentUsed = parseCodexNumber(window["used_percent"] ?? window["usedPercent"], 0) / 100;
return { percentUsed, resetAt: parseWindowReset(window) };
}
function parseBankedResetCredits(data: Record<string, unknown>): number | undefined {
const resetCredits = toRecord(data["rate_limit_reset_credits"] ?? data["rateLimitResetCredits"]);
const availableCount = resetCredits["available_count"] ?? resetCredits["availableCount"];
Expand Down Expand Up @@ -76,11 +67,18 @@ function findSparkRateLimit(data: Record<string, unknown>): Record<string, unkno
}
return null;
}
function getCodexRateLimitWindows(rateLimit: Record<string, unknown>) {
function getCodexRateLimitWindows(rateLimit: Record<string, unknown>): {
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"
),
};
}
Expand All @@ -90,8 +88,14 @@ 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;
// 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<string, unknown>,
Expand All @@ -105,14 +109,13 @@ function parseCodexPlan(data: Record<string, unknown>): string | undefined {
const rawPlan = data["plan_type"] ?? data["planType"];
return typeof rawPlan === "string" && rawPlan.trim() ? rawPlan.trim() : undefined;
}
function getDominantResetAt(
quota: Pick<CodexDualWindowQuota, "window5h" | "window7d">
): 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;
}
/** The dispatch fetcher's parser, also usable without importing its network/identity graph. */
export function parseCodexUsageResponse(
Expand All @@ -132,12 +135,11 @@ export function parseCodexUsageResponse(
const { primary: parsedPrimary, secondary: parsedSecondary } =
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 limitReached = Boolean(
selectedRateLimit["limit_reached"] ?? selectedRateLimit["limitReached"]
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<string, { percentUsed: number; resetAt: string | null }> = {};
assignCodexWindows(windows, selectedRateLimit, {
primary: useSparkWindows ? CODEX_SPARK_QUOTA_SESSION : CODEX_WINDOW_SESSION,
Expand All @@ -153,6 +155,9 @@ export function parseCodexUsageResponse(
primary: CODEX_WINDOW_SESSION,
secondary: CODEX_WINDOW_WEEKLY,
});
// Fixed compatibility slots follow measured durations, never primary/secondary position.
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);
Expand All @@ -162,7 +167,7 @@ export function parseCodexUsageResponse(
used: Math.round(worstPercentUsed * 100),
total: 100,
percentUsed: worstPercentUsed,
resetAt: getDominantResetAt({ window5h, window7d }),
resetAt: getDominantResetAt(measured),
windows,
allWindows,
window5h,
Expand Down
Loading
Loading