fix(providers): stop quota card re-sorting Codex/GLM bars by remaining % (#6687)

QuotaCardExpanded.tsx unconditionally re-sorted quotas by remaining
percentage via sortQuotasByRemaining(), discarding the deterministic
CODEX_QUOTA_ORDER/GLM_QUOTA_ORDER window order quotaParsing.ts's
sortCodexOrder()/sortGlmOrder() had already established. A new
hasFixedQuotaOrder() + resolveQuotaDisplayOrder() skip the re-sort for
providers with a fixed window order (codex, glm family), threading
providerId from QuotaCard.tsx through to the display layer.

Regression guard: tests/unit/quota-card-expanded-fixed-order-6687.test.ts
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-09 04:56:46 -03:00
parent 1bc6da5318
commit 0e7bc3b4dc
5 changed files with 107 additions and 2 deletions

View File

@@ -23,6 +23,7 @@ _Living section — bullets land here as PRs merge into `release/v3.8.47` (paral
### 🐛 Bug Fixes
- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`.
- **fix(cli):** per-agent AgentBridge DNS toggle was broken for 8 of the 9 supported agents, and a failed MITM startup step could orphan the spawned proxy child — `addDNSEntry`/`removeDNSEntry` (`src/mitm/dns/dnsConfig.ts`) always resolved the legacy Antigravity default hosts regardless of which agent's toggle was flipped, so enabling DNS for Cursor/Codex/Claude Code/etc. silently added only `daily-cloudcode-pa.googleapis.com` while the DB recorded `dns_enabled=true` for the selected agent. Both functions now accept an optional `agentId` and resolve hosts via `ALL_TARGETS`; `POST /api/tools/agent-bridge/agents/[id]/dns` passes the route's `id` through and now returns 404 for an id that doesn't match a known target instead of silently falling back. Separately, `startMitmInternal()` (`src/mitm/manager.ts`) now wraps `generateCert()` (log + rethrow), the `provisionDnsEntries()` call, and the PID-file write in try/catch so a mid-startup failure can't orphan the already-spawned MITM child process. On Windows, `addDNSEntries`/`removeDNSEntries` also batch every missing/present entry into a single elevated PowerShell invocation instead of one UAC prompt per host line. Regression guard: `tests/unit/dns-config-generic.test.ts` (agent-specific resolution + batching), `tests/unit/agent-bridge-dns-route-validation.test.ts` (404 for unknown agent id). ([#6338](https://github.com/diegosouzapw/OmniRoute/pull/6338) — thanks @hamsa0x7)
- **fix(guardrails):** Vision Bridge's individual-model auto-reroute (route an image-bearing request straight to a vision-capable model instead of describe-then-forward) could bypass a policy-restricted API key's model allowlist/budget ([#6640](https://github.com/diegosouzapw/OmniRoute/pull/6640)) — `VisionBridgeGuardrail.preCall()` (`src/lib/guardrails/visionBridge.ts`) swaps `body.model` to the best available vision-capable model, but that swap happens in the guardrail pipeline AFTER `chat.ts` already called `enforceApiKeyPolicy()` against the ORIGINAL model, so a key scoped to a narrow `allowedModels` list could still execute against an unvetted (and possibly costlier) vision model the reroute picked. `chat.ts` now re-validates any guardrail-driven model change against the same per-key allowlist (`isModelAllowedForKey`) before honoring it, falling back to the original already-approved model when the reroute target is not allowed. The reroute path also now honors an explicit `settings.visionBridgeModel` operator override (previously ignored, unlike the combo/describe path a few lines below it, which already respects it via `getVisionBridgeConfig`). Regression guard: `tests/unit/guardrails/visionBridge.test.ts` (22 tests). (thanks @herjarsa)
- **fix(auth):** an API key restricted via `allowedModels`/`allowedCombos` could bypass that restriction entirely over the Codex Responses-over-WebSocket bridge ([#6564](https://github.com/diegosouzapw/OmniRoute/issues/6564)) — `prepare()` in `src/app/api/internal/codex-responses-ws/route.ts` authenticated the WS bridge's API key (`authenticate()`/`authorizeWebSocketHandshake()`) and honored `allowedConnections`, but never called `enforceApiKeyPolicy()`, the same model/combo policy gate the HTTP `/v1/responses` path enforces via `handleChat()` — so a key scoped to e.g. `combo/model-1.0` could still reach a direct Codex model like `gpt-5.5` through this transport, as long as an eligible Codex OAuth connection existed. The bridge's WS auth token arrives via query params (`api_key`/`token`/`access_token`), not a normal `Authorization` header, so a new `enforceCodexWsApiKeyPolicy()` builds an equivalent `Request` carrying an explicit `Authorization: Bearer <apiKey>` header and calls `enforceApiKeyPolicy()` against the CLIENT-requested model, before any Codex-specific model remapping or credential selection. Regression guard: `tests/unit/codex-ws-policy-enforcement-6564.test.ts` (a model-restricted key is rejected 403 before reaching credential selection; a combo-restricted key is rejected 403 requesting a disallowed combo; a key that DOES allow the requested model still proceeds past policy).

View File

@@ -110,6 +110,7 @@ export default function QuotaCard({
/>
<QuotaCardExpanded
quotas={quotas}
providerId={connection.provider}
loading={loading}
error={error}
message={quota?.message ?? null}

View File

@@ -11,6 +11,7 @@ import {
} from "../utils";
import QuotaMiniBar from "../QuotaMiniBar";
import { translateUsageOrFallback, type UsageTranslationValues } from "../i18nFallback";
import { hasFixedQuotaOrder } from "../quotaParsing";
const CURRENCY_SYMBOLS: Record<string, string> = {
USD: "$",
@@ -31,6 +32,18 @@ export function sortQuotasByRemaining(quotas: any[]): any[] {
);
}
/**
* Pure helper — resolves the display order for a provider's quotas.
* Providers with a deterministic fixed-window order (codex, glm family — see
* quotaParsing.ts's sortCodexOrder()/sortGlmOrder()) keep the order
* parseQuotaData() already established. Every other provider still gets the
* remaining-percentage sort. Fixes #6687 (bars re-sorted by % undid the fixed
* session/weekly order).
*/
export function resolveQuotaDisplayOrder(providerId: string | undefined, quotas: any[]): any[] {
return hasFixedQuotaOrder(providerId) ? [...quotas] : sortQuotasByRemaining(quotas);
}
/** Pure helper — slices the sorted quotas down to the visible window. */
export function getVisibleQuotas(sortedQuotas: any[], expanded: boolean): any[] {
return expanded ? sortedQuotas : sortedQuotas.slice(0, DEFAULT_VISIBLE_ROWS);
@@ -38,6 +51,7 @@ export function getVisibleQuotas(sortedQuotas: any[], expanded: boolean): any[]
interface Props {
quotas: any[];
providerId?: string;
loading: boolean;
error: string | null;
message?: string | null;
@@ -155,6 +169,7 @@ function QuotaDetailRow({ q }: { q: any }) {
export default function QuotaCardExpanded({
quotas,
providerId,
loading,
error,
message,
@@ -174,7 +189,10 @@ export default function QuotaCardExpanded({
translateUsageOrFallback(t, key, fallback, values);
const [expanded, setExpanded] = useState(false);
const sortedQuotas = useMemo(() => sortQuotasByRemaining(quotas), [quotas]);
const sortedQuotas = useMemo(
() => resolveQuotaDisplayOrder(providerId, quotas),
[quotas, providerId]
);
const visibleQuotas = useMemo(
() => getVisibleQuotas(sortedQuotas, expanded),
[sortedQuotas, expanded]

View File

@@ -9,6 +9,17 @@ const CODEX_QUOTA_ORDER: Record<string, number> = {
gpt_5_3_codex_spark_weekly: 3,
banked_reset_credits: 4,
};
const GLM_FAMILY_PROVIDERS = ["glm", "glm-cn", "glmt", "opencode-go"];
/**
* Providers whose quotas already get a deterministic fixed-window order from
* sortGlmOrder()/sortCodexOrder() below. Display layers (e.g. QuotaCardExpanded)
* must not re-sort these by remaining percentage, or they undo this order (#6687).
*/
export function hasFixedQuotaOrder(providerId: string | undefined): boolean {
const id = String(providerId || "").toLowerCase();
return id === "codex" || GLM_FAMILY_PROVIDERS.includes(id);
}
function quotaEntries(data: any): Array<[string, any]> {
return data?.quotas && typeof data.quotas === "object" ? Object.entries(data.quotas) : [];
@@ -190,7 +201,7 @@ function sortProviderModelOrder(provider: string, quotas: any[]) {
}
function sortGlmOrder(providerId: string, quotas: any[]) {
if (!["glm", "glm-cn", "glmt", "opencode-go"].includes(providerId)) return;
if (!GLM_FAMILY_PROVIDERS.includes(providerId)) return;
quotas.sort((a, b) => (GLM_QUOTA_ORDER[a.name] ?? 99) - (GLM_QUOTA_ORDER[b.name] ?? 99));
}

View File

@@ -0,0 +1,74 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { parseQuotaData } from "@/app/(dashboard)/dashboard/usage/components/ProviderLimits/quotaParsing";
import { resolveQuotaDisplayOrder } from "@/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardExpanded";
interface TestQuota {
name: string;
[key: string]: unknown;
}
// Regression for #6687: quotaParsing.ts applies a deterministic CODEX_QUOTA_ORDER
// (session:0, weekly:1, ...) via sortCodexOrder(). QuotaCardExpanded.tsx used to
// unconditionally re-sort the already-ordered array by remaining percentage via
// sortQuotasByRemaining(), discarding the fixed window order. resolveQuotaDisplayOrder()
// is the fix: it keeps parseQuotaData()'s order for providers with a fixed window
// order (codex, glm family) and only applies the remaining-% sort elsewhere.
test("#6687 codex: display order stays session-then-weekly regardless of remaining %", () => {
const rawCodexData = {
quotas: {
session: { used: 90, total: 100, remainingPercentage: 10, resetAt: null },
weekly: { used: 10, total: 100, remainingPercentage: 90, resetAt: null },
},
};
const parsed = parseQuotaData("codex", rawCodexData);
// Sanity check: quotaParsing.ts's sortCodexOrder should order session before weekly.
assert.deepEqual(
parsed.map((q: TestQuota) => q.name),
["session", "weekly"]
);
// The display layer must NOT undo the fixed order, even though weekly
// (90% remaining) would sort before session (10% remaining) by percentage.
const displayed = resolveQuotaDisplayOrder("codex", parsed);
assert.deepEqual(
displayed.map((q: TestQuota) => q.name),
["session", "weekly"]
);
});
test("#6687 glm family: display order stays session-then-weekly regardless of remaining %", () => {
const rawGlmData = {
quotas: {
session: { used: 95, total: 100, remainingPercentage: 5, resetAt: null },
weekly: { used: 5, total: 100, remainingPercentage: 95, resetAt: null },
},
};
const parsed = parseQuotaData("glm", rawGlmData);
assert.deepEqual(
parsed.map((q: TestQuota) => q.name),
["session", "weekly"]
);
const displayed = resolveQuotaDisplayOrder("glm", parsed);
assert.deepEqual(
displayed.map((q: TestQuota) => q.name),
["session", "weekly"]
);
});
test("#6687 non-fixed-order providers still sort by remaining percentage descending", () => {
const quotas = [
{ name: "low", remainingPercentage: 10 },
{ name: "high", remainingPercentage: 90 },
];
const displayed = resolveQuotaDisplayOrder("openai", quotas);
assert.deepEqual(
displayed.map((q: TestQuota) => q.name),
["high", "low"]
);
});