From 79a91359ea1d37976b74aabd83fc0e1af9783c93 Mon Sep 17 00:00:00 2001
From: luvs01 <27862058+luvs01@users.noreply.github.com>
Date: Sun, 2 Aug 2026 15:53:30 +0900
Subject: [PATCH 01/10] feat(gui): show weighted Codex pool capacity
Use configured plan weights to summarize routable Codex quota windows while retaining effective-account raw quota and incomplete coverage.
Show next capacity recovery as a share of the weighted pool without changing routing or account selection.
Refs #874
---
.../ProviderOverviewDashboard.tsx | 68 +++++-
gui/src/i18n/de.ts | 5 +
gui/src/i18n/en.ts | 5 +
gui/src/i18n/ja.ts | 5 +
gui/src/i18n/ko.ts | 5 +
gui/src/i18n/ru.ts | 5 +
gui/src/i18n/zh.ts | 5 +
gui/src/provider-workspace/report.ts | 106 +++++++--
.../styles/provider-overview-dashboard.css | 39 ++++
gui/tests/provider-capacity.test.ts | 41 ++++
src/codex/auth-api.ts | 4 +
src/providers/codex-capacity.ts | 219 ++++++++++++++++++
src/providers/quota.ts | 33 ++-
tests/provider-capacity.test.ts | 101 ++++++++
tests/provider-quota.test.ts | 16 +-
15 files changed, 623 insertions(+), 34 deletions(-)
create mode 100644 gui/tests/provider-capacity.test.ts
create mode 100644 src/providers/codex-capacity.ts
create mode 100644 tests/provider-capacity.test.ts
diff --git a/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx b/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx
index b4d188da50..5ae0d6c5b3 100644
--- a/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx
+++ b/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx
@@ -7,7 +7,12 @@ import { useMemo } from "react";
import { useT, useI18n } from "../../i18n/shared";
import { IconAlert, IconChevron } from "../../icons";
import type { WorkspaceSections, WorkspaceItem } from "../../provider-workspace/catalog";
-import { accountQuotaFromReport, type ProviderQuotaReportView } from "../../provider-workspace/report";
+import {
+ accountQuotaFromReport,
+ capacityAggregationFromReport,
+ type CapacityWindowView,
+ type ProviderQuotaReportView,
+} from "../../provider-workspace/report";
import {
attentionReasonKey,
buildAttentionItems,
@@ -162,13 +167,7 @@ export default function ProviderOverviewDashboard({
))}
@@ -236,6 +235,59 @@ export default function ProviderOverviewDashboard({
);
}
+function ProviderCapacityQuota({ report, pending }: { report: ProviderQuotaReportView; pending: boolean }) {
+ const t = useT();
+ const { locale } = useI18n();
+ const aggregation = capacityAggregationFromReport(report);
+ const recoveryRows: Array<{ label: string; window: CapacityWindowView }> = aggregation ? [
+ ...(aggregation.fiveHour ? [{ label: t("codexAuth.fiveHour"), window: aggregation.fiveHour }] : []),
+ ...(aggregation.weekly ? [{ label: t("codexAuth.weekly"), window: aggregation.weekly }] : []),
+ ...(aggregation.monthly ? [{ label: t("codexAuth.monthly"), window: aggregation.monthly }] : []),
+ ...(aggregation.customWindows ?? []).map(window => ({ label: window.label, window })),
+ ] : [];
+ const formatPercent = (value: number) => new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }).format(value);
+ const formatRecoveryAt = (value: number) => new Intl.DateTimeFormat(locale, {
+ dateStyle: "medium",
+ timeStyle: "short",
+ }).format(new Date(value > 10_000_000_000 ? value : value * 1000));
+
+ return (
+ <>
+ {aggregation && {t("pws.capacity.estimate")}
}
+
+ {aggregation && (
+
+ {recoveryRows.flatMap(({ label, window }) => (
+ window.nextRecoveryAt !== undefined && window.nextRecoveryPercent !== undefined
+ ? [
+ {t("pws.capacity.nextRecovery")} · {label} · {formatRecoveryAt(window.nextRecoveryAt)}
+ {t("pws.capacity.recoveryShare", { percent: formatPercent(window.nextRecoveryPercent) })}
+
]
+ : []
+ ))}
+ {aggregation.currentAccount?.quota && (
+
+
+ {t("pws.capacity.currentAccount")}
+ {aggregation.currentAccount.plan ? ` · ${aggregation.currentAccount.plan}` : ""}
+
+
+
+ )}
+ {aggregation.incomplete && (
+
+ {t("pws.capacity.incomplete", {
+ excluded: aggregation.excludedAccounts,
+ unknown: aggregation.unknownPlanAccounts,
+ })}
+
+ )}
+
+ )}
+ >
+ );
+}
+
function SummaryCard({ count, label, tone }: { count: number; label: string; tone: "ok" | "warn" | "muted" }) {
return (
diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts
index 2811acd6fd..c882769f28 100644
--- a/gui/src/i18n/de.ts
+++ b/gui/src/i18n/de.ts
@@ -1380,6 +1380,11 @@ export const de: Record
= {
"pws.dashboard.title": "Anbieterübersicht",
"pws.dashboard.subtitle": "Verwalten Sie alle Ihre Modellanbieter an einem Ort.",
"pws.dashboard.rateLimits": "RATE LIMITS",
+ "pws.capacity.estimate": "Konfiguriert gewichtete Pool-Schätzung",
+ "pws.capacity.currentAccount": "Aktuelles effektives Konto",
+ "pws.capacity.nextRecovery": "Nächste Kapazitätswiederherstellung",
+ "pws.capacity.recoveryShare": "+{percent} % Pool-Kapazität",
+ "pws.capacity.incomplete": "Unvollständige Abdeckung: {excluded} Konten ausgeschlossen, davon {unknown} mit unbekanntem Tarif",
"pws.dashboard.recentlyUsed": "KÜRZLICH VERWENDET",
"pws.dashboard.requests": "{count} Anfragen",
"pws.dashboard.checkedAgo": "Geprüft {time}",
diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts
index 877214622a..eda18bc277 100644
--- a/gui/src/i18n/en.ts
+++ b/gui/src/i18n/en.ts
@@ -1046,6 +1046,11 @@ export const en = {
"pws.dashboard.title": "Providers overview",
"pws.dashboard.subtitle": "Manage all your model providers in one place.",
"pws.dashboard.rateLimits": "RATE LIMITS",
+ "pws.capacity.estimate": "Configured-weight pool estimate",
+ "pws.capacity.currentAccount": "Current effective account",
+ "pws.capacity.nextRecovery": "Next capacity recovery",
+ "pws.capacity.recoveryShare": "+{percent}% pool capacity",
+ "pws.capacity.incomplete": "Incomplete coverage: {excluded} account(s) excluded, including {unknown} unknown plan(s)",
"pws.dashboard.recentlyUsed": "RECENTLY USED",
"pws.dashboard.requests": "{count} requests",
"pws.dashboard.checkedAgo": "Checked {time}",
diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts
index 97d5e3ee48..4e88dec9f8 100644
--- a/gui/src/i18n/ja.ts
+++ b/gui/src/i18n/ja.ts
@@ -996,6 +996,11 @@ export const ja: Record = {
"pws.dashboard.title": "プロバイダー概要",
"pws.dashboard.subtitle": "すべてのモデルプロバイダーを一か所で管理します。",
"pws.dashboard.rateLimits": "レート制限",
+ "pws.capacity.estimate": "設定済み重みによるプール推定",
+ "pws.capacity.currentAccount": "現在の有効アカウント",
+ "pws.capacity.nextRecovery": "次の容量回復",
+ "pws.capacity.recoveryShare": "+{percent}% のプール容量",
+ "pws.capacity.incomplete": "対象範囲が不完全です: {excluded} 件を除外(不明なプラン {unknown} 件)",
"pws.dashboard.recentlyUsed": "最近の使用",
"pws.dashboard.requests": "{count} リクエスト",
"pws.dashboard.checkedAgo": "{time} に確認",
diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts
index 474d007c0c..42098d5e12 100644
--- a/gui/src/i18n/ko.ts
+++ b/gui/src/i18n/ko.ts
@@ -1407,6 +1407,11 @@ export const ko: Record = {
"pws.dashboard.title": "프로바이더 개요",
"pws.dashboard.subtitle": "모든 모델 프로바이더를 한곳에서 관리합니다.",
"pws.dashboard.rateLimits": "사용량 제한",
+ "pws.capacity.estimate": "설정 가중치 기반 풀 추정치",
+ "pws.capacity.currentAccount": "현재 유효 계정",
+ "pws.capacity.nextRecovery": "다음 용량 회복",
+ "pws.capacity.recoveryShare": "+{percent}% 풀 용량",
+ "pws.capacity.incomplete": "불완전한 범위: {excluded}개 계정 제외, 알 수 없는 요금제 {unknown}개 포함",
"pws.dashboard.recentlyUsed": "최근 사용",
"pws.dashboard.requests": "{count}건 요청",
"pws.dashboard.checkedAgo": "{time} 전 확인",
diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts
index 80c4d8d395..c03eef27c7 100644
--- a/gui/src/i18n/ru.ts
+++ b/gui/src/i18n/ru.ts
@@ -1038,6 +1038,11 @@ export const ru: Record = {
"pws.dashboard.title": "Обзор провайдеров",
"pws.dashboard.subtitle": "Управляйте всеми провайдерами моделей в одном месте.",
"pws.dashboard.rateLimits": "Лимиты запросов",
+ "pws.capacity.estimate": "Оценка пула по настроенным весам",
+ "pws.capacity.currentAccount": "Текущая активная учётная запись",
+ "pws.capacity.nextRecovery": "Следующее восстановление ёмкости",
+ "pws.capacity.recoveryShare": "+{percent}% ёмкости пула",
+ "pws.capacity.incomplete": "Неполное покрытие: исключено аккаунтов: {excluded}, неизвестных планов: {unknown}",
"pws.dashboard.recentlyUsed": "Недавно использованные",
"pws.dashboard.requests": "{count} запросов",
"pws.dashboard.checkedAgo": "Проверено {time}",
diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts
index 7f24d45d32..6e63f86f5e 100644
--- a/gui/src/i18n/zh.ts
+++ b/gui/src/i18n/zh.ts
@@ -1400,6 +1400,11 @@ export const zh: Record = {
"pws.dashboard.title": "提供商概览",
"pws.dashboard.subtitle": "在一个地方管理所有模型提供商。",
"pws.dashboard.rateLimits": "速率限制",
+ "pws.capacity.estimate": "按配置权重估算的账户池",
+ "pws.capacity.currentAccount": "当前有效账户",
+ "pws.capacity.nextRecovery": "下一次容量恢复",
+ "pws.capacity.recoveryShare": "+{percent}% 账户池容量",
+ "pws.capacity.incomplete": "覆盖不完整:已排除 {excluded} 个账户,其中 {unknown} 个套餐未知",
"pws.dashboard.recentlyUsed": "最近使用",
"pws.dashboard.requests": "{count} 个请求",
"pws.dashboard.checkedAgo": "{time} 前检查",
diff --git a/gui/src/provider-workspace/report.ts b/gui/src/provider-workspace/report.ts
index c38e982ad9..b53b9d3bcc 100644
--- a/gui/src/provider-workspace/report.ts
+++ b/gui/src/provider-workspace/report.ts
@@ -11,41 +11,115 @@ export interface ProviderQuotaReportView {
source?: string;
updatedAt?: number;
quota?: unknown;
+ aggregation?: unknown;
}
-/** Narrow an unknown quota payload into the AccountQuota display shape (null when unusable). */
-export function accountQuotaFromReport(report?: ProviderQuotaReportView): AccountQuota | null {
- const quota = report?.quota;
+export interface CapacityWindowView {
+ usedPercent: number;
+ nextRecoveryAt?: number;
+ nextRecoveryPercent?: number;
+}
+
+export interface ProviderCapacityAggregationView {
+ incomplete: boolean;
+ excludedAccounts: number;
+ unknownPlanAccounts: number;
+ fiveHour?: CapacityWindowView;
+ weekly?: CapacityWindowView;
+ monthly?: CapacityWindowView;
+ customWindows?: Array;
+ currentAccount?: { plan?: string | null; quota: AccountQuota | null };
+}
+
+const finite = (value: unknown): number | undefined => (
+ typeof value === "number" && Number.isFinite(value) ? value : undefined
+);
+
+function quotaFromUnknown(quota: unknown, fallbackUpdatedAt?: number): AccountQuota | null {
if (!quota || typeof quota !== "object" || Array.isArray(quota)) return null;
const q = quota as Record;
- const num = (v: unknown): number | undefined => (typeof v === "number" && Number.isFinite(v) ? v : undefined);
const windows = Array.isArray(q.customWindows)
? (q.customWindows as unknown[]).flatMap(w => {
if (!w || typeof w !== "object") return [];
const row = w as Record;
- if (typeof row.label !== "string" || num(row.percent) === undefined) return [];
+ if (typeof row.label !== "string" || finite(row.percent) === undefined) return [];
return [{
label: row.label,
percent: row.percent as number,
- ...(num(row.resetAt) !== undefined ? { resetAt: row.resetAt as number } : {}),
+ ...(finite(row.resetAt) !== undefined ? { resetAt: row.resetAt as number } : {}),
}];
})
: [];
const out: AccountQuota = {
- ...(num(q.fiveHourPercent) !== undefined ? { fiveHourPercent: q.fiveHourPercent as number } : {}),
- ...(num(q.fiveHourResetAt) !== undefined ? { fiveHourResetAt: q.fiveHourResetAt as number } : {}),
- ...(num(q.weeklyPercent) !== undefined ? { weeklyPercent: q.weeklyPercent as number } : {}),
- ...(num(q.weeklyResetAt) !== undefined ? { weeklyResetAt: q.weeklyResetAt as number } : {}),
- ...(num(q.monthlyPercent) !== undefined ? { monthlyPercent: q.monthlyPercent as number } : {}),
- ...(num(q.monthlyResetAt) !== undefined ? { monthlyResetAt: q.monthlyResetAt as number } : {}),
+ ...(finite(q.fiveHourPercent) !== undefined ? { fiveHourPercent: q.fiveHourPercent as number } : {}),
+ ...(finite(q.fiveHourResetAt) !== undefined ? { fiveHourResetAt: q.fiveHourResetAt as number } : {}),
+ ...(finite(q.weeklyPercent) !== undefined ? { weeklyPercent: q.weeklyPercent as number } : {}),
+ ...(finite(q.weeklyResetAt) !== undefined ? { weeklyResetAt: q.weeklyResetAt as number } : {}),
+ ...(finite(q.monthlyPercent) !== undefined ? { monthlyPercent: q.monthlyPercent as number } : {}),
+ ...(finite(q.monthlyResetAt) !== undefined ? { monthlyResetAt: q.monthlyResetAt as number } : {}),
...(windows.length > 0 ? { customWindows: windows } : {}),
- updatedAt: num(q.updatedAt) ?? report?.updatedAt ?? Date.now(),
+ updatedAt: finite(q.updatedAt) ?? fallbackUpdatedAt ?? Date.now(),
};
- const hasSignal = out.fiveHourPercent !== undefined
+ return out.fiveHourPercent !== undefined
|| out.weeklyPercent !== undefined
|| out.monthlyPercent !== undefined
- || (out.customWindows?.length ?? 0) > 0;
- return hasSignal ? out : null;
+ || (out.customWindows?.length ?? 0) > 0
+ ? out
+ : null;
+}
+
+/** Narrow an unknown quota payload into the AccountQuota display shape (null when unusable). */
+export function accountQuotaFromReport(report?: ProviderQuotaReportView): AccountQuota | null {
+ return quotaFromUnknown(report?.quota, report?.updatedAt);
+}
+
+function capacityWindow(value: unknown): CapacityWindowView | undefined {
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
+ const row = value as Record;
+ const usedPercent = finite(row.usedPercent);
+ if (usedPercent === undefined) return undefined;
+ return {
+ usedPercent,
+ ...(finite(row.nextRecoveryAt) !== undefined ? { nextRecoveryAt: row.nextRecoveryAt as number } : {}),
+ ...(finite(row.nextRecoveryPercent) !== undefined ? { nextRecoveryPercent: row.nextRecoveryPercent as number } : {}),
+ };
+}
+
+/** Strictly narrows optional weighted-pool metadata; legacy reports return null. */
+export function capacityAggregationFromReport(report?: ProviderQuotaReportView): ProviderCapacityAggregationView | null {
+ const value = report?.aggregation;
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
+ const row = value as Record;
+ if (row.kind !== "capacity-weighted-v1" || row.scope !== "routable-known") return null;
+ const excludedAccounts = finite(row.excludedAccounts);
+ const unknownPlanAccounts = finite(row.unknownPlanAccounts);
+ if (excludedAccounts === undefined || unknownPlanAccounts === undefined || typeof row.incomplete !== "boolean") return null;
+ const currentRaw = row.currentAccount && typeof row.currentAccount === "object" && !Array.isArray(row.currentAccount)
+ ? row.currentAccount as Record
+ : null;
+ const customWindows = Array.isArray(row.customWindows)
+ ? row.customWindows.flatMap(value => {
+ if (!value || typeof value !== "object" || Array.isArray(value)) return [];
+ const custom = value as Record;
+ const window = capacityWindow(custom);
+ return typeof custom.label === "string" && window ? [{ label: custom.label, ...window }] : [];
+ })
+ : [];
+ return {
+ incomplete: row.incomplete,
+ excludedAccounts,
+ unknownPlanAccounts,
+ ...(capacityWindow(row.fiveHour) ? { fiveHour: capacityWindow(row.fiveHour) } : {}),
+ ...(capacityWindow(row.weekly) ? { weekly: capacityWindow(row.weekly) } : {}),
+ ...(capacityWindow(row.monthly) ? { monthly: capacityWindow(row.monthly) } : {}),
+ ...(customWindows.length > 0 ? { customWindows } : {}),
+ ...(currentRaw ? {
+ currentAccount: {
+ ...(typeof currentRaw.plan === "string" || currentRaw.plan === null ? { plan: currentRaw.plan } : {}),
+ quota: quotaFromUnknown(currentRaw.quota),
+ },
+ } : {}),
+ };
}
/** Human label for a quota report source id (e.g. "cursor:period-usage"). */
diff --git a/gui/src/styles/provider-overview-dashboard.css b/gui/src/styles/provider-overview-dashboard.css
index ba11de9d06..d09b8058fc 100644
--- a/gui/src/styles/provider-overview-dashboard.css
+++ b/gui/src/styles/provider-overview-dashboard.css
@@ -190,6 +190,45 @@
min-height: 64px;
}
+.pws-capacity-label,
+.pws-capacity-recovery,
+.pws-capacity-incomplete {
+ font-size: 0.7rem;
+}
+
+.pws-capacity-label {
+ color: var(--fg-muted, #888);
+ margin-bottom: 3px;
+}
+
+.pws-capacity-details {
+ display: grid;
+ gap: 7px;
+ margin-top: 8px;
+ padding-top: 7px;
+ border-top: 1px solid var(--border-soft);
+}
+
+.pws-capacity-recovery {
+ display: flex;
+ justify-content: space-between;
+ gap: 12px;
+ color: var(--fg-muted, #888);
+}
+
+.pws-capacity-recovery strong {
+ color: var(--text);
+ white-space: nowrap;
+}
+
+.pws-capacity-current {
+ padding-top: 2px;
+}
+
+.pws-capacity-incomplete {
+ color: var(--warning, #b7791f);
+}
+
.pws-dashboard-section--rate-limits,
.pws-dashboard-section--recent {
min-height: 180px;
diff --git a/gui/tests/provider-capacity.test.ts b/gui/tests/provider-capacity.test.ts
new file mode 100644
index 0000000000..863bb5c2ad
--- /dev/null
+++ b/gui/tests/provider-capacity.test.ts
@@ -0,0 +1,41 @@
+import { expect, test } from "bun:test";
+import { capacityAggregationFromReport } from "../src/provider-workspace/report";
+
+test("legacy provider quota reports remain valid without aggregation metadata", () => {
+ expect(capacityAggregationFromReport({ quota: { weeklyPercent: 42 } })).toBeNull();
+});
+
+test("capacity metadata preserves estimate, raw current quota, recovery percent, and incomplete coverage", () => {
+ const aggregation = capacityAggregationFromReport({
+ quota: { weeklyPercent: 30.769230769, updatedAt: 123 },
+ aggregation: {
+ kind: "capacity-weighted-v1",
+ scope: "routable-known",
+ incomplete: true,
+ excludedAccounts: 2,
+ unknownPlanAccounts: 1,
+ weekly: {
+ usedPercent: 30.769230769,
+ nextRecoveryAt: 1_800_000_010_000,
+ nextRecoveryPercent: 19.23076923,
+ },
+ currentAccount: {
+ plan: "pro",
+ quota: { weeklyPercent: 10, weeklyResetAt: 1_800_000_030, updatedAt: 123 },
+ },
+ },
+ });
+ expect(aggregation).toMatchObject({
+ incomplete: true,
+ excludedAccounts: 2,
+ unknownPlanAccounts: 1,
+ weekly: { usedPercent: 30.769230769, nextRecoveryPercent: 19.23076923 },
+ currentAccount: { plan: "pro", quota: { weeklyPercent: 10 } },
+ });
+ expect(aggregation?.weekly).not.toHaveProperty("projectedUsedPercentAfterReset");
+});
+
+test("malformed or future aggregation contracts fail closed", () => {
+ expect(capacityAggregationFromReport({ aggregation: { kind: "capacity-weighted-v2" } })).toBeNull();
+ expect(capacityAggregationFromReport({ aggregation: { kind: "capacity-weighted-v1", scope: "routable-known" } })).toBeNull();
+});
diff --git a/src/codex/auth-api.ts b/src/codex/auth-api.ts
index fa0472267a..f4345d0fec 100644
--- a/src/codex/auth-api.ts
+++ b/src/codex/auth-api.ts
@@ -750,6 +750,10 @@ export function clearCodexQuotaPrimeState(): void {
primeInFlight = null;
}
+export function effectiveCodexAuthAccountId(config: OcxConfig): string {
+ return getEffectiveActiveCodexAccountId(config) ?? MAIN_CODEX_ACCOUNT_ID;
+}
+
export async function listCodexAuthAccounts(config: OcxConfig, forceRefresh = false): Promise {
const runtimeConfig = getRuntimeConfig(config);
const poolAccounts = (runtimeConfig.codexAccounts ?? []).filter(isSelectableCodexPoolAccount);
diff --git a/src/providers/codex-capacity.ts b/src/providers/codex-capacity.ts
new file mode 100644
index 0000000000..c24c4b741a
--- /dev/null
+++ b/src/providers/codex-capacity.ts
@@ -0,0 +1,219 @@
+export const CODEX_CONFIGURED_CAPACITY_WEIGHTS = {
+ plus: 1,
+ business: 1,
+ prolite: 5,
+ pro: 20,
+} as const;
+
+export type CodexCapacityQuota = {
+ fiveHourPercent?: number;
+ fiveHourResetAt?: number;
+ weeklyPercent?: number;
+ weeklyResetAt?: number;
+ monthlyPercent?: number;
+ monthlyResetAt?: number;
+ customWindows?: Array<{ label: string; percent: number; resetAt?: number }>;
+ updatedAt: number;
+};
+
+export interface CodexCapacityAccount {
+ isMain: boolean;
+ active?: boolean;
+ plan?: string | null;
+ paused: boolean;
+ needsReauth?: boolean;
+ quota: CodexCapacityQuota | null;
+}
+
+export interface CodexCapacityWindowAggregation {
+ usedPercent: number;
+ includedAccounts: number;
+ totalWeight: number;
+ consumedWeight: number;
+ remainingWeight: number;
+ nextRecoveryAt?: number;
+ nextRecoveryPercent?: number;
+}
+
+export interface CodexCapacityAggregation {
+ kind: "capacity-weighted-v1";
+ scope: "routable-known";
+ includedAccounts: number;
+ excludedAccounts: number;
+ unknownPlanAccounts: number;
+ missingQuotaAccounts: number;
+ pausedAccounts: number;
+ reauthAccounts: number;
+ incomplete: boolean;
+ fiveHour?: CodexCapacityWindowAggregation;
+ weekly?: CodexCapacityWindowAggregation;
+ monthly?: CodexCapacityWindowAggregation;
+ customWindows?: Array;
+ currentAccount?: {
+ isMain: boolean;
+ plan?: string | null;
+ quota: CodexCapacityQuota | null;
+ };
+}
+
+export interface CodexCapacityResult {
+ quota: CodexCapacityQuota | null;
+ aggregation: CodexCapacityAggregation | null;
+ currentAccount?: CodexCapacityAggregation["currentAccount"];
+}
+
+type MutableWindow = {
+ totalWeight: number;
+ consumedWeight: number;
+ includedAccounts: number;
+ recoveries: Map;
+};
+
+function configuredWeight(plan: string | null | undefined): number | undefined {
+ const normalized = plan?.trim().toLowerCase();
+ return normalized && normalized in CODEX_CONFIGURED_CAPACITY_WEIGHTS
+ ? CODEX_CONFIGURED_CAPACITY_WEIGHTS[normalized as keyof typeof CODEX_CONFIGURED_CAPACITY_WEIGHTS]
+ : undefined;
+}
+
+function normalizedPercent(value: unknown): number | undefined {
+ return typeof value === "number" && Number.isFinite(value)
+ ? Math.max(0, Math.min(100, value))
+ : undefined;
+}
+
+function futureResetMs(value: unknown, now: number): number | undefined {
+ if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
+ const milliseconds = value > 10_000_000_000 ? value : value * 1000;
+ return milliseconds > now ? milliseconds : undefined;
+}
+
+function addWindow(
+ windows: Map,
+ key: string,
+ weight: number,
+ percent: number,
+ resetAt: number | undefined,
+): void {
+ const window = windows.get(key) ?? {
+ totalWeight: 0,
+ consumedWeight: 0,
+ includedAccounts: 0,
+ recoveries: new Map(),
+ };
+ const consumed = weight * percent / 100;
+ window.totalWeight += weight;
+ window.consumedWeight += consumed;
+ window.includedAccounts += 1;
+ if (resetAt !== undefined) {
+ window.recoveries.set(resetAt, (window.recoveries.get(resetAt) ?? 0) + consumed);
+ }
+ windows.set(key, window);
+}
+
+function finalizeWindow(window: MutableWindow): CodexCapacityWindowAggregation {
+ const nextRecoveryAt = [...window.recoveries.keys()].sort((a, b) => a - b)[0];
+ const recovered = nextRecoveryAt === undefined ? undefined : window.recoveries.get(nextRecoveryAt);
+ return {
+ usedPercent: window.consumedWeight / window.totalWeight * 100,
+ includedAccounts: window.includedAccounts,
+ totalWeight: window.totalWeight,
+ consumedWeight: window.consumedWeight,
+ remainingWeight: window.totalWeight - window.consumedWeight,
+ ...(nextRecoveryAt !== undefined ? { nextRecoveryAt } : {}),
+ ...(recovered !== undefined ? { nextRecoveryPercent: recovered / window.totalWeight * 100 } : {}),
+ };
+}
+
+/** Display-only configured-weight estimate. It never participates in account selection or routing. */
+export function aggregateCodexPoolCapacity(
+ accounts: readonly CodexCapacityAccount[],
+ now = Date.now(),
+): CodexCapacityResult {
+ const current = accounts.find(account => account.active)
+ ?? accounts.find(account => account.isMain)
+ ?? accounts[0];
+ const currentAccount = current ? {
+ isMain: current.isMain,
+ ...(current.plan !== undefined ? { plan: current.plan } : {}),
+ quota: current.quota,
+ } : undefined;
+ const windows = new Map();
+ const included = new Set();
+ let unknownPlanAccounts = 0;
+ let missingQuotaAccounts = 0;
+ let pausedAccounts = 0;
+ let reauthAccounts = 0;
+ let updatedAt = 0;
+
+ for (const account of accounts) {
+ const weight = configuredWeight(account.plan);
+ if (weight === undefined) unknownPlanAccounts += 1;
+ if (account.paused) pausedAccounts += 1;
+ if (account.needsReauth) reauthAccounts += 1;
+ const quota = account.quota;
+ const standard = quota ? [
+ ["fiveHour", quota.fiveHourPercent, quota.fiveHourResetAt],
+ ["weekly", quota.weeklyPercent, quota.weeklyResetAt],
+ ["monthly", quota.monthlyPercent, quota.monthlyResetAt],
+ ] as const : [];
+ const custom = quota?.customWindows ?? [];
+ const hasQuota = standard.some(([, percent]) => normalizedPercent(percent) !== undefined)
+ || custom.some(window => normalizedPercent(window.percent) !== undefined);
+ if (!hasQuota) missingQuotaAccounts += 1;
+ if (account.paused || account.needsReauth || weight === undefined || !quota || !hasQuota) continue;
+
+ let contributed = false;
+ for (const [key, rawPercent, rawReset] of standard) {
+ const percent = normalizedPercent(rawPercent);
+ if (percent === undefined) continue;
+ addWindow(windows, key, weight, percent, futureResetMs(rawReset, now));
+ contributed = true;
+ }
+ for (const customWindow of custom) {
+ const percent = normalizedPercent(customWindow.percent);
+ if (percent === undefined) continue;
+ addWindow(windows, `custom:${customWindow.label}`, weight, percent, futureResetMs(customWindow.resetAt, now));
+ contributed = true;
+ }
+ if (contributed) {
+ included.add(account);
+ updatedAt = Math.max(updatedAt, quota.updatedAt);
+ }
+ }
+
+ if (windows.size === 0) return { quota: null, aggregation: null, ...(currentAccount ? { currentAccount } : {}) };
+ const fiveHour = windows.get("fiveHour") ? finalizeWindow(windows.get("fiveHour")!) : undefined;
+ const weekly = windows.get("weekly") ? finalizeWindow(windows.get("weekly")!) : undefined;
+ const monthly = windows.get("monthly") ? finalizeWindow(windows.get("monthly")!) : undefined;
+ const customWindows = [...windows.entries()].flatMap(([key, window]) => key.startsWith("custom:")
+ ? [{ label: key.slice("custom:".length), ...finalizeWindow(window) }]
+ : []);
+ const quota: CodexCapacityQuota = {
+ ...(fiveHour ? { fiveHourPercent: fiveHour.usedPercent } : {}),
+ ...(weekly ? { weeklyPercent: weekly.usedPercent } : {}),
+ ...(monthly ? { monthlyPercent: monthly.usedPercent } : {}),
+ ...(customWindows.length > 0 ? {
+ customWindows: customWindows.map(window => ({ label: window.label, percent: window.usedPercent })),
+ } : {}),
+ updatedAt: updatedAt || now,
+ };
+ const excludedAccounts = accounts.length - included.size;
+ const aggregation: CodexCapacityAggregation = {
+ kind: "capacity-weighted-v1",
+ scope: "routable-known",
+ includedAccounts: included.size,
+ excludedAccounts,
+ unknownPlanAccounts,
+ missingQuotaAccounts,
+ pausedAccounts,
+ reauthAccounts,
+ incomplete: excludedAccounts > 0,
+ ...(fiveHour ? { fiveHour } : {}),
+ ...(weekly ? { weekly } : {}),
+ ...(monthly ? { monthly } : {}),
+ ...(customWindows.length > 0 ? { customWindows } : {}),
+ ...(currentAccount ? { currentAccount } : {}),
+ };
+ return { quota, aggregation, ...(currentAccount ? { currentAccount } : {}) };
+}
diff --git a/src/providers/quota.ts b/src/providers/quota.ts
index 131bc1358e..b637993361 100644
--- a/src/providers/quota.ts
+++ b/src/providers/quota.ts
@@ -1,4 +1,4 @@
-import { fetchMainAccountInfo, listCodexAuthAccounts } from "../codex/auth-api";
+import { effectiveCodexAuthAccountId, fetchMainAccountInfo, listCodexAuthAccounts } from "../codex/auth-api";
import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
import { resolveEnvValue } from "../config";
import { getValidAccessToken, getValidAccessTokenForAccount } from "../oauth";
@@ -12,6 +12,11 @@ import {
sweepExpiredOnWrite,
type GenerationContext,
} from "../lib/state-store-sweeper";
+import {
+ aggregateCodexPoolCapacity,
+ type CodexCapacityAggregation,
+ type CodexCapacityQuota,
+} from "./codex-capacity";
/** Match oauth/index REFRESH_SKEW_MS — use stored access without refresh when still fresh. */
const ACCOUNT_TOKEN_SKEW_MS = 60_000;
@@ -47,6 +52,7 @@ export interface ProviderQuotaReport {
quota: ProviderQuota;
updatedAt: number;
reverseEngineered?: boolean;
+ aggregation?: CodexCapacityAggregation;
}
export interface ProviderQuotaResponse {
@@ -70,7 +76,7 @@ function cacheKey(config: OcxConfig): string {
.map(([name, provider]) => `${name}:${provider.adapter}:${provider.authMode ?? "key"}:${providerCodexAccountMode(name, provider) ?? "none"}:${provider.disabled === true ? "off" : "on"}:${provider.baseUrl}`)
.sort()
.join("|");
- return `${config.defaultProvider}|${config.activeCodexAccountId ?? ""}|${providers}`;
+ return `${config.defaultProvider}|${config.activeCodexAccountId ?? ""}|${effectiveCodexAuthAccountId(config)}|${providers}`;
}
function hasQuotaRows(quota: ProviderQuota | null | undefined): quota is ProviderQuota {
@@ -123,7 +129,12 @@ function isBuiltInChatGptForwardProvider(name: string, provider: OcxProviderConf
return name === OPENAI_CODEX_PROVIDER_ID && isCanonicalOpenAiForwardProvider(provider);
}
-function report(provider: string, source: string, quota: ProviderQuota): ProviderQuotaReport | null {
+function report(
+ provider: string,
+ source: string,
+ quota: ProviderQuota,
+ aggregation?: CodexCapacityAggregation,
+): ProviderQuotaReport | null {
if (!hasQuotaRows(quota)) return null;
return {
provider,
@@ -131,6 +142,7 @@ function report(provider: string, source: string, quota: ProviderQuota): Provide
source,
quota,
updatedAt: quota.updatedAt,
+ ...(aggregation ? { aggregation } : {}),
};
}
@@ -146,12 +158,19 @@ async function fetchChatGptForwardQuota(
return quota ? report(provider, "chatgpt:wham", quota) : null;
}
const accounts = await listCodexAuthAccounts(config, forceRefresh);
- const activeId = config.activeCodexAccountId || MAIN_CODEX_ACCOUNT_ID;
- const active = accounts.find(account => account.id === activeId)
+ const activeId = effectiveCodexAuthAccountId(config);
+ const capacityAccounts = accounts.map(account => ({ ...account, active: account.id === activeId }));
+ const active = capacityAccounts.find(account => account.active)
?? accounts.find(account => account.id === MAIN_CODEX_ACCOUNT_ID)
?? accounts[0];
- const quota = active?.quota ? { ...active.quota, updatedAt: active.quota.updatedAt ?? Date.now() } as ProviderQuota : null;
- return quota ? report(provider, "chatgpt:wham", quota) : null;
+ const capacity = aggregateCodexPoolCapacity(capacityAccounts, Date.now());
+ if (capacity.aggregation && capacity.quota) {
+ return report(provider, "chatgpt:wham", capacity.quota as ProviderQuota, capacity.aggregation);
+ }
+ const quota = active?.quota
+ ? { ...active.quota, updatedAt: active.quota.updatedAt ?? Date.now() } as CodexCapacityQuota
+ : null;
+ return quota ? report(provider, "chatgpt:wham", quota as ProviderQuota) : null;
}
function centsValue(value: unknown): number | undefined {
diff --git a/tests/provider-capacity.test.ts b/tests/provider-capacity.test.ts
new file mode 100644
index 0000000000..5844d328c1
--- /dev/null
+++ b/tests/provider-capacity.test.ts
@@ -0,0 +1,101 @@
+import { describe, expect, test } from "bun:test";
+import { aggregateCodexPoolCapacity, type CodexCapacityAccount } from "../src/providers/codex-capacity";
+
+const NOW = 1_800_000_000_000;
+const account = (
+ plan: string | null,
+ weeklyPercent: number | undefined,
+ options: Partial & { weeklyResetAt?: number; monthlyPercent?: number; monthlyResetAt?: number } = {},
+): CodexCapacityAccount => ({
+ isMain: false,
+ plan,
+ paused: false,
+ quota: weeklyPercent === undefined && options.monthlyPercent === undefined ? null : {
+ ...(weeklyPercent !== undefined ? { weeklyPercent } : {}),
+ ...(options.weeklyResetAt !== undefined ? { weeklyResetAt: options.weeklyResetAt } : {}),
+ ...(options.monthlyPercent !== undefined ? { monthlyPercent: options.monthlyPercent } : {}),
+ ...(options.monthlyResetAt !== undefined ? { monthlyResetAt: options.monthlyResetAt } : {}),
+ updatedAt: NOW,
+ },
+ ...options,
+});
+
+describe("configured-weight Codex pool capacity", () => {
+ test("Pro + Prolite + Plus produces the issue #874 estimate and recovery share", () => {
+ const result = aggregateCodexPoolCapacity([
+ account("pro", 10, { isMain: true, active: true, weeklyResetAt: NOW + 30_000 }),
+ account("prolite", 100, { weeklyResetAt: NOW + 10_000 }),
+ account("plus", 100, { weeklyResetAt: NOW + 20_000 }),
+ ], NOW);
+ expect(result.quota?.weeklyPercent).toBeCloseTo(30.769230769, 8);
+ expect(result.aggregation?.weekly).toMatchObject({ totalWeight: 26, consumedWeight: 8, remainingWeight: 18 });
+ expect(result.aggregation?.weekly?.nextRecoveryAt).toBe(NOW + 10_000);
+ expect(result.aggregation?.weekly?.nextRecoveryPercent).toBeCloseTo(19.23076923, 8);
+ expect(result.aggregation?.currentAccount?.quota?.weeklyPercent).toBe(10);
+ });
+
+ test("observed 8/100/100 and refreshed 9/100/100 remain exact weighted regressions", () => {
+ const rows = (mainPercent: number) => [
+ account("pro", mainPercent, { isMain: true, active: true }),
+ account("prolite", 100),
+ account("prolite", 100),
+ ];
+ expect(aggregateCodexPoolCapacity(rows(8), NOW).quota?.weeklyPercent).toBeCloseTo(38.66666667, 8);
+ expect(aggregateCodexPoolCapacity(rows(9), NOW).quota?.weeklyPercent).toBeCloseTo(39.33333333, 8);
+ });
+
+ test("same-time resets group partial consumed capacity and expose only recovery percent", () => {
+ const result = aggregateCodexPoolCapacity([
+ account("pro", 25, { weeklyResetAt: NOW + 10_000 }),
+ account("prolite", 40, { weeklyResetAt: NOW + 10_000 }),
+ account("plus", 100, { weeklyResetAt: NOW + 20_000 }),
+ ], NOW);
+ expect(result.aggregation?.weekly?.nextRecoveryPercent).toBeCloseTo(7 / 26 * 100, 8);
+ expect(result.aggregation?.weekly).not.toHaveProperty("projectedUsedPercentAfterReset");
+ });
+
+ test("unknown, missing, paused, and reauth rows are excluded with incomplete coverage", () => {
+ const result = aggregateCodexPoolCapacity([
+ account("pro", 10, { active: true, isMain: true }),
+ account("team", 50),
+ account("plus", undefined),
+ account("prolite", 20, { paused: true }),
+ account("business", 30, { needsReauth: true }),
+ ], NOW);
+ expect(result.aggregation).toMatchObject({
+ includedAccounts: 1,
+ excludedAccounts: 4,
+ unknownPlanAccounts: 1,
+ missingQuotaAccounts: 1,
+ pausedAccounts: 1,
+ reauthAccounts: 1,
+ incomplete: true,
+ });
+ expect(result.quota?.weeklyPercent).toBe(10);
+ });
+
+ test("weekly and monthly windows aggregate independently", () => {
+ const result = aggregateCodexPoolCapacity([
+ account("pro", 20),
+ account("prolite", undefined, { monthlyPercent: 60 }),
+ account("plus", 100, { monthlyPercent: 10 }),
+ ], NOW);
+ expect(result.aggregation?.weekly?.totalWeight).toBe(21);
+ expect(result.aggregation?.monthly?.totalWeight).toBe(6);
+ expect(result.quota?.weeklyPercent).toBeCloseTo(23.8095238, 7);
+ expect(result.quota?.monthlyPercent).toBeCloseTo(51.6666667, 7);
+ });
+
+ test("expired resets are ignored and all-excluded pools retain effective-account fallback truth", () => {
+ const expired = aggregateCodexPoolCapacity([
+ account("pro", 10, { weeklyResetAt: NOW - 1, active: true, isMain: true }),
+ ], NOW);
+ expect(expired.aggregation?.weekly).not.toHaveProperty("nextRecoveryAt");
+ const fallback = aggregateCodexPoolCapacity([
+ account("team", 70, { active: true, isMain: true }),
+ account("go", 80),
+ ], NOW);
+ expect(fallback.aggregation).toBeNull();
+ expect(fallback.currentAccount?.quota?.weeklyPercent).toBe(70);
+ });
+});
diff --git a/tests/provider-quota.test.ts b/tests/provider-quota.test.ts
index 11e22aaa46..d6cd47e5aa 100644
--- a/tests/provider-quota.test.ts
+++ b/tests/provider-quota.test.ts
@@ -3,6 +3,7 @@ import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync
import { tmpdir } from "node:os";
import { join } from "node:path";
import { clearAccountQuota } from "../src/codex/quota";
+import { clearCodexUpstreamHealth } from "../src/codex/routing";
import { saveCodexAccountCredential } from "../src/codex/account-store";
import { saveCredential } from "../src/oauth/store";
import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../src/providers/quota";
@@ -70,6 +71,7 @@ beforeEach(() => {
tokens: { access_token: "chatgpt-main-access", account_id: "chatgpt-main-account" },
}));
clearAccountQuota();
+ clearCodexUpstreamHealth();
clearProviderQuotaCache();
});
@@ -480,7 +482,7 @@ describe("fetchProviderQuotaReports", () => {
expect(expired.reports).toEqual([]);
});
- test("pool mode reports the active added account", async () => {
+ test("pool mode reports a weighted estimate while preserving the effective account raw quota", async () => {
saveCodexAccountCredential("added", {
accessToken: "added-access",
refreshToken: "added-refresh",
@@ -488,18 +490,26 @@ describe("fetchProviderQuotaReports", () => {
chatgptAccountId: "added-chatgpt-id",
});
const config = testConfig();
- config.codexAccounts = [{ id: "added", email: "a@example.test", isMain: false }];
+ config.codexAccounts = [{ id: "added", email: "a@example.test", plan: "prolite", isMain: false }];
config.activeCodexAccountId = "added";
globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => {
const headers = init?.headers as Record | undefined;
const percent = headers?.["ChatGPT-Account-Id"] === "added-chatgpt-id" ? 77 : 11;
return new Response(JSON.stringify({
+ plan_type: headers?.["ChatGPT-Account-Id"] === "added-chatgpt-id" ? "prolite" : "plus",
rate_limit: { secondary_window: { used_percent: percent, reset_at: 1_789_000_000 } },
}), { status: 200, headers: { "content-type": "application/json" } });
}) as typeof fetch;
const result = await fetchProviderQuotaReports(config, true);
- expect(result.reports.find(row => row.provider === "openai")?.quota.weeklyPercent).toBe(77);
+ const openai = result.reports.find(row => row.provider === "openai");
+ expect(openai?.quota.weeklyPercent).toBe(66);
+ expect(openai?.aggregation).toMatchObject({
+ includedAccounts: 2,
+ excludedAccounts: 0,
+ incomplete: false,
+ currentAccount: { plan: "prolite", quota: { weeklyPercent: 77 } },
+ });
});
test("direct mode reports main without reading or repairing the added-account store", async () => {
From f3163da7cf64302600df7ca1c579347a5de208f6 Mon Sep 17 00:00:00 2001
From: luvs01 <27862058+luvs01@users.noreply.github.com>
Date: Sun, 2 Aug 2026 16:25:35 +0900
Subject: [PATCH 02/10] fix(gui): preserve Codex pool capacity freshness
Carry weighted pool metadata through the provider workspace shell and reject stale per-account quota readings.
Key report caching to a privacy-safe signature of every aggregation-relevant account state while keeping configured weight units internal.
Refs #874
---
.../ProviderWorkspaceShell.tsx | 3 +-
gui/tests/provider-capacity-shell.test.tsx | 103 +++++++++++++++++
src/codex/auth-api.ts | 7 +-
src/providers/codex-capacity.ts | 56 ++++++++--
src/providers/quota.ts | 88 ++++++++++++++-
tests/provider-capacity.test.ts | 41 ++++++-
tests/provider-quota.test.ts | 104 +++++++++++++++++-
7 files changed, 382 insertions(+), 20 deletions(-)
create mode 100644 gui/tests/provider-capacity-shell.test.tsx
diff --git a/gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx b/gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx
index b771ca7208..583ad279a0 100644
--- a/gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx
+++ b/gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx
@@ -218,7 +218,7 @@ export default function ProviderWorkspaceShell({
// be bypassed. The old derived-key effect always read the cached view, which is why a
// switch could leave the bars showing the previous account's quota.
void fetch(`${apiBase}/api/provider-quotas${quotaForceRefresh ? "?refresh=1" : ""}`)
- .then(r => readJsonIfOk<{ reports?: Array<{ provider: string; label?: string; source?: string; updatedAt?: number; quota?: unknown }> }>(r))
+ .then(r => readJsonIfOk<{ reports?: Array<{ provider: string; label?: string; source?: string; updatedAt?: number; quota?: unknown; aggregation?: unknown }> }>(r))
.then((data) => {
if (cancelled || !data) return;
// Merge so a partial/failed probe cannot wipe a previously good provider row.
@@ -231,6 +231,7 @@ export default function ProviderWorkspaceShell({
source: report.source,
updatedAt: typeof report.updatedAt === "number" ? report.updatedAt : Date.now(),
quota: report.quota,
+ ...(report.aggregation !== undefined ? { aggregation: report.aggregation } : {}),
};
}
writeSessionListCache(quotasCacheKey, next);
diff --git a/gui/tests/provider-capacity-shell.test.tsx b/gui/tests/provider-capacity-shell.test.tsx
new file mode 100644
index 0000000000..7039bc50dd
--- /dev/null
+++ b/gui/tests/provider-capacity-shell.test.tsx
@@ -0,0 +1,103 @@
+import { afterEach, beforeEach, expect, test } from "bun:test";
+import { Window } from "happy-dom";
+import { act } from "react";
+import type { Root } from "react-dom/client";
+import ProviderWorkspaceShell from "../src/components/provider-workspace/ProviderWorkspaceShell";
+import { LanguageProvider } from "../src/i18n/provider";
+
+const globals = ["document", "window", "navigator", "localStorage", "sessionStorage", "IS_REACT_ACT_ENVIRONMENT"] as const;
+let previous: Record<(typeof globals)[number], unknown>;
+let originalFetch: typeof globalThis.fetch;
+let win: Window;
+let host: HTMLElement;
+let root: Root | null = null;
+
+beforeEach(() => {
+ previous = Object.fromEntries(globals.map(key => [key, Reflect.get(globalThis, key)])) as typeof previous;
+ originalFetch = globalThis.fetch;
+ win = new Window({ url: "http://localhost/" });
+ Object.defineProperty(win.navigator, "language", { configurable: true, value: "en-US" });
+ Object.defineProperty(win, "event", { configurable: true, writable: true, value: undefined });
+ Object.defineProperties(globalThis, {
+ document: { configurable: true, value: win.document },
+ window: { configurable: true, value: win },
+ navigator: { configurable: true, value: win.navigator },
+ localStorage: { configurable: true, value: win.localStorage },
+ sessionStorage: { configurable: true, value: win.sessionStorage },
+ });
+ (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
+ const recoveryAt = Date.UTC(2026, 7, 8, 4, 32);
+ Object.defineProperty(globalThis, "fetch", {
+ configurable: true,
+ value: async (input: string) => {
+ const url = String(input);
+ const body = url.includes("/api/provider-quotas") ? {
+ reports: [{
+ provider: "openai",
+ label: "OpenAI (Codex login)",
+ source: "chatgpt:wham",
+ updatedAt: Date.now(),
+ quota: { weeklyPercent: 30.8, updatedAt: Date.now() },
+ aggregation: {
+ kind: "capacity-weighted-v1",
+ scope: "routable-known",
+ includedAccounts: 2,
+ excludedAccounts: 1,
+ unknownPlanAccounts: 1,
+ missingQuotaAccounts: 0,
+ pausedAccounts: 0,
+ reauthAccounts: 0,
+ staleQuotaAccounts: 0,
+ incomplete: true,
+ weekly: { usedPercent: 30.8, includedAccounts: 2, updatedAt: Date.now(), nextRecoveryAt: recoveryAt, nextRecoveryPercent: 19.2 },
+ currentAccount: { isMain: true, plan: "pro", quota: { weeklyPercent: 8, updatedAt: Date.now() } },
+ },
+ }],
+ } : {};
+ return { ok: true, status: 200, json: async () => body, text: async () => JSON.stringify(body) } as unknown as Response;
+ },
+ });
+ host = win.document.createElement("div") as unknown as HTMLElement;
+ win.document.body.appendChild(host as never);
+});
+
+afterEach(async () => {
+ if (root) {
+ const current = root;
+ await act(async () => { current.unmount(); });
+ root = null;
+ }
+ for (const key of globals) Object.defineProperty(globalThis, key, { configurable: true, value: previous[key] });
+ Object.defineProperty(globalThis, "fetch", { configurable: true, value: originalFetch });
+});
+
+test("provider quota fetch preserves aggregate capacity through shell state and render", async () => {
+ const { createRoot } = await import("react-dom/client");
+ await act(async () => {
+ root = createRoot(host);
+ root.render(
+
+ {}}
+ onAddProvider={() => {}}
+ />
+ ,
+ );
+ });
+ await act(async () => { await new Promise(resolve => setTimeout(resolve, 30)); });
+
+ const text = host.textContent ?? "";
+ expect(text).toContain("Configured-weight pool estimate");
+ expect(text).toContain("31% used");
+ expect(text).toContain("Current effective account · pro");
+ expect(text).toContain("8%");
+ expect(text).toContain("Incomplete coverage: 1 account(s) excluded, including 1 unknown plan(s)");
+ expect(text).toContain("Next capacity recovery");
+ expect(text).toContain("+19.2% pool capacity");
+ expect(text).toMatch(/Aug 8, 2026.*(4:32|1:32)/);
+ expect(text).not.toMatch(/configured units|weighted units|units remaining|projected/i);
+});
diff --git a/src/codex/auth-api.ts b/src/codex/auth-api.ts
index f4345d0fec..8f9e257ed7 100644
--- a/src/codex/auth-api.ts
+++ b/src/codex/auth-api.ts
@@ -834,7 +834,12 @@ export async function listCodexAuthAccounts(config: OcxConfig, forceRefresh = fa
paused: isCodexAccountPaused(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
hasCredential: hasMainCredential,
needsReauth: mainNeedsReauth,
- quota: mainInfo.quota ? { ...quotaForPlan({ ...mainInfo.quota, updatedAt: Date.now() }, mainInfo.plan) } : null,
+ quota: mainInfo.quota ? {
+ ...quotaForPlan({
+ ...mainInfo.quota,
+ updatedAt: getAccountQuota(MAIN_CODEX_ACCOUNT_ID)?.updatedAt ?? Date.now(),
+ }, mainInfo.plan),
+ } : null,
...oauthAccountHealthFields("codex", MAIN_CODEX_ACCOUNT_ID, mainHealth),
};
return [main, ...withQuota];
diff --git a/src/providers/codex-capacity.ts b/src/providers/codex-capacity.ts
index c24c4b741a..4e413e11fa 100644
--- a/src/providers/codex-capacity.ts
+++ b/src/providers/codex-capacity.ts
@@ -5,6 +5,9 @@ export const CODEX_CONFIGURED_CAPACITY_WEIGHTS = {
pro: 20,
} as const;
+/** Match the provider-report last-good freshness bound. */
+export const CODEX_CAPACITY_MAX_QUOTA_AGE_MS = 30 * 60_000;
+
export type CodexCapacityQuota = {
fiveHourPercent?: number;
fiveHourResetAt?: number;
@@ -28,9 +31,13 @@ export interface CodexCapacityAccount {
export interface CodexCapacityWindowAggregation {
usedPercent: number;
includedAccounts: number;
- totalWeight: number;
- consumedWeight: number;
- remainingWeight: number;
+ /** Internal calculation evidence; stripped from the management API response. */
+ totalWeight?: number;
+ /** Internal calculation evidence; stripped from the management API response. */
+ consumedWeight?: number;
+ /** Internal calculation evidence; stripped from the management API response. */
+ remainingWeight?: number;
+ updatedAt: number;
nextRecoveryAt?: number;
nextRecoveryPercent?: number;
}
@@ -44,6 +51,7 @@ export interface CodexCapacityAggregation {
missingQuotaAccounts: number;
pausedAccounts: number;
reauthAccounts: number;
+ staleQuotaAccounts: number;
incomplete: boolean;
fiveHour?: CodexCapacityWindowAggregation;
weekly?: CodexCapacityWindowAggregation;
@@ -67,6 +75,7 @@ type MutableWindow = {
consumedWeight: number;
includedAccounts: number;
recoveries: Map;
+ oldestUpdatedAt: number;
};
function configuredWeight(plan: string | null | undefined): number | undefined {
@@ -94,17 +103,20 @@ function addWindow(
weight: number,
percent: number,
resetAt: number | undefined,
+ updatedAt: number,
): void {
const window = windows.get(key) ?? {
totalWeight: 0,
consumedWeight: 0,
includedAccounts: 0,
recoveries: new Map(),
+ oldestUpdatedAt: updatedAt,
};
const consumed = weight * percent / 100;
window.totalWeight += weight;
window.consumedWeight += consumed;
window.includedAccounts += 1;
+ window.oldestUpdatedAt = Math.min(window.oldestUpdatedAt, updatedAt);
if (resetAt !== undefined) {
window.recoveries.set(resetAt, (window.recoveries.get(resetAt) ?? 0) + consumed);
}
@@ -120,6 +132,7 @@ function finalizeWindow(window: MutableWindow): CodexCapacityWindowAggregation {
totalWeight: window.totalWeight,
consumedWeight: window.consumedWeight,
remainingWeight: window.totalWeight - window.consumedWeight,
+ updatedAt: window.oldestUpdatedAt,
...(nextRecoveryAt !== undefined ? { nextRecoveryAt } : {}),
...(recovered !== undefined ? { nextRecoveryPercent: recovered / window.totalWeight * 100 } : {}),
};
@@ -144,7 +157,7 @@ export function aggregateCodexPoolCapacity(
let missingQuotaAccounts = 0;
let pausedAccounts = 0;
let reauthAccounts = 0;
- let updatedAt = 0;
+ let staleQuotaAccounts = 0;
for (const account of accounts) {
const weight = configuredWeight(account.plan);
@@ -152,6 +165,10 @@ export function aggregateCodexPoolCapacity(
if (account.paused) pausedAccounts += 1;
if (account.needsReauth) reauthAccounts += 1;
const quota = account.quota;
+ const quotaFresh = !!quota
+ && Number.isFinite(quota.updatedAt)
+ && now - quota.updatedAt <= CODEX_CAPACITY_MAX_QUOTA_AGE_MS;
+ if (quota && !quotaFresh) staleQuotaAccounts += 1;
const standard = quota ? [
["fiveHour", quota.fiveHourPercent, quota.fiveHourResetAt],
["weekly", quota.weeklyPercent, quota.weeklyResetAt],
@@ -161,28 +178,43 @@ export function aggregateCodexPoolCapacity(
const hasQuota = standard.some(([, percent]) => normalizedPercent(percent) !== undefined)
|| custom.some(window => normalizedPercent(window.percent) !== undefined);
if (!hasQuota) missingQuotaAccounts += 1;
- if (account.paused || account.needsReauth || weight === undefined || !quota || !hasQuota) continue;
+ if (account.paused || account.needsReauth || weight === undefined || !quota || !hasQuota || !quotaFresh) continue;
let contributed = false;
for (const [key, rawPercent, rawReset] of standard) {
const percent = normalizedPercent(rawPercent);
if (percent === undefined) continue;
- addWindow(windows, key, weight, percent, futureResetMs(rawReset, now));
+ addWindow(windows, key, weight, percent, futureResetMs(rawReset, now), quota.updatedAt);
contributed = true;
}
for (const customWindow of custom) {
const percent = normalizedPercent(customWindow.percent);
if (percent === undefined) continue;
- addWindow(windows, `custom:${customWindow.label}`, weight, percent, futureResetMs(customWindow.resetAt, now));
+ addWindow(windows, `custom:${customWindow.label}`, weight, percent, futureResetMs(customWindow.resetAt, now), quota.updatedAt);
contributed = true;
}
if (contributed) {
included.add(account);
- updatedAt = Math.max(updatedAt, quota.updatedAt);
}
}
- if (windows.size === 0) return { quota: null, aggregation: null, ...(currentAccount ? { currentAccount } : {}) };
+ if (windows.size === 0) {
+ if (staleQuotaAccounts === 0) return { quota: null, aggregation: null, ...(currentAccount ? { currentAccount } : {}) };
+ const aggregation: CodexCapacityAggregation = {
+ kind: "capacity-weighted-v1",
+ scope: "routable-known",
+ includedAccounts: 0,
+ excludedAccounts: accounts.length,
+ unknownPlanAccounts,
+ missingQuotaAccounts,
+ pausedAccounts,
+ reauthAccounts,
+ staleQuotaAccounts,
+ incomplete: true,
+ ...(currentAccount ? { currentAccount } : {}),
+ };
+ return { quota: null, aggregation, ...(currentAccount ? { currentAccount } : {}) };
+ }
const fiveHour = windows.get("fiveHour") ? finalizeWindow(windows.get("fiveHour")!) : undefined;
const weekly = windows.get("weekly") ? finalizeWindow(windows.get("weekly")!) : undefined;
const monthly = windows.get("monthly") ? finalizeWindow(windows.get("monthly")!) : undefined;
@@ -196,7 +228,10 @@ export function aggregateCodexPoolCapacity(
...(customWindows.length > 0 ? {
customWindows: customWindows.map(window => ({ label: window.label, percent: window.usedPercent })),
} : {}),
- updatedAt: updatedAt || now,
+ updatedAt: Math.min(
+ ...[fiveHour, weekly, monthly, ...customWindows]
+ .flatMap(window => window ? [window.updatedAt] : []),
+ ),
};
const excludedAccounts = accounts.length - included.size;
const aggregation: CodexCapacityAggregation = {
@@ -208,6 +243,7 @@ export function aggregateCodexPoolCapacity(
missingQuotaAccounts,
pausedAccounts,
reauthAccounts,
+ staleQuotaAccounts,
incomplete: excludedAccounts > 0,
...(fiveHour ? { fiveHour } : {}),
...(weekly ? { weekly } : {}),
diff --git a/src/providers/quota.ts b/src/providers/quota.ts
index b637993361..3fd18a8f24 100644
--- a/src/providers/quota.ts
+++ b/src/providers/quota.ts
@@ -1,3 +1,4 @@
+import { createHash } from "node:crypto";
import { effectiveCodexAuthAccountId, fetchMainAccountInfo, listCodexAuthAccounts } from "../codex/auth-api";
import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
import { resolveEnvValue } from "../config";
@@ -14,6 +15,7 @@ import {
} from "../lib/state-store-sweeper";
import {
aggregateCodexPoolCapacity,
+ CODEX_CAPACITY_MAX_QUOTA_AGE_MS,
type CodexCapacityAggregation,
type CodexCapacityQuota,
} from "./codex-capacity";
@@ -26,7 +28,7 @@ const REQUEST_TIMEOUT_MS = 8_000;
const KIMI_CODE_BASE_URL = "https://api.kimi.com/coding/v1";
const KIMI_CODE_USAGE_URL = `${KIMI_CODE_BASE_URL}/usages`;
/** Keep a failed probe's previous row at most this long before dropping it. */
-const LAST_GOOD_MAX_AGE_MS = 30 * 60_000;
+const LAST_GOOD_MAX_AGE_MS = CODEX_CAPACITY_MAX_QUOTA_AGE_MS;
export interface ProviderQuotaWindow {
label: string;
@@ -76,7 +78,73 @@ function cacheKey(config: OcxConfig): string {
.map(([name, provider]) => `${name}:${provider.adapter}:${provider.authMode ?? "key"}:${providerCodexAccountMode(name, provider) ?? "none"}:${provider.disabled === true ? "off" : "on"}:${provider.baseUrl}`)
.sort()
.join("|");
- return `${config.defaultProvider}|${config.activeCodexAccountId ?? ""}|${effectiveCodexAuthAccountId(config)}|${providers}`;
+ return `${config.defaultProvider}|${providers}`;
+}
+
+function quotaSignatureValue(quota: CodexCapacityQuota | null): unknown {
+ if (!quota) return null;
+ return {
+ fiveHourPercent: quota.fiveHourPercent,
+ fiveHourResetAt: quota.fiveHourResetAt,
+ weeklyPercent: quota.weeklyPercent,
+ weeklyResetAt: quota.weeklyResetAt,
+ monthlyPercent: quota.monthlyPercent,
+ monthlyResetAt: quota.monthlyResetAt,
+ updatedAt: quota.updatedAt,
+ customWindows: [...(quota.customWindows ?? [])]
+ .map(window => ({ label: window.label, percent: window.percent, resetAt: window.resetAt }))
+ .sort((a, b) => a.label.localeCompare(b.label)),
+ };
+}
+
+/** Hash only presentation-relevant state; account ids and email addresses never enter the key. */
+function cacheKeyWithAggregationState(config: OcxConfig): string | Promise {
+ const base = cacheKey(config);
+ const poolEnabled = Object.entries(config.providers).some(([name, provider]) => (
+ provider.disabled !== true
+ && isBuiltInChatGptForwardProvider(name, provider)
+ && providerCodexAccountMode(name, provider) !== "direct"
+ ));
+ if (!poolEnabled) return base;
+ return (async () => {
+ try {
+ const activeId = effectiveCodexAuthAccountId(config);
+ const rows = (await listCodexAuthAccounts(config, false)).map(account => ({
+ isMain: account.isMain,
+ active: account.id === activeId,
+ plan: account.plan?.trim().toLowerCase() ?? null,
+ paused: account.paused,
+ needsReauth: account.needsReauth === true,
+ quota: quotaSignatureValue(account.quota as CodexCapacityQuota | null),
+ }));
+ const canonicalRows = rows.map(row => JSON.stringify(row)).sort();
+ const digest = createHash("sha256").update(JSON.stringify(canonicalRows)).digest("hex").slice(0, 24);
+ return `${base}|codex-pool:${digest}`;
+ } catch {
+ return `${base}|codex-pool:unavailable`;
+ }
+ })();
+}
+
+function publicCapacityWindow(window: import("./codex-capacity").CodexCapacityWindowAggregation) {
+ const { totalWeight: _totalWeight, consumedWeight: _consumedWeight, remainingWeight: _remainingWeight, ...safe } = window;
+ return safe;
+}
+
+/** Management API metadata intentionally omits configured/weighted unit counts. */
+function publicCapacityAggregation(aggregation: CodexCapacityAggregation): CodexCapacityAggregation {
+ return {
+ ...aggregation,
+ ...(aggregation.fiveHour ? { fiveHour: publicCapacityWindow(aggregation.fiveHour) } : {}),
+ ...(aggregation.weekly ? { weekly: publicCapacityWindow(aggregation.weekly) } : {}),
+ ...(aggregation.monthly ? { monthly: publicCapacityWindow(aggregation.monthly) } : {}),
+ ...(aggregation.customWindows ? {
+ customWindows: aggregation.customWindows.map(window => ({
+ label: window.label,
+ ...publicCapacityWindow(window),
+ })),
+ } : {}),
+ };
}
function hasQuotaRows(quota: ProviderQuota | null | undefined): quota is ProviderQuota {
@@ -165,12 +233,17 @@ async function fetchChatGptForwardQuota(
?? accounts[0];
const capacity = aggregateCodexPoolCapacity(capacityAccounts, Date.now());
if (capacity.aggregation && capacity.quota) {
- return report(provider, "chatgpt:wham", capacity.quota as ProviderQuota, capacity.aggregation);
+ return report(provider, "chatgpt:wham", capacity.quota as ProviderQuota, publicCapacityAggregation(capacity.aggregation));
}
const quota = active?.quota
? { ...active.quota, updatedAt: active.quota.updatedAt ?? Date.now() } as CodexCapacityQuota
: null;
- return quota ? report(provider, "chatgpt:wham", quota as ProviderQuota) : null;
+ return quota ? report(
+ provider,
+ "chatgpt:wham",
+ quota as ProviderQuota,
+ capacity.aggregation ? publicCapacityAggregation(capacity.aggregation) : undefined,
+ ) : null;
}
function centsValue(value: unknown): number | undefined {
@@ -936,7 +1009,8 @@ async function maybeFetchProviderQuota(
}
export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh = false): Promise {
- const key = cacheKey(config);
+ const keyCandidate = cacheKeyWithAggregationState(config);
+ const key = typeof keyCandidate === "string" ? keyCandidate : await keyCandidate;
const writerGeneration = captureConfigGeneration();
const now = Date.now();
// The cache fast path must not extend a preserved last-good row past its 30-minute bound:
@@ -972,7 +1046,9 @@ export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh
// Commit only when this probe still holds authority (no clear/force superseded it).
if (epoch === invalidationEpoch) {
const reports = response.reports.filter(item => mayCommitProviderQuotaKey(item.provider, writerGeneration));
- cache = { key, ts: Date.now(), response: { ...response, reports } };
+ const commitKeyCandidate = cacheKeyWithAggregationState(config);
+ const commitKey = typeof commitKeyCandidate === "string" ? commitKeyCandidate : await commitKeyCandidate;
+ cache = { key: commitKey, ts: Date.now(), response: { ...response, reports } };
}
return response;
})();
diff --git a/tests/provider-capacity.test.ts b/tests/provider-capacity.test.ts
index 5844d328c1..4ae364d909 100644
--- a/tests/provider-capacity.test.ts
+++ b/tests/provider-capacity.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
-import { aggregateCodexPoolCapacity, type CodexCapacityAccount } from "../src/providers/codex-capacity";
+import { aggregateCodexPoolCapacity, CODEX_CAPACITY_MAX_QUOTA_AGE_MS, type CodexCapacityAccount } from "../src/providers/codex-capacity";
const NOW = 1_800_000_000_000;
const account = (
@@ -98,4 +98,43 @@ describe("configured-weight Codex pool capacity", () => {
expect(fallback.aggregation).toBeNull();
expect(fallback.currentAccount?.quota?.weeklyPercent).toBe(70);
});
+
+ test("mixed-age rows exclude stale capacity and use the oldest included reading", () => {
+ const oldestIncluded = account("plus", 40);
+ oldestIncluded.quota = { ...oldestIncluded.quota!, updatedAt: NOW - 20_000 };
+ const newerIncluded = account("business", 20);
+ newerIncluded.quota = { ...newerIncluded.quota!, updatedAt: NOW - 5_000 };
+ const staleHighWeight = account("pro", 100);
+ staleHighWeight.quota = {
+ ...staleHighWeight.quota!,
+ updatedAt: NOW - CODEX_CAPACITY_MAX_QUOTA_AGE_MS - 1,
+ };
+
+ const result = aggregateCodexPoolCapacity([oldestIncluded, newerIncluded, staleHighWeight], NOW);
+ expect(result.quota?.weeklyPercent).toBeCloseTo(30, 8);
+ expect(result.quota?.updatedAt).toBe(NOW - 20_000);
+ expect(result.aggregation?.weekly?.updatedAt).toBe(NOW - 20_000);
+ expect(result.aggregation).toMatchObject({
+ includedAccounts: 2,
+ excludedAccounts: 1,
+ staleQuotaAccounts: 1,
+ incomplete: true,
+ });
+ });
+
+ test("all-stale rows expose incomplete coverage without an aggregate window", () => {
+ const rows = [account("pro", 80, { active: true, isMain: true }), account("prolite", 20)];
+ for (const row of rows) {
+ row.quota = { ...row.quota!, updatedAt: NOW - CODEX_CAPACITY_MAX_QUOTA_AGE_MS - 1 };
+ }
+ const result = aggregateCodexPoolCapacity(rows, NOW);
+ expect(result.quota).toBeNull();
+ expect(result.aggregation).toMatchObject({
+ includedAccounts: 0,
+ excludedAccounts: 2,
+ staleQuotaAccounts: 2,
+ incomplete: true,
+ });
+ expect(result.aggregation?.weekly).toBeUndefined();
+ });
});
diff --git a/tests/provider-quota.test.ts b/tests/provider-quota.test.ts
index d6cd47e5aa..38c6c3bc78 100644
--- a/tests/provider-quota.test.ts
+++ b/tests/provider-quota.test.ts
@@ -2,7 +2,8 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
-import { clearAccountQuota } from "../src/codex/quota";
+import { clearAccountQuota, updateAccountQuota } from "../src/codex/quota";
+import { clearAccountNeedsReauth, markAccountNeedsReauth } from "../src/codex/account-runtime-state";
import { clearCodexUpstreamHealth } from "../src/codex/routing";
import { saveCodexAccountCredential } from "../src/codex/account-store";
import { saveCredential } from "../src/oauth/store";
@@ -510,6 +511,107 @@ describe("fetchProviderQuotaReports", () => {
incomplete: false,
currentAccount: { plan: "prolite", quota: { weeklyPercent: 77 } },
});
+ expect(JSON.stringify(openai?.aggregation)).not.toMatch(/(?:total|consumed|remaining)Weight|projectedUsedPercent/i);
+ });
+
+ test("ordinary fetch reflects pausing a non-active pool account", async () => {
+ saveCodexAccountCredential("added", {
+ accessToken: "added-access", refreshToken: "added-refresh",
+ expiresAt: Date.now() + 3600_000, chatgptAccountId: "added-chatgpt-id",
+ });
+ const config = testConfig();
+ config.providers = { openai: config.providers.openai };
+ config.codexAccounts = [{ id: "added", email: "a@example.test", plan: "prolite", isMain: false }];
+ globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => {
+ const added = (init?.headers as Record | undefined)?.["ChatGPT-Account-Id"] === "added-chatgpt-id";
+ return new Response(JSON.stringify({
+ plan_type: added ? "prolite" : "plus",
+ rate_limit: { secondary_window: { used_percent: added ? 77 : 11, reset_at: 1_999_000_000 } },
+ }), { status: 200, headers: { "content-type": "application/json" } });
+ }) as typeof fetch;
+
+ expect((await fetchProviderQuotaReports(config, true)).reports[0]?.quota.weeklyPercent).toBe(66);
+ config.pausedCodexAccountIds = ["added"];
+ const paused = (await fetchProviderQuotaReports(config)).reports[0];
+ expect(paused?.quota.weeklyPercent).toBe(11);
+ expect(paused?.aggregation).toMatchObject({ includedAccounts: 1, excludedAccounts: 1, incomplete: true });
+ });
+
+ test("ordinary fetch separates plan, quota, and effective-account cache states", async () => {
+ saveCodexAccountCredential("added", {
+ accessToken: "added-access", refreshToken: "added-refresh",
+ expiresAt: Date.now() + 3600_000, chatgptAccountId: "added-chatgpt-id",
+ });
+ const config = testConfig();
+ config.providers = { openai: config.providers.openai };
+ config.codexAccounts = [{ id: "added", email: "a@example.test", plan: "prolite", isMain: false }];
+ globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => {
+ const added = (init?.headers as Record | undefined)?.["ChatGPT-Account-Id"] === "added-chatgpt-id";
+ return new Response(JSON.stringify({
+ plan_type: added ? "prolite" : "plus",
+ rate_limit: { secondary_window: { used_percent: added ? 77 : 11, reset_at: 1_999_000_000 } },
+ }), { status: 200, headers: { "content-type": "application/json" } });
+ }) as typeof fetch;
+
+ await fetchProviderQuotaReports(config, true);
+ config.codexAccounts[0]!.plan = "pro";
+ expect((await fetchProviderQuotaReports(config)).reports[0]?.aggregation?.weekly?.usedPercent).toBeCloseTo((20 * 77 + 11) / 21, 8);
+ config.activeCodexAccountId = "added";
+ expect((await fetchProviderQuotaReports(config)).reports[0]?.aggregation?.currentAccount).toMatchObject({ plan: "pro", quota: { weeklyPercent: 77 } });
+ updateAccountQuota("added", 20, 1_999_000_000);
+ expect((await fetchProviderQuotaReports(config)).reports[0]?.aggregation?.weekly?.usedPercent).toBeCloseTo((20 * 20 + 11) / 21, 8);
+ });
+
+ test("ordinary fetch reflects runtime reauthentication state", async () => {
+ saveCodexAccountCredential("added", {
+ accessToken: "added-access", refreshToken: "added-refresh",
+ expiresAt: Date.now() + 3600_000, chatgptAccountId: "added-chatgpt-id",
+ });
+ const config = testConfig();
+ config.providers = { openai: config.providers.openai };
+ config.codexAccounts = [{ id: "added", email: "a@example.test", plan: "prolite", isMain: false }];
+ globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => {
+ const added = (init?.headers as Record | undefined)?.["ChatGPT-Account-Id"] === "added-chatgpt-id";
+ return new Response(JSON.stringify({
+ plan_type: added ? "prolite" : "plus",
+ rate_limit: { secondary_window: { used_percent: added ? 77 : 11, reset_at: 1_999_000_000 } },
+ }), { status: 200, headers: { "content-type": "application/json" } });
+ }) as typeof fetch;
+
+ await fetchProviderQuotaReports(config, true);
+ markAccountNeedsReauth("added");
+ const reauth = (await fetchProviderQuotaReports(config)).reports[0];
+ expect(reauth?.aggregation).toMatchObject({ includedAccounts: 1, reauthAccounts: 1, incomplete: true });
+ clearAccountNeedsReauth("added");
+ });
+
+ test("ordinary fetch reflects pool account add and remove", async () => {
+ saveCodexAccountCredential("added", {
+ accessToken: "added-access", refreshToken: "added-refresh",
+ expiresAt: Date.now() + 3600_000, chatgptAccountId: "added-chatgpt-id",
+ });
+ const config = testConfig();
+ config.providers = { openai: config.providers.openai };
+ config.codexAccounts = [{ id: "added", email: "a@example.test", plan: "prolite", isMain: false }];
+ globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => {
+ const id = (init?.headers as Record | undefined)?.["ChatGPT-Account-Id"];
+ const plan = id === "added-chatgpt-id" ? "prolite" : id === "second-chatgpt-id" ? "business" : "plus";
+ const percent = id === "added-chatgpt-id" ? 77 : id === "second-chatgpt-id" ? 33 : 11;
+ return new Response(JSON.stringify({
+ plan_type: plan,
+ rate_limit: { secondary_window: { used_percent: percent, reset_at: 1_999_000_000 } },
+ }), { status: 200, headers: { "content-type": "application/json" } });
+ }) as typeof fetch;
+
+ expect((await fetchProviderQuotaReports(config, true)).reports[0]?.aggregation?.includedAccounts).toBe(2);
+ saveCodexAccountCredential("second", {
+ accessToken: "second-access", refreshToken: "second-refresh",
+ expiresAt: Date.now() + 3600_000, chatgptAccountId: "second-chatgpt-id",
+ });
+ config.codexAccounts.push({ id: "second", email: "b@example.test", plan: "business", isMain: false });
+ expect((await fetchProviderQuotaReports(config)).reports[0]?.aggregation?.includedAccounts).toBe(3);
+ config.codexAccounts = config.codexAccounts.filter(account => account.id !== "second");
+ expect((await fetchProviderQuotaReports(config)).reports[0]?.aggregation?.includedAccounts).toBe(2);
});
test("direct mode reports main without reading or repairing the added-account store", async () => {
From ca35a559f1a1afe57772a68a0b8cb2a73b87c9ef Mon Sep 17 00:00:00 2001
From: luvs01 <27862058+luvs01@users.noreply.github.com>
Date: Sun, 2 Aug 2026 16:33:26 +0900
Subject: [PATCH 03/10] docs: explain Codex pool capacity estimate
Document the Providers overview's weighted pool percentage, effective-account quota, recovery display, incomplete coverage, and display-only routing boundary.
Refs #874
---
docs-site/src/content/docs/guides/providers.md | 16 ++++++++++++++++
.../src/content/docs/guides/web-dashboard.md | 5 +++++
2 files changed, 21 insertions(+)
diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md
index 341d822203..907e7efc9e 100644
--- a/docs-site/src/content/docs/guides/providers.md
+++ b/docs-site/src/content/docs/guides/providers.md
@@ -24,6 +24,22 @@ Auth page can restore it: absent rows are created from the canonical preset, dis
rows are re-enabled without replacing saved mode or model settings, and noncanonical `openai`
rows are not offered that recovery path.
+### Providers overview pool capacity
+
+For Codex login in Pool mode, the Providers overview shows a configured-weight estimate of the
+pool's used capacity rather than presenting one arbitrary account as the provider total. The same
+row also shows the current effective account's raw quota percentage, so you can distinguish the
+pool estimate from the account that a new request would use.
+
+When reset information is available, the overview shows the next reset time and the capacity that
+reset is expected to recover as `+N% pool capacity`. **Incomplete coverage** means one or more pool
+accounts could not safely contribute to the estimate, for example because their plan or quota is
+unknown, their reading is stale, or the account is paused or needs reauthentication.
+
+This estimate is display-only. It does not change account selection, session affinity, automatic
+switching, cooldowns, or any other routing decision. Use the [Codex Auth account pool](/guides/web-dashboard/#codex-auth-and-account-pools)
+for the individual account state and routing controls.
+
Shipped v1 configs migrate automatically to marker 2 and one option-aware row. The original config
is retained once at `~/.opencodex/config.json.pre-openai-tiers-v2.bak`; restore it with
`cp ~/.opencodex/config.json.pre-openai-tiers-v2.bak ~/.opencodex/config.json`.
diff --git a/docs-site/src/content/docs/guides/web-dashboard.md b/docs-site/src/content/docs/guides/web-dashboard.md
index a4198dbe59..04d74877c1 100644
--- a/docs-site/src/content/docs/guides/web-dashboard.md
+++ b/docs-site/src/content/docs/guides/web-dashboard.md
@@ -100,6 +100,11 @@ catalog entry.
The **Codex Auth** page manages the native ChatGPT/Codex route:
+The Providers overview separately summarizes Pool-mode usage as a display-only weighted capacity
+estimate, alongside the effective account's raw quota and the next capacity recovery. See
+[Providers overview pool capacity](/guides/providers/#providers-overview-pool-capacity) for the
+visible fields, incomplete-coverage meaning, and routing boundary.
+
- Manually choosing an account changes the next new Codex session; an already-bound thread keeps its
current account for that manual switch.
- Thread affinity prevents per-request flapping. With quota auto-switch enabled, a long-running
From eb5f7b356160a005481f9408f3238e09515f5355 Mon Sep 17 00:00:00 2001
From: luvs01 <27862058+luvs01@users.noreply.github.com>
Date: Sun, 2 Aug 2026 16:50:17 +0900
Subject: [PATCH 04/10] fix(gui): harden pool capacity presentation
Replace stale provider quota state authoritatively, retain per-window coverage, and distinguish aggregate, fallback, and coverage-only reports.
Reject cache commits when pool presentation state changes during a probe and allow recovery details to wrap in narrow panes.
Refs #874
---
.../ProviderOverviewDashboard.tsx | 10 +-
.../ProviderWorkspaceShell.tsx | 76 +++++++--
gui/src/provider-workspace/report.ts | 21 ++-
.../styles/provider-overview-dashboard.css | 21 +++
gui/tests/provider-capacity-shell.test.tsx | 144 ++++++++++++++----
gui/tests/provider-capacity.test.ts | 36 +++++
src/providers/codex-capacity.ts | 28 +++-
src/providers/quota.ts | 48 ++++--
tests/provider-capacity.test.ts | 33 +++-
tests/provider-quota.test.ts | 64 ++++++++
10 files changed, 414 insertions(+), 67 deletions(-)
diff --git a/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx b/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx
index 5ae0d6c5b3..ce0f676db4 100644
--- a/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx
+++ b/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx
@@ -239,7 +239,9 @@ function ProviderCapacityQuota({ report, pending }: { report: ProviderQuotaRepor
const t = useT();
const { locale } = useI18n();
const aggregation = capacityAggregationFromReport(report);
- const recoveryRows: Array<{ label: string; window: CapacityWindowView }> = aggregation ? [
+ const primaryQuota = accountQuotaFromReport(report);
+ const showsAggregate = aggregation?.presentation === "aggregate";
+ const recoveryRows: Array<{ label: string; window: CapacityWindowView }> = showsAggregate && aggregation ? [
...(aggregation.fiveHour ? [{ label: t("codexAuth.fiveHour"), window: aggregation.fiveHour }] : []),
...(aggregation.weekly ? [{ label: t("codexAuth.weekly"), window: aggregation.weekly }] : []),
...(aggregation.monthly ? [{ label: t("codexAuth.monthly"), window: aggregation.monthly }] : []),
@@ -253,8 +255,8 @@ function ProviderCapacityQuota({ report, pending }: { report: ProviderQuotaRepor
return (
<>
- {aggregation && {t("pws.capacity.estimate")}
}
-
+ {showsAggregate && {t("pws.capacity.estimate")}
}
+ {(primaryQuota || pending) && }
{aggregation && (
{recoveryRows.flatMap(({ label, window }) => (
@@ -265,7 +267,7 @@ function ProviderCapacityQuota({ report, pending }: { report: ProviderQuotaRepor
]
: []
))}
- {aggregation.currentAccount?.quota && (
+ {showsAggregate && aggregation.currentAccount?.quota && (
{t("pws.capacity.currentAccount")}
diff --git a/gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx b/gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx
index 583ad279a0..ecce71a6fc 100644
--- a/gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx
+++ b/gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx
@@ -53,6 +53,51 @@ const SORT_DEFS: { id: ProviderSortMode; labelKey: "pws.sort.az" | "pws.sort.za"
{ id: "accounts-first", labelKey: "pws.sort.accountsFirst" },
];
+const QUOTA_REPORT_MAX_AGE_MS = 30 * 60_000;
+
+function freshQuotaReport(value: unknown, now: number): ProviderQuotaReportView | null {
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
+ const row = value as Record;
+ if (typeof row.updatedAt !== "number" || !Number.isFinite(row.updatedAt)) return null;
+ if (now - row.updatedAt >= QUOTA_REPORT_MAX_AGE_MS) return null;
+ if (!("quota" in row)) return null;
+ if (row.label !== undefined && typeof row.label !== "string") return null;
+ if (row.source !== undefined && typeof row.source !== "string") return null;
+ return {
+ ...(typeof row.label === "string" ? { label: row.label } : {}),
+ ...(typeof row.source === "string" ? { source: row.source } : {}),
+ updatedAt: row.updatedAt,
+ quota: row.quota,
+ ...(row.aggregation !== undefined ? { aggregation: row.aggregation } : {}),
+ };
+}
+
+function freshQuotaReportRecord(value: unknown, now = Date.now()): Record | null {
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
+ const out: Record = {};
+ for (const [provider, raw] of Object.entries(value)) {
+ const report = freshQuotaReport(raw, now);
+ if (provider.trim() && report) out[provider] = report;
+ }
+ return out;
+}
+
+function readFreshQuotaReportCache(key: string): Record | null {
+ return freshQuotaReportRecord(readSessionListCache(key));
+}
+
+function freshQuotaReportsFromResponse(value: unknown, now = Date.now()): Record {
+ if (!Array.isArray(value)) return {};
+ const out: Record = {};
+ for (const raw of value) {
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
+ const provider = (raw as Record).provider;
+ const report = freshQuotaReport(raw, now);
+ if (typeof provider === "string" && provider.trim() && report) out[provider] = report;
+ }
+ return out;
+}
+
export default function ProviderWorkspaceShell({
providers,
apiBase,
@@ -119,10 +164,13 @@ export default function ProviderWorkspaceShell({
readSessionListCache<{ models: Record }>(usageCacheKey)?.models ?? {}
));
const [quotaReports, setQuotaReports] = useState>(() => (
- readSessionListCache>(quotasCacheKey) ?? {}
+ readFreshQuotaReportCache(quotasCacheKey) ?? {}
));
const [usageLoading, setUsageLoading] = useState(() => !readSessionListCache(usageCacheKey));
- const [quotasLoading, setQuotasLoading] = useState(() => !readSessionListCache(quotasCacheKey));
+ const [quotasLoading, setQuotasLoading] = useState(() => {
+ const cached = readFreshQuotaReportCache(quotasCacheKey);
+ return !cached || Object.keys(cached).length === 0;
+ });
const [modelsLoadEpoch, setModelsLoadEpoch] = useState(0);
const filterWrapRef = useRef(null);
@@ -213,7 +261,8 @@ export default function ProviderWorkspaceShell({
useEffect(() => {
let cancelled = false;
const timeout = window.setTimeout(() => {
- if (!readSessionListCache(quotasCacheKey)) setQuotasLoading(true);
+ const cached = readFreshQuotaReportCache(quotasCacheKey);
+ if (!cached || Object.keys(cached).length === 0) setQuotasLoading(true);
// A forced bump means a mutation just changed the answer, so the server's TTL has to
// be bypassed. The old derived-key effect always read the cached view, which is why a
// switch could leave the bars showing the previous account's quota.
@@ -221,24 +270,19 @@ export default function ProviderWorkspaceShell({
.then(r => readJsonIfOk<{ reports?: Array<{ provider: string; label?: string; source?: string; updatedAt?: number; quota?: unknown; aggregation?: unknown }> }>(r))
.then((data) => {
if (cancelled || !data) return;
- // Merge so a partial/failed probe cannot wipe a previously good provider row.
+ // A successful endpoint response is authoritative, including an empty report list.
+ const next = freshQuotaReportsFromResponse(data.reports);
+ setQuotaReports(next);
+ writeSessionListCache(quotasCacheKey, next);
+ })
+ .catch(() => {
+ // Keep last-good only inside the same server freshness bound.
setQuotaReports(prev => {
- const next = { ...prev };
- for (const report of data.reports ?? []) {
- if (!report?.provider) continue;
- next[report.provider] = {
- label: report.label,
- source: report.source,
- updatedAt: typeof report.updatedAt === "number" ? report.updatedAt : Date.now(),
- quota: report.quota,
- ...(report.aggregation !== undefined ? { aggregation: report.aggregation } : {}),
- };
- }
+ const next = freshQuotaReportRecord(prev) ?? {};
writeSessionListCache(quotasCacheKey, next);
return next;
});
})
- .catch(() => { /* keep last-good */ })
.finally(() => { if (!cancelled) setQuotasLoading(false); });
}, 0);
return () => {
diff --git a/gui/src/provider-workspace/report.ts b/gui/src/provider-workspace/report.ts
index b53b9d3bcc..9a8a3ec380 100644
--- a/gui/src/provider-workspace/report.ts
+++ b/gui/src/provider-workspace/report.ts
@@ -16,11 +16,14 @@ export interface ProviderQuotaReportView {
export interface CapacityWindowView {
usedPercent: number;
+ incomplete?: boolean;
+ excludedAccounts?: number;
nextRecoveryAt?: number;
nextRecoveryPercent?: number;
}
export interface ProviderCapacityAggregationView {
+ presentation: "aggregate" | "effective-account-fallback" | "coverage-only";
incomplete: boolean;
excludedAccounts: number;
unknownPlanAccounts: number;
@@ -80,6 +83,8 @@ function capacityWindow(value: unknown): CapacityWindowView | undefined {
if (usedPercent === undefined) return undefined;
return {
usedPercent,
+ ...(typeof row.incomplete === "boolean" ? { incomplete: row.incomplete } : {}),
+ ...(finite(row.excludedAccounts) !== undefined ? { excludedAccounts: row.excludedAccounts as number } : {}),
...(finite(row.nextRecoveryAt) !== undefined ? { nextRecoveryAt: row.nextRecoveryAt as number } : {}),
...(finite(row.nextRecoveryPercent) !== undefined ? { nextRecoveryPercent: row.nextRecoveryPercent as number } : {}),
};
@@ -105,13 +110,23 @@ export function capacityAggregationFromReport(report?: ProviderQuotaReportView):
return typeof custom.label === "string" && window ? [{ label: custom.label, ...window }] : [];
})
: [];
+ const fiveHour = capacityWindow(row.fiveHour);
+ const weekly = capacityWindow(row.weekly);
+ const monthly = capacityWindow(row.monthly);
+ const hasAggregateWindow = !!fiveHour || !!weekly || !!monthly || customWindows.length > 0;
+ const presentation = row.presentation === "aggregate"
+ || row.presentation === "effective-account-fallback"
+ || row.presentation === "coverage-only"
+ ? row.presentation
+ : hasAggregateWindow ? "aggregate" : "coverage-only";
return {
+ presentation,
incomplete: row.incomplete,
excludedAccounts,
unknownPlanAccounts,
- ...(capacityWindow(row.fiveHour) ? { fiveHour: capacityWindow(row.fiveHour) } : {}),
- ...(capacityWindow(row.weekly) ? { weekly: capacityWindow(row.weekly) } : {}),
- ...(capacityWindow(row.monthly) ? { monthly: capacityWindow(row.monthly) } : {}),
+ ...(fiveHour ? { fiveHour } : {}),
+ ...(weekly ? { weekly } : {}),
+ ...(monthly ? { monthly } : {}),
...(customWindows.length > 0 ? { customWindows } : {}),
...(currentRaw ? {
currentAccount: {
diff --git a/gui/src/styles/provider-overview-dashboard.css b/gui/src/styles/provider-overview-dashboard.css
index d09b8058fc..09f00fd1e2 100644
--- a/gui/src/styles/provider-overview-dashboard.css
+++ b/gui/src/styles/provider-overview-dashboard.css
@@ -211,14 +211,35 @@
.pws-capacity-recovery {
display: flex;
+ flex-wrap: wrap;
justify-content: space-between;
+ align-items: flex-start;
gap: 12px;
color: var(--fg-muted, #888);
}
+.pws-capacity-recovery span {
+ min-width: 0;
+ overflow-wrap: anywhere;
+}
+
.pws-capacity-recovery strong {
color: var(--text);
white-space: nowrap;
+ margin-left: auto;
+}
+
+@container (max-width: 520px) {
+ .pws-capacity-recovery {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr);
+ gap: 3px;
+ }
+
+ .pws-capacity-recovery strong {
+ margin-left: 0;
+ white-space: normal;
+ }
}
.pws-capacity-current {
diff --git a/gui/tests/provider-capacity-shell.test.tsx b/gui/tests/provider-capacity-shell.test.tsx
index 7039bc50dd..7712d73d57 100644
--- a/gui/tests/provider-capacity-shell.test.tsx
+++ b/gui/tests/provider-capacity-shell.test.tsx
@@ -4,6 +4,7 @@ import { act } from "react";
import type { Root } from "react-dom/client";
import ProviderWorkspaceShell from "../src/components/provider-workspace/ProviderWorkspaceShell";
import { LanguageProvider } from "../src/i18n/provider";
+import { readSessionListCache, writeSessionListCache } from "../src/session-list-cache";
const globals = ["document", "window", "navigator", "localStorage", "sessionStorage", "IS_REACT_ACT_ENVIRONMENT"] as const;
let previous: Record<(typeof globals)[number], unknown>;
@@ -11,6 +12,49 @@ let originalFetch: typeof globalThis.fetch;
let win: Window;
let host: HTMLElement;
let root: Root | null = null;
+let quotaPayload: unknown;
+let rejectQuotaFetch = false;
+
+const QUOTA_CACHE_KEY = "ocx.providers.quotas.v1:";
+const RECOVERY_AT = Date.UTC(2026, 7, 8, 4, 32);
+const providers = {
+ openai: { adapter: "openai-responses", authMode: "forward", baseUrl: "https://chatgpt.com/backend-api/codex" },
+} as never;
+
+function aggregatePayload() {
+ return {
+ reports: [{
+ provider: "openai",
+ label: "OpenAI (Codex login)",
+ source: "chatgpt:wham",
+ updatedAt: Date.now(),
+ quota: { weeklyPercent: 30.8, updatedAt: Date.now() },
+ aggregation: {
+ kind: "capacity-weighted-v1",
+ scope: "routable-known",
+ presentation: "aggregate",
+ includedAccounts: 2,
+ excludedAccounts: 1,
+ unknownPlanAccounts: 1,
+ missingQuotaAccounts: 0,
+ pausedAccounts: 0,
+ reauthAccounts: 0,
+ staleQuotaAccounts: 0,
+ incomplete: true,
+ weekly: {
+ usedPercent: 30.8,
+ includedAccounts: 2,
+ excludedAccounts: 1,
+ incomplete: true,
+ updatedAt: Date.now(),
+ nextRecoveryAt: RECOVERY_AT,
+ nextRecoveryPercent: 19.2,
+ },
+ currentAccount: { isMain: true, plan: "pro", quota: { weeklyPercent: 8, updatedAt: Date.now() } },
+ },
+ }],
+ };
+}
beforeEach(() => {
previous = Object.fromEntries(globals.map(key => [key, Reflect.get(globalThis, key)])) as typeof previous;
@@ -26,34 +70,14 @@ beforeEach(() => {
sessionStorage: { configurable: true, value: win.sessionStorage },
});
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
- const recoveryAt = Date.UTC(2026, 7, 8, 4, 32);
+ quotaPayload = aggregatePayload();
+ rejectQuotaFetch = false;
Object.defineProperty(globalThis, "fetch", {
configurable: true,
value: async (input: string) => {
const url = String(input);
- const body = url.includes("/api/provider-quotas") ? {
- reports: [{
- provider: "openai",
- label: "OpenAI (Codex login)",
- source: "chatgpt:wham",
- updatedAt: Date.now(),
- quota: { weeklyPercent: 30.8, updatedAt: Date.now() },
- aggregation: {
- kind: "capacity-weighted-v1",
- scope: "routable-known",
- includedAccounts: 2,
- excludedAccounts: 1,
- unknownPlanAccounts: 1,
- missingQuotaAccounts: 0,
- pausedAccounts: 0,
- reauthAccounts: 0,
- staleQuotaAccounts: 0,
- incomplete: true,
- weekly: { usedPercent: 30.8, includedAccounts: 2, updatedAt: Date.now(), nextRecoveryAt: recoveryAt, nextRecoveryPercent: 19.2 },
- currentAccount: { isMain: true, plan: "pro", quota: { weeklyPercent: 8, updatedAt: Date.now() } },
- },
- }],
- } : {};
+ if (url.includes("/api/provider-quotas") && rejectQuotaFetch) throw new Error("quota unavailable");
+ const body = url.includes("/api/provider-quotas") ? quotaPayload : {};
return { ok: true, status: 200, json: async () => body, text: async () => JSON.stringify(body) } as unknown as Response;
},
});
@@ -71,14 +95,14 @@ afterEach(async () => {
Object.defineProperty(globalThis, "fetch", { configurable: true, value: originalFetch });
});
-test("provider quota fetch preserves aggregate capacity through shell state and render", async () => {
+async function mountShell() {
const { createRoot } = await import("react-dom/client");
await act(async () => {
root = createRoot(host);
root.render(
{ await new Promise(resolve => setTimeout(resolve, 30)); });
+}
+
+test("provider quota fetch preserves aggregate capacity through shell state and render", async () => {
+ await mountShell();
const text = host.textContent ?? "";
expect(text).toContain("Configured-weight pool estimate");
@@ -101,3 +129,67 @@ test("provider quota fetch preserves aggregate capacity through shell state and
expect(text).toMatch(/Aug 8, 2026.*(4:32|1:32)/);
expect(text).not.toMatch(/configured units|weighted units|units remaining|projected/i);
});
+
+test("successful empty quota response removes cached providers and updates session cache", async () => {
+ const seeded = (aggregatePayload().reports[0]);
+ const { provider: _provider, ...cached } = seeded;
+ writeSessionListCache(QUOTA_CACHE_KEY, { openai: cached });
+ quotaPayload = { reports: [] };
+
+ await mountShell();
+
+ expect(host.textContent ?? "").not.toContain("Configured-weight pool estimate");
+ expect(readSessionListCache(QUOTA_CACHE_KEY)).toEqual({});
+});
+
+test("expired session quota is rejected and a failed fetch cannot keep it rendered", async () => {
+ const old = Date.now() - 31 * 60_000;
+ writeSessionListCache(QUOTA_CACHE_KEY, {
+ openai: {
+ label: "OpenAI (Codex login)",
+ source: "chatgpt:wham",
+ updatedAt: old,
+ quota: { weeklyPercent: 99, updatedAt: old },
+ aggregation: { ...aggregatePayload().reports[0].aggregation, presentation: "aggregate" },
+ },
+ });
+ rejectQuotaFetch = true;
+
+ await mountShell();
+
+ const text = host.textContent ?? "";
+ expect(text).not.toContain("Configured-weight pool estimate");
+ expect(text).not.toContain("99% used");
+ expect(readSessionListCache(QUOTA_CACHE_KEY)).toEqual({});
+});
+
+test("all-stale effective fallback renders one raw bar without aggregate labelling", async () => {
+ const old = Date.now() - 31 * 60_000;
+ quotaPayload = {
+ reports: [{
+ provider: "openai",
+ label: "OpenAI (Codex login)",
+ source: "chatgpt:wham",
+ updatedAt: Date.now(),
+ quota: { weeklyPercent: 80, updatedAt: old },
+ aggregation: {
+ kind: "capacity-weighted-v1",
+ scope: "routable-known",
+ presentation: "effective-account-fallback",
+ includedAccounts: 0,
+ excludedAccounts: 2,
+ unknownPlanAccounts: 0,
+ incomplete: true,
+ currentAccount: { isMain: true, plan: "pro", quota: { weeklyPercent: 80, updatedAt: old } },
+ },
+ }],
+ };
+
+ await mountShell();
+
+ const text = host.textContent ?? "";
+ expect(text).not.toContain("Configured-weight pool estimate");
+ expect(text).not.toContain("Current effective account");
+ expect(text.match(/80% used/g)?.length).toBe(1);
+ expect(text).toContain("Incomplete coverage: 2 account(s) excluded");
+});
diff --git a/gui/tests/provider-capacity.test.ts b/gui/tests/provider-capacity.test.ts
index 863bb5c2ad..23bfb11f0d 100644
--- a/gui/tests/provider-capacity.test.ts
+++ b/gui/tests/provider-capacity.test.ts
@@ -11,6 +11,7 @@ test("capacity metadata preserves estimate, raw current quota, recovery percent,
aggregation: {
kind: "capacity-weighted-v1",
scope: "routable-known",
+ presentation: "aggregate",
incomplete: true,
excludedAccounts: 2,
unknownPlanAccounts: 1,
@@ -26,6 +27,7 @@ test("capacity metadata preserves estimate, raw current quota, recovery percent,
},
});
expect(aggregation).toMatchObject({
+ presentation: "aggregate",
incomplete: true,
excludedAccounts: 2,
unknownPlanAccounts: 1,
@@ -35,6 +37,40 @@ test("capacity metadata preserves estimate, raw current quota, recovery percent,
expect(aggregation?.weekly).not.toHaveProperty("projectedUsedPercentAfterReset");
});
+test("fallback and coverage-only metadata never become aggregate presentation", () => {
+ const fallback = capacityAggregationFromReport({
+ quota: { weeklyPercent: 80, updatedAt: 123 },
+ aggregation: {
+ kind: "capacity-weighted-v1",
+ scope: "routable-known",
+ presentation: "effective-account-fallback",
+ incomplete: true,
+ excludedAccounts: 2,
+ unknownPlanAccounts: 0,
+ },
+ });
+ expect(fallback?.presentation).toBe("effective-account-fallback");
+ const legacyCoverage = capacityAggregationFromReport({
+ aggregation: {
+ kind: "capacity-weighted-v1",
+ scope: "routable-known",
+ incomplete: true,
+ excludedAccounts: 2,
+ unknownPlanAccounts: 1,
+ },
+ });
+ expect(legacyCoverage?.presentation).toBe("coverage-only");
+});
+
+test("capacity recovery layout wraps and stacks in narrow provider panes", async () => {
+ const css = await Bun.file(new URL("../src/styles/provider-overview-dashboard.css", import.meta.url)).text();
+ expect(css).toContain(".pws-capacity-recovery {");
+ expect(css).toContain("flex-wrap: wrap;");
+ expect(css).toContain("overflow-wrap: anywhere;");
+ expect(css).toContain("@container (max-width: 520px)");
+ expect(css.slice(css.indexOf("@container (max-width: 520px)"))).toContain("grid-template-columns: minmax(0, 1fr);");
+});
+
test("malformed or future aggregation contracts fail closed", () => {
expect(capacityAggregationFromReport({ aggregation: { kind: "capacity-weighted-v2" } })).toBeNull();
expect(capacityAggregationFromReport({ aggregation: { kind: "capacity-weighted-v1", scope: "routable-known" } })).toBeNull();
diff --git a/src/providers/codex-capacity.ts b/src/providers/codex-capacity.ts
index 4e413e11fa..698d82f732 100644
--- a/src/providers/codex-capacity.ts
+++ b/src/providers/codex-capacity.ts
@@ -31,6 +31,8 @@ export interface CodexCapacityAccount {
export interface CodexCapacityWindowAggregation {
usedPercent: number;
includedAccounts: number;
+ excludedAccounts: number;
+ incomplete: boolean;
/** Internal calculation evidence; stripped from the management API response. */
totalWeight?: number;
/** Internal calculation evidence; stripped from the management API response. */
@@ -53,6 +55,7 @@ export interface CodexCapacityAggregation {
reauthAccounts: number;
staleQuotaAccounts: number;
incomplete: boolean;
+ presentation?: "aggregate" | "effective-account-fallback" | "coverage-only";
fiveHour?: CodexCapacityWindowAggregation;
weekly?: CodexCapacityWindowAggregation;
monthly?: CodexCapacityWindowAggregation;
@@ -123,12 +126,14 @@ function addWindow(
windows.set(key, window);
}
-function finalizeWindow(window: MutableWindow): CodexCapacityWindowAggregation {
+function finalizeWindow(window: MutableWindow, totalAccounts: number): CodexCapacityWindowAggregation {
const nextRecoveryAt = [...window.recoveries.keys()].sort((a, b) => a - b)[0];
const recovered = nextRecoveryAt === undefined ? undefined : window.recoveries.get(nextRecoveryAt);
return {
usedPercent: window.consumedWeight / window.totalWeight * 100,
includedAccounts: window.includedAccounts,
+ excludedAccounts: totalAccounts - window.includedAccounts,
+ incomplete: window.includedAccounts < totalAccounts,
totalWeight: window.totalWeight,
consumedWeight: window.consumedWeight,
remainingWeight: window.totalWeight - window.consumedWeight,
@@ -153,6 +158,7 @@ export function aggregateCodexPoolCapacity(
} : undefined;
const windows = new Map();
const included = new Set();
+ const contributions = new Map>();
let unknownPlanAccounts = 0;
let missingQuotaAccounts = 0;
let pausedAccounts = 0;
@@ -181,25 +187,29 @@ export function aggregateCodexPoolCapacity(
if (account.paused || account.needsReauth || weight === undefined || !quota || !hasQuota || !quotaFresh) continue;
let contributed = false;
+ const contributionKeys = new Set();
for (const [key, rawPercent, rawReset] of standard) {
const percent = normalizedPercent(rawPercent);
if (percent === undefined) continue;
addWindow(windows, key, weight, percent, futureResetMs(rawReset, now), quota.updatedAt);
+ contributionKeys.add(key);
contributed = true;
}
for (const customWindow of custom) {
const percent = normalizedPercent(customWindow.percent);
if (percent === undefined) continue;
addWindow(windows, `custom:${customWindow.label}`, weight, percent, futureResetMs(customWindow.resetAt, now), quota.updatedAt);
+ contributionKeys.add(`custom:${customWindow.label}`);
contributed = true;
}
if (contributed) {
included.add(account);
+ contributions.set(account, contributionKeys);
}
}
if (windows.size === 0) {
- if (staleQuotaAccounts === 0) return { quota: null, aggregation: null, ...(currentAccount ? { currentAccount } : {}) };
+ if (accounts.length === 0) return { quota: null, aggregation: null, ...(currentAccount ? { currentAccount } : {}) };
const aggregation: CodexCapacityAggregation = {
kind: "capacity-weighted-v1",
scope: "routable-known",
@@ -215,11 +225,11 @@ export function aggregateCodexPoolCapacity(
};
return { quota: null, aggregation, ...(currentAccount ? { currentAccount } : {}) };
}
- const fiveHour = windows.get("fiveHour") ? finalizeWindow(windows.get("fiveHour")!) : undefined;
- const weekly = windows.get("weekly") ? finalizeWindow(windows.get("weekly")!) : undefined;
- const monthly = windows.get("monthly") ? finalizeWindow(windows.get("monthly")!) : undefined;
+ const fiveHour = windows.get("fiveHour") ? finalizeWindow(windows.get("fiveHour")!, accounts.length) : undefined;
+ const weekly = windows.get("weekly") ? finalizeWindow(windows.get("weekly")!, accounts.length) : undefined;
+ const monthly = windows.get("monthly") ? finalizeWindow(windows.get("monthly")!, accounts.length) : undefined;
const customWindows = [...windows.entries()].flatMap(([key, window]) => key.startsWith("custom:")
- ? [{ label: key.slice("custom:".length), ...finalizeWindow(window) }]
+ ? [{ label: key.slice("custom:".length), ...finalizeWindow(window, accounts.length) }]
: []);
const quota: CodexCapacityQuota = {
...(fiveHour ? { fiveHourPercent: fiveHour.usedPercent } : {}),
@@ -233,7 +243,11 @@ export function aggregateCodexPoolCapacity(
.flatMap(window => window ? [window.updatedAt] : []),
),
};
- const excludedAccounts = accounts.length - included.size;
+ const visibleWindowKeys = [...windows.keys()];
+ const excludedAccounts = accounts.filter(account => {
+ const keys = contributions.get(account);
+ return !keys || visibleWindowKeys.some(key => !keys.has(key));
+ }).length;
const aggregation: CodexCapacityAggregation = {
kind: "capacity-weighted-v1",
scope: "routable-known",
diff --git a/src/providers/quota.ts b/src/providers/quota.ts
index 3fd18a8f24..2bf1d1f5c4 100644
--- a/src/providers/quota.ts
+++ b/src/providers/quota.ts
@@ -132,9 +132,13 @@ function publicCapacityWindow(window: import("./codex-capacity").CodexCapacityWi
}
/** Management API metadata intentionally omits configured/weighted unit counts. */
-function publicCapacityAggregation(aggregation: CodexCapacityAggregation): CodexCapacityAggregation {
+function publicCapacityAggregation(
+ aggregation: CodexCapacityAggregation,
+ presentation: NonNullable,
+): CodexCapacityAggregation {
return {
...aggregation,
+ presentation,
...(aggregation.fiveHour ? { fiveHour: publicCapacityWindow(aggregation.fiveHour) } : {}),
...(aggregation.weekly ? { weekly: publicCapacityWindow(aggregation.weekly) } : {}),
...(aggregation.monthly ? { monthly: publicCapacityWindow(aggregation.monthly) } : {}),
@@ -233,17 +237,39 @@ async function fetchChatGptForwardQuota(
?? accounts[0];
const capacity = aggregateCodexPoolCapacity(capacityAccounts, Date.now());
if (capacity.aggregation && capacity.quota) {
- return report(provider, "chatgpt:wham", capacity.quota as ProviderQuota, publicCapacityAggregation(capacity.aggregation));
+ return report(
+ provider,
+ "chatgpt:wham",
+ capacity.quota as ProviderQuota,
+ publicCapacityAggregation(capacity.aggregation, "aggregate"),
+ );
}
const quota = active?.quota
? { ...active.quota, updatedAt: active.quota.updatedAt ?? Date.now() } as CodexCapacityQuota
: null;
- return quota ? report(
- provider,
- "chatgpt:wham",
- quota as ProviderQuota,
- capacity.aggregation ? publicCapacityAggregation(capacity.aggregation) : undefined,
- ) : null;
+ if (quota) {
+ const fallback = report(
+ provider,
+ "chatgpt:wham",
+ quota as ProviderQuota,
+ capacity.aggregation
+ ? publicCapacityAggregation(capacity.aggregation, "effective-account-fallback")
+ : undefined,
+ );
+ return fallback ? { ...fallback, updatedAt: Date.now() } : null;
+ }
+ if (capacity.aggregation) {
+ const updatedAt = Date.now();
+ return {
+ provider,
+ label: providerLabel(provider),
+ source: "chatgpt:wham",
+ quota: { updatedAt },
+ updatedAt,
+ aggregation: publicCapacityAggregation(capacity.aggregation, "coverage-only"),
+ };
+ }
+ return null;
}
function centsValue(value: unknown): number | undefined {
@@ -1045,10 +1071,12 @@ export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh
const response = { generatedAt: Date.now(), reports: [...byProvider.values()] };
// Commit only when this probe still holds authority (no clear/force superseded it).
if (epoch === invalidationEpoch) {
- const reports = response.reports.filter(item => mayCommitProviderQuotaKey(item.provider, writerGeneration));
const commitKeyCandidate = cacheKeyWithAggregationState(config);
const commitKey = typeof commitKeyCandidate === "string" ? commitKeyCandidate : await commitKeyCandidate;
- cache = { key: commitKey, ts: Date.now(), response: { ...response, reports } };
+ if (epoch === invalidationEpoch && commitKey === key) {
+ const reports = response.reports.filter(item => mayCommitProviderQuotaKey(item.provider, writerGeneration));
+ cache = { key, ts: Date.now(), response: { ...response, reports } };
+ }
}
return response;
})();
diff --git a/tests/provider-capacity.test.ts b/tests/provider-capacity.test.ts
index 4ae364d909..96384a2fc7 100644
--- a/tests/provider-capacity.test.ts
+++ b/tests/provider-capacity.test.ts
@@ -82,6 +82,9 @@ describe("configured-weight Codex pool capacity", () => {
], NOW);
expect(result.aggregation?.weekly?.totalWeight).toBe(21);
expect(result.aggregation?.monthly?.totalWeight).toBe(6);
+ expect(result.aggregation).toMatchObject({ incomplete: true, excludedAccounts: 2 });
+ expect(result.aggregation?.weekly).toMatchObject({ includedAccounts: 2, excludedAccounts: 1, incomplete: true });
+ expect(result.aggregation?.monthly).toMatchObject({ includedAccounts: 2, excludedAccounts: 1, incomplete: true });
expect(result.quota?.weeklyPercent).toBeCloseTo(23.8095238, 7);
expect(result.quota?.monthlyPercent).toBeCloseTo(51.6666667, 7);
});
@@ -95,7 +98,12 @@ describe("configured-weight Codex pool capacity", () => {
account("team", 70, { active: true, isMain: true }),
account("go", 80),
], NOW);
- expect(fallback.aggregation).toBeNull();
+ expect(fallback.aggregation).toMatchObject({
+ includedAccounts: 0,
+ excludedAccounts: 2,
+ unknownPlanAccounts: 2,
+ incomplete: true,
+ });
expect(fallback.currentAccount?.quota?.weeklyPercent).toBe(70);
});
@@ -137,4 +145,27 @@ describe("configured-weight Codex pool capacity", () => {
});
expect(result.aggregation?.weekly).toBeUndefined();
});
+
+ test("all exclusion reasons retain a coverage envelope without a quota window", () => {
+ const stale = account("pro", 90);
+ stale.quota = { ...stale.quota!, updatedAt: NOW - CODEX_CAPACITY_MAX_QUOTA_AGE_MS - 1 };
+ const result = aggregateCodexPoolCapacity([
+ account("team", 10, { active: true, isMain: true }),
+ account("plus", undefined),
+ account("prolite", 20, { paused: true }),
+ account("business", 30, { needsReauth: true }),
+ stale,
+ ], NOW);
+ expect(result.quota).toBeNull();
+ expect(result.aggregation).toMatchObject({
+ includedAccounts: 0,
+ excludedAccounts: 5,
+ unknownPlanAccounts: 1,
+ missingQuotaAccounts: 1,
+ pausedAccounts: 1,
+ reauthAccounts: 1,
+ staleQuotaAccounts: 1,
+ incomplete: true,
+ });
+ });
});
diff --git a/tests/provider-quota.test.ts b/tests/provider-quota.test.ts
index 38c6c3bc78..5fbe6bf323 100644
--- a/tests/provider-quota.test.ts
+++ b/tests/provider-quota.test.ts
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { clearAccountQuota, updateAccountQuota } from "../src/codex/quota";
import { clearAccountNeedsReauth, markAccountNeedsReauth } from "../src/codex/account-runtime-state";
+import { clearMainAccountInfoCache } from "../src/codex/auth-api";
import { clearCodexUpstreamHealth } from "../src/codex/routing";
import { saveCodexAccountCredential } from "../src/codex/account-store";
import { saveCredential } from "../src/oauth/store";
@@ -514,6 +515,26 @@ describe("fetchProviderQuotaReports", () => {
expect(JSON.stringify(openai?.aggregation)).not.toMatch(/(?:total|consumed|remaining)Weight|projectedUsedPercent/i);
});
+ test("all-excluded pool still returns a coverage-only OpenAI report", async () => {
+ rmSync(join(codexHome, "auth.json"), { force: true });
+ clearMainAccountInfoCache();
+ const config = testConfig();
+ config.providers = { openai: config.providers.openai };
+ config.codexAccounts = [{ id: "missing", email: "missing@example.test", plan: "plus", isMain: false }];
+
+ const result = await fetchProviderQuotaReports(config);
+ const openai = result.reports.find(row => row.provider === "openai");
+ expect(openai).toBeDefined();
+ expect(openai?.quota).toEqual({ updatedAt: openai?.updatedAt });
+ expect(openai?.aggregation).toMatchObject({
+ presentation: "coverage-only",
+ includedAccounts: 0,
+ excludedAccounts: 2,
+ reauthAccounts: 2,
+ incomplete: true,
+ });
+ });
+
test("ordinary fetch reflects pausing a non-active pool account", async () => {
saveCodexAccountCredential("added", {
accessToken: "added-access", refreshToken: "added-refresh",
@@ -859,6 +880,49 @@ describe("fetchProviderQuotaReports", () => {
expect(cached.reports[0]?.quota.monthlyPercent).toBe(90);
});
+ test("effective-account change during a pool probe cannot cache under the new signature", async () => {
+ saveCodexAccountCredential("added", {
+ accessToken: "added-access", refreshToken: "added-refresh",
+ expiresAt: Date.now() + 3600_000, chatgptAccountId: "added-chatgpt-id",
+ });
+ const config = testConfig();
+ config.providers = { openai: config.providers.openai };
+ config.codexAccounts = [{ id: "added", email: "a@example.test", plan: "prolite", isMain: false }];
+ const responseFor = (init?: RequestInit) => {
+ const added = (init?.headers as Record | undefined)?.["ChatGPT-Account-Id"] === "added-chatgpt-id";
+ return new Response(JSON.stringify({
+ plan_type: added ? "prolite" : "plus",
+ rate_limit: { secondary_window: { used_percent: added ? 77 : 11, reset_at: 1_999_000_000 } },
+ }), { status: 200, headers: { "content-type": "application/json" } });
+ };
+ globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => responseFor(init)) as typeof fetch;
+ await fetchProviderQuotaReports(config, true);
+ clearProviderQuotaCache();
+
+ let release!: () => void;
+ const gate = new Promise(resolve => { release = resolve; });
+ let startedResolve!: () => void;
+ const started = new Promise(resolve => { startedResolve = resolve; });
+ let startedProbe = false;
+ globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => {
+ if (!startedProbe) {
+ startedProbe = true;
+ startedResolve();
+ }
+ await gate;
+ return responseFor(init);
+ }) as typeof fetch;
+
+ const racing = fetchProviderQuotaReports(config, true);
+ await started;
+ config.activeCodexAccountId = "added";
+ release();
+ const racedResponse = await racing;
+ const next = await fetchProviderQuotaReports(config);
+ expect(next).not.toBe(racedResponse);
+ expect(next.reports[0]?.aggregation?.currentAccount).toMatchObject({ plan: "prolite", quota: { weeklyPercent: 77 } });
+ });
+
test("last-good rows survive a transient failure with original timestamps, are replaced by fresh rows, expire past the cap, and a disabled provider yields no rows", async () => {
await saveCredential("cursor", { access: "cursor-access-secret", refresh: "cursor-refresh-secret", expires: Date.now() + 3600_000 });
let mode: "ok" | "fail" = "ok";
From f428336a30276c89712f0131fbcbe57ad835c945 Mon Sep 17 00:00:00 2001
From: luvs01 <27862058+luvs01@users.noreply.github.com>
Date: Sun, 2 Aug 2026 17:18:22 +0900
Subject: [PATCH 05/10] fix(gui): surface partial and coverage-only capacity
Keep pool account counts partitioned while reporting partial limit-window coverage separately.
Render coverage-only reports in the Providers overview and expire stale effective-account fallbacks without restamping their quota data.
Refs #874
---
.../ProviderOverviewDashboard.tsx | 12 +++-
gui/src/i18n/de.ts | 1 +
gui/src/i18n/en.ts | 1 +
gui/src/i18n/ja.ts | 1 +
gui/src/i18n/ko.ts | 1 +
gui/src/i18n/ru.ts | 1 +
gui/src/i18n/zh.ts | 1 +
gui/src/provider-workspace/report.ts | 2 +
gui/tests/provider-capacity-shell.test.tsx | 63 +++++++++++++++++--
gui/tests/provider-capacity.test.ts | 2 +
src/providers/codex-capacity.ts | 10 ++-
src/providers/quota.ts | 14 ++++-
tests/provider-capacity.test.ts | 7 ++-
tests/provider-quota.test.ts | 44 +++++++++++++
14 files changed, 145 insertions(+), 15 deletions(-)
diff --git a/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx b/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx
index ce0f676db4..68c86d2223 100644
--- a/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx
+++ b/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx
@@ -69,8 +69,9 @@ export default function ProviderOverviewDashboard({
for (const item of allItems) {
const report = quotaReports[item.name];
const quota = report ? accountQuotaFromReport(report) : null;
- if (report && quota) {
- result.push({ item, report, urgency: maxQuotaUtilisation(quota) });
+ const aggregation = report ? capacityAggregationFromReport(report) : null;
+ if (report && (quota || aggregation?.presentation === "coverage-only")) {
+ result.push({ item, report, urgency: quota ? maxQuotaUtilisation(quota) : -1 });
}
}
return result.sort((a, b) => b.urgency - a.urgency || a.item.name.localeCompare(b.item.name));
@@ -276,7 +277,7 @@ function ProviderCapacityQuota({ report, pending }: { report: ProviderQuotaRepor
)}
- {aggregation.incomplete && (
+ {aggregation.incomplete && aggregation.excludedAccounts > 0 && (
{t("pws.capacity.incomplete", {
excluded: aggregation.excludedAccounts,
@@ -284,6 +285,11 @@ function ProviderCapacityQuota({ report, pending }: { report: ProviderQuotaRepor
})}
)}
+ {aggregation.partialWindowAccounts > 0 && (
+
+ {t("pws.capacity.partial", { count: aggregation.partialWindowAccounts })}
+
+ )}
)}
>
diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts
index c882769f28..e122599b66 100644
--- a/gui/src/i18n/de.ts
+++ b/gui/src/i18n/de.ts
@@ -1385,6 +1385,7 @@ export const de: Record = {
"pws.capacity.nextRecovery": "Nächste Kapazitätswiederherstellung",
"pws.capacity.recoveryShare": "+{percent} % Pool-Kapazität",
"pws.capacity.incomplete": "Unvollständige Abdeckung: {excluded} Konten ausgeschlossen, davon {unknown} mit unbekanntem Tarif",
+ "pws.capacity.partial": "Teilweise Fensterabdeckung: {count} Konten melden nicht jedes angezeigte Limitfenster",
"pws.dashboard.recentlyUsed": "KÜRZLICH VERWENDET",
"pws.dashboard.requests": "{count} Anfragen",
"pws.dashboard.checkedAgo": "Geprüft {time}",
diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts
index eda18bc277..8d1d6c601d 100644
--- a/gui/src/i18n/en.ts
+++ b/gui/src/i18n/en.ts
@@ -1051,6 +1051,7 @@ export const en = {
"pws.capacity.nextRecovery": "Next capacity recovery",
"pws.capacity.recoveryShare": "+{percent}% pool capacity",
"pws.capacity.incomplete": "Incomplete coverage: {excluded} account(s) excluded, including {unknown} unknown plan(s)",
+ "pws.capacity.partial": "Partial window coverage: {count} account(s) do not report every displayed limit window",
"pws.dashboard.recentlyUsed": "RECENTLY USED",
"pws.dashboard.requests": "{count} requests",
"pws.dashboard.checkedAgo": "Checked {time}",
diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts
index 4e88dec9f8..e814c425cd 100644
--- a/gui/src/i18n/ja.ts
+++ b/gui/src/i18n/ja.ts
@@ -1001,6 +1001,7 @@ export const ja: Record = {
"pws.capacity.nextRecovery": "次の容量回復",
"pws.capacity.recoveryShare": "+{percent}% のプール容量",
"pws.capacity.incomplete": "対象範囲が不完全です: {excluded} 件を除外(不明なプラン {unknown} 件)",
+ "pws.capacity.partial": "一部の期間の対象範囲が不完全です: {count} 件のアカウントでは表示中のすべての制限期間を取得できません",
"pws.dashboard.recentlyUsed": "最近の使用",
"pws.dashboard.requests": "{count} リクエスト",
"pws.dashboard.checkedAgo": "{time} に確認",
diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts
index 42098d5e12..9bbf4d3734 100644
--- a/gui/src/i18n/ko.ts
+++ b/gui/src/i18n/ko.ts
@@ -1412,6 +1412,7 @@ export const ko: Record = {
"pws.capacity.nextRecovery": "다음 용량 회복",
"pws.capacity.recoveryShare": "+{percent}% 풀 용량",
"pws.capacity.incomplete": "불완전한 범위: {excluded}개 계정 제외, 알 수 없는 요금제 {unknown}개 포함",
+ "pws.capacity.partial": "일부 기간의 범위가 불완전합니다: {count}개 계정에서 표시된 모든 한도 기간을 확인할 수 없습니다",
"pws.dashboard.recentlyUsed": "최근 사용",
"pws.dashboard.requests": "{count}건 요청",
"pws.dashboard.checkedAgo": "{time} 전 확인",
diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts
index c03eef27c7..ceb23446b7 100644
--- a/gui/src/i18n/ru.ts
+++ b/gui/src/i18n/ru.ts
@@ -1043,6 +1043,7 @@ export const ru: Record = {
"pws.capacity.nextRecovery": "Следующее восстановление ёмкости",
"pws.capacity.recoveryShare": "+{percent}% ёмкости пула",
"pws.capacity.incomplete": "Неполное покрытие: исключено аккаунтов: {excluded}, неизвестных планов: {unknown}",
+ "pws.capacity.partial": "Частичное покрытие окон: для {count} аккаунтов доступны не все показанные окна лимитов",
"pws.dashboard.recentlyUsed": "Недавно использованные",
"pws.dashboard.requests": "{count} запросов",
"pws.dashboard.checkedAgo": "Проверено {time}",
diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts
index 6e63f86f5e..a3225c041e 100644
--- a/gui/src/i18n/zh.ts
+++ b/gui/src/i18n/zh.ts
@@ -1405,6 +1405,7 @@ export const zh: Record = {
"pws.capacity.nextRecovery": "下一次容量恢复",
"pws.capacity.recoveryShare": "+{percent}% 账户池容量",
"pws.capacity.incomplete": "覆盖不完整:已排除 {excluded} 个账户,其中 {unknown} 个套餐未知",
+ "pws.capacity.partial": "部分窗口覆盖不完整:{count} 个账户未报告所有显示的限额窗口",
"pws.dashboard.recentlyUsed": "最近使用",
"pws.dashboard.requests": "{count} 个请求",
"pws.dashboard.checkedAgo": "{time} 前检查",
diff --git a/gui/src/provider-workspace/report.ts b/gui/src/provider-workspace/report.ts
index 9a8a3ec380..6b4f98ccce 100644
--- a/gui/src/provider-workspace/report.ts
+++ b/gui/src/provider-workspace/report.ts
@@ -27,6 +27,7 @@ export interface ProviderCapacityAggregationView {
incomplete: boolean;
excludedAccounts: number;
unknownPlanAccounts: number;
+ partialWindowAccounts: number;
fiveHour?: CapacityWindowView;
weekly?: CapacityWindowView;
monthly?: CapacityWindowView;
@@ -124,6 +125,7 @@ export function capacityAggregationFromReport(report?: ProviderQuotaReportView):
incomplete: row.incomplete,
excludedAccounts,
unknownPlanAccounts,
+ partialWindowAccounts: finite(row.partialWindowAccounts) ?? 0,
...(fiveHour ? { fiveHour } : {}),
...(weekly ? { weekly } : {}),
...(monthly ? { monthly } : {}),
diff --git a/gui/tests/provider-capacity-shell.test.tsx b/gui/tests/provider-capacity-shell.test.tsx
index 7712d73d57..01909979b1 100644
--- a/gui/tests/provider-capacity-shell.test.tsx
+++ b/gui/tests/provider-capacity-shell.test.tsx
@@ -163,7 +163,7 @@ test("expired session quota is rejected and a failed fetch cannot keep it render
expect(readSessionListCache(QUOTA_CACHE_KEY)).toEqual({});
});
-test("all-stale effective fallback renders one raw bar without aggregate labelling", async () => {
+test("all-stale response renders coverage only without a numeric fallback", async () => {
const old = Date.now() - 31 * 60_000;
quotaPayload = {
reports: [{
@@ -171,16 +171,17 @@ test("all-stale effective fallback renders one raw bar without aggregate labelli
label: "OpenAI (Codex login)",
source: "chatgpt:wham",
updatedAt: Date.now(),
- quota: { weeklyPercent: 80, updatedAt: old },
+ quota: { updatedAt: Date.now() },
aggregation: {
kind: "capacity-weighted-v1",
scope: "routable-known",
- presentation: "effective-account-fallback",
+ presentation: "coverage-only",
includedAccounts: 0,
excludedAccounts: 2,
unknownPlanAccounts: 0,
incomplete: true,
- currentAccount: { isMain: true, plan: "pro", quota: { weeklyPercent: 80, updatedAt: old } },
+ partialWindowAccounts: 0,
+ currentAccount: { isMain: true, plan: "pro", quota: null },
},
}],
};
@@ -190,6 +191,58 @@ test("all-stale effective fallback renders one raw bar without aggregate labelli
const text = host.textContent ?? "";
expect(text).not.toContain("Configured-weight pool estimate");
expect(text).not.toContain("Current effective account");
- expect(text.match(/80% used/g)?.length).toBe(1);
+ expect(text).not.toContain("80% used");
expect(text).toContain("Incomplete coverage: 2 account(s) excluded");
});
+
+test("coverage-only API report remains visible in the rate-limit overview", async () => {
+ quotaPayload = {
+ reports: [{
+ provider: "openai",
+ label: "OpenAI (Codex login)",
+ source: "chatgpt:wham",
+ updatedAt: Date.now(),
+ quota: { updatedAt: Date.now() },
+ aggregation: {
+ kind: "capacity-weighted-v1",
+ scope: "routable-known",
+ presentation: "coverage-only",
+ includedAccounts: 0,
+ excludedAccounts: 3,
+ unknownPlanAccounts: 1,
+ partialWindowAccounts: 0,
+ incomplete: true,
+ },
+ }],
+ };
+
+ await mountShell();
+
+ const text = host.textContent ?? "";
+ expect(text).toContain("OpenAI (Codex login)");
+ expect(text).toContain("Incomplete coverage: 3 account(s) excluded, including 1 unknown plan(s)");
+ expect(text).not.toContain("No rate-limit data yet");
+ expect(text).not.toMatch(/\d+(?:\.\d+)?% used/);
+});
+
+test("mixed-window coverage uses a distinct warning without whole-account exclusion", async () => {
+ const payload = aggregatePayload();
+ payload.reports[0].quota = { weeklyPercent: 25, monthlyPercent: 40, updatedAt: Date.now() };
+ payload.reports[0].aggregation.excludedAccounts = 0;
+ payload.reports[0].aggregation.unknownPlanAccounts = 0;
+ payload.reports[0].aggregation.partialWindowAccounts = 2;
+ payload.reports[0].aggregation.monthly = {
+ usedPercent: 40,
+ includedAccounts: 2,
+ excludedAccounts: 1,
+ incomplete: true,
+ updatedAt: Date.now(),
+ };
+ quotaPayload = payload;
+
+ await mountShell();
+
+ const text = host.textContent ?? "";
+ expect(text).toContain("Partial window coverage: 2 account(s) do not report every displayed limit window");
+ expect(text).not.toContain("Incomplete coverage: 0 account(s) excluded");
+});
diff --git a/gui/tests/provider-capacity.test.ts b/gui/tests/provider-capacity.test.ts
index 23bfb11f0d..695e6e8f78 100644
--- a/gui/tests/provider-capacity.test.ts
+++ b/gui/tests/provider-capacity.test.ts
@@ -15,6 +15,7 @@ test("capacity metadata preserves estimate, raw current quota, recovery percent,
incomplete: true,
excludedAccounts: 2,
unknownPlanAccounts: 1,
+ partialWindowAccounts: 0,
weekly: {
usedPercent: 30.769230769,
nextRecoveryAt: 1_800_000_010_000,
@@ -31,6 +32,7 @@ test("capacity metadata preserves estimate, raw current quota, recovery percent,
incomplete: true,
excludedAccounts: 2,
unknownPlanAccounts: 1,
+ partialWindowAccounts: 0,
weekly: { usedPercent: 30.769230769, nextRecoveryPercent: 19.23076923 },
currentAccount: { plan: "pro", quota: { weeklyPercent: 10 } },
});
diff --git a/src/providers/codex-capacity.ts b/src/providers/codex-capacity.ts
index 698d82f732..f55501c47b 100644
--- a/src/providers/codex-capacity.ts
+++ b/src/providers/codex-capacity.ts
@@ -54,6 +54,7 @@ export interface CodexCapacityAggregation {
pausedAccounts: number;
reauthAccounts: number;
staleQuotaAccounts: number;
+ partialWindowAccounts: number;
incomplete: boolean;
presentation?: "aggregate" | "effective-account-fallback" | "coverage-only";
fiveHour?: CodexCapacityWindowAggregation;
@@ -220,6 +221,7 @@ export function aggregateCodexPoolCapacity(
pausedAccounts,
reauthAccounts,
staleQuotaAccounts,
+ partialWindowAccounts: 0,
incomplete: true,
...(currentAccount ? { currentAccount } : {}),
};
@@ -244,10 +246,11 @@ export function aggregateCodexPoolCapacity(
),
};
const visibleWindowKeys = [...windows.keys()];
- const excludedAccounts = accounts.filter(account => {
+ const partialWindowAccounts = accounts.filter(account => {
const keys = contributions.get(account);
- return !keys || visibleWindowKeys.some(key => !keys.has(key));
+ return !!keys && visibleWindowKeys.some(key => !keys.has(key));
}).length;
+ const excludedAccounts = accounts.length - included.size;
const aggregation: CodexCapacityAggregation = {
kind: "capacity-weighted-v1",
scope: "routable-known",
@@ -258,7 +261,8 @@ export function aggregateCodexPoolCapacity(
pausedAccounts,
reauthAccounts,
staleQuotaAccounts,
- incomplete: excludedAccounts > 0,
+ partialWindowAccounts,
+ incomplete: excludedAccounts > 0 || partialWindowAccounts > 0,
...(fiveHour ? { fiveHour } : {}),
...(weekly ? { weekly } : {}),
...(monthly ? { monthly } : {}),
diff --git a/src/providers/quota.ts b/src/providers/quota.ts
index 2bf1d1f5c4..1c40d06950 100644
--- a/src/providers/quota.ts
+++ b/src/providers/quota.ts
@@ -136,9 +136,13 @@ function publicCapacityAggregation(
aggregation: CodexCapacityAggregation,
presentation: NonNullable,
): CodexCapacityAggregation {
+ const safeCurrentAccount = presentation === "coverage-only" && aggregation.currentAccount
+ ? { ...aggregation.currentAccount, quota: null }
+ : aggregation.currentAccount;
return {
...aggregation,
presentation,
+ ...(safeCurrentAccount ? { currentAccount: safeCurrentAccount } : {}),
...(aggregation.fiveHour ? { fiveHour: publicCapacityWindow(aggregation.fiveHour) } : {}),
...(aggregation.weekly ? { weekly: publicCapacityWindow(aggregation.weekly) } : {}),
...(aggregation.monthly ? { monthly: publicCapacityWindow(aggregation.monthly) } : {}),
@@ -235,7 +239,8 @@ async function fetchChatGptForwardQuota(
const active = capacityAccounts.find(account => account.active)
?? accounts.find(account => account.id === MAIN_CODEX_ACCOUNT_ID)
?? accounts[0];
- const capacity = aggregateCodexPoolCapacity(capacityAccounts, Date.now());
+ const now = Date.now();
+ const capacity = aggregateCodexPoolCapacity(capacityAccounts, now);
if (capacity.aggregation && capacity.quota) {
return report(
provider,
@@ -247,7 +252,10 @@ async function fetchChatGptForwardQuota(
const quota = active?.quota
? { ...active.quota, updatedAt: active.quota.updatedAt ?? Date.now() } as CodexCapacityQuota
: null;
- if (quota) {
+ const quotaFresh = !!quota
+ && Number.isFinite(quota.updatedAt)
+ && now - quota.updatedAt < CODEX_CAPACITY_MAX_QUOTA_AGE_MS;
+ if (quota && quotaFresh) {
const fallback = report(
provider,
"chatgpt:wham",
@@ -256,7 +264,7 @@ async function fetchChatGptForwardQuota(
? publicCapacityAggregation(capacity.aggregation, "effective-account-fallback")
: undefined,
);
- return fallback ? { ...fallback, updatedAt: Date.now() } : null;
+ return fallback;
}
if (capacity.aggregation) {
const updatedAt = Date.now();
diff --git a/tests/provider-capacity.test.ts b/tests/provider-capacity.test.ts
index 96384a2fc7..ca61b8d8d3 100644
--- a/tests/provider-capacity.test.ts
+++ b/tests/provider-capacity.test.ts
@@ -82,7 +82,12 @@ describe("configured-weight Codex pool capacity", () => {
], NOW);
expect(result.aggregation?.weekly?.totalWeight).toBe(21);
expect(result.aggregation?.monthly?.totalWeight).toBe(6);
- expect(result.aggregation).toMatchObject({ incomplete: true, excludedAccounts: 2 });
+ expect(result.aggregation).toMatchObject({
+ includedAccounts: 3,
+ excludedAccounts: 0,
+ partialWindowAccounts: 2,
+ incomplete: true,
+ });
expect(result.aggregation?.weekly).toMatchObject({ includedAccounts: 2, excludedAccounts: 1, incomplete: true });
expect(result.aggregation?.monthly).toMatchObject({ includedAccounts: 2, excludedAccounts: 1, incomplete: true });
expect(result.quota?.weeklyPercent).toBeCloseTo(23.8095238, 7);
diff --git a/tests/provider-quota.test.ts b/tests/provider-quota.test.ts
index 5fbe6bf323..5273f8c3e6 100644
--- a/tests/provider-quota.test.ts
+++ b/tests/provider-quota.test.ts
@@ -535,6 +535,50 @@ describe("fetchProviderQuotaReports", () => {
});
});
+ test("stale effective-account quota becomes coverage-only and is never restamped as numeric fallback", async () => {
+ saveCodexAccountCredential("added", {
+ accessToken: "added-access", refreshToken: "added-refresh",
+ expiresAt: Date.now() + 3600_000, chatgptAccountId: "added-chatgpt-id",
+ });
+ const config = testConfig();
+ config.providers = { openai: config.providers.openai };
+ config.codexAccounts = [{ id: "added", email: "a@example.test", plan: "prolite", isMain: false }];
+ config.activeCodexAccountId = "added";
+ globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => {
+ const added = (init?.headers as Record | undefined)?.["ChatGPT-Account-Id"] === "added-chatgpt-id";
+ return new Response(JSON.stringify({
+ plan_type: added ? "prolite" : "plus",
+ rate_limit: { secondary_window: { used_percent: added ? 77 : 11, reset_at: 1_999_000_000 } },
+ }), { status: 200, headers: { "content-type": "application/json" } });
+ }) as typeof fetch;
+ await fetchProviderQuotaReports(config, true);
+ clearProviderQuotaCache();
+
+ const realDateNow = Date.now;
+ const future = realDateNow() + 31 * 60_000;
+ try {
+ Date.now = () => future;
+ globalThis.fetch = (async () => new Response("unavailable", { status: 500 })) as typeof fetch;
+ const expired = await fetchProviderQuotaReports(config);
+ const openai = expired.reports.find(row => row.provider === "openai");
+ expect(openai?.aggregation).toMatchObject({
+ presentation: "coverage-only",
+ includedAccounts: 0,
+ staleQuotaAccounts: 1,
+ missingQuotaAccounts: 1,
+ unknownPlanAccounts: 1,
+ incomplete: true,
+ currentAccount: { plan: "prolite", quota: null },
+ });
+ expect(openai?.quota).toEqual({ updatedAt: future });
+ expect(openai?.quota).not.toHaveProperty("weeklyPercent");
+ const cached = await fetchProviderQuotaReports(config);
+ expect(cached.reports[0]?.quota).not.toHaveProperty("weeklyPercent");
+ } finally {
+ Date.now = realDateNow;
+ }
+ });
+
test("ordinary fetch reflects pausing a non-active pool account", async () => {
saveCodexAccountCredential("added", {
accessToken: "added-access", refreshToken: "added-refresh",
From 4c376d3b981b517e2678a07dbea4f8fc05099761 Mon Sep 17 00:00:00 2001
From: luvs01 <27862058+luvs01@users.noreply.github.com>
Date: Sun, 2 Aug 2026 17:26:08 +0900
Subject: [PATCH 06/10] docs: explain partial quota window coverage
---
docs-site/src/content/docs/guides/providers.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md
index 907e7efc9e..a9c8e8758d 100644
--- a/docs-site/src/content/docs/guides/providers.md
+++ b/docs-site/src/content/docs/guides/providers.md
@@ -36,6 +36,10 @@ reset is expected to recover as `+N% pool capacity`. **Incomplete coverage** mea
accounts could not safely contribute to the estimate, for example because their plan or quota is
unknown, their reading is stale, or the account is paused or needs reauthentication.
+A **partial window coverage** warning means some included accounts reported one quota window but
+not another. The overview keeps those windows separate and marks each affected window incomplete
+instead of treating the missing reading as usage for that window.
+
This estimate is display-only. It does not change account selection, session affinity, automatic
switching, cooldowns, or any other routing decision. Use the [Codex Auth account pool](/guides/web-dashboard/#codex-auth-and-account-pools)
for the individual account state and routing controls.
From e43fce7fddc48cce7044d08b12e00765a2caea8e Mon Sep 17 00:00:00 2001
From: luvs01 <27862058+luvs01@users.noreply.github.com>
Date: Sun, 2 Aug 2026 17:48:37 +0900
Subject: [PATCH 07/10] fix(gui): identify partial quota windows
---
gui/src/components/QuotaBars.tsx | 63 ++++++++++--
.../ProviderOverviewDashboard.tsx | 24 ++++-
gui/src/i18n/de.ts | 2 +
gui/src/i18n/en.ts | 2 +
gui/src/i18n/ja.ts | 2 +
gui/src/i18n/ko.ts | 2 +
gui/src/i18n/ru.ts | 2 +
gui/src/i18n/zh.ts | 2 +
gui/src/styles/provider-quota.css | 24 +++++
gui/tests/provider-capacity-shell.test.tsx | 99 +++++++++++++++++++
gui/tests/provider-capacity.test.ts | 34 +++++++
11 files changed, 248 insertions(+), 8 deletions(-)
diff --git a/gui/src/components/QuotaBars.tsx b/gui/src/components/QuotaBars.tsx
index 8669deafb1..e90f40a1cb 100644
--- a/gui/src/components/QuotaBars.tsx
+++ b/gui/src/components/QuotaBars.tsx
@@ -7,7 +7,15 @@ import { type AccountQuota, normalizeQuotaForPlan } from "../codex-quota-utils";
/* Helpers are co-located with QuotaBars for overview sorting / stacked layout. */
/* eslint-disable react-refresh/only-export-components */
-export type QuotaBarRow = { label: string; limitLabel: string; percent: number; resetAt?: number };
+export type QuotaWindowKey = "fiveHour" | "weekly" | "monthly";
+export type QuotaBarRow = {
+ windowKey?: QuotaWindowKey;
+ customLabel?: string;
+ label: string;
+ limitLabel: string;
+ percent: number;
+ resetAt?: number;
+};
/**
* Window ordering is computed from RAW wire identities BEFORE localization
@@ -43,6 +51,7 @@ export function buildQuotaRows(quota: AccountQuota | null, plan: string | null |
ranked.push({
rank: 0,
row: {
+ windowKey: "fiveHour",
label: t("codexAuth.fiveHour"),
limitLabel: t("quota.fiveHourLimit"),
percent: displayQuota.fiveHourPercent,
@@ -54,6 +63,7 @@ export function buildQuotaRows(quota: AccountQuota | null, plan: string | null |
ranked.push({
rank: 1,
row: {
+ windowKey: "weekly",
label: t("codexAuth.weekly"),
limitLabel: t("quota.weeklyLimit"),
percent: displayQuota.weeklyPercent,
@@ -65,6 +75,7 @@ export function buildQuotaRows(quota: AccountQuota | null, plan: string | null |
ranked.push({
rank: 4,
row: {
+ windowKey: "monthly",
label: t("codexAuth.monthly"),
limitLabel: t("quota.monthlyLimit"),
percent: displayQuota.monthlyPercent,
@@ -76,7 +87,13 @@ export function buildQuotaRows(quota: AccountQuota | null, plan: string | null |
const localized = localizeCustomQuotaLabel(w.label, t);
ranked.push({
rank: rawCustomWindowRank(w.label),
- row: { label: localized, limitLabel: localized, percent: w.percent, resetAt: w.resetAt },
+ row: {
+ customLabel: w.label,
+ label: localized,
+ limitLabel: localized,
+ percent: w.percent,
+ resetAt: w.resetAt,
+ },
});
}
return ranked.sort((a, b) => a.rank - b.rank).map(entry => entry.row);
@@ -138,7 +155,17 @@ function barFillStyle(percent: number): CSSProperties {
return { ["--bar-scale" as string]: String(barWidth(percent) / 100) };
}
-export default function QuotaBars({ quota, plan, threshold, t, className, layout = "compact", pending = false }: {
+export default function QuotaBars({
+ quota,
+ plan,
+ threshold,
+ t,
+ className,
+ layout = "compact",
+ pending = false,
+ incompleteWindowKeys,
+ incompleteCustomWindowLabels,
+}: {
quota: AccountQuota | null;
plan?: string | null;
threshold: number;
@@ -151,6 +178,9 @@ export default function QuotaBars({ quota, plan, threshold, t, className, layout
* bar slot so deferred fill does not shove the page down.
*/
pending?: boolean;
+ /** Optional overview-only coverage status. Other quota surfaces remain unchanged when omitted. */
+ incompleteWindowKeys?: ReadonlySet;
+ incompleteCustomWindowLabels?: ReadonlySet;
}) {
const { locale } = useI18n();
const rows = buildQuotaRows(quota, plan, t);
@@ -201,7 +231,16 @@ export default function QuotaBars({ quota, plan, threshold, t, className, layout
return (
{rows.map(row => (
-
+
))}
);
@@ -254,11 +293,12 @@ function QuotaRow({ label, percent, resetAt, threshold, t, locale }: {
);
}
-function StackedQuotaRow({ row, threshold, t, locale }: {
+function StackedQuotaRow({ row, threshold, t, locale, incomplete }: {
row: QuotaBarRow;
threshold: number;
t: TFn;
locale: Locale;
+ incomplete: boolean;
}) {
const exhausted = isQuotaExhausted(row.percent);
const warn = isQuotaWarn(row.percent, threshold);
@@ -267,7 +307,18 @@ function StackedQuotaRow({ row, threshold, t, locale }: {
return (
- {row.limitLabel}
+
+ {row.limitLabel}
+ {incomplete && (
+
+ {t("pws.capacity.windowPartial")}
+
+ )}
+
{resetText}
diff --git a/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx b/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx
index 68c86d2223..375cbc3fbe 100644
--- a/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx
+++ b/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx
@@ -22,7 +22,7 @@ import {
relativeTimeLabelsFromT,
type ProviderUsageTotals,
} from "../../provider-workspace/usage";
-import { maxQuotaUtilisation } from "../QuotaBars";
+import { maxQuotaUtilisation, type QuotaWindowKey } from "../QuotaBars";
import { ProviderIcon } from "./ProviderRail";
import { formatProviderDisplayName } from "../../provider-icons";
import QuotaBars from "../QuotaBars";
@@ -242,6 +242,16 @@ function ProviderCapacityQuota({ report, pending }: { report: ProviderQuotaRepor
const aggregation = capacityAggregationFromReport(report);
const primaryQuota = accountQuotaFromReport(report);
const showsAggregate = aggregation?.presentation === "aggregate";
+ const incompleteWindowKeys = new Set
();
+ const incompleteCustomWindowLabels = new Set();
+ if (showsAggregate && aggregation) {
+ if (aggregation.fiveHour?.incomplete) incompleteWindowKeys.add("fiveHour");
+ if (aggregation.weekly?.incomplete) incompleteWindowKeys.add("weekly");
+ if (aggregation.monthly?.incomplete) incompleteWindowKeys.add("monthly");
+ for (const window of aggregation.customWindows ?? []) {
+ if (window.incomplete) incompleteCustomWindowLabels.add(window.label);
+ }
+ }
const recoveryRows: Array<{ label: string; window: CapacityWindowView }> = showsAggregate && aggregation ? [
...(aggregation.fiveHour ? [{ label: t("codexAuth.fiveHour"), window: aggregation.fiveHour }] : []),
...(aggregation.weekly ? [{ label: t("codexAuth.weekly"), window: aggregation.weekly }] : []),
@@ -257,7 +267,17 @@ function ProviderCapacityQuota({ report, pending }: { report: ProviderQuotaRepor
return (
<>
{showsAggregate && {t("pws.capacity.estimate")}
}
- {(primaryQuota || pending) && }
+ {(primaryQuota || pending) && (
+
+ )}
{aggregation && (
{recoveryRows.flatMap(({ label, window }) => (
diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts
index e122599b66..d171ae0f30 100644
--- a/gui/src/i18n/de.ts
+++ b/gui/src/i18n/de.ts
@@ -1386,6 +1386,8 @@ export const de: Record
= {
"pws.capacity.recoveryShare": "+{percent} % Pool-Kapazität",
"pws.capacity.incomplete": "Unvollständige Abdeckung: {excluded} Konten ausgeschlossen, davon {unknown} mit unbekanntem Tarif",
"pws.capacity.partial": "Teilweise Fensterabdeckung: {count} Konten melden nicht jedes angezeigte Limitfenster",
+ "pws.capacity.windowPartial": "Teilweise",
+ "pws.capacity.windowPartialA11y": "{window}: unvollständige Kontoabdeckung",
"pws.dashboard.recentlyUsed": "KÜRZLICH VERWENDET",
"pws.dashboard.requests": "{count} Anfragen",
"pws.dashboard.checkedAgo": "Geprüft {time}",
diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts
index 8d1d6c601d..960efac07f 100644
--- a/gui/src/i18n/en.ts
+++ b/gui/src/i18n/en.ts
@@ -1052,6 +1052,8 @@ export const en = {
"pws.capacity.recoveryShare": "+{percent}% pool capacity",
"pws.capacity.incomplete": "Incomplete coverage: {excluded} account(s) excluded, including {unknown} unknown plan(s)",
"pws.capacity.partial": "Partial window coverage: {count} account(s) do not report every displayed limit window",
+ "pws.capacity.windowPartial": "Partial",
+ "pws.capacity.windowPartialA11y": "{window}: incomplete account coverage",
"pws.dashboard.recentlyUsed": "RECENTLY USED",
"pws.dashboard.requests": "{count} requests",
"pws.dashboard.checkedAgo": "Checked {time}",
diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts
index e814c425cd..8f1d50e236 100644
--- a/gui/src/i18n/ja.ts
+++ b/gui/src/i18n/ja.ts
@@ -1002,6 +1002,8 @@ export const ja: Record = {
"pws.capacity.recoveryShare": "+{percent}% のプール容量",
"pws.capacity.incomplete": "対象範囲が不完全です: {excluded} 件を除外(不明なプラン {unknown} 件)",
"pws.capacity.partial": "一部の期間の対象範囲が不完全です: {count} 件のアカウントでは表示中のすべての制限期間を取得できません",
+ "pws.capacity.windowPartial": "一部のみ",
+ "pws.capacity.windowPartialA11y": "{window}: アカウントの対象範囲が不完全です",
"pws.dashboard.recentlyUsed": "最近の使用",
"pws.dashboard.requests": "{count} リクエスト",
"pws.dashboard.checkedAgo": "{time} に確認",
diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts
index 9bbf4d3734..9df982c527 100644
--- a/gui/src/i18n/ko.ts
+++ b/gui/src/i18n/ko.ts
@@ -1413,6 +1413,8 @@ export const ko: Record = {
"pws.capacity.recoveryShare": "+{percent}% 풀 용량",
"pws.capacity.incomplete": "불완전한 범위: {excluded}개 계정 제외, 알 수 없는 요금제 {unknown}개 포함",
"pws.capacity.partial": "일부 기간의 범위가 불완전합니다: {count}개 계정에서 표시된 모든 한도 기간을 확인할 수 없습니다",
+ "pws.capacity.windowPartial": "일부만",
+ "pws.capacity.windowPartialA11y": "{window}: 계정 범위가 불완전합니다",
"pws.dashboard.recentlyUsed": "최근 사용",
"pws.dashboard.requests": "{count}건 요청",
"pws.dashboard.checkedAgo": "{time} 전 확인",
diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts
index ceb23446b7..ace73a9742 100644
--- a/gui/src/i18n/ru.ts
+++ b/gui/src/i18n/ru.ts
@@ -1044,6 +1044,8 @@ export const ru: Record = {
"pws.capacity.recoveryShare": "+{percent}% ёмкости пула",
"pws.capacity.incomplete": "Неполное покрытие: исключено аккаунтов: {excluded}, неизвестных планов: {unknown}",
"pws.capacity.partial": "Частичное покрытие окон: для {count} аккаунтов доступны не все показанные окна лимитов",
+ "pws.capacity.windowPartial": "Частично",
+ "pws.capacity.windowPartialA11y": "{window}: неполное покрытие аккаунтов",
"pws.dashboard.recentlyUsed": "Недавно использованные",
"pws.dashboard.requests": "{count} запросов",
"pws.dashboard.checkedAgo": "Проверено {time}",
diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts
index a3225c041e..37d06b6620 100644
--- a/gui/src/i18n/zh.ts
+++ b/gui/src/i18n/zh.ts
@@ -1406,6 +1406,8 @@ export const zh: Record = {
"pws.capacity.recoveryShare": "+{percent}% 账户池容量",
"pws.capacity.incomplete": "覆盖不完整:已排除 {excluded} 个账户,其中 {unknown} 个套餐未知",
"pws.capacity.partial": "部分窗口覆盖不完整:{count} 个账户未报告所有显示的限额窗口",
+ "pws.capacity.windowPartial": "部分",
+ "pws.capacity.windowPartialA11y": "{window}:账户覆盖不完整",
"pws.dashboard.recentlyUsed": "最近使用",
"pws.dashboard.requests": "{count} 个请求",
"pws.dashboard.checkedAgo": "{time} 前检查",
diff --git a/gui/src/styles/provider-quota.css b/gui/src/styles/provider-quota.css
index b11ed3d45c..bc990d6820 100644
--- a/gui/src/styles/provider-quota.css
+++ b/gui/src/styles/provider-quota.css
@@ -44,7 +44,31 @@
font-weight: 600;
}
+.quota-stacked-limit-group {
+ display: inline-flex;
+ flex: 1 1 auto;
+ flex-wrap: wrap;
+ align-items: baseline;
+ gap: 4px;
+ min-width: 0;
+ overflow-wrap: anywhere;
+}
+
+.quota-window-partial {
+ flex: 0 0 auto;
+ padding: 1px 5px;
+ border: 1px solid color-mix(in srgb, var(--amber) 45%, transparent);
+ border-radius: 999px;
+ color: var(--amber);
+ font-size: 10px;
+ font-weight: 600;
+ line-height: 1.3;
+ white-space: nowrap;
+}
+
.quota-stacked-reset {
+ min-width: 0;
+ overflow-wrap: anywhere;
font-size: 12px;
}
diff --git a/gui/tests/provider-capacity-shell.test.tsx b/gui/tests/provider-capacity-shell.test.tsx
index 01909979b1..49e1bc50f5 100644
--- a/gui/tests/provider-capacity-shell.test.tsx
+++ b/gui/tests/provider-capacity-shell.test.tsx
@@ -56,6 +56,44 @@ function aggregatePayload() {
};
}
+function aggregateWindowPayload(weeklyIncomplete: boolean, monthlyIncomplete: boolean) {
+ const now = Date.now();
+ const incomplete = weeklyIncomplete || monthlyIncomplete;
+ return {
+ reports: [{
+ provider: "openai",
+ label: "OpenAI (Codex login)",
+ source: "chatgpt:wham",
+ updatedAt: now,
+ quota: { weeklyPercent: 20, monthlyPercent: 40, updatedAt: now },
+ aggregation: {
+ kind: "capacity-weighted-v1",
+ scope: "routable-known",
+ presentation: "aggregate",
+ includedAccounts: 2,
+ excludedAccounts: 0,
+ unknownPlanAccounts: 0,
+ partialWindowAccounts: incomplete ? 1 : 0,
+ incomplete,
+ weekly: {
+ usedPercent: 20,
+ includedAccounts: weeklyIncomplete ? 1 : 2,
+ excludedAccounts: weeklyIncomplete ? 1 : 0,
+ incomplete: weeklyIncomplete,
+ updatedAt: now,
+ },
+ monthly: {
+ usedPercent: 40,
+ includedAccounts: monthlyIncomplete ? 1 : 2,
+ excludedAccounts: monthlyIncomplete ? 1 : 0,
+ incomplete: monthlyIncomplete,
+ updatedAt: now,
+ },
+ },
+ }],
+ };
+}
+
beforeEach(() => {
previous = Object.fromEntries(globals.map(key => [key, Reflect.get(globalThis, key)])) as typeof previous;
originalFetch = globalThis.fetch;
@@ -246,3 +284,64 @@ test("mixed-window coverage uses a distinct warning without whole-account exclus
expect(text).toContain("Partial window coverage: 2 account(s) do not report every displayed limit window");
expect(text).not.toContain("Incomplete coverage: 0 account(s) excluded");
});
+
+test("only the monthly aggregate window receives a localized partial marker", async () => {
+ quotaPayload = aggregateWindowPayload(false, true);
+ await mountShell();
+
+ const markers = [...host.querySelectorAll(".quota-window-partial")];
+ expect(markers).toHaveLength(1);
+ expect(markers[0]?.textContent).toBe("Partial");
+ expect(markers[0]?.getAttribute("aria-label")).toBe("30-day limit: incomplete account coverage");
+});
+
+test("only the weekly aggregate window receives a localized partial marker", async () => {
+ quotaPayload = aggregateWindowPayload(true, false);
+ await mountShell();
+
+ const markers = [...host.querySelectorAll(".quota-window-partial")];
+ expect(markers).toHaveLength(1);
+ expect(markers[0]?.getAttribute("aria-label")).toBe("Weekly limit: incomplete account coverage");
+});
+
+test("complete aggregate windows do not receive partial markers", async () => {
+ quotaPayload = aggregateWindowPayload(false, false);
+ await mountShell();
+
+ expect(host.querySelectorAll(".quota-window-partial")).toHaveLength(0);
+ expect(host.textContent ?? "").not.toContain("Partial window coverage");
+});
+
+test("five-hour and custom aggregate windows can be marked independently", async () => {
+ const now = Date.now();
+ quotaPayload = {
+ reports: [{
+ provider: "openai",
+ label: "OpenAI (Codex login)",
+ source: "chatgpt:wham",
+ updatedAt: now,
+ quota: {
+ fiveHourPercent: 10,
+ customWindows: [{ label: "Burst", percent: 30 }],
+ updatedAt: now,
+ },
+ aggregation: {
+ kind: "capacity-weighted-v1",
+ scope: "routable-known",
+ presentation: "aggregate",
+ includedAccounts: 2,
+ excludedAccounts: 0,
+ unknownPlanAccounts: 0,
+ partialWindowAccounts: 1,
+ incomplete: true,
+ fiveHour: { usedPercent: 10, includedAccounts: 2, excludedAccounts: 0, incomplete: false, updatedAt: now },
+ customWindows: [{ label: "Burst", usedPercent: 30, includedAccounts: 1, excludedAccounts: 1, incomplete: true, updatedAt: now }],
+ },
+ }],
+ };
+ await mountShell();
+
+ const markers = [...host.querySelectorAll(".quota-window-partial")];
+ expect(markers).toHaveLength(1);
+ expect(markers[0]?.getAttribute("aria-label")).toBe("Burst: incomplete account coverage");
+});
diff --git a/gui/tests/provider-capacity.test.ts b/gui/tests/provider-capacity.test.ts
index 695e6e8f78..4d1eaec0f5 100644
--- a/gui/tests/provider-capacity.test.ts
+++ b/gui/tests/provider-capacity.test.ts
@@ -39,6 +39,36 @@ test("capacity metadata preserves estimate, raw current quota, recovery percent,
expect(aggregation?.weekly).not.toHaveProperty("projectedUsedPercentAfterReset");
});
+test("window coverage metadata keeps monthly, weekly, and complete states independent", () => {
+ const adapt = (weeklyIncomplete: boolean, monthlyIncomplete: boolean) => capacityAggregationFromReport({
+ quota: { weeklyPercent: 20, monthlyPercent: 40, updatedAt: 123 },
+ aggregation: {
+ kind: "capacity-weighted-v1",
+ scope: "routable-known",
+ presentation: "aggregate",
+ incomplete: weeklyIncomplete || monthlyIncomplete,
+ excludedAccounts: 0,
+ unknownPlanAccounts: 0,
+ partialWindowAccounts: weeklyIncomplete || monthlyIncomplete ? 1 : 0,
+ weekly: { usedPercent: 20, incomplete: weeklyIncomplete, excludedAccounts: weeklyIncomplete ? 1 : 0 },
+ monthly: { usedPercent: 40, incomplete: monthlyIncomplete, excludedAccounts: monthlyIncomplete ? 1 : 0 },
+ },
+ });
+
+ expect(adapt(false, true)).toMatchObject({
+ weekly: { incomplete: false, excludedAccounts: 0 },
+ monthly: { incomplete: true, excludedAccounts: 1 },
+ });
+ expect(adapt(true, false)).toMatchObject({
+ weekly: { incomplete: true, excludedAccounts: 1 },
+ monthly: { incomplete: false, excludedAccounts: 0 },
+ });
+ expect(adapt(false, false)).toMatchObject({
+ weekly: { incomplete: false, excludedAccounts: 0 },
+ monthly: { incomplete: false, excludedAccounts: 0 },
+ });
+});
+
test("fallback and coverage-only metadata never become aggregate presentation", () => {
const fallback = capacityAggregationFromReport({
quota: { weeklyPercent: 80, updatedAt: 123 },
@@ -66,11 +96,15 @@ test("fallback and coverage-only metadata never become aggregate presentation",
test("capacity recovery layout wraps and stacks in narrow provider panes", async () => {
const css = await Bun.file(new URL("../src/styles/provider-overview-dashboard.css", import.meta.url)).text();
+ const quotaCss = await Bun.file(new URL("../src/styles/provider-quota.css", import.meta.url)).text();
expect(css).toContain(".pws-capacity-recovery {");
expect(css).toContain("flex-wrap: wrap;");
expect(css).toContain("overflow-wrap: anywhere;");
expect(css).toContain("@container (max-width: 520px)");
expect(css.slice(css.indexOf("@container (max-width: 520px)"))).toContain("grid-template-columns: minmax(0, 1fr);");
+ expect(quotaCss).toContain(".quota-stacked-limit-group {");
+ expect(quotaCss).toContain("flex-wrap: wrap;");
+ expect(quotaCss).toContain("overflow-wrap: anywhere;");
});
test("malformed or future aggregation contracts fail closed", () => {
From b4f963d868628f2191fc5a8c514c99d5d52ff69b Mon Sep 17 00:00:00 2001
From: luvs01 <27862058+luvs01@users.noreply.github.com>
Date: Sun, 2 Aug 2026 18:40:09 +0900
Subject: [PATCH 08/10] fix: address provider capacity review feedback
---
.../src/content/docs/ja/guides/providers.md | 19 ++++
.../src/content/docs/ko/guides/providers.md | 18 ++++
.../src/content/docs/ru/guides/providers.md | 21 ++++
.../content/docs/zh-cn/guides/providers.md | 16 +++
.../ProviderOverviewDashboard.tsx | 14 +--
.../ProviderWorkspaceShell.tsx | 1 +
gui/src/i18n/de.ts | 2 +-
gui/src/i18n/ru.ts | 2 +-
gui/src/provider-workspace/report.ts | 6 +-
gui/tests/provider-capacity-shell.test.tsx | 102 +++++++++++++++++-
gui/tests/provider-capacity.test.ts | 21 ++--
src/providers/codex-capacity.ts | 25 ++++-
src/providers/quota.ts | 43 +++++---
tests/provider-capacity.test.ts | 44 ++++++++
tests/provider-quota.test.ts | 23 ++++
15 files changed, 317 insertions(+), 40 deletions(-)
diff --git a/docs-site/src/content/docs/ja/guides/providers.md b/docs-site/src/content/docs/ja/guides/providers.md
index 2005aa3296..98a5a0c577 100644
--- a/docs-site/src/content/docs/ja/guides/providers.md
+++ b/docs-site/src/content/docs/ja/guides/providers.md
@@ -20,6 +20,25 @@ max input 922,000 で `*-pro` virtual ID は公開状態を維持し、wire で
組み込み `openai` が欠落または無効な場合、ダッシュボードの Accounts ピッカーと Codex Auth から復元できます。欠落行は正規プリセットから作成され、正規の無効行は保存済みのモードやモデル設定を置き換えずに再有効化され、非正規の `openai` 行にはその復元経路は出ません。
+### プロバイダー概要のプール容量
+
+Codex login を Pool モードで使うと、Providers の概要には任意の 1 アカウントではなく、
+プール全体の使用済み容量の推定値が表示されます。同じ行には現在の有効アカウントの
+生のクォータ使用率も表示されるため、プールの推定値と次のリクエストで使われる
+アカウントの状態を区別できます。
+
+リセット情報がある場合は、次のリセット時刻と、その時点で回復するプール容量が表示されます。
+**対象範囲が不完全**という警告は、プランやクォータが不明、読み取りが古い、アカウントが
+一時停止中、または再認証が必要などの理由で、安全に推定へ含められないアカウントがあることを示します。
+
+**期間別の対象範囲が一部不完全**という警告は、含まれるアカウントの一部が、表示中の
+クォータ期間のうち一部だけを報告したことを示します。概要では各期間を分けたまま、影響を受ける
+期間を個別に不完全と表示し、欠けた値をその期間の使用量として扱いません。
+
+この推定値は表示専用です。アカウント選択、セッション affinity、自動切り替え、cooldown、
+その他のルーティング判断には影響しません。個別アカウントの状態とルーティング設定は
+[Codex Auth のアカウントプール](/ja/guides/web-dashboard/#codex-auth-and-account-pools)を参照してください。
+
出荷版 v1 config は marker 2 の単一オプション行に自動移行されます。オリジナルは
`~/.opencodex/config.json.pre-openai-tiers-v2.bak` に一度保存され、次のコマンドで復元します:
`cp ~/.opencodex/config.json.pre-openai-tiers-v2.bak ~/.opencodex/config.json`。
diff --git a/docs-site/src/content/docs/ko/guides/providers.md b/docs-site/src/content/docs/ko/guides/providers.md
index f7cf96e29f..5824614915 100644
--- a/docs-site/src/content/docs/ko/guides/providers.md
+++ b/docs-site/src/content/docs/ko/guides/providers.md
@@ -20,6 +20,24 @@ max input 922,000이며 `*-pro` virtual id는 공개 상태에 유지되고 wire
내장 `openai` 제공자가 없거나 비활성화된 경우 대시보드 Accounts 선택기와 Codex Auth 페이지에서 복구할 수 있습니다. 없는 항목은 정규 프리셋으로 만들고, 비활성화된 정규 항목은 저장된 모드/모델 설정을 바꾸지 않고 다시 켜며, 비정규 `openai` 항목에는 그 복구 경로를 제공하지 않습니다.
+### 프로바이더 개요의 풀 용량
+
+Codex 로그인을 Pool 모드로 사용하면 Providers 개요에는 임의의 한 계정이 아니라 풀 전체의
+사용 용량 추정치가 표시됩니다. 같은 행에는 현재 유효 계정의 원본 quota 사용률도 표시되므로,
+풀 추정치와 다음 요청에서 사용할 계정의 상태를 구분할 수 있습니다.
+
+리셋 정보가 있으면 다음 리셋 시각과 그때 회복되는 풀 용량을 표시합니다. **불완전한 범위**는
+요금제나 quota를 알 수 없거나, 측정값이 오래되었거나, 계정이 일시 중지되었거나 재인증이 필요한
+등의 이유로 일부 계정을 안전하게 추정치에 포함하지 못했음을 뜻합니다.
+
+**일부 기간의 범위가 불완전함**은 포함된 계정 중 일부가 표시된 quota 기간을 모두 보고하지
+않았음을 뜻합니다. 개요는 각 기간을 서로 분리한 채 영향을 받은 기간을 개별적으로 불완전하다고
+표시하며, 누락된 값을 해당 기간의 사용량으로 간주하지 않습니다.
+
+이 추정치는 표시 전용입니다. 계정 선택, 세션 affinity, 자동 전환, cooldown 또는 다른 라우팅
+판단을 변경하지 않습니다. 개별 계정 상태와 라우팅 제어는
+[Codex Auth 계정 풀](/ko/guides/web-dashboard/#codex-auth-and-account-pools)을 참고하세요.
+
shipped v1 config는 marker 2의 단일 옵션 행으로 자동 이관됩니다. 원본은
`~/.opencodex/config.json.pre-openai-tiers-v2.bak`에 한 번 보존되며 다음 명령으로 복원합니다:
`cp ~/.opencodex/config.json.pre-openai-tiers-v2.bak ~/.opencodex/config.json`.
diff --git a/docs-site/src/content/docs/ru/guides/providers.md b/docs-site/src/content/docs/ru/guides/providers.md
index 1ed4d5cbcb..6476a60f8a 100644
--- a/docs-site/src/content/docs/ru/guides/providers.md
+++ b/docs-site/src/content/docs/ru/guides/providers.md
@@ -25,6 +25,27 @@ description: Все способы, которыми opencodex аутентиф
канонические записи включаются без замены сохранённого режима и настроек моделей, а неканонические
записи `openai` этот путь восстановления не получают.
+### Ёмкость пула в обзоре провайдеров
+
+Для входа Codex в режиме Pool обзор Providers показывает оценку использованной ёмкости всего пула,
+а не показатель произвольного аккаунта. В той же строке отображается исходный процент квоты текущего
+активного аккаунта, поэтому оценку пула можно отличить от состояния аккаунта, который будет использован
+для следующего запроса.
+
+Когда доступны сведения о сбросе, обзор показывает время следующего сброса и ёмкость пула, которая
+восстановится в этот момент. **Неполное покрытие** означает, что некоторые аккаунты нельзя безопасно
+включить в оценку, например из-за неизвестного плана или квоты, устаревшего показания, приостановки
+аккаунта либо необходимости повторной аутентификации.
+
+Предупреждение **о частичном покрытии окон** означает, что некоторые включённые аккаунты сообщили данные
+только для части показанных окон квоты. Обзор сохраняет окна раздельными, отмечает каждое затронутое
+окно как неполное и не считает отсутствующее значение использованием в этом окне.
+
+Эта оценка предназначена только для отображения. Она не меняет выбор аккаунта, привязку сессии,
+автоматическое переключение, cooldown или другие решения маршрутизации. Состояние отдельных аккаунтов
+и настройки маршрутизации описаны в разделе
+[пула аккаунтов Codex Auth](/ru/guides/web-dashboard/#codex-auth-and-account-pools).
+
Поставляемые v1-конфигурации автоматически мигрируют на маркер 2 и одну строку с поддержкой опций.
Исходная конфигурация один раз сохраняется в `~/.opencodex/config.json.pre-openai-tiers-v2.bak`;
восстановить её можно командой
diff --git a/docs-site/src/content/docs/zh-cn/guides/providers.md b/docs-site/src/content/docs/zh-cn/guides/providers.md
index 6e6e4796f1..e78a29ae89 100644
--- a/docs-site/src/content/docs/zh-cn/guides/providers.md
+++ b/docs-site/src/content/docs/zh-cn/guides/providers.md
@@ -19,6 +19,22 @@ bare `gpt-5.6-sol` 遵循 Providers 页面中的 Pool/Direct 选项,
若内置 `openai` 提供商缺失或已禁用,可在仪表盘 Accounts 选择器或 Codex Auth 页面恢复:缺失行会从规范预设创建,已禁用的规范行会在不替换已保存模式/模型设置的情况下重新启用,非规范的 `openai` 行不会提供该恢复路径。
+### 提供商概览中的账户池容量
+
+Codex 登录使用 Pool 模式时,Providers 概览显示整个账户池的已用容量估算,而不是任意一个
+账户的数值。同一行还会显示当前有效账户的原始配额使用率,便于区分账户池估算与下一次请求
+将使用的账户状态。
+
+如果有重置信息,概览会显示下一次重置时间以及届时恢复的账户池容量。**覆盖不完整**表示某些
+账户无法安全计入估算,例如套餐或配额未知、读数过旧、账户已暂停或需要重新认证。
+
+**部分窗口覆盖不完整**表示某些已计入账户只报告了部分显示的配额窗口。概览会保持各窗口相互
+独立,逐一标记受影响的窗口,并且不会把缺失值当作该窗口的使用量。
+
+此估算仅用于显示,不会改变账户选择、会话关联、自动切换、cooldown 或任何其他路由决策。
+各账户状态和路由控制请参阅
+[Codex Auth 账户池](/zh-cn/guides/web-dashboard/#codex-auth-and-account-pools)。
+
shipped v1 配置自动迁移到 marker 2 的单一选项行。原配置只保留一次到
`~/.opencodex/config.json.pre-openai-tiers-v2.bak`;恢复命令:
`cp ~/.opencodex/config.json.pre-openai-tiers-v2.bak ~/.opencodex/config.json`。
diff --git a/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx b/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx
index 375cbc3fbe..b2741a851f 100644
--- a/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx
+++ b/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx
@@ -252,11 +252,11 @@ function ProviderCapacityQuota({ report, pending }: { report: ProviderQuotaRepor
if (window.incomplete) incompleteCustomWindowLabels.add(window.label);
}
}
- const recoveryRows: Array<{ label: string; window: CapacityWindowView }> = showsAggregate && aggregation ? [
- ...(aggregation.fiveHour ? [{ label: t("codexAuth.fiveHour"), window: aggregation.fiveHour }] : []),
- ...(aggregation.weekly ? [{ label: t("codexAuth.weekly"), window: aggregation.weekly }] : []),
- ...(aggregation.monthly ? [{ label: t("codexAuth.monthly"), window: aggregation.monthly }] : []),
- ...(aggregation.customWindows ?? []).map(window => ({ label: window.label, window })),
+ const recoveryRows: Array<{ key: number; label: string; window: CapacityWindowView }> = showsAggregate && aggregation ? [
+ ...(aggregation.fiveHour ? [{ key: 0, label: t("codexAuth.fiveHour"), window: aggregation.fiveHour }] : []),
+ ...(aggregation.weekly ? [{ key: 1, label: t("codexAuth.weekly"), window: aggregation.weekly }] : []),
+ ...(aggregation.monthly ? [{ key: 2, label: t("codexAuth.monthly"), window: aggregation.monthly }] : []),
+ ...(aggregation.customWindows ?? []).map((window, index) => ({ key: index + 3, label: window.label, window })),
] : [];
const formatPercent = (value: number) => new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }).format(value);
const formatRecoveryAt = (value: number) => new Intl.DateTimeFormat(locale, {
@@ -280,9 +280,9 @@ function ProviderCapacityQuota({ report, pending }: { report: ProviderQuotaRepor
)}
{aggregation && (
- {recoveryRows.flatMap(({ label, window }) => (
+ {recoveryRows.flatMap(({ key, label, window }) => (
window.nextRecoveryAt !== undefined && window.nextRecoveryPercent !== undefined
- ? [
+ ? [
{t("pws.capacity.nextRecovery")} · {label} · {formatRecoveryAt(window.nextRecoveryAt)}
{t("pws.capacity.recoveryShare", { percent: formatPercent(window.nextRecoveryPercent) })}
]
diff --git a/gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx b/gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx
index ecce71a6fc..f762e6e2c3 100644
--- a/gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx
+++ b/gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx
@@ -276,6 +276,7 @@ export default function ProviderWorkspaceShell({
writeSessionListCache(quotasCacheKey, next);
})
.catch(() => {
+ if (cancelled) return;
// Keep last-good only inside the same server freshness bound.
setQuotaReports(prev => {
const next = freshQuotaReportRecord(prev) ?? {};
diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts
index d171ae0f30..c4919962c8 100644
--- a/gui/src/i18n/de.ts
+++ b/gui/src/i18n/de.ts
@@ -1380,7 +1380,7 @@ export const de: Record
= {
"pws.dashboard.title": "Anbieterübersicht",
"pws.dashboard.subtitle": "Verwalten Sie alle Ihre Modellanbieter an einem Ort.",
"pws.dashboard.rateLimits": "RATE LIMITS",
- "pws.capacity.estimate": "Konfiguriert gewichtete Pool-Schätzung",
+ "pws.capacity.estimate": "Pool-Schätzung anhand konfigurierter Gewichtungen",
"pws.capacity.currentAccount": "Aktuelles effektives Konto",
"pws.capacity.nextRecovery": "Nächste Kapazitätswiederherstellung",
"pws.capacity.recoveryShare": "+{percent} % Pool-Kapazität",
diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts
index ace73a9742..b88d993433 100644
--- a/gui/src/i18n/ru.ts
+++ b/gui/src/i18n/ru.ts
@@ -1042,7 +1042,7 @@ export const ru: Record = {
"pws.capacity.currentAccount": "Текущая активная учётная запись",
"pws.capacity.nextRecovery": "Следующее восстановление ёмкости",
"pws.capacity.recoveryShare": "+{percent}% ёмкости пула",
- "pws.capacity.incomplete": "Неполное покрытие: исключено аккаунтов: {excluded}, неизвестных планов: {unknown}",
+ "pws.capacity.incomplete": "Неполное покрытие: исключено аккаунтов: {excluded}, в том числе с неизвестным планом: {unknown}",
"pws.capacity.partial": "Частичное покрытие окон: для {count} аккаунтов доступны не все показанные окна лимитов",
"pws.capacity.windowPartial": "Частично",
"pws.capacity.windowPartialA11y": "{window}: неполное покрытие аккаунтов",
diff --git a/gui/src/provider-workspace/report.ts b/gui/src/provider-workspace/report.ts
index 6b4f98ccce..0907c2be81 100644
--- a/gui/src/provider-workspace/report.ts
+++ b/gui/src/provider-workspace/report.ts
@@ -104,9 +104,9 @@ export function capacityAggregationFromReport(report?: ProviderQuotaReportView):
? row.currentAccount as Record
: null;
const customWindows = Array.isArray(row.customWindows)
- ? row.customWindows.flatMap(value => {
- if (!value || typeof value !== "object" || Array.isArray(value)) return [];
- const custom = value as Record;
+ ? row.customWindows.flatMap(entry => {
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
+ const custom = entry as Record;
const window = capacityWindow(custom);
return typeof custom.label === "string" && window ? [{ label: custom.label, ...window }] : [];
})
diff --git a/gui/tests/provider-capacity-shell.test.tsx b/gui/tests/provider-capacity-shell.test.tsx
index 49e1bc50f5..bccdc8629e 100644
--- a/gui/tests/provider-capacity-shell.test.tsx
+++ b/gui/tests/provider-capacity-shell.test.tsx
@@ -14,6 +14,7 @@ let host: HTMLElement;
let root: Root | null = null;
let quotaPayload: unknown;
let rejectQuotaFetch = false;
+let quotaFetchOverride: (() => Promise) | null = null;
const QUOTA_CACHE_KEY = "ocx.providers.quotas.v1:";
const RECOVERY_AT = Date.UTC(2026, 7, 8, 4, 32);
@@ -21,7 +22,44 @@ const providers = {
openai: { adapter: "openai-responses", authMode: "forward", baseUrl: "https://chatgpt.com/backend-api/codex" },
} as never;
-function aggregatePayload() {
+type TestCapacityWindow = {
+ usedPercent: number;
+ includedAccounts: number;
+ excludedAccounts: number;
+ incomplete: boolean;
+ updatedAt: number;
+ nextRecoveryAt?: number;
+ nextRecoveryPercent?: number;
+};
+
+type AggregateTestPayload = {
+ reports: [{
+ provider: string;
+ label: string;
+ source: string;
+ updatedAt: number;
+ quota: { weeklyPercent: number; monthlyPercent?: number; updatedAt: number };
+ aggregation: {
+ kind: string;
+ scope: string;
+ presentation: string;
+ includedAccounts: number;
+ excludedAccounts: number;
+ unknownPlanAccounts: number;
+ missingQuotaAccounts: number;
+ pausedAccounts: number;
+ reauthAccounts: number;
+ staleQuotaAccounts: number;
+ partialWindowAccounts?: number;
+ incomplete: boolean;
+ weekly: TestCapacityWindow;
+ monthly?: TestCapacityWindow;
+ currentAccount: { isMain: boolean; plan: string; quota: { weeklyPercent: number; updatedAt: number } };
+ };
+ }];
+};
+
+function aggregatePayload(): AggregateTestPayload {
return {
reports: [{
provider: "openai",
@@ -56,6 +94,15 @@ function aggregatePayload() {
};
}
+function quotaResponse(body: unknown): Response {
+ return {
+ ok: true,
+ status: 200,
+ json: async () => body,
+ text: async () => JSON.stringify(body),
+ } as unknown as Response;
+}
+
function aggregateWindowPayload(weeklyIncomplete: boolean, monthlyIncomplete: boolean) {
const now = Date.now();
const incomplete = weeklyIncomplete || monthlyIncomplete;
@@ -110,13 +157,15 @@ beforeEach(() => {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
quotaPayload = aggregatePayload();
rejectQuotaFetch = false;
+ quotaFetchOverride = null;
Object.defineProperty(globalThis, "fetch", {
configurable: true,
value: async (input: string) => {
const url = String(input);
if (url.includes("/api/provider-quotas") && rejectQuotaFetch) throw new Error("quota unavailable");
+ if (url.includes("/api/provider-quotas") && quotaFetchOverride) return quotaFetchOverride();
const body = url.includes("/api/provider-quotas") ? quotaPayload : {};
- return { ok: true, status: 200, json: async () => body, text: async () => JSON.stringify(body) } as unknown as Response;
+ return quotaResponse(body);
},
});
host = win.document.createElement("div") as unknown as HTMLElement;
@@ -133,10 +182,10 @@ afterEach(async () => {
Object.defineProperty(globalThis, "fetch", { configurable: true, value: originalFetch });
});
-async function mountShell() {
+async function mountShell(quotaRefreshEpoch = 0) {
const { createRoot } = await import("react-dom/client");
await act(async () => {
- root = createRoot(host);
+ root ??= createRoot(host);
root.render(
{}}
onAddProvider={() => {}}
+ quotaRefreshEpoch={quotaRefreshEpoch}
/>
,
);
@@ -164,7 +214,11 @@ test("provider quota fetch preserves aggregate capacity through shell state and
expect(text).toContain("Incomplete coverage: 1 account(s) excluded, including 1 unknown plan(s)");
expect(text).toContain("Next capacity recovery");
expect(text).toContain("+19.2% pool capacity");
- expect(text).toMatch(/Aug 8, 2026.*(4:32|1:32)/);
+ const expectedRecoveryAt = new Intl.DateTimeFormat("en", {
+ dateStyle: "medium",
+ timeStyle: "short",
+ }).format(new Date(RECOVERY_AT));
+ expect(text).toContain(expectedRecoveryAt);
expect(text).not.toMatch(/configured units|weighted units|units remaining|projected/i);
});
@@ -201,6 +255,44 @@ test("expired session quota is rejected and a failed fetch cannot keep it render
expect(readSessionListCache(QUOTA_CACHE_KEY)).toEqual({});
});
+test("a cancelled superseded quota rejection cannot rewrite state or session cache", async () => {
+ let rejectFirst!: (reason?: unknown) => void;
+ const first = new Promise((_resolve, reject) => { rejectFirst = reject; });
+ const fresh = aggregatePayload();
+ fresh.reports[0].quota.weeklyPercent = 63;
+ fresh.reports[0].aggregation.weekly.usedPercent = 63;
+ let calls = 0;
+ quotaFetchOverride = () => {
+ calls += 1;
+ return calls === 1 ? first : Promise.resolve(quotaResponse(fresh));
+ };
+
+ await mountShell(0);
+ await mountShell(1);
+ expect(calls).toBe(2);
+ expect(host.textContent ?? "").toContain("63% used");
+ const cached = readSessionListCache(QUOTA_CACHE_KEY);
+ const writes: string[] = [];
+ const storage = win.sessionStorage as unknown as Storage;
+ const setItem = storage.setItem.bind(storage);
+ Object.defineProperty(storage, "setItem", {
+ configurable: true,
+ value: (key: string, value: string) => {
+ writes.push(key);
+ setItem(key, value);
+ },
+ });
+
+ await act(async () => {
+ rejectFirst(new Error("superseded"));
+ await new Promise(resolve => setTimeout(resolve, 0));
+ });
+
+ expect(writes).toEqual([]);
+ expect(readSessionListCache(QUOTA_CACHE_KEY)).toEqual(cached);
+ expect(host.textContent ?? "").toContain("63% used");
+});
+
test("all-stale response renders coverage only without a numeric fallback", async () => {
const old = Date.now() - 31 * 60_000;
quotaPayload = {
diff --git a/gui/tests/provider-capacity.test.ts b/gui/tests/provider-capacity.test.ts
index 4d1eaec0f5..92a80c9de3 100644
--- a/gui/tests/provider-capacity.test.ts
+++ b/gui/tests/provider-capacity.test.ts
@@ -1,6 +1,14 @@
import { expect, test } from "bun:test";
import { capacityAggregationFromReport } from "../src/provider-workspace/report";
+function selectorBlock(css: string, selector: string): string {
+ const start = css.indexOf(`${selector} {`);
+ expect(start).toBeGreaterThanOrEqual(0);
+ const end = css.indexOf("}", start);
+ expect(end).toBeGreaterThan(start);
+ return css.slice(start, end + 1);
+}
+
test("legacy provider quota reports remain valid without aggregation metadata", () => {
expect(capacityAggregationFromReport({ quota: { weeklyPercent: 42 } })).toBeNull();
});
@@ -97,14 +105,15 @@ test("fallback and coverage-only metadata never become aggregate presentation",
test("capacity recovery layout wraps and stacks in narrow provider panes", async () => {
const css = await Bun.file(new URL("../src/styles/provider-overview-dashboard.css", import.meta.url)).text();
const quotaCss = await Bun.file(new URL("../src/styles/provider-quota.css", import.meta.url)).text();
- expect(css).toContain(".pws-capacity-recovery {");
- expect(css).toContain("flex-wrap: wrap;");
- expect(css).toContain("overflow-wrap: anywhere;");
+ const recovery = selectorBlock(css, ".pws-capacity-recovery");
+ const recoveryText = selectorBlock(css, ".pws-capacity-recovery span");
+ expect(recovery).toContain("flex-wrap: wrap;");
+ expect(recoveryText).toContain("overflow-wrap: anywhere;");
expect(css).toContain("@container (max-width: 520px)");
expect(css.slice(css.indexOf("@container (max-width: 520px)"))).toContain("grid-template-columns: minmax(0, 1fr);");
- expect(quotaCss).toContain(".quota-stacked-limit-group {");
- expect(quotaCss).toContain("flex-wrap: wrap;");
- expect(quotaCss).toContain("overflow-wrap: anywhere;");
+ const limitGroup = selectorBlock(quotaCss, ".quota-stacked-limit-group");
+ expect(limitGroup).toContain("flex-wrap: wrap;");
+ expect(limitGroup).toContain("overflow-wrap: anywhere;");
});
test("malformed or future aggregation contracts fail closed", () => {
diff --git a/src/providers/codex-capacity.ts b/src/providers/codex-capacity.ts
index f55501c47b..7ced2ce9bb 100644
--- a/src/providers/codex-capacity.ts
+++ b/src/providers/codex-capacity.ts
@@ -84,7 +84,7 @@ type MutableWindow = {
function configuredWeight(plan: string | null | undefined): number | undefined {
const normalized = plan?.trim().toLowerCase();
- return normalized && normalized in CODEX_CONFIGURED_CAPACITY_WEIGHTS
+ return normalized && Object.hasOwn(CODEX_CONFIGURED_CAPACITY_WEIGHTS, normalized)
? CODEX_CONFIGURED_CAPACITY_WEIGHTS[normalized as keyof typeof CODEX_CONFIGURED_CAPACITY_WEIGHTS]
: undefined;
}
@@ -101,6 +101,22 @@ function futureResetMs(value: unknown, now: number): number | undefined {
return milliseconds > now ? milliseconds : undefined;
}
+function hasKnownQuotaWindow(quota: CodexCapacityQuota | null): quota is CodexCapacityQuota {
+ if (!quota) return false;
+ return normalizedPercent(quota.fiveHourPercent) !== undefined
+ || normalizedPercent(quota.weeklyPercent) !== undefined
+ || normalizedPercent(quota.monthlyPercent) !== undefined
+ || !!quota.customWindows?.some(window => normalizedPercent(window.percent) !== undefined);
+}
+
+function currentQuotaForDisplay(account: CodexCapacityAccount, now: number): CodexCapacityQuota | null {
+ const quota = account.quota;
+ const fresh = !!quota
+ && Number.isFinite(quota.updatedAt)
+ && now - quota.updatedAt <= CODEX_CAPACITY_MAX_QUOTA_AGE_MS;
+ return !account.paused && !account.needsReauth && fresh && hasKnownQuotaWindow(quota) ? quota : null;
+}
+
function addWindow(
windows: Map,
key: string,
@@ -121,7 +137,7 @@ function addWindow(
window.consumedWeight += consumed;
window.includedAccounts += 1;
window.oldestUpdatedAt = Math.min(window.oldestUpdatedAt, updatedAt);
- if (resetAt !== undefined) {
+ if (resetAt !== undefined && consumed > 0) {
window.recoveries.set(resetAt, (window.recoveries.get(resetAt) ?? 0) + consumed);
}
windows.set(key, window);
@@ -155,7 +171,7 @@ export function aggregateCodexPoolCapacity(
const currentAccount = current ? {
isMain: current.isMain,
...(current.plan !== undefined ? { plan: current.plan } : {}),
- quota: current.quota,
+ quota: currentQuotaForDisplay(current, now),
} : undefined;
const windows = new Map();
const included = new Set();
@@ -182,8 +198,7 @@ export function aggregateCodexPoolCapacity(
["monthly", quota.monthlyPercent, quota.monthlyResetAt],
] as const : [];
const custom = quota?.customWindows ?? [];
- const hasQuota = standard.some(([, percent]) => normalizedPercent(percent) !== undefined)
- || custom.some(window => normalizedPercent(window.percent) !== undefined);
+ const hasQuota = hasKnownQuotaWindow(quota);
if (!hasQuota) missingQuotaAccounts += 1;
if (account.paused || account.needsReauth || weight === undefined || !quota || !hasQuota || !quotaFresh) continue;
diff --git a/src/providers/quota.ts b/src/providers/quota.ts
index 1c40d06950..f043db12b3 100644
--- a/src/providers/quota.ts
+++ b/src/providers/quota.ts
@@ -81,6 +81,16 @@ function cacheKey(config: OcxConfig): string {
return `${config.defaultProvider}|${providers}`;
}
+type CodexAuthAccountsPromise = ReturnType;
+
+function hasCodexPoolProvider(config: OcxConfig): boolean {
+ return Object.entries(config.providers).some(([name, provider]) => (
+ provider.disabled !== true
+ && isBuiltInChatGptForwardProvider(name, provider)
+ && providerCodexAccountMode(name, provider) !== "direct"
+ ));
+}
+
function quotaSignatureValue(quota: CodexCapacityQuota | null): unknown {
if (!quota) return null;
return {
@@ -98,18 +108,16 @@ function quotaSignatureValue(quota: CodexCapacityQuota | null): unknown {
}
/** Hash only presentation-relevant state; account ids and email addresses never enter the key. */
-function cacheKeyWithAggregationState(config: OcxConfig): string | Promise {
+function cacheKeyWithAggregationState(
+ config: OcxConfig,
+ prefetchedAccounts?: CodexAuthAccountsPromise,
+): string | Promise {
const base = cacheKey(config);
- const poolEnabled = Object.entries(config.providers).some(([name, provider]) => (
- provider.disabled !== true
- && isBuiltInChatGptForwardProvider(name, provider)
- && providerCodexAccountMode(name, provider) !== "direct"
- ));
- if (!poolEnabled) return base;
+ if (!hasCodexPoolProvider(config)) return base;
return (async () => {
try {
const activeId = effectiveCodexAuthAccountId(config);
- const rows = (await listCodexAuthAccounts(config, false)).map(account => ({
+ const rows = (await (prefetchedAccounts ?? listCodexAuthAccounts(config, false))).map(account => ({
isMain: account.isMain,
active: account.id === activeId,
plan: account.plan?.trim().toLowerCase() ?? null,
@@ -227,13 +235,14 @@ async function fetchChatGptForwardQuota(
provider: string,
providerConfig: OcxProviderConfig,
forceRefresh: boolean,
+ prefetchedAccounts?: CodexAuthAccountsPromise,
): Promise {
if (providerCodexAccountMode(provider, providerConfig) === "direct") {
const main = await fetchMainAccountInfo(forceRefresh);
const quota = main.quota ? { ...main.quota, updatedAt: Date.now() } as ProviderQuota : null;
return quota ? report(provider, "chatgpt:wham", quota) : null;
}
- const accounts = await listCodexAuthAccounts(config, forceRefresh);
+ const accounts = await (prefetchedAccounts ?? listCodexAuthAccounts(config, forceRefresh));
const activeId = effectiveCodexAuthAccountId(config);
const capacityAccounts = accounts.map(account => ({ ...account, active: account.id === activeId }));
const active = capacityAccounts.find(account => account.active)
@@ -1022,10 +1031,13 @@ async function maybeFetchProviderQuota(
provider: OcxProviderConfig,
config: OcxConfig,
forceRefresh: boolean,
+ prefetchedCodexAccounts?: CodexAuthAccountsPromise,
): Promise {
if (provider.disabled === true) return null;
try {
- if (isBuiltInChatGptForwardProvider(name, provider)) return fetchChatGptForwardQuota(config, name, provider, forceRefresh);
+ if (isBuiltInChatGptForwardProvider(name, provider)) {
+ return fetchChatGptForwardQuota(config, name, provider, forceRefresh, prefetchedCodexAccounts);
+ }
if (provider.authMode === "oauth" && name === "xai") return fetchXaiQuota(name);
if (provider.authMode === "oauth" && name === "anthropic") return fetchAnthropicQuota(name);
if (provider.authMode === "oauth" && name === "cursor") return fetchCursorQuota(name);
@@ -1043,7 +1055,12 @@ async function maybeFetchProviderQuota(
}
export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh = false): Promise {
- const keyCandidate = cacheKeyWithAggregationState(config);
+ // A forced Pool refresh must share one account-list probe between the pre-signature and
+ // provider fetch. The commit-time signature still re-reads current state to reject races.
+ const prefetchedCodexAccounts = forceRefresh && hasCodexPoolProvider(config)
+ ? listCodexAuthAccounts(config, true)
+ : undefined;
+ const keyCandidate = cacheKeyWithAggregationState(config, prefetchedCodexAccounts);
const key = typeof keyCandidate === "string" ? keyCandidate : await keyCandidate;
const writerGeneration = captureConfigGeneration();
const now = Date.now();
@@ -1061,7 +1078,9 @@ export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh
const promise = (async (): Promise => {
const previous = cache && cache.key === key ? cache.response.reports : [];
const fresh = (await Promise.all(
- Object.entries(config.providers).map(([name, provider]) => maybeFetchProviderQuota(name, provider, config, forceRefresh)),
+ Object.entries(config.providers).map(([name, provider]) => (
+ maybeFetchProviderQuota(name, provider, config, forceRefresh, prefetchedCodexAccounts)
+ )),
)).filter((item): item is ProviderQuotaReport => item !== null);
// Keep bounded last-good rows when a probe fails (e.g. transient upstream flake); never
diff --git a/tests/provider-capacity.test.ts b/tests/provider-capacity.test.ts
index ca61b8d8d3..e45afb45b7 100644
--- a/tests/provider-capacity.test.ts
+++ b/tests/provider-capacity.test.ts
@@ -54,6 +54,15 @@ describe("configured-weight Codex pool capacity", () => {
expect(result.aggregation?.weekly).not.toHaveProperty("projectedUsedPercentAfterReset");
});
+ test("the next recovery skips earlier zero-consumption resets", () => {
+ const result = aggregateCodexPoolCapacity([
+ account("pro", 0, { weeklyResetAt: NOW + 10_000 }),
+ account("prolite", 40, { weeklyResetAt: NOW + 20_000 }),
+ ], NOW);
+ expect(result.aggregation?.weekly?.nextRecoveryAt).toBe(NOW + 20_000);
+ expect(result.aggregation?.weekly?.nextRecoveryPercent).toBeCloseTo(2 / 25 * 100, 8);
+ });
+
test("unknown, missing, paused, and reauth rows are excluded with incomplete coverage", () => {
const result = aggregateCodexPoolCapacity([
account("pro", 10, { active: true, isMain: true }),
@@ -135,6 +144,41 @@ describe("configured-weight Codex pool capacity", () => {
});
});
+ test("a stale effective secondary quota is hidden while a fresh main account still aggregates", () => {
+ const main = account("plus", 20, { isMain: true });
+ const staleActive = account("pro", 90, { active: true });
+ staleActive.quota = {
+ ...staleActive.quota!,
+ updatedAt: NOW - CODEX_CAPACITY_MAX_QUOTA_AGE_MS - 1,
+ };
+
+ const result = aggregateCodexPoolCapacity([main, staleActive], NOW);
+ expect(result.quota?.weeklyPercent).toBe(20);
+ expect(result.aggregation).toMatchObject({
+ includedAccounts: 1,
+ excludedAccounts: 1,
+ staleQuotaAccounts: 1,
+ currentAccount: { plan: "pro", quota: null },
+ });
+ });
+
+ test("prototype and unknown plan names never become configured weights", () => {
+ const result = aggregateCodexPoolCapacity([
+ account("plus", 20),
+ account("constructor", 100),
+ account("toString", 100),
+ account("valueOf", 100),
+ account("unknown", 100),
+ ], NOW);
+ expect(result.quota?.weeklyPercent).toBe(20);
+ expect(result.aggregation).toMatchObject({
+ includedAccounts: 1,
+ excludedAccounts: 4,
+ unknownPlanAccounts: 4,
+ });
+ expect(Number.isFinite(result.quota?.weeklyPercent)).toBe(true);
+ });
+
test("all-stale rows expose incomplete coverage without an aggregate window", () => {
const rows = [account("pro", 80, { active: true, isMain: true }), account("prolite", 20)];
for (const row of rows) {
diff --git a/tests/provider-quota.test.ts b/tests/provider-quota.test.ts
index 5273f8c3e6..d1dd80c265 100644
--- a/tests/provider-quota.test.ts
+++ b/tests/provider-quota.test.ts
@@ -515,6 +515,29 @@ describe("fetchProviderQuotaReports", () => {
expect(JSON.stringify(openai?.aggregation)).not.toMatch(/(?:total|consumed|remaining)Weight|projectedUsedPercent/i);
});
+ test("one forced Pool refresh probes each account once", async () => {
+ saveCodexAccountCredential("added", {
+ accessToken: "added-access", refreshToken: "added-refresh",
+ expiresAt: Date.now() + 3600_000, chatgptAccountId: "added-chatgpt-id",
+ });
+ const config = testConfig();
+ config.providers = { openai: config.providers.openai };
+ config.codexAccounts = [{ id: "added", email: "a@example.test", plan: "prolite", isMain: false }];
+ const calls = new Map();
+ globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => {
+ const accountId = (init?.headers as Record | undefined)?.["ChatGPT-Account-Id"] ?? "main";
+ calls.set(accountId, (calls.get(accountId) ?? 0) + 1);
+ return new Response(JSON.stringify({
+ plan_type: accountId === "added-chatgpt-id" ? "prolite" : "plus",
+ rate_limit: { secondary_window: { used_percent: 25, reset_at: 1_999_000_000 } },
+ }), { status: 200, headers: { "content-type": "application/json" } });
+ }) as typeof fetch;
+
+ await fetchProviderQuotaReports(config, true);
+
+ expect(calls).toEqual(new Map([["chatgpt-main-account", 1], ["added-chatgpt-id", 1]]));
+ });
+
test("all-excluded pool still returns a coverage-only OpenAI report", async () => {
rmSync(join(codexHome, "auth.json"), { force: true });
clearMainAccountInfoCache();
From 72a258bd57c660366e563154851c1117fbdbc7a5 Mon Sep 17 00:00:00 2001
From: Wibias <37517432+Wibias@users.noreply.github.com>
Date: Mon, 3 Aug 2026 19:03:09 +0200
Subject: [PATCH 09/10] fix(gui): address capacity review feedback and quota
test isolation
- docs: keep the Providers overview paragraph after the Codex Auth bullet list
- docs: stable codex-auth-and-account-pools anchors in ja/ko/ru/zh-cn
- gui: expose the partial-window aria-label via role=note
- quota: exclude paused/reauth accounts from the effective-account fallback
- tests: reset the provider-quota reconcile guard between test files
---
docs-site/src/content/docs/guides/web-dashboard.md | 10 +++++-----
docs-site/src/content/docs/ja/guides/web-dashboard.md | 2 ++
docs-site/src/content/docs/ko/guides/web-dashboard.md | 2 ++
docs-site/src/content/docs/ru/guides/web-dashboard.md | 2 ++
.../src/content/docs/zh-cn/guides/web-dashboard.md | 2 ++
gui/src/components/QuotaBars.tsx | 1 +
gui/tests/provider-capacity-shell.test.tsx | 1 +
src/providers/quota.ts | 10 +++++++++-
tests/provider-account-quota.test.ts | 2 ++
9 files changed, 26 insertions(+), 6 deletions(-)
diff --git a/docs-site/src/content/docs/guides/web-dashboard.md b/docs-site/src/content/docs/guides/web-dashboard.md
index afe7134713..660fbcfdac 100644
--- a/docs-site/src/content/docs/guides/web-dashboard.md
+++ b/docs-site/src/content/docs/guides/web-dashboard.md
@@ -105,11 +105,6 @@ catalog entry.
The **Codex Auth** page manages the native ChatGPT/Codex route:
-The Providers overview separately summarizes Pool-mode usage as a display-only weighted capacity
-estimate, alongside the effective account's raw quota and the next capacity recovery. See
-[Providers overview pool capacity](/guides/providers/#providers-overview-pool-capacity) for the
-visible fields, incomplete-coverage meaning, and routing boundary.
-
- Manually choosing an account changes the next new Codex session; an already-bound thread keeps its
current account for that manual switch.
- Thread affinity prevents per-request flapping. With quota auto-switch enabled, a long-running
@@ -124,6 +119,11 @@ visible fields, incomplete-coverage meaning, and routing boundary.
values.
- Pool request logs use opaque labels such as `p3fa91c`, never account emails.
+The Providers overview separately summarizes Pool-mode usage as a display-only weighted capacity
+estimate, alongside the effective account's raw quota and the next capacity recovery. See
+[Providers overview pool capacity](/guides/providers/#providers-overview-pool-capacity) for the
+visible fields, incomplete-coverage meaning, and routing boundary.
+
## Starring is yours to decide, not an agent's
The sidebar's star button — and the one-time question `ocx start` asks in an interactive
diff --git a/docs-site/src/content/docs/ja/guides/web-dashboard.md b/docs-site/src/content/docs/ja/guides/web-dashboard.md
index 912badc71e..33f063c044 100644
--- a/docs-site/src/content/docs/ja/guides/web-dashboard.md
+++ b/docs-site/src/content/docs/ja/guides/web-dashboard.md
@@ -83,6 +83,8 @@ Codex タスクだけに適用され、このオプション自体が委任を
選んだ強度がグローバル段階にあるか検査し、Codex は再び対象カタログ項目がその強度をサポートするか
検査します。
+
+
## Codex 認証とアカウントプール
**Codex 認証**ページはネイティブ ChatGPT/Codex ルートを管理します。
diff --git a/docs-site/src/content/docs/ko/guides/web-dashboard.md b/docs-site/src/content/docs/ko/guides/web-dashboard.md
index 43080714e0..601762d28e 100644
--- a/docs-site/src/content/docs/ko/guides/web-dashboard.md
+++ b/docs-site/src/content/docs/ko/guides/web-dashboard.md
@@ -83,6 +83,8 @@ Dashboard의 **Sub-agent delegation** 선택기는 `injectionModel`과 선택적
선택한 강도가 전역 단계에 있는지 검사하고, Codex는 다시 대상 카탈로그 항목이 그 강도를 지원하는지
검사합니다.
+
+
## Codex Auth와 계정 풀
**Codex Auth** 페이지는 네이티브 ChatGPT/Codex 라우트를 관리합니다.
diff --git a/docs-site/src/content/docs/ru/guides/web-dashboard.md b/docs-site/src/content/docs/ru/guides/web-dashboard.md
index 9fb5622e70..b82add77c7 100644
--- a/docs-site/src/content/docs/ru/guides/web-dashboard.md
+++ b/docs-site/src/content/docs/ru/guides/web-dashboard.md
@@ -86,6 +86,8 @@ bun run dev:gui
рассуждений Codex. API валидирует выбранный уровень глобально; Codex дополнительно валидирует
уровень порождения по целевой записи каталога.
+
+
## Codex Auth и пулы аккаунтов
Страница **Codex Auth** управляет нативным маршрутом ChatGPT/Codex:
diff --git a/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md b/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md
index 7a09d8b061..5503bbc900 100644
--- a/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md
+++ b/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md
@@ -77,6 +77,8 @@ Dashboard 的 **Sub-agent delegation** 选择器会保存 `injectionModel`,以
选择器会列出已启用的原生与路由模型,以及全局 Codex reasoning 阶梯。API 会先验证所选强度是否
属于全局阶梯;Codex 仍会根据目标目录条目再次校验该 spawn 强度。
+
+
## Codex Auth 与账号池
**Codex Auth** 页面用于管理原生 ChatGPT/Codex 路由:
diff --git a/gui/src/components/QuotaBars.tsx b/gui/src/components/QuotaBars.tsx
index e90f40a1cb..da782d0d02 100644
--- a/gui/src/components/QuotaBars.tsx
+++ b/gui/src/components/QuotaBars.tsx
@@ -312,6 +312,7 @@ function StackedQuotaRow({ row, threshold, t, locale, incomplete }: {
{incomplete && (
diff --git a/gui/tests/provider-capacity-shell.test.tsx b/gui/tests/provider-capacity-shell.test.tsx
index bccdc8629e..315d47f857 100644
--- a/gui/tests/provider-capacity-shell.test.tsx
+++ b/gui/tests/provider-capacity-shell.test.tsx
@@ -384,6 +384,7 @@ test("only the monthly aggregate window receives a localized partial marker", as
const markers = [...host.querySelectorAll(".quota-window-partial")];
expect(markers).toHaveLength(1);
expect(markers[0]?.textContent).toBe("Partial");
+ expect(markers[0]?.getAttribute("role")).toBe("note");
expect(markers[0]?.getAttribute("aria-label")).toBe("30-day limit: incomplete account coverage");
});
diff --git a/src/providers/quota.ts b/src/providers/quota.ts
index f043db12b3..836d17869f 100644
--- a/src/providers/quota.ts
+++ b/src/providers/quota.ts
@@ -258,7 +258,8 @@ async function fetchChatGptForwardQuota(
publicCapacityAggregation(capacity.aggregation, "aggregate"),
);
}
- const quota = active?.quota
+ const activeUsable = !!active && !active.paused && active.needsReauth !== true;
+ const quota = activeUsable && active?.quota
? { ...active.quota, updatedAt: active.quota.updatedAt ?? Date.now() } as CodexCapacityQuota
: null;
const quotaFresh = !!quota
@@ -506,6 +507,13 @@ export function reconcileProviderAccountQuotaRows(context: GenerationContext): n
return removed;
}
+/** Test-only reset so a direct reconcile call in one file cannot leak across files. */
+export function resetProviderQuotaReconcileStateForTests(): void {
+ lastReconciledGeneration = 0;
+ liveAccountQuotaKeys = new Set();
+ liveProviderQuotaKeys = new Set();
+}
+
/** Drop cached per-account rows (all, or just one provider's). */
export function clearAccountQuotaCache(provider?: string): void {
if (!provider) {
diff --git a/tests/provider-account-quota.test.ts b/tests/provider-account-quota.test.ts
index a8dafae677..634c663ca2 100644
--- a/tests/provider-account-quota.test.ts
+++ b/tests/provider-account-quota.test.ts
@@ -11,6 +11,7 @@ import {
fetchProviderQuotaReports,
getCachedProviderAccountQuota,
reconcileProviderAccountQuotaRows,
+ resetProviderQuotaReconcileStateForTests,
supportsPerAccountQuota,
} from "../src/providers/quota";
@@ -49,6 +50,7 @@ afterEach(() => {
rmSync(opencodexHome, { recursive: true, force: true });
clearAccountQuotaCache();
clearProviderQuotaCache();
+ resetProviderQuotaReconcileStateForTests();
});
describe("fetchProviderAccountQuotas", () => {
From 6fadee9f2ed0587f728d02a02297e81261d72b0b Mon Sep 17 00:00:00 2001
From: Wibias <37517432+Wibias@users.noreply.github.com>
Date: Mon, 3 Aug 2026 19:15:57 +0200
Subject: [PATCH 10/10] docs(i18n): sync translated dashboard guides with
Providers overview capacity
---
docs-site/src/content/docs/ja/guides/web-dashboard.md | 4 ++++
docs-site/src/content/docs/ko/guides/web-dashboard.md | 4 ++++
docs-site/src/content/docs/ru/guides/web-dashboard.md | 5 +++++
docs-site/src/content/docs/zh-cn/guides/web-dashboard.md | 4 ++++
4 files changed, 17 insertions(+)
diff --git a/docs-site/src/content/docs/ja/guides/web-dashboard.md b/docs-site/src/content/docs/ja/guides/web-dashboard.md
index 33f063c044..5dd87b4bdd 100644
--- a/docs-site/src/content/docs/ja/guides/web-dashboard.md
+++ b/docs-site/src/content/docs/ja/guides/web-dashboard.md
@@ -99,6 +99,10 @@ Codex タスクだけに適用され、このオプション自体が委任を
- **クォータ更新**はアカウント使用量を即座に再読み込みし、ルーティングと画面のアカウントカードが同じ値を見るようにします。
- プールリクエストログにはメールの代わりに `p3fa91c` のような不透明なラベルを使います。
+Providers の概要は、Pool モードの使用状況を表示専用の重み付き容量推定値として別途まとめ、現在の
+有効アカウントの生のクォータと次の容量回復も併せて表示します。表示される項目、不完全な対象範囲の
+意味、ルーティング上の境界については、[プロバイダー概要のプール容量](/ja/guides/providers/#プロバイダー概要のプール容量)を参照してください。
+
## ダッシュボードがプロキシと通信する方式
GUI はプロキシの JSON 管理 API を使うシンクライアントです。主なエンドポイントは次のとおりです。
diff --git a/docs-site/src/content/docs/ko/guides/web-dashboard.md b/docs-site/src/content/docs/ko/guides/web-dashboard.md
index 601762d28e..aedea3b6c3 100644
--- a/docs-site/src/content/docs/ko/guides/web-dashboard.md
+++ b/docs-site/src/content/docs/ko/guides/web-dashboard.md
@@ -101,6 +101,10 @@ Dashboard의 **Sub-agent delegation** 선택기는 `injectionModel`과 선택적
- **Refresh quotas**는 계정 사용량을 즉시 다시 읽어 라우팅과 화면의 계정 카드가 같은 값을 보게 합니다.
- 풀 요청 로그에는 이메일 대신 `p3fa91c` 같은 불투명한 라벨을 사용합니다.
+Providers 개요는 Pool 모드 사용량을 표시 전용 가중 용량 추정치로 별도 요약하고, 현재 유효 계정의
+원본 quota와 다음 용량 회복도 함께 표시합니다. 표시 필드, 불완전한 범위의 의미, 라우팅 경계는
+[프로바이더 개요의 풀 용량](/ko/guides/providers/#프로바이더-개요의-풀-용량)을 참고하세요.
+
## 스타는 에이전트가 아니라 사용자가 결정합니다
사이드바의 스타 버튼, 그리고 `ocx start`가 대화형 터미널에서 한 번 묻는 질문은 모두
diff --git a/docs-site/src/content/docs/ru/guides/web-dashboard.md b/docs-site/src/content/docs/ru/guides/web-dashboard.md
index b82add77c7..bb825211c2 100644
--- a/docs-site/src/content/docs/ru/guides/web-dashboard.md
+++ b/docs-site/src/content/docs/ru/guides/web-dashboard.md
@@ -105,6 +105,11 @@ bun run dev:gui
карточки аккаунтов опирались на одни и те же значения.
- Логи запросов пула используют непрозрачные метки вида `p3fa91c` и никогда — email аккаунтов.
+Обзор Providers дополнительно сводит использование Pool-режима в оценочную взвешенную ёмкость
+только для отображения, рядом с исходной квотой текущего активного аккаунта и следующим восстановлением
+ёмкости. Поля, значение неполного покрытия и границы маршрутизации описаны в разделе
+[ёмкость пула в обзоре провайдеров](/ru/guides/providers/#ёмкость-пула-в-обзоре-провайдеров).
+
## Как дашборд взаимодействует с прокси
GUI — это тонкий клиент поверх JSON-API управления прокси. Полезные эндпоинты:
diff --git a/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md b/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md
index 5503bbc900..4a3eb55053 100644
--- a/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md
+++ b/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md
@@ -93,6 +93,10 @@ Dashboard 的 **Sub-agent delegation** 选择器会保存 `injectionModel`,以
- **Refresh quotas** 会立即重新读取账号 usage,使路由逻辑与页面上的账号卡片使用同一份数据。
- 池账号的请求日志使用 `p3fa91c` 这类不透明标签,不会记录账号邮箱。
+Providers 概览会单独汇总 Pool 模式的显示专用加权容量估算,并同时显示当前有效账户的原始配额和
+下一次容量恢复。可见字段、覆盖不完整的含义以及路由边界,请参阅
+[提供商概览中的账户池容量](/zh-cn/guides/providers/#提供商概览中的账户池容量)。
+
## 仪表盘如何与代理通信
GUI 是代理 JSON 管理 API 之上的轻量客户端。常用 endpoint 包括: