Skip to content

feat: add 5h fixed interval reset mode for rate limiting#972

Open
sususu98 wants to merge 1 commit intoding113:devfrom
sususu98:feat/5h-fixed-reset-mode
Open

feat: add 5h fixed interval reset mode for rate limiting#972
sususu98 wants to merge 1 commit intoding113:devfrom
sususu98:feat/5h-fixed-reset-mode

Conversation

@sususu98
Copy link
Contributor

@sususu98 sususu98 commented Mar 23, 2026

Summary

Add configurable 5-hour quota reset mode (fixed or rolling) for users, keys, and providers. This enables administrators to choose between a fixed 5-hour block window with a configurable anchor timestamp, or the existing rolling/sliding window behavior.

Fixes #947

Problem

The 5-hour rate limit (limit_5h_usd) previously only supported a rolling/sliding window mode using Redis ZSET. While this provides smooth usage tracking, some use cases require:

  • Predictable budgeting: Users need to know exactly when their quota resets
  • Consistency: Align with external API providers that use fixed window rate limits
  • Parity: The daily limit already supports both fixed and rolling modes

Solution

Replicate the existing daily_reset_mode pattern to support both modes for the 5-hour limit:

Mode Behavior Redis Structure
rolling (default) Continuously sliding past 5h ZSET with timestamps
fixed 5h block from configurable anchor STRING + INCRBYFLOAT + TTL

Key implementation decisions:

  • Atomic Redis operations: Uses Lua script (TRACK_COST_5H_FIXED_WINDOW) for INCRBYFLOAT+EXPIRE to prevent TTL-less keys on crash
  • Lease window identity tracking: Correctly invalidates cache when switching between modes
  • Backward compatible: Defaults to 'rolling' - zero behavioral change for existing deployments

Changes

Schema & Migration

  • New columns five_hour_reset_mode and five_hour_reset_anchor on users, keys, providers tables
  • Reuses existing daily_reset_mode PostgreSQL enum (no new enum creation)
  • Migration: drizzle/0086_furry_donald_blake.sql

Core Changes

  • Rate limit core: time-utils.ts, lease.ts, lease-service.ts, service.ts updated for fixed block calculation
  • Proxy pipeline: rate-limit-guard.ts, provider-selector.ts, response-handler.ts pass new fields through
  • Redis: New Lua script for atomic 5h fixed window tracking

API & Actions

  • keys.ts, providers.ts, users.ts handle new fields with timezone-aware anchor parsing
  • Provider batch patch contract updated for new fields

Frontend

  • All create/edit forms support the new mode selector and anchor input
  • Batch edit support for providers

Validation & Utilities

  • Extract strict datetime validation to date-input.ts for reuse
  • Updated provider-patch-contract.ts for batch operations

Internationalization

  • Full i18n support for all 5 languages (zh-CN, zh-TW, en, ja, ru)
  • Added translations for mode labels, anchor inputs, and error messages

Breaking Changes

None. The migration is purely additive with default 'rolling' ensuring backward compatibility.

Testing

Automated Tests

  • Unit tests for get5hFixedBlockRange and get5hFixedBlockKeySuffix (time-utils)
  • Unit tests for lease window identity changes on mode switch
  • Unit tests for anchor timezone parsing in key/user/provider actions
  • Unit tests for provider batch patch contract (normalize, apply, clear)
  • Unit tests for UI form expiry/anchor clear behavior
  • TypeScript type check passes

Manual Testing

  • Create key with fixed 5h mode and verify reset behavior
  • Switch between rolling/fixed modes and verify cache invalidation

Files Changed (66 files)

Schema & Types: schema.ts, user.ts, key.ts, provider.ts, schemas.ts
Repository: user.ts, key.ts, provider.ts, transformers.ts
Rate Limit Core: service.ts, time-utils.ts, lease.ts, lease-service.ts
Proxy: rate-limit-guard.ts, response-handler.ts, provider-selector.ts
Actions: users.ts, keys.ts, providers.ts
Frontend: All provider/user/key form components
i18n: All 5 language files


Description enhanced by Claude AI

Greptile Summary

This PR introduces a fixed 5-hour interval reset mode for rate limiting, complementing the existing rolling-window 5h behaviour. When fiveHourResetMode = "fixed", usage is bucketed into discrete 5-hour blocks anchored to a configurable timestamp (fiveHourResetAnchor, falling back to createdAt); each block gets its own Redis string key keyed by block-start milliseconds, written atomically via a new Lua script (TRACK_COST_5H_FIXED_WINDOW). When fiveHourResetMode = "rolling" (the default), the existing ZSET sliding-window path is preserved unchanged. The change is wired end-to-end: DB migration, schema, repository, types, actions, proxy pipeline, UI forms, i18n, and tests are all updated consistently.

Key changes and findings:

  • Core algorithm (time-utils.ts, lease.ts, lease-service.ts, service.ts): get5hFixedBlockRange / get5hFixedBlockKeySuffix correctly compute block boundaries and produce per-block Redis key suffixes. Lease window-identity tracking (buildLeaseWindowIdentity) correctly invalidates cached leases on block transitions or mode switches.
  • Atomic Redis tracking (lua-scripts.ts): TRACK_COST_5H_FIXED_WINDOW uses an atomic Lua INCRBYFLOAT + EXPIRE to prevent TTL-less keys on crash. EXPIRE is called on every increment (harmless, but slightly re-resets an already-correct TTL on each write — see inline comment).
  • User 5h costs not Redis-tracked (service.ts): Only key and provider 5h fixed costs are written to Redis; user-level 5h costs always fall back to a DB query. This is documented as a TODO but means every request with a user 5h fixed limit will generate a synchronous DB round-trip, which could become a bottleneck at scale.
  • Batch-edit error path (providers.ts): mapApplyUpdatesToRepositoryFormat throws a plain Error on future-date anchor validation. The outer try-catch catches it and returns { ok: false, error: message }, but no errorCode is included — inconsistent with all other structured returns in applyProviderBatchPatch.
  • Code duplication: validateFiveHourResetAnchor is copy-pasted into keys.ts, providers.ts, and users.ts; should be extracted to a shared utility.
  • Anchor resolution inconsistency: rate-limit-guard.ts pre-resolves the anchor with resolveEffective5hFixedAnchor(anchor, createdAt) before passing it to the service, but provider-selector.ts and response-handler.ts pass the raw ?? createdAt value directly. The service layer re-resolves it, so behaviour is the same, but the asymmetry is a readability concern.

Confidence Score: 4/5

  • PR is safe to merge; the fixed 5h window logic is correct and well-tested, with one documented Redis-tracking gap for user-level limits.
  • Core algorithm (block-range math, Lua atomic increment, lease window-identity invalidation) is correct with comprehensive unit tests. The main functional gap — user 5h costs not tracked in Redis for fixed mode — is documented as a TODO and only affects performance (falls back to DB), not correctness. The mapApplyUpdatesToRepositoryFormat error path issue is a minor inconsistency (missing errorCode) caught by the outer try-catch. No data-loss or security issues found. Code duplication and anchor-resolution asymmetry are style-level concerns. Score reflects one concrete follow-up (user Redis tracking) before high-load production use.
  • src/actions/providers.ts (structured error code missing from batch-apply validation path), src/lib/rate-limit/service.ts (user 5h fixed-mode always queries DB)

Important Files Changed

Filename Overview
src/lib/rate-limit/time-utils.ts Adds get5hFixedBlockRange, get5hFixedBlockKeySuffix, and resolveEffective5hFixedAnchor; all existing TTL/range functions extended with optional anchor param; logic is correct and well-tested.
src/lib/rate-limit/service.ts Implements dual-path (fixed STRING vs rolling ZSET) for 5h checks and tracking; key/provider Redis tracking correct, but user 5h fixed-mode costs are never written to Redis — every user-level check hits the DB (documented TODO).
src/lib/redis/lua-scripts.ts Adds TRACK_COST_5H_FIXED_WINDOW Lua script with atomic INCRBYFLOAT + EXPIRE to prevent TTL-less keys; logic is sound, though EXPIRE resets TTL on every increment (see review comment).
src/lib/rate-limit/lease-service.ts Window identity tracking added to correctly invalidate the lease cache when 5h mode/anchor changes; buildLeaseWindowIdentity correctly encodes block-start timestamp for fixed mode.
src/actions/providers.ts New five_hour_reset_anchor validated for future-date in mapApplyUpdatesToRepositoryFormat, but validation failure throws a plain Error (no errorCode) that is caught by the outer catch — inconsistent with all other structured error returns in this function.
src/app/v1/_lib/proxy/rate-limit-guard.ts Properly resolves anchor with createdAt fallback via resolveEffective5hFixedAnchor; error messages correctly branch on fixed vs rolling mode to include or omit a reset time.
src/app/v1/_lib/proxy/provider-selector.ts Passes new 5h fields through; anchor is passed raw (?? createdAt) without going through resolveEffective5hFixedAnchor first, unlike the consistent pattern used in rate-limit-guard.ts.
src/lib/validation/schemas.ts Adds four schema variants for the anchor input (with-default, optional, nullable, nullable-update); uses isValidDateTimeString for format validation; handles null/empty string correctly.
src/lib/utils/date-input.ts Extracts and adds isValidDateTimeLocalString, isValidDateTimeString, and isFutureDate; regex + calendar validation is thorough (leap year handling, days-per-month).
drizzle/0086_furry_donald_blake.sql Adds five_hour_reset_mode (reusing existing daily_reset_mode enum, defaults to rolling) and nullable five_hour_reset_anchor timestamp to keys, providers, and users tables; backward-safe.

Sequence Diagram

sequenceDiagram
    participant Client
    participant RateLimitGuard
    participant LeaseService
    participant RateLimitService
    participant Redis
    participant DB

    Client->>RateLimitGuard: Incoming request
    RateLimitGuard->>RateLimitGuard: Resolve 5h anchor (fiveHourResetAnchor OR createdAt)
    RateLimitGuard->>LeaseService: checkCostLimitsWithLease(key.id, mode=fixed, anchor)

    LeaseService->>LeaseService: buildLeaseWindowIdentity => "5h:fixed:BLOCK_START_MS"
    LeaseService->>Redis: GET cached lease

    alt Cache hit AND identity matches
        Redis-->>LeaseService: BudgetLease (same block)
        LeaseService-->>RateLimitGuard: allowed / denied
    else Cache miss OR identity changed (new block)
        LeaseService->>DB: Query spend in current 5h block
        DB-->>LeaseService: Cost entries
        LeaseService->>Redis: Cache new lease with block TTL
        LeaseService-->>RateLimitGuard: allowed / denied
    end

    alt Request allowed
        RateLimitGuard->>Client: Pass through to provider

        note over RateLimitService,Redis: After response - trackCost
        alt fixed mode
            RateLimitService->>Redis: Lua script: INCRBYFLOAT key cost, then EXPIRE key remaining_block_secs
        else rolling mode
            RateLimitService->>Redis: Lua script: ZADD + ZREMRANGEBYSCORE on ZSET
        end
    else Rate limited
        RateLimitGuard->>Client: 429 RateLimitError with resetTime
    end
Loading

Comments Outside Diff (3)

  1. src/actions/providers.ts, line 1591-1602 (link)

    P1 Unstructured throw loses errorCode in batch-patch flow

    mapApplyUpdatesToRepositoryFormat throws a plain Error when the future-date validation fails inside the batch-apply path. The outer try-catch in applyProviderBatchPatch does catch it and returns { ok: false, error: message }, but the response loses the errorCode that all other structured error returns in this function include.

    Callers that pattern-match on errorCode (e.g. to display a localised error or to distinguish a validation failure from an internal error) will see undefined. All other validation errors in the same function return a proper errorCode.

    Consider either returning a structured result from mapApplyUpdatesToRepositoryFormat (matching the rest of the codebase's { ok, error, errorCode } pattern), or at minimum re-throwing a typed error that carries the code:

    // Option A: make the function return a result type
    if (fiveHourResetAnchor) {
      const anchorValidation = validateFiveHourResetAnchor(fiveHourResetAnchor, tError);
      if (anchorValidation) {
        return { ok: false as const, ...anchorValidation };
      }
    }
    // ...and change the signature to Promise<{ ok: true; data: BatchProviderUpdates } | { ok: false; error: string; errorCode: string }>
    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/actions/providers.ts
    Line: 1591-1602
    
    Comment:
    **Unstructured throw loses `errorCode` in batch-patch flow**
    
    `mapApplyUpdatesToRepositoryFormat` throws a plain `Error` when the future-date validation fails inside the batch-apply path. The outer `try-catch` in `applyProviderBatchPatch` does catch it and returns `{ ok: false, error: message }`, but the response loses the `errorCode` that all other structured error returns in this function include.
    
    Callers that pattern-match on `errorCode` (e.g. to display a localised error or to distinguish a validation failure from an internal error) will see `undefined`. All other validation errors in the same function return a proper `errorCode`.
    
    Consider either returning a structured result from `mapApplyUpdatesToRepositoryFormat` (matching the rest of the codebase's `{ ok, error, errorCode }` pattern), or at minimum re-throwing a typed error that carries the code:
    
    ```ts
    // Option A: make the function return a result type
    if (fiveHourResetAnchor) {
      const anchorValidation = validateFiveHourResetAnchor(fiveHourResetAnchor, tError);
      if (anchorValidation) {
        return { ok: false as const, ...anchorValidation };
      }
    }
    // ...and change the signature to Promise<{ ok: true; data: BatchProviderUpdates } | { ok: false; error: string; errorCode: string }>
    ```
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. src/lib/rate-limit/service.ts, line 403-405 (link)

    P1 User 5h fixed-mode costs untracked in Redis — every check hits the DB

    The comment acknowledges this explicitly, but it's worth calling out as a concrete correctness gap rather than just a performance note:

    NOTE: 仅追踪 key 和 provider 的 5h 费用到 Redis,user 5h 费用通过 DB 查询获取
    TODO: 后续可添加 user 5h Redis 追踪以减少 DB 查询延迟
    

    Under fixed 5h mode, every inbound request that hits a user-level 5h limit will fall through the Redis fast path (since nothing writes to user:${userId}:cost_5h_fixed_*) and issue a synchronous DB query. For high-throughput scenarios this may become a bottleneck and effectively removes the benefits of the Redis-first architecture for user-level fixed 5h rate limiting. The rolling mode continues to be tracked via the existing ZSET path, so only users on fixed mode are affected.

    This is tracked as a TODO, but consider prioritising it before enabling fixed mode widely in production.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/lib/rate-limit/service.ts
    Line: 403-405
    
    Comment:
    **User 5h fixed-mode costs untracked in Redis — every check hits the DB**
    
    The comment acknowledges this explicitly, but it's worth calling out as a concrete correctness gap rather than just a performance note:
    
    ```
    NOTE: 仅追踪 key 和 provider 的 5h 费用到 Redis,user 5h 费用通过 DB 查询获取
    TODO: 后续可添加 user 5h Redis 追踪以减少 DB 查询延迟
    ```
    
    Under `fixed` 5h mode, every inbound request that hits a user-level 5h limit will fall through the Redis fast path (since nothing writes to `user:${userId}:cost_5h_fixed_*`) and issue a synchronous DB query. For high-throughput scenarios this may become a bottleneck and effectively removes the benefits of the Redis-first architecture for user-level fixed 5h rate limiting. The rolling mode continues to be tracked via the existing ZSET path, so only users on fixed mode are affected.
    
    This is tracked as a TODO, but consider prioritising it before enabling fixed mode widely in production.
    
    How can I resolve this? If you propose a fix, please make it concise.
  3. src/lib/redis/lua-scripts.ts, line 278-291 (link)

    P2 EXPIRE is called unconditionally on every increment, not just on key creation

    The Lua script resets the TTL on every INCRBYFLOAT:

    local new_val = redis.call('INCRBYFLOAT', key, cost)
    redis.call('EXPIRE', key, ttl)

    Because ttl is calculated as Math.ceil((blockEnd - now) / 1000) immediately before redis.eval is called, each write resets the TTL to the current remaining time in the block. Within a single block this is functionally harmless (the key always expires at approximately block-end), but it means the TTL in Redis is always slightly behind the wall-clock calculation (by the few milliseconds of network round-trip).

    A tighter alternative that avoids re-resetting already-correct TTLs is to use EXPIRE key ttl XX (only set TTL if key already has one) combined with EXPIRE key ttl NX (only set TTL if key has none). That said, the current approach is safe; this is a minor precision note rather than a bug.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/lib/redis/lua-scripts.ts
    Line: 278-291
    
    Comment:
    **`EXPIRE` is called unconditionally on every increment, not just on key creation**
    
    The Lua script resets the TTL on every `INCRBYFLOAT`:
    
    ```lua
    local new_val = redis.call('INCRBYFLOAT', key, cost)
    redis.call('EXPIRE', key, ttl)
    ```
    
    Because `ttl` is calculated as `Math.ceil((blockEnd - now) / 1000)` immediately before `redis.eval` is called, each write resets the TTL to the *current* remaining time in the block. Within a single block this is functionally harmless (the key always expires at approximately block-end), but it means the TTL in Redis is always slightly behind the wall-clock calculation (by the few milliseconds of network round-trip).
    
    A tighter alternative that avoids re-resetting already-correct TTLs is to use `EXPIRE key ttl XX` (only set TTL if key already has one) combined with `EXPIRE key ttl NX` (only set TTL if key has none). That said, the current approach is safe; this is a minor precision note rather than a bug.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/actions/providers.ts
Line: 1591-1602

Comment:
**Unstructured throw loses `errorCode` in batch-patch flow**

`mapApplyUpdatesToRepositoryFormat` throws a plain `Error` when the future-date validation fails inside the batch-apply path. The outer `try-catch` in `applyProviderBatchPatch` does catch it and returns `{ ok: false, error: message }`, but the response loses the `errorCode` that all other structured error returns in this function include.

Callers that pattern-match on `errorCode` (e.g. to display a localised error or to distinguish a validation failure from an internal error) will see `undefined`. All other validation errors in the same function return a proper `errorCode`.

Consider either returning a structured result from `mapApplyUpdatesToRepositoryFormat` (matching the rest of the codebase's `{ ok, error, errorCode }` pattern), or at minimum re-throwing a typed error that carries the code:

```ts
// Option A: make the function return a result type
if (fiveHourResetAnchor) {
  const anchorValidation = validateFiveHourResetAnchor(fiveHourResetAnchor, tError);
  if (anchorValidation) {
    return { ok: false as const, ...anchorValidation };
  }
}
// ...and change the signature to Promise<{ ok: true; data: BatchProviderUpdates } | { ok: false; error: string; errorCode: string }>
```

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/lib/rate-limit/service.ts
Line: 403-405

Comment:
**User 5h fixed-mode costs untracked in Redis — every check hits the DB**

The comment acknowledges this explicitly, but it's worth calling out as a concrete correctness gap rather than just a performance note:

```
NOTE: 仅追踪 key 和 provider 的 5h 费用到 Redis,user 5h 费用通过 DB 查询获取
TODO: 后续可添加 user 5h Redis 追踪以减少 DB 查询延迟
```

Under `fixed` 5h mode, every inbound request that hits a user-level 5h limit will fall through the Redis fast path (since nothing writes to `user:${userId}:cost_5h_fixed_*`) and issue a synchronous DB query. For high-throughput scenarios this may become a bottleneck and effectively removes the benefits of the Redis-first architecture for user-level fixed 5h rate limiting. The rolling mode continues to be tracked via the existing ZSET path, so only users on fixed mode are affected.

This is tracked as a TODO, but consider prioritising it before enabling fixed mode widely in production.

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/actions/keys.ts
Line: 36-52

Comment:
**`validateFiveHourResetAnchor` duplicated across three action files**

An identical function (or near-identical: `keys.ts` uses a local `TranslationFunction` alias while `providers.ts` and `users.ts` use `Awaited<ReturnType<typeof getTranslations<"errors">>>`) is defined in each of `keys.ts`, `providers.ts`, and `users.ts`. Any future change to the validation rules (e.g., adding a minimum-age check) must be made in three places and is prone to drift.

The same pattern was already solved for `parseDateInputAsTimezone` and `isFutureDate` being shared from `src/lib/utils/date-input.ts`. Consider extracting `validateFiveHourResetAnchor` into a shared utility module (e.g. `src/lib/rate-limit/anchor-validation.ts` or alongside `date-input.ts`) that all three action files import.

The same duplication exists at `src/actions/providers.ts:122-143` and `src/actions/users.ts:297-318`.

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/lib/redis/lua-scripts.ts
Line: 278-291

Comment:
**`EXPIRE` is called unconditionally on every increment, not just on key creation**

The Lua script resets the TTL on every `INCRBYFLOAT`:

```lua
local new_val = redis.call('INCRBYFLOAT', key, cost)
redis.call('EXPIRE', key, ttl)
```

Because `ttl` is calculated as `Math.ceil((blockEnd - now) / 1000)` immediately before `redis.eval` is called, each write resets the TTL to the *current* remaining time in the block. Within a single block this is functionally harmless (the key always expires at approximately block-end), but it means the TTL in Redis is always slightly behind the wall-clock calculation (by the few milliseconds of network round-trip).

A tighter alternative that avoids re-resetting already-correct TTLs is to use `EXPIRE key ttl XX` (only set TTL if key already has one) combined with `EXPIRE key ttl NX` (only set TTL if key has none). That said, the current approach is safe; this is a minor precision note rather than a bug.

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/provider-selector.ts
Line: 649-650

Comment:
**Raw anchor passed without `resolveEffective5hFixedAnchor` validation**

In `rate-limit-guard.ts` the anchor is pre-resolved before passing it to the service:

```ts
const key5hAnchor = resolveEffective5hFixedAnchor(key.fiveHourResetAnchor, key.createdAt);
```

But here and at line 1050, the anchor is passed raw:

```ts
five_hour_reset_anchor: provider.fiveHourResetAnchor ?? provider.createdAt ?? undefined,
```

The service layer does call `resolveEffective5hFixedAnchor` on whatever is passed in, so the behaviour is ultimately the same. But the asymmetry is confusing for readers and could cause unexpected behaviour if the service's internal handling ever changes. For consistency, consider applying `resolveEffective5hFixedAnchor` at the call site here too, matching the pattern used in `rate-limit-guard.ts`.

The same applies at line 1050 and in `response-handler.ts` lines ~3449-3450.

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

Reviews (1): Last reviewed commit: "feat: add 5h fixed interval reset mode f..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

Support both "rolling" (sliding window) and "fixed" (interval from anchor)
modes for 5-hour quota limits on users, keys, and providers. Uses atomic
Lua script for Redis INCRBYFLOAT+EXPIRE to prevent TTL-less keys on crash.

- Add fiveHourResetMode/fiveHourResetAnchor columns to users/keys/providers
- Implement fixed block calculation from anchor timestamp in time-utils
- Add lease window identity tracking for mode-switch cache invalidation
- Add TRACK_COST_5H_FIXED_WINDOW Lua script for atomic cost tracking
- Extract strict datetime validation to date-input.ts
- Add UI controls for 5h reset mode/anchor across all entity forms
- Add i18n support for all 5 languages
- Add comprehensive tests for anchor parsing, timezone handling, and validation
@coderabbitai
Copy link

coderabbitai bot commented Mar 23, 2026

📝 Walkthrough

Walkthrough

该PR为userskeysproviders表添加了5小时速率限制重置配置,支持"固定"和"滚动"两种模式及可选的锚点时间戳,涵盖数据库模式、业务逻辑、速率限制服务、前端表单和多语言国际化。

Changes

Cohort / File(s) Summary
数据库迁移与模式
drizzle/0086_furry_donald_blake.sql, drizzle/meta/_journal.json, src/drizzle/schema.ts
userskeysproviders表各添加fiveHourResetModefiveHourResetAnchor两列;后者为时区感知时间戳(可空,默认值为null)。
国际化文本(英文、日文、俄文、中文、繁体中文)
messages/en/dashboard.json, messages/en/errors.json, messages/en/quota.json, messages/en/settings/providers/form/sections.json, messages/ja/*, messages/ru/*, messages/zh-CN/*, messages/zh-TW/*
为5小时重置模式和锚点时间新增UI标签、选项描述和错误消息(共8个语言文件,每个文件新增10-30行)。
用户、密钥、提供商类型定义
src/types/user.ts, src/types/key.ts, src/types/provider.ts
扩展UserKeyProvider及其创建/更新数据接口,添加fiveHourResetModefiveHourResetAnchor可选字段;同时调整某些配额字段的可空性。
用户操作处理
src/actions/users.ts
新增validateFiveHourResetAnchor验证函数;在addUsercreateUserOnlyeditUser中添加时区解析、日期验证和条件化持久化逻辑;扩展返回的用户对象包含新字段。
密钥操作处理
src/actions/keys.ts
新增validateFiveHourResetAnchor验证;在addKeyeditKey中添加日期解析、验证和条件化持久化;扩展BatchUpdateKeysParams以支持批量更新。
提供商操作处理与补丁合约
src/actions/providers.ts, src/lib/provider-patch-contract.ts
扩展addProvidereditProvider以支持新字段的解析和验证;更新批量补丁验证和应用逻辑以支持five_hour_reset_modefive_hour_reset_anchorset/clear操作。
数据库存储库层
src/repository/user.ts, src/repository/key.ts, src/repository/provider.ts, src/repository/_shared/transformers.ts
扩展所有查询和插入操作以选择和持久化新字段;在toUsertoKeytoProvider转换器中添加字段映射。
验证模式
src/lib/validation/schemas.ts
CreateUserSchemaUpdateUserSchemaKeyFormSchemaCreateProviderSchemaUpdateProviderSchema添加Zod模式定义,验证fiveHourResetModefiveHourResetAnchor
日期和时间工具
src/lib/utils/date-input.ts, src/lib/utils/date.ts, src/lib/utils/error-messages.ts
新增isValidDateTimeLocalStringisValidDateTimeStringisFutureDate验证函数;添加formatDateToDatetimeLocal格式化函数;新增FIVE_HOUR_RESET_ANCHOR_MUST_NOT_BE_FUTURE错误代码。
速率限制时间工具
src/lib/rate-limit/time-utils.ts
为5小时固定窗口模式新增resolveEffective5hFixedAnchorget5hFixedBlockRangeget5hFixedBlockKeySuffix函数;扩展getTimeRangeForPeriodWithModegetTTLForPeriodWithModegetResetInfoWithMode以支持可选的anchor参数。
速率限制租约与服务
src/lib/rate-limit/lease.ts, src/lib/rate-limit/lease-service.ts, src/lib/rate-limit/service.ts
为租约添加windowIdentity字段;新增buildLeaseWindowIdentity函数;扩展多个函数签名以支持anchor参数;在RateLimitService中实现5小时固定窗口的STRING键查询和动态TTL管理;新增Lua脚本TRACK_COST_5H_FIXED_WINDOW
Redis脚本
src/lib/redis/lua-scripts.ts
新增TRACK_COST_5H_FIXED_WINDOW Lua脚本以支持固定窗口成本跟踪。
认证和代理
src/lib/auth.ts, src/lib/security/api-key-auth-cache.ts, src/app/v1/_lib/proxy/provider-selector.ts, src/app/v1/_lib/proxy/rate-limit-guard.ts, src/app/v1/_lib/proxy/response-handler.ts
在虚拟管理令牌中设置新字段默认值;扩展缓存水合逻辑;更新代理端点以在成本检查和跟踪时传递新参数;为固定模式与滚动模式实现不同的错误代码和重置时间计算。
仪表板用户/密钥管理UI
src/app/[locale]/dashboard/_components/user/create-user-dialog.tsx, src/app/[locale]/dashboard/_components/user/edit-user-dialog.tsx, src/app/[locale]/dashboard/_components/user/user-key-manager.tsx, src/app/[locale]/dashboard/_components/user/forms/user-form.tsx, src/app/[locale]/dashboard/_components/user/forms/user-edit-section.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/key-edit-section.tsx
扩展多个表单和对话框以添加5小时重置模式选择和锚点时间datetime-local输入,支持条件化渲染(仅在选择固定模式时);添加日期格式化和验证逻辑。
提供商批量编辑UI
src/app/[locale]/settings/providers/_components/batch-edit/analyze-batch-settings.ts, src/app/[locale]/settings/providers/_components/batch-edit/build-patch-draft.ts, src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts, src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx, src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx, src/app/[locale]/settings/providers/_components/forms/provider-form/sections/limits-section.tsx
扩展提供商表单状态、reducer操作和UI控件以支持5小时重置配置;添加批量补丁草稿构建和初始状态管理。
单元和集成测试
tests/unit/actions/*, tests/unit/dashboard/*, tests/unit/lib/rate-limit/*, tests/unit/settings/providers/*, tests/unit/validation/*, tests/integration/usage-ledger.test.ts
为新的日期验证、时区处理、速率限制窗口计算、UI表单行为和补丁合约逻辑添加全面测试覆盖(测试数量和行数显著增加)。

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related issues

  • 该PR实现了对5小时固定窗口vs滚动窗口特性的完整支持,包括数据库模式、速率限制服务、代理守卫、业务操作、存储库和前端UI的所有必要层级。

Possibly related PRs

  • 该PR与多个相关PR共享核心改动范围,特别是在速率限制/租约服务层(如lease-service.tsservice.tstime-utils.ts)、数据库模式、代理守卫逻辑和仪表板表单组件方面有重叠。建议在审查期间参考这些相关PR的实现,以确保一致性和集成兼容性。

Suggested reviewers

  • ding113
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.27% 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 Pull request标题准确简洁地概括了主要变更:为5小时限额添加固定间隔重置模式,这是本PR的核心特性。
Description check ✅ Passed 拉取请求的描述详细解释了5小时配额重置模式功能的实现,包括问题、解决方案、变更内容、向后兼容性和测试覆盖。描述与代码更改密切相关。

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

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

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

❤️ Share

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

@gemini-code-assist
Copy link
Contributor

Warning

Gemini encountered an error creating the summary. You can try again by commenting /gemini summary.

@github-actions github-actions bot added the size/XL Extra Large PR (> 1000 lines) label Mar 23, 2026
Comment on lines 36 to +52
type TranslationFunction = (key: string, values?: Record<string, string>) => string;

function validateFiveHourResetAnchor(
anchor: Date,
tError: TranslationFunction
): { error: string; errorCode: string } | null {
if (Number.isNaN(anchor.getTime())) {
return {
error: tError("INVALID_FORMAT", { field: tError("FIVE_HOUR_RESET_ANCHOR_FIELD") }),
errorCode: ERROR_CODES.INVALID_FORMAT,
};
}

if (isFutureDate(anchor)) {
return {
error: tError("FIVE_HOUR_RESET_ANCHOR_MUST_NOT_BE_FUTURE"),
errorCode: ERROR_CODES.FIVE_HOUR_RESET_ANCHOR_MUST_NOT_BE_FUTURE,
Copy link

Choose a reason for hiding this comment

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

P2 validateFiveHourResetAnchor duplicated across three action files

An identical function (or near-identical: keys.ts uses a local TranslationFunction alias while providers.ts and users.ts use Awaited<ReturnType<typeof getTranslations<"errors">>>) is defined in each of keys.ts, providers.ts, and users.ts. Any future change to the validation rules (e.g., adding a minimum-age check) must be made in three places and is prone to drift.

The same pattern was already solved for parseDateInputAsTimezone and isFutureDate being shared from src/lib/utils/date-input.ts. Consider extracting validateFiveHourResetAnchor into a shared utility module (e.g. src/lib/rate-limit/anchor-validation.ts or alongside date-input.ts) that all three action files import.

The same duplication exists at src/actions/providers.ts:122-143 and src/actions/users.ts:297-318.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/actions/keys.ts
Line: 36-52

Comment:
**`validateFiveHourResetAnchor` duplicated across three action files**

An identical function (or near-identical: `keys.ts` uses a local `TranslationFunction` alias while `providers.ts` and `users.ts` use `Awaited<ReturnType<typeof getTranslations<"errors">>>`) is defined in each of `keys.ts`, `providers.ts`, and `users.ts`. Any future change to the validation rules (e.g., adding a minimum-age check) must be made in three places and is prone to drift.

The same pattern was already solved for `parseDateInputAsTimezone` and `isFutureDate` being shared from `src/lib/utils/date-input.ts`. Consider extracting `validateFiveHourResetAnchor` into a shared utility module (e.g. `src/lib/rate-limit/anchor-validation.ts` or alongside `date-input.ts`) that all three action files import.

The same duplication exists at `src/actions/providers.ts:122-143` and `src/actions/users.ts:297-318`.

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

Comment on lines +649 to +650
five_hour_reset_mode: provider.fiveHourResetMode,
five_hour_reset_anchor: provider.fiveHourResetAnchor ?? provider.createdAt ?? undefined,
Copy link

Choose a reason for hiding this comment

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

P2 Raw anchor passed without resolveEffective5hFixedAnchor validation

In rate-limit-guard.ts the anchor is pre-resolved before passing it to the service:

const key5hAnchor = resolveEffective5hFixedAnchor(key.fiveHourResetAnchor, key.createdAt);

But here and at line 1050, the anchor is passed raw:

five_hour_reset_anchor: provider.fiveHourResetAnchor ?? provider.createdAt ?? undefined,

The service layer does call resolveEffective5hFixedAnchor on whatever is passed in, so the behaviour is ultimately the same. But the asymmetry is confusing for readers and could cause unexpected behaviour if the service's internal handling ever changes. For consistency, consider applying resolveEffective5hFixedAnchor at the call site here too, matching the pattern used in rate-limit-guard.ts.

The same applies at line 1050 and in response-handler.ts lines ~3449-3450.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/provider-selector.ts
Line: 649-650

Comment:
**Raw anchor passed without `resolveEffective5hFixedAnchor` validation**

In `rate-limit-guard.ts` the anchor is pre-resolved before passing it to the service:

```ts
const key5hAnchor = resolveEffective5hFixedAnchor(key.fiveHourResetAnchor, key.createdAt);
```

But here and at line 1050, the anchor is passed raw:

```ts
five_hour_reset_anchor: provider.fiveHourResetAnchor ?? provider.createdAt ?? undefined,
```

The service layer does call `resolveEffective5hFixedAnchor` on whatever is passed in, so the behaviour is ultimately the same. But the asymmetry is confusing for readers and could cause unexpected behaviour if the service's internal handling ever changes. For consistency, consider applying `resolveEffective5hFixedAnchor` at the call site here too, matching the pattern used in `rate-limit-guard.ts`.

The same applies at line 1050 and in `response-handler.ts` lines ~3449-3450.

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

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Code Review Summary

This PR adds configurable 5-hour fixed interval reset mode for rate limiting, complementing the existing rolling window behavior. The implementation is well-structured with proper database schema changes, Redis atomic operations via Lua scripts, and comprehensive UI/i18n support.

PR Size: XL

  • Lines changed: 7,621 (7,320 additions + 301 deletions)
  • Files changed: 78

Note: This is a large PR. Consider splitting future features of similar scope into smaller, focused PRs for easier review:

  • Schema + core rate-limit logic (lease, service, time-utils)
  • Actions (keys, providers, users)
  • UI components + i18n

Issues Found

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

Key Implementation Highlights

Positive patterns observed:

  • Atomic Redis operations: TRACK_COST_5H_FIXED_WINDOW Lua script correctly uses INCRBYFLOAT + EXPIRE atomically to prevent TTL-less keys on crash
  • Cache invalidation: buildLeaseWindowIdentity correctly invalidates cache when switching between fixed/rolling modes
  • Timezone awareness: Anchor timestamps are properly parsed using system timezone via parseDateInputAsTimezone
  • Fail-open design: Rate limiting gracefully falls back to database queries when Redis is unavailable
  • i18n coverage: All 5 languages (zh-CN, zh-TW, en, ja, ru) properly supported

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean
  • Error handling - Clean (proper logging, fail-open patterns)
  • Type safety - Clean
  • Documentation accuracy - Clean
  • Test coverage - Adequate (time-utils, lease, actions, UI forms)
  • Code clarity - Good

Automated review by Claude AI

Copy link

@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: be389fab0b

ℹ️ 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 +9 to +13
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
const hours = String(d.getHours()).padStart(2, "0");
const minutes = String(d.getMinutes()).padStart(2, "0");

Choose a reason for hiding this comment

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

P1 Badge Format datetime-local values in configured system timezone

formatDateToDatetimeLocal renders using the browser's local clock (getFullYear/getHours), but the submitted value is later parsed on the server with parseDateInputAsTimezone(..., resolveSystemTimezone()). When an admin's browser timezone differs from the configured system timezone, existing 5h anchors are shown shifted and saving the displayed value stores a different instant, which moves fixed-window reset boundaries unexpectedly.

Useful? React with 👍 / 👎.

Comment on lines +884 to +885
const ttl5hKey = await getTTLForPeriodWithMode("5h", "00:00", "fixed", key5hAnchor);
const key5hFixed = RateLimitService.resolve5hFixedCostKey("key", keyId, key5hAnchor, nowDate);

Choose a reason for hiding this comment

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

P2 Badge Derive fixed-window TTL from the same timestamp as key suffix

In fixed 5h mode, the Redis key suffix is computed from nowDate (which comes from createdAtMs), but TTL is computed via getTTLForPeriodWithMode, which uses the current wall clock internally. If a request starts before a block boundary and finishes after it, cost is written to the previous block key with a fresh TTL for the new block, so expired block keys can linger for hours and accumulate under long-running traffic.

Useful? React with 👍 / 👎.

Copy link

@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: 10

Caution

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

⚠️ Outside diff range comments (1)
src/lib/security/api-key-auth-cache.ts (1)

145-157: ⚠️ Potential issue | 🟠 Major

旧缓存命中时会把新加的 fiveHourResetMode 漏成 undefined

这里补了 fiveHourResetAnchor,但 CACHE_VERSION 仍然是 1,hydrate 逻辑也没有校验/兜底 fiveHourResetMode。上线后命中旧版缓存 payload 时,Key/User 会以缺少 mode 的形态返回,直到 TTL 过期;新限流逻辑如果直接读取这个字段,就会拿到不完整配置。建议至少在 hydrate 阶段把缺失值回填为 "rolling",或者直接 bump cache version。

建议修复
 function hydrateKeyFromCache(keyString: string, payload: CachedKeyPayloadV1): Key | null {
   const key = payload.key as unknown as Record<string, unknown>;
   if (!key || typeof key !== "object") return null;
@@
+  const fiveHourResetMode =
+    key.fiveHourResetMode === "fixed" || key.fiveHourResetMode === "rolling"
+      ? key.fiveHourResetMode
+      : "rolling";
+
   return {
     ...(payload.key as Omit<Key, "key">),
     key: keyString,
@@
+    fiveHourResetMode,
     fiveHourResetAnchor: fiveHourResetAnchor === undefined ? undefined : fiveHourResetAnchor,
   } as Key;
 }
@@
+  const fiveHourResetMode =
+    user.fiveHourResetMode === "fixed" || user.fiveHourResetMode === "rolling"
+      ? user.fiveHourResetMode
+      : "rolling";
+
   return {
     ...(payload.user as User),
@@
+    fiveHourResetMode,
     fiveHourResetAnchor: fiveHourResetAnchor === undefined ? undefined : fiveHourResetAnchor,
   } as User;
 }

Based on learnings: explicitly document and enforce distinct meanings for null and undefined.

Also applies to: 177-189

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

In `@src/lib/security/api-key-auth-cache.ts` around lines 145 - 157, The hydrate
logic in src/lib/security/api-key-auth-cache.ts is not backfilling the new
fiveHourResetMode field (so old cache hits yield undefined mode); update the
hydrate function that builds the cached Key payload (the same place where
fiveHourResetAnchor is parsed) to default missing fiveHourResetMode to "rolling"
(or alternatively increment CACHE_VERSION) so old cached payloads are
normalized; ensure the returned object includes fiveHourResetMode:
payload.fiveHourResetMode ?? "rolling" and apply the same fix where similar
hydration occurs later in the file.
🧹 Nitpick comments (4)
tests/integration/usage-ledger.test.ts (1)

281-323: 改进建议:保持超时配置格式的一致性。

该测试的重构本身是正确的,添加 60 秒超时对于涉及数据库操作的集成测试是合理的。但是,该测试使用了多行格式:

test(
  "test name",
  { timeout: 60_000 },
  async () => { ... }
)

而第 325 行的另一个超时测试使用了内联格式:

test("test name", { timeout: 60_000 }, async () => { ... })

建议统一这两个测试的格式风格,以提高代码的可维护性和可读性。

♻️ 建议的一致性改进

将第 325 行的测试也改为多行格式:

-    test("backfill is idempotent when running twice", { timeout: 60_000 }, async () => {
+    test(
+      "backfill is idempotent when running twice",
+      { timeout: 60_000 },
+      async () => {
       const requestId = await insertMessageRequestRow({

或者将第 281-323 行的测试改为内联格式(如果字符数不超过 100):

-    test(
-      "backfill copies non-warmup message_request rows when ledger rows are missing",
-      {
-        timeout: 60_000,
-      },
-      async () => {
+    test("backfill copies non-warmup message_request rows when ledger rows are missing", { timeout: 60_000 }, async () => {
         const userId = nextUserId();

不过,考虑到第一个测试名称较长,多行格式可能更具可读性,因此建议采用第一种方案。

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

In `@tests/integration/usage-ledger.test.ts` around lines 281 - 323, The test
"backfill copies non-warmup message_request rows when ledger rows are missing"
uses the multi-line test(...) timeout style while another nearby test uses the
inline style; make the styles consistent by converting the other test that
currently calls test("...", { timeout: 60_000 }, async () => { ... }) to the
multi-line form used here (test(\n  "description",\n  { timeout: 60_000 },\n 
async () => { ... }\n)); update the other test invocation accordingly so both
use the same multi-line formatting.
src/app/[locale]/dashboard/_components/user/user-key-manager.tsx (1)

43-66: 建议抽取 keyOwnerUser 构造逻辑,避免双分支字段漂移

Line 59-60 和 Line 115-116 需要在两处同步维护相同字段。本次已出现重复扩展,后续继续加字段时容易漏改。

可选重构示例
+  const buildKeyOwnerUser = (user: UserDisplay | undefined) =>
+    user
+      ? {
+          id: user.id,
+          name: user.name,
+          description: user.note || "",
+          role: user.role,
+          rpm: user.rpm,
+          dailyQuota: user.dailyQuota,
+          providerGroup: user.providerGroup || "default",
+          createdAt: new Date(),
+          updatedAt: new Date(),
+          limit5hUsd: user.limit5hUsd ?? undefined,
+          limitWeeklyUsd: user.limitWeeklyUsd ?? undefined,
+          limitMonthlyUsd: user.limitMonthlyUsd ?? undefined,
+          limitConcurrentSessions: user.limitConcurrentSessions ?? undefined,
+          fiveHourResetMode: user.fiveHourResetMode ?? "rolling",
+          fiveHourResetAnchor: user.fiveHourResetAnchor ?? null,
+          dailyResetMode: user.dailyResetMode ?? "fixed",
+          dailyResetTime: user.dailyResetTime ?? "00:00",
+          isEnabled: user.isEnabled,
+          expiresAt: user.expiresAt ?? undefined,
+        }
+      : undefined;
...
-            keyOwnerUser={
-              activeUser
-                ? { ... }
-                : undefined
-            }
+            keyOwnerUser={buildKeyOwnerUser(activeUser)}
...
-            keyOwnerUser={
-              activeUser
-                ? { ... }
-                : undefined
-            }
+            keyOwnerUser={buildKeyOwnerUser(activeUser)}

Also applies to: 99-122

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

In `@src/app/`[locale]/dashboard/_components/user/user-key-manager.tsx around
lines 43 - 66, The keyOwnerUser object construction is duplicated and will drift
when fields change; extract the mapping into a single helper (e.g.,
buildKeyOwnerUser or mapActiveUserToKeyOwner) that accepts activeUser and
returns the normalized object (handling defaults like providerGroup,
createdAt/updatedAt, limit* fields, reset modes/times, isEnabled, expiresAt,
etc.); replace both inline constructions of keyOwnerUser with a call to that
helper and ensure the helper is exported/defined near the UserKeyManager
component in user-key-manager.tsx so both usages reference the same source of
truth.
src/types/key.ts (1)

54-55: 建议把 five_hour_reset_anchornull / undefined 语义直接写进类型注释。

这两个位置在行为上已经区分了“省略字段”和“显式清空”,但类型本身没有把这个约定写出来,后续在 action / repository 层很容易把两者混用。建议明确 undefined = 不传/不更新null = 清空并回退默认锚点

Based on learnings: In TypeScript interfaces, explicitly document and enforce distinct meanings for null and undefined. Example: for numeric limits like limitTotalUsd, use 'number | null | undefined' when null signifies explicitly unlimited (e.g., matches DB schema or special UI logic) and undefined signifies 'inherit default'. This pattern should be consistently reflected in type definitions across related fields to preserve semantic clarity between database constraints and UI behavior.

Also applies to: 82-83

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

In `@src/types/key.ts` around lines 54 - 55, Update the type signature and JSDoc
for five_hour_reset_anchor (and similarly for the other fields noted around the
82-83 region) to explicitly include both undefined and null in the type and
document their semantics: make the type Date | null | undefined and add a short
comment stating "undefined = field omitted / no update; null = explicitly clear
and revert to default anchor", so callers and repository/action code can
distinguish omission from explicit clearing; update any adjacent types (e.g.,
numeric limit fields) to follow the same pattern (T | null | undefined) with
matching comments.
src/actions/users.ts (1)

1259-1271: 5h 锚点解析/校验流程已经复制了三遍。

resolveSystemTimezone() -> parseDateInputAsTimezone() -> validateFiveHourResetAnchor() 现在分别散落在 addUsercreateUserOnlyeditUser。后续只要锚点规则再改一次,就很容易漏改其中一个分支。建议抽成一个小 helper,统一返回 Date | null 或结构化错误。

Also applies to: 1463-1475, 1637-1651

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

In `@src/actions/users.ts` around lines 1259 - 1271, The repeated flow
resolveSystemTimezone -> parseDateInputAsTimezone -> validateFiveHourResetAnchor
is duplicated across addUser, createUserOnly and editUser; extract it into a
single helper (e.g., parseAndValidateFiveHourResetAnchor) that accepts the raw
input and tError and returns either a Date | null or a structured error result,
then replace the inline blocks in addUser, createUserOnly and editUser to call
this helper and return on error or use the returned Date; ensure the helper
calls resolveSystemTimezone, parseDateInputAsTimezone and
validateFiveHourResetAnchor internally and preserves existing error shape.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@messages/ru/quota.json`:
- Around line 239-242: The description for the message key fiveHourResetAnchor
is missing that the anchor value is interpreted in the system timezone and
converted to UTC; update the "description" text for fiveHourResetAnchor to
explicitly state that the anchor is parsed using the system timezone (and then
converted to UTC by the action layer), and apply the same wording change to the
other anchor message entries mentioned in the comment (the similar reset-anchor
keys in the file) so cross-timezone admins understand the timezone source.

In `@src/actions/keys.ts`:
- Around line 1155-1158: 批量更新直接把 updates.fiveHourResetAnchor 写入 dbUpdates 绕过了
addKey/editKey 对 fiveHourResetAnchor 的时区解析与未来时间校验;请在更新前复用或提取出 addKey/editKey
使用的解析/校验逻辑(例如 parse/validateFiveHourResetAnchor)对 updates.fiveHourResetAnchor
进行按系统时区的解析并拒绝未来时间,然后仅在验证通过后再把规范化后的值赋给 dbUpdates.fiveHourResetAnchor;保留对
fiveHourResetMode 的现有赋值逻辑不变,确保批量编辑与其它入口的时区语义一致并阻止写入未来 anchor。

In `@src/actions/providers.ts`:
- Around line 629-630: Provider 的 5h 用量计算仍然硬编码使用 getTimeRangeForPeriod("5h") /
getResetInfo("5h") 并在批量接口中复用同一范围,导致配置为 fixed 的 provider 仍按 rolling 计算。请修改调用
getProviderLimitUsage() 和 getProviderLimitUsageBatch() 的逻辑,改为基于每个 provider 的
fiveHourResetMode / fiveHourResetAnchor(在本文件中标识为 fiveHourResetMode /
fiveHourResetAnchor 或 validated.five_hour_reset_mode /
validated.five_hour_reset_anchor)来分别计算时间范围和重置信息(替换对 getTimeRangeForPeriod("5h")
/ getResetInfo("5h") 的直接调用);对 getProviderLimitUsageBatch() 不要为所有 provider 使用单一
5h 范围,改为为每个 provider 生成各自的范围并传入对应参数以确保 fixed window 语义正确生效。

In `@src/app/`[locale]/dashboard/_components/user/edit-user-dialog.tsx:
- Around line 66-67: Move the import of formatDateToDatetimeLocal to the top of
the module with the other imports (placing it after the existing import list),
so it appears before the EditUserSchema and EditUserValues declarations; ensure
the symbol formatDateToDatetimeLocal is imported prior to any use or definition
of EditUserSchema/EditUserValues to avoid transpile/runtime errors.

In `@src/app/`[locale]/dashboard/_components/user/forms/add-key-form.tsx:
- Around line 89-90: When submitting the form ensure hidden anchors are dropped:
normalize the payload so that when data.fiveHourResetMode === 'rolling' you set
fiveHourResetAnchor to null before sending (i.e., replace uses of
data.fiveHourResetAnchor in the submit/payload assembly with a normalized
value). Locate where the submit payload is built (the code referencing
fiveHourResetMode and fiveHourResetAnchor) and enforce the rule there;
optionally also clear the form field when the UI toggles mode, but the mandatory
fix is payload normalization so backend never receives a stale anchor.

In
`@src/app/`[locale]/settings/providers/_components/batch-edit/build-patch-draft.ts:
- Around line 192-201: When SET_FIVE_HOUR_RESET_MODE changes to "rolling",
ensure the anchor is cleared and marked dirty so build-patch-draft.ts will emit
a clear: either update the SET_FIVE_HOUR_RESET_MODE dispatch handler (or
reducer) to set state.rateLimit.fiveHourResetAnchor = "" and add
"rateLimit.fiveHourResetAnchor" to dirtyFields (so SET_FIVE_HOUR_RESET_ANCHOR is
effectively flagged), or modify build-patch-draft.ts to treat mode changes as
implicit clears by checking state.rateLimit.fiveHourResetMode === "rolling" (in
addition to dirtyFields) and emitting draft.five_hour_reset_anchor = { clear:
true } when appropriate; reference symbols: SET_FIVE_HOUR_RESET_MODE,
SET_FIVE_HOUR_RESET_ANCHOR, dirtyFields, state.rateLimit.fiveHourResetMode,
state.rateLimit.fiveHourResetAnchor, and draft.five_hour_reset_anchor.

In `@src/lib/rate-limit/service.ts`:
- Around line 1509-1523: The code reads a non-existent fixed 5h key when
fiveHourMode === "fixed" but fiveHourConfig.resetAnchor is missing, causing
reads of `provider:${providerId}:cost_5h_fixed` that are never written; update
the logic in the block using fiveHourConfig/fiveHourMode (around fiveHourConfig,
fiveHourMode, RateLimitService.resolve5hFixedCostKey, pipeline.get, queryMeta)
to: if resetAnchor is absent, either fallback to the rolling query branch (treat
as isRolling=true) or derive a valid fixed anchor (e.g. from provider
createdAt/created timestamp used by trackCost) and then call
resolve5hFixedCostKey with that anchor before calling pipeline.get; ensure
queryMeta reflects the chosen period/isRolling accordingly.
- Around line 186-197: The five-hour fixed mode is falling back to rolling
because resolveEffective5hFixedAnchor is called with only five_hour_reset_anchor
and never given the entity createdAt, so when anchor is null the code treats it
as rolling; update the call sites (including where normalizeResetTime,
fiveHourResetMode, fiveHourAnchor and the costLimits array are built) to pass
the entity's createdAt into resolveEffective5hFixedAnchor (e.g., compute a
single effective5hAnchor = resolveEffective5hFixedAnchor(five_hour_reset_anchor,
createdAt)), then reuse that effective5hAnchor throughout all 5h fixed branches
(checks, tracing, current usage reads and lease logic) so fixed mode uses the
createdAt fallback consistently instead of degrading to rolling.

In `@src/lib/validation/schemas.ts`:
- Around line 22-40: The Zod schemas (FIVE_HOUR_RESET_ANCHOR_STRING,
FIVE_HOUR_RESET_ANCHOR_UPDATE_NULLABLE, etc.) currently embed hardcoded Chinese
error messages; replace those literal messages with the project's i18n error key
or error-code mechanism (e.g., use the existing translation key constant or
return an error code string that the form layer resolves via i18n.t instead of a
literal message). Concretely, remove the Chinese string passed to .refine(...)
and return/throw the canonical validation key or use the shared helper (the same
pattern used elsewhere for field errors) so the UI can translate into
zh-CN/zh-TW/en/ja/ru; ensure the preprocess/refine calls still surface that key
(or Error object containing the key) and update any unit tests to expect the
i18n key instead of the hardcoded Chinese text.

---

Outside diff comments:
In `@src/lib/security/api-key-auth-cache.ts`:
- Around line 145-157: The hydrate logic in
src/lib/security/api-key-auth-cache.ts is not backfilling the new
fiveHourResetMode field (so old cache hits yield undefined mode); update the
hydrate function that builds the cached Key payload (the same place where
fiveHourResetAnchor is parsed) to default missing fiveHourResetMode to "rolling"
(or alternatively increment CACHE_VERSION) so old cached payloads are
normalized; ensure the returned object includes fiveHourResetMode:
payload.fiveHourResetMode ?? "rolling" and apply the same fix where similar
hydration occurs later in the file.

---

Nitpick comments:
In `@src/actions/users.ts`:
- Around line 1259-1271: The repeated flow resolveSystemTimezone ->
parseDateInputAsTimezone -> validateFiveHourResetAnchor is duplicated across
addUser, createUserOnly and editUser; extract it into a single helper (e.g.,
parseAndValidateFiveHourResetAnchor) that accepts the raw input and tError and
returns either a Date | null or a structured error result, then replace the
inline blocks in addUser, createUserOnly and editUser to call this helper and
return on error or use the returned Date; ensure the helper calls
resolveSystemTimezone, parseDateInputAsTimezone and validateFiveHourResetAnchor
internally and preserves existing error shape.

In `@src/app/`[locale]/dashboard/_components/user/user-key-manager.tsx:
- Around line 43-66: The keyOwnerUser object construction is duplicated and will
drift when fields change; extract the mapping into a single helper (e.g.,
buildKeyOwnerUser or mapActiveUserToKeyOwner) that accepts activeUser and
returns the normalized object (handling defaults like providerGroup,
createdAt/updatedAt, limit* fields, reset modes/times, isEnabled, expiresAt,
etc.); replace both inline constructions of keyOwnerUser with a call to that
helper and ensure the helper is exported/defined near the UserKeyManager
component in user-key-manager.tsx so both usages reference the same source of
truth.

In `@src/types/key.ts`:
- Around line 54-55: Update the type signature and JSDoc for
five_hour_reset_anchor (and similarly for the other fields noted around the
82-83 region) to explicitly include both undefined and null in the type and
document their semantics: make the type Date | null | undefined and add a short
comment stating "undefined = field omitted / no update; null = explicitly clear
and revert to default anchor", so callers and repository/action code can
distinguish omission from explicit clearing; update any adjacent types (e.g.,
numeric limit fields) to follow the same pattern (T | null | undefined) with
matching comments.

In `@tests/integration/usage-ledger.test.ts`:
- Around line 281-323: The test "backfill copies non-warmup message_request rows
when ledger rows are missing" uses the multi-line test(...) timeout style while
another nearby test uses the inline style; make the styles consistent by
converting the other test that currently calls test("...", { timeout: 60_000 },
async () => { ... }) to the multi-line form used here (test(\n  "description",\n
{ timeout: 60_000 },\n  async () => { ... }\n)); update the other test
invocation accordingly so both use the same multi-line formatting.
🪄 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: c3ddd8e5-d29b-44bc-8d87-2c890a2bf975

📥 Commits

Reviewing files that changed from the base of the PR and between 96b116e and be389fa.

📒 Files selected for processing (78)
  • drizzle/0086_furry_donald_blake.sql
  • drizzle/meta/0086_snapshot.json
  • drizzle/meta/_journal.json
  • messages/en/dashboard.json
  • messages/en/errors.json
  • messages/en/quota.json
  • messages/en/settings/providers/form/sections.json
  • messages/ja/dashboard.json
  • messages/ja/errors.json
  • messages/ja/quota.json
  • messages/ja/settings/providers/form/sections.json
  • messages/ru/dashboard.json
  • messages/ru/errors.json
  • messages/ru/quota.json
  • messages/ru/settings/providers/form/sections.json
  • messages/zh-CN/dashboard.json
  • messages/zh-CN/errors.json
  • messages/zh-CN/quota.json
  • messages/zh-CN/settings/providers/form/sections.json
  • messages/zh-TW/dashboard.json
  • messages/zh-TW/errors.json
  • messages/zh-TW/quota.json
  • messages/zh-TW/settings/providers/form/sections.json
  • src/actions/keys.ts
  • src/actions/providers.ts
  • src/actions/users.ts
  • src/app/[locale]/dashboard/_components/user/create-user-dialog.tsx
  • src/app/[locale]/dashboard/_components/user/edit-user-dialog.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/key-edit-section.tsx
  • src/app/[locale]/dashboard/_components/user/forms/user-edit-section.tsx
  • src/app/[locale]/dashboard/_components/user/forms/user-form.tsx
  • src/app/[locale]/dashboard/_components/user/user-key-manager.tsx
  • src/app/[locale]/settings/providers/_components/batch-edit/analyze-batch-settings.ts
  • src/app/[locale]/settings/providers/_components/batch-edit/build-patch-draft.ts
  • src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx
  • src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx
  • src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts
  • src/app/[locale]/settings/providers/_components/forms/provider-form/sections/limits-section.tsx
  • src/app/v1/_lib/proxy/provider-selector.ts
  • src/app/v1/_lib/proxy/rate-limit-guard.ts
  • src/app/v1/_lib/proxy/response-handler.ts
  • src/drizzle/schema.ts
  • src/lib/auth.ts
  • src/lib/provider-patch-contract.ts
  • src/lib/rate-limit/lease-service.ts
  • src/lib/rate-limit/lease.ts
  • src/lib/rate-limit/service.ts
  • src/lib/rate-limit/time-utils.ts
  • src/lib/redis/lua-scripts.ts
  • src/lib/security/api-key-auth-cache.ts
  • src/lib/utils/date-input.ts
  • src/lib/utils/date.ts
  • src/lib/utils/error-messages.ts
  • src/lib/validation/schemas.ts
  • src/repository/_shared/transformers.ts
  • src/repository/key.ts
  • src/repository/provider.ts
  • src/repository/user.ts
  • src/types/key.ts
  • src/types/provider.ts
  • src/types/user.ts
  • tests/integration/usage-ledger.test.ts
  • tests/unit/actions/keys-edit-key-expires-at-clear.test.ts
  • tests/unit/actions/providers-apply-engine.test.ts
  • tests/unit/actions/providers-patch-contract.test.ts
  • tests/unit/actions/providers.test.ts
  • tests/unit/actions/users-edit-user-expires-at-clear.test.ts
  • tests/unit/dashboard/add-key-form-expiry-clear-ui.test.tsx
  • tests/unit/dashboard/edit-key-form-expiry-clear-ui.test.tsx
  • tests/unit/dashboard/user-form-expiry-clear-ui.test.tsx
  • tests/unit/lib/rate-limit/lease-service.test.ts
  • tests/unit/lib/rate-limit/lease.test.ts
  • tests/unit/lib/rate-limit/time-utils.test.ts
  • tests/unit/settings/providers/build-patch-draft.test.ts
  • tests/unit/settings/providers/provider-form-endpoint-pool.test.tsx
  • tests/unit/validation/user-schemas-expires-at-clear.test.ts

Comment on lines +239 to +242
"fiveHourResetAnchor": {
"label": "Якорное время сброса 5ч",
"description": "Начальное время для режима фиксированных интервалов. По умолчанию - время создания."
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

fiveHourResetAnchor 的说明最好补上“按系统时区解析”。

这里的 anchor 在 action 层是按系统时区转换成 UTC 的,但当前文案只说明了默认值,没有说明时区来源。跨时区管理员很容易按浏览器本地时间理解,最后会保存出偏移几小时的锚点。

Also applies to: 341-344

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

In `@messages/ru/quota.json` around lines 239 - 242, The description for the
message key fiveHourResetAnchor is missing that the anchor value is interpreted
in the system timezone and converted to UTC; update the "description" text for
fiveHourResetAnchor to explicitly state that the anchor is parsed using the
system timezone (and then converted to UTC by the action layer), and apply the
same wording change to the other anchor message entries mentioned in the comment
(the similar reset-anchor keys in the file) so cross-timezone admins understand
the timezone source.

Comment on lines +340 to +341
five_hour_reset_mode: validatedData.fiveHourResetMode,
five_hour_reset_anchor: fiveHourResetAnchor,
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Key 的 5h 用量展示还停留在 rolling 逻辑。

这里已经把 fiveHourResetMode / fiveHourResetAnchor 落库了,但同文件后面的 getKeyLimitUsage() 仍调用 getTimeRangeForPeriod("5h") / getResetInfo("5h"),不会读取这两个新字段。fixed 模式下,仪表盘上的 5h 已用额度和重置时间会和真实限流窗口不一致。

Also applies to: 599-605

Comment on lines +1155 to +1158
if (updates.fiveHourResetMode !== undefined)
dbUpdates.fiveHourResetMode = updates.fiveHourResetMode;
if (updates.fiveHourResetAnchor !== undefined)
dbUpdates.fiveHourResetAnchor = updates.fiveHourResetAnchor;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

批量更新路径绕过了 anchor 的解析和校验。

addKey/editKey 都会按系统时区解析 fiveHourResetAnchor 并拒绝 future anchor,这里却直接把 updates.fiveHourResetAnchor 写进数据库。这样 batch edit 可以写入未来时间,而且和其它入口的时区语义也不一致。

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

In `@src/actions/keys.ts` around lines 1155 - 1158, 批量更新直接把
updates.fiveHourResetAnchor 写入 dbUpdates 绕过了 addKey/editKey 对
fiveHourResetAnchor 的时区解析与未来时间校验;请在更新前复用或提取出 addKey/editKey 使用的解析/校验逻辑(例如
parse/validateFiveHourResetAnchor)对 updates.fiveHourResetAnchor
进行按系统时区的解析并拒绝未来时间,然后仅在验证通过后再把规范化后的值赋给 dbUpdates.fiveHourResetAnchor;保留对
fiveHourResetMode 的现有赋值逻辑不变,确保批量编辑与其它入口的时区语义一致并阻止写入未来 anchor。

Comment on lines +629 to +630
five_hour_reset_mode: validated.five_hour_reset_mode ?? "rolling",
five_hour_reset_anchor: fiveHourResetAnchor,
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Provider 的 5h 用量查询还没有接入 fixed window 语义。

这几处已经把 fiveHourResetMode / fiveHourResetAnchor 写进去了,但同文件后面的 getProviderLimitUsage()getProviderLimitUsageBatch() 仍固定调用 getTimeRangeForPeriod("5h") / getResetInfo("5h"),批量版本还把 5h 范围作为所有 provider 的共享值。配置成 fixed 的 provider 在管理台看到的 5h 已用额度和重置信息会继续按 rolling 计算,和真实限流窗口不一致。

Also applies to: 826-828, 1579-1595

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

In `@src/actions/providers.ts` around lines 629 - 630, Provider 的 5h 用量计算仍然硬编码使用
getTimeRangeForPeriod("5h") / getResetInfo("5h") 并在批量接口中复用同一范围,导致配置为 fixed 的
provider 仍按 rolling 计算。请修改调用 getProviderLimitUsage() 和
getProviderLimitUsageBatch() 的逻辑,改为基于每个 provider 的 fiveHourResetMode /
fiveHourResetAnchor(在本文件中标识为 fiveHourResetMode / fiveHourResetAnchor 或
validated.five_hour_reset_mode /
validated.five_hour_reset_anchor)来分别计算时间范围和重置信息(替换对 getTimeRangeForPeriod("5h")
/ getResetInfo("5h") 的直接调用);对 getProviderLimitUsageBatch() 不要为所有 provider 使用单一
5h 范围,改为为每个 provider 生成各自的范围并传入对应参数以确保 fixed window 语义正确生效。

Comment on lines +66 to +67
import { formatDateToDatetimeLocal } from "@/lib/utils/date";

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 验证当前文件中 import 语句的位置
rg -n "^import " src/app/[locale]/dashboard/_components/user/edit-user-dialog.tsx | head -20

Repository: ding113/claude-code-hub

Length of output: 1058


🏁 Script executed:

#!/bin/bash
# Check the actual content at lines 66-67 and surrounding context
sed -n '60,75p' src/app/\[locale\]/dashboard/_components/user/edit-user-dialog.tsx | cat -n

Repository: ding113/claude-code-hub

Length of output: 714


🏁 Script executed:

#!/bin/bash
# Search for formatDateToDatetimeLocal usage in the file
rg "formatDateToDatetimeLocal" src/app/\[locale\]/dashboard/_components/user/edit-user-dialog.tsx -n

Repository: ding113/claude-code-hub

Length of output: 256


导入语句位置错误,应移至文件顶部。

formatDateToDatetimeLocal 的导入语句位于第 66 行,位置在 EditUserSchema 定义和 EditUserValues 类型定义之后。import 语句必须位于所有代码之前,位于文件的最顶部。此缺陷会导致运行时或转译错误。

应将该导入语句移至文件顶部,与其他导入语句一起(在现有导入列表的末尾,约第 45 行之后)。

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

In `@src/app/`[locale]/dashboard/_components/user/edit-user-dialog.tsx around
lines 66 - 67, Move the import of formatDateToDatetimeLocal to the top of the
module with the other imports (placing it after the existing import list), so it
appears before the EditUserSchema and EditUserValues declarations; ensure the
symbol formatDateToDatetimeLocal is imported prior to any use or definition of
EditUserSchema/EditUserValues to avoid transpile/runtime errors.

Comment on lines +89 to +90
fiveHourResetMode: data.fiveHourResetMode,
fiveHourResetAnchor: data.fiveHourResetAnchor || null,
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

切回 rolling 时需要主动丢弃隐藏的 anchor。

fiveHourResetAnchor 在切到 rolling 后只是被隐藏,没有被清空;当前提交逻辑仍会直接发送 data.fiveHourResetAnchor。用户如果先选了 fixed 并填写 anchor,再切回 rolling 提交,后端仍会收到旧值并把它存下来。至少需要在提交层按 mode 归一化成 null;是否在 UI 切换时同步清空,可以再按交互取舍。

建议修复
-          fiveHourResetAnchor: data.fiveHourResetAnchor || null,
+          fiveHourResetAnchor:
+            data.fiveHourResetMode === "fixed" ? data.fiveHourResetAnchor || null : null,
-            onValueChange={(value: "fixed" | "rolling") =>
-              form.setValue("fiveHourResetMode", value)
-            }
+            onValueChange={(value: "fixed" | "rolling") => {
+              form.setValue("fiveHourResetMode", value);
+              if (value === "rolling") {
+                form.setValue("fiveHourResetAnchor", "");
+              }
+            }}

Also applies to: 279-317

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

In `@src/app/`[locale]/dashboard/_components/user/forms/add-key-form.tsx around
lines 89 - 90, When submitting the form ensure hidden anchors are dropped:
normalize the payload so that when data.fiveHourResetMode === 'rolling' you set
fiveHourResetAnchor to null before sending (i.e., replace uses of
data.fiveHourResetAnchor in the submit/payload assembly with a normalized
value). Locate where the submit payload is built (the code referencing
fiveHourResetMode and fiveHourResetAnchor) and enforce the rule there;
optionally also clear the form field when the UI toggles mode, but the mandatory
fix is payload normalization so backend never receives a stale anchor.

Comment on lines +192 to +201
if (dirtyFields.has("rateLimit.fiveHourResetMode")) {
draft.five_hour_reset_mode = { set: state.rateLimit.fiveHourResetMode };
}
if (dirtyFields.has("rateLimit.fiveHourResetAnchor")) {
if (state.rateLimit.fiveHourResetAnchor === "") {
draft.five_hour_reset_anchor = { clear: true };
} else {
draft.five_hour_reset_anchor = { set: state.rateLimit.fiveHourResetAnchor };
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 检查 mode 切换是否会同时清空/标脏 fiveHourResetAnchor
fd 'provider-form-context\.tsx$|provider-form-types\.ts$' src -t f | while read -r file; do
  echo "=== $file ==="
  rg -n -C3 "SET_FIVE_HOUR_RESET_MODE|SET_FIVE_HOUR_RESET_ANCHOR|ACTION_TO_FIELD_PATH|fiveHourResetAnchor" "$file"
done

Repository: ding113/claude-code-hub

Length of output: 3759


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 查找表单组件中 fiveHourResetMode 变更时是否有 effect 或处理逻辑
fd 'provider-form\.' src -t f | while read -r file; do
  echo "=== $file ==="
  rg -n -B2 -A5 "SET_FIVE_HOUR_RESET_MODE|fiveHourResetMode.*onChange|useEffect.*fiveHourResetMode" "$file" || echo "(no match)"
done

echo ""
echo "=== 检查是否有 useWatch 或 useEffect 在 mode 变更时清空 anchor ==="
rg -n "useWatch.*fiveHourResetMode|useEffect.*\[.*fiveHourResetMode" src -t ts -t tsx

Repository: ding113/claude-code-hub

Length of output: 265


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 查找所有包含 fiveHourResetMode 的源文件
echo "=== 查找相关的表单和批量编辑文件 ==="
fd 'provider-form|batch-edit' src --type f | head -20

echo ""
echo "=== 检查 batch-edit 目录下是否有处理 anchor 清空的逻辑 ==="
rg -n "fiveHourResetMode|fiveHourResetAnchor" src/app/\[locale\]/settings/providers/_components/batch-edit/ -A3 -B3

echo ""
echo "=== 查找 build-patch-draft.ts 周围是否有钩子处理 mode 变更 ==="
cat -n src/app/\[locale\]/settings/providers/_components/batch-edit/build-patch-draft.ts | head -50

echo ""
echo "=== 检查 batch-edit 中是否有处理 mode 清空 anchor 的代码 ==="
rg -n "rolling.*clear\|mode.*anchor\|SET_FIVE_HOUR_RESET" src/app/\[locale\]/settings/providers/_components/batch-edit/

Repository: ding113/claude-code-hub

Length of output: 6791


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 查找使用 build-patch-draft 的文件
echo "=== 查找使用 buildPatchDraftFromFormState 的地方 ==="
rg -l "buildPatchDraftFromFormState" src --type f

echo ""
echo "=== 查找批量编辑对话框或表单 ==="
fd "batch.*edit.*dialog\|batch.*edit.*form" src --type f

echo ""
echo "=== 检查批量编辑组件中是否有 onChange 处理 fiveHourResetMode ==="
rg -n "fiveHourResetMode" src/app/\[locale\]/settings/providers/_components/ -A2 -B2 | head -50

Repository: ding113/claude-code-hub

Length of output: 155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 查找使用 build-patch-draft 的文件
echo "=== 查找使用 buildPatchDraftFromFormState 的地方 ==="
rg "buildPatchDraftFromFormState" src

echo ""
echo "=== 查找批量编辑组件文件 ==="
fd "batch" src/app/\[locale\]/settings/providers/_components

echo ""
echo "=== 检查批量编辑对话框的完整内容(查看 form 处理逻辑)==="
find src/app/\[locale\]/settings/providers/_components/batch-edit -name "*.tsx" -o -name "*.ts" | head -10

Repository: ding113/claude-code-hub

Length of output: 1991


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 检查 provider-batch-dialog.tsx 中的 form 处理逻辑
echo "=== provider-batch-dialog.tsx 内容 ==="
wc -l src/app/\[locale\]/settings/providers/_components/batch-edit/provider-batch-dialog.tsx

echo ""
echo "=== 查看 form state 和 dirty tracking 部分 ==="
sed -n '1,100p' src/app/\[locale\]/settings/providers/_components/batch-edit/provider-batch-dialog.tsx

echo ""
echo "=== 查看 dirtyFields 和 patch 生成的逻辑 ==="
rg -n "dirtyFields\|buildPatchDraftFromFormState" src/app/\[locale\]/settings/providers/_components/batch-edit/provider-batch-dialog.tsx -B3 -A3

Repository: ding113/claude-code-hub

Length of output: 3459


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 查看完整的 BatchEditDialog 组件逻辑(包括 form 处理)
echo "=== 查看 BatchEditDialog 部分 (行 100-250) ==="
sed -n '100,250p' src/app/\[locale\]/settings/providers/_components/batch-edit/provider-batch-dialog.tsx

echo ""
echo "=== 查看 buildPatchDraftFromFormState 调用处 (行 250-350) ==="
sed -n '250,350p' src/app/\[locale\]/settings/providers/_components/batch-edit/provider-batch-dialog.tsx

Repository: ding113/claude-code-hub

Length of output: 8458


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 检查 LimitsSection 中的 five hour reset mode 字段处理
echo "=== 查找 LimitsSection 的位置 ==="
fd "limits-section" src

echo ""
echo "=== 查看 limits-section.tsx 内容 ==="
wc -l src/app/\[locale\]/settings/providers/_components/forms/provider-form/sections/limits-section.tsx

echo ""
echo "=== 检查 limits-section 中是否有 onChange 处理 fiveHourResetMode ==="
rg -n "fiveHourResetMode\|SET_FIVE_HOUR_RESET_MODE" src/app/\[locale\]/settings/providers/_components/forms/provider-form/sections/limits-section.tsx -B2 -A5

Repository: ding113/claude-code-hub

Length of output: 387


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 查看 limits-section 中关于 five hour reset 的部分
echo "=== 搜索 limits-section.tsx 中的 fiveHourResetMode 相关代码 ==="
grep -n "fiveHourResetMode\|five.hour\|FIVE_HOUR" src/app/\[locale\]/settings/providers/_components/forms/provider-form/sections/limits-section.tsx

echo ""
echo "=== 查看 limits-section.tsx 文件主要结构 (前150行) ==="
head -150 src/app/\[locale\]/settings/providers/_components/forms/provider-form/sections/limits-section.tsx

echo ""
echo "=== 查看 dispatch 的 SET_FIVE_HOUR_RESET_MODE 处理代码 ==="
sed -n '200,400p' src/app/\[locale\]/settings/providers/_components/forms/provider-form/sections/limits-section.tsx | grep -n -B3 -A10 "fiveHourResetMode\|SET_FIVE_HOUR_RESET_MODE"

Repository: ding113/claude-code-hub

Length of output: 7243


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 检查当 mode 变更时是否有任何清空 anchor 的逻辑
echo "=== 完整查看 SET_FIVE_HOUR_RESET_MODE dispatch 和之后的处理 ==="
sed -n '230,280p' src/app/\[locale\]/settings/providers/_components/forms/provider-form/sections/limits-section.tsx

echo ""
echo "=== 检查是否有 useEffect 或其他 hook 在 mode 变更时清空 anchor ==="
grep -n "useEffect\|useCallback\|fiveHourResetMode" src/app/\[locale\]/settings/providers/_components/forms/provider-form/sections/limits-section.tsx | head -20

echo ""
echo "=== 再次确认 provider-form-context.tsx 的 reducer 对 SET_FIVE_HOUR_RESET_MODE 的处理 ==="
sed -n '595,610p' src/app/\[locale\]/settings/providers/_components/forms/provider-form/provider-form-context.tsx

Repository: ding113/claude-code-hub

Length of output: 4129


批量编辑中切换 fiveHourResetModerolling 时需自动清空 fiveHourResetAnchor

当前实现中,SET_FIVE_HOUR_RESET_MODESET_FIVE_HOUR_RESET_ANCHOR 在 dirty tracking、reducer、patch 生成中都是独立的。用户在批量编辑时将 5 小时重置模式从 fixed 切到 rolling,只有 mode 字段被标记为 dirty,anchor 不会被标记。这导致 build-patch-draft.ts 的第 195 行条件永远不满足,历史 anchor 值会保留在数据库中。后续若再切回 fixed,会意外复用这个陈旧的窗口时间。

建议在 SET_FIVE_HOUR_RESET_MODE 的 dispatch handler 中添加逻辑:当 mode 变为 rolling 时,自动清空 anchor 并标记其为 dirty(或在 reducer 中直接清空并标记),或在 build-patch-draft.ts 第 192 行判断 mode 为 rolling 时显式生成 { clear: true}

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

In
`@src/app/`[locale]/settings/providers/_components/batch-edit/build-patch-draft.ts
around lines 192 - 201, When SET_FIVE_HOUR_RESET_MODE changes to "rolling",
ensure the anchor is cleared and marked dirty so build-patch-draft.ts will emit
a clear: either update the SET_FIVE_HOUR_RESET_MODE dispatch handler (or
reducer) to set state.rateLimit.fiveHourResetAnchor = "" and add
"rateLimit.fiveHourResetAnchor" to dirtyFields (so SET_FIVE_HOUR_RESET_ANCHOR is
effectively flagged), or modify build-patch-draft.ts to treat mode changes as
implicit clears by checking state.rateLimit.fiveHourResetMode === "rolling" (in
addition to dirtyFields) and emitting draft.five_hour_reset_anchor = { clear:
true } when appropriate; reference symbols: SET_FIVE_HOUR_RESET_MODE,
SET_FIVE_HOUR_RESET_ANCHOR, dirtyFields, state.rateLimit.fiveHourResetMode,
state.rateLimit.fiveHourResetAnchor, and draft.five_hour_reset_anchor.

Comment on lines 186 to +197
const normalizedDailyReset = normalizeResetTime(limits.daily_reset_time);
const dailyResetMode = limits.daily_reset_mode ?? "fixed";
const fiveHourResetMode = limits.five_hour_reset_mode ?? "rolling";
const fiveHourAnchor = resolveEffective5hFixedAnchor(limits.five_hour_reset_anchor);
const costLimits: CostLimit[] = [
{ amount: limits.limit_5h_usd, period: "5h", name: "5小时" },
{
amount: limits.limit_5h_usd,
period: "5h",
name: "5小时",
resetMode: fiveHourResetMode,
anchor: fiveHourAnchor ?? undefined,
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

5h fixed 模式没有吃到 createdAt fallback,会被整体退化成 rolling。

这些入口都只调用了 resolveEffective5hFixedAnchor(...five_hour_reset_anchor),没有把“anchor 为空时回退到实体 createdAt”的语义一起传进来。结果是 five_hour_reset_mode === "fixed" 且 anchor 为 null 时,检查、追踪、当前用量读取和 lease 分支都会改走 rolling 5h 键/窗口,默认 fixed 模式实际上不会按固定 5 小时块重置。建议把实体创建时间一并透传到这里,先解析出统一的 effective anchor,再复用到所有 5h fixed 路径。

Also applies to: 844-860, 1007-1020, 1635-1656

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

In `@src/lib/rate-limit/service.ts` around lines 186 - 197, The five-hour fixed
mode is falling back to rolling because resolveEffective5hFixedAnchor is called
with only five_hour_reset_anchor and never given the entity createdAt, so when
anchor is null the code treats it as rolling; update the call sites (including
where normalizeResetTime, fiveHourResetMode, fiveHourAnchor and the costLimits
array are built) to pass the entity's createdAt into
resolveEffective5hFixedAnchor (e.g., compute a single effective5hAnchor =
resolveEffective5hFixedAnchor(five_hour_reset_anchor, createdAt)), then reuse
that effective5hAnchor throughout all 5h fixed branches (checks, tracing,
current usage reads and lease logic) so fixed mode uses the createdAt fallback
consistently instead of degrading to rolling.

Comment on lines +1509 to +1523
const fiveHourConfig = fiveHourResetConfigs?.get(providerId);
const fiveHourMode = (fiveHourConfig?.resetMode ?? "rolling") as DailyResetMode;

// 5h: 根据模式选择查询方式
if (fiveHourMode === "fixed") {
const fixed5hKey = fiveHourConfig?.resetAnchor
? RateLimitService.resolve5hFixedCostKey(
"provider",
providerId,
fiveHourConfig.resetAnchor,
new Date(now)
)
: `provider:${providerId}:cost_5h_fixed`;
pipeline.get(fixed5hKey);
queryMeta.push({ providerId, period: "5h", isRolling: false });
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

缺少锚点时这里会查询一个从未写入的 fixed 5h key。

trackCost() 和 DB 回填写入的都是 provider:${id}:cost_5h_fixed_<blockStartMs>。当前 fiveHourMode === "fixed"resetAnchor 为空时却去读 provider:${providerId}:cost_5h_fixed,这个 key 在现有实现里不会被任何路径写入,所以 batch 结果会把该提供商的 5h 用量永久看成 0。拿不到有效锚点时应回退到 rolling 查询,或者先把 createdAt fallback 解析出来再拼 fixed key。

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

In `@src/lib/rate-limit/service.ts` around lines 1509 - 1523, The code reads a
non-existent fixed 5h key when fiveHourMode === "fixed" but
fiveHourConfig.resetAnchor is missing, causing reads of
`provider:${providerId}:cost_5h_fixed` that are never written; update the logic
in the block using fiveHourConfig/fiveHourMode (around fiveHourConfig,
fiveHourMode, RateLimitService.resolve5hFixedCostKey, pipeline.get, queryMeta)
to: if resetAnchor is absent, either fallback to the rolling query branch (treat
as isRolling=true) or derive a valid fixed anchor (e.g. from provider
createdAt/created timestamp used by trackCost) and then call
resolve5hFixedCostKey with that anchor before calling pipeline.get; ensure
queryMeta reflects the chosen period/isRolling accordingly.

Comment on lines +22 to +40
const FIVE_HOUR_RESET_ANCHOR_STRING = z
.string()
.refine((value) => value === "" || isValidDateTimeString(value), "5小时重置锚点时间格式无效");

const FIVE_HOUR_RESET_ANCHOR_STRING_WITH_DEFAULT = z.preprocess(
(value) => normalizeFiveHourResetAnchorInput(value === null ? "" : value),
FIVE_HOUR_RESET_ANCHOR_STRING.optional().default("")
);

const FIVE_HOUR_RESET_ANCHOR_OPTIONAL_STRING = z.preprocess(
(value) => normalizeFiveHourResetAnchorInput(value === null ? "" : value),
FIVE_HOUR_RESET_ANCHOR_STRING.optional()
);

const FIVE_HOUR_RESET_ANCHOR_UPDATE_NULLABLE = z.preprocess((val) => {
if (val === undefined) return undefined;
if (val === null || val === "") return null;
return normalizeFiveHourResetAnchorInput(val);
}, z.string().refine(isValidDateTimeString, "5小时重置锚点时间格式无效").nullable().optional());
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

新的锚点校验文案仍然是硬编码中文。

这两个 message 会直接进入 user/key/provider 表单校验错误;现在写死中文会让非 zh-CN 语言的界面回退到错误语言。这里应该继续走现有的错误码/翻译键链路,而不是在 schema 里放字面量。

As per coding guidelines "All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text"

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

In `@src/lib/validation/schemas.ts` around lines 22 - 40, The Zod schemas
(FIVE_HOUR_RESET_ANCHOR_STRING, FIVE_HOUR_RESET_ANCHOR_UPDATE_NULLABLE, etc.)
currently embed hardcoded Chinese error messages; replace those literal messages
with the project's i18n error key or error-code mechanism (e.g., use the
existing translation key constant or return an error code string that the form
layer resolves via i18n.t instead of a literal message). Concretely, remove the
Chinese string passed to .refine(...) and return/throw the canonical validation
key or use the shared helper (the same pattern used elsewhere for field errors)
so the UI can translate into zh-CN/zh-TW/en/ja/ru; ensure the preprocess/refine
calls still surface that key (or Error object containing the key) and update any
unit tests to expect the i18n key instead of the hardcoded Chinese text.

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

Labels

area:core area:i18n area:UI enhancement New feature or request size/XL Extra Large PR (> 1000 lines)

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant