fix(quota): improve Grok credential quota accuracy and subscription details

- Keep weekly usage separate from monthly spending
- Enrich subscription plans in the background with stale-result guards
- Display prepaid balances and hide empty monthly quota rows
- Add localized labels and regression coverage
This commit is contained in:
lzt404 committed 2026-10-08 18:36:58 +08:00
1 parent 8eab980d72
commit b5189bb436
14 files changed
+217 -18

No files matched your search

+1
View File
@@ -1927,6 +1927,7 @@ export const jaOverrides = {
'quota.service.limit.numbered': '制限 {index}',
'quota.service.product.numbered': '製品 {index}',
'quota.service.monthlyIncluded': '月間内包クォータ',
'quota.service.prepaidBalance': '前払い残高',
'quota.service.onDemand': '従量課金クォータ',
'quota.service.quota': 'クォータ',
'quota.service.quota.numbered': 'クォータ {index}',
+1
View File
@@ -1927,6 +1927,7 @@ export const en: Record<MessageKey, string> = {
'quota.service.limit.numbered': 'Limit {index}',
'quota.service.product.numbered': 'Product {index}',
'quota.service.monthlyIncluded': 'Monthly included quota',
'quota.service.prepaidBalance': 'Prepaid balance',
'quota.service.onDemand': 'On-demand quota',
'quota.service.quota': 'Quota',
'quota.service.quota.numbered': 'Quota {index}',
+1
View File
@@ -1926,6 +1926,7 @@ export const zhCN = {
'quota.service.limit.numbered': '限制 {index}',
'quota.service.product.numbered': '产品 {index}',
'quota.service.monthlyIncluded': '月度包含额度',
'quota.service.prepaidBalance': '预付余额',
'quota.service.onDemand': '按量付费额度',
'quota.service.quota': '配额',
'quota.service.quota.numbered': '配额 {index}',
+2 -4
View File
@@ -1,3 +1,4 @@
import { commitFetchedQuota } from '../services/quotaEnrichment';
import { ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useConfirmation } from '../components/ConfirmationDialog';
import { QuotaActionFeedback } from '../components/QuotaActionFeedback';
@@ -49,7 +50,6 @@ import {
} from '../services/quotaService';
import {
captureQuotaCacheGeneration,
commitQuotaCacheIfCurrent,
getQuotaCacheSnapshot,
pruneQuotaCache,
updateQuotaCache,
@@ -240,9 +240,7 @@ export function AuthFileManagementPage() {
const cacheGeneration = captureQuotaCacheGeneration();
updateQuotaCache((current) => ({ ...current, [key]: { status: 'loading', rows: [] } }));
const result = await loadQuota(file);
commitQuotaCacheIfCurrent(cacheGeneration, () => {
updateQuotaCache((current) => ({ ...current, [key]: result }));
});
commitFetchedQuota(cacheGeneration, file, result);
};
const refreshAllQuotas = async () => {
+3 -7
View File
@@ -1,3 +1,4 @@
import { commitFetchedQuota } from '../services/quotaEnrichment';
import { ResetCreditExpiries } from '../components/ResetCreditExpiries';
import { MessageNotice } from '../appNotice';
import { useCallback, useEffect, useMemo, useState, type KeyboardEvent } from 'react';
@@ -28,7 +29,6 @@ import {
} from '../services/quotaService';
import {
captureQuotaCacheGeneration,
commitQuotaCacheIfCurrent,
getQuotaCacheSnapshot,
pruneQuotaCache,
updateQuotaCache,
@@ -96,9 +96,7 @@ export function QuotaPage() {
const cacheGeneration = captureQuotaCacheGeneration();
updateQuotaCache((current) => ({ ...current, [key]: { status: 'loading', rows: [] } }));
const result = await loadQuota(file);
commitQuotaCacheIfCurrent(cacheGeneration, () => {
updateQuotaCache((current) => ({ ...current, [key]: result }));
});
commitFetchedQuota(cacheGeneration, file, result);
}, [t]);
const resetQuota = useQuotaReset(askConfirmation, setError);
@@ -119,9 +117,7 @@ export function QuotaPage() {
const batch = files.slice(index, index + REFRESH_CONCURRENCY);
await Promise.all(batch.map(async (file) => {
const result = await loadQuota(file);
commitQuotaCacheIfCurrent(cacheGeneration, () => {
updateQuotaCache((current) => ({ ...current, [quotaKey(file)]: result }));
});
commitFetchedQuota(cacheGeneration, file, result);
}));
}
} finally {
+17
View File
@@ -0,0 +1,17 @@
import { enrichXaiQuotaPlan, providerForFile, quotaKey, type AuthFile, type QuotaState } from './quotaService';
import { commitQuotaCacheIfCurrent, getQuotaCacheSnapshot, updateQuotaCache } from './quotaCache';
export function commitFetchedQuota(generation: number, file: AuthFile, quota: QuotaState) {
const key = quotaKey(file);
const committed = commitQuotaCacheIfCurrent(generation, () => {
updateQuotaCache((current) => ({ ...current, [key]: quota }));
});
if (committed && quota.status === 'success' && providerForFile(file) === 'xai') {
void enrichXaiQuotaPlan(file, quota).then((enriched) => {
if (enriched === quota || getQuotaCacheSnapshot()[key] !== quota) return;
commitQuotaCacheIfCurrent(generation, () => {
updateQuotaCache((current) => ({ ...current, [key]: enriched }));
});
}).catch(() => { /* Optional plan failures preserve the original quota. */ });
}
}
+46 -3
View File
@@ -527,8 +527,8 @@ export const quotaRowsFor = (provider: QuotaProvider, payload: unknown): QuotaRo
reset: absoluteResetLabel(billing.periodEnd),
resetAtMs: billing.resetAtMs ?? undefined,
};
if (billing.periodType === 'weekly'
&& (billing.usagePercent !== null || billing.periodEnd || billing.productUsage.length > 0)) {
const hasWeeklyData = billing.periodType === 'weekly';
if (hasWeeklyData) {
rows.push({
label: quotaText('quota.service.weekly'),
remainingPercent: remainingFromUsedPercent(billing.usagePercent),
@@ -555,7 +555,8 @@ export const quotaRowsFor = (provider: QuotaProvider, payload: unknown): QuotaRo
detail: amount(billing.onDemandCapCents, billing.onDemandUsedCents),
});
}
if (billing.monthlyLimitCents !== null || billing.usedCents !== null || billing.billingPeriodEnd) {
if ((billing.monthlyLimitCents !== null || billing.usedCents !== null || billing.billingPeriodEnd)
&& !(hasWeeklyData && billing.monthlyLimitCents === 0 && billing.usedCents === 0)) {
rows.push({
label: quotaText('quota.service.monthlyIncluded'),
remainingPercent: remainingFromUsedPercent(billing.usedPercent),
@@ -563,6 +564,10 @@ export const quotaRowsFor = (provider: QuotaProvider, payload: unknown): QuotaRo
...monthlyReset,
});
}
if (billing.prepaidBalanceCents !== null && billing.prepaidBalanceCents > 0) {
rows.push({ label: quotaText('quota.service.prepaidBalance'), remainingPercent: null,
detail: formatUsdFromCents(billing.prepaidBalanceCents) });
}
return rows.length > 0 ? rows : [{
label: quotaText(billing.periodType === 'weekly'
? 'quota.service.weekly' : 'quota.service.monthlyIncluded'),
@@ -709,6 +714,43 @@ const callXaiQuota = async (file: AuthFile): Promise<unknown> => {
}
};
// Optional subscription requests run after the quota has been committed to the shared cache.
export const enrichXaiQuotaPlan = async (file: AuthFile, quota: QuotaState): Promise<QuotaState> => {
if (providerForFile(file) !== 'xai' || quota.status !== 'success') return quota;
const authIndex = normalizeAuthIndex(file.auth_index ?? file.authIndex);
if (!authIndex) return quota;
const header = { ...headersByProvider.xai };
const userId = resolveXaiUserId(file);
if (userId) header['x-userid'] = userId;
const [user, settings] = await Promise.allSettled([
requestQuotaPayload(authIndex, 'https://cli-chat-proxy.grok.com/v1/user?include=subscription', header, 'GET', undefined, 8_000),
requestQuotaPayload(authIndex, 'https://cli-chat-proxy.grok.com/v1/settings', header, 'GET', undefined, 8_000),
]);
const readPlan = (result: PromiseSettledResult<unknown>, keys: string[]) => {
if (result.status !== 'fulfilled' || !isRecord(result.value)) return '';
for (const key of keys) {
const value = result.value[key];
if (typeof value === 'string' && value.trim()) return value.trim();
}
return '';
};
const plan = readPlan(settings, ['subscription_tier_display', 'subscriptionTierDisplay'])
|| readPlan(user, ['subscriptionTier', 'subscription_tier']);
return plan ? { ...quota, plan } : quota;
};
const xaiBillingPlan = (payload: unknown): string | undefined => {
if (!isRecord(payload)) return undefined;
const configs = [payload.monthly, payload.weekly, payload].filter(isRecord)
.map((record) => isRecord(record.config) ? record.config : record);
for (const config of configs) {
const limit = buildXaiBillingSummary(config as XaiBillingConfig)?.monthlyLimitCents;
if (limit === 150_000) return 'SuperGrok Heavy';
if (limit === 15_000) return 'SuperGrok';
}
return undefined;
};
const booleanValue = (value: unknown): boolean | null => {
if (typeof value === 'boolean') return value;
if (typeof value === 'number') return Number.isFinite(value) ? value !== 0 : null;
@@ -923,6 +965,7 @@ async function loadQuotaSnapshot(file: AuthFile): Promise<QuotaState> {
status: 'success',
rows,
plan: (provider === 'devin' ? readDevinQuota(payload).plan : detectedPlan)
?? (provider === 'xai' ? xaiBillingPlan(payload) : undefined)
?? (readString(isRecord(payload) ? payload : {}, 'plan_type', 'planType') || codexMetadata?.plan),
creditBalance: accountCredits.balance,
creditsUnlimited: accountCredits.unlimited,
+10 -2
View File
@@ -24,6 +24,8 @@ export interface XaiBillingConfig {
monthlyLimit?: XaiBillingCent | number | string | null;
monthly_limit?: XaiBillingCent | number | string | null;
used?: XaiBillingCent | number | string | null;
prepaidBalance?: XaiBillingCent | number | string | null;
prepaid_balance?: XaiBillingCent | number | string | null;
onDemandCap?: XaiBillingCent | number | string | null;
on_demand_cap?: XaiBillingCent | number | string | null;
onDemandUsed?: XaiBillingCent | number | string | null;
@@ -59,6 +61,7 @@ export interface XaiBillingSummary {
productUsage: XaiProductUsageSummary[];
monthlyLimitCents: number | null;
usedCents: number | null;
prepaidBalanceCents: number | null;
includedUsedCents: number | null;
onDemandCapCents: number | null;
onDemandUsedCents: number | null;
@@ -154,6 +157,7 @@ const emptyXaiBillingSummary = (): XaiBillingSummary => ({
productUsage: [],
monthlyLimitCents: null,
usedCents: null,
prepaidBalanceCents: null,
includedUsedCents: null,
onDemandCapCents: null,
onDemandUsedCents: null,
@@ -200,6 +204,7 @@ export function buildXaiBillingSummary(
const monthlyLimitCents = normalizeXaiCentValue(config.monthlyLimit ?? config.monthly_limit);
const usedCents = normalizeXaiCentValue(config.used);
const prepaidBalanceCents = normalizeXaiCentValue(config.prepaidBalance ?? config.prepaid_balance);
const onDemandCapCents = normalizeXaiCentValue(config.onDemandCap ?? config.on_demand_cap);
const explicitOnDemandUsedCents = normalizeXaiCentValue(
config.onDemandUsed ?? config.on_demand_used
@@ -236,7 +241,7 @@ export function buildXaiBillingSummary(
usedCents !== null ||
(!hasWeeklyData && (onDemandCapCents !== null || !!billingPeriodEnd));
if (!hasWeeklyData && !hasMonthlyData) return null;
if (!hasWeeklyData && !hasMonthlyData && prepaidBalanceCents === null) return null;
summary.periodType = hasWeeklyData
? periodType === 'unknown'
@@ -249,6 +254,7 @@ export function buildXaiBillingSummary(
summary.productUsage = productUsage;
summary.monthlyLimitCents = monthlyLimitCents;
summary.usedCents = usedCents;
summary.prepaidBalanceCents = prepaidBalanceCents;
summary.includedUsedCents = includedUsedCents;
summary.onDemandCapCents = onDemandCapCents;
summary.onDemandUsedCents = onDemandUsedCents;
@@ -285,7 +291,8 @@ export function mergeXaiBillingSummaries(
mode: 'billing',
source: 'cli-chat-proxy',
periodType: periodSummary.periodType,
usagePercent: primary.usagePercent ?? fallback.usagePercent,
// Usage belongs to the same period as its dates; monthly spending cannot fill a weekly gap.
usagePercent: periodSummary.usagePercent,
periodStart,
periodEnd,
resetAtMs: periodInstants.resetAtMs,
@@ -293,6 +300,7 @@ export function mergeXaiBillingSummaries(
productUsage: primary.productUsage.length > 0 ? primary.productUsage : fallback.productUsage,
monthlyLimitCents: primary.monthlyLimitCents ?? fallback.monthlyLimitCents,
usedCents: primary.usedCents ?? fallback.usedCents,
prepaidBalanceCents: primary.prepaidBalanceCents ?? fallback.prepaidBalanceCents,
includedUsedCents: primary.includedUsedCents ?? fallback.includedUsedCents,
onDemandCapCents: primary.onDemandCapCents ?? fallback.onDemandCapCents,
onDemandUsedCents: primary.onDemandUsedCents ?? fallback.onDemandUsedCents,