feat(quota): sync upstream quota queries and improve reset handling

- Update Claude, Codex, Kimi, Antigravity, and xAI compatibility
 - Display plan details, subscription expiry, and dynamic reset times
 - Deduplicate quota requests and serialize Codex resets
 - Require confirmation for potentially billable xAI health probes
 - Add quota parsing, request, and rendering regression tests
This commit is contained in:
lzt404 committed 2026-09-06 19:02:49 +08:00
1 parent 34cab99a0a
commit e4d0059efa
13 files changed
+911 -141

No files matched your search

+10
View File
@@ -133,6 +133,16 @@ export const jaOverrides = {
'quota.resetCredits': '手動リセット回数',
'quota.earliestExpiry': '最も早い有効期限',
'quota.remaining': '残り {percent}%',
'quota.resetPassed': 'リセット時刻到達。更新して確認してください',
'quota.subscriptionExpiry': 'サブスクリプション期限:{time}',
'quota.resetApplicable': '現在適用可能:{count} 回',
'quota.resetCreditsWarning': 'リセット回数の詳細取得に失敗:{error}',
'quota.xaiProbeConfirm': 'xAI 有料アカウントはこの請求クォータ API に対応していません。短い grok-4.5 リクエストで利用可能か確認しますか?少額の料金が発生する可能性があり、残りクォータは取得できません。',
'quota.service.window.sevenDayFable': '7 日間 Fable 枠',
'quota.service.xaiPaidHealth': '有料 API 利用可能',
'quota.service.xaiPaidHealthDetail': '短い会話テストに成功。残りクォータは提供されていません',
'quota.service.error.xaiProbeRequired': '有料 API テストには確認が必要です。課金リクエストは送信していません',
'quota.service.error.resetCreditsInvalid': 'リセット回数 API のデータ形式を認識できません',
'quota.confirm.title': '「{name}」の Codex クォータをリセットしますか?',
'quota.confirm.cost': 'この操作では手動リセットを 1 回消費します。',
'quota.confirm.available': '現在利用可能:{count} 回',
+10
View File
@@ -170,6 +170,16 @@ export const en: Record<MessageKey, string> = {
'quota.resetCredits': 'Manual resets',
'quota.earliestExpiry': 'Earliest expiry',
'quota.remaining': '{percent}% remaining',
'quota.resetPassed': 'Reset time reached; refresh to verify',
'quota.subscriptionExpiry': 'Subscription expires: {time}',
'quota.resetApplicable': 'Currently applicable: {count}',
'quota.resetCreditsWarning': 'Reset credit details unavailable: {error}',
'quota.xaiProbeConfirm': 'Paid xAI accounts do not expose this billing quota API. Send a tiny grok-4.5 request to check availability? This may incur a small charge and does not report remaining quota.',
'quota.service.window.sevenDayFable': '7-day Fable window',
'quota.service.xaiPaidHealth': 'Paid API available',
'quota.service.xaiPaidHealthDetail': 'Tiny chat probe succeeded; remaining quota is not provided',
'quota.service.error.xaiProbeRequired': 'Paid API health probing requires confirmation; no billable request was sent',
'quota.service.error.resetCreditsInvalid': 'The reset credits endpoint returned unrecognized data',
'quota.confirm.title': 'Reset the Codex quota for “{name}”?',
'quota.confirm.cost': 'This will consume one manual reset credit.',
'quota.confirm.available': 'Currently available: {count}',
+10
View File
@@ -168,6 +168,16 @@ export const zhCN = {
'quota.resetCredits': '主动重置次数',
'quota.earliestExpiry': '最早过期',
'quota.remaining': '剩余 {percent}%',
'quota.resetPassed': '重置时间已到,请刷新确认',
'quota.subscriptionExpiry': '订阅到期:{time}',
'quota.resetApplicable': '当前适用:{count} 次',
'quota.resetCreditsWarning': '重置次数详情查询失败:{error}',
'quota.xaiProbeConfirm': 'xAI 付费账户不提供此账单额度接口。是否发送极短的 grok-4.5 请求检测可用性?这可能产生少量费用,且不会返回剩余额度。',
'quota.service.window.sevenDayFable': '7 天 Fable 窗口',
'quota.service.xaiPaidHealth': '付费 API 可用',
'quota.service.xaiPaidHealthDetail': '极短对话探测成功;上游未提供剩余额度',
'quota.service.error.xaiProbeRequired': '付费 API 可用性探测需要确认,尚未发送计费请求',
'quota.service.error.resetCreditsInvalid': '重置次数接口返回了无法识别的数据',
'quota.confirm.title': '确认重置“{name}”的 Codex 额度吗?',
'quota.confirm.cost': '本操作将消耗 1 次主动重置机会。',
'quota.confirm.available': '当前可用:{count} 次',
+12 -3
View File
@@ -38,6 +38,7 @@ import {
import {
captureQuotaCacheGeneration,
commitQuotaCacheIfCurrent,
getQuotaCacheSnapshot,
pruneQuotaCache,
updateQuotaCache,
useQuotaCache,
@@ -57,6 +58,7 @@ import {
type OAuthModelDefinition,
} from '../services/oauthModels';
import { getCurrentLocale, translate, useI18n } from '../i18n';
import { formatQuotaReset, useQuotaClock } from '../services/quotaTime';
type AuthFile = Record<string, unknown>;
@@ -102,6 +104,7 @@ const statusText = (file: AuthFile) => {
function AuthFileQuotaSummary({ quota }: { quota: QuotaState }) {
const { locale, t } = useI18n();
const now = useQuotaClock() + (quota.serverTimeOffsetMs ?? 0);
if (quota.status === 'loading') {
return (
<div className="auth-file-quota loading">
@@ -124,18 +127,23 @@ function AuthFileQuotaSummary({ quota }: { quota: QuotaState }) {
<div className="auth-file-quota" aria-label={t('authFiles.quota.aria')}>
{quota.plan ? <span className="auth-file-quota-plan">{quota.plan}</span> : null}
{quota.rows.length > 0 ? quota.rows.map((row, index) => {
const detail = [row.detail, row.reset].filter(Boolean).join(' · ');
const reset = formatQuotaReset(row.resetAtMs, row.reset, locale, now);
const detail = [row.detail, reset].filter(Boolean).join(' · ');
return (
<span className="auth-file-quota-item" key={`${row.label}-${index}`} title={detail || undefined}>
<span>{row.label}</span>
<strong>{row.remainingPercent === null ? '—' : `${Math.round(row.remainingPercent)}%`}</strong>
{row.reset ? <small>{row.reset}</small> : null}
{reset ? <small>{reset}</small> : null}
{row.remainingPercent === null && row.detail ? <small>{row.detail}</small> : null}
</span>
);
}) : <span className="auth-file-quota-empty">{t('authFiles.quota.empty')}</span>}
{quota.resetCredits !== undefined ? (
<span className="auth-file-quota-credit">{t('authFiles.quota.resets', { count: quota.resetCredits })}</span>
) : null}
{quota.resetCreditsApplicable !== undefined ? <span className="auth-file-quota-credit">{t('quota.resetApplicable', { count: quota.resetCreditsApplicable })}</span> : null}
{quota.subscriptionActiveUntil ? <span className="auth-file-quota-credit">{t('quota.subscriptionExpiry', { time: formatQuotaTimestamp(quota.subscriptionActiveUntil, locale) })}</span> : null}
{quota.resetCreditsError ? <small>{t('quota.resetCreditsWarning', { error: quota.resetCreditsError })}</small> : null}
{quota.resetCreditsEarliestExpiry ? (
<span className="auth-file-quota-credit">{t('authFiles.quota.expiry', { time: formatQuotaTimestamp(quota.resetCreditsEarliestExpiry, locale) })}</span>
) : null}
@@ -205,9 +213,10 @@ export function AuthFileManagementPage() {
const refreshQuota = async (file: AuthFile) => {
if (readBoolean(file, 'disabled')) return;
const key = quotaKey(file);
if (getQuotaCacheSnapshot()[key]?.status === 'loading') return;
const cacheGeneration = captureQuotaCacheGeneration();
updateQuotaCache((current) => ({ ...current, [key]: { status: 'loading', rows: [] } }));
const result = await loadQuota(file);
const result = await loadQuota(file, { confirmXaiPaidProbe: () => window.confirm(t('quota.xaiProbeConfirm')) });
commitQuotaCacheIfCurrent(cacheGeneration, () => {
updateQuotaCache((current) => ({ ...current, [key]: result }));
});
+39 -13
View File
@@ -6,6 +6,7 @@ import codexIcon from '../assets/icons/codex.svg';
import grokIcon from '../assets/icons/grok.svg';
import kimiIcon from '../assets/icons/kimi-light.svg';
import { managementApi, readBoolean, responseList } from '../services/managementApi';
import { formatQuotaReset, useQuotaClock } from '../services/quotaTime';
import {
consumeCodexResetCredit,
fileName,
@@ -21,6 +22,7 @@ import {
import {
captureQuotaCacheGeneration,
commitQuotaCacheIfCurrent,
getQuotaCacheSnapshot,
pruneQuotaCache,
updateQuotaCache,
useQuotaCache,
@@ -46,17 +48,17 @@ export function QuotaPage() {
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState('');
const querying = Object.values(quotas).some((quota) => quota.status === 'loading');
const loadFiles = useCallback(async () => {
setLoading(true);
setError('');
try {
const payload = await managementApi.get('/auth-files');
const nextFiles = dedupeAuthFiles(responseList(payload, 'files')).filter(
(file) => !readBoolean(file, 'disabled') && providerForFile(file),
);
const allFiles = dedupeAuthFiles(responseList(payload, 'files'));
const nextFiles = allFiles.filter((file) => !readBoolean(file, 'disabled') && providerForFile(file));
setFiles(nextFiles);
const validQuotaKeys = new Set(nextFiles.map(quotaKey));
const validQuotaKeys = new Set(allFiles.map(quotaKey));
pruneQuotaCache(validQuotaKeys);
updateQuotaCache((current) => {
const next = { ...current };
@@ -79,15 +81,17 @@ export function QuotaPage() {
const refreshOne = useCallback(async (file: AuthFile) => {
const key = quotaKey(file);
if (getQuotaCacheSnapshot()[key]?.status === 'loading') return;
const cacheGeneration = captureQuotaCacheGeneration();
updateQuotaCache((current) => ({ ...current, [key]: { status: 'loading', rows: [] } }));
const result = await loadQuota(file);
const result = await loadQuota(file, { confirmXaiPaidProbe: () => window.confirm(t('quota.xaiProbeConfirm')) });
commitQuotaCacheIfCurrent(cacheGeneration, () => {
updateQuotaCache((current) => ({ ...current, [key]: result }));
});
}, []);
}, [t]);
const resetCodexQuota = useCallback(async (file: AuthFile, quota: QuotaState) => {
if (getQuotaCacheSnapshot()[quotaKey(file)]?.status === 'loading') return;
const confirmed = window.confirm([
t('quota.confirm.title', { name: fileName(file) }),
'',
@@ -121,15 +125,23 @@ export function QuotaPage() {
}, [locale, t]);
const refreshAll = useCallback(async () => {
if (Object.values(getQuotaCacheSnapshot()).some((quota) => quota.status === 'loading')) return;
setRefreshing(true);
setError('');
const cacheGeneration = captureQuotaCacheGeneration();
updateQuotaCache((current) => Object.fromEntries(files.map((file) => [quotaKey(file), { ...current[quotaKey(file)], status: 'loading', rows: [] }])));
updateQuotaCache((current) => ({
...current,
...Object.fromEntries(files.map((file) => [quotaKey(file), {
...current[quotaKey(file)], status: 'loading', rows: [],
}])),
}));
try {
for (let index = 0; index < files.length; index += REFRESH_CONCURRENCY) {
const batch = files.slice(index, index + REFRESH_CONCURRENCY);
await Promise.all(batch.map(async (file) => {
const result = await loadQuota(file);
const result = await loadQuota(file, {
confirmXaiPaidProbe: () => window.confirm(`${fileName(file)}\n\n${t('quota.xaiProbeConfirm')}`),
});
commitQuotaCacheIfCurrent(cacheGeneration, () => {
updateQuotaCache((current) => ({ ...current, [quotaKey(file)]: result }));
});
@@ -138,7 +150,7 @@ export function QuotaPage() {
} finally {
setRefreshing(false);
}
}, [files]);
}, [files, t]);
const grouped = useMemo(() => {
const groups = new Map<QuotaProvider, { file: AuthFile; quota: QuotaState }[]>();
@@ -161,10 +173,10 @@ export function QuotaPage() {
<div><span>Quota</span><h1>{t('quota.title')}</h1></div>
<div className="management-heading-actions">
<span className="muted-summary">{t(files.length === 1 ? 'quota.queryableCredentials.one' : 'quota.queryableCredentials.other', { count: files.length })}</span>
<button type="button" className="secondary-button compact-button" onClick={() => void loadFiles()} disabled={loading || refreshing}>
<button type="button" className="secondary-button compact-button" onClick={() => void loadFiles()} disabled={loading || refreshing || querying}>
<RefreshCw size={16} />{t('quota.readList')}
</button>
<button type="button" className="secondary-button compact-button" onClick={() => void refreshAll()} disabled={refreshing || loading || files.length === 0}>
<button type="button" className="secondary-button compact-button" onClick={() => void refreshAll()} disabled={refreshing || loading || querying || files.length === 0}>
<RefreshCw size={16} className={refreshing ? 'spin' : ''} />{t('quota.refreshAll')}
</button>
</div>
@@ -190,6 +202,7 @@ export function QuotaPage() {
export function QuotaCard({ file, quota, onRefresh, onReset }: { file: AuthFile; quota: QuotaState; onRefresh: () => void; onReset?: () => void }) {
const { locale, t } = useI18n();
const now = useQuotaClock() + (quota.serverTimeOffsetMs ?? 0);
const provider = providerForFile(file);
const name = fileName(file);
const disabled = readBoolean(file, 'disabled');
@@ -199,8 +212,21 @@ export function QuotaCard({ file, quota, onRefresh, onReset }: { file: AuthFile;
{quota.status === 'idle' ? <div className="quota-card-message"><span>{disabled ? t('quota.fileDisabled') : t('quota.notFetched')}</span><button type="button" className="secondary-button compact-button" onClick={onRefresh} disabled={disabled}>{disabled ? t('quota.disabled') : t('quota.fetch')}</button></div> : null}
{quota.status === 'loading' ? <div className="quota-card-message"><LoaderCircle size={18} className="spin" />{t('quota.querying')}</div> : null}
{quota.status === 'error' ? <div className="quota-card-error"><AlertCircle size={18} />{quota.error}</div> : null}
{quota.status === 'success' && provider === 'codex' ? <div className="quota-reset-credit-summary"><span>{t('quota.resetCredits')} <strong>{quota.resetCredits ?? '—'}</strong></span><span>{t('quota.earliestExpiry')} <strong>{formatQuotaTimestamp(quota.resetCreditsEarliestExpiry, locale)}</strong></span></div> : null}
{quota.status === 'success' ? <div className="quota-row-list">{quota.rows.map((row, index) => <div className="real-quota-row" key={`${row.label}-${index}`}><div><span>{row.label}</span><strong>{row.remainingPercent === null ? '—' : t('quota.remaining', { percent: Math.round(row.remainingPercent) })}</strong></div><div className="real-quota-track"><span style={{ width: `${Math.max(0, Math.min(100, row.remainingPercent ?? 0))}%` }} /></div><small>{row.detail ?? ''}{row.reset ? `${row.detail ? ' · ' : ''}${row.reset}` : ''}</small></div>)}</div> : null}
{quota.status === 'success' && provider === 'codex' ? <div className="quota-reset-credit-summary">
<span>{t('quota.resetCredits')} <strong>{quota.resetCredits ?? '—'}</strong></span>
{quota.resetCreditsApplicable !== undefined ? <span>{t('quota.resetApplicable', { count: quota.resetCreditsApplicable })}</span> : null}
<span>{t('quota.earliestExpiry')} <strong>{formatQuotaTimestamp(quota.resetCreditsEarliestExpiry, locale)}</strong></span>
{quota.subscriptionActiveUntil ? <span>{t('quota.subscriptionExpiry', { time: formatQuotaTimestamp(quota.subscriptionActiveUntil, locale) })}</span> : null}
{quota.resetCreditsError ? <small>{t('quota.resetCreditsWarning', { error: quota.resetCreditsError })}</small> : null}
</div> : null}
{quota.status === 'success' ? <div className="quota-row-list">{quota.rows.map((row, index) => {
const reset = formatQuotaReset(row.resetAtMs, row.reset, locale, now);
return <div className="real-quota-row" key={`${row.label}-${index}`}>
<div><span>{row.label}</span><strong>{row.remainingPercent === null ? '—' : t('quota.remaining', { percent: Math.round(row.remainingPercent) })}</strong></div>
{row.remainingPercent !== null ? <div className="real-quota-track"><span style={{ width: `${Math.max(0, Math.min(100, row.remainingPercent))}%` }} /></div> : null}
<small>{[row.detail, reset].filter(Boolean).join(' · ')}</small>
</div>;
})}</div> : null}
</article>
);
}
+74
View File
@@ -0,0 +1,74 @@
import { isRecord, readString } from './managementApi';
export const decodeQuotaToken = (value: unknown): Record<string, unknown> | null => {
if (isRecord(value)) return value;
if (typeof value !== 'string' || !value.trim()) return null;
try {
const parsed = JSON.parse(value);
if (isRecord(parsed)) return parsed;
} catch { /* Try the JWT payload next. */ }
const segment = value.trim().split('.')[1];
if (!segment) return null;
try {
const normalized = segment.replace(/-/g, '+').replace(/_/g, '/')
.padEnd(Math.ceil(segment.length / 4) * 4, '=');
const bytes = Uint8Array.from(atob(normalized), (character) => character.charCodeAt(0));
const parsed = JSON.parse(new TextDecoder().decode(bytes));
return isRecord(parsed) ? parsed : null;
} catch {
return null;
}
};
export const quotaAuthRecords = (file: Record<string, unknown>) =>
[file, file.metadata, file.attributes].filter(isRecord);
export const codexMetadataFor = (file: Record<string, unknown>) => {
const records = quotaAuthRecords(file).flatMap((record) => {
const tokens = [record.id_token, record.idToken].flatMap((token) => {
const payload = decodeQuotaToken(token);
if (!payload) return [];
return [payload, payload['https://api.openai.com/auth']].filter(isRecord);
});
return [record, ...tokens];
});
const first = (...keys: string[]) => records.map((record) => readString(record, ...keys)).find(Boolean);
const expiry = first(
'chatgpt_subscription_active_until', 'chatgptSubscriptionActiveUntil',
'subscription_active_until', 'subscriptionActiveUntil',
) || records.map((record) => readString(record.subscription, 'active_until', 'activeUntil')).find(Boolean);
return {
accountId: first('chatgpt_account_id', 'chatgptAccountId', 'account_id', 'accountId') || '',
plan: first('plan_type', 'planType', 'chatgpt_plan_type', 'chatgptPlanType')?.toLowerCase(),
subscriptionActiveUntil: expiry && expiry !== '0' ? expiry : undefined,
};
};
export const antigravityProjectFor = (file: Record<string, unknown>): string => {
const records = [...quotaAuthRecords(file), file.installed, file.web].filter(isRecord);
return records.map((record) => readString(record, 'project_id', 'projectId', 'gemini_virtual_project'))
.find(Boolean) || '';
};
export const isPaidXaiFile = (file: Record<string, unknown>): boolean => {
const records: Record<string, unknown>[] = [];
const visited = new Set<Record<string, unknown>>();
const visit = (value: unknown, depth: number) => {
if (!isRecord(value) || visited.has(value) || depth > 2) return;
visited.add(value);
records.push(value);
for (const key of ['metadata', 'attributes', 'oauth', 'raw', 'credential', 'auth']) {
visit(value[key], depth + 1);
}
};
visit(file, 0);
const usingApi = records.some((record) =>
['true', '1', 'yes', 'y', 'on'].includes(String(record.using_api ?? record.usingApi).trim().toLowerCase()));
if (usingApi && records.some((record) => readString(record, 'prefix').toLowerCase() === 'paid')) return true;
return records.some((record) => ['access_token', 'accessToken', 'id_token', 'idToken', 'token'].some((key) => {
const payload = decodeQuotaToken(record[key]);
if (!payload) return false;
return Object.entries(payload).some(([claim, tier]) =>
/(?:^|[/:])tier$/i.test(claim) && (typeof tier === 'number' || typeof tier === 'string') && Number(tier) >= 1);
}));
};
+220 -123
View File
@@ -6,6 +6,8 @@ import {
readString,
} from './managementApi';
import { authFileName } from './authFiles';
import { antigravityProjectFor, codexMetadataFor, isPaidXaiFile } from './quotaMetadata';
import { quotaResetFor, quotaResetInstant } from './quotaTime';
import { getCurrentLocale, translate, type AppLocale } from '../i18n';
const quotaText = (
@@ -20,6 +22,7 @@ export type QuotaRow = {
label: string;
remainingPercent: number | null;
reset?: string;
resetAtMs?: number;
detail?: string;
};
export type QuotaState = {
@@ -28,7 +31,11 @@ export type QuotaState = {
error?: string;
plan?: string;
resetCredits?: number;
resetCreditsApplicable?: number;
resetCreditsError?: string;
resetCreditsEarliestExpiry?: string;
subscriptionActiveUntil?: string;
serverTimeOffsetMs?: number;
fetchedAt?: number;
};
@@ -60,7 +67,7 @@ const headersByProvider: Record<QuotaProvider, Record<string, string>> = {
codex: {
Authorization: 'Bearer $TOKEN$',
'Content-Type': 'application/json',
'User-Agent': 'codex_cli_rs/0.76.0 (Debian 13.0.0; x86_64) WindowsTerminal',
'User-Agent': 'codex-tui/0.149.1 (Mac OS 26.5.2; arm64) iTerm.app/3.6.11 (codex-tui; 0.149.1)',
},
kimi: { Authorization: 'Bearer $TOKEN$' },
xai: {
@@ -78,7 +85,8 @@ const headersByProvider: Record<QuotaProvider, Record<string, string>> = {
};
export const providerForFile = (file: AuthFile): QuotaProvider | null => {
const value = readString(file, 'provider', 'type', 'account_type').toLowerCase();
const value = readString(file, 'provider', 'type', 'account_type').toLowerCase().replace(/_/g, '-');
if (value === 'x-ai' || value === 'grok') return 'xai';
if (value === 'anthropic') return 'claude';
if (value === 'anti-gravity') return 'antigravity';
return ['claude', 'codex', 'kimi', 'xai', 'antigravity'].includes(value)
@@ -102,7 +110,7 @@ const parseBody = (value: unknown): unknown => {
const numberValue = (value: unknown): number | null => {
if (isRecord(value) && 'val' in value) return numberValue(value.val);
if (value === null || value === undefined || typeof value === 'boolean') return null;
if (typeof value !== 'number' && typeof value !== 'string') return null;
if (typeof value === 'string' && !value.trim()) return null;
const parsed = typeof value === 'number' ? value : Number(value);
return Number.isFinite(parsed) ? parsed : null;
@@ -141,8 +149,9 @@ const formatDateTime = (date: Date): string | undefined => {
export const formatQuotaTimestamp = (value: string | undefined, locale: AppLocale = 'zh-CN'): string => {
if (!value) return '—';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return '—';
const ms = quotaResetInstant(value);
if (ms === undefined) return '—';
const date = new Date(ms);
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: '2-digit',
@@ -154,13 +163,8 @@ export const formatQuotaTimestamp = (value: string | undefined, locale: AppLocal
};
const absoluteResetLabel = (value: unknown): string | undefined => {
if (value === null || value === undefined || value === '') return undefined;
const numeric = numberValue(value);
const numericText = typeof value === 'string' && /^\d+(?:\.\d+)?$/.test(value.trim());
const date = typeof value === 'number' || numericText
? new Date((numeric ?? 0) < 1e12 ? (numeric ?? 0) * 1000 : (numeric ?? 0))
: new Date(String(value));
return formatDateTime(date);
const ms = quotaResetInstant(value);
return ms === undefined ? undefined : formatDateTime(new Date(ms));
};
const relativeResetLabel = (value: unknown): string | undefined => {
@@ -213,7 +217,6 @@ const codexWindowLabel = (
duration: number | null,
prefix: string,
kind: 'primary' | 'secondary',
teamPlan: boolean,
) => {
if (duration === FIVE_HOUR_SECONDS) return `${prefix}${quotaText('quota.service.limit.fiveHours')}`;
if (duration === WEEK_SECONDS) return `${prefix}${quotaText('quota.service.limit.week')}`;
@@ -224,7 +227,7 @@ const codexWindowLabel = (
return `${prefix}${quotaText('quota.service.limit.duration', { duration: formatWindowDuration(duration) })}`;
}
if (kind === 'primary') return `${prefix}${quotaText('quota.service.limit.fiveHours')}`;
return `${prefix}${quotaText(teamPlan ? 'quota.service.limit.month' : 'quota.service.limit.week')}`;
return `${prefix}${quotaText('quota.service.limit.week')}`;
};
const codexWindowRows = (value: Record<string, unknown>): QuotaRow[] => {
@@ -236,10 +239,17 @@ const codexWindowRows = (value: Record<string, unknown>): QuotaRow[] => {
}> = [];
const addRateLimit = (rawRateLimit: unknown, prefix: string) => {
if (!isRecord(rawRateLimit)) return;
windows.push(
const entries: typeof windows = [
{ raw: rawRateLimit.primary_window ?? rawRateLimit.primaryWindow, kind: 'primary', prefix, source: rawRateLimit },
{ raw: rawRateLimit.secondary_window ?? rawRateLimit.secondaryWindow, kind: 'secondary', prefix, source: rawRateLimit },
);
];
const order = ({ raw, kind }: typeof entries[number]) => {
const duration = isRecord(raw) ? numberValue(raw.limit_window_seconds ?? raw.limitWindowSeconds) : null;
if (duration === FIVE_HOUR_SECONDS) return 0;
if (duration === WEEK_SECONDS || (duration !== null && duration >= MIN_MONTH_SECONDS && duration <= MAX_MONTH_SECONDS)) return 1;
return kind === 'primary' ? 0 : 1;
};
windows.push(...entries.sort((a, b) => order(a) - order(b)));
};
addRateLimit(value.rate_limit ?? value.rateLimit, '');
@@ -257,15 +267,17 @@ const codexWindowRows = (value: Record<string, unknown>): QuotaRow[] => {
});
}
const teamPlan = readString(value, 'plan_type', 'planType').toLowerCase() === 'team';
return windows.map(({ raw, kind, prefix, source }): QuotaRow | null => {
if (!isRecord(raw)) return null;
const duration = numberValue(raw.limit_window_seconds ?? raw.limitWindowSeconds);
const reached = source.limit_reached === true || source.limitReached === true || source.allowed === false;
const resetAtMs = quotaResetFor(raw, ['reset_at', 'resetAt'], ['reset_after_seconds', 'resetAfterSeconds']);
return {
label: codexWindowLabel(duration, prefix, kind, teamPlan),
remainingPercent: remainingFromUsedPercent(raw.used_percent ?? raw.usedPercent) ?? (reached ? 0 : null),
label: codexWindowLabel(duration, prefix, kind),
remainingPercent: remainingFromUsedPercent(raw.used_percent ?? raw.usedPercent)
?? (reached && resetAtMs !== undefined ? 0 : null),
reset: codexResetLabel(raw),
resetAtMs,
};
}).filter((row): row is QuotaRow => row !== null);
};
@@ -285,7 +297,7 @@ export const codexResetCreditsFor = (payload: unknown): number | undefined => {
export const codexResetCreditDetailsFor = (
payload: unknown,
nowMs = Date.now(),
): { availableCount?: number; earliestExpiry?: string } => {
): { availableCount?: number; applicableAvailableCount?: number; earliestExpiry?: string } => {
const value = parseBody(payload);
if (!isRecord(value)) return {};
const credits = Array.isArray(value.credits)
@@ -297,9 +309,14 @@ export const codexResetCreditDetailsFor = (
)
: [];
const availableCount = numberValue(value.available_count ?? value.availableCount);
const earliestExpiry = credits
const applicableCount = numberValue(value.applicable_available_count ?? value.applicableAvailableCount);
const validCredits = credits.filter((credit) => {
const expiry = quotaResetInstant(credit.expires_at ?? credit.expiresAt);
return expiry !== undefined && expiry > nowMs;
});
const earliestExpiry = validCredits
.map((credit) => readString(credit, 'expires_at', 'expiresAt'))
.map((expiresAt) => ({ expiresAt, expiresAtMs: new Date(expiresAt).getTime() }))
.map((expiresAt) => ({ expiresAt, expiresAtMs: quotaResetInstant(expiresAt) ?? NaN }))
.filter((credit) =>
credit.expiresAt
&& Number.isFinite(credit.expiresAtMs)
@@ -308,9 +325,11 @@ export const codexResetCreditDetailsFor = (
.sort((left, right) => left.expiresAtMs - right.expiresAtMs)[0]?.expiresAt;
return {
// An empty detail list is not an explicit zero; keep the usage-summary fallback.
availableCount: availableCount === null
? credits.length || undefined
? validCredits.length || undefined
: Math.max(0, Math.floor(availableCount)),
...(applicableCount === null ? {} : { applicableAvailableCount: Math.max(0, Math.floor(applicableCount)) }),
earliestExpiry,
};
};
@@ -329,19 +348,35 @@ export const quotaRowsFor = (provider: QuotaProvider, payload: unknown): QuotaRo
seven_day_opus: quotaText('quota.service.window.sevenDayOpus'),
seven_day_sonnet: quotaText('quota.service.window.sevenDaySonnet'),
seven_day_cowork: quotaText('quota.service.window.sevenDayCowork'),
iguana_necktie: 'Iguana Necktie',
iguana_necktie: quotaText('quota.service.window.sevenDayFable'),
};
const rows = Object.entries(value)
.filter(([key]) => key in labels)
.map(([key, raw]): QuotaRow | null => {
if (!isRecord(raw)) return null;
const fableCandidates = (Array.isArray(value.limits) ? value.limits : []).filter(isRecord)
.filter((limit) => {
const scope = isRecord(limit.scope) ? limit.scope : null;
const name = readString(scope?.model, 'display_name', 'displayName').toLowerCase();
return readString(limit, 'kind').toLowerCase() === 'weekly_scoped'
&& ['fable', 'fable 5'].includes(name) && numberValue(limit.percent) !== null;
});
const fable = fableCandidates.find((limit) => limit.is_active === true) ?? fableCandidates[0];
const rows = Object.entries(labels)
.filter(([key]) => key !== 'iguana_necktie' || !fable)
.map(([key]): QuotaRow | null => {
const raw = value[key];
if (!isRecord(raw) || !('utilization' in raw)) return null;
return {
label: labels[key],
remainingPercent: remainingFromUsedPercent(raw.utilization),
reset: absoluteResetLabel(raw.resets_at ?? raw.resetsAt),
resetAtMs: quotaResetFor(raw, ['resets_at', 'resetsAt']),
};
})
.filter((row): row is QuotaRow => row !== null);
if (fable) rows.push({
label: quotaText('quota.service.window.sevenDayFable'),
remainingPercent: remainingFromUsedPercent(fable.percent),
reset: absoluteResetLabel(fable.resets_at ?? fable.resetsAt),
resetAtMs: quotaResetFor(fable, ['resets_at', 'resetsAt']),
});
const extraUsage = isRecord(value.extra_usage)
? value.extra_usage
: isRecord(value.extraUsage)
@@ -369,11 +404,10 @@ export const quotaRowsFor = (provider: QuotaProvider, payload: unknown): QuotaRo
}
if (provider === 'kimi') {
const items: unknown[] = [];
const items: unknown[] = Array.isArray(value.limits) ? [...value.limits] : [];
if (isRecord(value.usage)) {
items.push({ ...value.usage, label: quotaText('quota.service.weekly') });
items.push({ ...value.usage, label: readString(value.usage, 'name', 'title') || quotaText('quota.service.weekly') });
}
if (Array.isArray(value.limits)) items.push(...value.limits);
return items
.map((raw, index): QuotaRow | null => {
if (!isRecord(raw)) return null;
@@ -385,19 +419,21 @@ export const quotaRowsFor = (provider: QuotaProvider, payload: unknown): QuotaRo
if (usedValue === null && limit === null) return null;
const window = isRecord(raw.window) ? raw.window : null;
const duration = numberValue(window?.duration ?? raw.duration ?? detail.duration);
const unit = readString(window, 'timeUnit', 'time_unit')
const unit = (readString(window, 'timeUnit', 'time_unit')
|| readString(raw, 'timeUnit', 'time_unit')
|| readString(detail, 'timeUnit', 'time_unit');
|| readString(detail, 'timeUnit', 'time_unit')).toLowerCase().replace(/^time_unit_/, '');
const durationText = duration !== null && duration > 0
? unit.toLowerCase().startsWith('day')
? quotaText('quota.service.duration.days', { count: duration })
: unit.toLowerCase().startsWith('hour')
? quotaText('quota.service.duration.hours', { count: duration })
: unit.toLowerCase().startsWith('second')
? quotaText('quota.service.duration.seconds', { count: duration })
: duration % 60 === 0
? quotaText('quota.service.duration.hours', { count: duration / 60 })
: quotaText('quota.service.duration.minutes', { count: duration })
? unit.startsWith('week')
? quotaText('quota.service.duration.days', { count: duration * 7 })
: unit.startsWith('day')
? quotaText('quota.service.duration.days', { count: duration })
: unit.startsWith('hour')
? quotaText('quota.service.duration.hours', { count: duration })
: unit.startsWith('second')
? quotaText('quota.service.duration.seconds', { count: duration })
: duration % 60 === 0
? quotaText('quota.service.duration.hours', { count: duration / 60 })
: quotaText('quota.service.duration.minutes', { count: duration })
: '';
const durationLabel = durationText
? quotaText('quota.service.window.duration', { duration: durationText })
@@ -420,6 +456,7 @@ export const quotaRowsFor = (provider: QuotaProvider, payload: unknown): QuotaRo
detail.reset_at ?? detail.resetAt ?? detail.reset_time ?? detail.resetTime,
)
?? relativeResetLabel(detail.reset_in ?? detail.resetIn ?? detail.ttl),
resetAtMs: quotaResetFor(detail, ['reset_at', 'resetAt', 'reset_time', 'resetTime'], ['reset_in', 'resetIn', 'ttl']),
detail: limit === null ? undefined : `${usedValue ?? 0} / ${limit}`,
};
})
@@ -427,6 +464,11 @@ export const quotaRowsFor = (provider: QuotaProvider, payload: unknown): QuotaRo
}
if (provider === 'xai') {
if (value.mode === 'paid-health') return [{
label: quotaText('quota.service.xaiPaidHealth'),
remainingPercent: null,
detail: quotaText('quota.service.xaiPaidHealthDetail'),
}];
const payloads = isRecord(value.weekly) || isRecord(value.monthly)
? [value.weekly, value.monthly]
: [value];
@@ -443,11 +485,13 @@ export const quotaRowsFor = (provider: QuotaProvider, payload: unknown): QuotaRo
const periodType = readString(currentPeriod, 'type').toLowerCase();
const weeklyUsed = numberValue(config.credit_usage_percent ?? config.creditUsagePercent);
if (weeklyUsed !== null || periodType.includes('week')) {
const productItems = config.product_usage ?? config.productUsage;
if (weeklyUsed !== null || periodType.includes('week') || (Array.isArray(productItems) && productItems.length > 0)) {
rows.push({
label: quotaText('quota.service.weekly'),
remainingPercent: remainingFromUsedPercent(weeklyUsed),
reset: absoluteResetLabel(currentPeriod?.end),
resetAtMs: currentPeriod ? quotaResetFor(currentPeriod, ['end']) : undefined,
});
const productUsage = Array.isArray(config.product_usage)
? config.product_usage
@@ -463,6 +507,7 @@ export const quotaRowsFor = (provider: QuotaProvider, payload: unknown): QuotaRo
item.usage_percent ?? item.usagePercent,
),
reset: absoluteResetLabel(currentPeriod?.end),
resetAtMs: currentPeriod ? quotaResetFor(currentPeriod, ['end']) : undefined,
});
});
}
@@ -482,6 +527,7 @@ export const quotaRowsFor = (provider: QuotaProvider, payload: unknown): QuotaRo
label: quotaText('quota.service.monthlyIncluded'),
remainingPercent: clampPercent(remaining),
reset: absoluteResetLabel(config.billing_period_end ?? config.billingPeriodEnd),
resetAtMs: quotaResetFor(config, ['billing_period_end', 'billingPeriodEnd']),
detail: limit !== null
? `${formatUsdFromCents(Math.max(0, limit - (includedUsed ?? 0)))} / ${formatUsdFromCents(limit)}`
: undefined,
@@ -499,6 +545,7 @@ export const quotaRowsFor = (provider: QuotaProvider, payload: unknown): QuotaRo
onDemandUsed === null ? null : ((onDemandCap - onDemandUsed) / onDemandCap) * 100,
),
reset: absoluteResetLabel(config.billing_period_end ?? config.billingPeriodEnd),
resetAtMs: quotaResetFor(config, ['billing_period_end', 'billingPeriodEnd']),
detail: `${formatUsdFromCents(Math.max(0, onDemandCap - (onDemandUsed ?? 0)))} / ${formatUsdFromCents(onDemandCap)}`,
});
}
@@ -513,10 +560,16 @@ export const quotaRowsFor = (provider: QuotaProvider, payload: unknown): QuotaRo
});
}
const groups = Array.isArray(value.groups) ? value.groups : [];
const nested = parseBody(value.body);
const summary = !Array.isArray(value.groups) && isRecord(nested) ? nested : value;
const groups = Array.isArray(summary.groups) ? summary.groups : [];
return groups.flatMap((group) => {
if (!isRecord(group) || !Array.isArray(group.buckets)) return [];
const buckets = group.buckets;
const order = (bucket: unknown) => {
const window = readString(bucket, 'window').toLowerCase();
return ['5h', 'five-hour', 'five_hour'].includes(window) ? 0 : ['weekly', 'week'].includes(window) ? 1 : 2;
};
const buckets = [...group.buckets].sort((a, b) => order(a) - order(b));
const groupLabel = readString(group, 'display_name', 'displayName')
|| quotaText('quota.service.quota');
const groupDescription = readString(group, 'description');
@@ -533,6 +586,7 @@ export const quotaRowsFor = (provider: QuotaProvider, payload: unknown): QuotaRo
label: label || quotaText('quota.service.quota.numbered', { index: index + 1 }),
remainingPercent: remaining * 100,
reset: absoluteResetLabel(bucket.reset_time ?? bucket.resetTime),
resetAtMs: quotaResetFor(bucket, ['reset_time', 'resetTime']),
detail: readString(bucket, 'description') || groupDescription || undefined,
};
})
@@ -540,66 +594,18 @@ export const quotaRowsFor = (provider: QuotaProvider, payload: unknown): QuotaRo
});
};
const resolveProjectId = (file: AuthFile): string =>
readString(file, 'project_id', 'projectId');
const decodeJwtPayload = (value: unknown): Record<string, unknown> | null => {
if (isRecord(value)) return value;
if (typeof value !== 'string') return null;
const trimmed = value.trim();
if (!trimmed) return null;
try {
const parsed = JSON.parse(trimmed);
if (isRecord(parsed)) return parsed;
} catch {
// Continue with JWT payload decoding.
}
const segment = trimmed.split('.')[1];
if (!segment) return null;
try {
const normalized = segment.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(segment.length / 4) * 4, '=');
const parsed = JSON.parse(atob(normalized));
return isRecord(parsed) ? parsed : null;
} catch {
return null;
}
};
const codexAccountIdFromRecord = (record: Record<string, unknown>): string => {
const direct = readString(
record,
'chatgpt_account_id',
'chatgptAccountId',
'account_id',
'accountId',
);
const resolveProjectId = async (file: AuthFile): Promise<string> => {
const direct = antigravityProjectFor(file);
if (direct) return direct;
for (const container of [record.metadata, record.attributes]) {
if (!isRecord(container)) continue;
const nestedDirect = readString(
container,
'chatgpt_account_id',
'chatgptAccountId',
'account_id',
'accountId',
);
if (nestedDirect) return nestedDirect;
try {
const payload = parseBody(await managementApi.get('/auth-files/download', { name: fileName(file) }));
return isRecord(payload) ? antigravityProjectFor(payload) : '';
} catch {
return '';
}
for (const candidate of [record.id_token, record.idToken]) {
const payload = decodeJwtPayload(candidate);
const nested = payload && isRecord(payload['https://api.openai.com/auth']) ? payload['https://api.openai.com/auth'] : payload;
const accountId = nested
? readString(nested, 'chatgpt_account_id', 'chatgptAccountId', 'account_id', 'accountId')
: '';
if (accountId) return accountId;
}
return '';
};
const resolveCodexAccountId = (file: AuthFile): string =>
codexAccountIdFromRecord(file);
const resolveCodexAccountId = (file: AuthFile): string => codexMetadataFor(file).accountId;
const xaiUserIdFromRecord = (record: Record<string, unknown>): string => {
const nestedRecords = [record, record.metadata, record.attributes]
@@ -625,6 +631,8 @@ const requestQuotaPayload = async (
header: Record<string, string>,
method: 'GET' | 'POST' = 'GET',
data?: string,
timeoutMs?: number,
responseClock?: { serverTimeOffsetMs?: number },
) => {
const response = await managementApi.post<Record<string, unknown>>('/api-call', {
authIndex,
@@ -632,17 +640,44 @@ const requestQuotaPayload = async (
url,
header,
data,
});
}, { timeoutMs });
const status = Number(response.status_code ?? response.statusCode ?? 0);
if (status < 200 || status >= 300) {
throw new Error(apiCallErrorMessage(response));
}
if (responseClock) {
const header = isRecord(response.header) ? response.header : {};
const raw = Object.entries(header).find(([key]) => key.toLowerCase() === 'date')?.[1];
const serverTime = quotaResetInstant(Array.isArray(raw) ? raw[0] : raw);
responseClock.serverTimeOffsetMs = serverTime === undefined ? undefined : serverTime - Date.now();
}
return parseBody(response.body ?? response.bodyText);
};
const callXaiQuota = async (file: AuthFile): Promise<unknown> => {
export type QuotaLoadOptions = {
// Paid health checks send a real (potentially billable) completion. Never do this silently.
confirmXaiPaidProbe?: () => boolean | Promise<boolean>;
};
const callXaiPaidHealth = async (authIndex: string, options: QuotaLoadOptions): Promise<unknown> => {
if (!await options.confirmXaiPaidProbe?.()) throw new Error(quotaText('quota.service.error.xaiProbeRequired'));
const header = { Authorization: 'Bearer $TOKEN$', accept: 'application/json' };
const [profile, chat] = await Promise.allSettled([
requestQuotaPayload(authIndex, 'https://api.x.ai/v1/me', header, 'GET', undefined, 15_000),
requestQuotaPayload(authIndex, 'https://api.x.ai/v1/chat/completions', {
...header, 'Content-Type': 'application/json',
}, 'POST', JSON.stringify({
model: 'grok-4.5', messages: [{ role: 'user', content: 'ping' }], max_tokens: 1, stream: false,
}), 15_000),
]);
if (chat.status === 'rejected') throw chat.reason;
return { mode: 'paid-health', plan_type: 'Paid', profile: profile.status === 'fulfilled' ? profile.value : null };
};
const callXaiQuota = async (file: AuthFile, options: QuotaLoadOptions): Promise<unknown> => {
const authIndex = normalizeAuthIndex(file.auth_index ?? file.authIndex);
if (!authIndex) throw new Error(quotaText('quota.service.error.missingAuthIndex'));
if (isPaidXaiFile(file)) return callXaiPaidHealth(authIndex, options);
const header = { ...headersByProvider.xai };
const userId = await resolveXaiUserId(file);
if (userId) header['x-userid'] = userId;
@@ -650,22 +685,28 @@ const callXaiQuota = async (file: AuthFile): Promise<unknown> => {
requestQuotaPayload(authIndex, XAI_WEEKLY_URL, header),
requestQuotaPayload(authIndex, endpointByProvider.xai, header),
]);
if (weekly.status === 'rejected' && monthly.status === 'rejected') {
throw weekly.reason;
}
return {
const payload = {
weekly: weekly.status === 'fulfilled' ? weekly.value : null,
monthly: monthly.status === 'fulfilled' ? monthly.value : null,
};
if (quotaRowsFor('xai', payload).length > 0) return payload;
const billingError = weekly.status === 'rejected' && monthly.status === 'rejected'
? weekly.reason : new Error(quotaText('quota.service.error.unrecognized'));
try {
return await callXaiPaidHealth(authIndex, options);
} catch {
// Keep the useful billing error if the alternative account mode also fails or is declined.
throw billingError;
}
};
const booleanValue = (value: unknown): boolean | null => {
if (typeof value === 'boolean') return value;
if (typeof value === 'number') return value !== 0;
if (typeof value === 'number') return Number.isFinite(value) ? value !== 0 : null;
if (typeof value === 'string') {
const normalized = value.trim().toLowerCase();
if (['true', '1', 'yes', 'on'].includes(normalized)) return true;
if (['false', '0', 'no', 'off'].includes(normalized)) return false;
if (['true', '1', 'yes', 'y', 'on'].includes(normalized)) return true;
if (['false', '0', 'no', 'n', 'off'].includes(normalized)) return false;
}
return null;
};
@@ -740,6 +781,7 @@ async function callUpstreamQuota(
file: AuthFile,
provider: QuotaProvider,
resolvedCodexAccountId?: string,
responseClock?: { serverTimeOffsetMs?: number },
): Promise<unknown> {
const authIndex = normalizeAuthIndex(file.auth_index ?? file.authIndex);
if (!authIndex) throw new Error(quotaText('quota.service.error.missingAuthIndex'));
@@ -765,6 +807,8 @@ async function callUpstreamQuota(
header,
provider === 'antigravity' ? 'POST' : 'GET',
project ? JSON.stringify({ project }) : undefined,
undefined,
provider === 'antigravity' ? responseClock : undefined,
);
if (provider === 'antigravity') {
hadSuccessfulResponse = true;
@@ -790,7 +834,7 @@ async function callUpstreamQuota(
const callCodexResetCredits = async (
file: AuthFile,
accountId: string,
): Promise<{ availableCount?: number; earliestExpiry?: string }> => {
): Promise<ReturnType<typeof codexResetCreditDetailsFor>> => {
const authIndex = normalizeAuthIndex(file.auth_index ?? file.authIndex);
if (!authIndex) throw new Error(quotaText('quota.service.error.missingResetAuthIndex'));
const header: Record<string, string> = {
@@ -800,11 +844,14 @@ const callCodexResetCredits = async (
Originator: 'Codex Desktop',
};
if (accountId) header['Chatgpt-Account-Id'] = accountId;
const payload = await requestQuotaPayload(authIndex, CODEX_RESET_CREDITS_URL, header);
const payload = await requestQuotaPayload(authIndex, CODEX_RESET_CREDITS_URL, header, 'GET', undefined, 8_000);
if (!isRecord(payload) || !['credits', 'available_count', 'availableCount', 'applicable_available_count', 'applicableAvailableCount'].some((key) => key in payload)) {
throw new Error(quotaText('quota.service.error.resetCreditsInvalid'));
}
return codexResetCreditDetailsFor(payload);
};
export async function loadQuota(file: AuthFile): Promise<QuotaState> {
async function loadQuotaSnapshot(file: AuthFile, options: QuotaLoadOptions = {}): Promise<QuotaState> {
const provider = providerForFile(file);
if (!provider) {
return {
@@ -814,17 +861,24 @@ export async function loadQuota(file: AuthFile): Promise<QuotaState> {
};
}
try {
const codexAccountId = provider === 'codex' ? await resolveCodexAccountId(file) : '';
if (booleanValue(file.disabled) === true) throw new Error(quotaText('quota.fileDisabled'));
const codexMetadata = provider === 'codex' ? codexMetadataFor(file) : undefined;
const codexAccountId = codexMetadata?.accountId || '';
const responseClock: { serverTimeOffsetMs?: number } = {};
const payloadPromise = provider === 'xai'
? callXaiQuota(file)
: callUpstreamQuota(file, provider, codexAccountId);
? callXaiQuota(file, options)
: callUpstreamQuota(file, provider, codexAccountId, responseClock);
const planPromise = provider === 'claude'
? loadClaudePlan(file)
: provider === 'antigravity'
? loadAntigravityPlan(file)
: Promise.resolve(undefined);
let resetCreditsError: string | undefined;
const resetCreditsPromise = provider === 'codex'
? callCodexResetCredits(file, codexAccountId).catch(() => null)
? callCodexResetCredits(file, codexAccountId).catch((error) => {
resetCreditsError = error instanceof Error ? error.message : String(error);
return null;
})
: Promise.resolve(null);
const [payload, detectedPlan, resetCreditDetails] = await Promise.all([
payloadPromise,
@@ -839,15 +893,24 @@ export async function loadQuota(file: AuthFile): Promise<QuotaState> {
error: quotaText('quota.service.error.unrecognized'),
};
}
const resetCredits = provider === 'codex'
? resetCreditDetails?.availableCount ?? codexResetCreditsFor(payload)
: undefined;
const usageCreditDetails = provider === 'codex' && isRecord(payload)
? codexResetCreditDetailsFor(payload.rate_limit_reset_credits ?? payload.rateLimitResetCredits)
: {};
return {
status: 'success',
rows,
plan: detectedPlan
?? readString(isRecord(payload) ? payload : {}, 'plan_type', 'planType'),
resetCredits: provider === 'codex'
? resetCreditDetails?.availableCount ?? codexResetCreditsFor(payload)
: undefined,
?? (readString(isRecord(payload) ? payload : {}, 'plan_type', 'planType') || codexMetadata?.plan),
subscriptionActiveUntil: codexMetadata?.subscriptionActiveUntil,
resetCreditsError,
resetCreditsApplicable: usageCreditDetails.applicableAvailableCount
?? resetCreditDetails?.applicableAvailableCount ?? resetCredits,
resetCredits,
resetCreditsEarliestExpiry: resetCreditDetails?.earliestExpiry,
serverTimeOffsetMs: responseClock.serverTimeOffsetMs,
fetchedAt: Date.now(),
};
} catch (error) {
@@ -868,10 +931,11 @@ const createRedeemRequestId = () => {
});
};
export async function consumeCodexResetCredit(file: AuthFile): Promise<QuotaState> {
async function consumeCodexResetCreditSnapshot(file: AuthFile): Promise<QuotaState> {
if (providerForFile(file) !== 'codex') {
throw new Error(quotaText('quota.service.error.codexResetOnly'));
}
if (booleanValue(file.disabled) === true) throw new Error(quotaText('quota.fileDisabled'));
const authIndex = normalizeAuthIndex(file.auth_index ?? file.authIndex);
if (!authIndex) throw new Error(quotaText('quota.service.error.missingConsumeAuthIndex'));
const header = {
@@ -886,5 +950,38 @@ export async function consumeCodexResetCredit(file: AuthFile): Promise<QuotaStat
'POST',
JSON.stringify({ redeem_request_id: createRedeemRequestId() }),
);
return loadQuota(file);
return loadQuotaSnapshot(file);
}
// Shared by both pages: deduplicate refreshes and serialize reset mutations per credential.
const quotaRequests = new Map<string, Promise<QuotaState>>();
const resetRequests = new Map<string, Promise<QuotaState>>();
export function loadQuota(file: AuthFile, options: QuotaLoadOptions = {}): Promise<QuotaState> {
const key = quotaKey(file);
const reset = resetRequests.get(key);
if (reset) return reset.catch((error): QuotaState => ({
status: 'error', rows: [], error: error instanceof Error ? error.message : String(error),
}));
const existing = quotaRequests.get(key);
if (existing) return existing;
const request = loadQuotaSnapshot(file, options).finally(() => {
if (quotaRequests.get(key) === request) quotaRequests.delete(key);
});
quotaRequests.set(key, request);
return request;
}
export function consumeCodexResetCredit(file: AuthFile): Promise<QuotaState> {
const key = quotaKey(file);
const existing = resetRequests.get(key);
if (existing) return existing;
const request = (async () => {
await quotaRequests.get(key);
return consumeCodexResetCreditSnapshot(file);
})().finally(() => {
if (resetRequests.get(key) === request) resetRequests.delete(key);
});
resetRequests.set(key, request);
return request;
}
+76
View File
@@ -0,0 +1,76 @@
import { useSyncExternalStore } from 'react';
import { getCurrentLocale, translate, type AppLocale } from '../i18n';
export const quotaResetInstant = (value: unknown): number | undefined => {
if (typeof value !== 'number' && typeof value !== 'string') return undefined;
if (typeof value === 'string' && !value.trim()) return undefined;
const numeric = Number(value);
const ms = Number.isFinite(numeric)
? numeric > 0 ? numeric < 1e12 ? numeric * 1000 : numeric : NaN
: Date.parse(String(value));
return Number.isFinite(ms) && Number.isFinite(new Date(ms).getTime()) ? ms : undefined;
};
export const quotaResetFor = (
record: Record<string, unknown>,
absoluteKeys: string[],
relativeKeys: string[] = [],
nowMs = Date.now(),
): number | undefined => {
for (const key of absoluteKeys) {
const ms = quotaResetInstant(record[key]);
if (ms !== undefined) return ms;
}
for (const key of relativeKeys) {
const raw = record[key];
if (typeof raw !== 'number' && typeof raw !== 'string') continue;
if (typeof raw === 'string' && !raw.trim()) continue;
const seconds = Number(raw);
const ms = nowMs + seconds * 1000;
if (Number.isFinite(seconds) && seconds >= 0 && Number.isFinite(new Date(ms).getTime())) return ms;
}
return undefined;
};
export const formatQuotaReset = (
resetAtMs: number | undefined,
fallback?: string,
locale: AppLocale = getCurrentLocale(),
nowMs = Date.now(),
): string => {
if (resetAtMs === undefined || !Number.isFinite(resetAtMs)) return fallback || '';
const absolute = new Intl.DateTimeFormat(locale, {
month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false,
}).format(resetAtMs);
const delta = resetAtMs - nowMs;
if (delta <= 0) return `${absolute} · ${translate(locale, 'quota.resetPassed')}`;
const minutes = Math.max(1, Math.ceil(delta / 60000));
const relative = new Intl.RelativeTimeFormat(locale, { numeric: 'always' });
const label = minutes >= 1440 ? relative.format(Math.ceil(minutes / 1440), 'day')
: minutes >= 60 ? relative.format(Math.ceil(minutes / 60), 'hour')
: relative.format(minutes, 'minute');
return `${absolute} · ${label}`;
};
// One clock shared by quota cards on both pages; no polling of upstream APIs.
let now = Date.now();
let timer: ReturnType<typeof setInterval> | undefined;
const listeners = new Set<() => void>();
const subscribe = (listener: () => void) => {
listeners.add(listener);
if (!timer) {
now = Date.now();
timer = setInterval(() => {
now = Date.now();
listeners.forEach((notify) => notify());
}, 60_000);
}
return () => {
listeners.delete(listener);
if (!listeners.size) {
clearInterval(timer);
timer = undefined;
}
};
};
export const useQuotaClock = () => useSyncExternalStore(subscribe, () => now, () => now);
+53
View File
@@ -0,0 +1,53 @@
import { describe, expect, it } from 'bun:test';
import { antigravityProjectFor, codexMetadataFor, isPaidXaiFile } from '../src/services/quotaMetadata';
import { providerForFile } from '../src/services/quotaService';
const jwt = (payload: unknown) => `header.${Buffer.from(JSON.stringify(payload)).toString('base64url')}.signature`;
describe('quota credential metadata', () => {
it('识别 xAI 别名,保留原有供应商别名', () => {
for (const provider of ['x_ai', 'x-ai', 'grok', 'xai', 'XAI']) {
expect(providerForFile({ provider })).toBe('xai');
}
expect(providerForFile({ type: 'anthropic' })).toBe('claude');
expect(providerForFile({ type: 'anti_gravity' })).toBe('antigravity');
});
it('Codex 支持各层的 JWT、JSON 和对象令牌以及命名空间字段', () => {
const info = {
chatgpt_account_id: 'account-中文', chatgpt_plan_type: 'PRO',
chatgpt_subscription_active_until: '2030-01-01T00:00:00Z',
};
for (const token of [info, JSON.stringify(info), jwt({ 'https://api.openai.com/auth': info })]) {
for (const file of [{ id_token: token }, { metadata: { idToken: token } }, { attributes: { id_token: token } }]) {
expect(codexMetadataFor(file)).toEqual({
accountId: 'account-中文', plan: 'pro', subscriptionActiveUntil: '2030-01-01T00:00:00Z',
});
}
}
});
it('保留 Codex 直接 account_id 和订阅对象,安全忽略坏令牌', () => {
expect(codexMetadataFor({ id_token: 'bad.jwt', account_id: 'direct', metadata: { subscription: { active_until: 1900000000 } } }))
.toEqual({ accountId: 'direct', plan: undefined, subscriptionActiveUntil: '1900000000' });
expect(codexMetadataFor({ id_token: [], subscription_active_until: 0 }).subscriptionActiveUntil).toBeUndefined();
});
it('Antigravity 支持 metadata、attributes 和下载文件的项目字段', () => {
for (const file of [
{ project_id: 'p' }, { metadata: { projectId: 'p' } },
{ attributes: { gemini_virtual_project: 'p' } },
{ installed: { project_id: 'p' } }, { web: { projectId: 'p' } },
]) expect(antigravityProjectFor(file)).toBe('p');
expect(antigravityProjectFor({})).toBe('');
});
it('单独 using_api 或 paid 前缀不能证明 xAI 是付费账户', () => {
expect(isPaidXaiFile({ using_api: true })).toBe(false);
expect(isPaidXaiFile({ prefix: 'paid' })).toBe(false);
expect(isPaidXaiFile({ usingApi: 'YES', metadata: { prefix: 'PAID' } })).toBe(true);
expect(isPaidXaiFile({ metadata: { oauth: { access_token: jwt({ 'https://x.ai/tier': 1 }) } } })).toBe(true);
expect(isPaidXaiFile({ token: jwt({ tier: 0 }) })).toBe(false);
expect(isPaidXaiFile({ token: 'bad.jwt' })).toBe(false);
});
});
+34
View File
@@ -0,0 +1,34 @@
import { describe, expect, it } from 'bun:test';
import { renderToStaticMarkup } from 'react-dom/server';
import { I18nProvider } from '../src/i18n';
import { QuotaCard } from '../src/pages/QuotaPage';
import type { QuotaState } from '../src/services/quotaService';
const render = (quota: QuotaState, provider = 'codex') => renderToStaticMarkup(
<I18nProvider><QuotaCard file={{ name: 'test.json', provider }} quota={quota} onRefresh={() => {}} onReset={() => {}} /></I18nProvider>,
);
describe('quota card rendering', () => {
it('即使当前适用次数为零,仍显示可用重置按钮、详情错误和订阅时间', () => {
const html = render({
status: 'success', rows: [], resetCredits: 2, resetCreditsApplicable: 0,
resetCreditsError: 'temporary failure', subscriptionActiveUntil: '2030-01-01T00:00:00Z',
});
expect(html).toContain('重置额度');
expect(html).toContain('当前适用:0 次');
expect(html).toContain('temporary failure');
expect(html).toContain('订阅到期');
});
it('付费 API 健康状态不画成 0% 或 100% 的额度条', () => {
const html = render({ status: 'success', rows: [{ label: '付费 API 可用', remainingPercent: null, detail: '上游未提供剩余额度' }] }, 'xai');
expect(html).toContain('上游未提供剩余额度');
expect(html).not.toContain('real-quota-track');
});
it('缓存中的原始重置时间提供动态提示,过期额度不伪造为已恢复', () => {
const html = render({ status: 'success', rows: [{ label: '5h', remainingPercent: 0, resetAtMs: Date.parse('2020-01-01T00:00:00Z') }] });
expect(html).toContain('重置时间已到,请刷新确认');
expect(html).toContain('剩余 0%');
});
});
+238
View File
@@ -0,0 +1,238 @@
import { afterEach, beforeEach, describe, expect, it, spyOn } from 'bun:test';
import { managementApi } from '../src/services/managementApi';
import { consumeCodexResetCredit, loadQuota } from '../src/services/quotaService';
const success = (body: unknown) => ({ status_code: 200, body });
const codexUsage = {
rate_limit: { primary_window: { used_percent: 25, limit_window_seconds: 18000 } },
rate_limit_reset_credits: { available_count: 2, applicable_available_count: 0 },
};
const codexFile = { name: 'codex-test.json', provider: 'codex', auth_index: 1 };
type Request = { url: string; method: string; header: Record<string, string>; data?: string; authIndex: string };
let post: ReturnType<typeof spyOn>;
let get: ReturnType<typeof spyOn>;
let calls: Request[];
let handler: (request: Request) => unknown | Promise<unknown>;
beforeEach(() => {
calls = [];
handler = () => { throw new Error('Unexpected API request'); };
post = spyOn(managementApi, 'post').mockImplementation(async (path, body) => {
expect(path).toBe('/api-call');
const request = body as unknown as Request;
calls.push(request);
return await handler(request) as never;
});
get = spyOn(managementApi, 'get').mockImplementation(async () => { throw new Error('Unexpected download'); });
});
afterEach(() => {
post.mockRestore();
get.mockRestore();
});
describe('quota API compatibility', () => {
it('Codex 使用新请求头和嵌套账户信息;积分详情失败仍显示用量及 usage 回退次数', async () => {
handler = (request) => request.url.endsWith('/usage')
? success(codexUsage) : { statusCode: 403, bodyText: '{"error":"credits forbidden"}' };
const result = await loadQuota({
...codexFile,
metadata: { id_token: { chatgpt_account_id: 'account-test', plan_type: 'pro', chatgpt_subscription_active_until: '2030-01-01T00:00:00Z' } },
});
expect(result).toMatchObject({ status: 'success', plan: 'pro', resetCredits: 2, resetCreditsApplicable: 0, resetCreditsError: 'credits forbidden' });
expect(result.subscriptionActiveUntil).toBe('2030-01-01T00:00:00Z');
expect(calls).toHaveLength(2);
for (const call of calls) {
expect(call.header['Chatgpt-Account-Id']).toBe('account-test');
expect(call.header['User-Agent']).toContain('codex-tui/0.149.1');
expect(call.authIndex).toBe('1');
expect(call.header.Authorization).toBe('Bearer $TOKEN$');
}
const creditCallIndex = calls.findIndex((call) => call.url.endsWith('/rate-limit-reset-credits'));
expect(post.mock.calls[creditCallIndex][2]).toEqual({ timeoutMs: 8000 });
});
it('Codex 保留零次数,usage 的适用次数优先于详情,并优先使用用量接口套餐', async () => {
handler = (request) => request.url.endsWith('/usage')
? success({ ...codexUsage, plan_type: 'team' })
: success({ availableCount: 0, applicableAvailableCount: 3, credits: [] });
expect(await loadQuota({ ...codexFile, plan_type: 'pro' })).toMatchObject({
status: 'success', plan: 'team', resetCredits: 0, resetCreditsApplicable: 0,
});
});
it('Codex 详情空列表不能覆盖 usage 报告的可用次数', async () => {
handler = (request) => success(request.url.endsWith('/usage') ? codexUsage : { credits: [] });
expect(await loadQuota(codexFile)).toMatchObject({ status: 'success', resetCredits: 2 });
});
it('Codex 详情返回坏数据时报告非致命错误而不是吞掉失败', async () => {
handler = (request) => success(request.url.endsWith('/usage') ? codexUsage : { unexpected: true });
const result = await loadQuota(codexFile);
expect(result.status).toBe('success');
expect(result.resetCredits).toBe(2);
expect(result.resetCreditsError).toContain('无法识别');
});
it('Claude profile 查询失败不影响现代 Fable 额度', async () => {
handler = (request) => request.url.endsWith('/profile')
? { status_code: 500 }
: success({ limits: [{ kind: 'weekly_scoped', percent: 40, scope: { model: { display_name: 'Fable 5' } } }] });
expect(await loadQuota({ name: 'claude.json', provider: 'claude', auth_index: 'c' })).toMatchObject({
status: 'success', rows: [{ label: '7 天 Fable 窗口', remainingPercent: 60 }],
});
});
it('Antigravity 使用元数据项目、不下载凭据,空响应后回退并读取服务端时间', async () => {
let quotaCalls = 0;
const serverTime = 'Tue, 01 Jan 2030 00:00:00 GMT';
handler = (request) => {
if (request.url.endsWith(':loadCodeAssist')) return success({ paidTier: { id: 'g1-ultra-lite-tier' } });
quotaCalls += 1;
return quotaCalls === 1 ? success({ groups: [] }) : {
...success({ groups: [{ displayName: 'Gemini', buckets: [{ remainingFraction: 0.8, resetTime: '2030-01-01T01:00:00Z' }] }] }),
header: { Date: [serverTime] },
};
};
const result = await loadQuota({ name: 'anti.json', provider: 'antigravity', auth_index: 'a', attributes: { gemini_virtual_project: 'project-test' } });
expect(result).toMatchObject({ status: 'success', plan: 'Ultra Lite' });
expect(get).not.toHaveBeenCalled();
const requests = calls.filter((request) => request.url.endsWith(':retrieveUserQuotaSummary'));
expect(requests).toHaveLength(2);
expect(requests[1].url).toContain('sandbox.googleapis.com');
requests.forEach((request) => expect(JSON.parse(request.data!)).toEqual({ project: 'project-test' }));
expect(Math.abs((result.serverTimeOffsetMs ?? 0) - (Date.parse(serverTime) - Date.now()))).toBeLessThan(1000);
});
it('Antigravity 项目缺失时从下载的 JSON 中提取 installed 项目', async () => {
get.mockImplementation(async () => JSON.stringify({ installed: { project_id: 'downloaded-project' } }));
handler = (request) => success(request.url.endsWith(':loadCodeAssist') ? {} : {
groups: [{ buckets: [{ remainingFraction: 1 }] }],
});
expect((await loadQuota({ name: 'anti-download.json', provider: 'antigravity', auth_index: 'a' })).status).toBe('success');
expect(get).toHaveBeenCalledWith('/auth-files/download', { name: 'anti-download.json' });
expect(calls.find((request) => request.url.endsWith(':retrieveUserQuotaSummary'))?.data).toBe('{"project":"downloaded-project"}');
});
it('缺少 auth-index 或禁用的凭据不发起请求', async () => {
expect((await loadQuota({ provider: 'kimi' })).status).toBe('error');
expect((await loadQuota({ ...codexFile, disabled: 'true' })).status).toBe('error');
await expect(consumeCodexResetCredit({ ...codexFile, disabled: true })).rejects.toThrow();
expect(post).not.toHaveBeenCalled();
});
});
describe('xAI billing and paid API fallback', () => {
const file = { name: 'xai.json', provider: 'x-ai', auth_index: 'x' };
it('单个账单接口失败仍展示另一接口的数据,不触发收费探测', async () => {
handler = (request) => request.url.includes('format=credits')
? { status_code: 403, body: 'weekly denied' }
: success({ config: { monthlyLimit: 1000, used: 200 } });
const result = await loadQuota({ ...file, metadata: { user: { id: 'u' } } });
expect(result.status).toBe('success');
expect(result.rows[0].remainingPercent).toBe(80);
expect(calls).toHaveLength(2);
calls.forEach((request) => expect(request.header['x-userid']).toBe('u'));
});
it('识别付费账户后先确认,默认不发送计费请求', async () => {
const result = await loadQuota({ ...file, using_api: true, prefix: 'paid' });
expect(result.status).toBe('error');
expect(result.error).toContain('需要确认');
expect(post).not.toHaveBeenCalled();
});
it('确认后执行极短对话探测,profile 失败不影响结果,也不伪造剩余额度', async () => {
handler = (request) => request.url.endsWith('/me') ? { status_code: 403 } : success({ choices: [] });
const result = await loadQuota({ ...file, using_api: true, prefix: 'paid' }, { confirmXaiPaidProbe: () => true });
expect(result).toMatchObject({ status: 'success', plan: 'Paid', rows: [{ remainingPercent: null }] });
expect(calls).toHaveLength(2);
expect(calls.every((request) => request.url.startsWith('https://api.x.ai/'))).toBe(true);
expect(JSON.parse(calls.find((request) => request.method === 'POST')!.data!)).toEqual({
model: 'grok-4.5', messages: [{ role: 'user', content: 'ping' }], max_tokens: 1, stream: false,
});
post.mock.calls.forEach((call: unknown[]) => expect(call[2]).toEqual({ timeoutMs: 15000 }));
});
it('两个账单均无数据时可以确认回退;两种模式均失败时保留原账单错误', async () => {
handler = (request) => request.url.includes('cli-chat-proxy') ? { status_code: 403, body: 'billing denied' }
: { status_code: 429, body: 'paid denied' };
const result = await loadQuota(file, { confirmXaiPaidProbe: () => true });
expect(result).toMatchObject({ status: 'error', error: 'billing denied' });
expect(calls).toHaveLength(4);
});
it('不允许仅凭 profile 成功把对话失败的付费账户标记为可用', async () => {
handler = (request) => request.url.endsWith('/me') ? success({ user_id: 'u' }) : { status_code: 429, body: 'chat denied' };
expect(await loadQuota({ ...file, using_api: true, prefix: 'paid' }, { confirmXaiPaidProbe: () => true }))
.toMatchObject({ status: 'error', error: 'chat denied' });
});
});
describe('quota request synchronization', () => {
it('跨页面重复刷新共用同一个请求', async () => {
let release!: () => void;
const gate = new Promise<void>((resolve) => { release = resolve; });
handler = async (request) => {
await gate;
return success(request.url.endsWith('/usage') ? codexUsage : { available_count: 2 });
};
const first = loadQuota(codexFile);
const second = loadQuota(codexFile);
expect(second).toBe(first);
release();
await Promise.all([first, second]);
expect(calls).toHaveLength(2);
});
it('重置期间刷新等待重置结果,连续点击只消费一次且随后重查用量', async () => {
let release!: () => void;
const gate = new Promise<void>((resolve) => { release = resolve; });
handler = async (request) => {
if (request.url.endsWith('/consume')) {
await gate;
return success({});
}
return success(request.url.endsWith('/usage') ? codexUsage : { available_count: 1 });
};
const reset = consumeCodexResetCredit(codexFile);
const secondReset = consumeCodexResetCredit(codexFile);
const refresh = loadQuota(codexFile);
expect(secondReset).toBe(reset);
release();
expect((await reset).status).toBe('success');
expect(await refresh).toEqual(await reset);
const consumes = calls.filter((request) => request.url.endsWith('/consume'));
expect(consumes).toHaveLength(1);
expect(JSON.parse(consumes[0].data!).redeem_request_id).toMatch(/^[0-9a-f-]{36}$/);
expect(calls.map((request) => request.method)).toEqual(['POST', 'GET', 'GET']);
});
it('已有刷新结束后才消费重置积分,避免把旧请求结果当成重置后的额度', async () => {
let release!: () => void;
const gate = new Promise<void>((resolve) => { release = resolve; });
handler = async (request) => {
await gate;
return success(request.url.endsWith('/usage') ? codexUsage : { available_count: 1 });
};
const refresh = loadQuota(codexFile);
const reset = consumeCodexResetCredit(codexFile);
await Promise.resolve();
expect(calls.some((request) => request.url.endsWith('/consume'))).toBe(false);
release();
await Promise.all([refresh, reset]);
expect(calls.map((request) => request.method)).toEqual(['GET', 'GET', 'POST', 'GET', 'GET']);
});
it('重置失败后释放锁,允许重新刷新', async () => {
handler = (request) => request.url.endsWith('/consume')
? { status_code: 409, body: 'reset denied' }
: success(request.url.endsWith('/usage') ? codexUsage : { available_count: 1 });
const reset = consumeCodexResetCredit(codexFile);
const refresh = loadQuota(codexFile);
await expect(reset).rejects.toThrow('reset denied');
expect(await refresh).toMatchObject({ status: 'error', error: 'reset denied' });
expect((await loadQuota(codexFile)).status).toBe('success');
});
});
+104 -2
View File
@@ -56,7 +56,7 @@ describe('quotaRowsFor', () => {
]);
});
it('Team 次级窗口缺少时长时按月限额处理', () => {
it('Team 次级窗口缺少时长时与新版一致按周窗口回退,不臆测月限额', () => {
const rows = quotaRowsFor('codex', {
plan_type: 'team',
rate_limit: {
@@ -65,7 +65,20 @@ describe('quotaRowsFor', () => {
},
});
expect(rows.map((row) => row.label)).toEqual(['5 小时限额', '月限额']);
expect(rows.map((row) => row.label)).toEqual(['5 小时限额', '周限额']);
});
it('支持反序窗口且只在有重置时间时推断已耗尽额度', () => {
const rows = quotaRowsFor('codex', {
rateLimit: {
allowed: false,
primaryWindow: { limitWindowSeconds: 604800 },
secondaryWindow: { limitWindowSeconds: 18000, resetAfterSeconds: 3600 },
},
});
expect(rows.map((row) => row.label)).toEqual(['5 小时限额', '周限额']);
expect(rows.map((row) => row.remainingPercent)).toEqual([0, null]);
expect(rows[0].resetAtMs).toBeGreaterThan(Date.now());
});
it('读取 Codex 可用重置额度', () => {
@@ -111,6 +124,27 @@ describe('quotaRowsFor', () => {
});
});
it('重置次数支持 applicable 字段、空列表,不把过期积分算入推断次数', () => {
expect(codexResetCreditDetailsFor({ credits: [] })).toEqual({ availableCount: undefined, earliestExpiry: undefined });
expect(codexResetCreditDetailsFor({
applicableAvailableCount: '0',
credits: [
{ resetType: 'codex_rate_limits', status: 'available', expiresAt: '2030-01-01T00:00:00Z' },
{ resetType: 'codex_rate_limits', status: 'available', expiresAt: '2020-01-01T00:00:00Z' },
{ resetType: 'codex_rate_limits', status: 'available', expiresAt: 'invalid' },
{ resetType: 'other', status: 'available', expiresAt: '2030-01-01T00:00:00Z' },
],
}, Date.parse('2026-01-01T00:00:00Z'))).toEqual({
availableCount: 1, applicableAvailableCount: 0, earliestExpiry: '2030-01-01T00:00:00Z',
});
});
it('不会把数组、对象和布尔值当作百分比', () => {
for (const used of [[], [25], {}, false, '', ' ', NaN, Infinity]) {
expect(quotaRowsFor('codex', { rate_limit: { primary_window: { used_percent: used } } })[0].remainingPercent).toBeNull();
}
});
it('不会把小于 1 的上游百分比错误放大 100 倍', () => {
const codex = quotaRowsFor('codex', {
rate_limit: { primary_window: { used_percent: 0.63 } },
@@ -123,6 +157,31 @@ describe('quotaRowsFor', () => {
expect(claude[0].remainingPercent).toBeCloseTo(99.56);
});
it('Claude 优先活动的现代 Fable 窗口,并去除旧字段重复项', () => {
const limit = (percent: unknown, active: boolean, name = 'Fable') => ({
kind: 'weekly_scoped', percent, is_active: active,
scope: { model: { display_name: name } }, resets_at: '2030-01-01T00:00:00Z',
});
const rows = quotaRowsFor('claude', {
iguana_necktie: { utilization: 41 },
five_hour: { utilization: 10 },
limits: [null, limit(null, true), limit(12, false, 'Fable 5'), limit(64, true), limit(99, true, 'Sonnet')],
});
expect(rows.map((row) => [row.label, row.remainingPercent])).toEqual([
['5 小时窗口', 90], ['7 天 Fable 窗口', 36],
]);
expect(rows[1].resetAtMs).toBe(Date.parse('2030-01-01T00:00:00Z'));
});
it('Claude 无有效现代 Fable 数据时兼容旧字段', () => {
const rows = quotaRowsFor('claude', {
iguana_necktie: { utilization: 41 },
limits: [{ kind: 'weekly_scoped', percent: null, scope: { model: { display_name: 'Fable' } } }],
});
expect(rows).toHaveLength(1);
expect(rows[0]).toMatchObject({ label: '7 天 Fable 窗口', remainingPercent: 59 });
});
it('显示 Claude 已启用的额外用量', () => {
const rows = quotaRowsFor('claude', {
five_hour: { utilization: 20, resets_at: '2027-01-01T00:00:00Z' },
@@ -159,6 +218,33 @@ describe('quotaRowsFor', () => {
expect(antigravity[0].remainingPercent).toBe(3);
});
it('Kimi 短期窗口排在周汇总前,识别 protobuf 时间单位且保留小数', () => {
const rows = quotaRowsFor('kimi', {
usage: { used: 99.5, limit: 100 },
limits: [
{ window: { duration: 5, timeUnit: 'TIME_UNIT_HOUR' }, detail: { limit: 100, remaining: 80, reset_in: 3600 } },
{ window: { duration: 1, time_unit: 'TIME_UNIT_WEEK' }, detail: { limit: 100, used: 20 } },
{ window: { duration: 300, timeUnit: 'TIME_UNIT_MINUTE' }, detail: { limit: 100, used: 0 } },
{ window: { duration: 90, timeUnit: 'TIME_UNIT_SECOND' }, detail: { limit: 100, used: 0 } },
],
});
expect(rows.map((row) => row.label)).toEqual(['5 小时窗口', '7 天窗口', '5 小时窗口', '90 秒窗口', '每周额度']);
expect(rows.at(-1)?.remainingPercent).toBeCloseTo(0.5);
expect(rows[0].remainingPercent).toBe(80);
expect(rows[0].resetAtMs).toBeGreaterThan(Date.now());
});
it('Antigravity 把 5h 排在周窗口前,兼容百分数字符串且不修改响应', () => {
const buckets = [{ window: 'weekly', remainingFraction: '40%' }, { window: 'five_hour', remaining_fraction: 0.8 }];
const rows = quotaRowsFor('antigravity', { groups: [{ displayName: 'Gemini', buckets }] });
expect(rows.map((row) => row.remainingPercent)).toEqual([80, 40]);
expect(buckets[0].window).toBe('weekly');
});
it('Antigravity 兼容 body 包装的额度响应', () => {
expect(quotaRowsFor('antigravity', { body: JSON.stringify({ groups: [{ buckets: [{ remainingFraction: 0.5 }] }] }) })[0].remainingPercent).toBe(50);
});
it('区分 Antigravity 同一分组中的不同窗口', () => {
const rows = quotaRowsFor('antigravity', {
groups: [{
@@ -176,6 +262,22 @@ describe('quotaRowsFor', () => {
]);
});
it('xAI 仅返回产品用量时仍能解析,付费探测不伪造百分比', () => {
expect(quotaRowsFor('xai', { config: { productUsage: [{ product: 'grok', usagePercent: 20 }] } })[1])
.toMatchObject({ label: 'grok', remainingPercent: 80 });
expect(quotaRowsFor('xai', { mode: 'paid-health' })[0])
.toMatchObject({ label: '付费 API 可用', remainingPercent: null });
});
it('xAI 不从月账单借用周窗口的重置时间', () => {
const rows = quotaRowsFor('xai', {
weekly: { config: { creditUsagePercent: 25 } },
monthly: { config: { monthlyLimit: 1000, used: 500, billingPeriodEnd: '2030-01-01T00:00:00Z' } },
});
expect(rows[0].resetAtMs).toBeUndefined();
expect(rows[1].resetAtMs).toBe(Date.parse('2030-01-01T00:00:00Z'));
});
it('合并 xAI 每周、月度和按量付费额度', () => {
const rows = quotaRowsFor('xai', {
weekly: {
+31
View File
@@ -0,0 +1,31 @@
import { describe, expect, it } from 'bun:test';
import { formatQuotaReset, quotaResetFor, quotaResetInstant } from '../src/services/quotaTime';
import { formatQuotaTimestamp } from '../src/services/quotaService';
const resetMs = Date.parse('2030-01-01T00:00:00Z');
describe('quota reset instants', () => {
it('兼容秒、毫秒、数字字符串与高精度 ISO 时间', () => {
for (const input of [resetMs, resetMs / 1000, String(resetMs), String(resetMs / 1000), '2030-01-01T00:00:00.000000000Z']) {
expect(quotaResetInstant(input)).toBe(resetMs);
}
for (const input of [null, undefined, '', ' ', 0, -1, false, [], {}, NaN, Infinity, 'invalid']) {
expect(quotaResetInstant(input)).toBeUndefined();
}
expect(formatQuotaTimestamp(String(resetMs / 1000), 'en')).not.toBe('—');
});
it('跳过坏绝对时间后使用有效别名或相对秒数,仅在获取时计算倒计时', () => {
expect(quotaResetFor({ reset_at: 'bad', resetAt: resetMs }, ['reset_at', 'resetAt'])).toBe(resetMs);
expect(quotaResetFor({ ttl: '3600' }, ['reset_at'], ['ttl'], resetMs)).toBe(resetMs + 3600000);
expect(quotaResetFor({ ttl: 0 }, [], ['ttl'], resetMs)).toBe(resetMs);
expect(quotaResetFor({ ttl: false }, [], ['ttl'], resetMs)).toBeUndefined();
});
it('同一个重置时间显示随时钟更新,过期后提示刷新而不是伪造已恢复额度', () => {
expect(formatQuotaReset(resetMs, undefined, 'zh-CN', resetMs - 60000)).toContain('1分钟后');
expect(formatQuotaReset(resetMs, undefined, 'en', resetMs - 60000)).toContain('in 1 minute');
expect(formatQuotaReset(resetMs, undefined, 'zh-CN', resetMs)).toContain('重置时间已到,请刷新确认');
expect(formatQuotaReset(undefined, 'legacy', 'en', resetMs)).toBe('legacy');
});
});