Skip to content

Commit b71b486

Browse files
authored
Merge pull request #293 from seakee/xai-paygo-quota-community-link
feat(web): show xAI pay-as-you-go quota
2 parents 991ff87 + 6d4c4a1 commit b71b486

11 files changed

Lines changed: 191 additions & 40 deletions

File tree

README_CN.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,10 @@ docker compose -f docker-compose.manager.yml up --build
216216
- 感谢上游项目 [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI)[Cli-Proxy-API-Management-Center](https://github.com/router-for-me/Cli-Proxy-API-Management-Center) 提供基础与参考。
217217
- 感谢 [Linux.do](https://linux.do/) 社区对项目推广与反馈的支持。
218218

219+
## 社区与反馈
220+
221+
- Telegram 交流群: https://t.me/cpa_mp
222+
219223
## Star History
220224

221225
<a href="https://www.star-history.com/?repos=seakee%2FCPA-Manager-Plus&type=date&logscale=&legend=top-left">

apps/web/src/components/quota/quotaConfigs.ts

Lines changed: 57 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1219,7 +1219,6 @@ const renderKimiItems = (
12191219
'div',
12201220
{ className: styleMap.quotaMeta },
12211221
h('span', { className: styleMap.quotaPercent }, percentLabel),
1222-
limit > 0 ? h('span', { className: styleMap.quotaAmount }, `${used} / ${limit}`) : null,
12231222
resetLabel ? h('span', { className: styleMap.quotaReset }, resetLabel) : null
12241223
)
12251224
),
@@ -1260,6 +1259,22 @@ const formatXaiCurrency = (value: number | null): string => {
12601259
return `$${(value / 100).toFixed(2)}`;
12611260
};
12621261

1262+
const formatXaiRemainingAmount = (billing: XaiBillingSummary): string => {
1263+
const remainingCents =
1264+
billing.monthlyLimitCents !== null && billing.includedUsedCents !== null
1265+
? Math.max(0, billing.monthlyLimitCents - billing.includedUsedCents)
1266+
: null;
1267+
return `${formatXaiCurrency(remainingCents)} / ${formatXaiCurrency(billing.monthlyLimitCents)}`;
1268+
};
1269+
1270+
const formatXaiOnDemandAmount = (billing: XaiBillingSummary): string => {
1271+
const remainingCents =
1272+
billing.onDemandCapCents !== null && billing.onDemandUsedCents !== null
1273+
? Math.max(0, billing.onDemandCapCents - billing.onDemandUsedCents)
1274+
: null;
1275+
return `${formatXaiCurrency(remainingCents)} / ${formatXaiCurrency(billing.onDemandCapCents)}`;
1276+
};
1277+
12631278
const XAI_SUPERGROK_LIMIT_CENTS = 15_000;
12641279
const XAI_SUPERGROK_HEAVY_LIMIT_CENTS = 150_000;
12651280

@@ -1288,21 +1303,24 @@ const renderXaiItems = (
12881303
return h('div', { className: styleMap.quotaMessage }, t('xai_quota.empty_data'));
12891304
}
12901305

1291-
const usedPercent = billing.usedPercent;
1292-
const clampedUsed = usedPercent === null ? null : Math.max(0, Math.min(100, usedPercent));
1306+
const clampedUsed =
1307+
billing.usedPercent === null ? null : Math.max(0, Math.min(100, billing.usedPercent));
12931308
const remaining = clampedUsed === null ? null : Math.max(0, Math.min(100, 100 - clampedUsed));
12941309
const percentLabel = remaining === null ? '--' : `${Math.round(remaining)}%`;
1295-
const remainingCents =
1296-
billing.monthlyLimitCents !== null && billing.usedCents !== null
1297-
? Math.max(0, billing.monthlyLimitCents - billing.usedCents)
1298-
: null;
1299-
const amountLabel = t('xai_quota.usage_amount', {
1300-
remaining: formatXaiCurrency(remainingCents),
1301-
limit: formatXaiCurrency(billing.monthlyLimitCents),
1302-
});
1310+
const amountLabel = formatXaiRemainingAmount(billing);
13031311
const resetLabel = billing.billingPeriodEnd
13041312
? formatQuotaResetTime(billing.billingPeriodEnd)
13051313
: t('xai_quota.reset_unknown');
1314+
const onDemandCap = billing.onDemandCapCents ?? 0;
1315+
const clampedOnDemandUsed =
1316+
billing.onDemandUsedPercent === null
1317+
? null
1318+
: Math.max(0, Math.min(100, billing.onDemandUsedPercent));
1319+
const onDemandRemaining =
1320+
clampedOnDemandUsed === null ? null : Math.max(0, Math.min(100, 100 - clampedOnDemandUsed));
1321+
const onDemandPercentLabel =
1322+
onDemandRemaining === null ? '--' : `${Math.round(onDemandRemaining)}%`;
1323+
const onDemandAmountLabel = formatXaiOnDemandAmount(billing);
13061324
const plan = resolveXaiPlan(billing.monthlyLimitCents);
13071325

13081326
const nodes: ReactNode[] = [
@@ -1318,13 +1336,40 @@ const renderXaiItems = (
13181336
)
13191337
)
13201338
: null,
1339+
onDemandCap > 0
1340+
? h(
1341+
'div',
1342+
{ key: 'pay-as-you-go', className: styleMap.quotaRow },
1343+
h(
1344+
'div',
1345+
{ className: styleMap.quotaRowHeader },
1346+
h('span', { className: styleMap.quotaModel }, t('xai_quota.pay_as_you_go_label')),
1347+
h(
1348+
'div',
1349+
{ className: styleMap.quotaMeta },
1350+
h('span', { className: styleMap.quotaPercent }, onDemandPercentLabel),
1351+
h('span', { className: styleMap.quotaAmount }, onDemandAmountLabel)
1352+
)
1353+
),
1354+
h(QuotaProgressBar, {
1355+
percent: onDemandRemaining,
1356+
highThreshold: QUOTA_PROGRESS_HIGH_THRESHOLD,
1357+
mediumThreshold: QUOTA_PROGRESS_MEDIUM_THRESHOLD,
1358+
})
1359+
)
1360+
: h(
1361+
'div',
1362+
{ key: 'pay-as-you-go', className: styleMap.codexPlan },
1363+
h('span', { className: styleMap.codexPlanLabel }, t('xai_quota.pay_as_you_go_label')),
1364+
h('span', { className: styleMap.codexPlanValue }, t('xai_quota.pay_as_you_go_disabled'))
1365+
),
13211366
h(
13221367
'div',
13231368
{ key: 'billing', className: styleMap.quotaRow },
13241369
h(
13251370
'div',
13261371
{ className: styleMap.quotaRowHeader },
1327-
h('span', { className: styleMap.quotaModel }, t('xai_quota.monthly_limit')),
1372+
h('span', { className: styleMap.quotaModel }, t('xai_quota.monthly_credits')),
13281373
h(
13291374
'div',
13301375
{ className: styleMap.quotaMeta },
@@ -1341,21 +1386,6 @@ const renderXaiItems = (
13411386
),
13421387
];
13431388

1344-
if (billing.onDemandCapCents !== null) {
1345-
nodes.push(
1346-
h(
1347-
'div',
1348-
{ key: 'on-demand-cap', className: styleMap.codexPlan },
1349-
h('span', { className: styleMap.codexPlanLabel }, t('xai_quota.on_demand_cap')),
1350-
h(
1351-
'span',
1352-
{ className: styleMap.codexPlanValue },
1353-
formatXaiCurrency(billing.onDemandCapCents)
1354-
)
1355-
)
1356-
);
1357-
}
1358-
13591389
return h(React.Fragment, null, ...nodes);
13601390
};
13611391

apps/web/src/features/monitoring/model/monitoringCenterPageModel.test.ts

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
fetchAntigravityQuota,
55
fetchClaudeQuota,
66
fetchCodexQuota,
7+
fetchKimiQuota,
78
fetchXaiQuota,
89
} from '@/utils/quota';
910
import type { MonitoringAccountQuotaTarget } from '@/features/monitoring/accountOverviewQuotaTargets';
@@ -57,6 +58,8 @@ const t = ((key: string, options?: Record<string, unknown>) => {
5758
'xai_quota.title': 'xAI Quota',
5859
'xai_quota.empty_data': 'No xAI quota data',
5960
'xai_quota.monthly_limit': 'Monthly billing limit',
61+
'xai_quota.monthly_credits': 'Monthly credits',
62+
'xai_quota.pay_as_you_go_label': 'Pay-as-you-go',
6063
'xai_quota.on_demand_cap': 'On-demand cap',
6164
'xai_quota.usage_amount': '{{remaining}} / {{limit}} remaining',
6265
};
@@ -844,14 +847,51 @@ describe('monitoringCenterPageModel account quota', () => {
844847
]);
845848
});
846849

