Skip to content

feat(ui): add API key breakdown on model usage page - #2346

Merged
steebchen merged 4 commits into
mainfrom
model-breakdown-by-api-key
May 20, 2026
Merged

steebchen merged 4 commits into
mainfrom
model-breakdown-by-api-key

Conversation

@steebchen

@steebchen steebchen commented May 20, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Adds a "Group By" selector on the model usage dashboard so the stacked chart can be broken down by API key in addition to model
  • When breakdown by API key is selected, the API key filter is disabled and cleared
  • Backend /activity now returns an apiKeyBreakdown alongside modelBreakdown (queried from api_key_hourly_stats joined with api_key for the human-readable description)

Test plan

  • Open the dashboard model-usage page, switch "Group By" between Model and API key — stacked bars and legend update accordingly
  • When "API key" is selected, the API key filter dropdown is disabled and any prior selection is cleared from the URL
  • When "Model" is selected, the API key filter works as before
  • Activity unit tests still pass (vitest run apps/api/src/routes/activity.spec.ts)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added ability to group activity metrics by API key in addition to model-based grouping.
    • New "Group by" control in the activity dashboard to switch visualization between API key and model; choosing API key clears the individual API key filter.
    • Daily activity responses and charts now include a per‑API‑key usage breakdown (missing slots appear as empty arrays) for richer tooltips and legends.

Review Change Stack

Adds a Group By selector on the model usage dashboard so the stacked
chart can be broken down by API key instead of model. Selecting "API
key" disables and clears the API key filter. Backend /activity now
returns an apiKeyBreakdown alongside modelBreakdown.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 20, 2026 12:27
@coderabbitai

coderabbitai Bot commented May 20, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@steebchen has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 6 minutes and 50 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f34eb65c-c354-423f-9a15-28a0a3fbaee4

📥 Commits

Reviewing files that changed from the base of the PR and between 056340f and 9ceee30.

⛔ Files ignored due to path filters (4)
  • apps/code/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
  • apps/playground/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
  • apps/ui/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
  • ee/admin/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
📒 Files selected for processing (2)
  • apps/api/src/routes/activity.ts
  • apps/ui/src/components/dashboard/activity-chart.tsx

Walkthrough

Backend adds per-API-key aggregates to activity responses; UI types and the ActivityChart are updated to include apiKeyBreakdown and a new groupBy mode. A "Group by" selector in the usage client toggles chart grouping between model and API key and adjusts filtering.

Changes

API Key Activity Breakdown Visualization

Layer / File(s) Summary
API Response Shape & Data Contracts
apps/ui/src/types/activity.ts, apps/api/src/routes/activity.ts
ActivityApiKeyUsage interface captures per-API-key metrics. Response types (DailyActivity, ActivitT) are extended to include apiKeyBreakdown arrays. Zod schemas validate per-API-key usage in the backend.
Backend API Key Aggregation
apps/api/src/routes/activity.ts
apiKeyHourlyStats joined with apiKey table to aggregate usage by day/hour/key. apiKeyBreakdownByDate map is built and injected into each activity record. Empty time slots now include apiKeyBreakdown: []. Both filtered (single API key) and unfiltered (all keys) query modes are supported.
Chart Visualization Refactoring
apps/ui/src/components/dashboard/activity-chart.tsx
ActivityChart gains groupBy prop to support model or API key grouping. Series derivation, legend rendering, tooltip, and bar generation are generalized to work with either breakdown type. Labels and headings adapt based on grouping mode.
Group-by Controls & Routing
apps/ui/src/components/usage/model-usage-client.tsx
“Group by” select control derives mode from URL query params and updates them. When grouping by API key, the API key filter is cleared and disabled to show aggregate data. ActivityChart receives the grouping mode and effective filter state.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding API key breakdown visualization to the model usage page, which aligns with the backend apiKeyBreakdown support and UI groupBy functionality.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch model-breakdown-by-api-key

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9b1d264e0d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/api/src/routes/activity.ts Outdated
Comment on lines +823 to +827
.from(apiKeyHourlyStats)
.leftJoin(apiKey, eq(apiKey.id, apiKeyHourlyStats.apiKeyId))
.where(
and(
inArray(apiKeyHourlyStats.projectId, projectIds),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Include non-API-key traffic in API key breakdown mode

This new breakdown query sources series data only from api_key_hourly_stats, so any traffic without an API key (the credits path tracked via creditsRequestCount/creditsCost) is dropped from the stacked series while the endpoint still returns overall totals from project-level aggregates; in API-key grouping this makes the chart/percentages undercount whenever credits traffic exists. Either add a synthetic “no API key” bucket or align totals to the same data source used for the breakdown so stacked values remain consistent with displayed totals.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

api key means api key not provide rkey, so this includes all usage regardless of which mode the user uses

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR adds an “API key” grouping option to the model usage dashboard by extending the /activity API response to include an apiKeyBreakdown, and updating the UI chart/filter controls to render either per-model or per-key stacked series.

Changes:

  • Backend: /activity now returns apiKeyBreakdown (aggregated from api_key_hourly_stats joined to api_key for descriptions).
  • UI: adds “Group by” selector (Model vs API key) and disables/clears the API key filter when grouping by API key.
  • Types: updates OpenAPI TS typings and UI activity types to include apiKeyBreakdown.

Reviewed changes

Copilot reviewed 4 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
ee/admin/src/lib/api/v1.d.ts Updates admin OpenAPI typings to include apiKeyBreakdown in activity response items.
apps/ui/src/types/activity.ts Adds ActivityApiKeyUsage and wires apiKeyBreakdown into UI activity types.
apps/ui/src/lib/api/v1.d.ts Updates UI OpenAPI typings to include apiKeyBreakdown.
apps/ui/src/components/usage/model-usage-client.tsx Adds Group By selector and disables/clears API key filter when grouping by API key.
apps/ui/src/components/dashboard/activity-chart.tsx Updates chart to render stacked series by model or by API key (labels + tooltip behavior).
apps/playground/src/lib/api/v1.d.ts Updates playground OpenAPI typings to include apiKeyBreakdown.
apps/code/src/lib/api/v1.d.ts Updates code app OpenAPI typings to include apiKeyBreakdown.
apps/api/src/routes/activity.ts Adds apiKeyBreakdown to schema and computes it via an additional aggregated query.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread apps/api/src/routes/activity.ts Outdated
Comment on lines +795 to +841
// Query api key breakdown from apiKeyHourlyStats table joined with apiKey
const apiKeyBreakdowns = await db
.select({
date: isHourly
? sql<string>`to_char(${apiKeyHourlyStats.hourTimestamp}, 'YYYY-MM-DD"T"HH24:MI:SS')`.as(
"date",
)
: sql<string>`DATE(${apiKeyHourlyStats.hourTimestamp})`.as("date"),
apiKeyId: apiKeyHourlyStats.apiKeyId,
description: apiKey.description,
requestCount:
sql<number>`COALESCE(SUM(${apiKeyHourlyStats.requestCount}), 0)`.as(
"requestCount",
),
inputTokens:
sql<number>`COALESCE(SUM(CAST(${apiKeyHourlyStats.inputTokens} AS NUMERIC)), 0)`.as(
"inputTokens",
),
outputTokens:
sql<number>`COALESCE(SUM(CAST(${apiKeyHourlyStats.outputTokens} AS NUMERIC)), 0)`.as(
"outputTokens",
),
totalTokens:
sql<number>`COALESCE(SUM(CAST(${apiKeyHourlyStats.totalTokens} AS NUMERIC)), 0)`.as(
"totalTokens",
),
cost: sql<number>`COALESCE(SUM(${apiKeyHourlyStats.cost}), 0)`.as("cost"),
})
.from(apiKeyHourlyStats)
.leftJoin(apiKey, eq(apiKey.id, apiKeyHourlyStats.apiKeyId))
.where(
and(
inArray(apiKeyHourlyStats.projectId, projectIds),
gte(apiKeyHourlyStats.hourTimestamp, startDate),
lte(apiKeyHourlyStats.hourTimestamp, endDate),
),
)
.groupBy(
isHourly
? sql`${apiKeyHourlyStats.hourTimestamp}, ${apiKeyHourlyStats.apiKeyId}, ${apiKey.description}`
: sql`DATE(${apiKeyHourlyStats.hourTimestamp}), ${apiKeyHourlyStats.apiKeyId}, ${apiKey.description}`,
)
.orderBy(
isHourly
? sql`${apiKeyHourlyStats.hourTimestamp} ASC, ${apiKeyHourlyStats.apiKeyId} ASC`
: sql`DATE(${apiKeyHourlyStats.hourTimestamp}) ASC, ${apiKeyHourlyStats.apiKeyId} ASC`,
);
Comment on lines +37 to +41
// Define the response schema for api-key-specific usage
const apiKeyUsageSchema = z.object({
id: z.string(),
description: z.string(),
requestCount: z.number(),
const uniqueModels = getUniqueModels(data.activity);
const visibleModels = showAllModels ? uniqueModels : uniqueModels.slice(0, 7);
const uniqueSeries = getUniqueSeries(data.activity, groupBy);
const visibleSeries = showAllModels ? uniqueSeries : uniqueSeries.slice(0, 7);
Comment on lines +481 to +490
const seriesLabelById = new Map<string, string>();
if (groupBy === "apiKey") {
data.activity.forEach((day) => {
day.apiKeyBreakdown.forEach((item) => {
if (!seriesLabelById.has(item.id)) {
seriesLabelById.set(item.id, item.description || item.id);
}
});
});
}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
apps/ui/src/types/activity.ts (1)

61-97: ⚡ Quick win

Collapse ActivitT onto ActivityResponse.

This inline type now duplicates the same DailyActivity contract a second time, so every response-field change has to be kept in sync manually. type ActivitT = ActivityResponse | undefined would remove that drift point.

♻️ Minimal refactor
-export type ActivitT =
-	| {
-			activity: {
-				date: string;
-				requestCount: number;
-				inputTokens: number;
-				outputTokens: number;
-				cachedTokens: number;
-				cacheWriteTokens: number;
-				totalTokens: number;
-				cost: number;
-				inputCost: number;
-				outputCost: number;
-				requestCost: number;
-				dataStorageCost: number;
-				imageInputCost: number;
-				audioInputCost: number;
-				imageOutputCost: number;
-				videoOutputCost: number;
-				cachedInputCost: number;
-				cacheWriteInputCost: number;
-				errorCount: number;
-				errorRate: number;
-				cacheCount: number;
-				cacheRate: number;
-				discountSavings: number;
-				creditsRequestCount: number;
-				apiKeysRequestCount: number;
-				creditsCost: number;
-				apiKeysCost: number;
-				creditsDataStorageCost: number;
-				apiKeysDataStorageCost: number;
-				modelBreakdown: ActivityModelUsage[];
-				apiKeyBreakdown: ActivityApiKeyUsage[];
-			}[];
-	  }
-	| undefined;
+export type ActivitT = ActivityResponse | undefined;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ui/src/types/activity.ts` around lines 61 - 97, Replace the duplicated
inline union type ActivitT with a direct alias to the canonical ActivityResponse
to avoid drift: change the type declaration of ActivitT to be "type ActivitT =
ActivityResponse | undefined", remove the long inline DailyActivity-like shape
(which duplicates fields such as date, requestCount, modelBreakdown:
ActivityModelUsage[], apiKeyBreakdown: ActivityApiKeyUsage[], etc.), and ensure
ActivityResponse is imported or already available in the same module so the
alias compiles.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/ui/src/components/dashboard/activity-chart.tsx`:
- Around line 498-505: The loading/error/empty/no-project branches still use
hardcoded "Model Usage Overview" and "Stacked model..." copy causing
inconsistent wording when groupBy === "apiKey"; update each branch that renders
CardTitle and CardDescription (the same places that use CardTitle,
CardDescription, seriesNoun, breakdownField, periodLabel and selectedProject) to
conditionally choose the API-key wording when groupBy === "apiKey" (e.g., "API
Key Usage Overview" and "Stacked API Key..." variants) so all UI states use the
same groupBy-based text logic as the main happy-path.
- Around line 478-480: The chart currently maps over uniqueSeries (from
getUniqueSeries(data.activity, groupBy)) while the legend is limited by
visibleSeries, causing extra bars/tooltips when showAllModels is false; change
the rendering to iterate over visibleSeries instead of uniqueSeries and, when
not showing all models, aggregate the remaining series into a single "Other"
series (summing their values per x-axis bucket and merging tooltip info) so the
rendered bars and tooltip payload match the legend; apply the same
swap-and-aggregation fix to the other rendering block that currently uses
uniqueSeries (the block analogous to lines 607-623).

In `@apps/ui/src/components/usage/model-usage-client.tsx`:
- Around line 79-92: When the page loads with groupBy=apiKey and a stale
apiKeyId in the URL, normalize by removing apiKeyId and replacing the history
entry; add logic (e.g., in the component init or an effect that runs on
mount/when searchParams changes) to check if searchParams.has("apiKeyId") &&
groupBy === "apiKey" and then create new URLSearchParams from searchParams,
params.delete("apiKeyId"), and call
router.replace(`${buildUrl("model-usage")}?${params.toString()}`) instead of
push. Keep this behavior consistent with the existing updateGroupBy, and ensure
apiKeyFilterDisabled/effectiveApiKeyId logic continues to use the cleaned
params.

---

Nitpick comments:
In `@apps/ui/src/types/activity.ts`:
- Around line 61-97: Replace the duplicated inline union type ActivitT with a
direct alias to the canonical ActivityResponse to avoid drift: change the type
declaration of ActivitT to be "type ActivitT = ActivityResponse | undefined",
remove the long inline DailyActivity-like shape (which duplicates fields such as
date, requestCount, modelBreakdown: ActivityModelUsage[], apiKeyBreakdown:
ActivityApiKeyUsage[], etc.), and ensure ActivityResponse is imported or already
available in the same module so the alias compiles.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0b34e826-d9c2-4792-a7b1-87a4f8ae8add

📥 Commits

Reviewing files that changed from the base of the PR and between 1146e11 and 9b1d264.

⛔ Files ignored due to path filters (4)
  • apps/code/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
  • apps/playground/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
  • apps/ui/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
  • ee/admin/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
📒 Files selected for processing (4)
  • apps/api/src/routes/activity.ts
  • apps/ui/src/components/dashboard/activity-chart.tsx
  • apps/ui/src/components/usage/model-usage-client.tsx
  • apps/ui/src/types/activity.ts

Comment on lines +478 to +480
const uniqueSeries = getUniqueSeries(data.activity, groupBy);
const visibleSeries = showAllModels ? uniqueSeries : uniqueSeries.slice(0, 7);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Cap the rendered series, not just the legend.

visibleSeries only limits the legend. The chart still renders uniqueSeries.map(...), so API-key mode can create a <Bar> per key, with equally large tooltip payloads and unlabeled segments beyond the first 7 legend entries. Please cap/aggregate the rendered series to match what the UI exposes.

Also applies to: 607-623

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ui/src/components/dashboard/activity-chart.tsx` around lines 478 - 480,
The chart currently maps over uniqueSeries (from getUniqueSeries(data.activity,
groupBy)) while the legend is limited by visibleSeries, causing extra
bars/tooltips when showAllModels is false; change the rendering to iterate over
visibleSeries instead of uniqueSeries and, when not showing all models,
aggregate the remaining series into a single "Other" series (summing their
values per x-axis bucket and merging tooltip info) so the rendered bars and
tooltip payload match the legend; apply the same swap-and-aggregation fix to the
other rendering block that currently uses uniqueSeries (the block analogous to
lines 607-623).

Comment thread apps/ui/src/components/dashboard/activity-chart.tsx Outdated
Comment thread apps/ui/src/components/usage/model-usage-client.tsx
steebchen and others added 3 commits May 20, 2026 13:47
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Default callers keep the existing model breakdown query and skip the
new api-key aggregate+join. When groupBy=apiKey is passed, the model
query is skipped and the api-key breakdown is computed instead.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@steebchen
steebchen enabled auto-merge May 20, 2026 12:59

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9ceee30f8a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 579 to +580
modelBreakdown: modelBreakdownByDate.get(day.date) ?? [],
apiKeyBreakdown: [],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor groupBy when apiKeyId filter is set

When a request includes both groupBy=apiKey and apiKeyId, this branch still returns apiKeyBreakdown: [], so the new grouping mode is silently ignored for a valid query combination. The endpoint advertises groupBy in its query schema, and callers outside the UI can send both params; in that case they get totals with no per-key series, which breaks the expected breakdown contract. Either reject this parameter combination explicitly or populate apiKeyBreakdown with the filtered key's series.

Useful? React with 👍 / 👎.

@steebchen
steebchen added this pull request to the merge queue May 20, 2026
Merged via the queue into main with commit 260c73c May 20, 2026
11 checks passed
@steebchen
steebchen deleted the model-breakdown-by-api-key branch May 20, 2026 13:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants