feat(my-usage): expose quota compatibility fields#1157
Conversation
📝 WalkthroughWalkthrough此PR重构配额数据模型,将 Changes配额数据模型与响应结构重构
代码审查工作量评估🎯 4 (Complex) | ⏱️ ~60 分钟 此PR涉及多层结构化变更:新的嵌套数据模型、多个相互关联的配额计算helper函数、复杂的窗口分析逻辑、多个文件中的schema同步更新,以及配套的文档与测试。虽然变更遵循一致的模式,但每个层级的逻辑密度和依赖关系需要仔细验证。 相关的PR
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
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. Comment |
There was a problem hiding this comment.
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:
- Quota compatibility fields for external quota checkers (
src/actions/my-usage.ts) - Structured quota windows (5h, daily, weekly, monthly, total) with complete metrics
- Flat compatibility aliases (
remaining,remaining5hUsd,todayRemainingUsd, etc.) - Example extractor script for ccswitch-style clients (
docs/examples/api-key-quota-extractor-compatible.js) - Documentation for usage-only quota extraction (
docs/usage-only-quota-extractor.md) - 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.tscovering:- 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
-
Zero/negative limit handling: The
normalizeQuotaLimit()function correctly treats0or negative limits as unlimited (null), matching the enforcement semantics described in CLAUDE.md. -
Total limit fallback: The
resolveTotalLimitWithMonthlyFallback()function properly falls back to monthly quota when no explicit total quota is set, maintaining backward compatibility. -
Type safety: All new fields are properly typed with
number | nullfor optional limits, and theMyUsageQuotaWindowinterface provides complete type coverage for quota window data. -
OpenAPI schema: All new response fields are properly documented in the Zod schema with Chinese descriptions, consistent with existing patterns in the codebase.
-
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
There was a problem hiding this comment.
💡 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".
| const keyLimitTotalUsd = resolveTotalLimitWithMonthlyFallback({ | ||
| totalLimit: key.limitTotalUsd, | ||
| monthlyLimit: key.limitMonthlyUsd, | ||
| }); |
There was a problem hiding this comment.
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 👍 / 👎.
| const concurrentSessions = Math.max(keyConcurrent, userKeyConcurrent); | ||
| const concurrentSessionsLimit = | ||
| effectiveKeyConcurrentLimit > 0 ? effectiveKeyConcurrentLimit : null; |
There was a problem hiding this 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.
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.There was a problem hiding this comment.
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.
| function round2(value: number): number { | ||
| return Math.round((value + Number.EPSILON) * 100) / 100; | ||
| } |
There was a problem hiding this comment.
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.
| 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(); | |
| } |
There was a problem hiding this comment.
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
📒 Files selected for processing (6)
docs/examples/api-key-quota-extractor-compatible.jsdocs/usage-only-quota-extractor.mdsrc/actions/my-usage.tssrc/app/api/actions/[...route]/route.tstests/api/my-usage-readonly.test.tstests/unit/actions/total-usage-semantics.test.ts
| if (boundedCandidates.length === 0) { | ||
| return { | ||
| limit: null, | ||
| used: Math.max(...candidates.map((candidate) => candidate.used), 0), | ||
| remaining: null, | ||
| }; | ||
| } |
There was a problem hiding this comment.
无限额窗口的 used 值会错误显示用户级别的聚合用量
当所有候选项均无限额时(boundedCandidates.length === 0),返回的 used 是 Math.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.
| 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.
| const keyLimitTotalUsd = resolveTotalLimitWithMonthlyFallback({ | ||
| totalLimit: key.limitTotalUsd, | ||
| monthlyLimit: key.limitMonthlyUsd, | ||
| }); | ||
| const userLimitTotalUsd = resolveTotalLimitWithMonthlyFallback({ | ||
| totalLimit: user.limitTotalUsd, | ||
| monthlyLimit: user.limitMonthlyUsd, | ||
| }); |
There was a problem hiding this comment.
keyLimitTotalUsd / userLimitTotalUsd 语义变更:从原始 DB 值变为含月度回退的衍生值
这两个字段是已有的向后兼容字段。之前赋值等价于 key.limitTotalUsd ?? null(直接反映 DB 值,未配置时为 null);现在改为 resolveTotalLimitWithMonthlyFallback({totalLimit, monthlyLimit}),当 limitTotalUsd 为 null/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).
Summary
This PR extends the existing self-service
my-usage/getMyQuotaaction 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/getMyQuotaAuthorization: Bearer <api key>{}allowReadOnlyAccessauth pathProblem
External quota checkers and ccswitch-style clients needed a standardized way to consume quota data from Claude Code Hub. The existing
getMyQuotaresponse lacked structured window data and flat compatibility aliases that these tools expect.Solution
Added comprehensive compatibility fields to the
getMyQuotaresponse while maintaining backward compatibility with existing clients.Core Changes
src/actions/my-usage.ts (+249 lines)
MyUsageQuotaWindowinterface with structured quota window datanormalizeQuotaLimit()- treats 0/negative limits as unlimitedresolveEffectiveQuotaWindow()- finds most restrictive limit between key and userresolveOverallRemaining()- calculates overall remaining across all windowsbuildQuotaWindow()- constructs standardized window objectsquotaWindowsobject withfiveHour,daily,weekly,monthly,totalremaining,remaining5hUsd,todayRemainingUsd,remainingPercentrpmLimit,concurrentSessions,concurrentSessionsLimitproviderGroup,resetMode,resetTime,unitsrc/app/api/actions/[...route]/route.ts (+61 lines)
myUsageQuotaWindowSchemadocs/examples/api-key-quota-extractor-compatible.js (new file)
normalizeQuotaResponse()function for client-side normalizationdocs/usage-only-quota-extractor.md (new file)
tests/unit/actions/total-usage-semantics.test.ts (+232 lines)
getMyQuotatests/api/my-usage-readonly.test.ts (+100 lines)
Breaking Changes
None. All changes are backward compatible:
Testing
Automated Tests
tests/unit/actions/total-usage-semantics.test.tstests/api/my-usage-readonly.test.tsManual Verification
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
{ ok, data })allowReadOnlyAccessauthorization gateChecklist
Original PR by @dofastted
Description enhanced with additional structure for reviewer clarity
Greptile Summary
This PR extends the existing
getMyQuotaaction response with structuredquotaWindowsobjects and flat compatibility aliases (remaining,todayRemainingUsd,remainingPercent,rpmLimit, concurrency fields, etc.) for ccswitch-style clients, without adding a new endpoint or auth surface.MyUsageQuotaWindowinterface and five quota windows (fiveHour,daily,weekly,monthly,total) computed viaresolveEffectiveQuotaWindow, which picks the most-restrictive key/user candidate per period.userRpmLimit,userAllowedModels, anduserAllowedClients.keyLimitTotalUsdanduserLimitTotalUsdfields by falling back to the monthly limit when no total limit is set, and exposesconcurrentSessionsasMath.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
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, quotaWindowsComments Outside Diff (1)
src/actions/my-usage.ts, line 516-517 (link)keyLimitTotalUsd/userLimitTotalUsdThese two existing response fields previously returned
key.limitTotalUsd ?? nullanduser.limitTotalUsd ?? null, givingnullwhenever no explicit total quota was configured. After this PR they return the monthly limit as a fallback. Any client that checkskeyLimitTotalUsd === nullto 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
Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "feat(my-usage): expose quota compatibili..." | Re-trigger Greptile