850+
it('maps Kimi quota rows without amount labels in account quota entries', async () => {
851+
vi.mocked(fetchKimiQuota).mockResolvedValue([
852+
{
853+
id: 'daily',
854+
label: 'Daily',
855+
used: 25,
856+
limit: 100,
857+
resetHint: '2026-07-31T00:00:00Z',
858+
},
859+
]);
860+
861+
const entry = await requestAccountQuota(
862+
createTarget({
863+
provider: 'kimi',
864+
authIndex: '4',
865+
fileName: 'kimi.json',
866+
}),
867+
t
868+
);
869+
870+
expect(entry).toMatchObject({
871+
provider: 'kimi',
872+
providerLabel: 'Kimi Quota',
873+
windows: [
874+
{
875+
id: 'daily',
876+
label: 'Daily',
877+
remainingPercent: 75,
878+
usageLabel: null,
879+
},
880+
],
881+
});
882+
});
883+
847884
it('maps xAI billing into account quota entries', async () => {
848885
vi.mocked(fetchXaiQuota).mockResolvedValue({
849886
monthlyLimitCents: 10000,
850-
usedCents: 2500,
887+
usedCents: 12500,
888+
includedUsedCents: 10000,
851889
onDemandCapCents: 5000,
890+
onDemandUsedCents: 2500,
891+
onDemandUsedPercent: 50,
852892
billingPeriodStart: '2026-05-01T00:00:00Z',
853893
billingPeriodEnd: '2026-06-01T00:00:00Z',
854-
usedPercent: 25,
894+
usedPercent: 100,
855895
});
856896

857897
const entry = await requestAccountQuota(
@@ -870,9 +910,15 @@ describe('monitoringCenterPageModel account quota', () => {
870910
windows: [
871911
{
872912
id: 'monthly-limit',
873-
label: 'Monthly billing limit',
874-
remainingPercent: 75,
875-
usageLabel: '$75.00 / $100.00 remaining',
913+
label: 'Monthly credits',
914+
remainingPercent: 0,
915+
usageLabel: '$0.00 / $100.00 remaining',
916+
},
917+
{
918+
id: 'pay-as-you-go',
919+
label: 'Pay-as-you-go',
920+
remainingPercent: 50,
921+
usageLabel: '$25.00 / $50.00 remaining',
876922
},
877923
],
878924
});

apps/web/src/features/monitoring/model/monitoringCenterPageModel.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,7 +1055,7 @@ const buildKimiAccountQuotaWindows = (rows: KimiQuotaRow[], t: TFunction): Accou
10551055
label: rowLabel,
10561056
remainingPercent,
10571057
resetLabel: resetLabel || '-',
1058-
usageLabel: limit > 0 ? `${used} / ${limit}` : null,
1058+
usageLabel: null,
10591059
};
10601060
});
10611061

@@ -1069,14 +1069,13 @@ const buildXaiAccountQuotaWindows = (
10691069
t: TFunction
10701070
): AccountQuotaWindow[] => {
10711071
const remainingCents =
1072-
billing.monthlyLimitCents !== null && billing.usedCents !== null
1073-
? Math.max(0, billing.monthlyLimitCents - billing.usedCents)
1072+
billing.monthlyLimitCents !== null && billing.includedUsedCents !== null
1073+
? Math.max(0, billing.monthlyLimitCents - billing.includedUsedCents)
10741074
: null;
1075-
1076-
return [
1075+
const windows: AccountQuotaWindow[] = [
10771076
{
10781077
id: 'monthly-limit',
1079-
label: t('xai_quota.monthly_limit'),
1078+
label: t('xai_quota.monthly_credits'),
10801079
remainingPercent: buildRemainingFromUsedPercent(billing.usedPercent),
10811080
resetLabel: billing.billingPeriodEnd ? formatQuotaResetTime(billing.billingPeriodEnd) : '-',
10821081
usageLabel: t('xai_quota.usage_amount', {
@@ -1085,6 +1084,25 @@ const buildXaiAccountQuotaWindows = (
10851084
}),
10861085
},
10871086
];
1087+
1088+
if (billing.onDemandCapCents !== null && billing.onDemandCapCents > 0) {
1089+
const onDemandRemainingCents =
1090+
billing.onDemandUsedCents !== null
1091+
? Math.max(0, billing.onDemandCapCents - billing.onDemandUsedCents)
1092+
: null;
1093+
windows.push({
1094+
id: 'pay-as-you-go',
1095+
label: t('xai_quota.pay_as_you_go_label'),
1096+
remainingPercent: buildRemainingFromUsedPercent(billing.onDemandUsedPercent),
1097+
resetLabel: '-',
1098+
usageLabel: t('xai_quota.usage_amount', {
1099+
remaining: formatXaiCurrency(onDemandRemainingCents),
1100+
limit: formatXaiCurrency(billing.onDemandCapCents),
1101+
}),
1102+
});
1103+
}
1104+
1105+
return windows;
10881106
};
10891107

10901108
export const getAccountQuotaProviderLabel = (

apps/web/src/i18n/locales/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,9 +1127,12 @@
11271127
"refresh_button": "Refresh Quota",
11281128
"fetch_all": "Fetch All",
11291129
"monthly_limit": "Monthly billing limit",
1130+
"monthly_credits": "Monthly credits",
11301131
"plan_label": "Plan",
11311132
"plan_supergrok": "SuperGrok",
11321133
"plan_supergrok_heavy": "SuperGrok Heavy",
1134+
"pay_as_you_go_label": "Pay-as-you-go",
1135+
"pay_as_you_go_disabled": "Disabled",
11331136
"on_demand_cap": "On-demand cap",
11341137
"usage_amount": "{{remaining}} / {{limit}} remaining",
11351138
"reset_unknown": "reset unknown"

apps/web/src/i18n/locales/ru.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,9 +1127,12 @@
11271127
"refresh_button": "Обновить квоту",
11281128
"fetch_all": "Получить все",
11291129
"monthly_limit": "Месячный лимит биллинга",
1130+
"monthly_credits": "Месячные кредиты",
11301131
"plan_label": "План",
11311132
"plan_supergrok": "SuperGrok",
11321133
"plan_supergrok_heavy": "SuperGrok Heavy",
1134+
"pay_as_you_go_label": "Pay-as-you-go",
1135+
"pay_as_you_go_disabled": "Отключено",
11331136
"on_demand_cap": "Лимит on-demand",
11341137
"usage_amount": "Осталось {{remaining}} / {{limit}}",
11351138
"reset_unknown": "сброс неизвестен"

apps/web/src/i18n/locales/zh-CN.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,9 +1127,12 @@
11271127
"refresh_button": "刷新额度",
11281128
"fetch_all": "获取全部",
11291129
"monthly_limit": "月度账单限额",
1130+
"monthly_credits": "月度额度",
11301131
"plan_label": "套餐",
11311132
"plan_supergrok": "SuperGrok",
11321133
"plan_supergrok_heavy": "SuperGrok Heavy",
1134+
"pay_as_you_go_label": "按量付费",
1135+
"pay_as_you_go_disabled": "未启用",
11331136
"on_demand_cap": "按需上限",
11341137
"usage_amount": "剩余 {{remaining}} / {{limit}}",
11351138
"reset_unknown": "重置时间未知"

apps/web/src/i18n/locales/zh-TW.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,9 +1127,12 @@
11271127
"refresh_button": "重新整理配額",
11281128
"fetch_all": "取得全部",
11291129
"monthly_limit": "月度帳單限額",
1130+
"monthly_credits": "月度配額",
11301131
"plan_label": "方案",
11311132
"plan_supergrok": "SuperGrok",
11321133
"plan_supergrok_heavy": "SuperGrok Heavy",
1134+
"pay_as_you_go_label": "按量付費",
1135+
"pay_as_you_go_disabled": "未啟用",
11331136
"on_demand_cap": "按需上限",
11341137
"usage_amount": "剩餘 {{remaining}} / {{limit}}",
11351138
"reset_unknown": "重置時間未知"

apps/web/src/types/quota.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,10 @@ export interface XaiBillingPayload {
434434
export interface XaiBillingSummary {
435435
monthlyLimitCents: number | null;
436436
usedCents: number | null;
437+
includedUsedCents: number | null;
437438
onDemandCapCents: number | null;
439+
onDemandUsedCents: number | null;
440+
onDemandUsedPercent: number | null;
438441
billingPeriodStart?: string;
439442
billingPeriodEnd?: string;
440443
usedPercent: number | null;

apps/web/src/utils/quota/providerRequests.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,11 +237,32 @@ describe('buildXaiBillingSummary', () => {
237237
expect(summary).toMatchObject({
238238
monthlyLimitCents: 15000,
239239
usedCents: 3750,
240+
includedUsedCents: 3750,
240241
onDemandCapCents: 2500,
242+
onDemandUsedCents: 0,
243+
onDemandUsedPercent: 0,
241244
billingPeriodEnd: '2026-07-31T00:00:00Z',
242245
usedPercent: 25,
243246
});
244247
});
248+
249+
it('splits included and pay-as-you-go usage after monthly credits are exhausted', () => {
250+
const summary = buildXaiBillingSummary({
251+
monthly_limit: 10000,
252+
used: 12500,
253+
on_demand_cap: 5000,
254+
});
255+
256+
expect(summary).toMatchObject({
257+
monthlyLimitCents: 10000,
258+
usedCents: 12500,
259+
includedUsedCents: 10000,
260+
onDemandCapCents: 5000,
261+
onDemandUsedCents: 2500,
262+
usedPercent: 100,
263+
onDemandUsedPercent: 50,
264+
});
265+
});
245266
});
246267

247268
describe('fetchAntigravityQuota', () => {

0 commit comments

Comments
 (0)