mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-24 08:02:14 +03:00
fix(usage): order quota windows chronologically on every provider card (#11241)
Validated on the combined 12-PR batch board: repro-7764 suite 17/17 on this tree (11/17 fail on a clean tip without the fix — proper red-to-green), typecheck:core clean, gates within baseline. Quota windows now order chronologically from the data shape instead of a provider whitelist. Thank you @pacocartones!
This commit is contained in:
1
changelog.d/fixes/7764-quota-window-order.md
Normal file
1
changelog.d/fixes/7764-quota-window-order.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(usage):** keep session/weekly/monthly quota windows in chronological order on every provider card. The order is now derived from the quota keys themselves instead of a provider whitelist, so Claude, MiniMax, Z.ai and Command Code stop rendering the two bars in opposite positions across sibling accounts ([#7764](https://github.com/diegosouzapw/OmniRoute/issues/7764))
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
} from "../utils";
|
||||
import QuotaMiniBar from "../QuotaMiniBar";
|
||||
import { translateUsageOrFallback, type UsageTranslationValues } from "../i18nFallback";
|
||||
import { hasFixedQuotaOrder } from "../quotaParsing";
|
||||
import { hasFixedQuotaOrder, hasCanonicalWindowOrder, sortQuotasByWindow } from "../quotaParsing";
|
||||
|
||||
const CURRENCY_SYMBOLS: Record<string, string> = {
|
||||
USD: "$",
|
||||
@@ -92,9 +92,17 @@ export function sortQuotasByRemaining(quotas: any[]): any[] {
|
||||
* parseQuotaData() already established. Every other provider still gets the
|
||||
* remaining-percentage sort. Fixes #6687 (bars re-sorted by % undid the fixed
|
||||
* session/weekly order).
|
||||
*
|
||||
* #7764 residual: providers outside that whitelist which nonetheless report
|
||||
* rolling time windows (claude, minimax, zai, command-code, ...) are ordered
|
||||
* chronologically via `hasCanonicalWindowOrder`/`sortQuotasByWindow`, so the
|
||||
* expanded card agrees with the collapsed card (`topQuotas`) and with sibling
|
||||
* accounts of the same provider.
|
||||
*/
|
||||
export function resolveQuotaDisplayOrder(providerId: string | undefined, quotas: any[]): any[] {
|
||||
return hasFixedQuotaOrder(providerId) ? [...quotas] : sortQuotasByRemaining(quotas);
|
||||
if (hasFixedQuotaOrder(providerId)) return [...quotas];
|
||||
if (hasCanonicalWindowOrder(quotas)) return sortQuotasByWindow(quotas);
|
||||
return sortQuotasByRemaining(quotas);
|
||||
}
|
||||
|
||||
/** Pure helper — slices the sorted quotas down to the visible window. */
|
||||
|
||||
@@ -22,6 +22,74 @@ export function hasFixedQuotaOrder(providerId: string | undefined): boolean {
|
||||
return id === "codex" || GLM_FAMILY_PROVIDERS.includes(id) || KIMI_CODING_PROVIDERS.includes(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonical chronological rank of a rolling usage window, derived from the
|
||||
* quota key itself rather than from a provider list.
|
||||
*
|
||||
* Providers name the same two windows in mutually incompatible ways —
|
||||
* `"session (5h)"` (claude, minimax, kimi), `"5 Hours Quota"` (GLM/zai),
|
||||
* `"five_hour"` (command-code, qwen-token-plan), `"code_5h"` (kimi-coding),
|
||||
* plain `"session"` (codex) — so matching on the shape of the key is the only
|
||||
* thing that generalizes. Returns `null` for anything that is not a recognizable
|
||||
* time window (per-model buckets, credit balances, token counters), which is
|
||||
* what keeps this from claiming quotas it has no opinion about.
|
||||
*/
|
||||
export function quotaWindowRank(name: unknown): number | null {
|
||||
const key = String(name ?? "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (!key) return null;
|
||||
// Order matters: "mcp_monthly" must not be caught by the weekly probe, and
|
||||
// "5 Hours Quota" must not be caught by anything before the session probe.
|
||||
if (/month/.test(key)) return 2;
|
||||
if (/week|7\s*d\b|_7d\b|seven[_\s-]?day/.test(key)) return 1;
|
||||
if (/session|hour|\b5\s*h\b|_5h\b/.test(key)) return 0;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* #7764: whether a quota list is a set of rolling time windows whose relative
|
||||
* order is inherent (session before weekly before monthly) and must therefore
|
||||
* survive rendering.
|
||||
*
|
||||
* This is the structural counterpart to the provider whitelist above. The
|
||||
* whitelist exists because a few providers need an order the window rank cannot
|
||||
* express (Codex interleaves GPT-5.3-Codex-Spark windows and a banked-credit
|
||||
* row between the canonical ones), but it went stale the moment any other
|
||||
* provider started reporting session+weekly — claude, minimax, zai and
|
||||
* command-code all do. Deriving the answer from the data means the next such
|
||||
* provider is covered on arrival.
|
||||
*
|
||||
* Requires at least two DISTINCT ranks: with a single window there is no pair
|
||||
* to keep stable, so the pre-existing worst-status-first sort is left alone.
|
||||
*/
|
||||
export function hasCanonicalWindowOrder(quotas: unknown): boolean {
|
||||
if (!Array.isArray(quotas)) return false;
|
||||
const ranks = new Set<number>();
|
||||
for (const quota of quotas) {
|
||||
if (!quota || (quota as any).isCredits) continue;
|
||||
const rank = quotaWindowRank((quota as any).name);
|
||||
if (rank !== null) ranks.add(rank);
|
||||
}
|
||||
return ranks.size >= 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stable sort of a quota list into canonical window order. Unrecognized entries
|
||||
* (credits, token counters, per-model buckets) sink below the windows while
|
||||
* keeping their relative order, so nothing is lost or shuffled.
|
||||
*/
|
||||
export function sortQuotasByWindow<T>(quotas: T[]): T[] {
|
||||
return [...quotas]
|
||||
.map((quota, index) => ({ quota, index }))
|
||||
.sort((a, b) => {
|
||||
const ra = quotaWindowRank((a.quota as any)?.name) ?? 99;
|
||||
const rb = quotaWindowRank((b.quota as any)?.name) ?? 99;
|
||||
return ra - rb || a.index - b.index;
|
||||
})
|
||||
.map((entry) => entry.quota);
|
||||
}
|
||||
|
||||
function quotaEntries(data: any): Array<[string, any]> {
|
||||
return data?.quotas && typeof data.quotas === "object" ? Object.entries(data.quotas) : [];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export { parseQuotaData } from "./quotaParsing";
|
||||
import { hasFixedQuotaOrder } from "./quotaParsing";
|
||||
import { hasFixedQuotaOrder, hasCanonicalWindowOrder, sortQuotasByWindow } from "./quotaParsing";
|
||||
|
||||
const PROVIDER_PLAN_FALLBACKS = new Set([
|
||||
"claude code",
|
||||
@@ -400,6 +400,15 @@ export function topQuotas(quotas: any[], n = 3, providerId?: string): any[] {
|
||||
return filtered.slice(0, n);
|
||||
}
|
||||
|
||||
// #7764 residual: any OTHER provider reporting rolling time windows (claude,
|
||||
// minimax, zai, command-code, ...) has an equally inherent session→weekly→
|
||||
// monthly order. Re-sorting those by remaining % makes two accounts of the
|
||||
// same provider render the bars in opposite positions. Detected from the
|
||||
// quota keys, so a new provider needs no list update.
|
||||
if (hasCanonicalWindowOrder(filtered)) {
|
||||
return sortQuotasByWindow(filtered).slice(0, n);
|
||||
}
|
||||
|
||||
return [...filtered]
|
||||
.sort((a, b) => {
|
||||
const sa = STATUS_ORDER[quotaStatus(a)];
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
parseQuotaData,
|
||||
hasFixedQuotaOrder,
|
||||
} from "@/app/(dashboard)/dashboard/usage/components/ProviderLimits/quotaParsing";
|
||||
import { resolveQuotaDisplayOrder } from "@/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardExpanded";
|
||||
|
||||
const quotaName = (quota: { name: string }) => quota.name;
|
||||
|
||||
@@ -62,3 +63,128 @@ test("#7764: providers WITHOUT a fixed order still sort worst-status-first (no r
|
||||
const rendered = topQuotas(quotas, 3, "some-other-provider").map(quotaName);
|
||||
assert.deepEqual(rendered, ["beta", "gamma", "alpha"]);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #7764 residual: the original fix only whitelisted codex / GLM family / Kimi
|
||||
// Coding in `hasFixedQuotaOrder`. Every OTHER provider that reports the same
|
||||
// session + weekly rolling windows still gets re-sorted by remaining %, so two
|
||||
// accounts of the SAME provider render the two bars in opposite positions
|
||||
// depending on which window happens to be more depleted — the exact symptom in
|
||||
// the report ("the indicators are not located in the same position per card").
|
||||
//
|
||||
// Quota names below are the real upstream keys, not simplified ones:
|
||||
// claude → open-sse/services/usage/claude.ts:107,112 "session (5h)" / "weekly (7d)"
|
||||
// minimax → open-sse/services/usage/minimax.ts:312,325 "session (5h)" / "weekly (7d)"
|
||||
// zai → routed to getGlmUsage (open-sse/services/usage.ts:191-194)
|
||||
// so it emits "5 Hours Quota" / "Weekly Quota" (glm.ts:33-34)
|
||||
// command-code → open-sse/services/usage/command-code.ts:193,196 "five_hour" / "weekly"
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Two refreshes of the same account family: in A the weekly window is the
|
||||
* depleted one, in B it is the session window. A remaining-% sort flips the
|
||||
* row order between the two; a canonical window order does not. */
|
||||
function windowPair(sessionKey: string, weeklyKey: string) {
|
||||
return {
|
||||
depletedWeekly: {
|
||||
quotas: {
|
||||
[sessionKey]: { used: 9, total: 100, remainingPercentage: 91, resetAt: null },
|
||||
[weeklyKey]: { used: 97, total: 100, remainingPercentage: 3, resetAt: null },
|
||||
},
|
||||
},
|
||||
depletedSession: {
|
||||
quotas: {
|
||||
[sessionKey]: { used: 99, total: 100, remainingPercentage: 1, resetAt: null },
|
||||
[weeklyKey]: { used: 43, total: 100, remainingPercentage: 57, resetAt: null },
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const WINDOW_PROVIDERS: Array<{ provider: string; session: string; weekly: string }> = [
|
||||
{ provider: "claude", session: "session (5h)", weekly: "weekly (7d)" },
|
||||
{ provider: "minimax", session: "session (5h)", weekly: "weekly (7d)" },
|
||||
{ provider: "minimax-cn", session: "session (5h)", weekly: "weekly (7d)" },
|
||||
{ provider: "zai", session: "5 Hours Quota", weekly: "Weekly Quota" },
|
||||
{ provider: "command-code", session: "five_hour", weekly: "weekly" },
|
||||
];
|
||||
|
||||
for (const { provider, session, weekly } of WINDOW_PROVIDERS) {
|
||||
test(`#7764 residual: ${provider} keeps session before weekly in the collapsed card across refreshes`, () => {
|
||||
const { depletedWeekly, depletedSession } = windowPair(session, weekly);
|
||||
const parsedA = parseQuotaData(provider, depletedWeekly);
|
||||
const parsedB = parseQuotaData(provider, depletedSession);
|
||||
|
||||
// parseQuotaData already yields the canonical upstream order for both.
|
||||
assert.deepEqual(parsedA.map(quotaName), [session, weekly]);
|
||||
assert.deepEqual(parsedB.map(quotaName), [session, weekly]);
|
||||
|
||||
assert.deepEqual(
|
||||
topQuotas(parsedA, 3, provider).map(quotaName),
|
||||
[session, weekly],
|
||||
`${provider}: collapsed card must not reorder rolling windows by remaining %`
|
||||
);
|
||||
assert.deepEqual(
|
||||
topQuotas(parsedB, 3, provider).map(quotaName),
|
||||
[session, weekly],
|
||||
`${provider}: window order must be identical on the sibling account`
|
||||
);
|
||||
});
|
||||
|
||||
test(`#7764 residual: ${provider} expanded card window order matches the collapsed card`, () => {
|
||||
const { depletedWeekly, depletedSession } = windowPair(session, weekly);
|
||||
const parsedA = parseQuotaData(provider, depletedWeekly);
|
||||
const parsedB = parseQuotaData(provider, depletedSession);
|
||||
|
||||
assert.deepEqual(resolveQuotaDisplayOrder(provider, parsedA).map(quotaName), [session, weekly]);
|
||||
assert.deepEqual(resolveQuotaDisplayOrder(provider, parsedB).map(quotaName), [session, weekly]);
|
||||
});
|
||||
}
|
||||
|
||||
test("#7764 residual: a card whose quotas are NOT rolling windows still sorts worst-first", () => {
|
||||
// Antigravity-style per-model buckets: no canonical chronological order
|
||||
// exists, so the worst-status-first sort remains the useful one.
|
||||
const parsed = parseQuotaData("antigravity", {
|
||||
quotas: {
|
||||
"gemini-3-pro": { used: 10, total: 100, remainingPercentage: 90 },
|
||||
"gemini-3-flash": { used: 95, total: 100, remainingPercentage: 5 },
|
||||
},
|
||||
});
|
||||
assert.deepEqual(topQuotas(parsed, 3, "antigravity").map(quotaName), [
|
||||
"gemini-3-flash",
|
||||
"gemini-3-pro",
|
||||
]);
|
||||
});
|
||||
|
||||
test("#7764 residual: a single rolling window plus credits is left to the remaining-% sort", () => {
|
||||
// Only ONE window → no two windows to keep in a stable relative order, so
|
||||
// nothing is claimed and the pre-existing behaviour is preserved.
|
||||
const quotas = [
|
||||
{ name: "credits", used: 0, total: 0, remainingPercentage: 90, isCredits: true },
|
||||
{ name: "session (5h)", used: 95, total: 100, remainingPercentage: 5 },
|
||||
];
|
||||
assert.deepEqual(topQuotas(quotas, 3, "some-credit-provider").map(quotaName), [
|
||||
"session (5h)",
|
||||
"credits",
|
||||
]);
|
||||
});
|
||||
|
||||
test("#7764 residual: Claude per-model weekly windows keep upstream order and credits sink last", () => {
|
||||
// Anthropic reports extra `weekly <model> (7d)` buckets plus an extra_usage
|
||||
// credits row. The window sort must be STABLE: same-rank siblings keep the
|
||||
// order parseQuotaData produced, and the credits row is not promoted.
|
||||
const parsed = parseQuotaData("claude", {
|
||||
quotas: {
|
||||
"session (5h)": { used: 9, total: 100, remainingPercentage: 91 },
|
||||
"weekly (7d)": { used: 97, total: 100, remainingPercentage: 3 },
|
||||
"weekly designer (7d)": { used: 50, total: 100, remainingPercentage: 50 },
|
||||
},
|
||||
extraUsage: { is_enabled: true, monthly_limit: 100, used_credits: 10, utilization: 10 },
|
||||
});
|
||||
|
||||
assert.deepEqual(topQuotas(parsed, 4, "claude").map(quotaName), [
|
||||
"session (5h)",
|
||||
"weekly (7d)",
|
||||
"weekly designer (7d)",
|
||||
"extra_usage",
|
||||
]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user