Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions messages/en/quota.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@
"label": "Monthly Cost",
"resetAt": "Resets at"
},
"costTotal": {
"label": "Total Cost"
},
"concurrentSessions": {
"label": "Concurrent Sessions"
},
Expand Down Expand Up @@ -183,6 +186,7 @@
"costDaily": "Daily Quota",
"costWeekly": "Weekly Quota",
"costMonthly": "Monthly Quota",
"costTotal": "Total Quota",
"concurrentSessions": "Concurrent Limit",
"status": "Status",
"actions": "Actions"
Expand Down Expand Up @@ -235,6 +239,11 @@
"placeholder": "Unlimited",
"current": "Current usage: {currency}{current} / {currency}{limit}"
},
"limitTotalUsd": {
"label": "Total Quota (USD)",
"placeholder": "Unlimited",
"current": "Current usage: {currency}{current} / {currency}{limit}"
},
"concurrentSessions": {
"label": "Concurrent Session Quota",
"placeholder": "0 = Unlimited",
Expand Down
9 changes: 9 additions & 0 deletions messages/ja/quota.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@
"label": "月次コスト",
"resetAt": "リセット時刻"
},
"costTotal": {
"label": "総コスト"
},
"concurrentSessions": {
"label": "同時セッション"
},
Expand Down Expand Up @@ -160,6 +163,7 @@
"costDaily": "日次クォータ",
"costWeekly": "週次クォータ",
"costMonthly": "月次クォータ",
"costTotal": "総クォータ",
"concurrentSessions": "同時制限",
"status": "ステータス",
"actions": "アクション"
Expand Down Expand Up @@ -212,6 +216,11 @@
"placeholder": "無制限",
"current": "現在使用: {currency}{current} / {currency}{limit}"
},
"limitTotalUsd": {
"label": "総クォータ (USD)",
"placeholder": "無制限",
"current": "現在使用: {currency}{current} / {currency}{limit}"
},
"concurrentSessions": {
"label": "同時セッションクォータ",
"placeholder": "0 = 無制限",
Expand Down
9 changes: 9 additions & 0 deletions messages/ru/quota.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@
"label": "Ежемесячные расходы",
"resetAt": "Сброс в"
},
"costTotal": {
"label": "Общие расходы"
},
"concurrentSessions": {
"label": "Параллельные сессии"
},
Expand Down Expand Up @@ -183,6 +186,7 @@
"costDaily": "Дневная квота",
"costWeekly": "Еженедельная квота",
"costMonthly": "Ежемесячная квота",
"costTotal": "Общая квота",
"concurrentSessions": "Лимит параллельных",
"status": "Статус",
"actions": "Действия"
Expand Down Expand Up @@ -235,6 +239,11 @@
"placeholder": "Неограниченно",
"current": "Использовано: {currency}{current} из {currency}{limit}"
},
"limitTotalUsd": {
"label": "Общая квота (USD)",
"placeholder": "Неограниченно",
"current": "Использовано: {currency}{current} из {currency}{limit}"
},
"concurrentSessions": {
"label": "Квота параллельных сессий",
"placeholder": "0 = без ограничений",
Expand Down
9 changes: 9 additions & 0 deletions messages/zh-CN/quota.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@
"label": "月消费",
"resetAt": "重置于"
},
"costTotal": {
"label": "总消费"
},
"concurrentSessions": {
"label": "并发 Session"
},
Expand Down Expand Up @@ -183,6 +186,7 @@
"costDaily": "每日限额",
"costWeekly": "周限额",
"costMonthly": "月限额",
"costTotal": "总限额",
"concurrentSessions": "并发限制",
"status": "状态",
"actions": "操作"
Expand Down Expand Up @@ -235,6 +239,11 @@
"placeholder": "不限制",
"current": "当前已用: {currency}{current} / {currency}{limit}"
},
"limitTotalUsd": {
"label": "总限额(USD)",
"placeholder": "不限制",
"current": "当前已用: {currency}{current} / {currency}{limit}"
},
"concurrentSessions": {
"label": "并发 Session 限额",
"placeholder": "0 = 不限制",
Expand Down
9 changes: 9 additions & 0 deletions messages/zh-TW/quota.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@
"label": "月消費",
"resetAt": "重置於"
},
"costTotal": {
"label": "總消費"
},
"concurrentSessions": {
"label": "並發 Session"
},
Expand Down Expand Up @@ -158,6 +161,7 @@
"costDaily": "每日限額",
"costWeekly": "周限額",
"costMonthly": "月限額",
"costTotal": "總限額",
"concurrentSessions": "並發限制",
"status": "狀態",
"actions": "操作"
Expand Down Expand Up @@ -210,6 +214,11 @@
"placeholder": "不限制",
"current": "當前已用: {currency}{current} / {currency}{limit}"
},
"limitTotalUsd": {
"label": "總限額 (USD)",
"placeholder": "不限制",
"current": "當前已用: {currency}{current} / {currency}{limit}"
},
"concurrentSessions": {
"label": "並發 Session 限額",
"placeholder": "0 = 不限制",
Expand Down
48 changes: 36 additions & 12 deletions src/actions/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2690,6 +2690,7 @@ export async function getProviderLimitUsage(providerId: number): Promise<
costDaily: { current: number; limit: number | null; resetAt?: Date };
costWeekly: { current: number; limit: number | null; resetAt: Date };
costMonthly: { current: number; limit: number | null; resetAt: Date };
limitTotalUsd: { current: number; limit: number | null };
concurrentSessions: { current: number; limit: number };
}>
> {
Expand All @@ -2713,7 +2714,9 @@ export async function getProviderLimitUsage(providerId: number): Promise<
getTimeRangeForPeriodWithMode,
} = await import("@/lib/rate-limit/time-utils");
const { RateLimitService } = await import("@/lib/rate-limit");
const { sumProviderCostInTimeRange } = await import("@/repository/statistics");
const { sumProviderCostInTimeRange, sumProviderTotalCost } = await import(
"@/repository/statistics"
);
const limit5hResetMode = provider.limit5hResetMode ?? "rolling";

// 计算各周期的时间范围
Expand All @@ -2732,15 +2735,23 @@ export async function getProviderLimitUsage(providerId: number): Promise<
]);

// 获取金额消费(直接查询数据库,确保配额显示与 DB 一致)
const [cost5h, costDaily, costWeekly, costMonthly, concurrentSessions] = await Promise.all([
limit5hResetMode === "fixed"
? RateLimitService.getCurrentCost(providerId, "provider", "5h", undefined, limit5hResetMode)
: sumProviderCostInTimeRange(providerId, range5h.startTime, range5h.endTime),
sumProviderCostInTimeRange(providerId, rangeDaily.startTime, rangeDaily.endTime),
sumProviderCostInTimeRange(providerId, rangeWeekly.startTime, rangeWeekly.endTime),
sumProviderCostInTimeRange(providerId, rangeMonthly.startTime, rangeMonthly.endTime),
SessionTracker.getProviderSessionCount(providerId),
]);
const [cost5h, costDaily, costWeekly, costMonthly, totalCost, concurrentSessions] =
await Promise.all([
limit5hResetMode === "fixed"
? RateLimitService.getCurrentCost(
providerId,
"provider",
"5h",
undefined,
limit5hResetMode
)
: sumProviderCostInTimeRange(providerId, range5h.startTime, range5h.endTime),
sumProviderCostInTimeRange(providerId, rangeDaily.startTime, rangeDaily.endTime),
sumProviderCostInTimeRange(providerId, rangeWeekly.startTime, rangeWeekly.endTime),
sumProviderCostInTimeRange(providerId, rangeMonthly.startTime, rangeMonthly.endTime),
sumProviderTotalCost(providerId),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Apply totalCostResetAt when computing provider total usage

sumProviderTotalCost is invoked without the provider’s reset marker, so this path always sums lifetime ledger rows even after resetProviderTotalUsage updates providers.total_cost_reset_at. That makes quota usage shown by this action diverge from enforcement (RateLimitService.checkTotalCostLimit(..., { resetAt })), and providers can appear over the total cap immediately after an admin reset. Pass provider.totalCostResetAt to the total-cost aggregation here (and in the batch variant) to keep displayed usage consistent with runtime limit checks.

Useful? React with 👍 / 👎.

SessionTracker.getProviderSessionCount(providerId),
]);

// 获取重置时间信息
const resetDaily = await getResetInfoWithMode(
Expand Down Expand Up @@ -2779,6 +2790,10 @@ export async function getProviderLimitUsage(providerId: number): Promise<
limit: provider.limitMonthlyUsd,
resetAt: resetMonthly.resetAt!,
},
limitTotalUsd: {
current: totalCost,
limit: provider.limitTotalUsd ?? null,
},
concurrentSessions: {
current: concurrentSessions,
limit: provider.limitConcurrentSessions || 0,
Expand All @@ -2800,6 +2815,7 @@ export type ProviderLimitUsageData = {
costDaily: { current: number; limit: number | null; resetAt?: Date };
costWeekly: { current: number; limit: number | null; resetAt: Date };
costMonthly: { current: number; limit: number | null; resetAt: Date };
limitTotalUsd: { current: number; limit: number | null };
concurrentSessions: { current: number; limit: number };
};

Expand All @@ -2820,6 +2836,7 @@ export async function getProviderLimitUsageBatch(
limitDailyUsd?: number | null;
limitWeeklyUsd?: number | null;
limitMonthlyUsd?: number | null;
limitTotalUsd?: number | null;
limitConcurrentSessions?: number | null;
Comment on lines +2839 to 2840
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The providers parameter in getProviderLimitUsageBatch should include totalCostResetAt to allow correct calculation of the total cost after a reset. Note that getProviders (not in this diff) also needs to be updated to include this field in its return mapping.

    limitTotalUsd?: number | null;
    totalCostResetAt?: Date | null;
    limitConcurrentSessions?: number | null;

}>
): Promise<Map<number, ProviderLimitUsageData>> {
Expand All @@ -2845,7 +2862,9 @@ export async function getProviderLimitUsageBatch(
getTimeRangeForPeriodWithMode,
} = await import("@/lib/rate-limit/time-utils");
const { RateLimitService } = await import("@/lib/rate-limit");
const { sumProviderCostInTimeRange } = await import("@/repository/statistics");
const { sumProviderCostInTimeRange, sumProviderTotalCost } = await import(
"@/repository/statistics"
);

const providerIds = providers.map((p) => p.id);

Expand All @@ -2871,7 +2890,7 @@ export async function getProviderLimitUsageBatch(
);

// 并行查询该供应商的各周期消费(直接查询数据库)
const [cost5h, resetAt5h, costDaily, costWeekly, costMonthly] = await Promise.all([
const [cost5h, resetAt5h, costDaily, costWeekly, costMonthly, totalCost] = await Promise.all([
limit5hResetMode === "fixed"
? RateLimitService.getCurrentCost(
provider.id,
Expand All @@ -2887,6 +2906,7 @@ export async function getProviderLimitUsageBatch(
sumProviderCostInTimeRange(provider.id, rangeDaily.startTime, rangeDaily.endTime),
sumProviderCostInTimeRange(provider.id, rangeWeekly.startTime, rangeWeekly.endTime),
sumProviderCostInTimeRange(provider.id, rangeMonthly.startTime, rangeMonthly.endTime),
sumProviderTotalCost(provider.id),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to getProviderLimitUsage, this call to sumProviderTotalCost must pass provider.totalCostResetAt to ensure the batch calculation respects manual resets.

Suggested change
sumProviderTotalCost(provider.id),
sumProviderTotalCost(provider.id, provider.totalCostResetAt),

Comment on lines 2906 to +2909
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sumProviderTotalCost(...) runs for every provider even when limitTotalUsd is null/0 (i.e., total quota disabled). This adds an extra DB aggregate per provider for no user-visible benefit. Consider only querying total cost when limitTotalUsd is a positive number (otherwise skip and set current to 0 or omit the field).

Copilot uses AI. Check for mistakes.
]);

const sessionCount = sessionCountMap.get(provider.id) || 0;
Expand Down Expand Up @@ -2926,6 +2946,10 @@ export async function getProviderLimitUsageBatch(
limit: provider.limitMonthlyUsd ?? null,
resetAt: resetMonthly.resetAt!,
},
limitTotalUsd: {
current: totalCost,
limit: provider.limitTotalUsd ?? null,
},
concurrentSessions: {
current: sessionCount,
limit: provider.limitConcurrentSessions || 0,
Expand Down
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] [ERROR-NO-USER-FEEDBACK] Current-usage tooltip hidden when total limit is exactly 0

Why this is a problem: The condition currentQuota?.costTotal.limit && ... is falsy when limit is 0. Because an admin can legitimately set a limitTotalUsd of 0 to hard-block a key, the tooltip won't appear in that case even though there is an active limit.

Suggested fix:

Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ interface KeyQuota {
costDaily: { current: number; limit: number | null; resetAt?: Date };
costWeekly: { current: number; limit: number | null };
costMonthly: { current: number; limit: number | null };
costTotal: { current: number; limit: number | null };
concurrentSessions: { current: number; limit: number };
}

Expand Down Expand Up @@ -79,6 +80,9 @@ export function EditKeyQuotaDialog({
const [limitMonthly, setLimitMonthly] = useState<string>(
currentQuota?.costMonthly.limit?.toString() ?? ""
);
const [limitTotal, setLimitTotal] = useState<string>(
currentQuota?.costTotal.limit?.toString() ?? ""
);
const [limitConcurrent, setLimitConcurrent] = useState<string>(
currentQuota?.concurrentSessions.limit?.toString() ?? "0"
);
Expand All @@ -98,6 +102,7 @@ export function EditKeyQuotaDialog({
dailyResetTime: resetTime,
limitWeeklyUsd: limitWeekly ? parseFloat(limitWeekly) : null,
limitMonthlyUsd: limitMonthly ? parseFloat(limitMonthly) : null,
limitTotalUsd: limitTotal ? parseFloat(limitTotal) : null,
limitConcurrentSessions: limitConcurrent ? parseInt(limitConcurrent, 10) : 0,
});

Expand Down Expand Up @@ -127,6 +132,7 @@ export function EditKeyQuotaDialog({
dailyResetTime: resetTime,
limitWeeklyUsd: null,
limitMonthlyUsd: null,
limitTotalUsd: null,
limitConcurrentSessions: 0,
});

Expand Down Expand Up @@ -335,6 +341,32 @@ export function EditKeyQuotaDialog({
)}
</div>

{/* 总限额 */}
<div className="grid gap-1.5">
<Label htmlFor="limitTotal" className="text-xs">
{t("limitTotalUsd.label")}
</Label>
<Input
id="limitTotal"
type="number"
step="0.01"
min="0"
placeholder={t("limitTotalUsd.placeholder")}
value={limitTotal}
onChange={(e) => setLimitTotal(e.target.value)}
className="h-9"
/>
{currentQuota?.costTotal.limit && (
<p className="text-xs text-muted-foreground">
{t("limitTotalUsd.current", {
currency: currencySymbol,
current: Number(currentQuota.costTotal.current).toFixed(4),
limit: Number(currentQuota.costTotal.limit).toFixed(2),
})}
</p>
Comment on lines +353 to +366
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Current-usage tooltip hidden when limit is exactly 0

The condition currentQuota?.costTotal.limit && … is falsy when limit is 0. Because an admin can legitimately set a limitTotalUsd of 0 to hard-block a key, the tooltip won't appear in that case even though there is an active limit.

Suggested change
min="0"
placeholder={t("limitTotalUsd.placeholder")}
value={limitTotal}
onChange={(e) => setLimitTotal(e.target.value)}
className="h-9"
/>
{currentQuota?.costTotal.limit && (
<p className="text-xs text-muted-foreground">
{t("limitTotalUsd.current", {
currency: currencySymbol,
current: Number(currentQuota.costTotal.current).toFixed(4),
limit: Number(currentQuota.costTotal.limit).toFixed(2),
})}
</p>
{currentQuota?.costTotal.limit !== null && currentQuota?.costTotal.limit !== undefined && (
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/quotas/keys/_components/edit-key-quota-dialog.tsx
Line: 351-364

Comment:
**Current-usage tooltip hidden when limit is exactly `0`**

The condition `currentQuota?.costTotal.limit && …` is falsy when `limit` is `0`. Because an admin can legitimately set a `limitTotalUsd` of `0` to hard-block a key, the tooltip won't appear in that case even though there is an active limit.

```suggestion
                {currentQuota?.costTotal.limit !== null && currentQuota?.costTotal.limit !== undefined && (
```

How can I resolve this? If you propose a fix, please make it concise.

)}
</div>

{/* 并发限额 */}
<div className="grid gap-1.5">
<Label htmlFor="limitConcurrent" className="text-xs">
Expand Down
Loading
Loading