fix(quota): classify codex windows by 5-hour and weekly limits

This commit is contained in:
hkfires
2026-02-04 09:31:09 +08:00
parent 08e8fe2edd
commit 473cece09e
3 changed files with 93 additions and 51 deletions

View File

@@ -10,13 +10,14 @@ import type {
AntigravityModelsPayload, AntigravityModelsPayload,
AntigravityQuotaState, AntigravityQuotaState,
AuthFileItem, AuthFileItem,
CodexRateLimitInfo,
CodexQuotaState, CodexQuotaState,
CodexUsageWindow, CodexUsageWindow,
CodexQuotaWindow, CodexQuotaWindow,
CodexUsagePayload, CodexUsagePayload,
GeminiCliParsedBucket, GeminiCliParsedBucket,
GeminiCliQuotaBucketState, GeminiCliQuotaBucketState,
GeminiCliQuotaState GeminiCliQuotaState,
} from '@/types'; } from '@/types';
import { apiCallApi, authFilesApi, getApiCallErrorMessage } from '@/services/api'; import { apiCallApi, authFilesApi, getApiCallErrorMessage } from '@/services/api';
import { import {
@@ -47,7 +48,7 @@ import {
isCodexFile, isCodexFile,
isDisabledAuthFile, isDisabledAuthFile,
isGeminiCliFile, isGeminiCliFile,
isRuntimeOnlyAuthFile isRuntimeOnlyAuthFile,
} from '@/utils/quota'; } from '@/utils/quota';
import type { QuotaRenderHelpers } from './QuotaCard'; import type { QuotaRenderHelpers } from './QuotaCard';
import styles from '@/pages/QuotaPage.module.scss'; import styles from '@/pages/QuotaPage.module.scss';
@@ -142,7 +143,7 @@ const fetchAntigravityQuota = async (
method: 'POST', method: 'POST',
url, url,
header: { ...ANTIGRAVITY_REQUEST_HEADERS }, header: { ...ANTIGRAVITY_REQUEST_HEADERS },
data: requestBody data: requestBody,
}); });
if (result.statusCode < 200 || result.statusCode >= 300) { if (result.statusCode < 200 || result.statusCode >= 300) {
@@ -189,6 +190,15 @@ const fetchAntigravityQuota = async (
}; };
const buildCodexQuotaWindows = (payload: CodexUsagePayload, t: TFunction): CodexQuotaWindow[] => { const buildCodexQuotaWindows = (payload: CodexUsagePayload, t: TFunction): CodexQuotaWindow[] => {
const FIVE_HOUR_SECONDS = 18000;
const WEEK_SECONDS = 604800;
const WINDOW_META = {
codeFiveHour: { id: 'five-hour', labelKey: 'codex_quota.primary_window' },
codeWeekly: { id: 'weekly', labelKey: 'codex_quota.secondary_window' },
codeReviewFiveHour: { id: 'code-review-five-hour', labelKey: 'codex_quota.code_review_primary_window' },
codeReviewWeekly: { id: 'code-review-weekly', labelKey: 'codex_quota.code_review_secondary_window' },
} as const;
const rateLimit = payload.rate_limit ?? payload.rateLimit ?? undefined; const rateLimit = payload.rate_limit ?? payload.rateLimit ?? undefined;
const codeReviewLimit = payload.code_review_rate_limit ?? payload.codeReviewRateLimit ?? undefined; const codeReviewLimit = payload.code_review_rate_limit ?? payload.codeReviewRateLimit ?? undefined;
const windows: CodexQuotaWindow[] = []; const windows: CodexQuotaWindow[] = [];
@@ -210,30 +220,74 @@ const buildCodexQuotaWindows = (payload: CodexUsagePayload, t: TFunction): Codex
label: t(labelKey), label: t(labelKey),
labelKey, labelKey,
usedPercent, usedPercent,
resetLabel resetLabel,
}); });
}; };
const getWindowSeconds = (window?: CodexUsageWindow | null): number | null => {
if (!window) return null;
return normalizeNumberValue(window.limit_window_seconds ?? window.limitWindowSeconds);
};
const rawLimitReached = rateLimit?.limit_reached ?? rateLimit?.limitReached;
const rawAllowed = rateLimit?.allowed;
const pickClassifiedWindows = (
limitInfo?: CodexRateLimitInfo | null
): { fiveHourWindow: CodexUsageWindow | null; weeklyWindow: CodexUsageWindow | null } => {
const rawWindows = [
limitInfo?.primary_window ?? limitInfo?.primaryWindow ?? null,
limitInfo?.secondary_window ?? limitInfo?.secondaryWindow ?? null,
];
let fiveHourWindow: CodexUsageWindow | null = null;
let weeklyWindow: CodexUsageWindow | null = null;
for (const window of rawWindows) {
if (!window) continue;
const seconds = getWindowSeconds(window);
if (seconds === FIVE_HOUR_SECONDS && !fiveHourWindow) {
fiveHourWindow = window;
} else if (seconds === WEEK_SECONDS && !weeklyWindow) {
weeklyWindow = window;
}
}
return { fiveHourWindow, weeklyWindow };
};
const rateWindows = pickClassifiedWindows(rateLimit);
addWindow( addWindow(
'primary', WINDOW_META.codeFiveHour.id,
'codex_quota.primary_window', WINDOW_META.codeFiveHour.labelKey,
rateLimit?.primary_window ?? rateLimit?.primaryWindow, rateWindows.fiveHourWindow,
rateLimit?.limit_reached ?? rateLimit?.limitReached, rawLimitReached,
rateLimit?.allowed rawAllowed
); );
addWindow( addWindow(
'secondary', WINDOW_META.codeWeekly.id,
'codex_quota.secondary_window', WINDOW_META.codeWeekly.labelKey,
rateLimit?.secondary_window ?? rateLimit?.secondaryWindow, rateWindows.weeklyWindow,
rateLimit?.limit_reached ?? rateLimit?.limitReached, rawLimitReached,
rateLimit?.allowed rawAllowed
);
const codeReviewWindows = pickClassifiedWindows(codeReviewLimit);
const codeReviewLimitReached = codeReviewLimit?.limit_reached ?? codeReviewLimit?.limitReached;
const codeReviewAllowed = codeReviewLimit?.allowed;
addWindow(
WINDOW_META.codeReviewFiveHour.id,
WINDOW_META.codeReviewFiveHour.labelKey,
codeReviewWindows.fiveHourWindow,
codeReviewLimitReached,
codeReviewAllowed
); );
addWindow( addWindow(
'code-review', WINDOW_META.codeReviewWeekly.id,
'codex_quota.code_review_window', WINDOW_META.codeReviewWeekly.labelKey,
codeReviewLimit?.primary_window ?? codeReviewLimit?.primaryWindow, codeReviewWindows.weeklyWindow,
codeReviewLimit?.limit_reached ?? codeReviewLimit?.limitReached, codeReviewLimitReached,
codeReviewLimit?.allowed codeReviewAllowed
); );
return windows; return windows;
@@ -257,14 +311,14 @@ const fetchCodexQuota = async (
const requestHeader: Record<string, string> = { const requestHeader: Record<string, string> = {
...CODEX_REQUEST_HEADERS, ...CODEX_REQUEST_HEADERS,
'Chatgpt-Account-Id': accountId 'Chatgpt-Account-Id': accountId,
}; };
const result = await apiCallApi.request({ const result = await apiCallApi.request({
authIndex, authIndex,
method: 'GET', method: 'GET',
url: CODEX_USAGE_URL, url: CODEX_USAGE_URL,
header: requestHeader header: requestHeader,
}); });
if (result.statusCode < 200 || result.statusCode >= 300) { if (result.statusCode < 200 || result.statusCode >= 300) {
@@ -301,7 +355,7 @@ const fetchGeminiCliQuota = async (
method: 'POST', method: 'POST',
url: GEMINI_CLI_QUOTA_URL, url: GEMINI_CLI_QUOTA_URL,
header: { ...GEMINI_CLI_REQUEST_HEADERS }, header: { ...GEMINI_CLI_REQUEST_HEADERS },
data: JSON.stringify({ project: projectId }) data: JSON.stringify({ project: projectId }),
}); });
if (result.statusCode < 200 || result.statusCode >= 300) { if (result.statusCode < 200 || result.statusCode >= 300) {
@@ -320,7 +374,9 @@ const fetchGeminiCliQuota = async (
const remainingFractionRaw = normalizeQuotaFraction( const remainingFractionRaw = normalizeQuotaFraction(
bucket.remainingFraction ?? bucket.remaining_fraction bucket.remainingFraction ?? bucket.remaining_fraction
); );
const remainingAmount = normalizeNumberValue(bucket.remainingAmount ?? bucket.remaining_amount); const remainingAmount = normalizeNumberValue(
bucket.remainingAmount ?? bucket.remaining_amount
);
const resetTime = normalizeStringValue(bucket.resetTime ?? bucket.reset_time) ?? undefined; const resetTime = normalizeStringValue(bucket.resetTime ?? bucket.reset_time) ?? undefined;
let fallbackFraction: number | null = null; let fallbackFraction: number | null = null;
if (remainingAmount !== null) { if (remainingAmount !== null) {
@@ -334,7 +390,7 @@ const fetchGeminiCliQuota = async (
tokenType, tokenType,
remainingFraction, remainingFraction,
remainingAmount, remainingAmount,
resetTime resetTime,
}; };
}) })
.filter((bucket): bucket is GeminiCliParsedBucket => bucket !== null); .filter((bucket): bucket is GeminiCliParsedBucket => bucket !== null);
@@ -366,11 +422,7 @@ const renderAntigravityItems = (
h( h(
'div', 'div',
{ className: styleMap.quotaRowHeader }, { className: styleMap.quotaRowHeader },
h( h('span', { className: styleMap.quotaModel, title: group.models.join(', ') }, group.label),
'span',
{ className: styleMap.quotaModel, title: group.models.join(', ') },
group.label
),
h( h(
'div', 'div',
{ className: styleMap.quotaMeta }, { className: styleMap.quotaMeta },
@@ -403,7 +455,6 @@ const renderCodexItems = (
}; };
const planLabel = getPlanLabel(planType); const planLabel = getPlanLabel(planType);
const isFreePlan = normalizePlanType(planType) === 'free';
const nodes: ReactNode[] = []; const nodes: ReactNode[] = [];
if (planLabel) { if (planLabel) {
@@ -417,17 +468,6 @@ const renderCodexItems = (
); );
} }
if (isFreePlan) {
nodes.push(
h(
'div',
{ key: 'warning', className: styleMap.quotaWarning },
t('codex_quota.no_access')
)
);
return h(Fragment, null, ...nodes);
}
if (windows.length === 0) { if (windows.length === 0) {
nodes.push( nodes.push(
h('div', { key: 'empty', className: styleMap.quotaMessage }, t('codex_quota.empty_windows')) h('div', { key: 'empty', className: styleMap.quotaMessage }, t('codex_quota.empty_windows'))
@@ -487,7 +527,7 @@ const renderGeminiCliItems = (
bucket.remainingAmount === null || bucket.remainingAmount === undefined bucket.remainingAmount === null || bucket.remainingAmount === undefined
? null ? null
: t('gemini_cli_quota.remaining_amount', { : t('gemini_cli_quota.remaining_amount', {
count: bucket.remainingAmount count: bucket.remainingAmount,
}); });
const titleBase = const titleBase =
bucket.modelIds && bucket.modelIds.length > 0 ? bucket.modelIds.join(', ') : bucket.label; bucket.modelIds && bucket.modelIds.length > 0 ? bucket.modelIds.join(', ') : bucket.label;
@@ -530,13 +570,13 @@ export const ANTIGRAVITY_CONFIG: QuotaConfig<AntigravityQuotaState, AntigravityQ
status: 'error', status: 'error',
groups: [], groups: [],
error: message, error: message,
errorStatus: status errorStatus: status,
}), }),
cardClassName: styles.antigravityCard, cardClassName: styles.antigravityCard,
controlsClassName: styles.antigravityControls, controlsClassName: styles.antigravityControls,
controlClassName: styles.antigravityControl, controlClassName: styles.antigravityControl,
gridClassName: styles.antigravityGrid, gridClassName: styles.antigravityGrid,
renderQuotaItems: renderAntigravityItems renderQuotaItems: renderAntigravityItems,
}; };
export const CODEX_CONFIG: QuotaConfig< export const CODEX_CONFIG: QuotaConfig<
@@ -553,19 +593,19 @@ export const CODEX_CONFIG: QuotaConfig<
buildSuccessState: (data) => ({ buildSuccessState: (data) => ({
status: 'success', status: 'success',
windows: data.windows, windows: data.windows,
planType: data.planType planType: data.planType,
}), }),
buildErrorState: (message, status) => ({ buildErrorState: (message, status) => ({
status: 'error', status: 'error',
windows: [], windows: [],
error: message, error: message,
errorStatus: status errorStatus: status,
}), }),
cardClassName: styles.codexCard, cardClassName: styles.codexCard,
controlsClassName: styles.codexControls, controlsClassName: styles.codexControls,
controlClassName: styles.codexControl, controlClassName: styles.codexControl,
gridClassName: styles.codexGrid, gridClassName: styles.codexGrid,
renderQuotaItems: renderCodexItems renderQuotaItems: renderCodexItems,
}; };
export const GEMINI_CLI_CONFIG: QuotaConfig<GeminiCliQuotaState, GeminiCliQuotaBucketState[]> = { export const GEMINI_CLI_CONFIG: QuotaConfig<GeminiCliQuotaState, GeminiCliQuotaBucketState[]> = {
@@ -582,11 +622,11 @@ export const GEMINI_CLI_CONFIG: QuotaConfig<GeminiCliQuotaState, GeminiCliQuotaB
status: 'error', status: 'error',
buckets: [], buckets: [],
error: message, error: message,
errorStatus: status errorStatus: status,
}), }),
cardClassName: styles.geminiCliCard, cardClassName: styles.geminiCliCard,
controlsClassName: styles.geminiCliControls, controlsClassName: styles.geminiCliControls,
controlClassName: styles.geminiCliControl, controlClassName: styles.geminiCliControl,
gridClassName: styles.geminiCliGrid, gridClassName: styles.geminiCliGrid,
renderQuotaItems: renderGeminiCliItems renderQuotaItems: renderGeminiCliItems,
}; };

View File

@@ -445,7 +445,8 @@
"fetch_all": "Fetch All", "fetch_all": "Fetch All",
"primary_window": "5-hour limit", "primary_window": "5-hour limit",
"secondary_window": "Weekly limit", "secondary_window": "Weekly limit",
"code_review_window": "Code review limit", "code_review_primary_window": "Code review 5-hour limit",
"code_review_secondary_window": "Code review weekly limit",
"plan_label": "Plan", "plan_label": "Plan",
"plan_plus": "Plus", "plan_plus": "Plus",
"plan_team": "Team", "plan_team": "Team",

View File

@@ -445,7 +445,8 @@
"fetch_all": "获取全部", "fetch_all": "获取全部",
"primary_window": "5 小时限额", "primary_window": "5 小时限额",
"secondary_window": "周限额", "secondary_window": "周限额",
"code_review_window": "代码审查限额", "code_review_primary_window": "代码审查 5 小时限额",
"code_review_secondary_window": "代码审查周限额",
"plan_label": "套餐", "plan_label": "套餐",
"plan_plus": "Plus", "plan_plus": "Plus",
"plan_team": "Team", "plan_team": "Team",