Skip to content

feat(leaderboard): provider avg-cost metrics and cache-hit model drilldown#753

Merged
ding113 merged 1 commit intodevfrom
feat/provider-leaderboard-cost-cache-expansion
Feb 10, 2026
Merged

feat(leaderboard): provider avg-cost metrics and cache-hit model drilldown#753
ding113 merged 1 commit intodevfrom
feat/provider-leaderboard-cost-cache-expansion

Conversation

@ding113
Copy link
Owner

@ding113 ding113 commented Feb 10, 2026

Summary

Add two average cost metrics to the provider leaderboard and a per-model expandable drilldown to the cache-hit rate ranking, with full i18n and test coverage.

Problem

The provider leaderboard showed total cost but lacked normalized cost-efficiency metrics (cost per request, cost per million tokens), making it hard to compare providers of different scale. The cache-hit rate ranking showed only provider-level aggregates with no way to see which models contributed to the hit rate.

Related PRs:

Solution

  • Repository layer: Compute avgCostPerRequest (totalCost / totalRequests) and avgCostPerMillionTokens (totalCost * 1_000_000 / totalTokens) with null-safe division (returns null when denominator is 0). Add a second grouped query for cache-hit rankings keyed by billingModelSource to produce per-model ModelCacheHitStat entries.
  • API layer: Format new cost fields via immutable spread pattern, preserving all existing fields.
  • UI layer: Make LeaderboardTable support generic expandable rows with chevron toggle. Add 2 new provider columns and a cache-hit model drilldown sub-table.

Changes

Core Changes

  • src/repository/leaderboard.ts - avgCostPerRequest, avgCostPerMillionTokens fields on ProviderLeaderboardEntry; new ModelCacheHitStat interface and model sub-query on ProviderCacheHitRateLeaderboardEntry
  • src/app/api/leaderboard/route.ts - Format new cost fields with currency display; refactored to immutable spread pattern
  • src/app/[locale]/dashboard/leaderboard/_components/leaderboard-table.tsx - Generic expandable row support (renderExpandedContent prop, chevron toggle, Fragment keying)
  • src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx - 2 new provider columns (avg cost/req, avg cost/1M tokens); cache-hit model drilldown with color-coded hit rates

Supporting Changes

  • messages/{en,ja,ru,zh-CN,zh-TW}/dashboard.json - 4 new i18n keys per locale (avgCostPerRequest, avgCostPerMillionTokens, expandModelStats, collapseModelStats)

Breaking Changes

None. All changes are additive - new fields are appended to existing interfaces and API responses. Existing fields and behavior are unchanged.

Testing

Automated Tests

  • Unit tests added: tests/unit/repository/leaderboard-provider-metrics.test.ts (10 tests covering avg-cost computation, null-safe division, model breakdown grouping, empty model exclusion, field preservation)
  • Unit tests added: tests/unit/api/leaderboard-route.test.ts (3 new tests covering provider formatted fields, null cost handling, modelStats passthrough)

Manual Testing

  1. Navigate to Dashboard > Leaderboard > Provider tab
  2. Verify "Avg Cost/Req" and "Avg Cost/1M Tokens" columns appear with formatted currency values
  3. Switch to Cache Hit Rate tab, click a provider row with the chevron icon
  4. Verify model-level breakdown table expands with per-model cache hit rates (color-coded: green >= 85%, yellow >= 60%, orange < 60%)

Changed Files

Layer Files What
Repository src/repository/leaderboard.ts avgCostPerRequest, avgCostPerMillionTokens, ModelCacheHitStat, model sub-query
API src/app/api/leaderboard/route.ts Formatted fields via immutable spread
UI leaderboard-table.tsx Generic expandable row support
UI leaderboard-view.tsx 2 new provider columns + cache-hit model drilldown
i18n messages/*/dashboard.json 4 new keys x 5 locales
Tests leaderboard-provider-metrics.test.ts 10 tests (avg-cost + cache-hit model)
Tests leaderboard-route.test.ts 3 new tests (API formatting)

Checklist

  • Code follows project conventions
  • Self-review completed
  • Tests pass locally (2063 tests GREEN)
  • TypeScript typecheck clean (tsgo)
  • Biome lint clean (971 files)
  • i18n: all 5 locales updated

Description enhanced by Claude AI

…drilldown

Provider ranking adds avgCostPerRequest and avgCostPerMillionTokens with
null-safe division guards. Cache-hit ranking gains per-provider expandable
model-level breakdown (modelStats) via a second grouped query keyed by
billingModelSource. LeaderboardTable now supports generic expandable rows.

API route formats new fields immutably via spread pattern. i18n keys added
for all 5 locales. 16 new tests cover repository semantics, API formatting,
and multi-provider model grouping.
@coderabbitai
Copy link

coderabbitai bot commented Feb 10, 2026

📝 Walkthrough

Walkthrough

此 PR 为提供商排行榜添加成本效率指标(每请求平均成本和每百万 Token 平均成本),为表格引入可展开行功能以显示按模型的缓存命中统计,跨五种语言扩展本地化支持,并包含相应测试覆盖。

Changes

Cohort / File(s) Summary
Localization Files
messages/en/dashboard.json, messages/ja/dashboard.json, messages/ru/dashboard.json, messages/zh-CN/dashboard.json, messages/zh-TW/dashboard.json
Added localization keys for leaderboard columns (avgCostPerRequest, avgCostPerMillionTokens) and expand/collapse model statistics controls (expandModelStats, collapseModelStats) across all language files.
UI Components
src/app/[locale]/dashboard/leaderboard/_components/leaderboard-table.tsx, src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx
Introduced expandable row functionality with state management for expanded rows; added two new cost metric columns with sorting support; enhanced provider view to render per-model cache hit statistics in collapsible subtable.
Backend/API
src/app/api/leaderboard/route.ts, src/repository/leaderboard.ts
Computed new formatted cost fields (avgCostPerRequestFormatted, avgCostPerMillionTokensFormatted); extended ProviderLeaderboardEntry with avgCostPerRequest and avgCostPerMillionTokens; introduced ModelCacheHitStat interface and per-model breakdown logic in cache-hit rate calculation.
Test Coverage
tests/unit/api/leaderboard-route.test.ts, tests/unit/repository/leaderboard-provider-metrics.test.ts
Added test cases for additive cost fields and formatting; introduced comprehensive test suite validating cost metric calculations, null handling, model stats grouping, and ordering stability.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • PR #623: 同时修改排行榜存储库和提供商缓存命中率管道(包括按提供商/模型的 Token 和成本字段),代码层面相关性强。
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed 标题清晰概括了主要变更:为提供商排行榜添加平均成本指标和缓存命中率模型钻取功能。
Description check ✅ Passed 拉取请求描述与变更集完全相关,详细说明了新增的成本指标、缓存命中率模型细分和国际化支持等改动。

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/provider-leaderboard-cost-cache-expansion

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 @ding113, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly upgrades the leaderboard functionality by introducing granular cost-efficiency metrics for providers and a detailed, expandable view of cache hit rates at the model level. These additions provide deeper insights into provider performance and resource utilization, while UI enhancements ensure a more interactive and informative user experience.

Highlights

  • New Provider Metrics: Introduced avgCostPerRequest and avgCostPerMillionTokens to the provider leaderboard, calculated with null-safe division to handle zero denominators gracefully.
  • Cache-Hit Model Drilldown: Enhanced cache-hit ranking to include an expandable, per-provider model-level breakdown (modelStats), allowing users to view detailed cache performance by specific models.
  • Generic Expandable Table Rows: The LeaderboardTable component was refactored to support generic expandable rows with chevron toggles, improving UI flexibility and data presentation.
  • API and i18n Updates: The API route now immutably formats the new average cost fields, and all necessary internationalization keys have been added across five locales.
  • Comprehensive Testing: Extensive unit tests were added for both the repository logic (average cost calculations, model stats grouping) and the API route (field formatting and inclusion).
Changelog
  • messages/en/dashboard.json
    • Added new translation keys for average cost metrics and model expansion toggles.
  • messages/ja/dashboard.json
    • Added new translation keys for average cost metrics and model expansion toggles.
  • messages/ru/dashboard.json
    • Added new translation keys for average cost metrics and model expansion toggles.
  • messages/zh-CN/dashboard.json
    • Added new translation keys for average cost metrics and model expansion toggles.
  • messages/zh-TW/dashboard.json
    • Added new translation keys for average cost metrics and model expansion toggles.
  • src/app/[locale]/dashboard/leaderboard/_components/leaderboard-table.tsx
    • Implemented generic expandable row functionality, including new props, state, and UI elements for toggling.
  • src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx
    • Integrated new average cost columns and enabled model-level drilldown for cache-hit rates using the updated LeaderboardTable.
  • src/app/api/leaderboard/route.ts
    • Modified to format new average cost fields and include model-level cache hit statistics in the API response.
  • src/repository/leaderboard.ts
    • Defined new data structures for provider and model cache hit statistics.
    • Implemented calculations for avgCostPerRequest and avgCostPerMillionTokens with null-safe division.
    • Added a new grouped query for ModelCacheHitStat to support model-level cache hit breakdown.
  • tests/unit/api/leaderboard-route.test.ts
    • Added new test cases to verify the API's handling and formatting of average cost metrics and model-level cache hit data.
  • tests/unit/repository/leaderboard-provider-metrics.test.ts
    • Added a new test file to validate the repository logic for calculating average cost metrics and grouping model-level cache hit statistics.
Activity
  • All 246 test files passed, totaling 2063 tests.
  • TypeScript type checks passed cleanly.
  • Biome linting passed cleanly across 971 files.
  • Verified avgCostPerRequest = totalCost / totalRequests (null when denominator is 0).
  • Verified avgCostPerMillionTokens = totalCost * 1M / totalTokens (null when denominator is 0).
  • Confirmed model stats are grouped correctly across multiple providers.
  • Ensured empty model names are excluded from modelStats.
  • Confirmed existing provider-level fields remain unchanged (additive-only).
  • Verified fragment key usage on expandable rows for React reconciliation.
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.

@github-actions github-actions bot added size/L Large PR (< 1000 lines) enhancement New feature or request area:statistics area:UI area:i18n labels Feb 10, 2026
@github-actions
Copy link
Contributor

🧪 测试结果

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

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

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.

11 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +526 to +528
return (
<div className="px-8 py-3">
<div className="text-xs text-muted-foreground mb-2 font-medium">
Copy link

Choose a reason for hiding this comment

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

Expand label is inverted

The expanded section currently renders {t("expandModelStats")} even when the row is already expanded. This makes the UI text incorrect in the expanded state and the new collapseModelStats i18n key is never used. Use collapseModelStats when the row is expanded (and expandModelStats when collapsed), or remove the unused key.

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: 526:528

Comment:
**Expand label is inverted**

The expanded section currently renders `{t("expandModelStats")}` even when the row is already expanded. This makes the UI text incorrect in the expanded state and the new `collapseModelStats` i18n key is never used. Use `collapseModelStats` when the row is expanded (and `expandModelStats` when collapsed), or remove the unused key.


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

@greptile-apps
Copy link

greptile-apps bot commented Feb 10, 2026

Additional Comments (1)

src/repository/leaderboard.ts
Cache-hit model query unbounded

findProviderCacheHitRateLeaderboardWithTimezone adds a second grouped query (modelRows) that reuses the same whereConditions, but it does not restrict results to the top-ranked providers returned by the first rankings query. In periods with many providers/models this will scan and group across all providers matching the date range, even when the UI only displays a small page of providers, which is a real performance regression for this endpoint. Filter the model sub-query by providerId IN (rankings providerIds) (or apply the same limit) before grouping.

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

Comment:
**Cache-hit model query unbounded**

`findProviderCacheHitRateLeaderboardWithTimezone` adds a second grouped query (`modelRows`) that reuses the same `whereConditions`, but it does not restrict results to the *top-ranked providers* returned by the first `rankings` query. In periods with many providers/models this will scan and group across all providers matching the date range, even when the UI only displays a small page of providers, which is a real performance regression for this endpoint. Filter the model sub-query by `providerId IN (rankings providerIds)` (or apply the same limit) before grouping.


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

This pull request introduces significant enhancements to the leaderboard feature. It adds average cost metrics (avgCostPerRequest, avgCostPerMillionTokens) to the provider leaderboard and implements an expandable row feature to show a detailed model-level breakdown for cache-hit statistics. The changes are well-structured, spanning the repository, API, and UI layers, and are accompanied by comprehensive i18n updates and new unit tests. The implementation of the expandable table component is robust, and the backend logic correctly handles calculations and data aggregation. I have one suggestion to optimize the database query for fetching model-level statistics, which could improve performance.

Comment on lines +512 to +560
// Model-level cache hit breakdown per provider
const systemSettings = await getSystemSettings();
const billingModelSource = systemSettings.billingModelSource;
const modelField =
billingModelSource === "original"
? sql<string>`COALESCE(${messageRequest.originalModel}, ${messageRequest.model})`
: sql<string>`COALESCE(${messageRequest.model}, ${messageRequest.originalModel})`;

const modelTotalInput = sql<number>`COALESCE(sum(${totalInputTokensExpr})::double precision, 0::double precision)`;
const modelCacheRead = sql<number>`COALESCE(sum(COALESCE(${messageRequest.cacheReadInputTokens}, 0))::double precision, 0::double precision)`;
const modelCacheHitRate = sql<number>`COALESCE(
${modelCacheRead} / NULLIF(${modelTotalInput}, 0::double precision),
0::double precision
)`;

const modelRows = await db
.select({
providerId: messageRequest.providerId,
model: modelField,
totalRequests: sql<number>`count(*)::double precision`,
cacheReadTokens: modelCacheRead,
totalInputTokens: modelTotalInput,
cacheHitRate: modelCacheHitRate,
})
.from(messageRequest)
.innerJoin(
providers,
and(sql`${messageRequest.providerId} = ${providers.id}`, isNull(providers.deletedAt))
)
.where(
and(...whereConditions.filter((c): c is NonNullable<(typeof whereConditions)[number]> => !!c))
)
.groupBy(messageRequest.providerId, modelField)
.orderBy(desc(modelCacheHitRate), desc(sql`count(*)`));

// Group model stats by providerId
const modelStatsByProvider = new Map<number, ModelCacheHitStat[]>();
for (const row of modelRows) {
if (!row.model || row.model.trim() === "") continue;
const stats = modelStatsByProvider.get(row.providerId) ?? [];
stats.push({
model: row.model,
totalRequests: row.totalRequests,
cacheReadTokens: row.cacheReadTokens,
totalInputTokens: row.totalInputTokens,
cacheHitRate: Math.min(Math.max(row.cacheHitRate ?? 0, 0), 1),
});
modelStatsByProvider.set(row.providerId, stats);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The current implementation uses two separate database queries to fetch provider-level and model-level statistics, which are then joined in the application code. This is functionally correct, but could be optimized for better performance and atomicity by using a single query.

Consider using a subquery with a PostgreSQL aggregation function like jsonb_agg to construct the modelStats array directly in the database. This would reduce database round-trips and delegate the joining logic to the database, which is generally more efficient. This would involve creating a subquery for the model stats and then LEFT JOIN-ing it in the main provider query.

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

🤖 Fix all issues with AI agents
In `@src/app/`[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx:
- Around line 527-530: Replace the action-style i18n key t("expandModelStats")
used as the expanded-section title with a descriptive content key (e.g. add
"modelStatsTitle" = "模型明细" to your locale files) or reuse an existing noun key
like t("columns.model"); update the component where t("expandModelStats") is
called (the t(...) inside the expanded content header in leaderboard-view.tsx)
to use the new/reused key, and remove or stop referencing the unused
"collapseModelStats" key if it’s not needed.
🧹 Nitpick comments (4)
src/app/api/leaderboard/route.ts (1)

200-211: 日志中记录了 userNameuserTags/userGroups,属于用户可识别信息。

logger.info 中包含 userNameuserTagsuserGroups,这些在 GDPR/CCPA 语境下可被视为 PII 或用户标识。虽然这是管理员审计日志且为已有模式,但建议确认日志存储策略是否满足合规要求,或考虑在非 admin 场景下脱敏用户名。

tests/unit/repository/leaderboard-provider-metrics.test.ts (1)

110-114: 类型检查断言可读性偏低,建议简化。

Lines 110-114 中的 _typeCheck 变量赋值实际上是一个编译期类型断言,运行时仅检查 toBeDefined(),对象始终为 {},这个 expect 永远通过。如果目的是类型约束,可以考虑用 TypeScript 的 satisfies 关键字或 expectTypeOf(Vitest 内置),无需运行时断言。

建议使用 Vitest 内置的 expectTypeOf
-    type HasAvgCostPerRequest = { avgCostPerRequest: number | null };
-    const _typeCheck: HasAvgCostPerRequest = {} as Awaited<
-      ReturnType<typeof findDailyProviderLeaderboard>
-    >[number];
-    expect(_typeCheck).toBeDefined();
+    expectTypeOf<Awaited<ReturnType<typeof findDailyProviderLeaderboard>>[number]>()
+      .toHaveProperty("avgCostPerRequest")
+      .toEqualTypeOf<number | null>();
src/app/[locale]/dashboard/leaderboard/_components/leaderboard-table.tsx (1)

232-234: renderExpandedContent 在每次渲染时对所有行都会被调用,存在性能隐患。

Line 233 对每一行都调用了 renderExpandedContent(row, index),即使该行处于折叠状态。这是因为返回值同时用于判断是否可展开(chevron 图标 + cursor 样式)和渲染展开内容。当数据量较大或展开内容渲染较复杂时,会产生不必要的开销。

建议将「是否可展开」的判断和「展开内容的渲染」解耦,或对 expandedContent 进行延迟求值:

建议的优化方案
 const hasExpandable = renderExpandedContent != null;
-const expandedContent = hasExpandable ? renderExpandedContent(row, index) : null;
+const canExpand = hasExpandable; // or a separate prop like `isExpandable`
 const isExpanded = expandedRows.has(rowKey);
+const expandedContent = isExpanded && hasExpandable ? renderExpandedContent(row, index) : null;

如果需要在折叠时也显示 chevron 图标来指示可展开(当前逻辑是只有 expandedContent 非 null 才显示),可以额外提供一个 isRowExpandable?: (row: T) => boolean 回调来做轻量判断。

src/repository/leaderboard.ts (1)

512-560: 模型级别缓存命中率子查询:逻辑正确,有两个可改进点。

  1. 额外的 getSystemSettings() 调用(Line 513):此函数中已执行了 provider 级查询,再调用一次 getSystemSettings() 增加了一次 DB roundtrip。考虑将 billingModelSource 作为参数从调用方传入,或在函数开头一次性获取。

  2. modelField 构建逻辑重复:Lines 515-518 与 findModelLeaderboardWithTimezone(Lines 653-656)中的逻辑完全相同。可以抽取为一个共享的辅助函数。

建议抽取共享辅助函数
+function buildModelField(billingModelSource: string) {
+  return billingModelSource === "original"
+    ? sql<string>`COALESCE(${messageRequest.originalModel}, ${messageRequest.model})`
+    : sql<string>`COALESCE(${messageRequest.model}, ${messageRequest.originalModel})`;
+}

然后在两处替换使用。

Comment on lines +527 to +530
<div className="px-8 py-3">
<div className="text-xs text-muted-foreground mb-2 font-medium">
{t("expandModelStats")}
</div>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

t("expandModelStats") 用作展开后的节标题语义不准确。

此处已经是展开后的内容区域,但使用了 expandModelStats("展开模型" / "Expand models")作为标题,这是一个动作描述而非内容标题。建议新增一个描述性的 i18n key(如 modelStatsTitle = "模型明细"),或者直接复用已有的 columns.model 等键来作为小节标题。collapseModelStats 键在当前实现中也未被使用。

🤖 Prompt for AI Agents
In `@src/app/`[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx
around lines 527 - 530, Replace the action-style i18n key t("expandModelStats")
used as the expanded-section title with a descriptive content key (e.g. add
"modelStatsTitle" = "模型明细" to your locale files) or reuse an existing noun key
like t("columns.model"); update the component where t("expandModelStats") is
called (the t(...) inside the expanded content header in leaderboard-view.tsx)
to use the new/reused key, and remove or stop referencing the unused
"collapseModelStats" key if it’s not needed.

return r.avgCostPerRequestFormatted ?? r.avgCostPerRequest.toFixed(4);
},
sortKey: "avgCostPerRequest",
getValue: (row) => (row as ProviderEntry).avgCostPerRequest ?? 0,
Copy link
Contributor

Choose a reason for hiding this comment

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

[HIGH] [LOGIC-BUG] Sorting null avg-cost values as 0 makes "no data" providers look cheapest

Evidence (new code)

  • src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx:282
    getValue: (row) => (row as ProviderEntry).avgCostPerRequest ?? 0,
  • src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx:293
    getValue: (row) => (row as ProviderEntry).avgCostPerMillionTokens ?? 0,

Backend defines these as null when the denominator is 0 (e.g. avgCostPerRequest: number | null; // ... null when totalRequests === 0 in src/repository/leaderboard.ts). The UI also renders "-" when the value is null, so sorting them as 0 is misleading (first click is ascending).

Suggested fix: allow getValue to return null and make LeaderboardTable sort nulls last for both directions.

// src/app/[locale]/dashboard/leaderboard/_components/leaderboard-table.tsx
export interface ColumnDef<T> {
  // ...
  getValue?: (row: T) => number | string | null | undefined;
}

// In the sort comparator (before number/string comparison):
if (valueA == null && valueB == null) return 0;
if (valueA == null) return 1;
if (valueB == null) return -1;

// src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx
getValue: (row) => (row as ProviderEntry).avgCostPerRequest,
getValue: (row) => (row as ProviderEntry).avgCostPerMillionTokens,

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 provider avg-cost metrics and adds model-level cache-hit drilldowns, with corresponding UI expansion support and API formatting. One high-confidence issue remains in the new provider avg-cost columns: sorting treats null as 0, which can mis-rank "no data" providers as the cheapest when sorting ascending.

PR Size: L

  • Lines changed: 936
  • Files changed: 11
  • Split suggestion (recommended for L PRs): (1) repository + API contract + unit tests, (2) UI table expansion + view changes, (3) i18n message updates.

Issues Found

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

Critical Issues (Must Fix)

  • None

High Priority Issues (Should Fix)

  • [HIGH] [LOGIC-BUG] null avg-cost values sorted as 0 misleads ranking on ascending sort (src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx:282, src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx:293). Confidence: 85

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.

  • Reviewed PR #753 and labeled it size/L (936 lines changed across 11 files) via gh pr edit --add-label "size/L".
  • Filed 1 inline comment ([HIGH] [LOGIC-BUG]) on new diff lines: sorting null avg-cost values as 0 mis-ranks “no data” providers when sorting ascending (src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx:282, src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx:293). Suggested fix: allow ColumnDef.getValue to return null and make LeaderboardTable sort nulls last (update comparator in src/app/[locale]/dashboard/leaderboard/_components/leaderboard-table.tsx).
  • Submitted the required review summary to the PR with gh pr review 753 --comment --body ... (includes the L-size split suggestion).

@ding113 ding113 merged commit 0872992 into dev Feb 10, 2026
22 checks passed
@github-project-automation github-project-automation bot moved this from Backlog to Done in Claude Code Hub Roadmap Feb 10, 2026
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

No significant issues identified in this PR. The implementation is clean, follows project conventions, and includes thorough test coverage. The null-safe division pattern for cost metrics, the generic expandable row support, and the model-level cache hit drilldown are all well-implemented.

PR Size: L

  • Lines changed: 936 (893 additions, 43 deletions)
  • Files changed: 11

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean
  • Error handling - Clean
  • Type safety - Clean
  • Documentation accuracy - Clean
  • Test coverage - Adequate (10 repository tests + 3 API tests)
  • Code clarity - Good

Automated review by Claude AI

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

Labels

area:i18n area:statistics area:UI enhancement New feature or request size/L Large PR (< 1000 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant

Comments