Skip to content

feat(my-usage): expose quota compatibility fields#1157

Open
dofastted wants to merge 1 commit into
ding113:devfrom
dofastted:feat/usage-quota-compat-fields-20260505
Open

feat(my-usage): expose quota compatibility fields#1157
dofastted wants to merge 1 commit into
ding113:devfrom
dofastted:feat/usage-quota-compat-fields-20260505

Conversation

@dofastted
Copy link
Copy Markdown
Contributor

@dofastted dofastted commented May 5, 2026

Summary

This PR extends the existing self-service my-usage/getMyQuota action response with compatibility fields for external quota checkers and ccswitch-style clients.

It does not add a new public quota endpoint. The route remains:

  • POST /api/actions/my-usage/getMyQuota
  • Authorization: Bearer <api key>
  • Request body: {}
  • Existing allowReadOnlyAccess auth path

Problem

External quota checkers and ccswitch-style clients needed a standardized way to consume quota data from Claude Code Hub. The existing getMyQuota response lacked structured window data and flat compatibility aliases that these tools expect.

Solution

Added comprehensive compatibility fields to the getMyQuota response while maintaining backward compatibility with existing clients.

Core Changes

src/actions/my-usage.ts (+249 lines)

  • Added MyUsageQuotaWindow interface with structured quota window data
  • Added helper functions for quota calculations:
    • normalizeQuotaLimit() - treats 0/negative limits as unlimited
    • resolveEffectiveQuotaWindow() - finds most restrictive limit between key and user
    • resolveOverallRemaining() - calculates overall remaining across all windows
    • buildQuotaWindow() - constructs standardized window objects
  • Added new response fields:
    • Structured quotaWindows object with fiveHour, daily, weekly, monthly, total
    • Flat compatibility aliases: remaining, remaining5hUsd, todayRemainingUsd, remainingPercent
    • Rate/session limits: rpmLimit, concurrentSessions, concurrentSessionsLimit
    • Metadata fields: providerGroup, resetMode, resetTime, unit
  • Total limit now falls back to monthly quota when no explicit total quota is set

src/app/api/actions/[...route]/route.ts (+61 lines)

  • Added Zod schema for myUsageQuotaWindowSchema
  • Extended OpenAPI response schema with all new compatibility fields

docs/examples/api-key-quota-extractor-compatible.js (new file)

  • Node.js extractor template for ccswitch-style quota checks
  • Provides normalizeQuotaResponse() function for client-side normalization
  • Includes both direct usage and ccswitch template export modes

docs/usage-only-quota-extractor.md (new file)

  • Documentation for the minimal compatibility path
  • Response field reference
  • Usage examples

tests/unit/actions/total-usage-semantics.test.ts (+232 lines)

  • Unit tests for compatibility fields on getMyQuota
  • Tests for zero-quota-as-unlimited semantics

tests/api/my-usage-readonly.test.ts (+100 lines)

  • Integration tests for read-only key quota queries
  • Verifies all new response fields are populated correctly

Breaking Changes

None. All changes are backward compatible:

  • Existing fields remain unchanged
  • New fields are additive only
  • Zero/negative limits treated as unlimited (matches existing enforcement semantics)

Testing

Automated Tests

  • Unit tests added in tests/unit/actions/total-usage-semantics.test.ts
  • Integration tests added in tests/api/my-usage-readonly.test.ts

Manual Verification

# Syntax check
node --check docs/examples/api-key-quota-extractor-compatible.js

# Unit tests
pnpm exec vitest run tests/unit/actions/total-usage-semantics.test.ts --reporter=verbose

# API integrity tests
pnpm exec vitest run tests/api/api-actions-integrity.test.ts --reporter=verbose

# Type checking
pnpm typecheck

Example Response

The enhanced response now includes:

{
  "ok": true,
  "data": {
    "remaining": 7.5,
    "todayRemainingUsd": 2.25,
    "remainingPercent": 85.0,
    "quotaWindows": {
      "daily": { "remainingUsd": 2.25, "usedUsd": 0.75, "limitUsd": 3.0, ... },
      "total": { "remainingUsd": 7.5, "usedUsd": 2.5, "limitUsd": 10.0, ... }
    },
    "rpmLimit": 60,
    "concurrentSessions": 2,
    "concurrentSessionsLimit": 5,
    "unit": "USD"
  }
}

Security Notes

  • Extractor uses existing action wrapper shape ({ ok, data })
  • Does not accept API keys in request bodies
  • Sends API key only through Bearer token header
  • Uses existing allowReadOnlyAccess authorization gate

Checklist

  • Code follows project conventions (Biome formatting)
  • Self-review completed
  • Unit tests pass
  • Type checking passes
  • Documentation added (examples + usage guide)
  • Backward compatibility maintained
  • No breaking changes

Original PR by @dofastted
Description enhanced with additional structure for reviewer clarity

Greptile Summary

This PR extends the existing getMyQuota action response with structured quotaWindows objects and flat compatibility aliases (remaining, todayRemainingUsd, remainingPercent, rpmLimit, concurrency fields, etc.) for ccswitch-style clients, without adding a new endpoint or auth surface.

  • Adds MyUsageQuotaWindow interface and five quota windows (fiveHour, daily, weekly, monthly, total) computed via resolveEffectiveQuotaWindow, which picks the most-restrictive key/user candidate per period.
  • Adds flat alias fields and backfills previously missing Zod schema declarations for userRpmLimit, userAllowedModels, and userAllowedClients.
  • Silently changes the semantics of the existing keyLimitTotalUsd and userLimitTotalUsd fields by falling back to the monthly limit when no total limit is set, and exposes concurrentSessions as Math.max(keyConcurrent, userKeyConcurrent), which may reflect sessions from other keys.

Confidence Score: 3/5

Hold for the two logic issues in src/actions/my-usage.ts before merging.

The core quota-window computation logic is well-structured and the new fields are thoroughly tested. However, two issues in my-usage.ts need resolution: the keyLimitTotalUsd/userLimitTotalUsd fields now silently return the monthly limit as a fallback, changing the meaning of existing API fields that ccswitch-style clients may use to detect whether a total quota is configured. Additionally, concurrentSessions is computed as Math.max(keyConcurrent, userKeyConcurrent), where userKeyConcurrent tracks sessions across all keys for the user — this can inflate the displayed value relative to concurrentSessionsLimit, which is a key-level limit.

src/actions/my-usage.ts — the resolveTotalLimitWithMonthlyFallback usage and the concurrentSessions derivation both warrant a closer look before this ships.

Important Files Changed

Filename Overview
src/actions/my-usage.ts Adds quota window computation logic and 20+ new compatibility fields; introduces a semantic break to the existing keyLimitTotalUsd/userLimitTotalUsd fields (monthly fallback) and a potentially misleading concurrentSessions value derived from cross-key session counts.
src/app/api/actions/[...route]/route.ts Adds the new quota fields and myUsageQuotaWindowSchema to the OpenAPI/Zod response schema; also backfills previously missing userRpmLimit, userAllowedModels, and userAllowedClients schema declarations.
docs/examples/api-key-quota-extractor-compatible.js New Node.js example/ccswitch-template; normalizes response fields with safe fallbacks and does not accept API keys in request bodies.
tests/api/my-usage-readonly.test.ts Extends the read-only API integration test with limit/usage assertions for the new quota fields; limitTotalUsd asserted as 35 confirms the intentional monthly fallback behavior.
tests/unit/actions/total-usage-semantics.test.ts Adds unit tests for new compatibility fields and the zero-limit-as-unlimited semantics; mocks are well-isolated.
docs/usage-only-quota-extractor.md Documentation-only file; accurately describes the endpoint, auth mechanism, and new response fields.

Sequence Diagram

sequenceDiagram
    participant Client as ccswitch / CLI client
    participant Route as POST /api/actions/my-usage/getMyQuota
    participant Action as getMyQuota() action
    participant DB as DB / SessionTracker

    Client->>Route: Bearer apiKey, body: {}
    Route->>Action: allowReadOnlyAccess auth path
    Action->>DB: sumKeyQuotaCostsById, sumUserQuotaCosts
    Action->>DB: SessionTracker.getKeySessionCount(key.id)
    Action->>DB: getUserConcurrentSessions(user.id)
    DB-->>Action: costs, keyConcurrent, userKeyConcurrent
    Action->>Action: resolveEffectiveQuotaWindow (5h/daily/weekly/monthly/total)
    Action->>Action: buildQuotaWindow x5 -> quotaWindows
    Action->>Action: resolveOverallRemaining -> remaining
    Action->>Action: Math.max(keyConcurrent, userKeyConcurrent) -> concurrentSessions
    Action-->>Route: ok, data: MyUsageQuota
    Route-->>Client: ok true, data with remaining, quotaWindows
Loading

Comments Outside Diff (1)

  1. src/actions/my-usage.ts, line 516-517 (link)

    P1 Breaking semantic change to keyLimitTotalUsd / userLimitTotalUsd

    These two existing response fields previously returned key.limitTotalUsd ?? null and user.limitTotalUsd ?? null, giving null whenever no explicit total quota was configured. After this PR they return the monthly limit as a fallback. Any client that checks keyLimitTotalUsd === null to detect "no total quota set" — including ccswitch-style consumers this PR targets — will now silently receive a non-null number for every user who has a monthly limit but no total limit, causing them to enforce a total cap that was never intended.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/actions/my-usage.ts
    Line: 516-517
    
    Comment:
    **Breaking semantic change to `keyLimitTotalUsd` / `userLimitTotalUsd`**
    
    These two existing response fields previously returned `key.limitTotalUsd ?? null` and `user.limitTotalUsd ?? null`, giving `null` whenever no explicit total quota was configured. After this PR they return the monthly limit as a fallback. Any client that checks `keyLimitTotalUsd === null` to detect "no total quota set" — including ccswitch-style consumers this PR targets — will now silently receive a non-null number for every user who has a monthly limit but no total limit, causing them to enforce a total cap that was never intended.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
src/actions/my-usage.ts:516-517
**Breaking semantic change to `keyLimitTotalUsd` / `userLimitTotalUsd`**

These two existing response fields previously returned `key.limitTotalUsd ?? null` and `user.limitTotalUsd ?? null`, giving `null` whenever no explicit total quota was configured. After this PR they return the monthly limit as a fallback. Any client that checks `keyLimitTotalUsd === null` to detect "no total quota set" — including ccswitch-style consumers this PR targets — will now silently receive a non-null number for every user who has a monthly limit but no total limit, causing them to enforce a total cap that was never intended.

### Issue 2 of 2
src/actions/my-usage.ts:685-687
**`concurrentSessions` may reflect sessions from other keys**

`keyConcurrent` comes from `SessionTracker.getKeySessionCount(key.id)` (sessions on this key only), while `userKeyConcurrent` comes from `getUserConcurrentSessions(user.id)` (sessions for the user, potentially across all keys). `Math.max(keyConcurrent, userKeyConcurrent)` means that when a user has active sessions on other keys, the displayed `concurrentSessions` value will exceed this key's actual session count. Since `concurrentSessionsLimit` is derived from key-level and user-level limit config, a consumer comparing `concurrentSessions` against `concurrentSessionsLimit` will see inflated utilization and potentially believe the quota is more consumed than it is for this specific key.

Reviews (1): Last reviewed commit: "feat(my-usage): expose quota compatibili..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

📝 Walkthrough

Walkthrough

此PR重构配额数据模型,将MyUsageQuota从扁平结构转变为基于时间窗口的结构(5小时/日/周/月/总计),每个窗口包含USD限额、已用、剩余及衍生的百分比/耗尽状态字段。相应更新API响应schema、计算逻辑、文档和测试,以支持新的分层配额查询响应。

Changes

配额数据模型与响应结构重构

层级 / 文件 总结
数据模型定义
src/actions/my-usage.ts(第170-271行)
新增MyUsageQuotaWindow接口定义单个时间窗口的USD limit/used/remaining、百分比、unlimited/exhausted标志;重构MyUsageQuotaquotaWindows(fiveHour/daily/weekly/monthly/total)为核心,取代原先的扁平key/user字段,并新增overallRemainingremainingPercentresetMode/resetTimeunit等统一字段。
配额计算逻辑
src/actions/my-usage.ts(第273-375行、642-769行)
添加normalizeQuotaLimitclampRemainingresolveEffectiveQuotaWindowresolveOverallRemainingbuildQuotaWindow等helper函数以计算有效的时间窗口限额(从key/user候选中选择最严格者);重构getMyQuota()以使用新的窗口模型生成响应,包括每周期USD指标、并发会话限制、provider分组和重置时间。
路由响应Schema
src/app/api/actions/[...route]/route.ts(第1208-1314行)
新增myUsageQuotaWindowSchema Zod对象以规范化单个窗口字段;重构getMyQuota路由的responseSchema,将离散的周期字段统一为quotaWindows结构,并补充rpmLimit、concurrentSessions、resetMode/resetTime、userAllowedModels/Clients等字段。
示例代码与文档
docs/examples/api-key-quota-extractor-compatible.jsdocs/usage-only-quota-extractor.md
新增完整的Node.js示例脚本,提供ccswitchTemplate模板、fetchQuota()实现和normalizeQuotaResponse()规范化器以便外部集成;新增文档说明通过POST /api/actions/my-usage/getMyQuota及Bearer认证访问现有配额端点的路径,并链接到示例代码。
测试与验证
tests/api/my-usage-readonly.test.tstests/unit/actions/total-usage-semantics.test.ts
扩展Bearer测试以设置显式配额/速率限制字段并验证返回的quotaWindows结构及其数值字段;新增getMyQuota测试用例验证兼容性字段与窗口信息,包括零限额处理时的unlimited/null字段行为。

代码审查工作量评估

🎯 4 (Complex) | ⏱️ ~60 分钟

此PR涉及多层结构化变更:新的嵌套数据模型、多个相互关联的配额计算helper函数、复杂的窗口分析逻辑、多个文件中的schema同步更新,以及配套的文档与测试。虽然变更遵循一致的模式,但每个层级的逻辑密度和依赖关系需要仔细验证。

相关的PR

  • ding113/claude-code-hub#674:同样修改src/actions/my-usage.ts中的配额计算逻辑和getMyQuota实现,涉及周期成本计算的改变。
  • ding113/claude-code-hub#811:修改my-usage读取路径(src/actions/my-usage.ts和路由/schema),直接相关于配额响应结构。
  • ding113/claude-code-hub#772:同样修改src/actions/my-usage.ts和配额响应模型,涉及并发会话限制的计算和窗口结构重组。
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.25% 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 PR标题准确概括了主要变更,即为my-usage接口暴露配额兼容性字段,与changeset的核心目标一致。
Description check ✅ Passed PR描述详细说明了所有变更内容、目标和验证步骤,与changeset各文件的修改内容高度相关,信息充分且有意义。
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.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/usage-quota-compat-fields-20260505

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.

@github-actions github-actions Bot added enhancement New feature or request area:core documentation Improvements or additions to documentation javascript Pull requests that update javascript code size/M Medium PR (< 500 lines) labels May 5, 2026
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

No significant issues identified in this PR. The code is well-structured, thoroughly tested, and follows project standards.

PR Size: M

  • Lines changed: 904 (901 additions, 3 deletions)
  • Files changed: 6

What was reviewed

New Features:

  1. Quota compatibility fields for external quota checkers (src/actions/my-usage.ts)
  2. Structured quota windows (5h, daily, weekly, monthly, total) with complete metrics
  3. Flat compatibility aliases (remaining, remaining5hUsd, todayRemainingUsd, etc.)
  4. Example extractor script for ccswitch-style clients (docs/examples/api-key-quota-extractor-compatible.js)
  5. Documentation for usage-only quota extraction (docs/usage-only-quota-extractor.md)
  6. Updated OpenAPI schema in route.ts with all new fields

Test Coverage:

  • Added comprehensive tests for new quota fields in tests/api/my-usage-readonly.test.ts
  • Added unit tests in tests/unit/actions/total-usage-semantics.test.ts covering:
    • Compatibility fields validation
    • Zero quota limits treated as unlimited

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean
  • Error handling - Clean
  • Type safety - Clean
  • Documentation accuracy - Clean
  • Test coverage - Adequate (new tests added for all new fields)
  • Code clarity - Good

Notes

  1. Zero/negative limit handling: The normalizeQuotaLimit() function correctly treats 0 or negative limits as unlimited (null), matching the enforcement semantics described in CLAUDE.md.

  2. Total limit fallback: The resolveTotalLimitWithMonthlyFallback() function properly falls back to monthly quota when no explicit total quota is set, maintaining backward compatibility.

  3. Type safety: All new fields are properly typed with number | null for optional limits, and the MyUsageQuotaWindow interface provides complete type coverage for quota window data.

  4. OpenAPI schema: All new response fields are properly documented in the Zod schema with Chinese descriptions, consistent with existing patterns in the codebase.

  5. Client example: The extractor script in docs/examples/ includes defensive programming patterns (null checks, type validation) appropriate for client-side compatibility code.


Automated review by Claude AI

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: 25eeeb7cc6

ℹ️ 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/my-usage.ts
Comment on lines +642 to +645
const keyLimitTotalUsd = resolveTotalLimitWithMonthlyFallback({
totalLimit: key.limitTotalUsd,
monthlyLimit: key.limitMonthlyUsd,
});
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 Keep key/user total limits from falling back to monthly

Using resolveTotalLimitWithMonthlyFallback for keyLimitTotalUsd/userLimitTotalUsd changes the meaning of existing fields: when limitTotalUsd is unset but monthly limits exist, these fields now report a monthly cap instead of null. Because keyCurrentTotalUsd/userCurrentTotalUsd are still all-time usage, clients that render or alert on the legacy total-limit fields (for example the my-usage quota cards) will show false total-limit exhaustion and lose the ability to distinguish “no total cap configured” from “explicit total cap configured”. This is a regression from the previous behavior where these fields reflected only explicit total limits.

Useful? React with 👍 / 👎.

Comment thread src/actions/my-usage.ts
Comment on lines +685 to +687
const concurrentSessions = Math.max(keyConcurrent, userKeyConcurrent);
const concurrentSessionsLimit =
effectiveKeyConcurrentLimit > 0 ? effectiveKeyConcurrentLimit : 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.

P1 concurrentSessions may reflect sessions from other keys

keyConcurrent comes from SessionTracker.getKeySessionCount(key.id) (sessions on this key only), while userKeyConcurrent comes from getUserConcurrentSessions(user.id) (sessions for the user, potentially across all keys). Math.max(keyConcurrent, userKeyConcurrent) means that when a user has active sessions on other keys, the displayed concurrentSessions value will exceed this key's actual session count. Since concurrentSessionsLimit is derived from key-level and user-level limit config, a consumer comparing concurrentSessions against concurrentSessionsLimit will see inflated utilization and potentially believe the quota is more consumed than it is for this specific key.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/actions/my-usage.ts
Line: 685-687

Comment:
**`concurrentSessions` may reflect sessions from other keys**

`keyConcurrent` comes from `SessionTracker.getKeySessionCount(key.id)` (sessions on this key only), while `userKeyConcurrent` comes from `getUserConcurrentSessions(user.id)` (sessions for the user, potentially across all keys). `Math.max(keyConcurrent, userKeyConcurrent)` means that when a user has active sessions on other keys, the displayed `concurrentSessions` value will exceed this key's actual session count. Since `concurrentSessionsLimit` is derived from key-level and user-level limit config, a consumer comparing `concurrentSessions` against `concurrentSessionsLimit` will see inflated utilization and potentially believe the quota is more consumed than it is for this specific key.

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

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 enhances the getMyQuota API by introducing detailed quota window tracking (5h, daily, weekly, monthly, and total) and normalization logic to provide a consistent view of usage and remaining limits. It also includes a new Node.js compatibility script and documentation for external quota checks. A review comment suggests using a decimal library for financial calculations to improve precision over floating-point arithmetic.

Comment thread src/actions/my-usage.ts
Comment on lines +338 to +340
function round2(value: number): number {
return Math.round((value + Number.EPSILON) * 100) / 100;
}
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

The round2 function uses Number.EPSILON to mitigate floating-point precision issues, which is a common practice. However, for financial data like USD amounts, it is generally safer to perform calculations in cents (integers) or use a dedicated library like decimal.js to avoid cumulative rounding errors. Since decimal.js-light is already a dependency in this project (as per general rules), consider using it here for better precision.

Suggested change
function round2(value: number): number {
return Math.round((value + Number.EPSILON) * 100) / 100;
}
function round2(value: number): number {
return new Decimal(value).toDecimalPlaces(2).toNumber();
}

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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/actions/my-usage.ts`:
- Around line 306-312: When boundedCandidates.length === 0 we incorrectly set
used to Math.max(...candidates.map(c => c.used), 0) which picks the user-level
aggregate instead of the current key's usage; change that branch to use the
first candidate's used value (candidates[0].used or 0) because callers always
place the key candidate first — update the return in the if
(boundedCandidates.length === 0) block so used comes from candidates[0].used and
keep limit/remaining as before.
- Around line 642-649: The change replaced the original DB-backed semantics of
keyLimitTotalUsd/userLimitTotalUsd with derived values from
resolveTotalLimitWithMonthlyFallback, which breaks callers that expect null when
no total limit is set; restore the original semantics by assigning
keyLimitTotalUsd = key.limitTotalUsd ?? null and userLimitTotalUsd =
user.limitTotalUsd ?? null, and only call resolveTotalLimitWithMonthlyFallback
when computing the effective limit for the active window (e.g., in the existing
effective-limit calculation flow, introduce a separate variable like
effectiveKeyTotalUsd/effectiveUserTotalUsd that uses
resolveTotalLimitWithMonthlyFallback({ totalLimit: key.limitTotalUsd,
monthlyLimit: key.limitMonthlyUsd }) so downstream fields keep DB values intact
while effective calculations use the fallback).
🪄 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: e2991a1a-39f1-4ffd-a350-bf1da6202b8a

📥 Commits

Reviewing files that changed from the base of the PR and between 0c58703 and 25eeeb7.

📒 Files selected for processing (6)
  • docs/examples/api-key-quota-extractor-compatible.js
  • docs/usage-only-quota-extractor.md
  • src/actions/my-usage.ts
  • src/app/api/actions/[...route]/route.ts
  • tests/api/my-usage-readonly.test.ts
  • tests/unit/actions/total-usage-semantics.test.ts

Comment thread src/actions/my-usage.ts
Comment on lines +306 to +312
if (boundedCandidates.length === 0) {
return {
limit: null,
used: Math.max(...candidates.map((candidate) => candidate.used), 0),
remaining: 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 | 🟡 Minor | ⚡ Quick win

无限额窗口的 used 值会错误显示用户级别的聚合用量

当所有候选项均无限额时(boundedCandidates.length === 0),返回的 usedMath.max(keyCost, userCost)。由于 userCost 是该用户所有 key 的聚合用量,而 keyCost 是当前 key 的用量,对于拥有多个 key 的用户,userCost >= keyCost 始终成立。

这意味着:当某个窗口同时对 key 和 user 均无限额时,used5hUsd/usedDailyUsd 等平坦字段和 quotaWindows.*.usedUsd 展示的是用户所有 key 的聚合用量,而非当前 key 的自身用量——与 my-usage 接口"仅展示当前 key 数据"的语义相矛盾。

现有测试在 mock 中 key 用量始终 ≥ user 用量,因此未捕获该问题。

建议修复:无限额时取 key 层级(第一个候选项)的用量
  if (boundedCandidates.length === 0) {
    return {
      limit: null,
-     used: Math.max(...candidates.map((candidate) => candidate.used), 0),
+     used: candidates.length > 0 ? candidates[0].used : 0,
      remaining: null,
    };
  }

调用方始终将 key 候选项放在首位([{ limit: key.limitXxx, used: keyXxxCost }, { limit: user.limitXxx, used: userXxxCost }]),因此取索引 0 即为 key 的自身用量。

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (boundedCandidates.length === 0) {
return {
limit: null,
used: Math.max(...candidates.map((candidate) => candidate.used), 0),
remaining: null,
};
}
if (boundedCandidates.length === 0) {
return {
limit: null,
used: candidates.length > 0 ? candidates[0].used : 0,
remaining: null,
};
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/actions/my-usage.ts` around lines 306 - 312, When
boundedCandidates.length === 0 we incorrectly set used to
Math.max(...candidates.map(c => c.used), 0) which picks the user-level aggregate
instead of the current key's usage; change that branch to use the first
candidate's used value (candidates[0].used or 0) because callers always place
the key candidate first — update the return in the if (boundedCandidates.length
=== 0) block so used comes from candidates[0].used and keep limit/remaining as
before.

Comment thread src/actions/my-usage.ts
Comment on lines +642 to +649
const keyLimitTotalUsd = resolveTotalLimitWithMonthlyFallback({
totalLimit: key.limitTotalUsd,
monthlyLimit: key.limitMonthlyUsd,
});
const userLimitTotalUsd = resolveTotalLimitWithMonthlyFallback({
totalLimit: user.limitTotalUsd,
monthlyLimit: user.limitMonthlyUsd,
});
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 | ⚡ Quick win

keyLimitTotalUsd / userLimitTotalUsd 语义变更:从原始 DB 值变为含月度回退的衍生值

这两个字段是已有的向后兼容字段。之前赋值等价于 key.limitTotalUsd ?? null(直接反映 DB 值,未配置时为 null);现在改为 resolveTotalLimitWithMonthlyFallback({totalLimit, monthlyLimit}),当 limitTotalUsdnull/undefined 时会回退到月度限额。

下游影响:外部消费方若用 keyLimitTotalUsd === null 来判断"key 是否配置了总限额",现在会错误地读到月度限额值而非 null,导致判断失真。该语义变更没有被现有测试覆盖(两处测试均只断言有效 limitTotalUsd 字段,未单独校验 keyLimitTotalUsd)。

如果想保留原始 DB 语义,建议将月度回退仅应用于有效窗口的 limit 计算,而保留原始 key/user 字段不变:

建议修复:保留原始 DB 语义,仅对有效窗口应用回退
+ // 仅用于有效窗口计算的回退总限额(不影响 keyLimitTotalUsd/userLimitTotalUsd 原始字段)
  const keyLimitTotalUsd = resolveTotalLimitWithMonthlyFallback({
    totalLimit: key.limitTotalUsd,
    monthlyLimit: key.limitMonthlyUsd,
  });
  const userLimitTotalUsd = resolveTotalLimitWithMonthlyFallback({
    totalLimit: user.limitTotalUsd,
    monthlyLimit: user.limitMonthlyUsd,
  });

  // ... 有效窗口仍使用带回退的 keyLimitTotalUsd/userLimitTotalUsd 计算
  const effectiveTotal = resolveEffectiveQuotaWindow([
    { limit: keyLimitTotalUsd, used: keyTotalCost },
    { limit: userLimitTotalUsd, used: userTotalCost },
  ]);

  // ...
  const quota: MyUsageQuota = {
-   keyLimitTotalUsd,           // 现在是回退后的衍生值
+   keyLimitTotalUsd: key.limitTotalUsd ?? null,   // 保留原始 DB 值
    // ...
-   userLimitTotalUsd,
+   userLimitTotalUsd: user.limitTotalUsd ?? null, // 保留原始 DB 值
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/actions/my-usage.ts` around lines 642 - 649, The change replaced the
original DB-backed semantics of keyLimitTotalUsd/userLimitTotalUsd with derived
values from resolveTotalLimitWithMonthlyFallback, which breaks callers that
expect null when no total limit is set; restore the original semantics by
assigning keyLimitTotalUsd = key.limitTotalUsd ?? null and userLimitTotalUsd =
user.limitTotalUsd ?? null, and only call resolveTotalLimitWithMonthlyFallback
when computing the effective limit for the active window (e.g., in the existing
effective-limit calculation flow, introduce a separate variable like
effectiveKeyTotalUsd/effectiveUserTotalUsd that uses
resolveTotalLimitWithMonthlyFallback({ totalLimit: key.limitTotalUsd,
monthlyLimit: key.limitMonthlyUsd }) so downstream fields keep DB values intact
while effective calculations use the fallback).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core documentation Improvements or additions to documentation enhancement New feature or request javascript Pull requests that update javascript code size/M Medium PR (< 500 lines)

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant