Skip to content

[plan][feat] Add configurable weekly reset time for rate limiting #694

@ylxmf2005

Description

@ylxmf2005

Implementation Plan: Configurable Weekly Reset Time

Consensus Summary

This plan adopts a providers-only scope for the first PR, following the Reducer's recommendation to minimize risk for an external contribution. It extends existing function signatures with optional parameters rather than creating parallel helpers, and adds a "Weekly Reset Settings" UI section. The Critique's Redis migration concern is addressed with a dual-read fallback strategy.

Goal

The weekly cost reset is currently hardcoded to Monday 00:00 in time-utils.ts (3 locations: getTimeRangeForPeriod, getTTLForPeriod, getResetInfo). Administrators need the ability to configure which day and time the weekly cost window resets, at least for providers.

Success criteria:

  • Provider weekly reset day (0-6, Sunday=0) and time (HH:mm) are configurable via the provider settings UI
  • Weekly cost tracking in Redis uses the configured reset day/time for TTL and time range calculations
  • Existing providers default to Monday 00:00 (backward compatible, nullable fields)
  • All user-facing strings use i18n (5 languages)
  • Unit test coverage >= 80% for changed logic
  • bun run build, bun run lint, bun run typecheck, bun run test all pass

Out of scope:

  • Weekly reset configuration for users and keys tables -- follow-up PR after pattern is proven on providers
  • Rolling mode for weekly windows -- significant complexity (sorted set tracking), deferred
  • Monthly reset customization -- not requested
  • Dashboard/leaderboard UI changes for weekly boundaries

Codebase Analysis

Files verified:

  • src/lib/rate-limit/time-utils.ts -- 3 hardcoded weekStartsOn: 1 locations (lines 68, 133, 195)
  • src/lib/rate-limit/service.ts -- Redis keys cost_weekly (lines 707-708, 720-721, 1242); trackCost options interface (line 610); checkCostLimits limits interface (line 143); checkCostLimitsWithLease weekly window config (line 1349-1354)
  • src/drizzle/schema.ts -- providers table has dailyResetMode/dailyResetTime pattern (lines 218-223); no weekly reset fields exist
  • src/app/[locale]/settings/providers/_components/forms/provider-form/sections/limits-section.tsx -- Daily Reset Settings FieldGroup (lines 198-245)
  • src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts -- RateLimitState (lines 52-61), ProviderFormAction (lines 124-131)
  • src/app/v1/_lib/proxy/response-handler.ts -- trackCostToRedis passes daily reset config to trackCost (lines 2000-2007)
  • src/actions/providers.ts -- server action that persists provider form data
  • messages/en/settings/providers/form/sections.json -- i18n pattern for daily reset fields (lines 133-146)
  • CONTRIBUTING.md -- PR targets dev, Conventional Commits, Squash and merge

File changes:

File Level Purpose
src/drizzle/schema.ts medium Add weeklyResetDay (integer, nullable) and weeklyResetTime (varchar(5), nullable) to providers table
drizzle/XXXX_*.sql medium Generated migration (via bun run db:generate)
src/lib/rate-limit/time-utils.ts major Extend getTimeRangeForPeriod, getTTLForPeriod, getResetInfo with optional weeklyResetDay/weeklyResetTime params
src/lib/rate-limit/service.ts major Thread weekly config through checkCostLimits, trackCost, getCurrentCost, getCurrentCostBatch, checkCostLimitsWithLease; change Redis key to cost_weekly_{day}_{HHmm} with fallback read
src/app/v1/_lib/proxy/response-handler.ts minor Pass providerWeeklyResetDay/providerWeeklyResetTime through trackCost options
src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts medium Add weeklyResetDay/weeklyResetTime to RateLimitState and actions
src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-reducer.ts medium Handle new actions and LOAD_PROVIDER
src/app/[locale]/settings/providers/_components/forms/provider-form/sections/limits-section.tsx medium Add "Weekly Reset Settings" FieldGroup (day select + time input)
src/actions/providers.ts medium Read/write new fields in create/update actions
messages/en/settings/providers/form/sections.json minor Add weekly reset i18n keys
messages/zh-CN/settings/providers/form/sections.json minor Chinese translations
messages/zh-TW/settings/providers/form/sections.json minor Traditional Chinese translations
messages/ja/settings/providers/form/sections.json minor Japanese translations
messages/ru/settings/providers/form/sections.json minor Russian translations
tests/unit/lib/rate-limit/time-utils.test.ts medium Test weekly reset with custom day/time
tests/unit/lib/rate-limit/service.test.ts medium Test Redis key naming with weekly config, fallback read

