diff --git a/src/components/AuthFileQuotaPanel.tsx b/src/components/AuthFileQuotaPanel.tsx
index e229379..f8e56dd 100644
--- a/src/components/AuthFileQuotaPanel.tsx
+++ b/src/components/AuthFileQuotaPanel.tsx
@@ -24,10 +24,14 @@ export function AuthFileQuotaPanel({ quota, file, disabled, onRefresh, onReset,
const fileDisabled = readBoolean(file, 'disabled');
const showReset = Boolean(onReset && (quota.resetCredits ?? 0) > 0);
const resetCreditsLabel = quota.resetCredits !== undefined ? t('authFiles.quota.resets', { count: quota.resetCredits }) : '';
+ const accountCreditsLabel = quota.creditsUnlimited
+ ? t('quota.creditUnlimited')
+ : quota.creditBalance !== undefined ? quota.creditBalance : '';
const denseMetadata = dense ? [
quota.resetCreditsApplicable !== undefined ? t('quota.resetApplicable', { count: quota.resetCreditsApplicable }) : '',
quota.subscriptionActiveUntil ? t('quota.subscriptionExpiry', { time: formatQuotaTimestamp(quota.subscriptionActiveUntil, locale) }) : '',
quota.resetCreditsEarliestExpiry ? t('authFiles.quota.expiry', { time: formatQuotaTimestamp(quota.resetCreditsEarliestExpiry, locale) }) : '',
+ accountCreditsLabel ? t('quota.creditBalance') + ': ' + accountCreditsLabel : '',
].filter(Boolean).join(' · ') : '';
const renderRow = (row: QuotaState['rows'][number], index: number) => {
const percent = row.remainingPercent !== null && Number.isFinite(row.remainingPercent) ? Math.max(0, Math.min(100, row.remainingPercent)) : null;
@@ -51,9 +55,9 @@ export function AuthFileQuotaPanel({ quota, file, disabled, onRefresh, onReset,
{!compactLayout || showReset || (dense && resetCreditsLabel) ?
{!compactLayout ? {t('authFiles.settings.quotaRemaining')} : null}
{!compactLayout && quota.plan ? {quota.plan} : null}
+ {dense && resetCreditsLabel ? {resetCreditsLabel} : null}
{showReset ? : null}
- {dense && !showReset && resetCreditsLabel ? {resetCreditsLabel} : null}
+ title={t('quota.reset')}>{t('quota.reset')} : null}
{!compactLayout ?
: null}
diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts
index 68e11c2..a9f070d 100644
--- a/src/i18n/ja.ts
+++ b/src/i18n/ja.ts
@@ -472,6 +472,8 @@ export const jaOverrides = {
'quota.fetch': 'クォータを取得',
'quota.querying': '照会中',
'quota.resetCredits': '手動リセット回数',
+ 'quota.creditBalance': 'クレジット残高',
+ 'quota.creditUnlimited': '無制限',
'quota.earliestExpiry': '最も早い有効期限',
'quota.remaining': '残り {percent}%',
'quota.resetPassed': 'リセット時刻到達。更新して確認してください',
diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts
index 4d66f61..58c93b3 100644
--- a/src/i18n/locales/en.ts
+++ b/src/i18n/locales/en.ts
@@ -459,6 +459,8 @@ export const en: Record = {
'quota.fetch': 'Fetch Quota',
'quota.querying': 'Querying',
'quota.resetCredits': 'Manual resets',
+ 'quota.creditBalance': 'Credit balance',
+ 'quota.creditUnlimited': 'Unlimited',
'quota.earliestExpiry': 'Earliest expiry',
'quota.remaining': '{percent}% remaining',
'quota.resetPassed': 'Reset time reached; refresh to verify',
diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts
index 0a652f8..5a030a5 100644
--- a/src/i18n/locales/zh-CN.ts
+++ b/src/i18n/locales/zh-CN.ts
@@ -458,6 +458,8 @@ export const zhCN = {
'quota.fetch': '获取额度',
'quota.querying': '查询中',
'quota.resetCredits': '主动重置次数',
+ 'quota.creditBalance': 'Credit 余额',
+ 'quota.creditUnlimited': '无限额',
'quota.earliestExpiry': '最早过期',
'quota.remaining': '剩余 {percent}%',
'quota.resetPassed': '重置时间已到,请刷新确认',
diff --git a/src/pages/QuotaPage.tsx b/src/pages/QuotaPage.tsx
index 9b24f26..04bc4d8 100644
--- a/src/pages/QuotaPage.tsx
+++ b/src/pages/QuotaPage.tsx
@@ -208,6 +208,7 @@ export function QuotaCard({ file, quota, onRefresh, onReset }: { file: AuthFile;
{t('authFiles.quota.failed')}
> : null}
{quota.status === 'success' && provider === 'codex' ?
+ {(quota.creditsUnlimited || quota.creditBalance !== undefined) ? {t('quota.creditBalance')} {quota.creditsUnlimited ? t('quota.creditUnlimited') : quota.creditBalance} : null}
{t('quota.resetCredits')} {quota.resetCredits ?? '—'}
{quota.resetCreditsApplicable !== undefined ? {t('quota.resetApplicable', { count: quota.resetCreditsApplicable })} : null}
{t('quota.earliestExpiry')} {formatQuotaTimestamp(quota.resetCreditsEarliestExpiry, locale)}
diff --git a/src/services/quotaService.ts b/src/services/quotaService.ts
index 85bfc51..eca098f 100644
--- a/src/services/quotaService.ts
+++ b/src/services/quotaService.ts
@@ -32,6 +32,8 @@ export type QuotaState = {
rows: QuotaRow[];
error?: string;
plan?: string;
+ creditBalance?: string;
+ creditsUnlimited?: boolean;
resetCredits?: number;
resetCreditsApplicable?: number;
resetCreditsError?: string;
@@ -304,6 +306,24 @@ export const codexResetCreditsFor = (payload: unknown): number | undefined => {
return count === null ? undefined : Math.max(0, Math.floor(count));
};
+/** Read the account-level credits returned by the Codex usage endpoint. */
+export const codexAccountCreditsFor = (
+ payload: unknown,
+): { balance?: string; unlimited: boolean } => {
+ const value = parseBody(payload);
+ if (!isRecord(value) || !isRecord(value.credits)) return { unlimited: false };
+ const rawBalance = value.credits.balance;
+ const balance = typeof rawBalance === 'number'
+ ? String(rawBalance)
+ : typeof rawBalance === 'string' && rawBalance.trim() !== ''
+ ? rawBalance.trim()
+ : undefined;
+ return {
+ ...(balance && /^\d+(?:\.\d+)?$/.test(balance) && Number.isFinite(Number(balance)) ? { balance } : {}),
+ unlimited: value.credits.unlimited === true,
+ };
+};
+
export const codexResetCreditDetailsFor = (
payload: unknown,
nowMs = Date.now(),
@@ -884,11 +904,14 @@ async function loadQuotaSnapshot(file: AuthFile): Promise {
const usageCreditDetails = provider === 'codex' && isRecord(payload)
? codexResetCreditDetailsFor(payload.rate_limit_reset_credits ?? payload.rateLimitResetCredits)
: {};
+ const accountCredits = provider === 'codex' ? codexAccountCreditsFor(payload) : { unlimited: false };
return {
status: 'success',
rows,
plan: (provider === 'devin' ? readDevinQuota(payload).plan : detectedPlan)
?? (readString(isRecord(payload) ? payload : {}, 'plan_type', 'planType') || codexMetadata?.plan),
+ creditBalance: accountCredits.balance,
+ creditsUnlimited: accountCredits.unlimited,
subscriptionActiveUntil: provider === 'devin'
? readDevinQuota(payload).subscriptionActiveUntil : codexMetadata?.subscriptionActiveUntil,
resetCreditsError,
diff --git a/tests/quotaRequests.test.ts b/tests/quotaRequests.test.ts
index 1626423..b7dc954 100644
--- a/tests/quotaRequests.test.ts
+++ b/tests/quotaRequests.test.ts
@@ -5,6 +5,7 @@ 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 } },
+ credits: { balance: 12.5, unlimited: false },
rate_limit_reset_credits: { available_count: 2, applicable_available_count: 0 },
};
const codexFile = { name: 'codex-test.json', provider: 'codex', auth_index: 1 };
@@ -39,7 +40,7 @@ describe('quota API compatibility', () => {
...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).toMatchObject({ status: 'success', plan: 'pro', creditBalance: '12.5', creditsUnlimited: false, 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) {
diff --git a/tests/quotaService.test.ts b/tests/quotaService.test.ts
index dca8a0b..604b4c0 100644
--- a/tests/quotaService.test.ts
+++ b/tests/quotaService.test.ts
@@ -1,10 +1,25 @@
import { describe, expect, it } from 'bun:test';
import {
+ codexAccountCreditsFor,
codexResetCreditDetailsFor,
codexResetCreditsFor,
quotaRowsFor,
} from '../src/services/quotaService';
+describe('Codex account credits', () => {
+ it('reads a numeric balance and unlimited flag from usage payloads', () => {
+ expect(codexAccountCreditsFor({ credits: { balance: 12.5, unlimited: false } }))
+ .toEqual({ balance: '12.5', unlimited: false });
+ expect(codexAccountCreditsFor({ credits: { unlimited: true } }))
+ .toEqual({ unlimited: true });
+ });
+
+ it('ignores malformed balances', () => {
+ expect(codexAccountCreditsFor({ credits: { balance: 'N/A', unlimited: false } }))
+ .toEqual({ unlimited: false });
+ });
+});
+
describe('quotaRowsFor', () => {
it('把 Codex 的已用百分比转换为剩余额度', () => {
const rows = quotaRowsFor('codex', {