Skip to content

release v0.6.3#897

Merged
ding113 merged 42 commits intomainfrom
dev
Mar 11, 2026
Merged

release v0.6.3#897
ding113 merged 42 commits intomainfrom
dev

Conversation

@ding113
Copy link
Owner

@ding113 ding113 commented Mar 10, 2026

Summary

Release v0.6.3 introducing provider hedge racing for optimized first-byte timeout handling, user insights dashboard, soft user limit reset, and comprehensive bug fixes and UI improvements.

Problem

This release addresses several key areas:

  1. First-byte timeout handling - When a provider exceeded its first-byte timeout, requests failed immediately. Users wanted better failover behavior
  2. User usage analytics - Admin lacked visibility into per-model usage distribution and trends for individual users
  3. User limit management - Resetting user limits required destructive deletion of all historical data
  4. API compatibility - Various API compatibility issues with /v1/responses, auth tokens, and request headers

Solution

This release implements multiple features and fixes:

  1. Provider Hedge Racing - Converts first-byte timeout into a hedge-based provider race that keeps earlier attempts alive until a winner is chosen, then aborts losers
  2. User Insights Page - New admin-only dashboard at /dashboard/leaderboard/user/[userId] with per-model breakdown and trend charts
  3. Soft User Limit Reset - New costResetAt mechanism allows resetting limits without deleting historical usage data
  4. Response Input Rectifier - Normalizes /v1/responses input format (string/object to array) before guard pipeline
  5. GPT Long-Context Pricing - Added priority long-context token pricing fields for billing calculations

Changes

Core Features

Feature Description PRs
Provider Hedge Racing Converts first-byte timeout into a race that aborts losers when winner emerges #894
User Insights Dashboard Per-model usage breakdown, trend charts, overview metrics for individual users #886
Soft User Limit Reset Reset user cost limits without deleting historical data #890
Response Input Rectifier Auto-normalizes /v1/responses input formats (string/object → array) #888
GPT Long-Context Pricing Added priority long-context token pricing for accurate billing #873

Bug Fixes

Fix Description PR/Issue
Admin token auth Restored admin token access in opaque session mode #884
Log cleanup Fixed log cleanup not actually deleting records #885
Transfer-encoding Strip transfer-encoding from forwarded upstream requests #880
Anthropic model routing Fixed Anthropic provider routing without model whitelist #832
Cached tokens Fixed cached tokens recording from chat completions usage #889
User search performance Fixed user management page search performance -
Request header parsing Fixed request header parsing for new-api compatibility -

UI Improvements

Database Changes

Migration Description
0079_easy_zeigeist.sql Added enable_response_input_rectifier system setting
0080_fresh_clint_barton.sql Added cost_reset_at column for soft user limit reset

Breaking Changes

Change Impact Migration
Provider first-byte timeout behavior Timeout now triggers hedge race instead of immediate failure. Recommend setting first-byte timeout to 10-15s for optimal experience No database migration required
Anthropic provider without model whitelist Behavior changes from routing only claude-* models to routing all models using Anthropic API format. Set model whitelist to maintain previous behavior No database migration required

Testing

Automated Tests

  • Unit tests added for hedge racing, user insights, soft reset, and response input rectifier
  • Integration tests for proxy flow and API compatibility

Manual Testing

  1. Test provider hedge racing with multiple providers

  2. Verify user insights page displays correct per-model statistics

  3. Test soft reset preserves historical data

  4. Test /v1/responses with string/object input formats

  5. Verify Anthropic provider model routing with and without whitelist

Related


Description enhanced by Claude AI

Greptile Summary

This release (v0.6.3) introduces Provider Hedge Racing, a User Insights Dashboard, Soft User Limit Reset (costResetAt), a Response Input Rectifier for /v1/responses, and GPT long-context priority pricing — accompanied by 215 changed files and extensive unit/integration test coverage.

Key observations from the review:

  • Hedge Racing (forwarder.ts): The core engine is well-structured but carries several known edge cases from prior reviews (unresolved resultPromise on Redis failure in settleFailure, stale chain entry after async gap in handleAttemptFailure, launchAlternative rejection leak). A newly identified issue: when resolveStreamingHedgeEndpoint throws for any candidate, the catch block silently calls launchAlternative() with no provider-chain entry recorded, creating an audit gap for operators reviewing why a provider was bypassed.
  • costResetAt / Soft Reset: The DB path correctly clips the query window by costResetAt. The Redis period-cache path (5 h rolling, daily rolling, fixed period) still reads a raw accumulated value without applying the costResetAt clip — a gap already noted in prior review that means stale enforcement can persist up to the cache TTL after a reset if Redis cleanup fails.
  • getUserProviderBreakdown: The INNER JOIN on providers silently excludes null-finalProviderId ledger rows, causing the provider breakdown total to diverge from the model breakdown total (prior review). The unescaped ILIKE on filters.model can broaden the match unexpectedly (prior review).
  • Log Cleanup: The FOR UPDATE SKIP LOCKED addition prevents concurrent-job deadlocks. The VACUUM ANALYZE call after bulk deletes is acknowledged as risky inside a transaction block (prior review).
  • readLiveChainBatch: Issues N individual Redis GET calls instead of a single pipeline/MGET, which is suboptimal at scale but not incorrect.
  • Breaking change awareness: The providerSupportsModel simplification removes modelRedirects from routing decisions — operators with redirect-only configs need to add the source model to allowedModels (documented in PR).

Confidence Score: 3/5

  • Merge with caution — hedge-path audit gaps and the costResetAt/Redis-cache divergence should be addressed before relying on them in production diagnostics and billing enforcement.
  • The PR has strong test coverage and the core logic for each feature is sound. However, several issues flagged across both this and prior reviews remain unaddressed: (1) hedge-path settleFailure can hang on Redis failure, (2) launchAlternative rejection can leave resultPromise unresolved, (3) period-based Redis cache hits bypass costResetAt enforcement, (4) getUserProviderBreakdown INNER JOIN silently drops null-provider rows, and (5) newly identified endpoint-resolution failures in startAttempt produce no chain entry. The accumulation of open edge cases in the hedge engine lowers confidence.
  • src/app/v1/_lib/proxy/forwarder.ts (hedge engine edge cases), src/lib/rate-limit/service.ts (costResetAt not applied to period-cache hits), src/repository/admin-user-insights.ts (INNER JOIN + unescaped ILIKE)

Important Files Changed

Filename Overview
src/app/v1/_lib/proxy/forwarder.ts Core hedge racing implementation (+829 lines). Several edge cases flagged in prior reviews (settled-state dedup, launchAlternative rejection leak, shadow session TS-private coupling, zero-length chunk). New issue: endpoint-resolution failures in startAttempt produce no chain entry, creating an audit gap.
src/lib/rate-limit/service.ts Wires costResetAt into the DB fallback path and total-cost cache key. The fixed-period Redis cache-hit path (daily/weekly/monthly) still reads raw parseFloat(value) without applying costResetAt, so enforcement may be stale for up to the cache TTL after a soft reset (flagged in prior review).
src/repository/admin-user-insights.ts New admin insights queries. INNER JOIN on providers drops null-provider ledger rows (flagged), and the model ILIKE filter is unescaped (flagged). Logic and structure are otherwise sound with proper parameterization everywhere else.
src/actions/users.ts Adds resetUserLimitsOnly (soft reset via costResetAt) and refactors resetUserAllStatistics to use an atomic transaction. Redis cache cleanup is correctly extracted to clearUserCostCache. costResetAt is correctly propagated to all getUsersBatch codepaths.
src/app/v1/_lib/proxy/response-input-rectifier.ts New rectifier normalises /v1/responses input field from string/object to array before guard pipeline. Logic is correct and comprehensive: handles empty string, non-empty string, single object, and passthrough for arrays and null/undefined. Well-tested.
src/lib/log-cleanup/service.ts Significant refactor: splits cleanup into active-row deletion and a separate soft-deleted purge pass, adds FOR UPDATE SKIP LOCKED to prevent deadlocks, and adds VACUUM ANALYZE after bulk deletes. VACUUM concern (transaction block risk, autovacuum conflict) flagged in prior review.
src/lib/redis/live-chain-store.ts New Redis store for live request chain snapshots used by log UI. readLiveChainBatch uses N individual GETs instead of MGET/pipeline — not incorrect but introduces extra Redis round-trips under load.
src/app/v1/_lib/proxy/provider-selector.ts Simplifies providerSupportsModel: removes claude-specific special-casing and modelRedirects participation. Breaking change is documented. New logic is cleaner but operators need to be aware that existing modelRedirects-only configurations require allowedModels updates.
src/lib/auth.ts Restores admin token auth in opaque mode via constantTimeEqual comparison followed by validateKey DB lookup. Safe for programmatic API access; uses timing-safe comparison. Covered by new unit tests.
src/lib/redis/cost-cache-cleanup.ts New utility that scans and deletes all cost-related Redis keys for a user, covering period counters, total-cost cache (with and without resetAt suffix), and lease keys. Correctly handles empty pipeline case and individual scan failures without throwing.

Sequence Diagram

sequenceDiagram
    participant Client
    participant ProxyHandler
    participant HedgeEngine as sendStreamingWithHedge
    participant ProviderA as Provider A (initial)
    participant ProviderB as Provider B (hedge)
    participant Redis

    Client->>ProxyHandler: POST /v1/chat/completions (stream=true)
    ProxyHandler->>HedgeEngine: shouldUseStreamingHedge? → true
    HedgeEngine->>ProviderA: startAttempt(initial, useOriginalSession=true)
    ProviderA-->>HedgeEngine: HTTP 200 streaming starts
    Note over HedgeEngine: firstByteTimeout timer starts
    alt First byte arrives before timeout
        ProviderA-->>HedgeEngine: first chunk received
        HedgeEngine->>HedgeEngine: commitWinner(attemptA, firstChunk)
        HedgeEngine-->>Client: Response stream (from Provider A)
    else Timeout fires before first byte
        HedgeEngine->>HedgeEngine: thresholdTriggered → launchAlternative()
        HedgeEngine->>ProviderB: startAttempt(alternative, useOriginalSession=false)
        Note over HedgeEngine: Both A and B racing
        alt Provider B wins (first chunk arrives)
            ProviderB-->>HedgeEngine: first chunk received
            HedgeEngine->>HedgeEngine: commitWinner(attemptB, firstChunk)
            HedgeEngine->>ProviderA: abortAttempt("hedge_loser")
            HedgeEngine->>Redis: updateSessionBinding(providerB)
            HedgeEngine-->>Client: Response stream (from Provider B)
        else Provider A wins after hedge launched
            ProviderA-->>HedgeEngine: first chunk received
            HedgeEngine->>HedgeEngine: commitWinner(attemptA, firstChunk)
            HedgeEngine->>ProviderB: abortAttempt("hedge_loser")
            HedgeEngine-->>Client: Response stream (from Provider A)
        end
    end
Loading

Comments Outside Diff (1)

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

    Endpoint-resolution failure in hedge path leaves no chain entry

    When resolveStreamingHedgeEndpoint throws for any provider (initial or alternative), the catch block silently calls launchAlternative() and returns without adding a chain entry:

    } catch (endpointError) {
      lastError = endpointError as Error;
      await launchAlternative();
      await finishIfExhausted();
      return;
    }

    launchedProviderIds has already been updated at this point but launchedProviderCount is not incremented, so neither handleAttemptFailure nor abortAttempt is called for the failed endpoint resolution. The provider chain will show the alternative's success (or failure) without any trace of why the first candidate was skipped.

    In the successful case (commitWinner fires on the winner), an operator reviewing the chain will see a winner appear with no explanation of which previous endpoint was tried and failed. In the total-failure case (finishIfExhausted settles with an error), the chain is empty or only carries the session-reuse entry.

    Consider recording a chain entry before the early return:

    } catch (endpointError) {
      lastError = endpointError as Error;
      session.addProviderToChain(provider, {
        reason: "retry_failed",
        attemptNumber: launchedProviderCount + 1,
        errorMessage: endpointError instanceof Error ? endpointError.message : String(endpointError),
        circuitState: getCircuitState(provider.id),
      });
      await launchAlternative();
      await finishIfExhausted();
      return;
    }

Last reviewed commit: 14740d1

miraserver and others added 30 commits March 8, 2026 23:18
…#876)

* feat(providers): add sub-navigation and Options tab to provider form sidebar

Add Scheduling, Circuit Breaker, Timeout sub-items under their parent tabs
in the desktop sidebar for quick scroll access. Promote Options to a
top-level tab. Includes scroll tracking, i18n (5 langs), and 13 tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(providers): add Active Time sub-item under Options tab

Add activeTime sub-navigation for quick scroll access to Scheduled Active
Time section. Also add variant="highlight" to Options SectionCard for
consistent visual styling with other main sections.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(providers): show green ShieldCheck icon when proxy is configured

Separate icon with tooltip next to endpoint count on provider list.
Visible only when proxyUrl is set. i18n for 5 languages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(providers): fix 7 review issues in sub-nav and form architecture

- Derive TAB_CONFIG, TAB_ORDER, NAV_ORDER, PARENT_MAP from NAV_CONFIG (DRY)
- Add sub-nav to tablet and mobile breakpoints
- Move activeSubTab from useState into form reducer
- Compute Options tab status from routing state instead of hardcoding
- Lift TooltipProvider from per-item to list container level
- Fix RU i18n: singular form for timeout label
- Add 8 new tests covering derived constants and responsive sub-nav

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(providers): extract OptionsSection, perf fixes, batch dialog fix

- Extract OptionsSection from RoutingSection (963 -> 432 lines)
- Throttle scroll handler via requestAnimationFrame
- Merge double dispatch into single SET_ACTIVE_NAV action
- Derive sectionRefs from NAV_ORDER instead of manual record
- Add NAV_BY_ID lookup map for O(1) tablet/mobile nav access
- Add excludeTabs prop to FormTabNav, hide Options in batch dialog
- Clean up setTimeout/rAF refs on unmount

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(providers): add Options tab to batch edit dialog

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(providers): show proxy badge for providers without resolved vendor

Move ShieldCheck proxy indicator out of the vendor-specific branch
so it renders for all providers with a configured proxyUrl.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(providers): remove dead SET_ACTIVE_SUB_TAB, add status grouping comments

Address Gemini Code Assist review findings:
- Remove unused SET_ACTIVE_SUB_TAB action type and reducer case (superseded by SET_ACTIVE_NAV)
- Add grouping comments to options tab status conditional for readability

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(providers): address CodeRabbit review findings

- Fix zh-CN/zh-TW i18n terminology consistency (供应商/供應商 instead of 提供者)
- Add activeTimeEnd check to options tab status indicator
- Add focus-visible ring to tablet/mobile sub-nav buttons for accessibility

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(providers): use i18n fallback for unknown tag errors and scrollend event for scroll lock

- Replace raw reason string fallback with tUI("unknownError") i18n key
  in tag validation callbacks (routing-section.tsx)
- Add "unknownError" key to tagInput namespace in all 5 locales
- Use scrollend event with { once: true } + 1000ms fallback timer
  instead of fixed 500ms setTimeout for scroll lock release (index.tsx)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test(providers): add unit tests for OptionsSection component (25 tests)

Covers rendering, conditional display by provider type (claude/codex/gemini),
batch mode, dispatch actions, active time UI, disabled state, edit mode IDs,
and batch-only badges.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(providers): remove stale scrollend listener on rapid tab clicks

Store previous scrollend listener in a ref and remove it at the start
of each scrollToSection call, preventing premature unlock when multiple
smooth scrolls overlap during fast sequential tab clicks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(providers): fix 3 style-level review findings

- Remove dead subSectionRefs.options property from OptionsSection
  (parent div in index.tsx already tracks this ref)
- Use filteredNav.find() instead of NAV_BY_ID for tablet/mobile
  sub-row lookup so excludeTabs is respected; remove unused NAV_BY_ID
- Replace non-null assertion with guarded clearTimeout in scrollend
  handler

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(ci): resolve 3 pre-existing test failures from PR #873

All caused by commit 2e663cd which changed billing model and session
API without updating tests/translations:

- i18n: add missing `prices.badges.multi` key to ja/ru/zh-TW locales
- tests: update cost-calculation expectations to match full-request
  pricing (all tokens at premium rate when context > 200K threshold)
- tests: fix lease-decrement session mock to use
  getResolvedPricingByBillingSource instead of removed method

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(types): narrow Recharts v3 dataKey type for React Key/ReactNode compat

Recharts v3 widened `dataKey` to `string | number | ((obj: any) => any) | undefined`,
which is incompatible with React `Key` and `ReactNode`. Wrap with `String()` in 2 files
to satisfy tsgo in CI. Pre-existing issue from main, not introduced by this branch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: format code (feat-providers-list-3-0732ba6)

* ci: retrigger CI after auto-format fix

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(types): narrow Recharts ValueType for formatter call in chart.tsx

Removing the explicit .map() parameter type exposed ValueType
(string | number | readonly (string|number)[]) which is too wide
for the formatter's (string | number) parameter. Cast item.value.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(types): restore narrowing annotation for Recharts v3 tooltip map

Bring back the explicit type annotation on .map() callback but extend
dataKey to include ((obj: any) => any) to match Recharts v3 DataKey<any>.
This keeps value/name narrowed for formatter compatibility while making
the annotation assignable from TooltipPayloadEntry.

Replaces the previous approach of removing the annotation entirely,
which exposed ValueType and NameType width issues one by one.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(types): use inline casts instead of annotation for Recharts v3 compat

Remove the .map() parameter annotation entirely (it causes TS2345 due to
contravariance — our narrower type is not assignable from TooltipPayloadEntry).
Instead, let TS infer the full type and cast only at the two call sites where
formatter needs narrower types: item.value as string|number, item.name as string.

All other usages of item.dataKey, item.name, item.value are compatible with
the wider Recharts v3 types (String() wraps, template literals, ReactNode).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: John Doe <johndoe@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* Red: 复现透传请求体回归

* fix: strip transfer-encoding from forwarded upstream requests

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix: preserve original request body bytes for raw passthrough endpoints

When bypassForwarderPreprocessing=true and session.request.buffer is
available, use the original ArrayBuffer directly instead of
JSON.stringify(messageToSend). This preserves whitespace, key ordering,
and trailing newlines in the forwarded request body.

---------

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Simplify providerSupportsModel() to treat all provider types uniformly.
Previously, claude-* models were hardcoded to only route to claude/claude-auth
providers, even when other providers explicitly declared them in allowedModels.
Now the logic is: explicit allowedModels/modelRedirects match -> accept;
empty allowedModels -> wildcard; otherwise reject. Format compatibility
remains enforced by checkFormatProviderTypeCompatibility independently.
- Export providerSupportsModel for direct unit testing
- Add table-driven tests (13 cases) covering all provider/model combinations
- Add pickRandomProvider path tests verifying format+model check interaction
- Add findReusable integration tests for session reuse scenarios
- Add invariant comment in findReusable documenting format-safety assumption
In opaque mode, validateAuthToken() rejected raw ADMIN_TOKEN passed via
cookie or Authorization header, breaking programmatic API access that
worked in legacy/dual mode. Add a constantTimeEqual check for the admin
token before returning null, so server-side env secret still authenticates
while regular API keys remain correctly rejected.
* feat: add response input rectifier for /v1/responses input normalization

OpenAI Responses API input field supports string shortcut, single object,
and array formats. This rectifier normalizes non-array input to standard
array format before the guard pipeline, with audit trail via special
settings persisted to message_requests.

- Normalize string input to [{role:"user",content:[{type:"input_text",text}]}]
- Wrap single object input (with role/type) into array
- Convert empty string to empty array
- Passthrough array input unchanged
- Default enabled, toggleable via system settings UI
- Broadened format-mapper body detection for string/object input
- Full i18n support (en, zh-CN, zh-TW, ja, ru)
- 14 unit tests covering all normalization paths

* fix: address bugbot review comments on response-input-rectifier PR

- Remove redundant ternary in rectifyResponseInput (both branches "other")
- Revert body-based detection broadening in detectClientFormat to prevent
  misrouting /v1/embeddings and other endpoints with string input fields
- Add missing enableBillingHeaderRectifier to returning clause (pre-existing)
- Refine i18n descriptions to specify "single message object" (5 locales)
- Add 4 normalizeResponseInput tests covering settings gate and audit trail
…861) (#886)

* feat(leaderboard): add user model drill-down and user insights page (#861)

- Add user-level modelStats data layer with null model preservation
- Cache key isolation for user scope includeModelStats
- Admin-only includeUserModelStats API gate (403 for non-admin)
- Extract shared ModelBreakdownColumn/Row UI primitive
- Admin-only expandable user rows with per-model breakdown
- User name links to /dashboard/leaderboard/user/[userId]
- Admin-only user insights page with overview cards, key trend chart, and model breakdown
- Server actions for getUserInsightsOverview, KeyTrend, ModelBreakdown
- Full i18n support (zh-CN, zh-TW, en, ja, ru)
- 54 new tests across 6 test files

* chore: format code (feat-861-user-leaderboard-drill-down-81697a4)

* fix(leaderboard): resolve bugbot review findings for user insights (#886)

- Fix critical bug: overview card labeled "Cache Hit Rate" was showing
  todayErrorRate; renamed to "Error Rate" with correct i18n (5 locales)
- Fix security: Cache-Control now private when allowGlobalUsageView=false
- Add date validation (YYYY-MM-DD regex + range check) in
  getUserInsightsModelBreakdown server action
- Normalize key trend date field to ISO string to prevent client crash
  when cache returns Date objects
- Replace hardcoded "OK" button with tCommon("ok") i18n key
- Replace hardcoded "Calls" chart label with tStats("requests")
- Tighten userId validation to positive integers only
- Add NULLIF(TRIM()) to model field for consistency with leaderboard.ts
- Use machine-readable error code for 403 response
- Strengthen return type from unknown to DatabaseKeyStatRow[]
- Update tests: assert errorRate metric, date normalization, date
  validation, and error code assertions

* test: use realistic DatabaseKeyStatRow fields in key trend test mock

Mock data now matches the actual DatabaseKeyStatRow interface with
key_id, key_name, api_calls, and total_cost fields instead of generic
cost/requests. Assertions verify the full data contract.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
- Remove `deletedAt IS NULL` filter from buildWhereConditions: cleanup
  should delete ALL matching records regardless of soft-delete status
- Add `RETURNING 1` to DELETE SQL for driver-agnostic row counting
  (result.length instead of fragile count/rowCount properties)
- Add `FOR UPDATE SKIP LOCKED` to prevent deadlocks with concurrent jobs
- Add purgeSoftDeleted: batched hard-delete of soft-deleted records
  as fallback after main cleanup loop
- Add VACUUM ANALYZE after deletions to reclaim disk space
  (failure is non-fatal)
- Update CleanupResult with softDeletedPurged and vacuumPerformed
- Pass new fields through API route and show in UI toast
- Add i18n keys for 5 languages
- 19 tests covering all new behavior
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Wrap DISTINCT ON subquery so the outer ORDER BY sorts by updatedAt DESC,
showing recently updated models first in the paginated price table.
…hart colors

- Fix chart colors: replace hsl(var(--chart-N)) with var(--chart-N) to
  resolve oklch incompatibility causing gray fallback
- Fix leaderboard username link styling: remove hyperlink appearance
  while keeping clickability
- Add provider breakdown component with repository, server action, and
  side-by-side layout alongside model breakdown
- Add unified filter bar (time preset, key, provider, model) controlling
  key trend chart and both breakdown panels
- Lift filter state to UserInsightsView, refactor child components to
  accept filter props instead of managing internal state
- Extend repository layer with filter params (keyId, providerId, model)
  for both model and provider breakdown queries
- Add i18n keys for all 5 languages (en, zh-CN, zh-TW, ja, ru)
- Add comprehensive tests for provider breakdown action and filter utils
#894)

* fix(proxy): hedge first-byte timeout failover and clear stale bindings

* docs(schema): clarify timeout field comments

* fix(proxy): handle hedge launcher failures and endpoint errors

* test(validation): add zero timeout disable case
…chain

Implements comprehensive observability for hedge (speculative execution) and client abort scenarios:

Backend (forwarder.ts):
- Record hedge_triggered when threshold timer fires and alternative provider launches
- Record hedge_winner when a provider wins the hedge race (first byte received)
- Record hedge_loser_cancelled when a provider loses and gets aborted
- Record client_abort when client disconnects (replaces generic system_error)

Frontend (provider-chain-popover.tsx, LogicTraceTab.tsx):
- Add icons and status colors for all 4 new reason types
- Correctly count hedge_triggered as informational (not actual request)
- Display hedge flow with GitBranch/CheckCircle/XCircle/MinusCircle icons

Langfuse (trace-proxy-request.ts):
- Add hedge_winner to SUCCESS_REASONS set
- Add client_abort to ERROR_REASONS set
- Create hedge-trigger event observation with WARNING level

i18n:
- Add translations for 4 new reasons across 5 languages (en, zh-CN, zh-TW, ja, ru)
- Include timeline, description, and reason label translations

Tests:
- Add 22 new tests covering all new reason types
- Test isActualRequest(), getItemStatus(), isSuccessReason(), isErrorReason()

Related improvements:
- Add isProviderFinalized() utility to detect when provider info is reliable
- Show in-progress state in logs table and big-screen for unfinalised requests
- Prevent displaying stale provider names during hedge/fallback transitions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… logs table

Consolidate the isActualRequest function that was duplicated across three
files (provider-chain-formatter, provider-chain-popover, virtualized-logs-table).
Export from provider-chain-formatter.ts as the single source of truth.

Fixes two bugs in virtualized-logs-table.tsx:
- successfulProvider lookup was missing hedge_winner, causing incorrect
  costMultiplier display for hedge-won requests
- Inline isActualRequest was missing hedge_winner, hedge_loser_cancelled,
  client_abort and other newer reasons, causing incorrect request count
  and cost badge visibility
When the hedge timer fires and no alternative provider is found,
let the sole in-flight request continue instead of aborting it
with a 524 error. Only call finishIfExhausted() when all attempts
have already completed (edge case).
…tives

This commit addresses three issues with hedge race tracking:

1. Decision chain missing hedge participants
   - Add hedge_launched reason to record alternative provider launches
   - Record hedge_launched in forwarder.ts when launchedProviderCount > 1
   - Add timeline formatting for hedge_launched events

2. Hedge races mislabeled as retries in UI
   - Add isHedgeRace() and getRetryCount() helper functions
   - Display "Hedge Race" badge instead of retry count
   - Update provider-chain-popover and virtualized-logs-table

3. hedge_winner false positives
   - Fix isActualHedgeWin logic to only check launchedProviderCount
   - Prevent marking single-provider requests as hedge_winner

Additional improvements:
   - Strengthen addProviderToChain deduplication logic
   - Add comprehensive JSDoc for getRetryCount design decisions
   - Add 6 edge case tests (empty chain, incomplete hedge, mixed scenarios)
   - Test coverage: 54 → 60 tests, all passing

i18n: Add hedge_launched translations for 5 languages (en, zh-CN, zh-TW, ja, ru)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove text label from hedge race badge, keeping only the GitBranch icon
for a more compact display. The aria-label still contains the full text
for accessibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
实现批量编辑供应商时自动预填充已有设置的功能:

核心功能:
- 创建 deep-equals.ts 实现深度比较工具
- 创建 analyze-batch-settings.ts 分析批量供应商设置
- 修改 provider-form-context.tsx 在批量模式下预填充表单

分析逻辑:
- uniform: 所有供应商值相同时显示该值
- mixed: 供应商值不同时使用默认值
- empty: 所有供应商未设置时使用默认值

测试覆盖:
- 单元测试:deepEquals 深度比较(13个测试)
- 单元测试:analyzeBatchProviderSettings 分析器(14个测试)
- 集成测试:批量编辑预填充(3个测试)

所有测试通过,类型检查通过,构建成功。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 2 implementation: UI enhancements for batch edit prefill

Changes:
- Created MixedValueIndicator component to show when selected providers have different values
- Added i18n translations for mixed value indicators (5 languages: zh-CN, zh-TW, en, ja, ru)
- Modified provider-form-context to export batch analysis results
- Integrated mixed value indicators in routing-section (priority, weight, cost multiplier, model redirects)
- Integrated mixed value indicators in limits-section (all rate limits, circuit breaker settings)
- Enhanced LimitCard component to support mixed value display

The mixed value indicator appears below fields when:
- Batch mode is active
- Selected providers have different values for that field
- Shows up to 5 different values with "...and N more" for additional values

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add missing translation keys for settings.providers.batchEdit namespace:
- Batch operation toolbar (selectedCount, actions, enterMode, etc.)
- Edit/delete/reset circuit dialogs (dialog.*)
- Preview step with field labels (preview.*, fields.*)
- Toast notifications (toast.*)
- Undo operations (undo.*)
- Confirmation buttons (confirm.*)

Covers all UI strings used in provider-batch-actions.tsx,
provider-batch-dialog.tsx, provider-batch-preview-step.tsx,
and provider-batch-toolbar.tsx components.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…#890)

* feat: add costResetAt for soft user limit reset without deleting data

Add costResetAt timestamp column to users table that clips all cost
calculations to start from reset time instead of all-time. This enables
admins to reset a user's rate limits without destroying historical
usage data (messageRequest/usageLedger rows are preserved).

Key changes:
- Schema: new cost_reset_at column on users table
- Repository: costResetAt propagated through all select queries, key
  validation, and statistics aggregation (with per-user batch support)
- Rate limiting: all 12 proxy guard checks pass costResetAt; service
  and lease layers clip time windows accordingly
- Auth cache: hydrate costResetAt from Redis cache as Date; invalidate
  auth cache on reset to avoid stale costResetAt
- Actions: resetUserLimitsOnly sets costResetAt + clears cost cache;
  getUserLimitUsage/getUserAllLimitUsage/getKeyLimitUsage/getMyQuota
  clip time ranges by costResetAt
- UI: edit-user-dialog with separate Reset Limits Only (amber) vs
  Reset All Statistics (red) with confirmation dialogs
- i18n: all 5 languages (en, zh-CN, zh-TW, ja, ru)
- Tests: 10 unit tests for resetUserLimitsOnly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: harden costResetAt handling -- repo layer, DRY Redis cleanup, Date validation

- Extract resetUserCostResetAt repo function with updatedAt + auth cache invalidation
- Extract clearUserCostCache helper to deduplicate Redis cleanup between reset functions
- Use instanceof Date checks in lease-service and my-usage for costResetAt validation
- Remove dead hasActiveSessions variable in cost-cache-cleanup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: unify costResetAt guards to instanceof Date, add last-reset badge in user edit dialog

R3: Replace truthiness checks with `instanceof Date` in 3 places (users.ts clipStart, quotas page).
R4: Show last reset timestamp in edit-user-dialog Reset Limits section (5 langs).
Add 47 unit tests covering costResetAt across key-quota, redis cleanup, statistics, and auth cache.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: apply costResetAt clipStart to key-quota usage queries

Clip all period time ranges by user's costResetAt and replace getTotalUsageForKey
with sumKeyTotalCost supporting resetAt parameter.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: format branch files with biome, suppress noThenProperty in thenable mock

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR #853 review findings -- guard consistency, window clipping, error handling

- keys.ts: eliminate redundant findUserById call, use joined costResetAt + instanceof Date guard
- users.ts: handle resetUserCostResetAt return value (false = soft-deleted user)
- service.ts: add instanceof Date guard to costResetAt comparison
- statistics.ts: fix sumKeyTotalCost/sumUserTotalCost to use max(resetAt, maxAgeCutoff) instead
  of replacing maxAgeDays; refactor nested ternaries to if-blocks in quota functions
- cost-cache-cleanup.ts: wrap pipeline.exec() in try/catch to honor never-throws contract
- Update test for pipeline.exec throw now caught inside clearUserCostCache

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: tighten key-quota clipStart assertions with toHaveBeenNthCalledWith

Verify each window (5h/daily/weekly/monthly) is clipped individually
instead of checking unordered calledWith matches.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: repair CI test failures caused by costResetAt feature changes

- ja/dashboard.json: replace fullwidth parens with halfwidth
- api-key-auth-cache-reset-at.test: override CI env so shouldUseRedisClient() works
- key-quota-concurrent-inherit.test: add logger.info mock, sumKeyTotalCost mock, userCostResetAt field
- my-usage-concurrent-inherit.test: add logger.info/debug mocks
- total-usage-semantics.test: update call assertions for new costResetAt parameter
- users-reset-all-statistics.test: mock resetUserCostResetAt, update pipeline.exec error expectations
- rate-limit-guard.test: add cost_reset_at: null to expected checkCostLimitsWithLease args

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR #890 review comments for costResetAt feature

Critical:
- Wrap resetUserAllStatistics DB writes in transaction for atomicity
- Change sumKeyTotalCost maxAgeDays from 365 to Infinity for unbounded
  accumulation from costResetAt
- Add costResetAtMs to BudgetLease cache with stale detection

Medium:
- Add logger.warn to silent Redis scan failure handlers
- Add fail-open documentation for costResetAt validation
- Fix test mock leak (vi.resetAllMocks + re-establish defaults)

Minor:
- Rename i18n "Reset Data" to "Reset Options" across 5 languages
- Remove brittle source code string assertion tests
- Update test assertions for transaction and Infinity changes

* chore: format code (fix-user-reset-stats-e5002db)

---------

Co-authored-by: John Doe <johndoe@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* fix: resolve client restriction checkboxes not working in create user dialog

Use useRef to track latest nested form state synchronously, preventing
stale state overwrites when AccessRestrictionsSection fires multiple
onChange calls within the same event handler.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add wildcard glob pattern support for custom client restrictions

Patterns with '*' use full-string glob matching (case-insensitive,
literal characters). Patterns without '*' retain existing substring
matching with normalization for backward compatibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add popover multi-select for Claude Code sub-client restrictions

Adds granular selection of individual Claude Code sub-clients (CLI,
VS Code, SDK-TS, SDK-PY, CLI-SDK, GitHub Action) via a popover
dropdown on the Claude Code preset row. Auto-consolidates to parent
"claude-code" when all 6 sub-clients are selected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: delegate client restrictions UI to shared editor component

Remove ~200 lines of duplicated preset/popover logic from
AccessRestrictionsSection by reusing ClientRestrictionsEditor.
Fixes i18n bug where sub-client "All" label used wrong translation key
(presetClients["sub-all"] instead of subClients?.all).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: replace regex glob with linear matcher to prevent ReDoS

Replace globToRegex (regex-based, vulnerable to catastrophic
backtracking on patterns like *a*a*a*a*c) with globMatch
(two-pointer linear algorithm). Remove globCache since regex
compilation is no longer needed.

Add adversarial tests: consecutive wildcards, regex metacharacters
as literals, and a performance guard for pathological patterns.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(i18n): correct parentheses and restrict wildcard to client editors

- ja/dashboard.json: fullwidth -> halfwidth parentheses (CI test)
- zh-TW/dashboard.json: halfwidth -> fullwidth in subClients (CI test)
- tag-input.tsx: remove * from DEFAULT_TAG_PATTERN (was leaking to all consumers)
- client-restrictions-editor.tsx: explicit validateTag with * for client inputs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(ui): disable inactive preset popover and allow dots/slashes in client patterns

- Disable sub-client popover button when preset is neither allowed nor blocked
- Extend validateTag regex to accept . and / for real-world UA patterns (e.g. my.tool/1.0)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(ui): require subClients/nSelected props and document glob anchoring

- Make subClients and nSelected required in component interfaces (all callers
  already provide them; removes hardcoded "All" fallbacks per i18n rules)
- Update customHelp in all 10 i18n files to document glob anchoring behavior
  (use *foo* to match anywhere)
- Update test fixtures with required translation props

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: format code (fix-ui-client-restriction-4-8d4ad51)

* style: fix formatting and extract shared client tag pattern constant

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: John Doe <johndoe@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@coderabbitai
Copy link

coderabbitai bot commented Mar 10, 2026

Important

Review skipped

Too many files!

This PR contains 215 files, which is 65 over the limit of 150.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 64a11423-d9de-437f-88cd-c3d2dd5d3bcc

📥 Commits

Reviewing files that changed from the base of the PR and between 2b60af9 and 14740d1.

📒 Files selected for processing (215)
  • .env.example
  • drizzle/0079_easy_zeigeist.sql
  • drizzle/0080_fresh_clint_barton.sql
  • drizzle/meta/0079_snapshot.json
  • drizzle/meta/0080_snapshot.json
  • drizzle/meta/_journal.json
  • messages/en/dashboard.json
  • messages/en/provider-chain.json
  • messages/en/settings/config.json
  • messages/en/settings/data.json
  • messages/en/settings/providers/batchEdit.json
  • messages/en/settings/providers/form/common.json
  • messages/en/settings/providers/form/sections.json
  • messages/en/settings/providers/list.json
  • messages/en/ui.json
  • messages/ja/dashboard.json
  • messages/ja/provider-chain.json
  • messages/ja/settings/config.json
  • messages/ja/settings/data.json
  • messages/ja/settings/prices.json
  • messages/ja/settings/providers/batchEdit.json
  • messages/ja/settings/providers/form/common.json
  • messages/ja/settings/providers/form/sections.json
  • messages/ja/settings/providers/list.json
  • messages/ja/ui.json
  • messages/ru/dashboard.json
  • messages/ru/provider-chain.json
  • messages/ru/settings/config.json
  • messages/ru/settings/data.json
  • messages/ru/settings/prices.json
  • messages/ru/settings/providers/batchEdit.json
  • messages/ru/settings/providers/form/common.json
  • messages/ru/settings/providers/form/sections.json
  • messages/ru/settings/providers/list.json
  • messages/ru/ui.json
  • messages/zh-CN/dashboard.json
  • messages/zh-CN/provider-chain.json
  • messages/zh-CN/settings/config.json
  • messages/zh-CN/settings/data.json
  • messages/zh-CN/settings/providers/batchEdit.json
  • messages/zh-CN/settings/providers/form/common.json
  • messages/zh-CN/settings/providers/form/sections.json
  • messages/zh-CN/settings/providers/list.json
  • messages/zh-CN/ui.json
  • messages/zh-TW/dashboard.json
  • messages/zh-TW/provider-chain.json
  • messages/zh-TW/settings/config.json
  • messages/zh-TW/settings/data.json
  • messages/zh-TW/settings/prices.json
  • messages/zh-TW/settings/providers/batchEdit.json
  • messages/zh-TW/settings/providers/form/common.json
  • messages/zh-TW/settings/providers/form/sections.json
  • messages/zh-TW/settings/providers/list.json
  • messages/zh-TW/ui.json
  • src/actions/admin-user-insights.ts
  • src/actions/dashboard-realtime.ts
  • src/actions/key-quota.ts
  • src/actions/keys.ts
  • src/actions/my-usage.ts
  • src/actions/system-config.ts
  • src/actions/usage-logs.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/access-restrictions-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/hooks/use-user-translations.ts
  • src/app/[locale]/dashboard/_components/user/user-limit-badge.tsx
  • src/app/[locale]/dashboard/availability/_components/provider/latency-chart.tsx
  • src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx
  • src/app/[locale]/dashboard/leaderboard/user/[userId]/_components/filters/types.ts
  • src/app/[locale]/dashboard/leaderboard/user/[userId]/_components/filters/user-insights-filter-bar.tsx
  • src/app/[locale]/dashboard/leaderboard/user/[userId]/_components/user-insights-view.tsx
  • src/app/[locale]/dashboard/leaderboard/user/[userId]/_components/user-key-trend-chart.tsx
  • src/app/[locale]/dashboard/leaderboard/user/[userId]/_components/user-model-breakdown.tsx
  • src/app/[locale]/dashboard/leaderboard/user/[userId]/_components/user-overview-cards.tsx
  • src/app/[locale]/dashboard/leaderboard/user/[userId]/_components/user-provider-breakdown.tsx
  • src/app/[locale]/dashboard/leaderboard/user/[userId]/page.tsx
  • src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LogicTraceTab.tsx
  • src/app/[locale]/dashboard/logs/_components/provider-chain-popover.test.tsx
  • src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx
  • src/app/[locale]/dashboard/logs/_components/usage-logs-sections.test.tsx
  • src/app/[locale]/dashboard/logs/_components/usage-logs-sections.tsx
  • src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx
  • src/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsx
  • src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.test.tsx
  • src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx
  • src/app/[locale]/dashboard/quotas/users/page.tsx
  • src/app/[locale]/dashboard/users/users-page-client.tsx
  • src/app/[locale]/internal/dashboard/big-screen/page.tsx
  • src/app/[locale]/my-usage/_components/statistics-summary-card.tsx
  • src/app/[locale]/settings/config/_components/system-settings-form.tsx
  • src/app/[locale]/settings/config/page.tsx
  • src/app/[locale]/settings/data/_components/log-cleanup-panel.tsx
  • src/app/[locale]/settings/providers/_components/batch-edit/analyze-batch-settings.ts
  • src/app/[locale]/settings/providers/_components/batch-edit/deep-equals.ts
  • src/app/[locale]/settings/providers/_components/batch-edit/mixed-value-indicator.tsx
  • src/app/[locale]/settings/providers/_components/batch-edit/provider-batch-dialog.tsx
  • src/app/[locale]/settings/providers/_components/forms/provider-form/components/form-tab-nav.tsx
  • 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/[locale]/settings/providers/_components/forms/provider-form/sections/network-section.tsx
  • src/app/[locale]/settings/providers/_components/forms/provider-form/sections/options-section.tsx
  • src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx
  • src/app/[locale]/settings/providers/_components/provider-list-item.legacy.tsx
  • src/app/[locale]/settings/providers/_components/provider-list.tsx
  • src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx
  • src/app/api/admin/log-cleanup/manual/route.ts
  • src/app/api/leaderboard/route.ts
  • src/app/v1/_lib/proxy-handler.ts
  • src/app/v1/_lib/proxy/client-detector.ts
  • src/app/v1/_lib/proxy/format-mapper.ts
  • src/app/v1/_lib/proxy/forwarder.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
  • src/app/v1/_lib/proxy/response-input-rectifier.ts
  • src/app/v1/_lib/proxy/session.ts
  • src/app/v1/_lib/proxy/stream-finalization.ts
  • src/components/analytics/model-breakdown-column.tsx
  • src/components/form/client-restrictions-editor.test.tsx
  • src/components/form/client-restrictions-editor.tsx
  • src/components/ui/chart.tsx
  • src/drizzle/schema.ts
  • src/lib/auth.ts
  • src/lib/client-restrictions/client-presets.test.ts
  • src/lib/client-restrictions/client-presets.ts
  • src/lib/config/env.schema.ts
  • src/lib/config/system-settings-cache.ts
  • src/lib/dashboard/user-limit-usage-cache.test.ts
  • src/lib/dashboard/user-limit-usage-cache.ts
  • src/lib/dashboard/user-usage-loader.test.ts
  • src/lib/dashboard/user-usage-loader.ts
  • src/lib/langfuse/trace-proxy-request.test.ts
  • src/lib/langfuse/trace-proxy-request.ts
  • src/lib/log-cleanup/service.ts
  • src/lib/rate-limit/lease-service.ts
  • src/lib/rate-limit/lease.ts
  • src/lib/rate-limit/service.ts
  • src/lib/redis/cost-cache-cleanup.ts
  • src/lib/redis/leaderboard-cache.ts
  • src/lib/redis/live-chain-store.test.ts
  • src/lib/redis/live-chain-store.ts
  • src/lib/security/api-key-auth-cache.ts
  • src/lib/utils/cost-calculation.ts
  • src/lib/utils/price-data.ts
  • src/lib/utils/pricing-resolution.ts
  • src/lib/utils/provider-chain-display.test.ts
  • src/lib/utils/provider-chain-display.ts
  • src/lib/utils/provider-chain-formatter.test.ts
  • src/lib/utils/provider-chain-formatter.ts
  • src/lib/utils/provider-display.ts
  • src/lib/utils/special-settings.ts
  • src/lib/validation/schemas.ts
  • src/repository/_shared/transformers.test.ts
  • src/repository/_shared/transformers.ts
  • src/repository/admin-user-insights.ts
  • src/repository/key.ts
  • src/repository/leaderboard.ts
  • src/repository/model-price.ts
  • src/repository/provider.ts
  • src/repository/statistics.ts
  • src/repository/system-config.ts
  • src/repository/usage-logs.ts
  • src/repository/user.ts
  • src/types/message.ts
  • src/types/model-price.ts
  • src/types/special-settings.ts
  • src/types/system-config.ts
  • src/types/user.ts
  • tests/integration/batch-edit-prefill.test.ts
  • tests/integration/billing-model-source.test.ts
  • tests/unit/actions/admin-user-insights.test.ts
  • tests/unit/actions/key-quota-concurrent-inherit.test.ts
  • tests/unit/actions/key-quota-cost-reset.test.ts
  • tests/unit/actions/my-usage-concurrent-inherit.test.ts
  • tests/unit/actions/total-usage-semantics.test.ts
  • tests/unit/actions/users-reset-all-statistics.test.ts
  • tests/unit/actions/users-reset-limits-only.test.ts
  • tests/unit/api/leaderboard-route.test.ts
  • tests/unit/auth/admin-token-opaque-fallback.test.ts
  • tests/unit/batch-edit/analyze-batch-settings.test.ts
  • tests/unit/batch-edit/deep-equals.test.ts
  • tests/unit/components/model-breakdown-column.test.tsx
  • tests/unit/dashboard/user-insights-page.test.tsx
  • tests/unit/lib/cost-calculation-breakdown.test.ts
  • tests/unit/lib/cost-calculation-priority.test.ts
  • tests/unit/lib/log-cleanup/service-count.test.ts
  • tests/unit/lib/redis/cost-cache-cleanup.test.ts
  • tests/unit/lib/security/api-key-auth-cache-reset-at.test.ts
  • tests/unit/lib/utils/provider-display.test.ts
  • tests/unit/proxy/client-detector.test.ts
  • tests/unit/proxy/extract-usage-metrics.test.ts
  • tests/unit/proxy/hedge-winner-dedup.test.ts
  • tests/unit/proxy/provider-selector-cross-type-model.test.ts
  • tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts
  • tests/unit/proxy/proxy-forwarder-raw-passthrough-regression.test.ts
  • tests/unit/proxy/proxy-forwarder.test.ts
  • tests/unit/proxy/rate-limit-guard.test.ts
  • tests/unit/proxy/response-handler-endpoint-circuit-isolation.test.ts
  • tests/unit/proxy/response-handler-gemini-stream-passthrough-timeouts.test.ts
  • tests/unit/proxy/response-handler-lease-decrement.test.ts
  • tests/unit/proxy/response-input-rectifier.test.ts
  • tests/unit/repository/leaderboard-user-model-stats.test.ts
  • tests/unit/repository/statistics-reset-at.test.ts
  • tests/unit/settings/providers/form-tab-nav.test.tsx
  • tests/unit/settings/providers/options-section.test.tsx
  • tests/unit/settings/providers/provider-form-total-limit-ui.test.tsx
  • tests/unit/settings/providers/provider-rich-list-item-endpoints.test.tsx
  • tests/unit/usage-ledger/cleanup-immunity.test.ts
  • tests/unit/user-insights-filters.test.ts
  • tests/unit/validation/provider-timeout-schemas.test.ts

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

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

❤️ Share

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

@github-actions
Copy link
Contributor

🧪 测试结果

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

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

@github-actions github-actions bot added size/XL Extra Large PR (> 1000 lines) enhancement New feature or request area:UI area:provider area:core labels Mar 10, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

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

This release introduces significant performance and monitoring enhancements, primarily through concurrent provider racing and a new user insights dashboard. It also refines administrative controls with improved UI for client restrictions and provider management, alongside critical bug fixes for API routing and billing accuracy. The update aims to provide a more robust, efficient, and transparent system for managing AI service consumption.

Highlights

  • Concurrent Provider Racing: Introduced concurrent provider racing to optimize first-byte timeout, significantly improving user experience. It is recommended to adjust provider first-byte timeouts to 10-15s to maximize this feature's effectiveness.
  • Enhanced Billing & Model Support: Completed billing support for GPT series long context and fast modes, and fixed an issue where Anthropic providers couldn't route to non-Claude models without a whitelist.
  • Improved UI/UX for Administration: Optimized the user interface for client restrictions, provider batch editing, and request records, making administrative tasks more intuitive.
  • New User Insights & Leaderboard Features: Added a new user insights page and sub-model statistics to the user leaderboard, providing deeper analytics into user consumption patterns.
  • User Limit Management: Implemented functionality to reset user limits, allowing administrators to clear accumulated cost counters without deleting historical logs.
  • API Input Normalization: Added automatic adjustment for non-array input in /v1/responses requests, ensuring compatibility with standard API formats.
  • Configurable Log Polling: Enabled adjustment of the request log polling interval via environment variables, offering more control over dashboard real-time updates.
Changelog
  • .env.example
    • Added DASHBOARD_LOGS_POLL_INTERVAL_MS environment variable for log polling interval.
  • drizzle/0079_easy_zeigeist.sql
    • Added 'enable_response_input_rectifier' boolean column to 'system_settings' table.
  • drizzle/0080_fresh_clint_barton.sql
    • Added 'cost_reset_at' timestamp column to 'users' table.
  • drizzle/meta/_journal.json
    • Updated Drizzle migration journal with new migration entries.
  • messages/en/dashboard.json
    • Added 'retrying' status and 'unknownModel' label.
    • Introduced new 'userInsights' section with various labels for user analytics.
    • Updated client restriction placeholders and added new user reset options.
  • messages/en/provider-chain.json
    • Added new provider chain reasons including 'hedgeTriggered', 'hedgeLaunched', 'hedgeWinner', 'hedgeLoserCancelled', and 'clientAbort'.
  • messages/en/settings/config.json
    • Added new setting for 'enableResponseInputRectifier' with description.
  • messages/en/settings/data.json
    • Added new messages for log cleanup success, including 'softDeletePurged' and 'vacuumComplete'.
  • messages/en/settings/providers/batchEdit.json
    • Refactored batch edit translations, reordered actions, and added mixed value indicators.
    • Updated dialog descriptions for clarity.
  • messages/en/settings/providers/form/common.json
    • Added new tab labels for provider form sections: 'scheduling', 'options', 'activeTime', 'circuitBreaker', and 'timeout'.
  • messages/en/settings/providers/form/sections.json
    • Updated client restriction help text and placeholders.
    • Added sub-client selection options and increased non-streaming total timeout range to 1800 seconds.
  • messages/en/settings/providers/list.json
    • Added 'proxyEnabled' label.
  • messages/en/ui.json
    • Added 'unknownError' message for invalid tag input.
  • messages/ja/dashboard.json
    • Added 'Retrying' status and '不明' model label.
    • Introduced new 'ユーザーインサイト' section with various labels for user analytics.
    • Updated client restriction placeholders and added new user reset options.
  • messages/ja/provider-chain.json
    • Added new provider chain reasons including 'Hedge 発動', 'Hedge 代替起動済み', 'Hedge 競争勝者', 'Hedge 競争敗者(キャンセル)', and 'クライアント中断'.
  • messages/ja/settings/config.json
    • Added new setting for 'Response Input 整流器を有効化' with description.
  • messages/ja/settings/data.json
    • Added new messages for log cleanup success, including '論理削除レコードも物理削除しました' and 'データベース領域を回収しました'.
  • messages/ja/settings/prices.json
    • Added 'マルチ' badge.
  • messages/ja/settings/providers/batchEdit.json
    • Refactored batch edit translations, reordered actions, and added mixed value indicators.
    • Updated dialog descriptions for clarity.
  • messages/ja/settings/providers/form/common.json
    • Added new tab labels for provider form sections: 'スケジューリング', 'オプション', 'アクティブ時間', 'サーキットブレーカー', and 'タイムアウト'.
  • messages/ja/settings/providers/form/sections.json
    • Updated client restriction help text and placeholders.
    • Added sub-client selection options and increased non-streaming total timeout range to 1800 seconds.
  • messages/ja/settings/providers/list.json
    • Added 'プロキシ有効' label.
  • messages/ja/ui.json
    • Added '無効な入力' error message for tag input.
  • messages/ru/dashboard.json
    • Added 'Retrying' status and 'Неизвестно' model label.
    • Introduced new 'Аналитика пользователя' section with various labels for user analytics.
    • Updated client restriction placeholders and added new user reset options.
  • messages/ru/provider-chain.json
    • Added new provider chain reasons including 'Hedge запущен', 'Hedge альтернатива запущена', 'Победитель Hedge-гонки', 'Проигравший Hedge-гонки (отменён)', and 'Клиент прервал запрос'.
  • messages/ru/settings/config.json
    • Added new setting for 'Включить исправление Response Input' with description.
  • messages/ru/settings/data.json
    • Added new messages for log cleanup success, including 'Также удалено мягко удаленных записей' and 'Дисковое пространство БД освобождено'.
  • messages/ru/settings/prices.json
    • Added 'Мульти' badge.
  • messages/ru/settings/providers/batchEdit.json
    • Refactored batch edit translations, reordered actions, and added mixed value indicators.
    • Updated dialog descriptions for clarity.
  • messages/ru/settings/providers/form/common.json
    • Added new tab labels for provider form sections: 'Планирование', 'Параметры', 'Активное время', 'Автовыключатель', and 'Таймаут'.
  • messages/ru/settings/providers/form/sections.json
    • Updated client restriction help text and placeholders.
    • Added sub-client selection options and increased non-streaming total timeout range to 1800 seconds.
  • messages/ru/settings/providers/list.json
    • Added 'Прокси включен' label.
  • messages/ru/ui.json
    • Added 'Некорректный ввод' error message for tag input.
  • messages/zh-CN/dashboard.json
    • Added 'Retrying' status and '未知' model label.
    • Introduced new '用户洞察' section with various labels for user analytics.
    • Updated client restriction placeholders and added new user reset options.
  • messages/zh-CN/provider-chain.json
    • Added new provider chain reasons including 'Hedge 已触发', 'Hedge 备选已启动', 'Hedge 竞速赢家', 'Hedge 竞速输家(已取消)', and '客户端中断'.
  • messages/zh-CN/settings/config.json
    • Added new setting for '启用 Response Input 整流器' with description.
  • messages/zh-CN/settings/data.json
    • Added new messages for log cleanup success, including '另外清除了软删除记录' and '数据库空间已回收'.
  • messages/zh-CN/settings/providers/batchEdit.json
    • Refactored batch edit translations, reordered actions, and added mixed value indicators.
    • Updated dialog descriptions for clarity.
  • messages/zh-CN/settings/providers/form/common.json
    • Added new tab labels for provider form sections: '调度', '选项', '活跃时间', '断路器', and '超时'.
  • messages/zh-CN/settings/providers/form/sections.json
    • Updated client restriction help text and placeholders.
    • Added sub-client selection options and increased non-streaming total timeout range to 1800 seconds.
  • messages/zh-CN/settings/providers/list.json
    • Added '已启用代理' label.
  • messages/zh-CN/ui.json
    • Added '输入无效' error message for tag input.
  • messages/zh-TW/dashboard.json
    • Added 'Retrying' status and '未知' model label.
    • Introduced new '使用者洞察' section with various labels for user analytics.
    • Updated client restriction placeholders and added new user reset options.
  • messages/zh-TW/provider-chain.json
    • Added new provider chain reasons including 'Hedge 已觸發', 'Hedge 備選已啟動', 'Hedge 競速贏家', 'Hedge 競速輸家(已取消)', and '客戶端中斷'.
  • messages/zh-TW/settings/config.json
    • Added new setting for '啟用 Response Input 整流器' with description.
  • messages/zh-TW/settings/data.json
    • Added new messages for log cleanup success, including '另外清除了軟刪除記錄' and '資料庫空間已回收'.
  • messages/zh-TW/settings/prices.json
    • Added '多供應商' badge.
  • messages/zh-TW/settings/providers/batchEdit.json
    • Refactored batch edit translations, reordered actions, and added mixed value indicators.
    • Updated dialog descriptions for clarity.
  • messages/zh-TW/settings/providers/form/common.json
    • Added new tab labels for provider form sections: '排程', '選項', '活躍時間', '斷路器', and '逾時'.
  • messages/zh-TW/settings/providers/form/sections.json
    • Updated client restriction help text and placeholders.
    • Added sub-client selection options and increased non-streaming total timeout range to 1800 seconds.
  • messages/zh-TW/settings/providers/list.json
    • Added '已啟用代理' label.
  • messages/zh-TW/ui.json
    • Added '輸入無效' error message for tag input.
  • src/actions/admin-user-insights.ts
    • Added new server actions for fetching user insights data, including overview, key trend, model breakdown, and provider breakdown.
  • src/actions/dashboard-realtime.ts
    • Modified real-time dashboard data to handle unfinalized provider and status, using live chain data.
  • src/actions/key-quota.ts
    • Modified key quota usage calculation to respect 'userCostResetAt' for time range clipping.
  • src/actions/keys.ts
    • Modified key limit usage calculation to respect 'userCostResetAt' for time range clipping.
  • src/actions/my-usage.ts
    • Modified user quota calculation to respect 'costResetAt' for time range clipping.
  • src/actions/system-config.ts
    • Added 'enableResponseInputRectifier' to system settings save action.
  • src/actions/usage-logs.ts
    • Modified 'getUsageLogsBatch' to merge Redis live chain data for unfinalized rows.
  • src/actions/users.ts
    • Added 'costResetAt' to user display data.
    • Introduced 'resetUserLimitsOnly' action to reset user cost limits without deleting logs.
    • Updated 'resetUserAllStatistics' to use 'clearUserCostCache' and invalidate user auth cache.
  • src/app/[locale]/dashboard/_components/user/create-user-dialog.tsx
    • Updated user creation dialog to use refs for latest user and key drafts, improving form state management.
  • src/app/[locale]/dashboard/_components/user/edit-user-dialog.tsx
    • Added a 'Reset Limits' option to the user edit dialog, allowing selective reset of cost counters.
  • src/app/[locale]/dashboard/_components/user/forms/access-restrictions-section.tsx
    • Replaced direct preset handling with a dedicated 'ClientRestrictionsEditor' component.
  • src/app/[locale]/dashboard/_components/user/forms/user-edit-section.tsx
    • Updated 'UserEditSection' to pass new sub-client translations to the client restrictions editor.
  • src/app/[locale]/dashboard/_components/user/forms/user-form.tsx
    • Added sub-client translations to the user form.
  • src/app/[locale]/dashboard/_components/user/hooks/use-user-translations.ts
    • Added sub-client translations to the user translations hook.
  • src/app/[locale]/dashboard/_components/user/user-limit-badge.tsx
    • Updated user limit badge to use a shared cache and peek function for usage data.
  • src/app/[locale]/dashboard/availability/_components/provider/latency-chart.tsx
    • Fixed dataKey type in latency chart tooltip for better compatibility.
  • src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx
    • Added 'includeUserModelStats' parameter to leaderboard queries and integrated user insights link.
  • src/app/[locale]/dashboard/leaderboard/user/[userId]/_components/filters/types.ts
    • Added types for user insights filters and time range resolution utilities.
  • src/app/[locale]/dashboard/leaderboard/user/[userId]/_components/filters/user-insights-filter-bar.tsx
    • Added a filter bar component for user insights.
  • src/app/[locale]/dashboard/leaderboard/user/[userId]/_components/user-insights-view.tsx
    • Added a new user insights view component to display detailed user analytics.
  • src/app/[locale]/dashboard/leaderboard/user/[userId]/_components/user-key-trend-chart.tsx
    • Added a user key trend chart component for visualizing usage over time.
  • src/app/[locale]/dashboard/leaderboard/user/[userId]/_components/user-model-breakdown.tsx
    • Added a user model breakdown component to show model-level usage.
  • src/app/[locale]/dashboard/leaderboard/user/[userId]/_components/user-overview-cards.tsx
    • Added user overview cards component to display key metrics.
  • src/app/[locale]/dashboard/leaderboard/user/[userId]/_components/user-provider-breakdown.tsx
    • Added a user provider breakdown component to show provider-level usage.
  • src/app/[locale]/dashboard/leaderboard/user/[userId]/page.tsx
    • Added the user insights page.
  • src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LogicTraceTab.tsx
    • Updated logic trace tab to handle new hedge and client abort reasons.
  • src/app/[locale]/dashboard/logs/_components/provider-chain-popover.test.tsx
    • Added tests for hedge/abort reason handling in provider chain popover.
  • src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx
    • Updated provider chain popover to display hedge race status and client abort events.
  • src/app/[locale]/dashboard/logs/_components/usage-logs-sections.test.tsx
    • Added tests for 'UsageLogsDataSection' props, including logs refresh interval.
  • src/app/[locale]/dashboard/logs/_components/usage-logs-sections.tsx
    • Added 'logsRefreshIntervalMs' and billing model source/currency code to 'UsageLogsViewVirtualized'.
  • src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx
    • Updated usage logs table to handle hedge winner and use 'shouldShowCostBadgeInCell' for multiplier display.
  • src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.test.tsx
    • Added tests for live chain display in virtualized logs table.
  • src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx
    • Updated virtualized logs table to display live chain status and handle hedge winner/retrying states.
  • src/app/[locale]/dashboard/quotas/users/page.tsx
    • Modified user quota page to handle 'costResetAt' for total cost calculation.
  • src/app/[locale]/dashboard/users/users-page-client.tsx
    • Updated user page client to use sequential usage loading and clear usage cache on refresh.
  • src/app/[locale]/internal/dashboard/big-screen/page.tsx
    • Updated activity stream to handle unfinalized provider and status, displaying '...' for pending states.
  • src/app/[locale]/my-usage/_components/statistics-summary-card.tsx
    • Removed local 'ModelBreakdownColumn' and 'ModelBreakdownRow' components, now imported from 'src/components/analytics/model-breakdown-column.tsx'.
  • src/app/[locale]/settings/config/_components/system-settings-form.tsx
    • Added 'enableResponseInputRectifier' to the system settings form.
  • src/app/[locale]/settings/config/page.tsx
    • Added 'enableResponseInputRectifier' to the system settings page.
  • src/app/[locale]/settings/data/_components/log-cleanup-panel.tsx
    • Updated log cleanup panel to display soft-deleted purged count and vacuum status in success message.
  • src/app/[locale]/settings/providers/_components/batch-edit/analyze-batch-settings.ts
    • Added new file for analyzing batch provider settings, determining uniform, mixed, or empty values.
  • src/app/[locale]/settings/providers/_components/batch-edit/deep-equals.ts
    • Added new file for deep equality comparison, used in batch settings analysis.
  • src/app/[locale]/settings/providers/_components/batch-edit/mixed-value-indicator.tsx
    • Added new file for a mixed value indicator component in batch edit forms.
  • src/app/[locale]/settings/providers/_components/batch-edit/provider-batch-dialog.tsx
    • Added 'OptionsSection' to the provider batch dialog.
  • src/app/[locale]/settings/providers/_components/forms/provider-form/components/form-tab-nav.tsx
    • Refactored form tab navigation to support sub-tabs and filtering of tabs.
  • src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx
    • Updated provider form to support sub-tabs and integrated the new 'OptionsSection'.
  • src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx
    • Updated provider form context to support batch analysis and sub-tabs for navigation.
  • src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts
    • Added 'SubTabId' and 'NavTargetId' types for enhanced navigation.
  • src/app/[locale]/settings/providers/_components/forms/provider-form/sections/limits-section.tsx
    • Added mixed value indicators to limit cards in the limits section.
  • src/app/[locale]/settings/providers/_components/forms/provider-form/sections/network-section.tsx
    • Increased the maximum value for non-streaming total timeout to 1800 seconds.
  • src/app/[locale]/settings/providers/_components/forms/provider-form/sections/options-section.tsx
    • Added new file for the provider options section, including advanced settings and overrides.
  • src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx
    • Removed advanced settings, Codex/Anthropic/Gemini overrides, and active time sections, moving them to 'OptionsSection'.
  • src/app/[locale]/settings/providers/_components/provider-list-item.legacy.tsx
    • Updated provider list item to use 'PROVIDER_TIMEOUT_DEFAULTS' and display proxy enabled status.
  • src/app/[locale]/settings/providers/_components/provider-list.tsx
    • Wrapped provider list with 'TooltipProvider' for consistent tooltip behavior.
  • src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx
    • Displayed proxy enabled status and updated timeout summary to use default values.
  • src/app/api/admin/log-cleanup/manual/route.ts
    • Added 'softDeletedPurged' and 'vacuumPerformed' to log cleanup API response.
  • src/app/api/leaderboard/route.ts
    • Added 'includeUserModelStats' parameter and logic for user leaderboard, requiring admin access.
  • src/app/v1/_lib/proxy-handler.ts
    • Added 'normalizeResponseInput' for '/v1/responses' endpoint to handle non-array inputs.
  • src/app/v1/_lib/proxy/client-detector.ts
    • Added glob matching functionality for client patterns in User-Agent strings.
  • src/app/v1/_lib/proxy/format-mapper.ts
    • Clarified 'detectClientFormat' for Response API, noting input normalization happens later.
  • src/app/v1/_lib/proxy/forwarder.ts
    • Implemented streaming hedge (concurrent racing) logic to improve first-byte performance.
    • Updated client abort reason to 'client_abort' and blacklisted more outbound headers.
  • src/app/v1/_lib/proxy/provider-selector.ts
    • Updated 'providerSupportsModel' logic for model selection and removed explicit modelRedirects check from this stage.
  • src/app/v1/_lib/proxy/rate-limit-guard.ts
    • Modified rate limit checks to incorporate 'costResetAt' for accurate usage tracking.
  • src/app/v1/_lib/proxy/response-handler.ts
    • Added live chain deletion upon request finalization and updated deferred streaming finalization logic.
  • src/app/v1/_lib/proxy/response-input-rectifier.ts
    • Added new file for Response API input rectifier, normalizing non-array inputs.
  • src/app/v1/_lib/proxy/session.ts
    • Added live chain persistence and new reasons for provider chain items related to hedging and client aborts.
  • src/components/analytics/model-breakdown-column.tsx
    • Added new file for a reusable model breakdown column component.
  • src/components/form/client-restrictions-editor.test.tsx
    • Added tests for sub-client selection functionality in the client restrictions editor.
  • src/components/form/client-restrictions-editor.tsx
    • Added sub-client selection functionality to the client restrictions editor.
  • src/components/ui/chart.tsx
    • Fixed dataKey type in chart tooltip for better type safety.
  • src/drizzle/schema.ts
    • Added 'cost_reset_at' column to the 'users' table and updated timeout defaults for 'providers' table.
  • src/lib/auth.ts
    • Added opaque mode fallback for raw admin tokens, allowing programmatic API access.
  • src/lib/client-restrictions/client-presets.test.ts
    • Added tests for child selection helpers in client restriction presets.
  • src/lib/client-restrictions/client-presets.ts
    • Added child selection logic for client restriction presets.
  • src/lib/config/env.schema.ts
    • Added 'DASHBOARD_LOGS_POLL_INTERVAL_MS' environment variable for dashboard log polling.
  • src/lib/config/system-settings-cache.ts
    • Added 'enableResponseInputRectifier' to default system settings.
  • src/lib/dashboard/user-limit-usage-cache.test.ts
    • Added tests for the new user limit usage cache.
  • src/lib/dashboard/user-limit-usage-cache.ts
    • Added new file for a shared user limit usage cache.
  • src/lib/dashboard/user-usage-loader.test.ts
    • Added tests for the new user usage loader.
  • src/lib/dashboard/user-usage-loader.ts
    • Added new file for a sequential user usage loader.
  • src/lib/langfuse/trace-proxy-request.test.ts
    • Added tests for trace proxy request reason classification, including hedge events.
  • src/lib/langfuse/trace-proxy-request.ts
    • Added 'hedge_winner' to success reasons and 'client_abort' to error reasons for Langfuse tracing.
  • src/lib/log-cleanup/service.ts
    • Implemented purging of soft-deleted records and VACUUM ANALYZE for database space reclamation.
  • src/lib/rate-limit/lease-service.ts
    • Modified lease service to respect 'costResetAt' for accurate rate limiting.
  • src/lib/rate-limit/lease.ts
    • Added 'costResetAtMs' to the 'BudgetLease' interface.
  • src/lib/rate-limit/service.ts
    • Modified rate limit service to respect 'cost_reset_at' in cost limit checks.
  • src/lib/redis/cost-cache-cleanup.ts
    • Added new file for Redis cost cache cleanup utilities.
  • src/lib/redis/leaderboard-cache.ts
    • Updated leaderboard cache key to include 'includeModelStats' for user scope.
  • src/lib/redis/live-chain-store.test.ts
    • Added tests for live chain store phase inference.
  • src/lib/redis/live-chain-store.ts
    • Added new file for a Redis-based live chain store.
  • src/lib/security/api-key-auth-cache.ts
    • Added 'costResetAt' to the cached user payload.
  • src/lib/utils/cost-calculation.ts
    • Updated cost calculation to handle priority-aware long context rates for GPT models.
  • src/lib/utils/price-data.ts
    • Added priority long-context pricing fields to model price data.
  • src/lib/utils/pricing-resolution.ts
    • Added priority long-context pricing fields to pricing resolution logic.
  • src/lib/utils/provider-chain-display.test.ts
    • Added tests for 'shouldShowCostBadgeInCell' utility.
  • src/lib/utils/provider-chain-display.ts
    • Added new file for provider chain display utilities.
  • src/lib/utils/provider-chain-formatter.test.ts
    • Added tests for hedge and client abort reason handling in provider chain formatter.
  • src/lib/utils/provider-chain-formatter.ts
    • Updated provider chain formatter to handle hedge and client abort reasons, and improved retry counting.
  • src/lib/utils/provider-display.test.ts
    • Added tests for 'isProviderFinalized' utility.
  • src/lib/utils/provider-display.ts
    • Added new file for provider display utilities, including 'isProviderFinalized'.
  • src/lib/utils/special-settings.ts
    • Added 'response_input_rectifier' to special settings.
  • src/lib/validation/schemas.ts
    • Added 'enableResponseInputRectifier' to the system settings schema.
  • src/repository/_shared/transformers.test.ts
    • Updated provider timeout defaults in transformers test and added 'costResetAt' to user transformer.
  • src/repository/_shared/transformers.ts
    • Updated provider timeout defaults and added 'costResetAt' to user transformer.
  • src/repository/admin-user-insights.ts
    • Added new file for admin user insights repository functions.
  • src/repository/key.ts
    • Added 'costResetAt' to key validation logic.
  • src/repository/leaderboard.ts
    • Added 'UserModelStat' interface and 'includeModelStats' parameter to leaderboard queries.
  • src/repository/model-price.ts
    • Updated 'findAllLatestPricesPaginated' to order by 'updatedAt' DESC for better sorting.
  • src/repository/provider.ts
    • Updated provider creation to use 'PROVIDER_TIMEOUT_DEFAULTS' for consistent timeout settings.
  • src/repository/statistics.ts
    • Modified total cost calculation to respect 'resetAt' for accurate usage aggregation.
  • src/repository/system-config.ts
    • Added 'enableResponseInputRectifier' to system settings.
  • src/repository/usage-logs.ts
    • Added '_liveChain' to 'UsageLogRow' interface for real-time log updates.
  • src/repository/user.ts
    • Added 'costResetAt' to user queries and introduced 'resetUserCostResetAt' function.
  • src/types/message.ts
    • Added new reasons for provider chain items, including 'hedge_triggered', 'hedge_launched', 'hedge_winner', 'hedge_loser_cancelled', and 'client_abort'.
  • src/types/model-price.ts
    • Added priority long-context pricing fields to the model price data interface.
  • src/types/special-settings.ts
    • Added 'ResponseInputRectifierSpecialSetting' for auditing input normalization.
  • src/types/system-config.ts
    • Added 'enableResponseInputRectifier' to system configuration.
  • src/types/user.ts
    • Added 'costResetAt' to user interfaces.
  • tests/integration/batch-edit-prefill.test.ts
    • Added new file for integration tests of batch edit prefill logic.
  • tests/integration/billing-model-source.test.ts
    • Added test case for long-context priority pricing in billing model source.
  • tests/unit/actions/admin-user-insights.test.ts
    • Added new file for unit tests of admin user insights actions.
  • tests/unit/actions/key-quota-concurrent-inherit.test.ts
    • Updated mocks and added 'userCostResetAt' to key quota tests.
  • tests/unit/actions/key-quota-cost-reset.test.ts
    • Added new file for unit tests of key quota cost reset functionality.
  • tests/unit/actions/my-usage-concurrent-inherit.test.ts
    • Updated logger mocks.
  • tests/unit/actions/total-usage-semantics.test.ts
    • Removed source code verification tests.
  • tests/unit/actions/users-reset-all-statistics.test.ts
    • Updated mocks for 'db.transaction' and 'invalidateCachedUser' in user statistics reset tests.
  • tests/unit/actions/users-reset-limits-only.test.ts
    • Added new file for unit tests of user reset limits only functionality.
  • tests/unit/auth/admin-token-opaque-fallback.test.ts
    • Added new file for unit tests of admin token opaque fallback in authentication.
  • tests/unit/batch-edit/analyze-batch-settings.test.ts
    • Added new file for unit tests of batch edit settings analysis.
  • tests/unit/batch-edit/deep-equals.test.ts
    • Added new file for unit tests of deep equality comparison.
  • tests/unit/components/model-breakdown-column.test.tsx
    • Added new file for unit tests of the model breakdown column component.
  • tests/unit/lib/cost-calculation-breakdown.test.ts
    • Updated cost calculation tests for long-context priority pricing.
  • tests/unit/lib/cost-calculation-priority.test.ts
    • Added tests for long-context priority pricing in cost calculation.
  • tests/unit/lib/log-cleanup/service-count.test.ts
    • Updated log cleanup service count tests for soft-delete purge and vacuum.
  • tests/unit/lib/redis/cost-cache-cleanup.test.ts
    • Added new file for unit tests of Redis cost cache cleanup.
  • tests/unit/lib/redis/live-chain-store.test.ts
    • Added new file for unit tests of live chain store phase inference.
  • tests/unit/lib/security/api-key-auth-cache-reset-at.test.ts
    • Added new file for unit tests of API key auth cache reset-at handling.
  • tests/unit/lib/utils/provider-display.test.ts
    • Added new file for unit tests of provider display utilities.
  • tests/unit/proxy/client-detector.test.ts
    • Added glob matching tests for client detector.
  • tests/unit/proxy/extract-usage-metrics.test.ts
    • Added tests for extracting cached tokens from Chat Completions format.
  • tests/unit/proxy/provider-selector-cross-type-model.test.ts
    • Added new file for unit tests of provider selector cross-type model routing.
  • tests/unit/proxy/response-handler-live-chain.test.ts
    • Added new file for unit tests of response handler live chain updates.
  • tests/unit/proxy/response-handler-streaming-hedge.test.ts
    • Added new file for unit tests of response handler streaming hedge logic.
  • tests/unit/proxy/response-input-rectifier.test.ts
    • Added new file for unit tests of Response API input rectifier.
  • tests/unit/proxy/session-live-chain.test.ts
    • Added new file for unit tests of session live chain persistence.
  • tests/unit/repository/admin-user-insights.test.ts
    • Added new file for unit tests of admin user insights repository functions.
  • tests/unit/repository/leaderboard-model-stats.test.ts
    • Added new file for unit tests of leaderboard model statistics.
  • tests/unit/repository/model-price-pagination.test.ts
    • Added new file for unit tests of model price pagination.
  • tests/unit/repository/user-reset-cost-at.test.ts
    • Added new file for unit tests of user reset cost at functionality.
  • tests/unit/repository/user-total-cost-batch.test.ts
    • Added new file for unit tests of user total cost batch calculation.
  • tests/unit/repository/user-total-cost-reset-at.test.ts
    • Added new file for unit tests of user total cost with reset at.
  • tests/unit/utils/provider-chain-formatter-hedge.test.ts
    • Added new file for unit tests of provider chain formatter hedge logic.
Using Gemini Code Assist

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

Invoking Gemini

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

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

Customization

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

Limitations & Feedback

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

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

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

Comment on lines +3144 to +3158
launchedProviderIds.add(provider.id);
launchedProviderCount += 1;

let endpointSelection: {
endpointId: number | null;
baseUrl: string;
endpointUrl: string;
};
try {
endpointSelection = await ProxyForwarder.resolveStreamingHedgeEndpoint(session, provider);
} catch (endpointError) {
lastError = endpointError as Error;
await launchAlternative();
await finishIfExhausted();
return;
Copy link

Choose a reason for hiding this comment

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

launchedProviderCount inflated before endpoint resolution succeeds

launchedProviderIds and launchedProviderCount are both incremented at lines 3144–3145, before resolveStreamingHedgeEndpoint is awaited (line 3153). If that call throws (e.g. the vendor-type circuit is open or no endpoint candidates exist), the count is already 1 and the function immediately calls launchAlternative(), which will increment the count to 2.

Later, inside commitWinner, the winning alternative is classified using:

const isActualHedgeWin = launchedProviderCount > 1;

Because the count is 2 due to the failed endpoint-resolution step (not a real in-flight network race), the provider chain entry will be logged with reason: "hedge_winner" even though no concurrent race actually took place — it was a plain failover triggered by endpoint selection failure. This can make the audit trail misleading for operators reviewing the decision chain.

Consider only incrementing launchedProviderCount after successful endpoint resolution, or tracking a separate boolean anyRealAttemptLaunched to gate the isActualHedgeWin check:

launchedProviderIds.add(provider.id);

let endpointSelection: { ... };
try {
  endpointSelection = await ProxyForwarder.resolveStreamingHedgeEndpoint(session, provider);
} catch (endpointError) {
  lastError = endpointError as Error;
  await launchAlternative();
  await finishIfExhausted();
  return;
}

// Only count after a real network attempt is about to launch
launchedProviderCount += 1;
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/forwarder.ts
Line: 3144-3158

Comment:
**`launchedProviderCount` inflated before endpoint resolution succeeds**

`launchedProviderIds` and `launchedProviderCount` are both incremented at lines 3144–3145, *before* `resolveStreamingHedgeEndpoint` is awaited (line 3153). If that call throws (e.g. the vendor-type circuit is open or no endpoint candidates exist), the count is already 1 and the function immediately calls `launchAlternative()`, which will increment the count to 2.

Later, inside `commitWinner`, the winning alternative is classified using:
```typescript
const isActualHedgeWin = launchedProviderCount > 1;
```
Because the count is 2 due to the failed *endpoint-resolution* step (not a real in-flight network race), the provider chain entry will be logged with `reason: "hedge_winner"` even though no concurrent race actually took place — it was a plain failover triggered by endpoint selection failure. This can make the audit trail misleading for operators reviewing the decision chain.

Consider only incrementing `launchedProviderCount` *after* successful endpoint resolution, or tracking a separate boolean `anyRealAttemptLaunched` to gate the `isActualHedgeWin` check:

```typescript
launchedProviderIds.add(provider.id);

let endpointSelection: { ... };
try {
  endpointSelection = await ProxyForwarder.resolveStreamingHedgeEndpoint(session, provider);
} catch (endpointError) {
  lastError = endpointError as Error;
  await launchAlternative();
  await finishIfExhausted();
  return;
}

// Only count after a real network attempt is about to launch
launchedProviderCount += 1;
```

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

Comment on lines +3531 to +3538
const { done, value } = await reader.read();
if (done) {
controller.close();
return;
}
if (value && value.byteLength > 0) {
controller.enqueue(value);
}
Copy link

Choose a reason for hiding this comment

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

Zero-length chunk not enqueued and stream not closed

When reader.read() returns a chunk with value.byteLength === 0, neither controller.enqueue nor controller.close is called before pull returns. Per the ReadableStream spec, the runtime will call pull again when the consumer's queue is empty, triggering another await reader.read() round-trip.

In practice, AI streaming providers never emit zero-length chunks after the first valid byte, so this is unlikely to cause problems. However, if any upstream ever produces a burst of empty frames (e.g. keep-alive padding), this could result in a loop of no-op reads that delay real data delivery to the client without any observable error.

A defensive guard would make the intent explicit:

const { done, value } = await reader.read();
if (done) {
  controller.close();
  return;
}
if (value && value.byteLength > 0) {
  controller.enqueue(value);
}
// else: skip empty chunk; pull will be called again

The comment on the else branch would at least document that the silent skip is intentional.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/forwarder.ts
Line: 3531-3538

Comment:
**Zero-length chunk not enqueued and stream not closed**

When `reader.read()` returns a chunk with `value.byteLength === 0`, neither `controller.enqueue` nor `controller.close` is called before `pull` returns. Per the `ReadableStream` spec, the runtime will call `pull` again when the consumer's queue is empty, triggering another `await reader.read()` round-trip.

In practice, AI streaming providers never emit zero-length chunks after the first valid byte, so this is unlikely to cause problems. However, if any upstream ever produces a burst of empty frames (e.g. keep-alive padding), this could result in a loop of no-op reads that delay real data delivery to the client without any observable error.

A defensive guard would make the intent explicit:

```typescript
const { done, value } = await reader.read();
if (done) {
  controller.close();
  return;
}
if (value && value.byteLength > 0) {
  controller.enqueue(value);
}
// else: skip empty chunk; pull will be called again
```
The comment on the `else` branch would at least document that the silent skip is intentional.

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

Comment on lines 113 to 121
function providerSupportsModel(provider: Provider, requestedModel: string): boolean {
const isClaudeModel = requestedModel.startsWith("claude-");
const isClaudeProvider =
provider.providerType === "claude" || provider.providerType === "claude-auth";

// Case 1: Claude 模型请求
if (isClaudeModel) {
// 1a. Anthropic 提供商
if (isClaudeProvider) {
// 未设置 allowedModels 或为空数组:允许所有 claude 模型
if (!provider.allowedModels || provider.allowedModels.length === 0) {
return true;
}
// 检查白名单
return provider.allowedModels.includes(requestedModel);
}

// 1b. 非 Anthropic 提供商不支持 Claude 模型调度
return false;
}

// Case 2: 非 Claude 模型请求(gpt-*, gemini-*, 或其他任意模型)
// 2a. 优先检查显式声明(支持跨类型代理)
// 原因:允许 Claude 类型供应商通过 allowedModels/modelRedirects 声明支持非 Claude 模型
// 场景:Claude 供应商配置模型重定向,将 gemini-* 请求转发到真实的 Gemini 上游
const explicitlyDeclared = !!(
provider.allowedModels?.includes(requestedModel) || provider.modelRedirects?.[requestedModel]
);

if (explicitlyDeclared) {
return true; // 显式声明优先级最高,允许跨类型代理
}

// 2b. Anthropic 提供商不支持非声明的非 Claude 模型
// 保护机制:防止将非 Claude 模型误路由到 Anthropic API
if (isClaudeProvider) {
return false;
}

// 2c. 非 Anthropic 提供商(codex, gemini, gemini-cli, openai-compatible)
// allowedModels 是声明列表,用于调度器匹配提供商
// 用户可以手动填写任意模型名称(不限于真实模型),用于声明该提供商"支持"哪些模型

// 未设置 allowedModels 或为空数组:接受任意模型(由上游提供商判断)
// 1. 未设置 allowedModels(null 或空数组):接受任意模型
if (!provider.allowedModels || provider.allowedModels.length === 0) {
return true;
}

// 不在声明列表中且无重定向配置(前面已检查过 explicitlyDeclared)
return false;
// 2. 设置了 allowedModels:只按原始请求模型做白名单匹配
return provider.allowedModels.includes(requestedModel);
}
Copy link

Choose a reason for hiding this comment

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

modelRedirects no longer participates in provider routing

The previous implementation of providerSupportsModel treated a modelRedirects entry as an explicit declaration that the provider supports a given source model, so it was factored into the routing decision alongside allowedModels:

// Old code
const explicitlyDeclared = !!(
  provider.allowedModels?.includes(requestedModel) || provider.modelRedirects?.[requestedModel]
);
if (explicitlyDeclared) return true;

With the new simplified logic, modelRedirects is no longer consulted here. A provider that has:

  • allowedModels: ["gpt-4o"]
  • modelRedirects: { "gpt-3.5-turbo": "gpt-4o" }

…will now return false for a gpt-3.5-turbo request because the source model is absent from allowedModels. Requests that used to be silently redirected will fail to route at all until the operator explicitly adds the redirect source model to allowedModels.

This is noted in the PR's BREAKING CHANGE section, but it is a silent behavioral regression — no error is surfaced to users; requests simply fall through to a 503. Consider adding a debug-level log when a provider is skipped because of model mismatch and a modelRedirects entry exists for the requested model, to help operators detect misconfigured providers:

// After the allowedModels check fails:
if (provider.modelRedirects?.[requestedModel]) {
  logger.debug(
    "providerSupportsModel: model in modelRedirects but not in allowedModels — skipping provider",
    { providerId: provider.id, requestedModel }
  );
}
return false;
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/provider-selector.ts
Line: 113-121

Comment:
**`modelRedirects` no longer participates in provider routing**

The previous implementation of `providerSupportsModel` treated a `modelRedirects` entry as an explicit declaration that the provider supports a given source model, so it was factored into the routing decision alongside `allowedModels`:

```typescript
// Old code
const explicitlyDeclared = !!(
  provider.allowedModels?.includes(requestedModel) || provider.modelRedirects?.[requestedModel]
);
if (explicitlyDeclared) return true;
```

With the new simplified logic, `modelRedirects` is no longer consulted here. A provider that has:
- `allowedModels: ["gpt-4o"]`
- `modelRedirects: { "gpt-3.5-turbo": "gpt-4o" }`

…will now return `false` for a `gpt-3.5-turbo` request because the source model is absent from `allowedModels`. Requests that used to be silently redirected will fail to route at all until the operator explicitly adds the redirect source model to `allowedModels`.

This is noted in the PR's BREAKING CHANGE section, but it is a silent behavioral regression — no error is surfaced to users; requests simply fall through to a 503. Consider adding a debug-level log when a provider is skipped because of model mismatch *and* a `modelRedirects` entry exists for the requested model, to help operators detect misconfigured providers:

```typescript
// After the allowedModels check fails:
if (provider.modelRedirects?.[requestedModel]) {
  logger.debug(
    "providerSupportsModel: model in modelRedirects but not in allowedModels — skipping provider",
    { providerId: provider.id, requestedModel }
  );
}
return false;
```

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

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

这是一个重要的版本发布,引入了多项重大功能,例如供应商并发竞速(hedging)、用户洞察页面和限额重置,同时还包含了大量的 UI/UX 优化和错误修复。这些功能的实现看起来相当稳健且经过深思熟虑。proxy-forwarder.ts 中的并发竞速逻辑虽然复杂,但妥善处理了包括客户端中止在内的各种边缘情况。用于重置用户限额的 costResetAt 功能在不同的用量和配额计算模块中得到了一致的应用。提供商模型支持逻辑的简化,提高了代码的清晰度和正确性。为客户端限制添加的 glob 模式匹配也是一个实用的增强功能。总的来说,这是一个高质量的拉取请求,在各个方面都有实质性的改进。

Note: Security Review did not run due to the size of the PR.

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 introduces release v0.6.3 with Provider Hedge Racing, User Insights Dashboard, Soft User Limit Reset, Response Input Rectifier, and GPT Long-Context Pricing. The implementation is well-structured with comprehensive test coverage for all new features.

PR Size: XL

  • Lines changed: 26,378 (23,587 additions + 2,791 deletions)
  • Files changed: 213

Note: This is an extra-large PR. For future releases, consider splitting into smaller PRs by feature area:

  1. Core proxy changes (hedge racing, response rectifier) - ~8,000 lines
  2. User insights dashboard - ~4,000 lines
  3. User limit management (costResetAt) - ~3,000 lines
  4. UI improvements and i18n - ~6,000 lines
  5. Database migrations and schema changes - ~1,000 lines

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

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean (SQL injection protected via Drizzle parameterized queries, admin routes properly authorized, input validated)
  • Error handling - Clean (no empty catch blocks, errors logged or propagated)
  • Type safety - Clean (TypeScript used throughout, no unsafe any in critical paths)
  • Documentation accuracy - Clean (comments match implementation)
  • Test coverage - Adequate (tests exist for hedge racing, response rectifier, user insights, cost reset)
  • Code clarity - Good

Notable Implementation Quality

  1. Hedge Racing (src/app/v1/_lib/proxy/forwarder.ts): Well-implemented concurrent provider racing with proper cleanup of losing attempts and session synchronization.

  2. Response Input Rectifier (src/app/v1/_lib/proxy/response-input-rectifier.ts): Clean normalization of /v1/responses input formats with proper type guards.

  3. User Insights Authorization (src/app/[locale]/dashboard/leaderboard/user/[userId]/page.tsx): Proper admin-only access control with userId validation.

  4. SQL Safety (src/repository/admin-user-insights.ts): All queries use Drizzle ORM with parameterized statements, preventing SQL injection.

  5. Soft Reset Pattern (src/actions/users.ts): The costResetAt mechanism provides a clean way to reset limits without destroying historical data.


Automated review by Claude AI

> {
const session = await getSession();
if (!session || session.user.role !== "admin") {
return { ok: false, error: "Unauthorized" };
Copy link
Contributor

Choose a reason for hiding this comment

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

[HIGH] [STANDARD-VIOLATION] Hardcoded ActionResult errors bypass i18n

Evidence: src/actions/admin-user-insights.ts:40 contains return { ok: false, error: "Unauthorized" };; similar hardcoded user-facing strings exist at lines 45, 72, 78, 100, 103, 106, 127, 163.

Why this is a problem: CLAUDE.md requires: i18n Required - All user-facing strings must use i18n (5 languages supported). Never hardcode display text.

Suggested fix:

import { getTranslations } from "next-intl/server";
import { ERROR_CODES } from "@/lib/utils/error-messages";

const tError = await getTranslations("errors");

if (!session || session.user.role !== "admin") {
  return {
    ok: false,
    error: tError("UNAUTHORIZED"),
    errorCode: ERROR_CODES.UNAUTHORIZED,
  };
}

if (!isValidTimeRange(timeRange)) {
  return {
    ok: false,
    error: tError("INVALID_FORMAT", { field: "timeRange" }),
    errorCode: ERROR_CODES.INVALID_FORMAT,
    errorParams: { field: "timeRange" },
  };
}

export function UserOverviewCards({ userId }: UserOverviewCardsProps) {
const t = useTranslations("dashboard.leaderboard.userInsights");

const { data, isLoading } = useQuery({
Copy link
Contributor

Choose a reason for hiding this comment

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

[HIGH] [ERROR-NO-USER-FEEDBACK] React Query failures render as blank/"no data"

Evidence: src/app/[locale]/dashboard/leaderboard/user/[userId]/_components/user-overview-cards.tsx:23-47 throws on failure (if (!result.ok) throw new Error(result.error);) but then hides the error with if (!data) return null;.

Same pattern (throw, but no isError/error render path) in:

  • src/app/[locale]/dashboard/leaderboard/user/[userId]/_components/user-key-trend-chart.tsx:41-52 (falls through to noData)
  • src/app/[locale]/dashboard/leaderboard/user/[userId]/_components/user-model-breakdown.tsx:36-69 (falls through to noData)
  • src/app/[locale]/dashboard/leaderboard/user/[userId]/_components/user-provider-breakdown.tsx:36-69 (falls through to noData)

Why this is a problem: Network/session errors become indistinguishable from empty datasets, and the UI provides no actionable feedback.

Suggested fix:

const tError = useTranslations("errors");
const { data, isLoading, isError, error } = useQuery({
  // ...
});

if (isError) {
  return (
    <div
      role="alert"
      className="rounded-lg border border-destructive/50 bg-destructive/5 p-3 text-sm text-destructive"
    >
      {error instanceof Error ? error.message : tError("OPERATION_FAILED")}
    </div>
  );
}

usageCache.set(userId, { data: result.data, timestamp: Date.now() });
return result.data;
})
.catch(() => null)
Copy link
Contributor

Choose a reason for hiding this comment

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

[MEDIUM] [ERROR-SILENT] getSharedUserLimitUsage swallows request errors

Evidence: src/lib/dashboard/user-limit-usage-cache.ts:59 uses .catch(() => null).

Why this is a problem: Exceptions (network/server) become indistinguishable from legitimate "no data" results, and the root cause is lost (no logging, no propagation). This makes the UI fall back silently and complicates debugging.

Suggested fix:

const request = getUserAllLimitUsage(userId)
  .then((result) => {
    if (!result.ok || !result.data) {
      return null;
    }

    usageCache.set(userId, { data: result.data, timestamp: Date.now() });
    return result.data;
  })
  .catch((error) => {
    console.error("[user-limit-usage-cache] getUserAllLimitUsage failed", error);
    return null;
  })
  .finally(() => {
    inFlightUsageRequests.delete(userId);
  });

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 is XL and spans proxy core, dashboard UI, settings, and database changes, which makes regression risk high and review/rollback difficult. The most actionable issues are (1) i18n rule violations in new server actions and (2) user-facing failures being rendered as blank/"no data" states due to missing React Query error UI.

PR Size: XL

  • Lines changed: 26378
  • Files changed: 213
  • Split suggestions (recommended):
    1. Proxy pipeline changes (hedge + rectifiers + response/log handling)
    2. Dashboard leaderboard/user-insights UI + supporting actions/repositories
    3. User quota reset/costResetAt + related DB migration
    4. Provider/settings UI + i18n message updates

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 0 0 0
Security 0 0 0 0
Error Handling 0 1 1 0
Types 0 0 0 0
Comments/Docs 0 1 0 0
Tests 0 0 0 0
Simplification 0 0 0 0

Critical Issues (Must Fix)

  • None

High Priority Issues (Should Fix)

  • [STANDARD-VIOLATION] Hardcoded ActionResult errors bypass i18n: src/actions/admin-user-insights.ts:40 (and other lines noted inline)
  • [ERROR-NO-USER-FEEDBACK] User-insights widgets throw React Query errors but render blank/"no data": src/app/[locale]/dashboard/leaderboard/user/[userId]/_components/user-overview-cards.tsx:23 (and related components)
  • [ERROR-SILENT] Shared limit-usage cache swallows exceptions: src/lib/dashboard/user-limit-usage-cache.ts:59

Review Coverage

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

Automated review by Codex AI

Copy link
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.

  • Applied PR label: size/XL
  • Left 3 inline review comments at:
    • src/actions/admin-user-insights.ts:40
    • src/app/[locale]/dashboard/leaderboard/user/[userId]/_components/user-overview-cards.tsx:23
    • src/lib/dashboard/user-limit-usage-cache.ts:59
  • Submitted PR review summary (comment) with the following body:

Code Review Summary

This PR is XL and spans proxy core, dashboard UI, settings, and database changes, which makes regression risk high and review/rollback difficult. The most actionable issues are (1) i18n rule violations in new server actions and (2) user-facing failures being rendered as blank/"no data" states due to missing React Query error UI.

PR Size: XL

  • Lines changed: 26378
  • Files changed: 213
  • Split suggestions (recommended):
    1. Proxy pipeline changes (hedge + rectifiers + response/log handling)
    2. Dashboard leaderboard/user-insights UI + supporting actions/repositories
    3. User quota reset/costResetAt + related DB migration
    4. Provider/settings UI + i18n message updates

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 0 0 0
Security 0 0 0 0
Error Handling 0 1 1 0
Types 0 0 0 0
Comments/Docs 0 1 0 0
Tests 0 0 0 0
Simplification 0 0 0 0

Critical Issues (Must Fix)

  • None

High Priority Issues (Should Fix)

  • [STANDARD-VIOLATION] Hardcoded ActionResult errors bypass i18n: src/actions/admin-user-insights.ts:40 (and other lines noted inline)
  • [ERROR-NO-USER-FEEDBACK] User-insights widgets throw React Query errors but render blank/"no data": src/app/[locale]/dashboard/leaderboard/user/[userId]/_components/user-overview-cards.tsx:23 (and related components)
  • [ERROR-SILENT] Shared limit-usage cache swallows exceptions: src/lib/dashboard/user-limit-usage-cache.ts:59

Review Coverage

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

Automated review by Codex AI

When a hedge winner streams, commitWinner() already logs the provider
to the chain with reason "hedge_winner" and updates session binding.
The deferred finalization was redundantly calling updateSessionBindingSmart,
updateSessionProvider, and addProviderToChain again with reason "retry_success",
causing the same provider to appear twice in dashboard logs.

Add isHedgeWinner flag to DeferredStreamingFinalization so finalization
skips these operations when commitWinner() has already performed them.
@github-actions
Copy link
Contributor

🧪 测试结果

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

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

Comment on lines +2873 to +2878

const launchedProviderIds = new Set<number>();
let launchedProviderCount = 0;
let settled = false;
let winnerCommitted = false;
let noMoreProviders = false;
Copy link

Choose a reason for hiding this comment

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

settleFailure can leave resultPromise permanently unresolved

settleFailure sets settled = true synchronously, then awaits clearSessionProviderBinding. If that Redis call throws (e.g., Redis is unavailable), resolveResult?.({ error }) is never reached, so resultPromise never resolves and the entire hedge call hangs indefinitely.

const settleFailure = async (error: Error) => {
  if (settled) return;
  settled = true;
  await ProxyForwarder.clearSessionProviderBinding(session).catch(() => {});  // swallow to guarantee resolve
  resolveResult?.({ error });
};

The same silent-swallow pattern already used in abortAttempt (attempt.responseController?.abort(...) inside try/catch) should be applied here. Without it, any Redis outage during a hedge failure leaves the client connection open with no response.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/forwarder.ts
Line: 2873-2878

Comment:
**`settleFailure` can leave `resultPromise` permanently unresolved**

`settleFailure` sets `settled = true` synchronously, then `await`s `clearSessionProviderBinding`. If that Redis call throws (e.g., Redis is unavailable), `resolveResult?.({ error })` is never reached, so `resultPromise` never resolves and the entire hedge call hangs indefinitely.

```typescript
const settleFailure = async (error: Error) => {
  if (settled) return;
  settled = true;
  await ProxyForwarder.clearSessionProviderBinding(session).catch(() => {});  // swallow to guarantee resolve
  resolveResult?.({ error });
};
```

The same silent-swallow pattern already used in `abortAttempt` (`attempt.responseController?.abort(...)` inside `try/catch`) should be applied here. Without it, any Redis outage during a hedge failure leaves the client connection open with no response.

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

Comment on lines +270 to +284
logger.info({
action: "log_cleanup_soft_delete_purge",
deletedInBatch: deleted,
totalPurged,
});

if (deleted === batchSize) {
await sleep(BATCH_SLEEP_MS);
}
}

return totalPurged;
}

/**
Copy link

Choose a reason for hiding this comment

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

VACUUM ANALYZE from application code may conflict with autovacuum

Manually calling VACUUM ANALYZE message_request after each cleanup run is typically unnecessary in production PostgreSQL — autovacuum already reclaims dead tuples after bulk deletes. Triggering it from application code has several downsides:

  • It can conflict with a concurrently running autovacuum on the same table, causing one to back off.
  • While VACUUM doesn't hold an exclusive lock during the heap scan, it does briefly lock during the analysis phase, which can affect query planning on a busy table.
  • If Drizzle's connection pool wraps connections in a transaction (or if a middleware does), the call will throw ERROR: VACUUM cannot run inside a transaction block — this is already acknowledged in the comment, but the implicit reliance on db.execute() being outside a transaction is fragile.

Consider removing the runVacuum() call and instead adjusting autovacuum_vacuum_cost_limit / autovacuum_vacuum_scale_factor on the message_request table to make autovacuum respond more aggressively after large deletes. If manual vacuuming is truly needed, a separate out-of-band maintenance job is safer.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/log-cleanup/service.ts
Line: 270-284

Comment:
**`VACUUM ANALYZE` from application code may conflict with autovacuum**

Manually calling `VACUUM ANALYZE message_request` after each cleanup run is typically unnecessary in production PostgreSQL — autovacuum already reclaims dead tuples after bulk deletes. Triggering it from application code has several downsides:

- It can conflict with a concurrently running autovacuum on the same table, causing one to back off.
- While VACUUM doesn't hold an exclusive lock during the heap scan, it does briefly lock during the analysis phase, which can affect query planning on a busy table.
- If Drizzle's connection pool wraps connections in a transaction (or if a middleware does), the call will throw `ERROR: VACUUM cannot run inside a transaction block` — this is already acknowledged in the comment, but the implicit reliance on `db.execute()` being outside a transaction is fragile.

Consider removing the `runVacuum()` call and instead adjusting `autovacuum_vacuum_cost_limit` / `autovacuum_vacuum_scale_factor` on the `message_request` table to make autovacuum respond more aggressively after large deletes. If manual vacuuming is truly needed, a separate out-of-band maintenance job is safer.

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

Comment on lines +3480 to +3505
const exists = mergedProviderChain.some(
(existing) =>
existing.id === item.id &&
existing.timestamp === item.timestamp &&
existing.reason === item.reason &&
existing.attemptNumber === item.attemptNumber
);
if (!exists) {
mergedProviderChain.push(item);
}
}
targetState.providerChain = mergedProviderChain;
targetState.specialSettings = [...sourceState.specialSettings];
targetState.originalModelName = sourceState.originalModelName;
targetState.originalUrlPathname = sourceState.originalUrlPathname;
targetState.clearResponseTimeout = sourceRuntime.clearResponseTimeout;
targetState.responseController = sourceRuntime.responseController;
}

private static async clearSessionProviderBinding(session: ProxySession): Promise<void> {
if (!session.sessionId) return;
await SessionManager.clearSessionProvider(session.sessionId);
}

private static async readFirstReadableChunk(
reader: ReadableStreamDefaultReader<Uint8Array>
Copy link

Choose a reason for hiding this comment

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

Shadow session cloning relies on TypeScript-private fields being JS-enumerable

createStreamingShadowSession creates the shadow via:

const shadow = Object.assign(
  Object.create(Object.getPrototypeOf(session)) as ProxySession,
  session
);

This works because TypeScript's private keyword produces regular, enumerable JS properties — so Object.assign copies them. However, if ProxySession ever introduces a field using the native JS #privateField syntax (truly private, non-enumerable), that field would silently not be copied and the shadow would be in a broken state with no compile-time or runtime warning.

The existing code has private readonly endpointPolicy and private originalModelName which are safe today, but this coupling to the TS-private implementation detail is fragile and worth a note or a factory method on ProxySession itself (ProxySession.createShadow(source, provider)) that can be kept in sync when fields change.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/forwarder.ts
Line: 3480-3505

Comment:
**Shadow session cloning relies on TypeScript-private fields being JS-enumerable**

`createStreamingShadowSession` creates the shadow via:

```typescript
const shadow = Object.assign(
  Object.create(Object.getPrototypeOf(session)) as ProxySession,
  session
);
```

This works because TypeScript's `private` keyword produces regular, enumerable JS properties — so `Object.assign` copies them. However, if `ProxySession` ever introduces a field using the native JS `#privateField` syntax (truly private, non-enumerable), that field would silently not be copied and the shadow would be in a broken state with no compile-time or runtime warning.

The existing code has `private readonly endpointPolicy` and `private originalModelName` which are safe today, but this coupling to the TS-private implementation detail is fragile and worth a note or a factory method on `ProxySession` itself (`ProxySession.createShadow(source, provider)`) that can be kept in sync when fields change.

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

- Move launchedProviderCount increment after endpoint resolution to
  prevent inflated count on resolution failure (hedge_winner misclass)
- Add comment documenting intentional zero-length chunk skip
- Add error logging to user-limit-usage-cache instead of silent swallow
- Add React Query error states to 4 leaderboard user insight components
- Add loadError i18n key across all 5 locales
- Add trailing newlines to SQL migration files for POSIX compliance
- Add test for endpoint resolution failure not inflating hedge count
- Add test for cache error logging behavior
@github-actions
Copy link
Contributor

🧪 测试结果

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

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

Comment on lines +135 to +138
.innerJoin(providers, eq(usageLedger.finalProviderId, providers.id))
.where(and(...conditions))
.groupBy(providers.id, providers.name)
.orderBy(desc(sql`sum(${usageLedger.costUsd})`));
Copy link

Choose a reason for hiding this comment

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

INNER JOIN silently drops null-provider ledger rows

getUserProviderBreakdown uses an INNER JOIN against the providers table. Any usageLedger row where finalProviderId is NULL (e.g. requests that failed before a provider was selected, or legacy rows) is excluded from the query entirely.

This means the sum of all costs in the provider breakdown will be lower than the sum in getUserModelBreakdown for the same user and date range, creating a silent discrepancy on the User Insights dashboard that can confuse admins reviewing the numbers.

Consider using a LEFT JOIN here, grouping the null case under "Unknown provider", to keep the two breakdowns consistent:

.from(usageLedger)
.leftJoin(providers, eq(usageLedger.finalProviderId, providers.id))
// then handle null providers.id in the select / groupBy
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/repository/admin-user-insights.ts
Line: 135-138

Comment:
**INNER JOIN silently drops null-provider ledger rows**

`getUserProviderBreakdown` uses an `INNER JOIN` against the `providers` table. Any `usageLedger` row where `finalProviderId` is `NULL` (e.g. requests that failed before a provider was selected, or legacy rows) is excluded from the query entirely.

This means the sum of all costs in the provider breakdown will be *lower* than the sum in `getUserModelBreakdown` for the same user and date range, creating a silent discrepancy on the User Insights dashboard that can confuse admins reviewing the numbers.

Consider using a `LEFT JOIN` here, grouping the null case under "Unknown provider", to keep the two breakdowns consistent:

```typescript
.from(usageLedger)
.leftJoin(providers, eq(usageLedger.finalProviderId, providers.id))
// then handle null providers.id in the select / groupBy
```

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

Comment on lines +117 to +120
if (filters?.model) {
conditions.push(
sql`(${usageLedger.model} ILIKE ${filters.model} OR ${usageLedger.originalModel} ILIKE ${filters.model})`
);
Copy link

Choose a reason for hiding this comment

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

ILIKE pattern not sanitised — wildcards in model names will match unexpectedly

filters.model is interpolated directly as an ILIKE operand. PostgreSQL ILIKE treats % and _ as wildcards, so a model name that happens to contain either character (e.g. gpt-3.5-turbo contains no such chars, but user-supplied filter inputs might) would match a broader set of rows than intended.

While this is an admin-only endpoint, the broader concern is that the model filter stops being an exact match and starts behaving like a search, which can mislead the breakdown charts. Consider escaping the pattern before passing it to ILIKE:

const escapedModel = filters.model.replace(/[%_\\]/g, "\\$&");
conditions.push(
  sql`(${usageLedger.model} ILIKE ${escapedModel} ESCAPE '\\' OR ${usageLedger.originalModel} ILIKE ${escapedModel} ESCAPE '\\')`
);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/repository/admin-user-insights.ts
Line: 117-120

Comment:
**ILIKE pattern not sanitised — wildcards in model names will match unexpectedly**

`filters.model` is interpolated directly as an ILIKE operand. PostgreSQL ILIKE treats `%` and `_` as wildcards, so a model name that happens to contain either character (e.g. `gpt-3.5-turbo` contains no such chars, but user-supplied filter inputs might) would match a broader set of rows than intended.

While this is an admin-only endpoint, the broader concern is that the model filter stops being an exact match and starts behaving like a search, which can mislead the breakdown charts. Consider escaping the pattern before passing it to ILIKE:

```typescript
const escapedModel = filters.model.replace(/[%_\\]/g, "\\$&");
conditions.push(
  sql`(${usageLedger.model} ILIKE ${escapedModel} ESCAPE '\\' OR ${usageLedger.originalModel} ILIKE ${escapedModel} ESCAPE '\\')`
);
```

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

…n logs table

The logs table computed finalProvider as the last entry in the provider
chain, which returns the hedge loser (cancelled provider) instead of
the actual winner. Add getFinalProviderName() utility with 3-tier
priority: hedge_winner > last successful request > last entry fallback.

Replace inline chain[length-1].name logic in both virtualized-logs-table
and usage-logs-table with the new utility.
@github-actions
Copy link
Contributor

🧪 测试结果

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

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

Comment on lines +2992 to +3010

await launchingAlternative;
};

const handleAttemptFailure = async (attempt: StreamingHedgeAttempt, error: Error) => {
if (settled || winnerCommitted || attempt.settled) return;

attempt.settled = true;
if (attempt.thresholdTimer) {
clearTimeout(attempt.thresholdTimer);
attempt.thresholdTimer = null;
}
attempts.delete(attempt);
lastError = error;

const errorCategory = await categorizeErrorAsync(error);
const statusCode = error instanceof ProxyError ? error.statusCode : undefined;

if (attempt.endpointAudit.endpointId != null) {
Copy link

Choose a reason for hiding this comment

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

Stale chain entry added for loser after async gap

handleAttemptFailure sets attempt.settled = true and removes the attempt from the set, then awaits categorizeErrorAsync. During this async gap another attempt can become the winner (commitWinner sets winnerCommitted = true and calls abortAllAttempts). Because abortAttempt skips already-settled attempts, the loser is not aborted again — but when handleAttemptFailure resumes it still executes session.addProviderToChain(attempt.provider, { reason: "retry_failed" ... }) without re-checking winnerCommitted. This appends a misleading "retry_failed" chain entry for an attempt that was effectively a hedge loser, after the winner entry has already been written.

Add a guard immediately after the async gap:

const errorCategory = await categorizeErrorAsync(error);
const statusCode = error instanceof ProxyError ? error.statusCode : undefined;

if (settled || winnerCommitted) return; // winner committed during async gap
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/forwarder.ts
Line: 2992-3010

Comment:
**Stale chain entry added for loser after async gap**

`handleAttemptFailure` sets `attempt.settled = true` and removes the attempt from the set, then `await`s `categorizeErrorAsync`. During this async gap another attempt can become the winner (`commitWinner` sets `winnerCommitted = true` and calls `abortAllAttempts`). Because `abortAttempt` skips already-settled attempts, the loser is not aborted again — but when `handleAttemptFailure` resumes it still executes `session.addProviderToChain(attempt.provider, { reason: "retry_failed" ... })` without re-checking `winnerCommitted`. This appends a misleading `"retry_failed"` chain entry for an attempt that was effectively a hedge loser, after the winner entry has already been written.

Add a guard immediately after the async gap:

```typescript
const errorCategory = await categorizeErrorAsync(error);
const statusCode = error instanceof ProxyError ? error.statusCode : undefined;

if (settled || winnerCommitted) return; // winner committed during async gap
```

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

Comment on lines +2948 to +2975
if (launchingAlternative) {
await launchingAlternative;
return;
}

launchingAlternative = (async () => {
const alternativeProvider = await ProxyForwarder.selectAlternative(
session,
Array.from(launchedProviderIds)
);
if (!alternativeProvider) {
noMoreProviders = true;
// No alternative providers available — let in-flight attempt(s) continue.
// If all attempts already completed, settle with last error.
if (attempts.size === 0) {
await finishIfExhausted();
}
return;
}

await startAttempt(alternativeProvider, false);
})()
.catch(async (error) => {
const normalizedError = error instanceof Error ? error : new Error(String(error));

logger.error("ProxyForwarder: Hedge failed to launch alternative provider", {
error: normalizedError,
sessionId: session.sessionId ?? null,
Copy link

Choose a reason for hiding this comment

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

launchAlternative rejection leaks to concurrent waiters

The IIFE assigned to launchingAlternative calls ProxyForwarder.selectAlternative and startAttempt. If either throws an unexpected exception the resulting rejected promise is stored in launchingAlternative. Any concurrent call that already passed the if (launchingAlternative) guard and is awaiting launchingAlternative will receive that rejection. It then propagates out of launchAlternative(), causing the await launchAlternative() line in handleAttemptFailure to throw — and finishIfExhausted() is never reached. If noMoreProviders is true and all attempts have already been deleted from the set at that point, resultPromise is permanently unresolved and the client connection hangs indefinitely.

Guarding the call-site in handleAttemptFailure is the safest fix:

await launchAlternative().catch((err) => {
  logger.warn("ProxyForwarder: launchAlternative threw unexpectedly in handleAttemptFailure", { error: err });
});
await finishIfExhausted();
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/forwarder.ts
Line: 2948-2975

Comment:
**`launchAlternative` rejection leaks to concurrent waiters**

The IIFE assigned to `launchingAlternative` calls `ProxyForwarder.selectAlternative` and `startAttempt`. If either throws an unexpected exception the resulting rejected promise is stored in `launchingAlternative`. Any concurrent call that already passed the `if (launchingAlternative)` guard and is `await`ing `launchingAlternative` will receive that rejection. It then propagates out of `launchAlternative()`, causing the `await launchAlternative()` line in `handleAttemptFailure` to throw — and `finishIfExhausted()` is never reached. If `noMoreProviders` is true and all attempts have already been deleted from the set at that point, `resultPromise` is permanently unresolved and the client connection hangs indefinitely.

Guarding the call-site in `handleAttemptFailure` is the safest fix:

```typescript
await launchAlternative().catch((err) => {
  logger.warn("ProxyForwarder: launchAlternative threw unexpectedly in handleAttemptFailure", { error: err });
});
await finishIfExhausted();
```

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

Comment on lines +265 to 280
);
}
}
} catch (error) {
logger.error(
"[RateLimit] Daily rolling window query failed, fallback to database:",
error
);
return await RateLimitService.checkCostLimitsFromDatabase(id, type, costLimits);
return await RateLimitService.checkCostLimitsFromDatabase(
id,
type,
costLimits,
limits.cost_reset_at
);
}
} else {
Copy link

Choose a reason for hiding this comment

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

Redis cache hits for period-based limits ignore costResetAt

When Redis holds a cached cost value for the 5h, daily (rolling), or fixed-period windows the code reads it with a plain parseFloat(value) and never applies the costResetAt clip. The costResetAt filter only applies on the database fallback path (checkCostLimitsFromDatabase). Correctness therefore depends entirely on clearUserCostCache having successfully purged all period keys at reset time.

resetUserLimitsOnly wraps the Redis cleanup in a try/catch that continues on failure (by design — "Continue execution - costResetAt already set in DB"). If Redis is temporarily unavailable during a soft-reset, subsequent requests that hit the warm Redis cache will see the pre-reset accumulated cost and can be incorrectly rate-limited (or under-charged) until the cache key TTL expires. Consider logging a warning that enforcement may be stale when the Redis cleanup fails, so operators can manually trigger a flush if needed.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/rate-limit/service.ts
Line: 265-280

Comment:
**Redis cache hits for period-based limits ignore `costResetAt`**

When Redis holds a cached cost value for the 5h, daily (rolling), or fixed-period windows the code reads it with a plain `parseFloat(value)` and never applies the `costResetAt` clip. The `costResetAt` filter only applies on the database fallback path (`checkCostLimitsFromDatabase`). Correctness therefore depends entirely on `clearUserCostCache` having successfully purged all period keys at reset time.

`resetUserLimitsOnly` wraps the Redis cleanup in a `try/catch` that continues on failure (by design — "Continue execution - costResetAt already set in DB"). If Redis is temporarily unavailable during a soft-reset, subsequent requests that hit the warm Redis cache will see the pre-reset accumulated cost and can be incorrectly rate-limited (or under-charged) until the cache key TTL expires. Consider logging a warning that enforcement may be stale when the Redis cleanup fails, so operators can manually trigger a flush if needed.

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

Add missing i18n keys referenced in batch edit UI components:
- fields.isEnabled: noChange/enable/disable options for batch dropdown
- affectedProviders: title and overflow text for provider summaries
- batchNotes: codexOnly/claudeOnly/geminiOnly badge labels
- undo: singleEditSuccess/singleEditUndone/singleDeleteSuccess/singleDeleteUndone toasts
@github-actions
Copy link
Contributor

🧪 测试结果

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

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

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

Labels

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

Projects

Status: Done

2 participants