Current architecture notes:

  • date-fns startOfWeek already accepts weekStartsOn: 0-6 -- core time calculation change is minimal
  • Redis keys for daily use cost_daily_{HHmm} suffix pattern; weekly will follow: cost_weekly_{day}_{HHmm}
  • trackCost options already carry per-entity daily reset config; weekly config follows same pattern
  • Lease system's checkCostLimitsWithLease hardcodes resetTime: "00:00" for weekly (line 1349-1354)

Interface Design

Modified interfaces:

// time-utils.ts - getTimeRangeForPeriod
 export async function getTimeRangeForPeriod(
   period: TimePeriod,
-  resetTime = "00:00"
+  resetTime = "00:00",
+  weeklyResetDay?: number,    // 0-6 (Sunday=0), default 1 (Monday)
+  weeklyResetTime?: string    // HH:mm, default "00:00"
 ): Promise<TimeRange>;

// Same pattern for getTTLForPeriod, getResetInfo, and WithMode variants
// service.ts - CostLimits interface (used by checkCostLimits)
 interface CostLimits {
   limit_5h_usd?: number;
   limit_daily_usd?: number;
   limit_weekly_usd?: number;
   limit_monthly_usd?: number;
+  weekly_reset_day?: number;
+  weekly_reset_time?: string;
 }
// service.ts - TrackCostOptions (used by trackCost)
 interface TrackCostOptions {
   keyResetTime?: string;
   keyResetMode?: string;
   providerResetTime?: string;
   providerResetMode?: string;
+  providerWeeklyResetDay?: number;
+  providerWeeklyResetTime?: string;
 }
// provider-form-types.ts - RateLimitState
 export interface RateLimitState {
   // ... existing fields ...
+  weeklyResetDay: number;   // 0-6, default 1
+  weeklyResetTime: string;  // HH:mm, default "00:00"
 }

Test Strategy

Test modifications:

  • tests/unit/lib/rate-limit/time-utils.test.ts - Extend existing weekly tests

    • Test case: Custom day (Sunday=0) with default time returns correct time range starting Sunday 00:00
    • Test case: Custom day (Wednesday=3) with custom time (18:00) returns correct boundary
    • Test case: Default behavior unchanged when weeklyResetDay/weeklyResetTime omitted
    • Test case: TTL calculation to next custom weekly boundary
    • Test case: Reset info returns correct next reset time for custom day/time
  • tests/unit/lib/rate-limit/service.test.ts - Test Redis key and fallback

    • Test case: Redis key includes cost_weekly_1_0000 for Monday 00:00
    • Test case: Redis key includes cost_weekly_0_1800 for Sunday 18:00
    • Test case: Fallback read from old cost_weekly key when new key returns null
    • Test case: Migration: old key value copied to new key and old key deleted

Implementation Steps

Step 1: Schema migration (Est: ~20 LOC)

  • Add weeklyResetDay (integer, nullable) and weeklyResetTime (varchar(5), nullable) to providers table in src/drizzle/schema.ts
  • Run bun run db:generate to auto-generate migration SQL
  • Review generated SQL; ensure columns are nullable with no default (runtime handles defaults)
    Dependencies: None

Step 2: Extend time-utils.ts (Est: ~40 LOC)

  • Add optional weeklyResetDay and weeklyResetTime params to getTimeRangeForPeriod, getTTLForPeriod, getResetInfo
  • In the weekly switch case: use weekStartsOn: weeklyResetDay ?? 1 for startOfWeek
  • Apply custom time offset using existing buildZonedDate helper pattern (same as daily fixed)
  • Also extend WithMode variants to pass through weekly params
    Dependencies: Step 1

Step 3: Extend service.ts (Est: ~80 LOC)

  • Add weekly_reset_day/weekly_reset_time to CostLimits interface and TrackCostOptions
  • Add resolveWeeklyReset(day?: number, time?: string): string helper that returns suffix like 1_0000
  • Change Redis key from cost_weekly to cost_weekly_{suffix} in all relevant methods
  • Add fallback: when new key returns null, check old cost_weekly key; if found, copy value to new key with appropriate TTL, then delete old key
  • Thread config through checkCostLimitsWithLease weekly window
    Dependencies: Step 2

Step 4: Update response-handler.ts (Est: ~10 LOC)

  • Pass providerWeeklyResetDay and providerWeeklyResetTime from provider object through trackCost options
    Dependencies: Step 3

Step 5: Update server action (Est: ~15 LOC)

  • In src/actions/providers.ts, read/write weeklyResetDay and weeklyResetTime in create/update actions
  • Add validation: day is integer 0-6, time matches HH:mm pattern
    Dependencies: Step 1

Step 6: Update UI types and reducer (Est: ~25 LOC)

  • Add weeklyResetDay/weeklyResetTime to RateLimitState in provider-form-types.ts
  • Add SET_WEEKLY_RESET_DAY/SET_WEEKLY_RESET_TIME actions
  • Handle in reducer, wire through LOAD_PROVIDER
    Dependencies: Step 5

Step 7: Update limits-section.tsx UI (Est: ~40 LOC)

  • Add "Weekly Reset Settings" FieldGroup below existing "Daily Reset Settings"
  • Day-of-week Select (7 options: Sunday through Saturday)
  • Time Input (type="time")
  • Conditionally show when limitWeeklyUsd is set (matching daily pattern)
    Dependencies: Step 6

Step 8: i18n translations (Est: ~30 LOC across 5 files)

  • Add keys: weeklyReset.title, weeklyReset.description, weeklyResetDay.label, weeklyResetTime.label, day names (Sun-Sat)
  • All 5 languages: en, zh-CN, zh-TW, ja, ru
    Dependencies: Step 7

Step 9: Unit tests (Est: ~60 LOC)

  • time-utils: test custom weekly boundaries (all 7 days, various times, default fallback)
  • service: test Redis key suffix, fallback read from old key, migration logic
    Dependencies: Steps 2, 3

Step 10: Pre-commit verification

  • Run bun run build && bun run lint:fix && bun run typecheck && bun run test
  • Ensure all checks pass before submitting PR
    Dependencies: All previous steps

Total estimated complexity: ~320 LOC (Medium)
Recommended approach: Single session

Success Criteria

  • Providers table has weekly_reset_day and weekly_reset_time nullable columns
  • Weekly time range calculation respects configured day and time
  • Redis keys for weekly cost use cost_weekly_{day}_{HHmm} format
  • Fallback read from old cost_weekly key prevents data loss during migration
  • Provider form UI allows configuring weekly reset day and time
  • All 5 language files updated with weekly reset translations
  • Unit tests pass with >= 80% coverage on changed files
  • bun run build && bun run lint && bun run typecheck && bun run test all green
  • PR targets dev branch with feat: Conventional Commit format

Risks and Mitigations

Risk Likelihood Impact Mitigation
Redis key migration: old cost_weekly keys orphaned, potential double-counting High High Dual-read fallback: check new key first, fall back to old key, migrate on read. Old keys expire naturally via TTL (max ~7 days).
time-utils function signature change breaks existing callers Medium Medium New params are optional with defaults matching current behavior (Monday 00:00). All existing callers pass no weekly args.
Lease system not receiving weekly config Medium Medium Update checkCostLimitsWithLease to pass weekly reset day/time for the weekly window.
trackCost options interface grows unwieldy Low Low Only adding 2 optional fields for provider weekly. User/key weekly is deferred.
Mid-week reset day change creates short/long transition window Low Low Acceptable behavior -- same as how daily reset time changes work. Old Redis key expires naturally.

Dependencies

  • date-fns startOfWeek already supports weekStartsOn option (0-6) -- no new dependency
  • Drizzle ORM migration tooling (bun run db:generate)
  • No external service dependencies

Metadata

Metadata

Assignees

No one assigned

    Projects

    Status

    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions