Skip to content

fix: address PR #968 review feedback#970

Merged
ding113 merged 4 commits intodevfrom
fix/pr-968-review-comments
Mar 23, 2026
Merged

fix: address PR #968 review feedback#970
ding113 merged 4 commits intodevfrom
fix/pr-968-review-comments

Conversation

@ding113
Copy link
Copy Markdown
Owner

@ding113 ding113 commented Mar 23, 2026

Summary

This PR addresses review feedback from PR #968 (release v0.6.6), fixing issues identified during code review including a breaking API change, unbounded queries, pricing fallback behavior, and several UI/UX improvements.

Problem

PR #968 (release v0.6.6) received review feedback identifying several issues:

Related PRs:

Issues Addressed

Issue Severity Description
Breaking API change P1 /api/actions/my-usage/getMyUsageLogs endpoint removed without backward-compat shim
Unbounded admin query P2 loadAllUsersForAdmin iterates without row cap or timeout, risking memory exhaustion
Pricing fallback skip P2 hasUsableBillingSettings() returns false when falling back to defaults, skipping cost calculation entirely
Hedge timeout behavior - First-byte timeout was cleared on retry attempts, causing hedge to not retrigger
Export filter bug - Export used stale filters instead of localFilters
Error visibility - Rate-limit filters silently failed when users/providers failed to load

Solution

Core Changes

  1. Restore legacy getMyUsageLogs API (src/actions/my-usage.ts, src/app/api/actions/[...route]/route.ts)

    • Adds back the /api/actions/my-usage/getMyUsageLogs endpoint with page/pageSize/total contract
    • Uses findUsageLogsForKeySlim for page-based pagination instead of cursor-based
    • Adds MyUsageLogsFilters and MyUsageLogsResult types for legacy API
  2. Add admin scan cap (src/actions/users.ts)

    • Introduces USER_LIST_ADMIN_SCAN_MAX = 2000 limit to prevent unbounded queries
    • Logs warning when cap is hit with context for debugging
  3. Fix pricing fallback behavior (src/app/v1/_lib/proxy/session.ts)

    • Changes hasUsableBillingSettings() to check cachedBillingModelSource instead of billingSettingsSource
    • Ensures billing still occurs even when falling back to cached/default settings
  4. Fix hedge timeout on retries (src/app/v1/_lib/proxy/forwarder.ts)

    • Extracts threshold arming into armAttemptThreshold() function
    • Re-arms threshold timer after reactive rectifier retry
    • Removes firstByteTimeoutStreamingMs: 0 override that prevented hedge retriggering
    • Adds debug logging for abort/cancel failures
  5. Fix usage export filters (src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx)

    • Uses localFilters instead of filters when calling startUsageLogsExport
    • Adds proper i18n error handling with getErrorMessage()

Supporting Changes

  • Unify full-key exposure rules - Consolidates canExposeFullKey() helper and applies consistently across getUsers, getUsersBatch, getUsersBatchCore
  • Improve rate-limit error visibility - Shows error message when users/providers fail to load in filters
  • Add export progress throttling - Throttles progress updates to 2-second intervals
  • Add lint to dev workflow - Runs bun run lint in CI dev workflow
  • Update Japanese proxy placeholder - Adds socks5 example to proxy URL placeholder
  • Fix CSS module types - Improves *.css module declaration

Changes

File Change Type Description
src/actions/my-usage.ts Feature Restore legacy getMyUsageLogs endpoint
src/app/api/actions/[...route]/route.ts Feature Register legacy getMyUsageLogs route
src/actions/users.ts Fix Add 2000-row cap to loadAllUsersForAdmin
src/app/v1/_lib/proxy/session.ts Fix Fix pricing fallback behavior
src/app/v1/_lib/proxy/forwarder.ts Fix Fix hedge timeout on retries, add debug logging
src/repository/usage-logs.ts Feature Add findUsageLogsForKeySlim for page-based queries
src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx Fix Use correct filters in export, add i18n errors
src/app/[locale]/dashboard/rate-limits/_components/rate-limit-filters.tsx Enhancement Show load errors for users/providers
src/app/[locale]/dashboard/_components/rate-limit-top-users.tsx Enhancement Show load errors, add limit param to search

Testing

Automated Tests

  • Unit tests added/updated for legacy API compatibility
  • DST parsing test for legacy page-based logs API
  • Hedge retry threshold timing test
  • Billing fallback behavior test (vi.clearAllMocks added)
  • Export progress UI test (proper afterEach cleanup)
  • Mock cleanup (afterEach with vi.useRealTimers())

Manual Testing

  1. Verify /api/actions/my-usage/getMyUsageLogs returns page/pageSize/total
  2. Test user list with >2000 users to verify cap warning in logs
  3. Test hedge timeout retries retrigger properly
  4. Verify rate-limit filter shows error when API fails

Breaking Changes

None. This PR restores backward compatibility that was broken in PR #968.

Checklist

  • Code follows project conventions
  • Self-review completed
  • Tests pass locally (bun run build && bun run lint && bun run typecheck && bun run test)
  • Documentation updated (OpenAPI schemas include new endpoint)

Description enhanced by Claude AI

Greptile Summary

This PR addresses six review issues from PR #968 (v0.6.6): restoring a removed legacy API endpoint, capping an unbounded admin scan, fixing a pricing fallback skip, re-arming hedge timeouts on retries, correcting a stale-filters export bug, and surfacing rate-limit filter load errors. The changes are generally well-implemented with good test coverage.

Key changes:

  • Legacy API restored: getMyUsageLogs is re-added with page/pageSize/total contract backed by the new findUsageLogsForKeySlim repository function (with a 10-second TTL count cache and ledger-only mode fallback).
  • Quotas page unbounded loop fixed: getUsersWithQuotas now uses a capped getUsersBatch loop (MAX = 2000 users) instead of the unbounded getUsers() call.
  • Pricing fallback corrected: hasUsableBillingSettings() now checks cachedBillingModelSource (always valid after loading) rather than billingSettingsSource, removing the return null that silently skipped cost calculations when settings fell back to defaults.
  • Hedge timeout re-armed on retry: The new armAttemptThreshold() function resets thresholdTriggered and re-arms the timer after a reactive rectifier retry, fixing the previous behavior where the hedge would not retrigger.
  • Export filter bug fixed: Export now correctly uses localFilters (current UI state) rather than the stale filters prop.
  • loadAllUsersForAdmin still unbounded: Despite the PR description claiming USER_LIST_ADMIN_SCAN_MAX = 2000 was added to loadAllUsersForAdmin, this constant and cap are absent from the function. The fix only covers the quotas page; getUsers() (user management page and API endpoint) still calls the unbounded loop.

Confidence Score: 4/5

  • Safe to merge; one concrete discrepancy between the PR description and loadAllUsersForAdmin implementation should be followed up, but does not block the addressed use-cases.
  • The majority of fixes are correct and well-tested (billing fallback, hedge re-arm, export filters, legacy API, quotas cap, rate-limit error visibility). The only remaining gap is that loadAllUsersForAdmin itself is still unbounded contrary to the PR description, while only the quotas page received a cap. This is a P2 concern that doesn't affect the primary fixes.
  • src/actions/users.ts — loadAllUsersForAdmin still has unbounded loop; src/app/v1/_lib/proxy/session.ts — dead warning branch can be cleaned up.

Important Files Changed

Filename Overview
src/actions/users.ts Adds canExposeFullKey() helper (correctly unifying key exposure logic across getUsers, getUsersBatch, getUsersBatchCore) and normalizeSearchUsersLimit() with a 5000 cap; however loadAllUsersForAdmin still contains the original unbounded while(true) loop contrary to the PR description.
src/app/v1/_lib/proxy/session.ts Fixes pricing fallback: removes the return null that skipped cost calculation when billing settings fell back to defaults; the dead !hasUsableBillingSettings() warning branch can be cleaned up since it can never fire after loadBillingSettings().
src/app/v1/_lib/proxy/forwarder.ts Extracts armAttemptThreshold() to correctly re-arm and reset the hedge timer on reactive rectifier retries; fixes attempt.sessionsession scope bug in cancelAttempt; replaces silent catch(() => {}) blocks with debug-level logging.
src/repository/usage-logs.ts Adds findUsageLogsForKeySlim with page/pageSize pagination, a 10-second TTL count cache, ledger-only mode fallback, and fixes if (filters.startTime)!== undefined falsy-zero guard in existing batch function.
src/actions/my-usage.ts Restores the legacy getMyUsageLogs endpoint with page/pageSize/total contract; correctly validates and clamps page and pageSize inputs; delegates to findUsageLogsForKeySlim.
src/app/[locale]/dashboard/quotas/users/page.tsx Replaces unbounded getUsers() call with a capped getUsersBatch loop (MAX_USERS_FOR_QUOTAS = 2000, MAX_ITERATIONS = 11); logs a warning when the cap is reached.
src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx Fixes export bug: uses localFilters (current UI state) instead of stale filters prop; adds i18n error handling for download failures.
src/app/[locale]/dashboard/rate-limits/_components/rate-limit-filters.tsx Improves provider load error visibility; adds cancellation logic to the providers useEffect (was missing); shows i18n error text under user/provider selectors on failure.
src/actions/usage-logs.ts Adds 800 ms throttle for progress updates during CSV export to reduce Redis write pressure; the constant name says 800 while the PR description says "2-second intervals" — minor documentation mismatch.

Sequence Diagram

sequenceDiagram
    participant Client
    participant API as /api/actions/my-usage/getMyUsageLogs
    participant Action as getMyUsageLogs()
    participant Repo as findUsageLogsForKeySlim()
    participant Cache as TTLMap (10s)
    participant DB

    Client->>API: GET ?page=2&pageSize=20
    API->>Action: getMyUsageLogs(filters)
    Action->>Repo: {keyString, page:2, pageSize:20}
    Repo->>Cache: get(totalCacheKey)
    alt Cache hit
        Cache-->>Repo: cachedTotal
        Repo->>DB: SELECT ... LIMIT 21 OFFSET 20
        DB-->>Repo: rows
        Repo-->>Action: {logs, total: max(cached, offset+rows)}
    else Cache miss
        Repo->>DB: SELECT ... LIMIT 21 OFFSET 20
        DB-->>Repo: rows
        alt hasMore = true
            Repo->>DB: SELECT count(*) ...
            DB-->>Repo: exactTotal
        end
        Repo->>Cache: set(totalCacheKey, total)
        Repo-->>Action: {logs, total}
    end
    Action-->>API: {ok:true, data:{logs, total, page, pageSize, ...}}
    API-->>Client: 200 JSON
Loading

Comments Outside Diff (3)

  1. src/app/v1/_lib/proxy/forwarder.ts, line 3115-3122 (link)

    P2 Dual-timeout race: internal abort and hedge threshold fire simultaneously

    The previous runAttempt deliberately passed a zeroed-out provider to doForward:

    // old: disabled internal timeout so the hedge could manage it externally
    const providerForRequest = attempt.provider.firstByteTimeoutStreamingMs > 0
      ? { ...attempt.provider, firstByteTimeoutStreamingMs: 0 }
      : attempt.provider;

    Now doForward receives the original provider. Because attempt.firstByteTimeoutMs is set to provider.firstByteTimeoutStreamingMs (same value), both:

    1. armAttemptThreshold → fires launchAlternative() at t = firstByteTimeoutMs, and
    2. doForward's internal responseController.abort() → fires at the same t = firstByteTimeoutMs

    fire at exactly the same tick. The abort causes doForward to reject, immediately routing through handleAttemptFailure while launchAlternative() is also being dispatched. Depending on microtask ordering the attempt can be marked settled (or a retry started) before the alternative is committed, potentially yielding unexpected behaviour under slow-start providers.

    The new armAttemptThreshold helper itself is clean; the concern is that disabling the internal timeout override was load-bearing for correct hedge semantics. Please add a test (or explain in a comment) why firing both timers simultaneously is safe.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/app/v1/_lib/proxy/forwarder.ts
    Line: 3115-3122
    
    Comment:
    **Dual-timeout race: internal abort and hedge threshold fire simultaneously**
    
    The previous `runAttempt` deliberately passed a zeroed-out provider to `doForward`:
    ```typescript
    // old: disabled internal timeout so the hedge could manage it externally
    const providerForRequest = attempt.provider.firstByteTimeoutStreamingMs > 0
      ? { ...attempt.provider, firstByteTimeoutStreamingMs: 0 }
      : attempt.provider;
    ```
    
    Now `doForward` receives the original provider. Because `attempt.firstByteTimeoutMs` is set to `provider.firstByteTimeoutStreamingMs` (same value), both:
    1. `armAttemptThreshold` → fires `launchAlternative()` at `t = firstByteTimeoutMs`, and
    2. `doForward`'s internal `responseController.abort()` → fires at the same `t = firstByteTimeoutMs`
    
    fire at exactly the same tick. The abort causes `doForward` to reject, immediately routing through `handleAttemptFailure` while `launchAlternative()` is also being dispatched. Depending on microtask ordering the attempt can be marked `settled` (or a retry started) before the alternative is committed, potentially yielding unexpected behaviour under slow-start providers.
    
    The new `armAttemptThreshold` helper itself is clean; the concern is that disabling the internal timeout override was load-bearing for correct hedge semantics. Please add a test (or explain in a comment) why firing both timers simultaneously is safe.
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. src/actions/usage-logs.ts, line 134 (link)

    P2 Progress throttle interval inconsistent with PR description

    The PR description states "Throttles progress updates to 2-second intervals", but the constant is set to 800 ms.

    800 ms will still trigger progress updates quite frequently (up to ~75 updates for a 60 s export). If 800 ms was intentional, the PR description and any inline comment should reflect that.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/actions/usage-logs.ts
    Line: 134
    
    Comment:
    **Progress throttle interval inconsistent with PR description**
    
    The PR description states "Throttles progress updates to **2-second** intervals", but the constant is set to 800 ms.
    
    
    
    800 ms will still trigger progress updates quite frequently (up to ~75 updates for a 60 s export). If 800 ms was intentional, the PR description and any inline comment should reflect that.
    
    How can I resolve this? If you propose a fix, please make it concise.
  3. src/actions/users.ts, line 154-168 (link)

    P2 loadAllUsersForAdmin still unbounded despite PR description

    The PR description states "Introduces USER_LIST_ADMIN_SCAN_MAX = 2000 limit to prevent unbounded queries" for loadAllUsersForAdmin, but the actual implementation at lines 149–169 still contains the original unbounded while (true) loop with no cap or warning:

    while (true) {
      const page = await findUserListBatch({ ...normalizedBaseParams, cursor, limit: USER_LIST_MAX_LIMIT });
      users.push(...page.users);
      if (!page.hasMore || !page.nextCursor) return users;
      cursor = page.nextCursor;
    }

    The only cap that was added lives in src/app/[locale]/dashboard/quotas/users/page.tsx as a local MAX_USERS_FOR_QUOTAS = 2000 constant. However, loadAllUsersForAdmin is still called from getUsers() (line 347/349), which is used by:

    • src/app/[locale]/dashboard/users/users-page-client.tsx (line 142)
    • The /api/actions route handler (route.ts line 164–165)

    Both of those call paths remain unbounded and carry the same memory exhaustion risk the fix was meant to address. Consider applying the cap directly in loadAllUsersForAdmin consistent with what the quotas page does.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/actions/users.ts
    Line: 154-168
    
    Comment:
    **`loadAllUsersForAdmin` still unbounded despite PR description**
    
    The PR description states *"Introduces `USER_LIST_ADMIN_SCAN_MAX = 2000` limit to prevent unbounded queries"* for `loadAllUsersForAdmin`, but the actual implementation at lines 149–169 still contains the original unbounded `while (true)` loop with no cap or warning:
    
    ```typescript
    while (true) {
      const page = await findUserListBatch({ ...normalizedBaseParams, cursor, limit: USER_LIST_MAX_LIMIT });
      users.push(...page.users);
      if (!page.hasMore || !page.nextCursor) return users;
      cursor = page.nextCursor;
    }
    ```
    
    The only cap that was added lives in `src/app/[locale]/dashboard/quotas/users/page.tsx` as a local `MAX_USERS_FOR_QUOTAS = 2000` constant. However, `loadAllUsersForAdmin` is still called from `getUsers()` (line 347/349), which is used by:
    - `src/app/[locale]/dashboard/users/users-page-client.tsx` (line 142)
    - The `/api/actions` route handler (`route.ts` line 164–165)
    
    Both of those call paths remain unbounded and carry the same memory exhaustion risk the fix was meant to address. Consider applying the cap directly in `loadAllUsersForAdmin` consistent with what the quotas page does.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/actions/users.ts
Line: 154-168

Comment:
**`loadAllUsersForAdmin` still unbounded despite PR description**

The PR description states *"Introduces `USER_LIST_ADMIN_SCAN_MAX = 2000` limit to prevent unbounded queries"* for `loadAllUsersForAdmin`, but the actual implementation at lines 149–169 still contains the original unbounded `while (true)` loop with no cap or warning:

```typescript
while (true) {
  const page = await findUserListBatch({ ...normalizedBaseParams, cursor, limit: USER_LIST_MAX_LIMIT });
  users.push(...page.users);
  if (!page.hasMore || !page.nextCursor) return users;
  cursor = page.nextCursor;
}
```

The only cap that was added lives in `src/app/[locale]/dashboard/quotas/users/page.tsx` as a local `MAX_USERS_FOR_QUOTAS = 2000` constant. However, `loadAllUsersForAdmin` is still called from `getUsers()` (line 347/349), which is used by:
- `src/app/[locale]/dashboard/users/users-page-client.tsx` (line 142)
- The `/api/actions` route handler (`route.ts` line 164–165)

Both of those call paths remain unbounded and carry the same memory exhaustion risk the fix was meant to address. Consider applying the cap directly in `loadAllUsersForAdmin` consistent with what the quotas page does.

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/v1/_lib/proxy/session.ts
Line: 745-751

Comment:
**Dead warning branch — `hasUsableBillingSettings()` can never be false here**

After `await this.loadBillingSettings()` completes, `this.cachedBillingModelSource` is always set to either `"original"` or `"redirected"` — the method's inner async IIFE catches all errors and falls back to `"redirected"` in every code path. Therefore `hasUsableBillingSettings()` (which checks `cachedBillingModelSource === "original" || cachedBillingModelSource === "redirected"`) will always return `true` at this point, making the `logger.warn` block unreachable dead code.

The core behavioral fix (removing `return null`) is correct and valuable. The dead guard can simply be removed to keep the code clear:

```suggestion
    const providerIdentity = provider ?? this.provider;
```

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

Reviews (3): Last reviewed commit: "fix: address second-round PR review feed..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 23, 2026

📝 Walkthrough

Walkthrough

新增分页式使用日志 API 与仓库查询(含缓存与分页)、用户搜索限额与权限细化、代理转发 hedging 与日志增强、导出进度节流、多个 UI 组件的错误状态处理,以及若干测试与 CI 文案调整与新增路由注册(OpenAPI)等改动。

Changes

Cohort / File(s) Summary
CI / 文案
\.github/workflows/dev.yml, messages/ja/settings/providers/form/sections.json
在 CI 步骤中新增 bun run lint;扩展日语代理 URL 占位符示例为多协议/端口示例(含 socks5)。
使用日志 Action / API / 路由
src/actions/my-usage.ts, src/app/api/actions/[...route]/route.ts
新增导出分页式读取操作 getMyUsageLogs(输入验证、分页、时区边界处理)并注册相应 API 路由与 OpenAPI 规范。
使用日志 仓库与分页实现
src/repository/usage-logs.ts, tests/unit/repository/usage-logs-slim-pagination.test.ts
新增 findUsageLogsForKeySlim 支持 page/pageSize、hasMore 语义、total 缓存与 ledger 回退逻辑;修正时间边界检查以接受 0 值。
使用日志 导出进度
src/actions/usage-logs.ts, src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx
为导出进度引入节流(lastProgressUpdateAt);改进导出/下载失败的错误文案处理与国际化错误映射。
用户搜索与权限
src/actions/users.ts, src/repository/user.ts, tests/unit/users-action-search-users-for-filter.test.ts
为用户搜索添加可选 limit 参数与归一化/上限(1..5000);新增权限判断 canExposeFullKey 并据此条件暴露 fullKey/canCopy;仓库层使用可配置 limit 并改进 providerGroup 分隔正则。
代理转发与会话
src/app/v1/_lib/proxy/forwarder.ts, src/app/v1/_lib/proxy/provider-selector.ts, src/app/v1/_lib/proxy/session.ts, tests/unit/proxy/*
引入 armAttemptThreshold 统一对冲阈值定时器;增强 abort/cancel 的 debug 日志与错误上下文;调整 provider 选择依赖与会话计费可用性判断;相关测试调整 mock 时序。
UI:速率限制 / 使用日志 / 表单
src/app/[locale]/dashboard/_components/rate-limit-top-users.tsx, src/app/[locale]/dashboard/rate-limits/_components/rate-limit-filters.tsx, src/app/[locale]/my-usage/_components/*, src/app/[locale]/dashboard/_components/user/forms/*.tsx
为用户/提供方加载添加失败标志与错误提示;调整 my-usage 过滤器的 refetch 触发逻辑;简化虚拟列表错误行显示;规范 providerGroup 标签解析与注释。
后台批量用户加载
src/app/[locale]/dashboard/quotas/users/page.tsx
将用户加载改为游标批次(limit=200)并累积至上限(2000),增加迭代上限和错误处理,使用显式 UserDisplay 类型。
测试与 OpenAPI 覆盖
tests/api/*, tests/integration/*, tests/unit/*
扩展 OpenAPI 注册断言(新增路径),新增/调整多项单元与集成测试以覆盖新 API、DST 行为、mock 清理与定时器恢复等。

Estimated code review effort

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

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed 标题准确概括了PR的主要目的:处理PR #968的审查反馈中发现的多个问题(API兼容性、查询限制、定价逻辑等),是对该PR的后续修复。
Description check ✅ Passed Pull request description clearly relates to the changeset, detailing how review feedback on PR #968 is addressed through fixes to API compatibility, admin user scanning, pricing fallback, hedge retry behavior, export filters, and error visibility.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/pr-968-review-comments

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.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request focuses on refining several core functionalities, including restoring backward compatibility for usage log retrieval, enhancing user management with clearer key exposure rules and administrative limits, and improving the robustness of usage data exports and proxy hedging mechanisms. It also incorporates better error handling and logging, ensuring a more stable and maintainable system.

Highlights

  • Legacy API Compatibility: Restored compatibility for the my-usage/getMyUsageLogs API endpoint, preserving its original page, pageSize, and total contract, and ensuring DST-safe date range parsing.
  • User Key Exposure & Admin Scan Cap: Unified rules for exposing full user keys, implemented an administrative scan cap for user lists to prevent excessive data retrieval, and improved error visibility for rate-limit user/provider loads.
  • Usage Export & Hedge Behavior Fixes: Addressed issues with usage export filters, enhanced i18n error handling, refined hedge timeout behavior, and improved pricing fallback logic.
  • Hedge Retry Logging: Added detailed logging for abort and cancel operations within hedge attempts to improve debugging and visibility into retry mechanisms.
  • Provider Group Normalization: Standardized the parsing and normalization of provider groups across various forms and the provider selection logic.
Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/dev.yml
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions github-actions bot added size/XL Extra Large PR (> 1000 lines) bug Something isn't working area:UI area:core labels Mar 23, 2026
@github-actions
Copy link
Copy Markdown
Contributor

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

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 new paginated usage logs API (getMyUsageLogs) and associated types, along with an update to the Japanese proxy URL placeholder to include socks5:// examples. It also refines the usage logs export functionality by adding a progress update interval to prevent excessive updates. User management actions (getUsers, getUsersBatch, getUsersBatchCore) have been refactored to use a new canExposeFullKey helper function for conditional exposure of full API keys and copying capabilities, and loadAllUsersForAdmin now limits the number of scanned users. Additionally, the searchUsersForFilter and searchUsers actions now accept an optional limit parameter. Error handling for usage logs export has been improved to display more specific messages using error codes, and several logger.debug calls were added in the proxy forwarder to provide better context for debugging ignored errors. The hasUsableBillingSettings logic in ProxySession was updated to check cachedBillingModelSource, and date range filtering in findUsageLogsBatch and findUsageLogsForKeySlim now explicitly checks for undefined to handle 0 values correctly. A new findUsageLogsForKeySlim function was added to src/repository/usage-logs.ts for paginated slim usage logs, including a TTL cache for total counts and fallback to ledger-only mode. Finally, the regex for splitting provider groups in findUserListBatch was updated to include newlines as delimiters for improved robustness.

I am having trouble creating individual review comments. Click here to see my feedback.

src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx (193-207)

high

high: The startUsageLogsExport function can return an error code. You should use the error code to display a more specific error message to the user.

        setExportStatus(null);\n        toast.error(\n          startResult.errorCode\n            ? getErrorMessage(tErrors, startResult.errorCode, startResult.errorParams)\n            : startResult.error || t("logs.filters.exportError")
        );

src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx (231-237)

high

high: The getUsageLogsExportStatus function can return an error code. You should use the error code to display a more specific error message to the user.

          setExportStatus(null);\n          toast.error(\n            statusResult.errorCode\n              ? getErrorMessage(tErrors, statusResult.errorCode, statusResult.errorParams)\n              : statusResult.error || t("logs.filters.exportError")
          );

src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx (260-265)

high

high: The downloadUsageLogsExport function can return an error code. You should use the error code to display a more specific error message to the user.

        if (!downloadResult.ok) {
          toast.error(
            downloadResult.errorCode
              ? getErrorMessage(tErrors, downloadResult.errorCode, downloadResult.errorParams)
              : downloadResult.error || t("logs.filters.exportError")
          );
        }

src/actions/my-usage.ts (587-588)

medium

medium: Consider adding more context to the error log to help with debugging. Include the filters that were used when getMyUsageLogs failed.

logger.error("[my-usage] getMyUsageLogs failed", { error, filters });

src/actions/users.ts (171-177)

medium

medium: Consider adding the normalizedBaseParams to the warn log to help with debugging.

  logger.warn("[UserAction] getUsers admin fallback hit scan cap", {\n    cap: USER_LIST_ADMIN_SCAN_MAX,\n    cursor,\n    searchTerm: normalizedBaseParams.searchTerm,\n    tagFilterCount: normalizedBaseParams.tagFilters?.length ?? 0,\n    keyGroupFilterCount: normalizedBaseParams.keyGroupFilters?.length ?? 0,\n    normalizedBaseParams\n  });

src/app/v1/_lib/proxy/forwarder.ts (3011-3013)

medium

medium: Instead of ignoring the error, log it with logger.debug to provide more context when debugging.

      } catch (abortError) {\n        logger.debug("ProxyForwarder: hedge attempt abort failed", { error: abortError });\n      }

src/app/v1/_lib/proxy/forwarder.ts (3016-3017)

medium

medium: Instead of ignoring the error, log it with logger.debug to provide more context when debugging.

      readerCancel?.catch((cancelError) => {\n        logger.debug("ProxyForwarder: hedge attempt reader cancel failed", { error: cancelError });\n      });

src/app/v1/_lib/proxy/forwarder.ts (3098-3099)

medium

medium: Instead of ignoring the error, log it with logger.debug to provide more context when debugging.

            } catch (abortError) {\n              logger.debug("ProxyForwarder: hedge loser abort failed", { error: abortError });\n            }

src/app/v1/_lib/proxy/forwarder.ts (3103-3104)

medium

medium: Instead of ignoring the error, log it with logger.debug to provide more context when debugging.

            cancelPromise?.catch((cancelError) => {\n              logger.debug("ProxyForwarder: hedge loser body cancel failed", { error: cancelError });\n            });

src/repository/user.ts (250)

medium

medium: Consider adding \n to the regex to handle newlines in addition to commas, to ensure that provider groups are split correctly even if they are separated by newlines.

        sql`${group} = ANY(regexp_split_to_array(coalesce(${users.providerGroup}, ''), '\\s*[,,\\n]+\\s*'))`

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: 02aa47cafe

ℹ️ 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 on lines +154 to +159
while (users.length < USER_LIST_ADMIN_SCAN_MAX) {
const remaining = USER_LIST_ADMIN_SCAN_MAX - users.length;
const page = await findUserListBatch({
...normalizedBaseParams,
cursor,
limit: USER_LIST_MAX_LIMIT,
limit: Math.min(USER_LIST_MAX_LIMIT, Math.max(1, remaining)),
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 Preserve unbounded getUsers() instead of truncating at 2,000 rows

This loop now hard-stops after 2,000 users and returns the partial array with no continuation token. I checked src/app/[locale]/dashboard/quotas/users/page.tsx, which still calls getUsers() once and assumes it received the full admin list, and /api/actions/users/getUsers, whose contract still says “不传时返回全部用户”. On deployments with more than 2,000 matching users (or filtered matches), the quotas page and compatibility API will silently omit the rest of the users.

Useful? React with 👍 / 👎.

Comment on lines 67 to 70
bun install
bun run typecheck
bun run lint
bun run format
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 Badge Keep formatting before lint in the dev workflow

Running bun run lint before bun run format breaks the workflow’s auto-fix path. In this repo, lint is biome check . while format is biome format --write . (package.json), so a branch that only has formatting drift will now fail at line 69 and never reach the formatter or the subsequent auto-commit step. That turns a self-healing CI job into a hard failure for a common case.

Useful? React with 👍 / 👎.

Comment on lines 202 to 208
className="flex items-center justify-center border-b"
>
{errorMessage && onLoadMore ? (
<div className="flex items-center gap-3 text-xs text-destructive">
<span>{errorMessage}</span>
<Button size="sm" variant="outline" onClick={onLoadMore}>
{tCommon("retry")}
</Button>
</div>
<span className="text-xs text-destructive">{errorMessage}</span>
) : (
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
)}
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 Retry button removed but onLoadMore still used as guard

The retry <Button> that called onLoadMore was removed, but onLoadMore is still checked in the condition (errorMessage && onLoadMore). This means the prop controls which branch renders, but is never actually invoked in the error branch — it's effectively dead. More importantly, users who hit a load-more error now have no interactive way to retry; they can only see the error text.

If the retry button was intentionally dropped, the condition guard can be simplified to just errorMessage, and the onLoadMore prop usage should be documented or removed upstream. If the retry capability should be preserved, the button needs to be restored.

Suggested change
className="flex items-center justify-center border-b"
>
{errorMessage && onLoadMore ? (
<div className="flex items-center gap-3 text-xs text-destructive">
<span>{errorMessage}</span>
<Button size="sm" variant="outline" onClick={onLoadMore}>
{tCommon("retry")}
</Button>
</div>
<span className="text-xs text-destructive">{errorMessage}</span>
) : (
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
)}
{errorMessage ? (
<span className="text-xs text-destructive">{errorMessage}</span>
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/my-usage/_components/usage-logs-table.tsx
Line: 202-208

Comment:
**Retry button removed but `onLoadMore` still used as guard**

The retry `<Button>` that called `onLoadMore` was removed, but `onLoadMore` is still checked in the condition (`errorMessage && onLoadMore`). This means the prop controls which branch renders, but is never actually invoked in the error branch — it's effectively dead. More importantly, users who hit a load-more error now have no interactive way to retry; they can only see the error text.

If the retry button was intentionally dropped, the condition guard can be simplified to just `errorMessage`, and the `onLoadMore` prop usage should be documented or removed upstream. If the retry capability should be preserved, the button needs to be restored.

```suggestion
                      {errorMessage ? (
                        <span className="text-xs text-destructive">{errorMessage}</span>
```

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

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

Caution

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

⚠️ Outside diff range comments (2)
src/actions/users.ts (2)

154-179: ⚠️ Potential issue | 🟠 Major

命中 scan cap 时不能静默返回“完整列表”。

这里达到 USER_LIST_ADMIN_SCAN_MAX 后直接返回 users,但上层 getUsers() 仍然只暴露 UserDisplay[]。实例用户数超过 2000 时,任何把它当全集使用的调用方都会无感知漏数。建议把“结果被截断”显式透传给上层,或者在命中 cap 时直接报错并强制改走分页接口。

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

In `@src/actions/users.ts` around lines 154 - 179, The current loop in getUsers
silently returns a truncated users array when USER_LIST_ADMIN_SCAN_MAX is
reached (users built from findUserListBatch), which can mislead callers that
expect the full set; change the contract so truncation is explicit: either throw
an error when the scan cap is hit or extend the getUsers return type to include
metadata (e.g., { users: UserDisplay[], truncated: true, cursor?: string } or
hasMore flag) and set that flag when you hit USER_LIST_ADMIN_SCAN_MAX (make sure
callers are updated), and keep the existing logger.warn call for observability
(refer to getUsers, USER_LIST_ADMIN_SCAN_MAX, findUserListBatch and logger.warn
to locate where to implement this).

493-518: ⚠️ Potential issue | 🟠 Major

请在 Action 边界收敛 limit

limit 现在直接透传到 searchUsersForFilterRepository(),这一层没有做 Number.isFiniteMath.trunc 或上限 clamp。服务端 action 不能只靠 TS 类型兜底;异常值和超大值都不该继续下沉到 repository。即便 5000 是这个接口的预期上限,也应该在这里固化成常量并显式截断,而不是接受任意运行时输入。

Also applies to: 526-530

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

In `@src/actions/users.ts` around lines 493 - 518, searchUsersForFilter currently
forwards the raw limit to searchUsersForFilterRepository allowing non-finite,
fractional, negative or excessively large values; add server-side validation in
the Action: define a MAX_LIMIT constant (e.g. 5000), normalize the incoming
limit with Number.isFinite and Math.trunc, default to a safe fallback when
missing, and clamp the value to the range [1, MAX_LIMIT] before calling
searchUsersForFilterRepository; apply the same validation/clamping logic to the
other similar action that also forwards limit.
🧹 Nitpick comments (2)
tests/unit/proxy/session.test.ts (1)

270-290: 建议同步更新用例名称,避免语义误导。

当前标题写“跳过价格解析”,但断言实际验证“继续解析并返回 redirected 价格”,建议改名以提升可读性。

建议修改
-  it("应在 getSystemSettings 失败且无缓存时跳过价格解析", async () => {
+  it("应在 getSystemSettings 失败且无缓存时回退到 redirected 并继续价格解析", async () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/proxy/session.test.ts` around lines 270 - 290, Rename the test
title that currently reads "应在 getSystemSettings 失败且无缓存时跳过价格解析" to accurately
reflect the assertions (it actually continues to resolve and returns the
redirected price); update the it(...) description to something like "应在
getSystemSettings 失败且无缓存时返回 redirected 模型价格" and ensure this change is applied
where the test constructs redirectedPriceData and calls
session.getCachedPriceDataByBillingSource(), and references mocked
getSystemSettings and findLatestPriceByModel so the new name matches the
behavior being asserted.
src/actions/usage-logs.ts (1)

46-46: UI 轮询间隔与服务端节流间隔不匹配,可能导致进度条"卡顿"感

根据 src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx 第 252 行,UI 每 800ms 轮询一次导出状态。但服务端节流间隔为 2000ms,这意味着 UI 会在约 2.5 次轮询期间收到相同的进度值,可能让用户感觉进度条"卡住"了。

建议考虑以下方案之一:

  1. 将 UI 轮询间隔调整为与服务端节流间隔一致(如 2000ms)
  2. 或者减小服务端节流间隔以匹配更频繁的 UI 轮询

Also applies to: 234-240

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

In `@src/actions/usage-logs.ts` at line 46, The server-side progress throttle
constant USAGE_LOGS_EXPORT_PROGRESS_UPDATE_INTERVAL_MS (in
src/actions/usage-logs.ts) is 2000ms which mismatches the UI polling interval in
usage-logs-filters.tsx (~800ms) and causes repeated identical progress values;
fix by either lowering USAGE_LOGS_EXPORT_PROGRESS_UPDATE_INTERVAL_MS to match
the UI (e.g., 800ms) or update the UI poll interval to 2000ms so they match, and
apply the same change to any other related constants in the same file/range (the
other occurrences around the 234-240 region) so server and client use the same
polling/throttle interval.
🤖 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/my-usage.ts`:
- Around line 558-560: The pagination handling for filters.page and
filters.pageSize must perform strict type validation and integer coercion
instead of relying on truthiness; update the handler in src/actions/my-usage.ts
to run the existing Zod schema (e.g. call filtersSchema.parse() or
filtersSchema.safeParse()) or explicitly coerce/validate the values before
computing rawPageSize/pageSize/page: convert values with
Number/parseInt/Math.trunc (or Math.floor) and check isFinite/NaN and >0,
default to 20 and 1 when invalid, clamp pageSize with Math.min(...,100) and
ensure page is an integer >=1; reference the variables rawPageSize, pageSize,
page and the filters object when making these changes.

In `@src/app/`[locale]/dashboard/logs/_components/usage-logs-filters.tsx:
- Around line 203-207: The ternary checking startResult.errorCode in the
toast.error call is dead code because startUsageLogsExport returns { ok:false,
error } only; remove the errorCode branch and always call
toast.error(t("logs.filters.exportError")), and optionally log or include
startResult.error for debugging (e.g., console.error(startResult.error)) inside
the same error handling block; also remove unused getErrorMessage import if it
becomes unused.

In `@src/app/v1/_lib/proxy/forwarder.ts`:
- Around line 3116-3121: 在 hedge 路径里不要把原始 Provider 直接传给
ProxyForwarder.doForward() 因为那会重新启用 provider.firstByteTimeoutStreamingMs
的内部首字节超时并造成“双重计时”;修复方法是在调用 ProxyForwarder.doForward(...) 时改为传入由
armAttemptThreshold(...) 返回或设置的“已装配/带阈值”的 provider(即用 armAttemptThreshold 的结果或
attempt 上存放的 armed/thresholded provider),确保 doForward 使用的是已应用首字节阈值的 provider
而不是原始 provider;同步修复其它相同调用点(当前还有 3294-3296 和 3491 的调用)以保持行为一致。
- Around line 3042-3051: 在 threshold 超时回调里不要把 "hedge_triggered" 写入
attempt.session(shadow session);将 attempt.session.addProviderToChain(...)
改为使用外层主
session(session.addProviderToChain(...)),保留原始字段(...attempt.endpointAudit,
reason: "hedge_triggered", attemptNumber: attempt.sequence, circuitState:
getCircuitState(attempt.provider.id))以确保 hedge 事件记录写入主 session,并继续调用 void
launchAlternative()
不变;参考符号:attempt.thresholdTimer、attempt.session.addProviderToChain、attempt.endpointAudit、attempt.sequence、getCircuitState、launchAlternative、session。

In `@src/types/css.d.ts`:
- Around line 1-4: The current declaration in src/types/css.d.ts exports a
default classes object which doesn't match how CSS is used (only side-effect
imports); update the module declaration for "*.css" to be a bare module with no
default export (so importing for side effects is allowed) by replacing the
exported classes type, or simply remove src/types/css.d.ts entirely if you
prefer; ensure the module name is "*.css" and no default export is declared so
files like the global CSS imports compile correctly.

---

Outside diff comments:
In `@src/actions/users.ts`:
- Around line 154-179: The current loop in getUsers silently returns a truncated
users array when USER_LIST_ADMIN_SCAN_MAX is reached (users built from
findUserListBatch), which can mislead callers that expect the full set; change
the contract so truncation is explicit: either throw an error when the scan cap
is hit or extend the getUsers return type to include metadata (e.g., { users:
UserDisplay[], truncated: true, cursor?: string } or hasMore flag) and set that
flag when you hit USER_LIST_ADMIN_SCAN_MAX (make sure callers are updated), and
keep the existing logger.warn call for observability (refer to getUsers,
USER_LIST_ADMIN_SCAN_MAX, findUserListBatch and logger.warn to locate where to
implement this).
- Around line 493-518: searchUsersForFilter currently forwards the raw limit to
searchUsersForFilterRepository allowing non-finite, fractional, negative or
excessively large values; add server-side validation in the Action: define a
MAX_LIMIT constant (e.g. 5000), normalize the incoming limit with
Number.isFinite and Math.trunc, default to a safe fallback when missing, and
clamp the value to the range [1, MAX_LIMIT] before calling
searchUsersForFilterRepository; apply the same validation/clamping logic to the
other similar action that also forwards limit.

---

Nitpick comments:
In `@src/actions/usage-logs.ts`:
- Line 46: The server-side progress throttle constant
USAGE_LOGS_EXPORT_PROGRESS_UPDATE_INTERVAL_MS (in src/actions/usage-logs.ts) is
2000ms which mismatches the UI polling interval in usage-logs-filters.tsx
(~800ms) and causes repeated identical progress values; fix by either lowering
USAGE_LOGS_EXPORT_PROGRESS_UPDATE_INTERVAL_MS to match the UI (e.g., 800ms) or
update the UI poll interval to 2000ms so they match, and apply the same change
to any other related constants in the same file/range (the other occurrences
around the 234-240 region) so server and client use the same polling/throttle
interval.

In `@tests/unit/proxy/session.test.ts`:
- Around line 270-290: Rename the test title that currently reads "应在
getSystemSettings 失败且无缓存时跳过价格解析" to accurately reflect the assertions (it
actually continues to resolve and returns the redirected price); update the
it(...) description to something like "应在 getSystemSettings 失败且无缓存时返回 redirected
模型价格" and ensure this change is applied where the test constructs
redirectedPriceData and calls session.getCachedPriceDataByBillingSource(), and
references mocked getSystemSettings and findLatestPriceByModel so the new name
matches the behavior being asserted.
🪄 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: 664a7af9-a266-458e-b756-ad65e5c687f2

📥 Commits

Reviewing files that changed from the base of the PR and between ed1d4db and 02aa47c.

📒 Files selected for processing (31)
  • .github/workflows/dev.yml
  • messages/ja/settings/providers/form/sections.json
  • src/actions/my-usage.ts
  • src/actions/usage-logs.ts
  • src/actions/users.ts
  • src/app/[locale]/dashboard/_components/rate-limit-top-users.tsx
  • src/app/[locale]/dashboard/_components/user/forms/add-key-form.tsx
  • src/app/[locale]/dashboard/_components/user/forms/edit-key-form.tsx
  • src/app/[locale]/dashboard/_components/user/forms/user-form.tsx
  • src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx
  • src/app/[locale]/dashboard/rate-limits/_components/rate-limit-filters.tsx
  • src/app/[locale]/dashboard/users/users-page-client.tsx
  • src/app/[locale]/my-usage/_components/usage-logs-section.test.tsx
  • src/app/[locale]/my-usage/_components/usage-logs-section.tsx
  • src/app/[locale]/my-usage/_components/usage-logs-table.tsx
  • src/app/api/actions/[...route]/route.ts
  • src/app/v1/_lib/proxy/forwarder.ts
  • src/app/v1/_lib/proxy/provider-selector.ts
  • src/app/v1/_lib/proxy/session.ts
  • src/repository/usage-logs.ts
  • src/repository/user.ts
  • src/types/css.d.ts
  • tests/api/api-actions-integrity.test.ts
  • tests/api/api-openapi-spec.test.ts
  • tests/integration/billing-model-source.test.ts
  • tests/unit/actions/my-usage-date-range-dst.test.ts
  • tests/unit/actions/usage-logs-export-retry-count.test.ts
  • tests/unit/dashboard-logs-export-progress-ui.test.tsx
  • tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts
  • tests/unit/proxy/session.test.ts
  • tests/unit/users-action-search-users-for-filter.test.ts
💤 Files with no reviewable changes (1)
  • src/app/[locale]/my-usage/_components/usage-logs-section.tsx

</div>
) : loadError ? (
<div className="flex h-[280px] items-center justify-center text-destructive">
{t("loadError")}
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] [STANDARD-VIOLATION] Missing i18n key for load-error state

Evidence (src/app/[locale]/dashboard/_components/rate-limit-top-users.tsx:117):

{t("loadError")}

Why this is a problem: The project requires i18n for user-facing strings. When a key is missing, this repo’s i18n config falls back to rendering ${namespace}.${key}, so users will see dashboard.rateLimits.topUsers.loadError instead of a translated message.

Suggested fix (reuse existing key to avoid adding new translations):

const t = useTranslations("dashboard.rateLimits.topUsers");
const tRateLimits = useTranslations("dashboard.rateLimits");

// ...
{tRateLimits("error")}

{/* 用户选择器 */}
<div className="space-y-2">
<Label htmlFor="user-select">{t("user")}</Label>
{usersLoadError ? <p className="text-xs text-destructive">{t("loadError")}</p> : 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] [STANDARD-VIOLATION] Missing i18n key for load-error hint

Evidence (src/app/[locale]/dashboard/rate-limits/_components/rate-limit-filters.tsx:183, :206):

{usersLoadError ? <p className="text-xs text-destructive">{t("loadError")}</p> : null}
{providersLoadError ? <p className="text-xs text-destructive">{t("loadError")}</p> : null}

Why this is a problem: t is scoped to dashboard.rateLimits.filters, but there is no loadError key under that namespace in the message files. With the current fallback behavior, users will see dashboard.rateLimits.filters.loadError rendered in the UI.

Suggested fix (reuse existing dashboard.rateLimits.error):

const t = useTranslations("dashboard.rateLimits.filters");
const tRateLimits = useTranslations("dashboard.rateLimits");

{usersLoadError ? <p className="text-xs text-destructive">{tRateLimits("error")}</p> : null}
{providersLoadError ? <p className="text-xs text-destructive">{tRateLimits("error")}</p> : null}


const usageLogSlimTotalCache = new TTLMap<string, number>({ ttlMs: 10_000, maxSize: 1000 });

export async function findUsageLogsForKeySlim(
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] New findUsageLogsForKeySlim logic is untested

Evidence (src/repository/usage-logs.ts:456):

export async function findUsageLogsForKeySlim(
  filters: UsageLogSlimFilters & { page?: number; pageSize?: number }
): Promise<{ logs: UsageLogSlimRow[]; total: number }> {

Why this is a problem: This function adds non-trivial behavior (page/pageSize clamping, total calculation, TTL total caching, and a ledger-only fallback path). The repo’s standards require new features to be covered by unit tests; currently there are no unit tests asserting the total/page/pageSize contract or ledger fallback behavior for this new API.

Suggested fix: Add unit coverage similar to existing repository tests (e.g. tests/unit/repository/usage-logs-sessionid-filter.test.ts) to lock down pagination + totals. For example:

// tests/unit/repository/usage-logs-slim-pagination.test.ts
import { describe, expect, test, vi } from "vitest";

test("findUsageLogsForKeySlim clamps page/pageSize", async () => {
  vi.resetModules();
  vi.doMock("@/lib/ledger-fallback", () => ({ isLedgerOnlyMode: vi.fn(async () => false) }));
  // mock db.select().from().where().orderBy().limit().offset() like existing repository tests

  const { findUsageLogsForKeySlim } = await import("@/repository/usage-logs");
  await findUsageLogsForKeySlim({ keyString: "k", page: 0, pageSize: 999 });

  // expect limit called with 101 (100 + 1) and offset called with 0, etc.
});

});

await launchingAlternative;
};
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] Dual-timeout race: internal abort and hedge threshold fire simultaneously

The PR removes the firstByteTimeoutStreamingMs: 0 override in runAttempt that previously prevented dual-timeout race conditions.

Why this is a problem:

  • In the base branch, runAttempt created a modified provider with zeroed timeout to let the external hedge threshold manage timing exclusively.
  • Now doForward receives the original provider with firstByteTimeoutStreamingMs > 0, causing both:
    1. External hedge threshold (armAttemptThreshold) fires at t = firstByteTimeoutMs
    2. Internal doForward abort fires at t = firstByteTimeoutStreamingMs (same value)

Both fire at the same tick, potentially causing race conditions in error handling paths. The abort causes doForward to reject immediately while launchAlternative() is also being dispatched.

Suggested fix:
Either restore the zeroing logic in runAttempt, or add a comment explaining why simultaneous timer firing is intentional/safe for this use case.

// Option A: Restore the safeguard
const runAttempt = (attempt: StreamingHedgeAttempt) => {
  const providerForRequest =
    attempt.provider.firstByteTimeoutStreamingMs > 0
      ? { ...attempt.provider, firstByteTimeoutStreamingMs: 0 }
      : attempt.provider;

  void ProxyForwarder.doForward(
    attempt.session,
    providerForRequest,
    // ...
  );

Validation: Checked base branch (dev) - the safeguard existed in original code. This is a regression introduced by this PR.

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 addresses review feedback from PR #968 (release v0.6.6), restoring backward API compatibility, adding admin query safeguards, and fixing several UI bugs. The implementation is generally solid with good test coverage.

PR Size: XL

  • Lines changed: 714 (605 additions, 109 deletions)
  • Files changed: 31

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 0 1 1
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 0 0
Simplification 0 0 0 0

Medium Priority Issues (Should Fix)

  1. Dual-timeout race in hedge forwarder (src/app/v1/_lib/proxy/forwarder.ts:3113)
    • The PR removes the firstByteTimeoutStreamingMs: 0 override in runAttempt, causing both the external hedge threshold timer and doForward's internal abort to fire simultaneously.
    • This could cause race conditions in error handling paths when providers have slow start times.
    • Suggested fix: Either restore the zeroing logic or document why simultaneous firing is intentional.

Low Priority Issues (Author's Discretion)

  1. Guard inconsistency in usage-logs-table (src/app/[locale]/my-usage/_components/usage-logs-table.tsx)
    • The condition errorMessage && onLoadMore checks onLoadMore but never invokes it in the error branch (retry button was removed).
    • Note: A retry button IS available in the table header, so users can still retry.
    • Suggested fix: Simplify the condition to just errorMessage if the retry capability in loader row is intentionally removed.

Positive Observations (Not Issues)

  • Admin scan cap: The 2000-user limit in loadAllUsersForAdmin is well-implemented with warning logging.
  • Key exposure security: The canExposeFullKey helper properly unifies security logic across batch endpoints.
  • Billing fallback fix: The hasUsableBillingSettings change correctly checks cachedBillingModelSource instead of billingSettingsSource.
  • Export filter fix: Using localFilters instead of filters correctly applies draft filters to exports.
  • Test coverage: Good regression tests for DST parsing, hedge retry timing, and billing fallback.

Review Coverage

  • Logic and correctness - 1 medium issue found
  • Security (OWASP Top 10) - Clean
  • Error handling - Clean (improved with debug logging)
  • Type safety - Clean
  • Documentation accuracy - Clean
  • Test coverage - Adequate
  • Code clarity - Clean

Automated review by Claude AI

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 bundles multiple behavior changes across actions, repository queries, proxy runtime, and dashboard UI. The main issues are missing i18n keys for newly added UI error states (will render fallback key strings) and a new pagination/total implementation added without dedicated unit coverage.

PR Size: XL

  • Lines changed: 714
  • Files changed: 31
  • Split suggestions:
    • PR 1: my-usage/getMyUsageLogs legacy compatibility + findUsageLogsForKeySlim
    • PR 2: Dashboard rate-limit UI load-error handling + i18n keys
    • PR 3: Proxy hedge + billing/pricing fallback changes
    • PR 4: Workflow/types/tests maintenance-only changes

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 2 0 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

Critical Issues (Must Fix)

  • src/app/[locale]/dashboard/_components/rate-limit-top-users.tsx:117 uses t("loadError") but dashboard.rateLimits.topUsers.loadError is missing, so the UI will render the fallback key string on error.
  • src/app/[locale]/dashboard/rate-limits/_components/rate-limit-filters.tsx:183 (also :206) uses t("loadError") but dashboard.rateLimits.filters.loadError is missing, so the UI will render the fallback key string on error.

High Priority Issues (Should Fix)

  • src/repository/usage-logs.ts:456 adds new findUsageLogsForKeySlim pagination/total logic (including caching + ledger fallback) without unit coverage asserting the total/page/pageSize contract.

Review Coverage

  • Logic and correctness
  • Security (OWASP Top 10)
  • Error handling
  • Type safety
  • Documentation accuracy
  • Test coverage
  • Code clarity

Automated review by Codex AI

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.

  • Reviewed PR #970 and applied the size label size/XL (31 files, 714 lines changed).
  • Left 3 inline review comments (diff-only) highlighting:
    • src/app/[locale]/dashboard/_components/rate-limit-top-users.tsx:117: t("loadError") references a missing i18n key → will render fallback key string; suggested reusing dashboard.rateLimits.error.
    • src/app/[locale]/dashboard/rate-limits/_components/rate-limit-filters.tsx:183 (also :206): same missing i18n key issue under dashboard.rateLimits.filters.
    • src/repository/usage-logs.ts:456: new findUsageLogsForKeySlim pagination/total + ledger fallback logic added without dedicated unit coverage; suggested adding a focused repo-style unit test.
  • Submitted the required PR review summary via gh pr review --comment (includes XL split suggestions).

@github-actions
Copy link
Copy Markdown
Contributor

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

Comment on lines +24 to +36
while (true) {
const result = await getUsersBatch({ cursor, limit: 200 });
if (!result.ok) {
throw new Error(result.error);
}

collectedUsers.push(...result.data.users);
if (!result.data.hasMore || !result.data.nextCursor) {
break;
}

cursor = result.data.nextCursor;
}
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 Unbounded pagination loop bypasses the admin scan cap

The while(true) loop here fetches all users via getUsersBatch with no row cap. This PR adds USER_LIST_ADMIN_SCAN_MAX = 2000 to loadAllUsersForAdmin to prevent memory exhaustion, but this new quotas-page loop bypasses that cap entirely by going through getUsersBatch directly.

For a deployment with, say, 10 000 users, this server-component loop will make 50 sequential DB round-trips and accumulate all 10 000 UserDisplay objects in collectedUsers before the page can render. That is the same memory and latency risk the fix was meant to address.

Consider adding an explicit cap consistent with USER_LIST_ADMIN_SCAN_MAX:

const MAX_USERS_FOR_QUOTAS = 2000;
let iterations = 0;

while (collectedUsers.length < MAX_USERS_FOR_QUOTAS) {
  const result = await getUsersBatch({ cursor, limit: 200 });
  if (!result.ok) throw new Error(result.error);

  collectedUsers.push(...result.data.users);
  if (!result.data.hasMore || !result.data.nextCursor) break;

  cursor = result.data.nextCursor;
  iterations += 1;
  if (iterations > MAX_USERS_FOR_QUOTAS / 200 + 1) break; // safety guard
}

Or, if quotas genuinely needs all users, at minimum add a logger.warn when the cap is approached so operators are alerted before an OOM.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/quotas/users/page.tsx
Line: 24-36

Comment:
**Unbounded pagination loop bypasses the admin scan cap**

The `while(true)` loop here fetches all users via `getUsersBatch` with no row cap. This PR adds `USER_LIST_ADMIN_SCAN_MAX = 2000` to `loadAllUsersForAdmin` to prevent memory exhaustion, but this new quotas-page loop bypasses that cap entirely by going through `getUsersBatch` directly.

For a deployment with, say, 10 000 users, this server-component loop will make 50 sequential DB round-trips and accumulate all 10 000 `UserDisplay` objects in `collectedUsers` before the page can render. That is the same memory and latency risk the fix was meant to address.

Consider adding an explicit cap consistent with `USER_LIST_ADMIN_SCAN_MAX`:

```typescript
const MAX_USERS_FOR_QUOTAS = 2000;
let iterations = 0;

while (collectedUsers.length < MAX_USERS_FOR_QUOTAS) {
  const result = await getUsersBatch({ cursor, limit: 200 });
  if (!result.ok) throw new Error(result.error);

  collectedUsers.push(...result.data.users);
  if (!result.data.hasMore || !result.data.nextCursor) break;

  cursor = result.data.nextCursor;
  iterations += 1;
  if (iterations > MAX_USERS_FOR_QUOTAS / 200 + 1) break; // safety guard
}
```

Or, if quotas genuinely needs all users, at minimum add a `logger.warn` when the cap is approached so operators are alerted before an OOM.

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: 407964e297

ℹ️ 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".

normalizedBaseParams,
});

throw new Error(`getUsers scan cap reached (${USER_LIST_ADMIN_SCAN_MAX})`);
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 getUsers compatibility requests working past 2,000 rows

Fresh evidence: this path now throws once the admin scan cap is reached instead of returning a partial array. The compatibility route in src/app/api/actions/[...route]/route.ts still documents getUsers as “不传时返回全部用户”, so any deployment with more than 2,000 matching users will now get a hard failure from /api/actions/users/getUsers (and any other admin caller that still relies on getUsers()), even though only the quotas page was migrated to paginated fetching.

Useful? React with 👍 / 👎.

Comment on lines +116 to +118
) : loadError ? (
<div className="flex h-[280px] items-center justify-center text-destructive">
{tRateLimits("error")}
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 Badge Keep top-users rankings visible when user lookup fails

The rate-limit stats are already present in the data prop, and this component still builds a User #<id> fallback for missing names. With the new loadError early return, any transient failure in searchUsers(undefined, 5000) now blanks the entire leaderboard instead of showing the counts with fallback labels, which makes the dashboard much less useful during the exact partial-outage case where only the name lookup fails.

Useful? React with 👍 / 👎.

Comment on lines +165 to +168
useEffect(() => {
if (!isAdmin) return;
void refetch();
}, [isAdmin, refetch]);
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 Badge Avoid refetching the first admin user page on mount

useInfiniteQuery already starts the initial admin getUsersBatchCore request for this query key. Calling refetch() unconditionally in a mount effect issues the same first-page fetch a second time on every admin page load, which doubles the expensive users/keys query work before any interaction and will be noticeable on larger installations.

Useful? React with 👍 / 👎.

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

Caution

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

⚠️ Outside diff range comments (1)
src/app/v1/_lib/proxy/forwarder.ts (1)

3003-3019: ⚠️ Potential issue | 🟡 Minor

hedge_loser_cancelled 仍然会写进 shadow session。

这里和 hedge_triggered 的可见性问题是同一类:失败的 shadow attempt 不会回灌主 session,所以 loser 事件会直接丢失。这里也应该写外层 session

建议修复
       if (reason === "hedge_loser") {
-        attempt.session.addProviderToChain(attempt.provider, {
+        session.addProviderToChain(attempt.provider, {
           ...attempt.endpointAudit,
           reason: "hedge_loser_cancelled",
           attemptNumber: attempt.sequence,
         });
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/v1/_lib/proxy/forwarder.ts` around lines 3003 - 3019, The loser hedge
is being recorded on the shadow attempt's session instead of the outer/main
session; change the addProviderToChain call in the reason === "hedge_loser"
branch to write into the outer session (the parent/main session) rather than the
shadow attempt's session—i.e. replace attempt.session.addProviderToChain(...)
with a call on the outer session object (e.g. attempt.session.session or the
parent/main session reference used elsewhere) so the "hedge_loser_cancelled"
entry is added to the outer session chain before aborting via
attempt.responseController.abort.
🧹 Nitpick comments (2)
src/app/[locale]/dashboard/quotas/users/page.tsx (1)

21-38: 建议添加最大迭代次数保护

虽然后端已添加 USER_LIST_ADMIN_SCAN_MAX = 2000 的扫描上限,但 while (true) 循环在客户端没有迭代保护。如果后端出现 bug(例如 cursor 卡住),可能导致无限循环。

建议添加防御性保护:

🛡️ 建议的防御性修改
 const collectedUsers: UserDisplay[] = [];
 let cursor: string | undefined;
+const MAX_ITERATIONS = 20; // 200 per batch * 20 = 4000 users max (well above 2000 cap)
+let iterations = 0;

-while (true) {
+while (iterations < MAX_ITERATIONS) {
+  iterations++;
   const result = await getUsersBatch({ cursor, limit: 200 });
   if (!result.ok) {
     throw new Error(result.error);
   }

   collectedUsers.push(...result.data.users);
   if (!result.data.hasMore || !result.data.nextCursor) {
     break;
   }

   cursor = result.data.nextCursor;
 }
+
+if (iterations >= MAX_ITERATIONS) {
+  console.warn("getUsersWithQuotas: reached max iterations, results may be incomplete");
+}

 const users = collectedUsers;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/dashboard/quotas/users/page.tsx around lines 21 - 38, The
unbounded while loop that repeatedly calls getUsersBatch and appends to
collectedUsers can hang if the backend cursor misbehaves; add a defensive
max-iteration guard (e.g., derive maxIterations from USER_LIST_ADMIN_SCAN_MAX
and the batch size 200 or use a fixed cap like 10) and track an iterations
counter inside the loop; if the counter exceeds maxIterations, break or throw a
descriptive error to avoid an infinite loop. Update the loop around
getUsersBatch, collectedUsers, cursor and use USER_LIST_ADMIN_SCAN_MAX (or a
clearly named local constant) to compute the cap so future backend limits stay
consistent.
src/app/[locale]/dashboard/rate-limits/_components/rate-limit-filters.tsx (1)

65-121: 可选:抽取通用异步加载模板以减少重复

用户与供应商两段加载逻辑结构几乎一致(loading/error/data/cancelled),后续可考虑提取为复用 hook,降低维护成本。当前不阻断本次 PR。

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

In `@src/app/`[locale]/dashboard/rate-limits/_components/rate-limit-filters.tsx
around lines 65 - 121, Both useEffect blocks for loading users and providers
duplicate the same async loading pattern (cancelled flag, loading/error/data
states); extract this into a small reusable hook (e.g., useAsyncLoad or
useListLoader) that accepts a loader function (searchUsers or getProviders) and
returns {data, loading, error, reload}. Replace the two useEffect
implementations in RateLimitFilters with calls to this hook, wiring
setUsers/setProviders and
setLoadingUsers/setLoadingProviders/setUsersLoadError/setProvidersLoadError to
the hook’s outputs, and ensure the hook preserves the cancellation logic and
finalization behavior currently implemented.
🤖 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/users/users-page-client.tsx:
- Around line 165-168: The useEffect that calls refetch() when isAdmin changes
causes duplicate fetches because the useInfiniteQuery keyed with isAdmin already
refetches on queryKey change; remove the useEffect block (the snippet using
useEffect, isAdmin, refetch) and rely on the useInfiniteQuery's queryKey change
to trigger the reload, or if you intentionally need an explicit refetch (e.g.,
to reset pagination), replace the effect with a guarded implementation that
compares previous isAdmin (useRef) and only calls refetch when necessary and add
a clear comment explaining why the manual refetch is required.

---

Outside diff comments:
In `@src/app/v1/_lib/proxy/forwarder.ts`:
- Around line 3003-3019: The loser hedge is being recorded on the shadow
attempt's session instead of the outer/main session; change the
addProviderToChain call in the reason === "hedge_loser" branch to write into the
outer session (the parent/main session) rather than the shadow attempt's
session—i.e. replace attempt.session.addProviderToChain(...) with a call on the
outer session object (e.g. attempt.session.session or the parent/main session
reference used elsewhere) so the "hedge_loser_cancelled" entry is added to the
outer session chain before aborting via attempt.responseController.abort.

---

Nitpick comments:
In `@src/app/`[locale]/dashboard/quotas/users/page.tsx:
- Around line 21-38: The unbounded while loop that repeatedly calls
getUsersBatch and appends to collectedUsers can hang if the backend cursor
misbehaves; add a defensive max-iteration guard (e.g., derive maxIterations from
USER_LIST_ADMIN_SCAN_MAX and the batch size 200 or use a fixed cap like 10) and
track an iterations counter inside the loop; if the counter exceeds
maxIterations, break or throw a descriptive error to avoid an infinite loop.
Update the loop around getUsersBatch, collectedUsers, cursor and use
USER_LIST_ADMIN_SCAN_MAX (or a clearly named local constant) to compute the cap
so future backend limits stay consistent.

In `@src/app/`[locale]/dashboard/rate-limits/_components/rate-limit-filters.tsx:
- Around line 65-121: Both useEffect blocks for loading users and providers
duplicate the same async loading pattern (cancelled flag, loading/error/data
states); extract this into a small reusable hook (e.g., useAsyncLoad or
useListLoader) that accepts a loader function (searchUsers or getProviders) and
returns {data, loading, error, reload}. Replace the two useEffect
implementations in RateLimitFilters with calls to this hook, wiring
setUsers/setProviders and
setLoadingUsers/setLoadingProviders/setUsersLoadError/setProvidersLoadError to
the hook’s outputs, and ensure the hook preserves the cancellation logic and
finalization behavior currently implemented.
🪄 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: d7edfc01-60f7-4663-aaf4-2731e4172824

📥 Commits

Reviewing files that changed from the base of the PR and between 02aa47c and 407964e.

📒 Files selected for processing (13)
  • .github/workflows/dev.yml
  • src/actions/my-usage.ts
  • src/actions/usage-logs.ts
  • src/actions/users.ts
  • src/app/[locale]/dashboard/_components/rate-limit-top-users.tsx
  • src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx
  • src/app/[locale]/dashboard/quotas/users/page.tsx
  • src/app/[locale]/dashboard/rate-limits/_components/rate-limit-filters.tsx
  • src/app/[locale]/dashboard/users/users-page-client.tsx
  • src/app/[locale]/my-usage/_components/usage-logs-table.tsx
  • src/app/v1/_lib/proxy/forwarder.ts
  • tests/unit/proxy/session.test.ts
  • tests/unit/repository/usage-logs-slim-pagination.test.ts
✅ Files skipped from review due to trivial changes (3)
  • .github/workflows/dev.yml
  • src/app/[locale]/my-usage/_components/usage-logs-table.tsx
  • tests/unit/repository/usage-logs-slim-pagination.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/actions/usage-logs.ts
  • src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx
  • src/app/[locale]/dashboard/_components/rate-limit-top-users.tsx
  • src/actions/users.ts
  • src/actions/my-usage.ts

@github-actions
Copy link
Copy Markdown
Contributor

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

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 (1)
src/app/[locale]/dashboard/_components/rate-limit-top-users.tsx (1)

118-124: 建议为错误提示补充可访问性语义。

当前错误文案是可见的,但缺少 role="alert"/aria-live,屏幕阅读器可能无法及时播报。

♿ 建议补丁
-          <div className="flex h-[280px] items-center justify-center text-muted-foreground">
+          <div
+            className="flex h-[280px] items-center justify-center text-muted-foreground"
+            role={loadError ? "alert" : undefined}
+            aria-live={loadError ? "polite" : undefined}
+          >
             {loadError ? tRateLimits("error") : t("noData")}
           </div>
@@
-            {loadError ? (
-              <div className="mb-2 text-xs text-destructive">{tRateLimits("error")}</div>
-            ) : null}
+            {loadError ? (
+              <div className="mb-2 text-xs text-destructive" role="alert" aria-live="polite">
+                {tRateLimits("error")}
+              </div>
+            ) : null}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/dashboard/_components/rate-limit-top-users.tsx around lines
118 - 124, The visible error text lacks accessibility semantics so screen
readers may not announce it; update the JSX that renders the error (the
conditional branches using loadError and the error div with className "mb-2
text-xs text-destructive" and the other inline error rendering that returns
tRateLimits("error")) to include ARIA live semantics (e.g., add role="alert" or
aria-live="assertive" and optionally aria-atomic="true") on those error
containers so assistive technologies announce the message promptly.
🤖 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/users/page.tsx:
- Around line 21-44: The loop that collects users (collectedUsers) must not use
iterations/MAX_ITERATIONS to infer truncation; instead detect truncation from
getUsersBatch's result.data.hasMore/nextCursor and whether we stopped because
MAX_USERS_FOR_QUOTAS was reached while hasMore stayed true. Change the logic in
the while that calls getUsersBatch(cursor, limit) to track if the final batch
indicated hasMore or provided a nextCursor when we hit MAX_USERS_FOR_QUOTAS (or
if the last returned page itself was trimmed), set a boolean truncated
accordingly (e.g., truncated = true when hasMore === true after collecting
MAX_USERS_FOR_QUOTAS or when the fetched page was smaller than requested because
it was trimmed), stop using iterations to drive the warning, and expose/return
that truncated flag to the caller/UI along with the collectedUsers so the UI can
display the result is incomplete; keep using cursor/nextCursor for paging but
rely on hasMore/nextCursor to determine truncation rather than iterations.

---

Nitpick comments:
In `@src/app/`[locale]/dashboard/_components/rate-limit-top-users.tsx:
- Around line 118-124: The visible error text lacks accessibility semantics so
screen readers may not announce it; update the JSX that renders the error (the
conditional branches using loadError and the error div with className "mb-2
text-xs text-destructive" and the other inline error rendering that returns
tRateLimits("error")) to include ARIA live semantics (e.g., add role="alert" or
aria-live="assertive" and optionally aria-atomic="true") on those error
containers so assistive technologies announce the message promptly.
🪄 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: 09fd90c4-e347-4cf7-bd0e-b7bcee0ac7b0

📥 Commits

Reviewing files that changed from the base of the PR and between 407964e and 1579706.

📒 Files selected for processing (5)
  • src/actions/users.ts
  • src/app/[locale]/dashboard/_components/rate-limit-top-users.tsx
  • src/app/[locale]/dashboard/quotas/users/page.tsx
  • src/app/[locale]/dashboard/users/users-page-client.tsx
  • src/app/v1/_lib/proxy/forwarder.ts
✅ Files skipped from review due to trivial changes (1)
  • src/app/[locale]/dashboard/users/users-page-client.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/actions/users.ts

Comment on lines +21 to +44
const collectedUsers: UserDisplay[] = [];
const MAX_USERS_FOR_QUOTAS = 2000;
const MAX_ITERATIONS = Math.ceil(MAX_USERS_FOR_QUOTAS / 200) + 1;
let cursor: string | undefined;
let iterations = 0;

while (collectedUsers.length < MAX_USERS_FOR_QUOTAS && iterations < MAX_ITERATIONS) {
iterations += 1;
const result = await getUsersBatch({ cursor, limit: 200 });
if (!result.ok) {
throw new Error(result.error);
}

collectedUsers.push(...result.data.users);
if (!result.data.hasMore || !result.data.nextCursor) {
break;
}

cursor = result.data.nextCursor;
}

if (iterations >= MAX_ITERATIONS) {
console.warn("getUsersWithQuotas: reached max iterations, results may be incomplete");
}
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

不要用 iterations 去推断是否发生了截断。

getUsersBatch() 已经返回了 hasMore / nextCursor,但这里仍然让 Line 42 只在命中迭代保护时告警。这样会出现两个方向的误判:当每页正好 200 条且实际用户数 > 2000 时,Line 34 会在第 10 页把列表凑满后静默退出,完全不会告警;而如果前几页不是满页,第 11 次合法请求又会被误报。更重要的是,这个函数没有把“结果已截断”向上层暴露,后面的总数展示就会把前 2000 条误当成全部用户。

建议基于“本批是否被裁剪”或“达到上限后仍然 hasMore”来设置 truncated,并把这个状态继续传给上层 UI。

可行的修正方式
   const collectedUsers: UserDisplay[] = [];
   const MAX_USERS_FOR_QUOTAS = 2000;
-  const MAX_ITERATIONS = Math.ceil(MAX_USERS_FOR_QUOTAS / 200) + 1;
+  const BATCH_SIZE = 200;
+  const MAX_ITERATIONS = Math.ceil(MAX_USERS_FOR_QUOTAS / BATCH_SIZE) + 1;
   let cursor: string | undefined;
   let iterations = 0;
+  let truncated = false;
+  let hasMoreAfterLastBatch = false;
 
   while (collectedUsers.length < MAX_USERS_FOR_QUOTAS && iterations < MAX_ITERATIONS) {
     iterations += 1;
-    const result = await getUsersBatch({ cursor, limit: 200 });
+    const result = await getUsersBatch({ cursor, limit: BATCH_SIZE });
     if (!result.ok) {
       throw new Error(result.error);
     }
 
-    collectedUsers.push(...result.data.users);
-    if (!result.data.hasMore || !result.data.nextCursor) {
+    const remaining = MAX_USERS_FOR_QUOTAS - collectedUsers.length;
+    collectedUsers.push(...result.data.users.slice(0, remaining));
+
+    hasMoreAfterLastBatch = result.data.hasMore && Boolean(result.data.nextCursor);
+    if (
+      result.data.users.length > remaining ||
+      (collectedUsers.length >= MAX_USERS_FOR_QUOTAS && hasMoreAfterLastBatch)
+    ) {
+      truncated = true;
+      break;
+    }
+
+    if (!hasMoreAfterLastBatch) {
       break;
     }
 
-    cursor = result.data.nextCursor;
+    cursor = result.data.nextCursor ?? undefined;
   }
 
-  if (iterations >= MAX_ITERATIONS) {
-    console.warn("getUsersWithQuotas: reached max iterations, results may be incomplete");
+  if (truncated || (iterations >= MAX_ITERATIONS && hasMoreAfterLastBatch)) {
+    console.warn("getUsersWithQuotas: results may be incomplete");
   }
📝 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
const collectedUsers: UserDisplay[] = [];
const MAX_USERS_FOR_QUOTAS = 2000;
const MAX_ITERATIONS = Math.ceil(MAX_USERS_FOR_QUOTAS / 200) + 1;
let cursor: string | undefined;
let iterations = 0;
while (collectedUsers.length < MAX_USERS_FOR_QUOTAS && iterations < MAX_ITERATIONS) {
iterations += 1;
const result = await getUsersBatch({ cursor, limit: 200 });
if (!result.ok) {
throw new Error(result.error);
}
collectedUsers.push(...result.data.users);
if (!result.data.hasMore || !result.data.nextCursor) {
break;
}
cursor = result.data.nextCursor;
}
if (iterations >= MAX_ITERATIONS) {
console.warn("getUsersWithQuotas: reached max iterations, results may be incomplete");
}
const collectedUsers: UserDisplay[] = [];
const MAX_USERS_FOR_QUOTAS = 2000;
const BATCH_SIZE = 200;
const MAX_ITERATIONS = Math.ceil(MAX_USERS_FOR_QUOTAS / BATCH_SIZE) + 1;
let cursor: string | undefined;
let iterations = 0;
let truncated = false;
let hasMoreAfterLastBatch = false;
while (collectedUsers.length < MAX_USERS_FOR_QUOTAS && iterations < MAX_ITERATIONS) {
iterations += 1;
const result = await getUsersBatch({ cursor, limit: BATCH_SIZE });
if (!result.ok) {
throw new Error(result.error);
}
const remaining = MAX_USERS_FOR_QUOTAS - collectedUsers.length;
collectedUsers.push(...result.data.users.slice(0, remaining));
hasMoreAfterLastBatch = result.data.hasMore && Boolean(result.data.nextCursor);
if (
result.data.users.length > remaining ||
(collectedUsers.length >= MAX_USERS_FOR_QUOTAS && hasMoreAfterLastBatch)
) {
truncated = true;
break;
}
if (!hasMoreAfterLastBatch) {
break;
}
cursor = result.data.nextCursor ?? undefined;
}
if (truncated || (iterations >= MAX_ITERATIONS && hasMoreAfterLastBatch)) {
console.warn("getUsersWithQuotas: results may be incomplete");
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/dashboard/quotas/users/page.tsx around lines 21 - 44, The
loop that collects users (collectedUsers) must not use iterations/MAX_ITERATIONS
to infer truncation; instead detect truncation from getUsersBatch's
result.data.hasMore/nextCursor and whether we stopped because
MAX_USERS_FOR_QUOTAS was reached while hasMore stayed true. Change the logic in
the while that calls getUsersBatch(cursor, limit) to track if the final batch
indicated hasMore or provided a nextCursor when we hit MAX_USERS_FOR_QUOTAS (or
if the last returned page itself was trimmed), set a boolean truncated
accordingly (e.g., truncated = true when hasMore === true after collecting
MAX_USERS_FOR_QUOTAS or when the fetched page was smaller than requested because
it was trimmed), stop using iterations to drive the warning, and expose/return
that truncated flag to the caller/UI along with the collectedUsers so the UI can
display the result is incomplete; keep using cursor/nextCursor for paging but
rely on hasMore/nextCursor to determine truncation rather than iterations.

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: 1579706f21

ℹ️ 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".

let cursor: string | undefined;
let iterations = 0;

while (collectedUsers.length < MAX_USERS_FOR_QUOTAS && iterations < MAX_ITERATIONS) {
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 Badge Surface when the quotas page truncates after 2,000 users

This loop now stops as soon as collectedUsers.length reaches MAX_USERS_FOR_QUOTAS, even if getUsersBatch() is still returning hasMore=true. Because the only warning path below is the max-iteration branch, an installation with more than 2,000 users will silently omit the tail of /dashboard/quotas/users while still rendering counts and quota totals from the truncated array, so admins have no indication that the page is incomplete.

Useful? React with 👍 / 👎.

Comment on lines 745 to 751
if (!this.hasUsableBillingSettings()) {
logger.warn("[ProxySession] Billing settings unavailable, skip pricing resolution");
return null;
logger.warn("[ProxySession] Billing settings unavailable, using fallback billing source", {
billingSettingsSource: this.billingSettingsSource,
fallbackBillingModelSource: this.cachedBillingModelSource,
});
}

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 Dead warning branch — hasUsableBillingSettings() can never be false here

After await this.loadBillingSettings() completes, this.cachedBillingModelSource is always set to either "original" or "redirected" — the method's inner async IIFE catches all errors and falls back to "redirected" in every code path. Therefore hasUsableBillingSettings() (which checks cachedBillingModelSource === "original" || cachedBillingModelSource === "redirected") will always return true at this point, making the logger.warn block unreachable dead code.

The core behavioral fix (removing return null) is correct and valuable. The dead guard can simply be removed to keep the code clear:

Suggested change
if (!this.hasUsableBillingSettings()) {
logger.warn("[ProxySession] Billing settings unavailable, skip pricing resolution");
return null;
logger.warn("[ProxySession] Billing settings unavailable, using fallback billing source", {
billingSettingsSource: this.billingSettingsSource,
fallbackBillingModelSource: this.cachedBillingModelSource,
});
}
const providerIdentity = provider ?? this.provider;
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/session.ts
Line: 745-751

Comment:
**Dead warning branch — `hasUsableBillingSettings()` can never be false here**

After `await this.loadBillingSettings()` completes, `this.cachedBillingModelSource` is always set to either `"original"` or `"redirected"` — the method's inner async IIFE catches all errors and falls back to `"redirected"` in every code path. Therefore `hasUsableBillingSettings()` (which checks `cachedBillingModelSource === "original" || cachedBillingModelSource === "redirected"`) will always return `true` at this point, making the `logger.warn` block unreachable dead code.

The core behavioral fix (removing `return null`) is correct and valuable. The dead guard can simply be removed to keep the code clear:

```suggestion
    const providerIdentity = provider ?? this.provider;
```

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

@ding113 ding113 merged commit 96b116e into dev Mar 23, 2026
16 checks passed
@github-project-automation github-project-automation bot moved this from Backlog to Done in Claude Code Hub Roadmap Mar 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core area:UI bug Something isn't working size/XL Extra Large PR (> 1000 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant