Skip to content

fix total quota#1126

Merged
ding113 merged 4 commits intoding113:devfrom
ilnli:dev
Apr 28, 2026
Merged

fix total quota#1126
ding113 merged 4 commits intoding113:devfrom
ilnli:dev

Conversation

@ilnli
Copy link
Copy Markdown
Contributor

@ilnli ilnli commented Apr 27, 2026

Summary

Adds total cost quota (`limitTotalUsd`) support to provider and API key quota management, completing the quota system alongside existing 5h, daily, weekly, and monthly quotas.

Problem

The quota system previously supported time-windowed limits (5h, daily, weekly, monthly) but lacked a lifetime/total cost limit. This prevented administrators from setting a hard cap on total spend for providers or keys, which is essential for budget control and preventing runaway costs.

Solution

Extends the quota system to support `limitTotalUsd` (total cost limit) for both providers and API keys:

  • Added total cost tracking using `sumProviderTotalCost()` repository function
  • Updated UI components to display and edit total quotas
  • Added i18n translations for all 5 supported languages (EN, JA, RU, ZH-CN, ZH-TW)
  • Updated quota helper functions to include total cost in usage calculations

Changes

Core Changes

  • `src/actions/providers.ts`: Added `limitTotalUsd` to `getProviderLimitUsage()` and `getProviderLimitUsageBatch()` functions
  • `src/lib/utils/quota-helpers.ts`: Updated `hasKeyQuotaSet()` and `getMaxUsageRate()` to consider total cost quota
  • `src/repository/statistics.ts`: Uses `sumProviderTotalCost()` for calculating lifetime spend

UI Changes

  • `src/app/[locale]/dashboard/quotas/providers/_components/`: Added total quota display and editing in provider quota list and client components
  • `src/app/[locale]/dashboard/quotas/keys/_components/`: Added total quota column to keys quota table and edit dialog

i18n Updates

  • `messages/*/quota.json`: Added translations for "Total Cost" labels across all 5 languages

Test Updates

  • `tests/unit/actions/providers-usage.test.ts`: Added mock for `sumProviderTotalCost`

Testing

Automated Tests

  • Unit tests updated (`providers-usage.test.ts`)

Manual Testing

  1. Navigate to Provider Quotas page
  2. Verify total quota appears in provider list items
  3. Edit a key quota and verify total quota field is present
  4. Check that quota progress bars display correctly for total limits

Related PRs

Checklist

  • i18n strings added for all supported languages (per CLAUDE.md requirements)
  • Unit tests updated
  • No breaking changes to existing quota APIs
  • UI components properly display total quota status

Description enhanced by Claude AI

Greptile Summary

This PR completes the quota system by adding limitTotalUsd (lifetime total cost limit) support for both providers and API keys, covering data fetching, UI display, quick-edit, and i18n across all five supported languages. The core logic is sound: sumProviderTotalCost correctly receives totalCostResetAt in both the single and batch paths, and publishProviderCacheInvalidation is now properly called after resetProviderTotalUsage.

Confidence Score: 5/5

Safe to merge; only P2 style/test quality issues found.

All core logic (quota enforcement, resetAt forwarding, cache invalidation) is correct. Two P2 findings: the test mock drops the resetAt argument, and the list item omits resetAt when calling renderQuotaItem, suppressing the countdown timer for total quota after a manual reset. Neither causes incorrect quota enforcement or data loss.

provider-quota-list-item.tsx (missing resetAt in renderQuotaItem) and providers-usage.test.ts (mock drops resetAt arg).

Important Files Changed

Filename Overview
src/actions/providers.ts Adds limitTotalUsd tracking to both single and batch provider usage functions; sumProviderTotalCost now correctly receives totalCostResetAt in both paths. Also adds a publishProviderCacheInvalidation call to resetProviderTotalUsage.
src/app/[locale]/dashboard/quotas/providers/_components/provider-quota-list-item.tsx Renders total quota item via renderQuotaItem but omits resetAt, so the countdown timer is never shown after a manual reset.
tests/unit/actions/providers-usage.test.ts Adds sumProviderTotalCostMock but the mock wrapper silently drops the resetAt argument, preventing future assertions on argument correctness.
src/lib/utils/quota-helpers.ts Extends hasKeyQuotaSet and getMaxUsageRate to include costTotal; uses optional chaining since the field is optional in KeyQuota, consistent with surrounding patterns.
src/app/[locale]/dashboard/quotas/keys/_components/edit-key-quota-dialog.tsx Adds limitTotal state and input field for the total quota in the key edit dialog; wired to limitTotalUsd on save.
src/app/[locale]/dashboard/quotas/keys/_components/keys-quota-client.tsx Adds a Total Quota column to the keys quota table with progress bar, quick-edit popover, and usage rate display.
src/app/[locale]/dashboard/quotas/providers/_components/providers-quota-client.tsx Updates hasQuotaLimit and calculateMaxUsage to include limitTotalUsd; null/zero checks are correct and consistent.
src/types/provider.ts Adds `totalCostResetAt?: Date

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[getProviderLimitUsage / getProviderLimitUsageBatch] --> B[sumProviderTotalCost\nproviderId, totalCostResetAt]
    A --> C[sumProviderCostInTimeRange\n5h / daily / weekly / monthly]
    B --> D[limitTotalUsd result\ncurrent, limit, resetAt]
    C --> E[cost5h / costDaily / costWeekly / costMonthly]
    D --> F[ProviderLimitUsageData]
    E --> F
    F --> G[UI: providers-quota-client\nhasQuotaLimit / calculateMaxUsage]
    F --> H[UI: provider-quota-list-item\nrenderQuotaItem — resetAt missing]
    F --> I[UI: keys-quota-client\nTotal Quota column]

    J[quota-helpers.ts\nhasKeyQuotaSet / getMaxUsageRate] --> K[includes costTotal?.limit]
    L[edit-key-quota-dialog\nlimitTotal state] --> M[updateKeyQuota\nlimitTotalUsd field]
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: tests/unit/actions/providers-usage.test.ts
Line: 41

Comment:
**Mock drops the `resetAt` argument**

The mock signature only captures `providerId`, silently dropping the second `resetAt` argument. The real call in `providers.ts` passes `provider.totalCostResetAt` (single path) and `provider.totalCostResetAt ?? null` (batch path). Any future assertion that verifies the correct `resetAt` is forwarded will always receive `undefined` instead.

```suggestion
  sumProviderTotalCost: (providerId: number, resetAt?: Date | null) =>
    sumProviderTotalCostMock(providerId, resetAt),
```

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

---

This is a comment left during a code review.
Path: src/app/[locale]/dashboard/quotas/providers/_components/provider-quota-list-item.tsx
Line: 222-228

Comment:
**`resetAt` not forwarded — countdown timer never shown for total quota**

Every other quota item (daily, weekly, monthly) passes its `resetAt` to `renderQuotaItem`, which conditionally renders a `CountdownTimer`. The total quota call omits `provider.quota.limitTotalUsd.resetAt`, so after an admin triggers a manual reset via `resetProviderTotalUsage`, the list item will never display the reset timestamp, unlike all sibling items.

```suggestion
        {provider.quota.limitTotalUsd.limit &&
          provider.quota.limitTotalUsd.limit > 0 &&
          renderQuotaItem(
            t("costTotal.label"),
            provider.quota.limitTotalUsd.current,
            provider.quota.limitTotalUsd.limit,
            provider.quota.limitTotalUsd.resetAt
          )}
```

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

Reviews (2): Last reviewed commit: "fix(quota): honor total reset markers" | Re-trigger Greptile

Copilot AI review requested due to automatic review settings April 27, 2026 15:53
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

📝 Walkthrough

Walkthrough

添加并传播“总 USD 配额 / 总消耗”维度:新增 i18n 文本,扩展后端动作返回值与输入,更新类型定义,并在前端多处 quota UI(提供商与密钥)显示、编辑与计算中加入该维度及其可选 resetAt 时间戳。

Changes

Cohort / File(s) Summary
国际化文件
messages/en/quota.json, messages/ja/quota.json, messages/ru/quota.json, messages/zh-CN/quota.json, messages/zh-TW/quota.json
新增用于“Total/总限额/总成本”显示与编辑的本地化键(例如 providers.costTotal.label, keys.table.costTotal, keys.editDialog.limitTotalUsd 等)。
提供商后端与类型
src/actions/providers.ts, src/types/provider.ts
在 provider 层添加 limitTotalUsd / totalCostResetAt 字段,getProviderLimitUsage* 调用集成 sumProviderTotalCost 以计算总消耗,返回数据与类型签名扩展;reset 后触发缓存无效化。
密钥后端与类型
src/actions/key-quota.ts, src/actions/keys.ts
KeyQuota/KeyQuotaItem 与 getKeyLimitUsage 返回中新增/传递 costTotalresetAt 时间戳;构建 quota 项目时包含 resetAt。
前端:密钥配额组件
src/app/[locale]/dashboard/quotas/keys/_components/edit-key-quota-dialog.tsx, src/app/[locale]/dashboard/quotas/keys/_components/keys-quota-client.tsx, src/app/[locale]/dashboard/quotas/keys/_components/keys-quota-manager.tsx
扩展 KeyQuota 类型以包含 costTotal,在表格和编辑对话框中添加“总限额 (USD)”输入、显示当前/限额、进度与快速编辑保存逻辑(使用 limitTotalUsd 补丁字段)。
前端:提供商配额组件与页面
src/app/[locale]/dashboard/quotas/providers/_components/provider-quota-list-item.tsx, src/app/[locale]/dashboard/quotas/providers/_components/providers-quota-client.tsx, src/app/[locale]/dashboard/quotas/providers/_components/providers-quota-manager.tsx, src/app/[locale]/dashboard/quotas/providers/page.tsx
扩展 ProviderQuota 类型以包含 limitTotalUsd,将其纳入有配额检测、使用率计算与排序逻辑;批量请求映射加入 limitTotalUsdtotalCostResetAt
工具函数与共享类型
src/lib/utils/quota-helpers.ts, src/app/[locale]/dashboard/_components/user/key-limit-usage.tsx
在 KeyQuota/LimitUsageData 类型中加入可选 costTotal.resetAt,并在 hasKeyQuotaSet / getMaxUsageRate 中纳入 costTotal 的比率计算。
测试
tests/unit/actions/providers-usage.test.ts
sumProviderTotalCost 添加了测试 mock(默认返回 0),以匹配后端使用新增统计函数的场景。

代码审查工作量估计

🎯 3 (中等) | ⏱️ ~20 分钟

Possibly related PRs

概述

此 PR 在多语言本地化文件、后端操作和前端 UI 组件中添加了新的"总 USD 配额"功能。新增了国际化字符串来支持提供商和密钥的总成本配额显示与编辑,扩展了提供商和密钥配额数据结构,并更新了计算逻辑以包含新的配额维度。

变更

队列/文件 摘要
国际化文件
messages/en/quota.json, messages/ja/quota.json, messages/ru/quota.json, messages/zh-CN/quota.json, messages/zh-TW/quota.json
为提供商成本标签、密钥表格列标题和密钥编辑对话框添加了新的本地化字符串,支持"总配额"概念的显示和编辑。
提供商配额后端
src/actions/providers.ts
添加了 limitTotalUsd 字段到 ProviderLimitUsageData 类型,并在 getProviderLimitUsagegetProviderLimitUsageBatch 中集成 sumProviderTotalCost 调用来计算总成本。
密钥配额 UI 组件
src/app/.../keys/_components/edit-key-quota-dialog.tsx, src/app/.../keys/_components/keys-quota-client.tsx, src/app/.../keys/_components/keys-quota-manager.tsx
扩展 KeyQuota 类型以包含 costTotal 字段,并在编辑对话框和列表中添加新的 USD 总限额输入控件及其显示逻辑。
提供商配额 UI 组件
src/app/.../providers/_components/provider-quota-list-item.tsx, src/app/.../providers/_components/providers-quota-client.tsx, src/app/.../providers/_components/providers-quota-manager.tsx, src/app/.../providers/page.tsx
扩展 ProviderQuota 类型以包含 limitTotalUsd 字段,更新有配额检测和使用率计算逻辑以支持总 USD 配额。
工具函数与测试
src/lib/utils/quota-helpers.ts, tests/unit/actions/providers-usage.test.ts
更新 KeyQuota 类型和 hasKeyQuotaSetgetMaxUsageRate 函数以支持 costTotal 维度;为 sumProviderTotalCost 添加单元测试模拟。

代码审查工作量估计

🎯 3 (中等) | ⏱️ ~20 分钟

相关 PR

  • PR #1061:都修改了 src/actions/providers.ts,扩展了提供商限额使用数据的形状和逻辑。
  • PR #1088:都修改了 keys-quota-client 组件和配额编辑保存流程,添加快速编辑和总成本配额处理。
  • PR #853:都添加和扩展了总成本求和函数(sumProviderTotalCost、sumKeyTotalCost 等)及其总成本处理的传播。
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 标题'fix total quota'与变更集的主要目标相关联,反映了该PR的核心目标——添加总成本配额功能。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed 拉取请求描述与代码变更密切相关,详细说明了添加总成本配额支持的问题、解决方案和实现细节。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a 'Total Quota' (limitTotalUsd) feature for providers and keys, enabling the tracking and enforcement of an overall cost limit. The changes span across localization files, backend actions for usage calculation, and frontend components for displaying and managing these quotas. The review feedback highlights a critical need to incorporate the totalCostResetAt timestamp into the cost summation logic to ensure that usage is correctly calculated relative to the last manual reset.

Comment thread src/actions/providers.ts Outdated
Comment thread src/actions/providers.ts
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;

Comment thread src/actions/providers.ts Outdated
Comment thread src/actions/providers.ts Outdated
Comment thread src/actions/providers.ts Outdated
Comment on lines +355 to +365
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),
})}
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.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ff7d5b6620

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/actions/providers.ts Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds “total quota / total cost” support to quota calculation and the admin dashboard UI, so providers/keys can display and manage lifetime (or reset-based) spend limits alongside existing 5h/daily/weekly/monthly and concurrency limits.

Changes:

  • Extend quota data structures and UI to include total cost / total quota fields.
  • Update provider usage actions to fetch and return total cost usage (sumProviderTotalCost) for single and batch endpoints.
  • Add i18n strings for the new total-quota labels across supported locales and update unit tests’ mocks accordingly.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/unit/actions/providers-usage.test.ts Mocks sumProviderTotalCost for updated provider usage actions.
src/lib/utils/quota-helpers.ts Adds optional costTotal to KeyQuota and includes it in quota detection/max-usage calculations.
src/actions/providers.ts Adds limitTotalUsd usage to provider limit usage responses and calls sumProviderTotalCost.
src/app/[locale]/dashboard/quotas/providers/page.tsx Includes limitTotalUsd when building the batch quota request inputs.
src/app/[locale]/dashboard/quotas/providers/_components/providers-quota-manager.tsx Updates provider quota typing to include limitTotalUsd.
src/app/[locale]/dashboard/quotas/providers/_components/providers-quota-client.tsx Considers total quota in “has any quota” logic and usage-based sorting.
src/app/[locale]/dashboard/quotas/providers/_components/provider-quota-list-item.tsx Renders the total quota item in the provider quota list UI.
src/app/[locale]/dashboard/quotas/keys/_components/keys-quota-manager.tsx Updates key quota typing to include costTotal.
src/app/[locale]/dashboard/quotas/keys/_components/keys-quota-client.tsx Adds a “total quota” column and rendering/editing UI for key total quota.
src/app/[locale]/dashboard/quotas/keys/_components/edit-key-quota-dialog.tsx Adds form state + submission payload for editing limitTotalUsd.
messages/zh-TW/quota.json Adds total cost/quota labels and edit-dialog strings.
messages/zh-CN/quota.json Adds total cost/quota labels and edit-dialog strings.
messages/ru/quota.json Adds total cost/quota labels and edit-dialog strings.
messages/ja/quota.json Adds total cost/quota labels and edit-dialog strings.
messages/en/quota.json Adds total cost/quota labels and edit-dialog strings.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/actions/providers.ts Outdated
Comment thread src/actions/providers.ts
Comment on lines 2906 to 2910
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

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.
limitDailyUsd: p.limitDailyUsd,
limitWeeklyUsd: p.limitWeeklyUsd,
limitMonthlyUsd: p.limitMonthlyUsd,
limitTotalUsd: p.limitTotalUsd,
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.

The batch quota fetch currently passes limitTotalUsd but not the provider’s total-cost reset timestamp (e.g. totalCostResetAt). If sumProviderTotalCost is meant to respect manual resets, you’ll need to include that field in this mapping and in the getProviderLimitUsageBatch input type, otherwise limitTotalUsd.current will be computed from lifetime spend.

Suggested change
limitTotalUsd: p.limitTotalUsd,
limitTotalUsd: p.limitTotalUsd,
totalCostResetAt: p.totalCostResetAt,

Copilot uses AI. Check for mistakes.
Comment on lines +92 to +93
if (quota.costTotal?.limit) {
rates.push(getUsageRate(quota.costTotal.current, quota.costTotal.limit));
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.

In strict TS mode, if (quota.costTotal?.limit) { ... quota.costTotal.current ... } does not reliably narrow quota.costTotal (it can still be undefined), which can cause a compile error (Object is possibly 'undefined'). Use an explicit guard (if (quota.costTotal && quota.costTotal.limit)) or assign const total = quota.costTotal and check total?.limit before accessing total.current/total.limit.

Suggested change
if (quota.costTotal?.limit) {
rates.push(getUsageRate(quota.costTotal.current, quota.costTotal.limit));
const total = quota.costTotal;
if (total?.limit) {
rates.push(getUsageRate(total.current, total.limit));

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/app/[locale]/dashboard/quotas/keys/_components/edit-key-quota-dialog.tsx (1)

396-409: ⚠️ Potential issue | 🟠 Major

"清除全部限额"按钮的显示条件遗漏了 costTotal.limit(以及历史上遗漏的 costDaily.limit)。

当前条件只检查 cost5h / costWeekly / costMonthly / concurrentSessions。因此当用户仅设置了"总限额"(limitTotalUsd)时,按钮不会渲染,用户无法在 UI 上一键清除该限额——这正是本 PR 新增维度引入的 UX 回归。costDaily 同样被遗漏(历史问题,顺手一并修复更稳妥)。

建议修复
-            {(currentQuota?.cost5h.limit ||
-              currentQuota?.costWeekly.limit ||
-              currentQuota?.costMonthly.limit ||
-              (currentQuota?.concurrentSessions.limit ?? 0) > 0) && (
+            {(currentQuota?.cost5h.limit ||
+              currentQuota?.costDaily.limit ||
+              currentQuota?.costWeekly.limit ||
+              currentQuota?.costMonthly.limit ||
+              currentQuota?.costTotal?.limit ||
+              (currentQuota?.concurrentSessions.limit ?? 0) > 0) && (
🤖 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 396 - 409, The clear-all button render condition in DialogFooter
uses currentQuota checks but omits costTotal and costDaily, so when only
limitTotalUsd or costDaily.limit is set the Button (variant "destructive",
onClick handleClearQuota) won't render; update the boolean expression that
decides rendering to include currentQuota?.costTotal.limit and
currentQuota?.costDaily.limit (and keep the existing
currentQuota?.concurrentSessions.limit null-coalescing check) so the Button (and
its disabled prop isPending and label t("clearAll")) appears whenever any quota
dimension is > 0.
tests/unit/actions/providers-usage.test.ts (1)

20-42: ⚠️ Potential issue | 🟡 Minor

总额度路径仅被 mock,尚未被断言验证。

目前只新增了 sumProviderTotalCost mock,但没有断言它被调用、也没有校验 result.data.limitTotalUsd.current。这会让总额度回归问题漏检。建议在单条与批量用例里补上对应断言。

建议补充的测试断言示例
it("should return correct structure with DB-sourced costs", async () => {
  sumProviderCostInTimeRangeMock
    .mockResolvedValueOnce(1.5)
    .mockResolvedValueOnce(10.0)
    .mockResolvedValueOnce(45.0)
    .mockResolvedValueOnce(120.0);
+ sumProviderTotalCostMock.mockResolvedValueOnce(300.0);

  const { getProviderLimitUsage } = await import("@/actions/providers");
  const result = await getProviderLimitUsage(1);

  expect(result.ok).toBe(true);
  if (result.ok) {
+   expect(sumProviderTotalCostMock).toHaveBeenCalledWith(1);
+   expect(result.data.limitTotalUsd.current).toBe(300.0);
    expect(result.data.cost5h.current).toBe(1.5);
    expect(result.data.costDaily.current).toBe(10.0);
    expect(result.data.costWeekly.current).toBe(45.0);
    expect(result.data.costMonthly.current).toBe(120.0);
  }
});

Also applies to: 171-171, 409-409

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

In `@tests/unit/actions/providers-usage.test.ts` around lines 20 - 42, The tests
define sumProviderTotalCostMock but never assert it or the returned total limit,
so add assertions in both the single-provider and batch-provider test cases to
ensure the mock is called with the correct provider id(s) and that
result.data.limitTotalUsd.current equals the expected mocked value; update the
tests that exercise the code paths which call sumProviderTotalCost (refer to
sumProviderTotalCostMock) to set a deterministic return value and assert both
sumProviderTotalCostMock was invoked and result.data.limitTotalUsd.current
matches that value (do this for the individual test and the batch test where
multiple providers are evaluated).
🧹 Nitpick comments (3)
src/app/[locale]/dashboard/quotas/providers/_components/providers-quota-client.tsx (1)

39-51: hasQuotaLimitcalculateMaxUsagelimitTotalUsd 的顺序不一致(可选优化)。

此处 limitTotalUsd 被放在判断列表的最前面(Line 44),而 calculateMaxUsage(Line 76-78)中又被追加在末尾。两个函数功能相关,统一顺序可提升可读性,例如都按"5h → daily → weekly → monthly → total → concurrent"排列。

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

In
`@src/app/`[locale]/dashboard/quotas/providers/_components/providers-quota-client.tsx
around lines 39 - 51, Reorder the checks inside hasQuotaLimit to match the same
time-sequence used in calculateMaxUsage: check cost5h, costDaily, costWeekly,
costMonthly, then limitTotalUsd, and finally concurrentSessions; update the
boolean expression in hasQuotaLimit (function name: hasQuotaLimit, symbol:
quota.limitTotalUsd, quota.cost5h, quota.costDaily, quota.costWeekly,
quota.costMonthly, quota.concurrentSessions) so the order matches
calculateMaxUsage for consistent readability.
messages/en/quota.json (1)

242-246: 与已存在的 editKeyForm.limitTotalUsd.label 文案略有差异(可选)。

新增的 keys.editDialog.limitTotalUsd.label = "Total Quota (USD)",而本文件已有的 keys.editKeyForm.limitTotalUsd.label(Line 348)= "Total Cost Limit (USD)"。两处指向同一概念但用词不同(Quota vs Cost Limit)。如果希望端到端文案统一,建议对齐其中一种,并同步五种语言文件。否则按现状即可,影响很小。

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

In `@messages/en/quota.json` around lines 242 - 246,
两个文案键在表达同一概念但用词不一致:keys.editDialog.limitTotalUsd.label ("Total Quota (USD)") 与
keys.editKeyForm.limitTotalUsd.label ("Total Cost Limit (USD)");请选择统一的术语(例如统一为
"Total Cost Limit (USD)" 或 "Total Quota (USD)"),然后在所有语言文件中同步替换相应键的值(确保
keys.editDialog.limitTotalUsd.label 和 keys.editKeyForm.limitTotalUsd.label
在各语言版本一致),提交修改以保持端到端文案统一。
src/app/[locale]/dashboard/quotas/providers/_components/provider-quota-list-item.tsx (1)

13-20: limitTotalUsd 字段命名与同接口其它成本字段风格不统一。

ProviderQuota 中其它成本维度字段名都是 cost5h/costDaily/costWeekly/costMonthly,而新增的"总额"被命名为 limitTotalUsd,既不遵循 cost* 命名模式,也不必要地在字段名中包含"Usd"后缀。

一致的命名应为 costTotal,与 KeyQuota.costTotalsrc/lib/utils/quota-helpers.ts)对齐,提升代码可读性。

建议改名
   concurrentSessions: { current: number; limit: number };
-  limitTotalUsd: { current: number; limit: number | null };
+  costTotal: { current: number; limit: number | null };
 }

并同步更新 providers-quota-client.tsx 中的字段引用。

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

In
`@src/app/`[locale]/dashboard/quotas/providers/_components/provider-quota-list-item.tsx
around lines 13 - 20, Rename the inconsistent field limitTotalUsd in the
ProviderQuota interface to costTotal to match the cost* naming pattern and align
with KeyQuota.costTotal; update all usages and imports that reference
limitTotalUsd (e.g., in providers-quota-client.tsx and any mappers or
serializers) to use costTotal, and ensure the shape and types remain unchanged
while running type checks to catch any missed references.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/actions/providers.ts`:
- Around line 2738-2754: The total-cost aggregation call to sumProviderTotalCost
lacks the provider's total-reset timestamp, so resets written by
resetProviderTotalUsage are ignored and limitTotalUsd.current keeps
accumulating; fix by retrieving the provider's total reset boundary (the same
timestamp used by resetProviderTotalUsage) and pass it as the reset/time-bound
parameter into sumProviderTotalCost wherever it's called (including the
single-call in providers.ts that awaits cost5h...totalCost and the batch calls
referenced around the other block), ensuring sumProviderTotalCost uses that
reset boundary when computing totals.

In
`@src/app/`[locale]/dashboard/quotas/keys/_components/edit-key-quota-dialog.tsx:
- Around line 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.

In `@src/app/`[locale]/dashboard/quotas/keys/_components/keys-quota-client.tsx:
- Around line 393-439: The code assumes key.quota.costTotal is always present
but KeyQuota.costTotal is optional in quota-helpers.ts, so update the UI to
guard access with key.quota?.costTotal (or check costTotal presence before
reading costTotal.limit/current) and pass those guarded values into
QuotaQuickEditPopover, QuotaProgress, getUsageRate and handleSaveLimit to avoid
runtime errors; alternatively, align the types by making KeyQuota.costTotal
required in quota-helpers.ts. For the visual inconsistency, either leave the
plain <span> (acceptable) or add a `total` variant to QuotaWindowType and render
<QuotaWindowType type="total" /> to match other columns' styling.

In `@src/lib/utils/quota-helpers.ts`:
- Line 13: Update the quota type so costTotal is required (change costTotal?: {
current: number; limit: number | null } to costTotal: { current: number; limit:
number | null }) and adjust helper logic accordingly: in hasKeyQuotaSet and
getMaxUsageRate replace any costTotal?.limit checks with costTotal.limit (and
remove unnecessary null guards where server guarantees costTotal), and ensure
any internal reads use costTotal.current/limit directly; keep function
signatures (hasKeyQuotaSet, getMaxUsageRate) and other helper uses consistent
with the non-optional costTotal type.

---

Outside diff comments:
In
`@src/app/`[locale]/dashboard/quotas/keys/_components/edit-key-quota-dialog.tsx:
- Around line 396-409: The clear-all button render condition in DialogFooter
uses currentQuota checks but omits costTotal and costDaily, so when only
limitTotalUsd or costDaily.limit is set the Button (variant "destructive",
onClick handleClearQuota) won't render; update the boolean expression that
decides rendering to include currentQuota?.costTotal.limit and
currentQuota?.costDaily.limit (and keep the existing
currentQuota?.concurrentSessions.limit null-coalescing check) so the Button (and
its disabled prop isPending and label t("clearAll")) appears whenever any quota
dimension is > 0.

In `@tests/unit/actions/providers-usage.test.ts`:
- Around line 20-42: The tests define sumProviderTotalCostMock but never assert
it or the returned total limit, so add assertions in both the single-provider
and batch-provider test cases to ensure the mock is called with the correct
provider id(s) and that result.data.limitTotalUsd.current equals the expected
mocked value; update the tests that exercise the code paths which call
sumProviderTotalCost (refer to sumProviderTotalCostMock) to set a deterministic
return value and assert both sumProviderTotalCostMock was invoked and
result.data.limitTotalUsd.current matches that value (do this for the individual
test and the batch test where multiple providers are evaluated).

---

Nitpick comments:
In `@messages/en/quota.json`:
- Around line 242-246: 两个文案键在表达同一概念但用词不一致:keys.editDialog.limitTotalUsd.label
("Total Quota (USD)") 与 keys.editKeyForm.limitTotalUsd.label ("Total Cost Limit
(USD)");请选择统一的术语(例如统一为 "Total Cost Limit (USD)" 或 "Total Quota
(USD)"),然后在所有语言文件中同步替换相应键的值(确保 keys.editDialog.limitTotalUsd.label 和
keys.editKeyForm.limitTotalUsd.label 在各语言版本一致),提交修改以保持端到端文案统一。

In
`@src/app/`[locale]/dashboard/quotas/providers/_components/provider-quota-list-item.tsx:
- Around line 13-20: Rename the inconsistent field limitTotalUsd in the
ProviderQuota interface to costTotal to match the cost* naming pattern and align
with KeyQuota.costTotal; update all usages and imports that reference
limitTotalUsd (e.g., in providers-quota-client.tsx and any mappers or
serializers) to use costTotal, and ensure the shape and types remain unchanged
while running type checks to catch any missed references.

In
`@src/app/`[locale]/dashboard/quotas/providers/_components/providers-quota-client.tsx:
- Around line 39-51: Reorder the checks inside hasQuotaLimit to match the same
time-sequence used in calculateMaxUsage: check cost5h, costDaily, costWeekly,
costMonthly, then limitTotalUsd, and finally concurrentSessions; update the
boolean expression in hasQuotaLimit (function name: hasQuotaLimit, symbol:
quota.limitTotalUsd, quota.cost5h, quota.costDaily, quota.costWeekly,
quota.costMonthly, quota.concurrentSessions) so the order matches
calculateMaxUsage for consistent readability.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0bad63f7-c79e-4e87-a7a6-1bad4669dcb8

📥 Commits

Reviewing files that changed from the base of the PR and between 4e7ff96 and ff7d5b6.

📒 Files selected for processing (15)
  • messages/en/quota.json
  • messages/ja/quota.json
  • messages/ru/quota.json
  • messages/zh-CN/quota.json
  • messages/zh-TW/quota.json
  • src/actions/providers.ts
  • src/app/[locale]/dashboard/quotas/keys/_components/edit-key-quota-dialog.tsx
  • src/app/[locale]/dashboard/quotas/keys/_components/keys-quota-client.tsx
  • src/app/[locale]/dashboard/quotas/keys/_components/keys-quota-manager.tsx
  • src/app/[locale]/dashboard/quotas/providers/_components/provider-quota-list-item.tsx
  • src/app/[locale]/dashboard/quotas/providers/_components/providers-quota-client.tsx
  • src/app/[locale]/dashboard/quotas/providers/_components/providers-quota-manager.tsx
  • src/app/[locale]/dashboard/quotas/providers/page.tsx
  • src/lib/utils/quota-helpers.ts
  • tests/unit/actions/providers-usage.test.ts

Comment thread src/actions/providers.ts
Comment on lines +83 to +85
const [limitTotal, setLimitTotal] = useState<string>(
currentQuota?.costTotal.limit?.toString() ?? ""
);
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.

Comment on lines +393 to +439
{/* 总限额 */}
<TableCell>
{hasKeyQuota && key.quota && key.quota.costTotal.limit !== null ? (
<div className="space-y-1">
<div className="flex items-center justify-between mb-1">
<span className="text-xs text-muted-foreground">
{t("table.costTotal")}
</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs font-mono">
{formatCurrency(key.quota.costTotal.current, currencyCode)}/
<QuotaQuickEditPopover
currentLimit={key.quota.costTotal.limit}
label={t("table.costTotal")}
unit="currency"
currencyCode={currencyCode}
onSave={(v) =>
handleSaveLimit(key.id, key.name, "limitTotalUsd", v)
}
>
<button
type="button"
className="underline-offset-4 hover:underline cursor-pointer rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{formatCurrency(key.quota.costTotal.limit, currencyCode)}
</button>
</QuotaQuickEditPopover>
</span>
</div>
<QuotaProgress
current={key.quota.costTotal.current}
limit={key.quota.costTotal.limit}
className="h-1"
/>
<div className="text-xs text-muted-foreground text-right">
{getUsageRate(
key.quota.costTotal.current,
key.quota.costTotal.limit
).toFixed(1)}
%
</div>
</div>
) : (
<span className="text-sm text-muted-foreground">-</span>
)}
</TableCell>
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

新增"总限额"列实现合理,但提示几处细节。

  1. Line 395 直接访问 key.quota.costTotal.limit,前提是 costTotal 必存在。src/lib/utils/quota-helpers.tsKeyQuota.costTotal 是可选(costTotal?),与此处本地类型(Line 38 必填)不一致——参见 quota-helpers.ts 中的相关评论,需统一以避免潜在运行时崩溃。
  2. 与其他列(5h/日/周/月)使用 QuotaWindowType 组件展示窗口标签不同,此处(Line 397-401)用了一个普通 <span> 显示 t("table.costTotal"),造成视觉风格略不一致。由于总额没有窗口概念,这种处理可以接受;若想统一可考虑增加一个 QuotaWindowTypetotal 类型。
🤖 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/keys-quota-client.tsx
around lines 393 - 439, The code assumes key.quota.costTotal is always present
but KeyQuota.costTotal is optional in quota-helpers.ts, so update the UI to
guard access with key.quota?.costTotal (or check costTotal presence before
reading costTotal.limit/current) and pass those guarded values into
QuotaQuickEditPopover, QuotaProgress, getUsageRate and handleSaveLimit to avoid
runtime errors; alternatively, align the types by making KeyQuota.costTotal
required in quota-helpers.ts. For the visual inconsistency, either leave the
plain <span> (acceptable) or add a `total` variant to QuotaWindowType and render
<QuotaWindowType type="total" /> to match other columns' styling.

costDaily: { current: number; limit: number | null };
costWeekly: { current: number; limit: number | null };
costMonthly: { current: number; limit: number | null };
costTotal?: { current: number; limit: number | null };
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 查找服务端构造 KeyQuota 的位置,确认是否始终填充 costTotal
rg -nP -C3 '\bcostTotal\s*:' --type=ts -g '!**/*.test.ts'
rg -nP -C5 'quota\s*[:=]\s*\{' --type=ts -g 'src/actions/**' -g 'src/repository/**'

Repository: ding113/claude-code-hub

Length of output: 6800


🏁 Script executed:

cat -n src/lib/utils/quota-helpers.ts | head -40

Repository: ding113/claude-code-hub

Length of output: 1372


🏁 Script executed:

# Find where KeyQuota type is defined
rg -nP 'type\s+KeyQuota|interface\s+KeyQuota' --type=ts

Repository: ding113/claude-code-hub

Length of output: 840


🏁 Script executed:

# Check src/actions/keys.ts to see if costTotal is always populated
sed -n '830,970p' src/actions/keys.ts

Repository: ding113/claude-code-hub

Length of output: 4969


🏁 Script executed:

# Check keys-quota-client.tsx for costTotal access
sed -n '30,100p' src/app/[locale]/dashboard/quotas/keys/_components/keys-quota-client.tsx

Repository: ding113/claude-code-hub

Length of output: 2036


🏁 Script executed:

# Check edit-key-quota-dialog.tsx for costTotal access
sed -n '30,100p' src/app/[locale]/dashboard/quotas/keys/_components/edit-key-quota-dialog.tsx

Repository: ding113/claude-code-hub

Length of output: 2584


🏁 Script executed:

# Search for all unsafe costTotal.limit access without optional chaining
rg -n 'costTotal\.limit' --type=ts -B2 -A2

Repository: ding113/claude-code-hub

Length of output: 6325


确认 costTotal 的可选性与其他文件不一致,存在潜在的类型安全隐患。

src/lib/utils/quota-helpers.tscostTotal 声明为可选字段(第 13 行),但:

  1. UI 文件本地声明为必填keys-quota-client.tsx 第 38 行、edit-key-quota-dialog.tsx 第 35 行、key-limit-usage.tsx 第 21 行均声明 costTotal 为必填

  2. 存在不安全的直接访问

    • keys-quota-client.tsx 第 395、404、406、418、425、431 行直接访问 key.quota.costTotal.limit 而未进行空值检查
    • edit-key-quota-dialog.tsx 第 363-364 行在 currentQuota?.costTotal.limit 条件内直接访问 costTotal.currentcostTotal.limit
    • key-limit-usage.tsx 第 102 行直接访问 data.costTotal.limit
  3. 服务端始终填充src/actions/keys.tsgetKeyLimitUsage() 函数(第 958-960 行)保证总是返回 costTotal 对象

建议将 costTotal 改为必填字段,并更新 quota-helpers.ts 中的辅助函数相应调整:

建议修改
-  costTotal?: { current: number; limit: number | null };
+  costTotal: { current: number; limit: number | null };

同时在 hasKeyQuotaSet()getMaxUsageRate() 中将 costTotal?.limit 改为 costTotal.limit(第 36、92 行)。

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

In `@src/lib/utils/quota-helpers.ts` at line 13, Update the quota type so
costTotal is required (change costTotal?: { current: number; limit: number |
null } to costTotal: { current: number; limit: number | null }) and adjust
helper logic accordingly: in hasKeyQuotaSet and getMaxUsageRate replace any
costTotal?.limit checks with costTotal.limit (and remove unnecessary null guards
where server guarantees costTotal), and ensure any internal reads use
costTotal.current/limit directly; keep function signatures (hasKeyQuotaSet,
getMaxUsageRate) and other helper uses consistent with the non-optional
costTotal type.

@github-actions github-actions Bot added the size/M Medium PR (< 500 lines) label Apr 27, 2026
@@ -17,6 +17,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
const getSessionMock = vi.fn();
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] [TEST-MISSING-CRITICAL] No test assertions for the new limitTotalUsd field

Why this is a problem: The PR adds a mock for sumProviderTotalCost returning 0, but no test case verifies that the returned data includes the limitTotalUsd field with correct values. The existing test "should return correct structure with DB-sourced costs" asserts cost5h, costDaily, costWeekly, costMonthly but not limitTotalUsd. If the limitTotalUsd computation logic breaks in the future, no test will catch it. CLAUDE.md requires: "All new features must have unit test coverage of at least 80%".

Suggested fix - Add assertions to the existing "should return correct structure" tests:

// In "should return correct structure with DB-sourced costs" test:
sumProviderTotalCostMock.mockResolvedValueOnce(50.0);

if (result.ok) {
  // ... existing assertions ...
  expect(result.data.limitTotalUsd.current).toBe(50.0);
  expect(result.data.limitTotalUsd.limit).toBeNull();
}

// In batch test "should return correct costs from DB for each provider":
// Add assertions for limitTotalUsd in both provider results
expect(p1Data?.limitTotalUsd.current).toBe(0);
expect(p1Data?.limitTotalUsd.limit).toBeNull();

@@ -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) && (

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Code Review Summary

This PR adds limitTotalUsd (total cost quota) support to provider and API key quota management, extending the existing time-windowed quota system with a lifetime spend limit. The implementation is consistent with existing patterns for time-based quotas. Two medium-severity issues were identified: missing test assertions for the new field and a UI button visibility gap.

PR Size: M

  • Lines changed: 207
  • Files changed: 15

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 0 1 0
Security 0 0 0 0
Error Handling 0 0 0 0
Types 0 0 0 0
Comments/Docs 0 0 0 0
Tests 0 0 1 0
Simplification 0 0 0 0

Medium Priority Issues (Should Fix)

  1. [TEST-MISSING-CRITICAL] No test assertions for limitTotalUsd field (tests/unit/actions/providers-usage.test.ts)

    • The mock for sumProviderTotalCost is added and configured to return 0, but no test verifies the limitTotalUsd field appears in the returned data with correct values. Both getProviderLimitUsage and getProviderLimitUsageBatch are affected. Per CLAUDE.md: "All new features must have unit test coverage of at least 80%".
  2. [LOGIC-BUG] "Clear All" button visibility condition missing costTotal.limit (edit-key-quota-dialog.tsx)

    • The handleClearQuota function now clears limitTotalUsd, but the button visibility condition in DialogFooter does not check currentQuota?.costTotal?.limit. A key with only a total quota set will not show the "Clear All" button.

Review Coverage

  • Logic and correctness - Gap found (Clear All button condition)
  • Security (OWASP Top 10) - Clean (numeric values only, schema validation in editKey action)
  • Error handling - Clean (follows existing try/catch patterns with logging)
  • Type safety - Clean (types updated consistently across interfaces)
  • Documentation accuracy - Clean (comments updated to include new quota type)
  • Test coverage - Gap found (no assertions for limitTotalUsd return value)
  • Code clarity - Good (follows existing patterns consistently)

Automated review by Claude AI

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/app/[locale]/dashboard/quotas/providers/_components/providers-quota-manager.tsx (1)

14-21: LGTM:类型扩展与后端返回结构一致。

ProviderQuota 新增 limitTotalUsd: { current; limit; resetAt? }src/actions/providers.tsProviderLimitUsageData 的形状对齐。

可选的小建议:在 providers-quota-client.tsx 中新增字段被放到了 concurrentSessions 之后,本文件放在了 concurrentSessions 之前。两处都正确,但保持字段顺序一致便于后续维护与代码评审 diff 阅读。长期看,这两处重复的 ProviderQuota 接口定义可以抽取到共享类型文件中(属于既有代码风格,不必本 PR 处理)。

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

In
`@src/app/`[locale]/dashboard/quotas/providers/_components/providers-quota-manager.tsx
around lines 14 - 21, The ProviderQuota interface field order differs from
providers-quota-client.tsx (limitTotalUsd placed before concurrentSessions here
but after in the client) which makes diffs noisy; update the ProviderQuota
definition in this file to match the same field order used in
providers-quota-client.tsx (place limitTotalUsd after concurrentSessions) and
ensure the shape still matches ProviderLimitUsageData from
src/actions/providers.ts so runtime typings remain correct.
src/actions/providers.ts (2)

2741-2757: LGTM:总额度统计已传入 reset 边界。

getProviderLimitUsagegetProviderLimitUsageBatch 两条路径都已将 provider.totalCostResetAt 传给 sumProviderTotalCost,与 resetProviderTotalUsage 写入的重置时间戳形成正确闭环。

附一个小小的一致性建议(可选):单条路径 line 2755 直接传 provider.totalCostResetAt,而批量路径 line 2914 使用 ?? null 兜底。函数签名是 Date | null | undefined,两种写法都正确,只是风格不一致。如愿统一成 ?? null 会更明显地表达"未设置即视为不限制聚合下限"的意图。

Also applies to: 2898-2915

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

In `@src/actions/providers.ts` around lines 2741 - 2757, Unify the way we pass the
provider total-cost reset boundary: in both getProviderLimitUsage and
getProviderLimitUsageBatch ensure the call to sumProviderTotalCost uses
provider.totalCostResetAt ?? null (rather than sometimes passing
provider.totalCostResetAt directly) so the Date | null | undefined signature is
handled consistently; update the call sites (e.g., in getProviderLimitUsage
where sumProviderTotalCost(providerId, provider.totalCostResetAt) is used and in
getProviderLimitUsageBatch where you already use ?? null) to all use ?? null.

1258-1270: 缓存失效失败会让 DB 已成功的重置返回错误。

publishProviderCacheInvalidation() 直接 await 而未做异常隔离:当 Redis 发布失败(连接断开/超时等)时,异常会被外层 catch 捕获并返回 { ok: false, error: "重置供应商总用量失败" },但此时数据库的 totalCostResetAt 已经更新成功,会让前端误以为重置失败而引发重复操作。

本文件中其它写路径(addProvidereditProviderremoveProviderbatchUpdateProvidersbatchDeleteProviders)统一使用了 broadcastProviderCacheInvalidation(...) 这一带 try/catch 的封装。建议这里同样改为带容错的调用,或就地包裹 try/catch 仅记录 warn 而不影响成功语义。

建议修正
     const ok = await resetProviderTotalCostResetAt(providerId, new Date());
     if (!ok) {
       return { ok: false, error: "供应商不存在" };
     }

-    await publishProviderCacheInvalidation();
+    await broadcastProviderCacheInvalidation({ operation: "edit", providerId });

     return { ok: true };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/providers.ts` around lines 1258 - 1270, The DB reset path
currently awaits publishProviderCacheInvalidation() directly which can throw and
turn a successful resetProviderTotalCostResetAt(providerId, new Date()) into an
overall failure; wrap the cache publish in a try/catch (or replace the call with
the existing fault-tolerant helper broadcastProviderCacheInvalidation(...)) so
any Redis/publish errors are logged (use logger.warn or similar) but do not
propagate, ensuring the function still returns { ok: true } when the DB update
succeeded; keep the outer catch for real DB errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/app/`[locale]/dashboard/quotas/keys/_components/edit-key-quota-dialog.tsx:
- Around line 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).

---

Nitpick comments:
In `@src/actions/providers.ts`:
- Around line 2741-2757: Unify the way we pass the provider total-cost reset
boundary: in both getProviderLimitUsage and getProviderLimitUsageBatch ensure
the call to sumProviderTotalCost uses provider.totalCostResetAt ?? null (rather
than sometimes passing provider.totalCostResetAt directly) so the Date | null |
undefined signature is handled consistently; update the call sites (e.g., in
getProviderLimitUsage where sumProviderTotalCost(providerId,
provider.totalCostResetAt) is used and in getProviderLimitUsageBatch where you
already use ?? null) to all use ?? null.
- Around line 1258-1270: The DB reset path currently awaits
publishProviderCacheInvalidation() directly which can throw and turn a
successful resetProviderTotalCostResetAt(providerId, new Date()) into an overall
failure; wrap the cache publish in a try/catch (or replace the call with the
existing fault-tolerant helper broadcastProviderCacheInvalidation(...)) so any
Redis/publish errors are logged (use logger.warn or similar) but do not
propagate, ensuring the function still returns { ok: true } when the DB update
succeeded; keep the outer catch for real DB errors.

In
`@src/app/`[locale]/dashboard/quotas/providers/_components/providers-quota-manager.tsx:
- Around line 14-21: The ProviderQuota interface field order differs from
providers-quota-client.tsx (limitTotalUsd placed before concurrentSessions here
but after in the client) which makes diffs noisy; update the ProviderQuota
definition in this file to match the same field order used in
providers-quota-client.tsx (place limitTotalUsd after concurrentSessions) and
ensure the shape still matches ProviderLimitUsageData from
src/actions/providers.ts so runtime typings remain correct.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 978144e0-4f21-47b5-bbfb-00904d6a16aa

📥 Commits

Reviewing files that changed from the base of the PR and between ff7d5b6 and 024fcce.

📒 Files selected for processing (12)
  • src/actions/key-quota.ts
  • src/actions/keys.ts
  • src/actions/providers.ts
  • src/app/[locale]/dashboard/_components/user/key-limit-usage.tsx
  • src/app/[locale]/dashboard/quotas/keys/_components/edit-key-quota-dialog.tsx
  • src/app/[locale]/dashboard/quotas/keys/_components/keys-quota-client.tsx
  • src/app/[locale]/dashboard/quotas/keys/_components/keys-quota-manager.tsx
  • src/app/[locale]/dashboard/quotas/providers/_components/provider-quota-list-item.tsx
  • src/app/[locale]/dashboard/quotas/providers/_components/providers-quota-client.tsx
  • src/app/[locale]/dashboard/quotas/providers/_components/providers-quota-manager.tsx
  • src/app/[locale]/dashboard/quotas/providers/page.tsx
  • src/types/provider.ts
✅ Files skipped from review due to trivial changes (3)
  • src/types/provider.ts
  • src/app/[locale]/dashboard/quotas/keys/_components/keys-quota-manager.tsx
  • src/app/[locale]/dashboard/_components/user/key-limit-usage.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/app/[locale]/dashboard/quotas/providers/page.tsx
  • src/app/[locale]/dashboard/quotas/providers/_components/provider-quota-list-item.tsx
  • src/app/[locale]/dashboard/quotas/keys/_components/keys-quota-client.tsx

Comment on lines +359 to +367
{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>
)}
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).

@ding113 ding113 merged commit 1facf20 into ding113:dev Apr 28, 2026
10 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Claude Code Hub Roadmap Apr 28, 2026
@github-actions github-actions Bot mentioned this pull request Apr 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:i18n area:provider area:UI bug Something isn't working size/M Medium PR (< 500 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants