Compare commits

...

4 Commits

Author SHA1 Message Date
diegosouzapw
896ce9c0e2 feat(release): v2.7.3 — fix Codex direct API weekly quota fallback
- fix(codex): resolveQuotaWindow() prefix-matches 'weekly' → 'weekly (7d)' cache keys
- fix(codex): applyCodexWindowPolicy() enforces useWeekly/use5h toggles in direct API
- 4 new regression tests, 766 total passing
- Closes #440
2026-03-18 08:41:13 -03:00
Diego Rodrigues de Sa e Souza
82934132e9 Merge pull request #441 from rexname/fix/issue-440-direct-api-fallback
fix(codex): block weekly-exhausted accounts in direct API fallback
2026-03-18 08:40:19 -03:00
rexname
a2012b70de chore(review): harden window normalization and deterministic quota matching 2026-03-18 14:17:37 +07:00
rexname
bcfeba8a57 fix(codex): enforce weekly quota blocking for direct API fallback 2026-03-18 13:57:25 +07:00
6 changed files with 125 additions and 8 deletions

View File

@@ -4,6 +4,19 @@
---
## [2.7.3] — 2026-03-18
> Sprint: Codex direct API quota fallback fix.
### 🐛 Bug Fixes
- **fix(codex)**: Block weekly-exhausted accounts in direct API fallback (#440)
- `resolveQuotaWindow()` prefix matching: `"weekly"` now matches `"weekly (7d)"` cache keys
- `applyCodexWindowPolicy()` enforces `useWeekly`/`use5h` toggles correctly
- 4 new regression tests (766 total)
---
## [2.7.2] — 2026-03-18
> Sprint: Light mode UI contrast fixes.

View File

@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: OmniRoute API
version: 2.7.2
version: 2.7.3
description: |
OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible
endpoint that routes requests to multiple AI providers with load balancing,

View File

@@ -1,6 +1,6 @@
{
"name": "omniroute",
"version": "2.7.2",
"version": "2.7.3",
"description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.",
"type": "module",
"bin": {

View File

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

View File

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

View File

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