From a37ee2fac5ff21bdd27d5d64d4176f766fddf021 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Sun, 21 Jun 2026 14:06:18 -0300 Subject: [PATCH] feat(usage): surface Codex code-review weekly window + additional_rate_limits fallback (#4494) Rebuilt onto release/v3.8.33 (squash-base-stale). Integrated into release/v3.8.33. --- CHANGELOG.md | 1 + open-sse/services/codexUsageQuotas.ts | 78 ++++++++++++- .../usage/components/ProviderLimits/utils.tsx | 1 + .../codex-usage-quotas-review-window.test.ts | 109 ++++++++++++++++++ tests/unit/provider-limits-ui.test.ts | 1 + 5 files changed, 185 insertions(+), 5 deletions(-) create mode 100644 tests/unit/codex-usage-quotas-review-window.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d53ecb4e65..58fdc4f764 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ _In development — bullets added per PR; finalized at release._ - **feat(ui): expose a `targetFormat` selector in the custom-models form** — the custom-models form now lets you pick the upstream target format explicitly, so a custom model can be pinned to the right wire format instead of relying on inference. ([#4475](https://github.com/diegosouzapw/OmniRoute/pull/4475) — thanks @adivekar-utexas) - **feat(providers): expose `gpt-4o` on the built-in GitHub Copilot (`gh`) provider** — GitHub Copilot still serves the original `gpt-4o` chat model via its `/chat/completions` endpoint, but the OmniRoute registry only shipped the GPT-5.x family, so clients that explicitly request `gpt-4o` against `gh` got an unknown-model error. `gpt-4o` is now registered under the `github` provider next to the GPT-5.x lineup (chat/completions, 128k context — no `openai-responses` targetFormat). Ported from [9router#98](https://github.com/decolua/9router/pull/98). (thanks @I3eka) - **feat(pricing): default pricing for Qwen `coder-model` on the `qw` provider** — the Qwen Coder Free (`qw`) registry already exposed the `coder-model` id (Qwen3.5/3.6 Coder Model) but `DEFAULT_PRICING.qw` was missing the row, so usage tracking reported `$0.00` for that model. The pricing row is now added with the same shape as the sibling `vision-model` tier, restoring non-zero cost tracking. Ported from upstream 9router PR [decolua/9router#156](https://github.com/decolua/9router/pull/156). (thanks @LinearSakana) +- **feat(usage): Codex review-quota now surfaces the weekly window and the `additional_rate_limits` fallback shape** — the dashboard's Codex usage card showed only the **session** half of `code_review_rate_limit` and dropped review descriptors that arrived inside `additional_rate_limits` (the shape some ChatGPT Codex plans report). `buildCodexUsageQuotas` now emits the secondary window as `quotas.code_review_weekly` and, when the dedicated `code_review_rate_limit` block is empty, falls back to the matching descriptor in `additional_rate_limits` (matched on `limit_name`/`metered_feature`/`limit_id` containing `code_review` / `codex_review` / `review`). The new label `code_review_weekly → "Code Review Weekly"` is registered in `ProviderLimits/utils.tsx` so the card renders both windows side-by-side. The existing `quotas.code_review` key is preserved for back-compat. Inspired by upstream decolua/9router PR #836. (thanks @hiepau1231) ### 🐛 Fixed diff --git a/open-sse/services/codexUsageQuotas.ts b/open-sse/services/codexUsageQuotas.ts index 0a0d74a37e..6696daca0b 100644 --- a/open-sse/services/codexUsageQuotas.ts +++ b/open-sse/services/codexUsageQuotas.ts @@ -113,6 +113,53 @@ function findCodexSparkRateLimit(data: JsonRecord): JsonRecord { return {}; } +/** + * Upstream parity (decolua/9router PR #836): some ChatGPT Codex plans report + * the review-window rate limit inside `additional_rate_limits` rather than the + * dedicated `code_review_rate_limit` block. Detect that descriptor so the + * caller can fall back to it when the dedicated block is empty. + */ +function isCodexReviewLimitDescriptor(...values: unknown[]): boolean { + return values.some((value) => { + if (typeof value !== "string") return false; + const normalized = value.trim().toLowerCase(); + if (!normalized) return false; + return ( + normalized === "code_review" || + normalized === "codex_review" || + normalized === "review" || + normalized.includes("code_review") || + normalized.includes("codex_review") || + normalized.includes("code review") + ); + }); +} + +function findCodexReviewRateLimit(data: JsonRecord): JsonRecord { + const additionalRateLimits = getFieldValue( + data, + "additional_rate_limits", + "additionalRateLimits" + ); + if (!Array.isArray(additionalRateLimits)) return {}; + + for (const entryValue of additionalRateLimits) { + const entry = toRecord(entryValue); + if ( + isCodexReviewLimitDescriptor( + getFieldValue(entry, "limit_name", "limitName"), + getFieldValue(entry, "metered_feature", "meteredFeature"), + getFieldValue(entry, "limit_id", "limitId"), + entry["id"], + entry["name"] + ) + ) { + return toRecord(getFieldValue(entry, "rate_limit", "rateLimit")); + } + } + return {}; +} + export function buildCodexUsageQuotas(dataValue: unknown): { rateLimit: JsonRecord; quotas: Record; @@ -128,12 +175,23 @@ export function buildCodexUsageQuotas(dataValue: unknown): { if (Object.keys(secondaryWindow).length > 0) quotas.weekly = buildPercentageQuota(secondaryWindow); + // Resolve the code-review rate limit block. ChatGPT Codex exposes the same + // information under two different shapes depending on the plan tier + // (decolua/9router PR #836): + // 1. Dedicated `code_review_rate_limit` block at the top level (preferred). + // 2. An entry inside `additional_rate_limits` with a `code_review` / + // `review` descriptor (fallback for plans that bucket every secondary + // limit into the same array). + const dedicatedReviewRateLimit = toRecord( + getFieldValue(data, "code_review_rate_limit", "codeReviewRateLimit") + ); + const reviewRateLimit = + Object.keys(dedicatedReviewRateLimit).length > 0 + ? dedicatedReviewRateLimit + : findCodexReviewRateLimit(data); + const codeReviewWindow = toRecord( - getFieldValue( - toRecord(getFieldValue(data, "code_review_rate_limit", "codeReviewRateLimit")), - "primary_window", - "primaryWindow" - ) + getFieldValue(reviewRateLimit, "primary_window", "primaryWindow") ); if ( getFieldValue(codeReviewWindow, "used_percent", "usedPercent") !== null || @@ -142,6 +200,16 @@ export function buildCodexUsageQuotas(dataValue: unknown): { quotas.code_review = buildPercentageQuota(codeReviewWindow); } + const codeReviewSecondaryWindow = toRecord( + getFieldValue(reviewRateLimit, "secondary_window", "secondaryWindow") + ); + if ( + getFieldValue(codeReviewSecondaryWindow, "used_percent", "usedPercent") !== null || + getFieldValue(codeReviewSecondaryWindow, "remaining_count", "remainingCount") !== null + ) { + quotas.code_review_weekly = buildPercentageQuota(codeReviewSecondaryWindow); + } + const sparkRateLimit = findCodexSparkRateLimit(data); const sparkPrimaryWindow = toRecord( getFieldValue(sparkRateLimit, "primary_window", "primaryWindow") diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx index e7488b8667..79735257ab 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx @@ -18,6 +18,7 @@ const QUOTA_LABEL_MAP: Record = { session: "Session", weekly: "Weekly", code_review: "Code Review", + code_review_weekly: "Code Review Weekly", gpt_5_3_codex_spark_session: "GPT-5.3-Codex-Spark", gpt_5_3_codex_spark_weekly: "GPT-5.3-Codex-Spark Weekly", agentic_request: "Agentic", diff --git a/tests/unit/codex-usage-quotas-review-window.test.ts b/tests/unit/codex-usage-quotas-review-window.test.ts new file mode 100644 index 0000000000..27d796a248 --- /dev/null +++ b/tests/unit/codex-usage-quotas-review-window.test.ts @@ -0,0 +1,109 @@ +/** + * Regression test for Codex review-quota plumbing. + * + * Upstream parity: decolua/9router PR #836 surfaces the SECONDARY window of + * `code_review_rate_limit` and supports descriptors that arrive via the + * `additional_rate_limits` array (some ChatGPT plans report the review limit + * there rather than in the dedicated `code_review_rate_limit` block). + * + * Before this fix: + * - `buildCodexUsageQuotas` only emitted `quotas.code_review` from the + * primary window of `code_review_rate_limit`, so the WEEKLY review window + * was invisible to the dashboard. + * - Review limits surfaced under `additional_rate_limits` (with + * `limit_name`/`metered_feature` containing "review") were dropped. + * + * After this fix: + * - The secondary window of `code_review_rate_limit` is emitted as + * `quotas.code_review_weekly` (parallel to `session`/`weekly`). + * - Review entries inside `additional_rate_limits` populate the same + * `code_review`/`code_review_weekly` keys when the dedicated block is + * absent. + * - The primary `code_review` key is preserved (backward-compat for the + * existing dashboard rendering & the usage-service-hardening regression). + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { buildCodexUsageQuotas } from "../../open-sse/services/codexUsageQuotas.ts"; + +test("buildCodexUsageQuotas surfaces the secondary code_review window", () => { + const { quotas } = buildCodexUsageQuotas({ + rate_limit: { + primary_window: { used_percent: 25 }, + secondary_window: { used_percent: 50 }, + }, + code_review_rate_limit: { + primary_window: { used_percent: 40, reset_after_seconds: 45 }, + secondary_window: { used_percent: 70, reset_after_seconds: 6000 }, + }, + }); + + // Pre-existing primary window stays under `code_review` (back-compat). + assert.equal(quotas.code_review?.used, 40); + assert.equal(quotas.code_review?.remaining, 60); + + // New: secondary window exposed as `code_review_weekly`. + assert.equal(quotas.code_review_weekly?.used, 70); + assert.equal(quotas.code_review_weekly?.remaining, 30); + assert.equal(quotas.code_review_weekly?.total, 100); + assert.equal(quotas.code_review_weekly?.unlimited, false); +}); + +test("buildCodexUsageQuotas reads review windows from additional_rate_limits", () => { + const { quotas } = buildCodexUsageQuotas({ + rate_limit: { + primary_window: { used_percent: 10 }, + }, + additional_rate_limits: [ + { + limit_name: "Code Review", + metered_feature: "code_review", + rate_limit: { + primary_window: { used_percent: 33 }, + secondary_window: { used_percent: 55 }, + }, + }, + ], + }); + + assert.equal(quotas.code_review?.used, 33); + assert.equal(quotas.code_review?.remaining, 67); + assert.equal(quotas.code_review_weekly?.used, 55); + assert.equal(quotas.code_review_weekly?.remaining, 45); +}); + +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 }, + }, + }); + + assert.equal(quotas.session?.used, 10); + assert.equal(quotas.weekly?.used, 20); + assert.equal(quotas.code_review, undefined); + assert.equal(quotas.code_review_weekly, undefined); +}); + +test("buildCodexUsageQuotas prefers dedicated block over additional_rate_limits fallback", () => { + const { quotas } = buildCodexUsageQuotas({ + code_review_rate_limit: { + primary_window: { used_percent: 11 }, + secondary_window: { used_percent: 22 }, + }, + additional_rate_limits: [ + { + limit_name: "review", + rate_limit: { + primary_window: { used_percent: 99 }, + secondary_window: { used_percent: 99 }, + }, + }, + ], + }); + + assert.equal(quotas.code_review?.used, 11); + assert.equal(quotas.code_review_weekly?.used, 22); +}); diff --git a/tests/unit/provider-limits-ui.test.ts b/tests/unit/provider-limits-ui.test.ts index 4ddeb8ec94..fb9741fe72 100644 --- a/tests/unit/provider-limits-ui.test.ts +++ b/tests/unit/provider-limits-ui.test.ts @@ -146,6 +146,7 @@ test("quota labels normalize session and weekly windows while preserving readabl assert.equal(providerLimitUtils.formatQuotaLabel("weekly (7d)"), "Weekly"); assert.equal(providerLimitUtils.formatQuotaLabel("weekly sonnet (7d)"), "Weekly Sonnet"); assert.equal(providerLimitUtils.formatQuotaLabel("code_review"), "Code Review"); + assert.equal(providerLimitUtils.formatQuotaLabel("code_review_weekly"), "Code Review Weekly"); assert.equal(providerLimitUtils.formatQuotaLabel("mcp_monthly"), "Monthly"); });