Skip to content
Merged
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
2 changes: 2 additions & 0 deletions src/actions/key-quota.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface KeyQuotaItem {
limit: number | null;
mode?: "fixed" | "rolling";
time?: string;
resetAt?: Date;
}

export interface KeyQuotaUsageResult {
Expand Down Expand Up @@ -164,6 +165,7 @@ export async function getKeyQuotaUsage(keyId: number): Promise<ActionResult<KeyQ
type: "limitTotal",
current: totalCost,
limit: parseNumericLimit(keyRow.limitTotalUsd),
resetAt: costResetAt ?? undefined,
},
{
type: "limitSessions",
Expand Down
3 changes: 2 additions & 1 deletion src/actions/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -837,7 +837,7 @@ export async function getKeyLimitUsage(keyId: 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 };
costTotal: { current: number; limit: number | null };
costTotal: { current: number; limit: number | null; resetAt?: Date };
concurrentSessions: { current: number; limit: number };
}>
> {
Expand Down Expand Up @@ -958,6 +958,7 @@ export async function getKeyLimitUsage(keyId: number): Promise<
costTotal: {
current: totalCost,
limit: key.limitTotalUsd ?? null,
resetAt: costResetAt ?? undefined,
},
concurrentSessions: {
current: concurrentSessions,
Expand Down
54 changes: 42 additions & 12 deletions src/actions/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ export async function getProviders(): Promise<ProviderDisplay[]> {
limitWeeklyUsd: provider.limitWeeklyUsd,
limitMonthlyUsd: provider.limitMonthlyUsd,
limitTotalUsd: provider.limitTotalUsd,
totalCostResetAt: provider.totalCostResetAt,
limitConcurrentSessions: provider.limitConcurrentSessions,
maxRetryAttempts: provider.maxRetryAttempts,
circuitBreakerFailureThreshold: provider.circuitBreakerFailureThreshold,
Expand Down Expand Up @@ -1259,6 +1260,8 @@ export async function resetProviderTotalUsage(providerId: number): Promise<Actio
return { ok: false, error: "供应商不存在" };
}

await publishProviderCacheInvalidation();

return { ok: true };
} catch (error) {
logger.error("重置供应商总用量失败:", error);
Expand Down Expand Up @@ -2690,6 +2693,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; resetAt?: Date };
concurrentSessions: { current: number; limit: number };
}>
> {
Expand All @@ -2713,7 +2717,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 +2738,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, provider.totalCostResetAt),
SessionTracker.getProviderSessionCount(providerId),
]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// 获取重置时间信息
const resetDaily = await getResetInfoWithMode(
Expand Down Expand Up @@ -2779,6 +2793,11 @@ export async function getProviderLimitUsage(providerId: number): Promise<
limit: provider.limitMonthlyUsd,
resetAt: resetMonthly.resetAt!,
},
limitTotalUsd: {
current: totalCost,
limit: provider.limitTotalUsd ?? null,
resetAt: provider.totalCostResetAt ?? undefined,
},
concurrentSessions: {
current: concurrentSessions,
limit: provider.limitConcurrentSessions || 0,
Expand All @@ -2800,6 +2819,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; resetAt?: Date };
concurrentSessions: { current: number; limit: number };
};

Expand All @@ -2820,6 +2840,8 @@ export async function getProviderLimitUsageBatch(
limitDailyUsd?: number | null;
limitWeeklyUsd?: number | null;
limitMonthlyUsd?: number | null;
limitTotalUsd?: number | null;
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

To support correct total cost calculation in batch mode, the totalCostResetAt field should be included in the provider data passed to this function.

Suggested change
limitTotalUsd?: number | null;
limitTotalUsd?: number | null;
totalCostResetAt?: Date | null;

totalCostResetAt?: Date | null;
limitConcurrentSessions?: number | null;
}>
): Promise<Map<number, ProviderLimitUsageData>> {
Expand All @@ -2845,7 +2867,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 +2895,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 +2911,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, provider.totalCostResetAt ?? null),
]);
Comment on lines 2911 to 2915
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.

getProviderLimitUsageBatch computes totalCost via sumProviderTotalCost(provider.id) but does not thread through the provider’s total-cost reset timestamp (e.g. totalCostResetAt). This makes batch results inconsistent with the total-limit reset semantics and with RateLimitService.checkTotalCostLimit({ resetAt }). Update the batch provider input type to include the reset field and pass it into sumProviderTotalCost.

Copilot uses AI. Check for mistakes.

const sessionCount = sessionCountMap.get(provider.id) || 0;
Expand Down Expand Up @@ -2926,6 +2951,11 @@ export async function getProviderLimitUsageBatch(
limit: provider.limitMonthlyUsd ?? null,
resetAt: resetMonthly.resetAt!,
},
limitTotalUsd: {
current: totalCost,
limit: provider.limitTotalUsd ?? null,
resetAt: provider.totalCostResetAt ?? undefined,
},
concurrentSessions: {
current: sessionCount,
limit: provider.limitConcurrentSessions || 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ interface LimitUsageData {
costDaily: { current: number; limit: number | null };
costWeekly: { current: number; limit: number | null };
costMonthly: { current: number; limit: number | null };
costTotal: { current: number; limit: number | null };
costTotal: { current: number; limit: number | null; resetAt?: Date };
concurrentSessions: { current: number; limit: number };
}

Expand Down
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; resetAt?: Date };
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() ?? ""
);
Comment on lines +83 to +85
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

currentQuota?.costTotal 的访问与 KeyQuota 可选性约定不一致。

本文件 Line 35 把 costTotal 声明为必填,这里(Line 84、359、363、364)也按必填访问;而 src/lib/utils/quota-helpers.tsKeyQuota.costTotal 是可选(costTotal?)。如果上游数据真的可能缺该字段,这些访问会在运行时抛错;反之应在 quota-helpers.ts 中也改为必填以消除歧义。建议先确定数据契约,再统一类型。

Also applies to: 359-367

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/dashboard/quotas/keys/_components/edit-key-quota-dialog.tsx
around lines 83 - 85, The component accesses currentQuota.costTotal as if always
present while KeyQuota.costTotal is declared optional in quota-helpers.ts;
either make the data contract consistent by removing the optional from
KeyQuota.costTotal in quota-helpers.ts (if costTotal is guaranteed) or make the
component defensive: update usages in edit-key-quota-dialog.tsx (references:
currentQuota, costTotal, the useState init for limitTotal and the later accesses
around the block noted ~lines 359–367) to guard against undefined (use optional
chaining, provide safe defaults, and gate UI/form logic when costTotal is
missing) so runtime errors cannot occur. Ensure the chosen change is applied
consistently across all places that read costTotal.

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,
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.

[Medium] [LOGIC-BUG] "Clear All" button visibility condition missing costTotal.limit

Why this is a problem: This new line clears limitTotalUsd when the admin clicks "Clear All", but the button visibility condition (in DialogFooter, the currentQuota?.cost5h.limit || costWeekly.limit || ... expression) does not include currentQuota?.costTotal?.limit. If a key has only limitTotalUsd set (no 5h/weekly/monthly/concurrent quotas), the "Clear All" button will not appear, making it impossible to clear the total quota via this button. Note: costDaily.limit has the same pre-existing gap.

Suggested fix - Update the button visibility condition in DialogFooter:

{(currentQuota?.cost5h.limit ||
  currentQuota?.costDaily.limit ||
  currentQuota?.costWeekly.limit ||
  currentQuota?.costMonthly.limit ||
  currentQuota?.costTotal?.limit ||
  (currentQuota?.concurrentSessions.limit ?? 0) > 0) && (

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),
})}
Comment on lines +355 to +365
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Current-usage hint hidden when limit is 0

currentQuota?.costTotal.limit && (...) is falsy when limit === 0, so the usage hint is silently suppressed for a zero limit. Other quota fields use limit !== null as the guard, which is safer.

Suggested change
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),
})}
{currentQuota?.costTotal.limit !== null && (
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: 355-365

Comment:
**Current-usage hint hidden when limit is 0**

`currentQuota?.costTotal.limit && (...)` is falsy when `limit === 0`, so the usage hint is silently suppressed for a zero limit. Other quota fields use `limit !== null` as the guard, which is safer.

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

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

</p>
)}
Comment on lines +359 to +367
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

limit 为 0 时不应吞掉提示展示。

L359 currentQuota?.costTotal.limit && (...) 使用 truthy 判断,当 limit === 0 时提示行不会渲染。虽然项目其它分支(limit5h 等)均沿用了同一模式,但本 PR 的目标说明里明确要求改为 limit !== null 以允许 0 值场景显示。建议至少在新增的 costTotal 上采用更准确的判定:

建议修改
-                {currentQuota?.costTotal.limit && (
+                {currentQuota?.costTotal?.limit != null && (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/dashboard/quotas/keys/_components/edit-key-quota-dialog.tsx
around lines 359 - 367, The conditional rendering for the costTotal line uses a
truthy check and hides the row when limit === 0; update the check in
edit-key-quota-dialog.tsx to explicitly test for null/undefined (e.g.,
currentQuota?.costTotal.limit !== null && currentQuota?.costTotal.limit !==
undefined or currentQuota?.costTotal.limit != null) so that a 0 limit still
renders the paragraph that references currentQuota.costTotal.current and .limit;
make the same change only for the new costTotal branch (leave other branches
unchanged).

</div>

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