Skip to content

供应商榜单支持展开模型明细#852

Open
tesgth032 wants to merge 9 commits intoding113:devfrom
tesgth032:feat/provider-model-breakdown-table
Open

供应商榜单支持展开模型明细#852
tesgth032 wants to merge 9 commits intoding113:devfrom
tesgth032:feat/provider-model-breakdown-table

Conversation

@tesgth032
Copy link
Contributor

@tesgth032 tesgth032 commented Mar 1, 2026

Summary

供应商榜单(Provider Leaderboard)新增模型级明细展开功能。用户现在可以在供应商排行和缓存命中率排行中展开查看每个供应商下的模型拆分数据。

This PR adds model-level breakdown expansion to the Provider Leaderboard. Users can now expand each provider to view per-model statistics in both the provider consumption ranking and cache hit rate ranking.


Changes

UI Enhancements

  • 表内子行展示 (In-table sub-row display): 模型明细改为表内子行展示,复用原表头列,以灰色背景区分,视觉体验更加统一
  • 展开交互优化 (Expand interaction): LeaderboardTable 组件重构为支持 getSubRowsgetSubRowKey 属性,取代原来的 renderExpandedContent 方式

API Changes

  • 新增 includeModelStats 参数 (New query parameter): GET /api/leaderboard?scope=provider 支持 includeModelStats=1 参数,返回供应商下各模型的拆分数据
  • Redis 缓存键更新 (Redis cache key): includeModelStats 开关纳入 Redis 缓存键,确保不同参数请求返回正确缓存

Repository Layer

  • 新增 ModelProviderStat 类型 (New type): 定义供应商消耗排行的模型级统计结构
  • 聚合查询优化 (Aggregation query): 启用 includeModelStats 时,通过 SQL 聚合查询 provider+model 级别的指标数据

Testing

新增单元测试

  • tests/unit/api/leaderboard-route.test.ts - 验证 includeModelStats 参数传递及响应格式化
  • tests/unit/dashboard/leaderboard-table-expandable-rows.test.tsx - 验证 LeaderboardTable 子行渲染逻辑
  • tests/unit/repository/leaderboard-provider-metrics.test.ts - 验证供应商指标计算及 modelStats 聚合

手动验证清单

  • 供应商排行页面展开/折叠功能正常
  • 缓存命中率排行展开/折叠功能正常
  • 子行数据(请求数、成本、Token 等)计算正确
  • Redis 缓存键包含 includeModelStats 标识
  • 无模型数据的供应商不显示展开按钮

API 文档

GET /api/leaderboard?scope=provider

新增参数:

参数 类型 说明
includeModelStats 1 | true | yes 是否返回供应商下各模型的拆分数据

响应示例:

[
  {
    "providerId": 1,
    "providerName": "test-provider",
    "totalRequests": 10,
    "totalCost": 1.5,
    "totalCostFormatted": "$1.50",
    "totalTokens": 1000,
    "successRate": 1,
    "avgTtfbMs": 100,
    "avgTokensPerSecond": 20,
    "avgCostPerRequest": 0.15,
    "avgCostPerRequestFormatted": "$0.15",
    "avgCostPerMillionTokens": 1500,
    "avgCostPerMillionTokensFormatted": "$1,500.00",
    "modelStats": [
      {
        "model": "model-a",
        "totalRequests": 6,
        "totalCost": 1.0,
        "totalCostFormatted": "$1.00",
        "totalTokens": 600,
        "successRate": 1,
        "avgTtfbMs": 110,
        "avgTokensPerSecond": 25,
        "avgCostPerRequest": 0.1667,
        "avgCostPerRequestFormatted": "$0.17",
        "avgCostPerMillionTokens": 1666.7,
        "avgCostPerMillionTokensFormatted": "$1,666.70"
      }
    ]
  }
]

Checklist

  • 代码符合项目规范 (Code follows project conventions)
  • 自测完成 (Self-review completed)
  • 单元测试通过 (Unit tests passing)
  • 类型检查通过 (TypeScript typecheck passing)
  • Lint 检查通过 (Lint checks passing)
  • 构建成功 (Build successful)

Description enhanced by Claude AI

Greptile Summary

This PR adds model-level breakdown expansion to provider leaderboards. Users can expand provider rows to view per-model statistics for consumption and cache hit rate rankings.

Key Changes

  • Expandable table component: Refactored LeaderboardTable to support sub-row rendering via getSubRows and getSubRowKey props
  • API parameter: Added includeModelStats query parameter for GET /api/leaderboard?scope=provider
  • Repository queries: Provider leaderboard conditionally executes a second SQL aggregation for model breakdown; cache hit rate leaderboard unconditionally includes model stats
  • Redis cache keys: Updated to include includeModelStats flag for correct cache differentiation
  • Test coverage: Comprehensive unit tests for API, UI, and repository layers

Issues Addressed in Previous Threads

Many potential issues were already identified in previous review threads and should be considered before merge. Notable concerns include performance implications of the unconditional cache hit rate model query, lack of configurability for includeModelStats in the UI, and missing error handling for model stats query failures.

Confidence Score: 3/5

  • This PR is moderately safe to merge with several performance and architecture concerns that should be addressed
  • Score reflects solid implementation with comprehensive tests, but previous review threads identified significant performance and design issues that remain unaddressed. The unconditional secondary query for cache hit rate leaderboard and lack of model stats pagination could impact production performance.
  • Pay close attention to src/repository/leaderboard.ts (unconditional model query performance) and src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx (hardcoded includeModelStats with no UI control)

Important Files Changed

Filename Overview
src/app/[locale]/dashboard/leaderboard/_components/leaderboard-table.tsx Added expandable row support with getSubRows and getSubRowKey props. Sub-rows render inline with gray background. Added isSubRow parameter to cell renderers for semantic clarity.
src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx Integrated expandable rows for provider and cache hit rate tables. Hardcoded includeModelStats=1 for provider scope with no UI toggle. Column definitions handle both parent and sub-row rendering.
src/app/api/leaderboard/route.ts Added includeModelStats parameter parsing for provider scope. Formats nested modelStats arrays with currency fields. Type assertions assume repository schema without runtime validation.
src/repository/leaderboard.ts Added model-level breakdown queries for provider and cache hit rate leaderboards. Provider leaderboard conditionally includes model stats. Cache hit rate leaderboard unconditionally runs secondary model query on every request.

Last reviewed commit: 8bf905a

@coderabbitai
Copy link

coderabbitai bot commented Mar 1, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

将表格展开 API 从单一渲染回调替换为基于子行的 getSubRows/getSubRowKey,在 provider 路径新增查询参数 includeModelStats 并将其贯穿 API、缓存与仓库以返回并格式化 per-model 聚合 modelStats;新增相应单元测试并调整缓存键构建与失效签名。

Changes

Cohort / File(s) Summary
表格组件 - 子行 API
src/app/[locale]/dashboard/leaderboard/_components/leaderboard-table.tsx
移除 renderExpandedContent,新增泛型 TParent,TSub,新增 getSubRowsgetSubRowKey,将展开渲染改为插入子行;添加 useEffect 在无稳定 getRowKey 时清理 expanded 状态。
视图层 - 范围分支与子行使用
src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx
引入混合行类型(provider/model 等),重构为按 scope 的 renderTable 分支;provider 与 providerCacheHitRate 分支传入 getSubRows/getSubRowKey 来显示 model 子行;移除原有内联 expanded-model-stats 渲染。
API 路由 - includeModelStats 透传
src/app/api/leaderboard/route.ts
新增查询参数 includeModelStats(支持 true/yes/1),仅在 scope=provider 时将该标志传给 getLeaderboardWithCache,响应在启用时包含已格式化的 modelStats
缓存层 - 键与查询传播
src/lib/redis/leaderboard-cache.ts
LeaderboardFilters 中加入 includeModelStats?;缓存键构建增加 includeModelStats 后缀以区分不同结果;在 provider 查询路径将该标志传递给对应的数据库查询函数;invalidateLeaderboardCache 签名调整以接收 dateRangefilters
仓库层 - 模型级聚合
src/repository/leaderboard.ts
新增 ModelProviderStat 类型并在 ProviderLeaderboardEntry 上增加可选 modelStats? 字段;多处 provider 查询函数及核心聚合函数新增 includeModelStats? 参数,在为 true 时按 provider+model 聚合并生成 modelStats;增加 clampRatio01 并调整若干度量计算。
测试 - 覆盖新功能与交互
tests/unit/api/leaderboard-route.test.ts, tests/unit/dashboard/leaderboard-table-expandable-rows.test.tsx, tests/unit/repository/leaderboard-provider-metrics.test.ts
新增测试:验证 includeModelStats 在路由透传与响应中包含格式化 modelStats;验证 LeaderboardTable 的子行展开行为与样式;验证仓库聚合产生正确的 modelStats(过滤/排序/内容)。

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed PR标题为中文"供应商榜单支持展开模型明细",清晰准确地总结了该PR的核心变更:为供应商榜单添加展开模型明细的功能。标题与PR内容高度相关,反映了主要改动点。
Description check ✅ Passed PR description is comprehensive and directly related to the changeset, providing clear details about model-level breakdown expansion for Provider Leaderboard.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

❤️ Share

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

@gemini-code-assist
Copy link
Contributor

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!

此拉取请求旨在增强排行榜功能,允许用户查看供应商级别数据的模型明细。通过引入新的API参数和前端组件的重构,现在可以以可展开的子行形式在排行榜中直接展示每个供应商的模型统计数据,从而提供更细致的性能洞察,并优化了用户界面体验。

Highlights

  • 供应商榜单模型明细: 供应商缓存命中率排行中,展开模型明细现在以表内子行形式展示,并复用原表头列,通过灰色背景进行区分。
  • 供应商排行模型拆分数据: 供应商排行现在支持展开查看各供应商的模型拆分数据,通过新增 includeModelStats 参数实现。
Changelog
  • src/app/[locale]/dashboard/leaderboard/_components/leaderboard-table.tsx
    • renderExpandedContent 属性替换为 getSubRowsgetSubRowKey,以支持内联子行。
    • 修改了表格渲染逻辑,将子行直接显示在表格结构中,取代了之前的展开内容块。
  • src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx
    • 导入了 ModelProviderStat 类型。
    • 定义了新的 ProviderRowProviderCacheHitRateRow 类型以适应模型统计数据。
    • scope 为 "provider" 时,向 API 调用添加了 includeModelStats=1 参数。
    • 更新了供应商和供应商缓存命中率表格的列定义,以处理主行和子行(模型统计数据)。
    • 将表格渲染重构为 renderTable 函数,并向 LeaderboardTable 传递 getSubRowsgetSubRowKey 属性。
    • 移除了旧的 renderExpandedContent 逻辑。
  • src/app/api/leaderboard/route.ts
    • 在 API 文档中添加了 includeModelStats 参数说明。
    • 解析了 includeModelStats 查询参数。
    • includeModelStats 传递给 getLeaderboardWithCache 函数。
    • 为 API 响应格式化,向 ProviderLeaderboardEntry 类型添加了 modelStats 属性。
    • 实现了当 includeModelStats 为 true 时,对 modelStats 进行货币格式化的逻辑。
    • includeModelStats 包含在缓存键中。
  • src/lib/redis/leaderboard-cache.ts
    • LeaderboardFilters 接口添加了 includeModelStats 属性。
    • 在供应商范围的缓存键生成中包含了 includeModelStats
    • 修改了 queryDatabase,将 includeModelStats 传递给供应商排行榜函数。
  • src/repository/leaderboard.ts
    • 定义了 ModelProviderStat 接口,用于模型级别的统计数据。
    • ProviderLeaderboardEntry 接口添加了可选的 modelStats ( ModelProviderStat[] )。
    • 更新了 findDailyProviderLeaderboardfindMonthlyProviderLeaderboardfindWeeklyProviderLeaderboardfindAllTimeProviderLeaderboardfindCustomRangeProviderLeaderboard,使其接受 includeModelStats 参数。
    • 修改了 findProviderLeaderboardWithTimezone,以便在 includeModelStats 为 true 时有条件地获取和聚合模型级别的统计数据。
    • 实现了 SQL 查询,以检索模型级别数据并将其与主供应商条目关联。
  • tests/unit/api/leaderboard-route.test.ts
    • 新增测试用例,验证 includeModelStats 是否正确传递给缓存,以及 API 响应中供应商 modelStats 条目是否正确格式化。
  • tests/unit/dashboard/leaderboard-table-expandable-rows.test.tsx
    • 新增测试文件,用于 LeaderboardTable,验证可展开子行的功能,包括渲染和切换行为。
  • tests/unit/repository/leaderboard-provider-metrics.test.ts
    • 新增 "Provider Leaderboard Model Breakdown" 测试套件,验证当 includeModelStats 为 true 时,modelStats 是否正确包含且空模型名称被排除。
Activity
  • 此拉取请求引入了新的功能,并对现有组件进行了重构。
  • 新增了测试文件和测试用例,以覆盖新功能。
  • 更改涉及前端(React 组件)和后端(API 路由、数据仓库、缓存)的修改。
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.

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

本次 PR 实现了供应商榜单对模型明细的展开支持,这是一个很棒的功能增强。主要改动包括:

  1. 重构了 LeaderboardTable 组件,用更通用的 getSubRows 替代了 renderExpandedContent,以支持内联子行展示,这使得组件更加灵活和类型安全。
  2. 更新了后端 API (/api/leaderboard) 和数据仓库层 (repository),增加了 includeModelStats 参数来按需查询和返回供应商下各模型的细分数据。
  3. 相应地更新了前端视图 LeaderboardView 来使用新的表格功能,并调整了列定义以兼容父行和子行(模型行)的数据结构。
  4. 添加了相应的单元测试来覆盖新的后端逻辑和前端组件功能。

代码整体结构清晰,实现方式合理。我提出了一些关于类型安全和代码复用性的改进建议,希望能帮助您在完成功能时进一步提升代码质量。

Comment on lines 289 to 294
cell: (row) => {
const r = row as ProviderEntry;
if (r.avgCostPerRequest == null) return "-";
return r.avgCostPerRequestFormatted ?? r.avgCostPerRequest.toFixed(4);
if (row.avgCostPerRequest == null) return "-";
const formatted = (row as { avgCostPerRequestFormatted?: string | null })
.avgCostPerRequestFormatted;
return formatted ?? row.avgCostPerRequest.toFixed(4);
},
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

此处以及其他几处 cell 定义中使用了类型断言(如 row as { avgCostPerRequestFormatted?: string | null })来访问格式化后的字段。这通常表明前端的类型定义与 API 的实际返回数据结构存在差异。

为了提高类型安全性和代码可读性,建议在组件内部为模型统计数据定义一个客户端专用的类型,该类型继承自 ModelProviderStat 并包含由 API 添加的 ...Formatted 字段。这样可以避免在多处进行类型断言,使代码更清晰且更易于维护。

例如,您可以这样定义:

type ModelProviderStatClient = ModelProviderStat & {
  totalCostFormatted?: string;
  avgCostPerRequestFormatted?: string | null;
  avgCostPerMillionTokensFormatted?: string | null;
};

type ProviderRow = ProviderEntry | ModelProviderStatClient;

然后在 LeaderboardTablegetSubRows prop 中进行一次类型转换,之后所有 cell 函数都可以直接、安全地访问这些属性,无需再次断言。

Comment on lines +522 to +557
const modelRows = await db
.select({
providerId: usageLedger.finalProviderId,
model: modelField,
totalRequests: sql<number>`count(*)::double precision`,
totalCost: sql<string>`COALESCE(sum(${usageLedger.costUsd}), 0)`,
totalTokens: sql<number>`COALESCE(
sum(
${usageLedger.inputTokens} +
${usageLedger.outputTokens} +
COALESCE(${usageLedger.cacheCreationInputTokens}, 0) +
COALESCE(${usageLedger.cacheReadInputTokens}, 0)
)::double precision,
0::double precision
)`,
successRate: sql<number>`COALESCE(
count(CASE WHEN ${usageLedger.isSuccess} THEN 1 END)::double precision
/ NULLIF(count(*)::double precision, 0),
0::double precision
)`,
avgTtfbMs: sql<number>`COALESCE(avg(${usageLedger.ttfbMs})::double precision, 0::double precision)`,
avgTokensPerSecond: sql<number>`COALESCE(
avg(
CASE
WHEN ${usageLedger.outputTokens} > 0
AND ${usageLedger.durationMs} IS NOT NULL
AND ${usageLedger.ttfbMs} IS NOT NULL
AND ${usageLedger.ttfbMs} < ${usageLedger.durationMs}
AND (${usageLedger.durationMs} - ${usageLedger.ttfbMs}) >= 100
THEN (${usageLedger.outputTokens}::double precision)
/ ((${usageLedger.durationMs} - ${usageLedger.ttfbMs}) / 1000.0)
END
)::double precision,
0::double precision
)`,
})
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

此处的模型分解查询(modelRows)与前面的供应商级别查询(rankings)中,用于计算 totalTokenssuccessRateavgTtfbMsavgTokensPerSecond 的 SQL 表达式存在大量重复。

这种代码重复会增加维护成本,当指标计算逻辑需要更新时,必须在两个地方同步修改,容易出错。为了遵循 DRY (Don't Repeat Yourself) 原则,建议将这些共享的 SQL 片段提取为常量。

例如:

const totalTokensExpr = sql<number>`COALESCE(sum(...)::double precision, 0::double precision)`;
const successRateExpr = sql<number>`COALESCE(count(...) / NULLIF(count(*), 0), 0)`;
// ... 其他指标

// 在两个查询中复用这些常量
const rankings = await db.select({
  // ...
  totalTokens: totalTokensExpr,
  successRate: successRateExpr,
  // ...
});

const modelRows = await db.select({
  // ...
  totalTokens: totalTokensExpr,
  successRate: successRateExpr,
  // ...
});

这样做可以确保指标计算逻辑的一致性,并使代码更易于维护。

@github-actions github-actions bot added the size/M Medium PR (< 500 lines) label Mar 1, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
src/app/api/leaderboard/route.ts (1)

1-12: ⚠️ Potential issue | 🟠 Major

迁移至 Hono 框架以符合编码指南要求。

根据验证,项目中 19 个 API 路由都使用标准 Next.js App Router 模式,当前文件与其他路由保持一致。然而,编码指南明确要求所有 API 路由应使用 Hono 框架。这是一个系统性问题,影响整个 API 层。建议将此文件及其他相关 API 路由迁移至 Hono 框架以符合项目编码指南。

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

In `@src/app/api/leaderboard/route.ts` around lines 1 - 12, The route currently
uses Next.js App Router imports (NextRequest/NextResponse) and must be migrated
to Hono: replace the exported Next.js handler with a Hono app instance (e.g.,
create new Hono(), register a GET handler) and use the Hono
Context/request/response APIs instead of NextRequest/NextResponse; wire in
existing helpers (getSession, logger, getLeaderboardWithCache,
getSystemSettings, formatCurrency) inside the Hono handler, adapt getSession to
accept the Hono context or extract auth from ctx.req headers/cookies, and return
results with ctx.json/ctx.text response methods; ensure you export the Hono app
as the default export so the route is served by Hono and update any typing to
use Hono-compatible types while keeping references to symbols like
getLeaderboardWithCache, getSystemSettings, formatCurrency, and getSession.
🧹 Nitpick comments (3)
src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx (1)

420-428: 子行渲染逻辑正确,但类型断言可优化。

data as ProviderEntry[] as ProviderRow[] 双重类型断言可以工作,但稍显冗余。

♻️ 可选优化:简化类型断言
-          data={data as ProviderEntry[] as ProviderRow[]}
+          data={data as ProviderRow[]}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx
around lines 420 - 428, The data prop is using a redundant double type assertion
("data as ProviderEntry[] as ProviderRow[]"); simplify by ensuring data is cast
once to the correct type or transformed to match ProviderRow before passing to
LeaderboardTable — update the call to LeaderboardTable<ProviderRow> to pass data
casted directly (e.g., data as ProviderRow[]) or map ProviderEntry->ProviderRow
if structural differences exist; locate the LeaderboardTable usage and the types
ProviderEntry and ProviderRow to make the single, accurate assertion or
conversion.
src/repository/leaderboard.ts (1)

514-593: 模型级别统计查询实现正确,但存在 SQL 聚合逻辑重复。

模型分组查询(lines 522-567)与基础查询(lines 447-492)存在大量重复的聚合表达式。当前实现可以正常工作,建议后续考虑抽取公共聚合表达式为常量或辅助函数,以减少维护成本。

此外,第 571 行对空模型名的过滤 if (!row.model || row.model.trim() === "") continue; 是正确的防御性编程。

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

In `@src/repository/leaderboard.ts` around lines 514 - 593, The model-level stats
query duplicates many aggregation expressions already used in the base query;
extract the repeated SQL aggregation expressions into shared constants or helper
functions (e.g., reusable sql snippets for totalRequests, totalCost,
totalTokens, successRate, avgTtfbMs, avgTokensPerSecond) and use those
identifiers in both the modelRows select block and the base query to avoid
duplication while keeping the existing modelField, modelRows, usageLedger, and
baseEntries logic intact; retain the defensive filter for empty model names (the
if (!row.model || row.model.trim() === "") continue;) when assembling
modelStatsByProvider.
src/app/[locale]/dashboard/leaderboard/_components/leaderboard-table.tsx (1)

251-257: 展开图标的条件渲染可能导致轻微的视觉不对齐。

当同一表格中部分行有子行(显示 chevron)、部分行没有子行(不显示 chevron)时,排名徽章的位置会有细微差异。不过考虑到当前使用场景:

  • user scope:所有行都没有子行
  • provider scope:所有行都有 modelStats(可能为空数组)
  • model scope:所有行都没有子行

在实际使用中,同一 scope 内的行为应该是一致的,所以这个问题影响有限。

如果后续出现混合场景,可以考虑在 hasExpandable 为 false 时也渲染一个同尺寸的占位元素:

♻️ 可选优化:统一占位元素
-                          {hasExpandable ? (
+                          {hasExpandable ? (
                             isExpanded ? (
                               <ChevronDown className="h-4 w-4 text-muted-foreground shrink-0" />
                             ) : (
                               <ChevronRight className="h-4 w-4 text-muted-foreground shrink-0" />
                             )
-                          ) : null}
+                          ) : (
+                            <div className="h-4 w-4 shrink-0" />
+                          )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/dashboard/leaderboard/_components/leaderboard-table.tsx
around lines 251 - 257, Rows without expanders cause badge alignment shifts;
when rendering the chevron block (using hasExpandable and isExpanded with
ChevronDown/ChevronRight), render a same-sized invisible placeholder element
when hasExpandable is false so the ranking badge stays aligned across rows.
Locate the chevron render (checks for hasExpandable and isExpanded and the
ChevronDown/ChevronRight usage) and add an element with identical dimensions and
spacing but visually hidden (e.g., transparent or aria-hidden) as the fallback
when hasExpandable is false.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/app/api/leaderboard/route.ts`:
- Around line 1-12: The route currently uses Next.js App Router imports
(NextRequest/NextResponse) and must be migrated to Hono: replace the exported
Next.js handler with a Hono app instance (e.g., create new Hono(), register a
GET handler) and use the Hono Context/request/response APIs instead of
NextRequest/NextResponse; wire in existing helpers (getSession, logger,
getLeaderboardWithCache, getSystemSettings, formatCurrency) inside the Hono
handler, adapt getSession to accept the Hono context or extract auth from
ctx.req headers/cookies, and return results with ctx.json/ctx.text response
methods; ensure you export the Hono app as the default export so the route is
served by Hono and update any typing to use Hono-compatible types while keeping
references to symbols like getLeaderboardWithCache, getSystemSettings,
formatCurrency, and getSession.

---

Nitpick comments:
In `@src/app/`[locale]/dashboard/leaderboard/_components/leaderboard-table.tsx:
- Around line 251-257: Rows without expanders cause badge alignment shifts; when
rendering the chevron block (using hasExpandable and isExpanded with
ChevronDown/ChevronRight), render a same-sized invisible placeholder element
when hasExpandable is false so the ranking badge stays aligned across rows.
Locate the chevron render (checks for hasExpandable and isExpanded and the
ChevronDown/ChevronRight usage) and add an element with identical dimensions and
spacing but visually hidden (e.g., transparent or aria-hidden) as the fallback
when hasExpandable is false.

In `@src/app/`[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx:
- Around line 420-428: The data prop is using a redundant double type assertion
("data as ProviderEntry[] as ProviderRow[]"); simplify by ensuring data is cast
once to the correct type or transformed to match ProviderRow before passing to
LeaderboardTable — update the call to LeaderboardTable<ProviderRow> to pass data
casted directly (e.g., data as ProviderRow[]) or map ProviderEntry->ProviderRow
if structural differences exist; locate the LeaderboardTable usage and the types
ProviderEntry and ProviderRow to make the single, accurate assertion or
conversion.

In `@src/repository/leaderboard.ts`:
- Around line 514-593: The model-level stats query duplicates many aggregation
expressions already used in the base query; extract the repeated SQL aggregation
expressions into shared constants or helper functions (e.g., reusable sql
snippets for totalRequests, totalCost, totalTokens, successRate, avgTtfbMs,
avgTokensPerSecond) and use those identifiers in both the modelRows select block
and the base query to avoid duplication while keeping the existing modelField,
modelRows, usageLedger, and baseEntries logic intact; retain the defensive
filter for empty model names (the if (!row.model || row.model.trim() === "")
continue;) when assembling modelStatsByProvider.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between 2769a75 and 8a9d72d.

📒 Files selected for processing (8)
  • src/app/[locale]/dashboard/leaderboard/_components/leaderboard-table.tsx
  • src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx
  • src/app/api/leaderboard/route.ts
  • src/lib/redis/leaderboard-cache.ts
  • src/repository/leaderboard.ts
  • tests/unit/api/leaderboard-route.test.ts
  • tests/unit/dashboard/leaderboard-table-expandable-rows.test.tsx
  • tests/unit/repository/leaderboard-provider-metrics.test.ts

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 implements provider model breakdown functionality in the leaderboard, allowing users to expand providers to see model-level statistics. The implementation is generally sound with good test coverage, but there are several issues that should be addressed.

PR Size: M

  • Lines changed: 790 (625 additions, 165 deletions)
  • Files changed: 8

Issues Found

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

High Priority Issues (Should Fix)

[High] [PERFORMANCE-ISSUE] Model stats query lacks pagination

Location: src/repository/leaderboard.ts:796-841

When includeModelStats=true, the model breakdown query fetches all models without any limit. For providers with many models or high volume, this could cause performance issues.

Suggested fix:

const modelRows = await db
  .select({...})
  .where(...)
  .groupBy(usageLedger.finalProviderId, modelField)
  .orderBy(desc(sql\`sum(\${usageLedger.costUsd})\`), desc(sql\`count(*)\`))
  .limit(100); // Add reasonable limit

Medium Priority Issues (Consider Fixing)

[Medium] [STANDARD-VIOLATION] PR marked as incomplete

The PR title contains "[未完成]" (Not Complete), indicating it may not be ready for review.

Suggested fix: Remove "[未完成]" from the title when ready.


[Medium] [TYPE-WEAK-INVARIANT] Type predicate using 'in' operator is fragile

Location: src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx:144-153

Using "providerName" in row as a type guard is fragile because it relies on runtime property existence rather than proper type discrimination.

Suggested fix: Use a discriminated union with a literal type field:

interface ProviderEntry { kind: 'provider'; providerId: number; ... }
interface ModelProviderStat { kind: 'model'; model: string; ... }
// Check: row.kind === 'provider'

[Medium] [SIMPLIFY-READABILITY] Unnecessary type assertions

Location: src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx:158-249

Multiple unnecessary type assertions (row as ProviderEntry) when row is already typed as ProviderRow.

Suggested fix: Remove redundant type assertions throughout the file.


[Medium] [LOGIC-BUG] Empty model filtering logic

Location: src/repository/leaderboard.ts:845

The check if (!row.model || row.model.trim() === "") could be simplified.

Suggested fix: Use optional chaining: if (!row.model?.trim()) continue;


[Medium] [SIMPLIFY-COMPLEXITY] Complex formatting logic can be extracted

Location: src/app/api/leaderboard/route.ts:517-546

The modelStatsFormatted mapping logic is complex and repeats similar patterns.

Suggested fix: Extract a helper function for formatting model stats.

Review Coverage

  • Logic and correctness - Minor issues identified
  • Security (OWASP Top 10) - Clean
  • Error handling - Adequate
  • Type safety - Minor improvements needed
  • Documentation accuracy - Acceptable
  • Test coverage - Good (new tests added)
  • Code clarity - Can be improved

Positive Notes

  1. Good test coverage: New test file added for expandable rows, existing tests extended for includeModelStats
  2. Proper cache key handling: The includeModelStats flag is correctly included in Redis cache keys
  3. Clean separation: Model breakdown logic is well-isolated in the repository layer
  4. i18n compliance: All user-facing strings use translation keys

Automated review by Claude AI

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (2)
src/lib/redis/leaderboard-cache.ts (1)

325-337: ⚠️ Potential issue | 🟡 Minor

invalidateLeaderboardCache 无法清除带 includeModelStats 的缓存。

当前函数不接受 filters 参数,无法构造包含 :includeModelStats 后缀的缓存键。当数据变更需要主动清除缓存时,includeModelStats=true 的缓存条目可能变为陈旧数据。

考虑扩展签名以支持可选的 filters 参数:

建议的修改
 export async function invalidateLeaderboardCache(
   period: LeaderboardPeriod,
   currencyDisplay: string,
-  scope: LeaderboardScope = "user"
+  scope: LeaderboardScope = "user",
+  filters?: LeaderboardFilters
 ): Promise<void> {
   const redis = getRedisClient();
   if (!redis) {
     return;
   }

   // Resolve timezone once per request
   const timezone = await resolveSystemTimezone();
-  const cacheKey = buildCacheKey(period, currencyDisplay, timezone, scope);
+  const cacheKey = buildCacheKey(period, currencyDisplay, timezone, scope, undefined, filters);

   try {
     await redis.del(cacheKey);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/redis/leaderboard-cache.ts` around lines 325 - 337, The
invalidateLeaderboardCache function currently cannot evict cache entries that
include the includeModelStats suffix because it doesn't accept filters; update
invalidateLeaderboardCache to accept an optional filters parameter (e.g.,
filters?: LeaderboardFilters) and pass those filters into buildCacheKey when
constructing the cacheKey so keys with :includeModelStats (or other
filter-derived suffixes) are generated and deleted; update any internal calls to
invalidateLeaderboardCache to supply filters where relevant and ensure
buildCacheKey and any key-deletion logic handle a undefined/null filters value
gracefully.
src/app/[locale]/dashboard/leaderboard/_components/leaderboard-table.tsx (1)

248-259: ⚠️ Potential issue | 🟠 Major

展开交互仅绑定在行点击上,键盘不可达。

Line 248 和 Line 249 把交互挂在 <TableRow>,键盘用户无法稳定触发展开/收起,且缺少 aria-expanded 语义,属于可访问性阻断问题。建议把交互放到可聚焦按钮上。

建议修复(按钮化展开控件)
-<TableRow
-  className={`${isTopThree ? "bg-muted/50" : ""} ${hasExpandable ? "cursor-pointer" : ""}`}
-  onClick={hasExpandable ? () => toggleRow(rowKey) : undefined}
->
+<TableRow className={`${isTopThree ? "bg-muted/50" : ""}`}>
...
-  {hasExpandable ? (
-    isExpanded ? (
-      <ChevronDown className="h-4 w-4 text-muted-foreground shrink-0" />
-    ) : (
-      <ChevronRight className="h-4 w-4 text-muted-foreground shrink-0" />
-    )
-  ) : null}
+  {hasExpandable ? (
+    <button
+      type="button"
+      className="inline-flex items-center"
+      onClick={() => toggleRow(rowKey)}
+      aria-expanded={isExpanded}
+      aria-label={isExpanded ? t("actions.collapseRow") : t("actions.expandRow")}
+    >
+      {isExpanded ? (
+        <ChevronDown className="h-4 w-4 text-muted-foreground shrink-0" />
+      ) : (
+        <ChevronRight className="h-4 w-4 text-muted-foreground shrink-0" />
+      )}
+    </button>
+  ) : null}

As per coding guidelines **/*.{ts,tsx,js,jsx}: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text.

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

In `@src/app/`[locale]/dashboard/leaderboard/_components/leaderboard-table.tsx
around lines 248 - 259, The row expand/collapse interaction is currently only on
<TableRow> (onClick using toggleRow(rowKey)) which is not keyboard-accessible
and lacks aria-expanded; move the interactive handler into a focusable button
rendered where the chevron icons are (inside <TableCell>), keep
toggleRow(rowKey) as the handler, set aria-expanded={isExpanded} on that button,
and preserve the row-level hasExpandable cursor styling but remove the row
onClick; also add an i18n label (e.g., t('leaderboard.expandRow') /
t('leaderboard.collapseRow')) for the button's aria-label or visually hidden
text using the existing translation mechanism to avoid hardcoded strings while
keeping ChevronDown/ChevronRight icons.
🧹 Nitpick comments (1)
src/repository/leaderboard.ts (1)

522-522: successRate 处理不一致:基础条目未做区间限制。

模型级统计(Line 574)对 successRate 进行了 Math.min(Math.max(..., 0), 1) 限制,但基础 provider 条目(Line 522)仅使用 entry.successRate ?? 0。建议统一处理,避免理论上出现超出 [0, 1] 的异常值。

统一 successRate 处理
       successRate: entry.successRate ?? 0,
+      // 如需与 modelStats 一致,可改为:
+      // successRate: Math.min(Math.max(entry.successRate ?? 0, 0), 1),

Also applies to: 574-574

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

In `@src/repository/leaderboard.ts` at line 522, The provider entry sets
successRate as "entry.successRate ?? 0" which can produce values outside [0,1],
inconsistent with the model-level clamp; update the mapping that assigns
successRate (the place using entry.successRate) to apply the same clamp used
later (e.g., successRate = Math.min(Math.max(entry.successRate ?? 0, 0), 1)) so
both provider-level and model-level statistics consistently restrict successRate
to the [0,1] range.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/`[locale]/dashboard/leaderboard/_components/leaderboard-table.tsx:
- Around line 241-243: The expanded state is computed using rowKey which falls
back to the current index when getRowKey is not provided, so sorting/reordering
causes expansion to attach to the wrong row; update the logic in
leaderboard-table.tsx (referencing getRowKey, getSubRows, expandedRows, and
rowKey) to derive a stable key that does not change on sort—either require or
prefer a stable identifier from the row (e.g., id field) or, if absent, generate
and persist a stable key map when the data first mounts (mapping original item
identity to a stable key) and use that map instead of the mutable index to check
expandedRows.

---

Outside diff comments:
In `@src/app/`[locale]/dashboard/leaderboard/_components/leaderboard-table.tsx:
- Around line 248-259: The row expand/collapse interaction is currently only on
<TableRow> (onClick using toggleRow(rowKey)) which is not keyboard-accessible
and lacks aria-expanded; move the interactive handler into a focusable button
rendered where the chevron icons are (inside <TableCell>), keep
toggleRow(rowKey) as the handler, set aria-expanded={isExpanded} on that button,
and preserve the row-level hasExpandable cursor styling but remove the row
onClick; also add an i18n label (e.g., t('leaderboard.expandRow') /
t('leaderboard.collapseRow')) for the button's aria-label or visually hidden
text using the existing translation mechanism to avoid hardcoded strings while
keeping ChevronDown/ChevronRight icons.

In `@src/lib/redis/leaderboard-cache.ts`:
- Around line 325-337: The invalidateLeaderboardCache function currently cannot
evict cache entries that include the includeModelStats suffix because it doesn't
accept filters; update invalidateLeaderboardCache to accept an optional filters
parameter (e.g., filters?: LeaderboardFilters) and pass those filters into
buildCacheKey when constructing the cacheKey so keys with :includeModelStats (or
other filter-derived suffixes) are generated and deleted; update any internal
calls to invalidateLeaderboardCache to supply filters where relevant and ensure
buildCacheKey and any key-deletion logic handle a undefined/null filters value
gracefully.

---

Nitpick comments:
In `@src/repository/leaderboard.ts`:
- Line 522: The provider entry sets successRate as "entry.successRate ?? 0"
which can produce values outside [0,1], inconsistent with the model-level clamp;
update the mapping that assigns successRate (the place using entry.successRate)
to apply the same clamp used later (e.g., successRate =
Math.min(Math.max(entry.successRate ?? 0, 0), 1)) so both provider-level and
model-level statistics consistently restrict successRate to the [0,1] range.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between 8a9d72d and 296918f.

📒 Files selected for processing (3)
  • src/app/[locale]/dashboard/leaderboard/_components/leaderboard-table.tsx
  • src/lib/redis/leaderboard-cache.ts
  • src/repository/leaderboard.ts

@tesgth032
Copy link
Contributor Author

已根据 AI review 做了小幅整理:

  • src/repository/leaderboard.ts:抽取 provider 聚合 SQL 片段与平均值计算,避免 base query 与 modelRows query 重复;同时简化空 model 过滤(!row.model?.trim())。
  • src/lib/redis/leaderboard-cache.ts / src/repository/leaderboard.ts / src/app/[locale]/dashboard/leaderboard/_components/leaderboard-table.tsx:补充 includeModelStats 与子行展开相关注释,便于后续维护。

说明:本次未对 modelStats 做硬性 limit,以保证“供应商 -> 模型”展开数据的完整性;如后续观察到极端大 model 数场景,可再考虑按 provider 做 top-N 或分页策略。

本地已再次验证:bun run lintbun run typecheck,并补跑相关 Vitest 用例。

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

8 files reviewed, 11 comments

Edit Code Review Agent Settings | Greptile

if (scope === "provider") {
return (
<LeaderboardTable<ProviderRow>
data={data as ProviderEntry[] as ProviderRow[]}
Copy link

Choose a reason for hiding this comment

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

avoid double type assertions - consider refactoring data to be properly typed upstream

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx
Line: 421

Comment:
avoid double type assertions - consider refactoring data to be properly typed upstream

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

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

columns={providerColumns}
getRowKey={(row) => ("providerId" in row ? row.providerId : row.model)}
getSubRows={(row) => ("modelStats" in row ? row.modelStats : null)}
getSubRowKey={(row) => ("model" in row ? row.model : row.providerId)}
Copy link

Choose a reason for hiding this comment

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

the fallback case in this ternary should never execute since subRow (first parameter) always has model field for provider sub-rows - simplify by using just subRow.model

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx
Line: 426

Comment:
the fallback case in this ternary should never execute since `subRow` (first parameter) always has `model` field for provider sub-rows - simplify by using just `subRow.model`

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

- 展开按钮化(aria-expanded + i18n label),避免仅依赖行点击

- getRowKey 缺失时在排序/数据变化清空展开态,避免错位

- invalidateLeaderboardCache 支持 dateRange/filters 以清理 includeModelStats 缓存

- successRate 统一 clamp 到 [0,1],并收敛前端类型断言
@tesgth032
Copy link
Contributor Author

已按 CodeRabbit / Greptile / Gemini 的 review 修复并补充:

  • LeaderboardTable:展开控件改为按钮(aria-expanded + i18n aria-label),不再仅依赖整行点击;并在未提供 getRowKey 时,于排序/数据变化清空展开态,避免 index key 导致错位。
  • invalidateLeaderboardCache:新增 dateRange/filters 参数,构造包含 includeModelStats 后缀的缓存 key,保证可正确失效。
  • repository/leaderboardsuccessRate 统一 clamp 到 [0,1](provider 行与 modelStats 一致)。
  • LeaderboardView:补齐 modelStats 的 ...Formatted 字段类型,减少类型断言;getSubRowKey 简化为直接使用 subRow.model
  • 单测:可展开行用例更新为点击展开按钮。

本地已验证:bun run buildbun run lintbun run typecheckbun run test 均通过。

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
src/repository/leaderboard.ts (2)

48-50: 建议补全 modelStats 的语义注释(undefined[])。

当前注释说明了启用条件,但建议再明确两种返回状态的含义,减少前后端契约歧义。

建议补充注释
-  /** 可选:按模型拆分(仅在 includeModelStats=true 时填充) */
+  /**
+   * 可选:按模型拆分
+   * - undefined: 未请求 includeModelStats
+   * - []: 已请求 includeModelStats,但该 provider 下无可用模型统计
+   */
   modelStats?: ModelProviderStat[];

Based on learnings: In TypeScript interfaces, explicitly document and enforce distinct meanings for null and undefined.

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

In `@src/repository/leaderboard.ts` around lines 48 - 50, 为避免前后端契约歧义,补充并明确
leaderboard 接口中 modelStats 字段的语义:说明当 includeModelStats=false 时字段为
undefined(未请求该统计),当 includeModelStats=true 且无数据时返回空数组 [](已请求但无条目);在注释中引用
modelStats 字段、ModelProviderStat 类型和 includeModelStats
参数以提示使用者这两种不同语义,并在必要时在接口文档或注释中建议前端按 undefined/[] 分支处理。

541-562: 建议为模型级聚合分支补充运行时观测。

建议记录该查询的耗时与返回行数,后续可基于真实负载再决定是否引入 top‑N 或分页策略。

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

In `@src/repository/leaderboard.ts` around lines 541 - 562, Wrap the model-level
aggregation query call (the await
db.select(...).from(...).innerJoin(...).where(...).groupBy(...).orderBy(...))
with simple runtime metrics: capture a start timestamp (e.g., Date.now() or
performance.now()), await the query into modelRows, compute duration = now -
start, then emit a log/metric including the duration and modelRows.length. Use
the existing logging/metrics facility in this module (e.g., processLogger or
logger) and include identifiers like usageLedger.finalProviderId and modelField
in the log to make the observation actionable.
src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx (1)

135-137: 可考虑将 includeModelStats 改为按需加载以降低默认开销。

当前 provider 榜单每次都会请求模型拆分数据;当数据量增大时,查询与响应体都会放大。后续可评估“先加载父行、展开时再请求子行”的策略。

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

In `@src/app/`[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx
around lines 135 - 137, The code currently appends "&includeModelStats=1"
whenever scope === "provider" which causes the provider leaderboard to always
request per-model breakdowns; change this to lazy-load child rows instead: stop
adding includeModelStats to the default URL in leaderboard-view.tsx, and
implement an onExpand (or loadChildRows) handler that, when a provider row is
expanded, issues a targeted request to the same API with includeModelStats=1 and
the provider identifier, then merge the returned model stats into the provider
row state; reference the variables/functions scope and url in
leaderboard-view.tsx and add a fetchModelStats or loadChildRows function to make
the on-demand request and update the component state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/app/`[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx:
- Around line 135-137: The code currently appends "&includeModelStats=1"
whenever scope === "provider" which causes the provider leaderboard to always
request per-model breakdowns; change this to lazy-load child rows instead: stop
adding includeModelStats to the default URL in leaderboard-view.tsx, and
implement an onExpand (or loadChildRows) handler that, when a provider row is
expanded, issues a targeted request to the same API with includeModelStats=1 and
the provider identifier, then merge the returned model stats into the provider
row state; reference the variables/functions scope and url in
leaderboard-view.tsx and add a fetchModelStats or loadChildRows function to make
the on-demand request and update the component state.

In `@src/repository/leaderboard.ts`:
- Around line 48-50: 为避免前后端契约歧义,补充并明确 leaderboard 接口中 modelStats 字段的语义:说明当
includeModelStats=false 时字段为 undefined(未请求该统计),当 includeModelStats=true
且无数据时返回空数组 [](已请求但无条目);在注释中引用 modelStats 字段、ModelProviderStat 类型和
includeModelStats 参数以提示使用者这两种不同语义,并在必要时在接口文档或注释中建议前端按 undefined/[] 分支处理。
- Around line 541-562: Wrap the model-level aggregation query call (the await
db.select(...).from(...).innerJoin(...).where(...).groupBy(...).orderBy(...))
with simple runtime metrics: capture a start timestamp (e.g., Date.now() or
performance.now()), await the query into modelRows, compute duration = now -
start, then emit a log/metric including the duration and modelRows.length. Use
the existing logging/metrics facility in this module (e.g., processLogger or
logger) and include identifiers like usageLedger.finalProviderId and modelField
in the log to make the observation actionable.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between 296918f and 90366b2.

📒 Files selected for processing (5)
  • src/app/[locale]/dashboard/leaderboard/_components/leaderboard-table.tsx
  • src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx
  • src/lib/redis/leaderboard-cache.ts
  • src/repository/leaderboard.ts
  • tests/unit/dashboard/leaderboard-table-expandable-rows.test.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/unit/dashboard/leaderboard-table-expandable-rows.test.tsx

url += `&providerType=${encodeURIComponent(providerTypeFilter)}`;
}
if (scope === "provider") {
url += "&includeModelStats=1";
Copy link

Choose a reason for hiding this comment

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

includeModelStats always set to 1 for provider scope with no option to disable. Consider making this configurable if large datasets cause performance issues.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx
Line: 136

Comment:
`includeModelStats` always set to `1` for provider scope with no option to disable. Consider making this configurable if large datasets cause performance issues.

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

Comment on lines +46 to +48
type ProviderRow = ProviderEntry | ModelProviderStatClient;
type ProviderCacheHitRateEntry = ProviderCacheHitRateLeaderboardEntry;
type ModelEntry = ModelLeaderboardEntry & { totalCostFormatted?: string };
type ProviderCacheHitRateRow = ProviderCacheHitRateEntry | ModelCacheHitStat;
Copy link

Choose a reason for hiding this comment

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

Union type suggests data array contains both parents and children, but only parents are in array (children nested in modelStats). Rename to ProviderEntryRow for clarity.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx
Line: 46-48

Comment:
Union type suggests data array contains both parents and children, but only parents are in array (children nested in `modelStats`). Rename to `ProviderEntryRow` for clarity.

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

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

Comment on lines +541 to +561
const modelRows = await db
.select({
providerId: usageLedger.finalProviderId,
model: modelField,
totalRequests: totalRequestsExpr,
totalCost: totalCostExpr,
totalTokens: totalTokensExpr,
successRate: successRateExpr,
avgTtfbMs: avgTtfbMsExpr,
avgTokensPerSecond: avgTokensPerSecondExpr,
})
.from(usageLedger)
.innerJoin(
providers,
and(sql`${usageLedger.finalProviderId} = ${providers.id}`, isNull(providers.deletedAt))
)
.where(
and(...whereConditions.filter((c): c is NonNullable<(typeof whereConditions)[number]> => !!c))
)
.groupBy(usageLedger.finalProviderId, modelField)
.orderBy(desc(sql`sum(${usageLedger.costUsd})`), desc(sql`count(*)`));
Copy link

Choose a reason for hiding this comment

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

Consider optimizing model breakdown query with WHERE finalProviderId IN (...) filter using IDs from first query, instead of re-joining and re-applying all conditions.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/repository/leaderboard.ts
Line: 541-561

Comment:
Consider optimizing model breakdown query with `WHERE finalProviderId IN (...)` filter using IDs from first query, instead of re-joining and re-applying all conditions.

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

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

- LeaderboardTable 支持 Parent/Sub 泛型,避免 data 误导为父子混合

- provider/cacheHitRate 调用改用 providerId 作为父行 key,子行 key 直接用 model

- 补充 ProviderLeaderboardEntry.modelStats 的 undefined/[] 语义注释

- 更新可展开行单测
@tesgth032
Copy link
Contributor Author

跟进最新的 Greptile / CodeRabbit nitpick(commit: f062847):

  • LeaderboardTable 增加 Parent/Sub 两个泛型参数:data 明确为“父行数组”,subRows 为“子行数组”,避免 union type 造成“data 里混有子行”的误解;同时 provider / cacheHitRate 的 rowKey 简化为 providerId,subRowKey 直接使用 subRow.model
  • ProviderLeaderboardEntry.modelStats 注释补充 undefined vs [] 的语义,减少前后端契约歧义。
  • 可展开行单测同步更新。

关于“includeModelStats 默认总是加载 / 以及用 WHERE IN 优化 modelRows 查询 / lazy-load”的建议:目前为保证展开体验与数据完整性,暂维持一次性返回;若后续在真实数据集上观测到响应体/耗时压力,再评估按展开懒加载或分页/top-N(需要 API 与 UI 配套改动)。

本地已再次验证:bun run buildbun run lintbun run typecheckbun run test 均通过。

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx (1)

227-234: 模型子行名称渲染存在重复逻辑,建议抽一个小 helper。

当前两处实现一致,后续样式或缩进策略调整时容易漏改。

可选重构示例
+  const renderSubModelLabel = (model: string) => (
+    <div className="pl-6">
+      <span className="font-mono text-sm">{model}</span>
+    </div>
+  );
+
   const providerColumns: ColumnDef<ProviderTableRow>[] = [
     {
       header: t("columns.provider"),
       cell: (row) => {
         if ("providerName" in row) return row.providerName;
-        return (
-          <div className="pl-6">
-            <span className="font-mono text-sm">{row.model}</span>
-          </div>
-        );
+        return renderSubModelLabel(row.model);
       },
@@
   const providerCacheHitRateColumns: ColumnDef<ProviderCacheHitRateTableRow>[] = [
     {
       header: t("columns.provider"),
       cell: (row) => {
         if ("providerName" in row) return row.providerName;
-        return (
-          <div className="pl-6">
-            <span className="font-mono text-sm">{row.model}</span>
-          </div>
-        );
+        return renderSubModelLabel(row.model);
       },

Also applies to: 312-319

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

In `@src/app/`[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx
around lines 227 - 234, The model/sub-row name rendering logic in the cell
renderers is duplicated; extract it into a small helper (e.g., formatModelCell
or renderModelCell) that accepts the row object and returns the JSX for either
row.providerName or the wrapped <div><span className="font-mono
text-sm">{row.model}</span></div>; replace the inline logic in the cell
renderers (current occurrences in the anonymous cell function around the shown
block and the similar block at the later occurrence) to call this helper so
future style/indent changes are updated in one place.
tests/unit/dashboard/leaderboard-table-expandable-rows.test.tsx (1)

54-113: 建议补充“再次点击收起”的断言,完善展开状态回归保护。

当前只验证了展开后的可见性,建议同时校验 aria-expandedtrue 回到 false 且子行再次隐藏。

可选补丁
@@
     act(() => {
       expandButton!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
     });
+    expect(expandButton!.getAttribute("aria-expanded")).toBe("true");
@@
     const modelRow = modelCell!.closest("tr");
     expect(modelRow).toBeTruthy();
     expect(modelRow!.className).toContain("bg-muted/30");
+
+    act(() => {
+      expandButton!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+    });
+    expect(expandButton!.getAttribute("aria-expanded")).toBe("false");
+    expect(findCellByText("model-x")).toBeNull();
   });
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/dashboard/leaderboard-table-expandable-rows.test.tsx` around lines
54 - 113, Test lacks assertion that clicking the expand button a second time
collapses the sub-rows; after the existing act() that clicks expand, add a
second act() that clicks the same expandButton (or re-query it) and then assert
that expandButton.getAttribute("aria-expanded") is "false" and that
findCellByText("model-x") returns null (and optionally that the model row is not
present). Update the test in leaderboard-table-expandable-rows.test.tsx around
the expandButton / modelCell assertions to perform this collapse click and the
two checks so the toggle behavior is fully verified.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/repository/leaderboard.ts`:
- Around line 569-577: The model name is only whitespace-checked (if
(!row.model?.trim()) continue) but the original row.model is still used when
pushing into stats, causing visually identical models with extra spaces to be
treated as distinct; before computing avgCosts and pushing into
modelStatsByProvider, normalize the model string by calling row.model =
row.model.trim() (or assign a trimmed const like const model = row.model.trim())
and use that trimmed identifier in computeAvgCosts input and in the object
pushed to stats so all downstream code (e.g., computeAvgCosts and
modelStatsByProvider entries) consistently use the trimmed model name.

---

Nitpick comments:
In `@src/app/`[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx:
- Around line 227-234: The model/sub-row name rendering logic in the cell
renderers is duplicated; extract it into a small helper (e.g., formatModelCell
or renderModelCell) that accepts the row object and returns the JSX for either
row.providerName or the wrapped <div><span className="font-mono
text-sm">{row.model}</span></div>; replace the inline logic in the cell
renderers (current occurrences in the anonymous cell function around the shown
block and the similar block at the later occurrence) to call this helper so
future style/indent changes are updated in one place.

In `@tests/unit/dashboard/leaderboard-table-expandable-rows.test.tsx`:
- Around line 54-113: Test lacks assertion that clicking the expand button a
second time collapses the sub-rows; after the existing act() that clicks expand,
add a second act() that clicks the same expandButton (or re-query it) and then
assert that expandButton.getAttribute("aria-expanded") is "false" and that
findCellByText("model-x") returns null (and optionally that the model row is not
present). Update the test in leaderboard-table-expandable-rows.test.tsx around
the expandButton / modelCell assertions to perform this collapse click and the
two checks so the toggle behavior is fully verified.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between 90366b2 and f062847.

📒 Files selected for processing (4)
  • src/app/[locale]/dashboard/leaderboard/_components/leaderboard-table.tsx
  • src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx
  • src/repository/leaderboard.ts
  • tests/unit/dashboard/leaderboard-table-expandable-rows.test.tsx

- provider/modelStats 聚合使用 TRIM+NULLIF 规范化模型名,避免空白差异导致重复行

- 提取 renderSubModelLabel,消除模型子行缩进渲染重复

- 单测补充二次点击收起断言(aria-expanded 回落 + 子行隐藏)
@tesgth032
Copy link
Contributor Author

跟进 CodeRabbit 最新的 CHANGES_REQUESTED / nitpick(commit: f49386f):

  • repository/leaderboard.ts:对 billing model 字段做 TRIM + NULLIF 规范化后再 groupBy/返回,避免模型名因前后空白导致视觉同名拆成多行(同时空白模型会被归一为 null 并过滤)。
  • leaderboard-view.tsx:抽出 renderSubModelLabel,消除两处模型子行缩进渲染重复。
  • leaderboard-table-expandable-rows.test.tsx:补充“再次点击收起”的断言(aria-expanded 回落 + 子行隐藏)。

本地已再次验证:bun run buildbun run lintbun run typecheckbun run test 均通过。

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/repository/leaderboard.ts (1)

696-717: 可选:复用 clampRatio01,减少重复夹紧逻辑。

当前这里仍有两处手写 Math.min(Math.max(...)),可直接复用已有 helper,后续维护更一致。

♻️ 可选补丁
-      cacheHitRate: Math.min(Math.max(row.cacheHitRate ?? 0, 0), 1),
+      cacheHitRate: clampRatio01(row.cacheHitRate),
...
-    cacheHitRate: Math.min(Math.max(entry.cacheHitRate ?? 0, 0), 1),
+    cacheHitRate: clampRatio01(entry.cacheHitRate),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/repository/leaderboard.ts` around lines 696 - 717, Replace the manual
clamping logic Math.min(Math.max(..., 0), 1) with the existing helper
clampRatio01 to avoid duplication: in the loop where you push stats (currently
clamping row.cacheHitRate) use clampRatio01(row.cacheHitRate) and in the return
mapping (currently clamping entry.cacheHitRate) use
clampRatio01(entry.cacheHitRate); update imports/usages if necessary so
clampRatio01 is referenced in leaderboard.ts.
src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx (1)

398-443: 建议减少分支中的 as 断言,降低后续维护风险。

renderTable 中多处分支强转可读性尚可,但后续字段演进时更容易产生静默类型漂移;可考虑按 scope 拆分成小型渲染函数并在函数边界收敛类型。

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

In `@src/app/`[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx
around lines 398 - 443, Split renderTable into small, typed renderer functions
to avoid repeated "as" assertions: create renderUserTable(): JSX.Element typed
to UserEntry[] that returns LeaderboardTable<UserEntry> with data typed as
UserEntry[], renderProviderTable(): JSX.Element typed to ProviderEntry[]
returning LeaderboardTable<ProviderEntry, ModelProviderStatClient> (no casts),
renderProviderCacheHitRateTable(): JSX.Element typed to
ProviderCacheHitRateEntry[] returning
LeaderboardTable<ProviderCacheHitRateEntry, ModelCacheHitStat>, and
renderModelTable(): JSX.Element typed to ModelEntry[] returning
LeaderboardTable<ModelEntry>; then have renderTable switch on scope and call the
appropriate renderer. Ensure each renderer uses the matching columns
(userColumns, providerColumns, providerCacheHitRateColumns, modelColumns) and
fully-typed getRowKey/getSubRows/getSubRowKey callbacks so there are no inline
"as" casts in renderTable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/app/`[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx:
- Around line 398-443: Split renderTable into small, typed renderer functions to
avoid repeated "as" assertions: create renderUserTable(): JSX.Element typed to
UserEntry[] that returns LeaderboardTable<UserEntry> with data typed as
UserEntry[], renderProviderTable(): JSX.Element typed to ProviderEntry[]
returning LeaderboardTable<ProviderEntry, ModelProviderStatClient> (no casts),
renderProviderCacheHitRateTable(): JSX.Element typed to
ProviderCacheHitRateEntry[] returning
LeaderboardTable<ProviderCacheHitRateEntry, ModelCacheHitStat>, and
renderModelTable(): JSX.Element typed to ModelEntry[] returning
LeaderboardTable<ModelEntry>; then have renderTable switch on scope and call the
appropriate renderer. Ensure each renderer uses the matching columns
(userColumns, providerColumns, providerCacheHitRateColumns, modelColumns) and
fully-typed getRowKey/getSubRows/getSubRowKey callbacks so there are no inline
"as" casts in renderTable.

In `@src/repository/leaderboard.ts`:
- Around line 696-717: Replace the manual clamping logic Math.min(Math.max(...,
0), 1) with the existing helper clampRatio01 to avoid duplication: in the loop
where you push stats (currently clamping row.cacheHitRate) use
clampRatio01(row.cacheHitRate) and in the return mapping (currently clamping
entry.cacheHitRate) use clampRatio01(entry.cacheHitRate); update imports/usages
if necessary so clampRatio01 is referenced in leaderboard.ts.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between f062847 and f49386f.

📒 Files selected for processing (3)
  • src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx
  • src/repository/leaderboard.ts
  • tests/unit/dashboard/leaderboard-table-expandable-rows.test.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/unit/dashboard/leaderboard-table-expandable-rows.test.tsx

Comment on lines +568 to +587
const modelStatsByProvider = new Map<number, ModelProviderStat[]>();
for (const row of modelRows) {
if (!row.model) continue;
const totalCost = parseFloat(row.totalCost);
const totalRequests = row.totalRequests;
const totalTokens = row.totalTokens;
const avgCosts = computeAvgCosts(totalCost, totalRequests, totalTokens);
const stats = modelStatsByProvider.get(row.providerId) ?? [];
stats.push({
model: row.model,
totalRequests,
totalCost,
totalTokens,
successRate: clampRatio01(row.successRate),
avgTtfbMs: row.avgTtfbMs ?? 0,
avgTokensPerSecond: row.avgTokensPerSecond ?? 0,
...avgCosts,
});
modelStatsByProvider.set(row.providerId, stats);
}
Copy link

Choose a reason for hiding this comment

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

no limit on number of models per provider - if a provider has 100+ models, modelStats array could become very large and slow down response

consider adding a LIMIT clause to the model breakdown query or implementing pagination for model stats

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/repository/leaderboard.ts
Line: 568-587

Comment:
no limit on number of models per provider - if a provider has 100+ models, `modelStats` array could become very large and slow down response

consider adding a `LIMIT` clause to the model breakdown query or implementing pagination for model stats

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

Comment on lines +206 to +228
const modelStatsFormatted =
scope === "provider" && Array.isArray(typedEntry.modelStats)
? typedEntry.modelStats.map((ms) => {
const stat = ms as {
totalCost: number;
avgCostPerRequest: number | null;
avgCostPerMillionTokens: number | null;
} & Record<string, unknown>;

return {
...stat,
totalCostFormatted: formatCurrency(stat.totalCost, systemSettings.currencyDisplay),
avgCostPerRequestFormatted:
stat.avgCostPerRequest != null
? formatCurrency(stat.avgCostPerRequest, systemSettings.currencyDisplay)
: null,
avgCostPerMillionTokensFormatted:
stat.avgCostPerMillionTokens != null
? formatCurrency(stat.avgCostPerMillionTokens, systemSettings.currencyDisplay)
: null,
};
})
: undefined;
Copy link

Choose a reason for hiding this comment

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

type assertion at line 209 assumes modelStats structure matches without runtime validation

if repository schema changes, this could cause silent errors. consider adding runtime type guards or validation

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/api/leaderboard/route.ts
Line: 206-228

Comment:
type assertion at line 209 assumes modelStats structure matches without runtime validation

if repository schema changes, this could cause silent errors. consider adding runtime type guards or validation

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

Comment on lines +535 to +537
if (!includeModelStats) return baseEntries;

// Model breakdown per provider
Copy link

Choose a reason for hiding this comment

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

if model stats query fails (lines 546-566), entire request fails with no fallback

consider wrapping the model stats query in try-catch to return base provider stats even if model breakdown fails

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/repository/leaderboard.ts
Line: 535-537

Comment:
if model stats query fails (lines 546-566), entire request fails with no fallback

consider wrapping the model stats query in try-catch to return base provider stats even if model breakdown fails

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

Comment on lines +84 to +90
// 当调用方未提供稳定 rowKey 时(回退到 index),排序会导致展开态错位;此时在排序/数据变化时清空展开态,至少避免错位展开。
// biome-ignore lint/correctness/useExhaustiveDependencies: 依赖用于在排序/数据变化时触发清空,避免 index key 造成错位展开
useEffect(() => {
if (!getRowKey) {
setExpandedRows(new Set());
}
}, [data, sortKey, sortDirection, getRowKey]);
Copy link

Choose a reason for hiding this comment

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

clearing expanded rows on data/sort changes prevents users from keeping rows expanded when changing sort order

consider preserving expand state across sorts by always requiring stable getRowKey instead of clearing on change

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/leaderboard/_components/leaderboard-table.tsx
Line: 84-90

Comment:
clearing expanded rows on data/sort changes prevents users from keeping rows expanded when changing sort order

consider preserving expand state across sorts by always requiring stable `getRowKey` instead of clearing on change

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

- providerCacheHitRate 的 cacheHitRate 复用 clampRatio01,避免重复实现

- 拆分 render*Table helpers,集中处理 scope 分支与类型断言
@tesgth032
Copy link
Contributor Author

继续跟进 CodeRabbit nitpick(commit: e9e4a71):

  • repository/leaderboard.ts:provider 缓存命中率榜单的 cacheHitRate 统一复用 clampRatio01,避免重复的 Math.min/Math.max 写法。
  • leaderboard-view.tsx:拆分 render*Table helpers,在函数边界集中收敛各 scope 的数据类型与渲染,减少分支内重复断言。

本地已再次验证:bun run buildbun run lintbun run typecheckbun run test 均通过。

Comment on lines 657 to 658
// Model-level cache hit breakdown per provider
const systemSettings = await getSystemSettings();
Copy link

Choose a reason for hiding this comment

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

Unconditional model-stats secondary query on every cache hit rate request

Unlike findProviderLeaderboardWithTimezone which gates the second DB query behind if (!includeModelStats) return baseEntries; (line 535), findProviderCacheHitRateLeaderboardWithTimezone unconditionally executes the model breakdown query (lines 673–691) and calls getSystemSettings() (which has no in-request caching and hits the DB every call) on every single cache hit rate leaderboard request.

This means:

  • Every GET /api/leaderboard?scope=providerCacheHitRate request incurs two full SQL aggregation queries + one extra system_settings DB hit, even when model stats are not needed.
  • route.ts also calls getSystemSettings() independently at line 53, so this path now has two redundant system_settings queries per request.

Consider either:

  1. Adding an opt-in includeModelStats parameter to this function (matching the provider leaderboard design), or
  2. Passing the already-fetched systemSettings and billingModelSource into the function to avoid the redundant DB query.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/repository/leaderboard.ts
Line: 657-658

Comment:
**Unconditional model-stats secondary query on every cache hit rate request**

Unlike `findProviderLeaderboardWithTimezone` which gates the second DB query behind `if (!includeModelStats) return baseEntries;` (line 535), `findProviderCacheHitRateLeaderboardWithTimezone` unconditionally executes the model breakdown query (lines 673–691) **and** calls `getSystemSettings()` (which has no in-request caching and hits the DB every call) on **every** single cache hit rate leaderboard request.

This means:
- Every `GET /api/leaderboard?scope=providerCacheHitRate` request incurs two full SQL aggregation queries + one extra `system_settings` DB hit, even when model stats are not needed.
- `route.ts` also calls `getSystemSettings()` independently at line 53, so this path now has two redundant `system_settings` queries per request.

Consider either:
1. Adding an opt-in `includeModelStats` parameter to this function (matching the provider leaderboard design), or
2. Passing the already-fetched `systemSettings` and `billingModelSource` into the function to avoid the redundant DB query.

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

key={idx}
className={`${col.className || ""} ${shouldBold ? "font-bold" : ""}`}
>
{col.cell(subRow, subIndex)}
Copy link

Choose a reason for hiding this comment

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

Sub-row index parameter receives within-parent position, not global row index

col.cell(subRow, subIndex) passes subIndex (the sub-row's position within its parent's subRows array: 0, 1, 2, …) as the index argument. For parent rows (line 286), index is the global sorted position (0 = top of table). The ColumnDef.cell signature doesn't distinguish between these two contexts.

If any current or future column renderer uses the index parameter for logic (e.g., alternate row styling, rank-dependent text), sub-rows would receive incorrect values. Consider documenting this semantics difference in the ColumnDef interface, or introduce a separate isSubRow flag so renderers can handle both cases safely:

export interface ColumnDef<T> {
  // ...
  /** index is the global sorted position for parent rows, and the within-parent
   *  position for sub-rows. */
  cell: (row: T, index: number, isSubRow?: boolean) => React.ReactNode;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/leaderboard/_components/leaderboard-table.tsx
Line: 311

Comment:
**Sub-row `index` parameter receives within-parent position, not global row index**

`col.cell(subRow, subIndex)` passes `subIndex` (the sub-row's position within its parent's `subRows` array: 0, 1, 2, …) as the `index` argument. For parent rows (line 286), `index` is the global sorted position (0 = top of table). The `ColumnDef.cell` signature doesn't distinguish between these two contexts.

If any current or future column renderer uses the `index` parameter for logic (e.g., alternate row styling, rank-dependent text), sub-rows would receive incorrect values. Consider documenting this semantics difference in the `ColumnDef` interface, or introduce a separate `isSubRow` flag so renderers can handle both cases safely:

```ts
export interface ColumnDef<T> {
  // ...
  /** index is the global sorted position for parent rows, and the within-parent
   *  position for sub-rows. */
  cell: (row: T, index: number, isSubRow?: boolean) => React.ReactNode;
}
```

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

@tesgth032
Copy link
Contributor Author

已修复:modelStats 的模型名现在在 SQL 层使用 TRIM + NULLIF 归一化后再 groupBy/返回,避免因前后空白导致视觉同名被拆成多行(commit: f49386f)。

@coderabbitai 如方便请重新给出 approve 以清除当前的 CHANGES_REQUESTED(CI/Checks 均已通过)。

@coderabbitai
Copy link

coderabbitai bot commented Mar 1, 2026

@tesgth032 感谢你的修复!模型名在 SQL 层使用 TRIM + NULLIF 进行归一化处理是正确的做法,这样可以有效避免因空白字符导致的重复行问题。既然所有 CI/Checks 都已通过,我现在批准这个 PR。


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

✅ Actions performed

Comments resolved and changes approved.

@tesgth032 tesgth032 changed the title 供应商榜单支持展开模型明细 [未完成] 供应商榜单支持展开模型明细 Mar 1, 2026
@tesgth032
Copy link
Contributor Author

补充修正:当 scope=provider&includeModelStats=1 且某供应商没有模型明细时,API 仍应返回 modelStats: [](而不是省略字段),以保持“已请求 vs 未请求”的语义一致;已在 src/app/api/leaderboard/route.ts 修复并新增单测覆盖。

本地已回归:bun run build / bun run lint / bun run typecheck / bun run test 全通过;PR Checks 已全部通过,当前 mergeStateStatus=CLEAN

@tesgth032
Copy link
Contributor Author

补充优化:对无可展开的行也渲染一个固定宽度的占位(与展开按钮同宽),避免同一榜单中“可展开/不可展开”行的排名徽标左右错位。

本地已回归:bun run build / bun run lint / bun run typecheck / bun run test 全通过;PR Checks 当前全部通过。

@tesgth032
Copy link
Contributor Author

跟进 AI review 的一个潜在语义坑:LeaderboardTableColumnDef.cell 现在增加了可选参数 isSubRow,并在父/子行渲染时分别传入 false/true,方便列渲染器在未来需要用到 index 时区分“全局行序 vs 父行内子行序”。

当前 PR Checks 已全绿(mergeStateStatus=CLEAN)。

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

Labels

area:provider area:statistics area:UI enhancement New feature or request size/M Medium PR (< 500 lines)

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant