diff --git a/CHANGELOG.md b/CHANGELOG.md index 991c2a8369..d53ecb4e65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,7 @@ _In development — bullets added per PR; finalized at release._ - **fix(command-code): pass `reasoning` / `thinking` fields through to upstream params** — Command Code requests carrying `reasoning`/`thinking` controls had those fields dropped before the upstream call, so reasoning-effort and extended-thinking settings were silently ignored; they are now forwarded to the upstream params. ([#4473](https://github.com/diegosouzapw/OmniRoute/pull/4473) — thanks @adivekar-utexas) - **fix(usage): keep Kiro overage-enabled accounts routable after base quota hits zero** — a Kiro account with overage enabled was excluded from routing once its base quota reached zero, even though overage billing should keep it serving; such accounts now stay routable past base-quota exhaustion. ([#4469](https://github.com/diegosouzapw/OmniRoute/issues/4469) — thanks @heaven321357 / @CleanDev-Fix) - **fix(providers): model-aware `supportsRedactedThinking` for mixed-format providers** — the redacted-thinking capability was resolved per provider rather than per model, so a mixed-format provider (some models support redacted thinking, others don't) got the wrong answer for some models; the check is now model-aware. ([#4479](https://github.com/diegosouzapw/OmniRoute/pull/4479) — thanks @TF0rd) +- **fix(usage): parse numeric-string quota reset timestamps as Unix seconds/ms** — when a provider returned the quota reset timestamp as a numeric string (e.g. `"1700000000"`), `parseResetTime` passed it straight to `new Date(str)`, which returned `Invalid Date` and dropped the reset entirely (UI showed no reset). Numeric strings are now detected and treated as Unix timestamps with the same `< 1e12` seconds-vs-ms heuristic already applied to numeric values; ISO/parseable strings are untouched. Applied symmetrically in `codexUsageQuotas.parseResetTime`. (Inspired by upstream [decolua/9router#768](https://github.com/decolua/9router/pull/768) — thanks @DEYLNN) ### 🔒 Security diff --git a/open-sse/services/codexUsageQuotas.ts b/open-sse/services/codexUsageQuotas.ts index 6f595854d6..0a0d74a37e 100644 --- a/open-sse/services/codexUsageQuotas.ts +++ b/open-sse/services/codexUsageQuotas.ts @@ -41,14 +41,20 @@ function toNumber(value: unknown, fallback = 0): number { function parseResetTime(resetValue: unknown): string | null { if (!resetValue) return null; try { - const date = - resetValue instanceof Date - ? resetValue - : typeof resetValue === "number" - ? new Date(resetValue < 1e12 ? resetValue * 1000 : resetValue) - : typeof resetValue === "string" - ? new Date(resetValue) - : null; + 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 { diff --git a/open-sse/services/usage.ts b/open-sse/services/usage.ts index 4e8c338c24..29446c9ad8 100644 --- a/open-sse/services/usage.ts +++ b/open-sse/services/usage.ts @@ -1625,7 +1625,14 @@ function parseResetTime(resetValue: unknown): string | null { } else if (typeof resetValue === "number") { date = new Date(resetValue < 1e12 ? resetValue * 1000 : resetValue); } else if (typeof resetValue === "string") { - date = new Date(resetValue); + // Numeric strings are Unix timestamps too (seconds or milliseconds). + // `new Date("1700000000")` otherwise returns Invalid Date. + if (/^\d+$/.test(resetValue)) { + const ts = Number(resetValue); + date = new Date(ts < 1e12 ? ts * 1000 : ts); + } else { + date = new Date(resetValue); + } } else { return null; } diff --git a/tests/unit/usage-utils.test.ts b/tests/unit/usage-utils.test.ts index 3d16d002e4..70f50dcc34 100644 --- a/tests/unit/usage-utils.test.ts +++ b/tests/unit/usage-utils.test.ts @@ -60,6 +60,21 @@ describe("parseResetTime", () => { it("returns null for invalid date strings", () => { assert.equal(__testing.parseResetTime("not-a-date"), null); }); + + // Inspired-by upstream decolua/9router#768 — provider APIs sometimes return + // the reset timestamp as a numeric string. Without explicit detection, + // `new Date("1700000000")` returns Invalid Date and the value is lost. + it("parses a numeric string in seconds (value < 1e12)", () => { + const sec = "1700000000"; + const out = __testing.parseResetTime(sec); + assert.equal(out, new Date(Number(sec) * 1000).toISOString()); + }); + + it("parses a numeric string already in milliseconds (value >= 1e12)", () => { + const ms = "1700000000000"; + const out = __testing.parseResetTime(ms); + assert.equal(out, new Date(Number(ms)).toISOString()); + }); }); /* ------------------------------------------------------------------ */