Repository navigation
feat(ui): add API key breakdown on model usage page - #2346
Conversation
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>
|
Warning Rate limit exceeded
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 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 configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (4)
📒 Files selected for processing (2)
WalkthroughBackend 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. ChangesAPI Key Activity Breakdown Visualization
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
💡 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".
| .from(apiKeyHourlyStats) | ||
| .leftJoin(apiKey, eq(apiKey.id, apiKeyHourlyStats.apiKeyId)) | ||
| .where( | ||
| and( | ||
| inArray(apiKeyHourlyStats.projectId, projectIds), |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
api key means api key not provide rkey, so this includes all usage regardless of which mode the user uses
There was a problem hiding this comment.
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:
/activitynow returnsapiKeyBreakdown(aggregated fromapi_key_hourly_statsjoined toapi_keyfor 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.
| // 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`, | ||
| ); |
| // 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); |
| 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); | ||
| } | ||
| }); | ||
| }); | ||
| } |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
apps/ui/src/types/activity.ts (1)
61-97: ⚡ Quick winCollapse
ActivitTontoActivityResponse.This inline type now duplicates the same
DailyActivitycontract a second time, so every response-field change has to be kept in sync manually.type ActivitT = ActivityResponse | undefinedwould 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
⛔ Files ignored due to path filters (4)
apps/code/src/lib/api/v1.d.tsis excluded by!**/v1.d.tsapps/playground/src/lib/api/v1.d.tsis excluded by!**/v1.d.tsapps/ui/src/lib/api/v1.d.tsis excluded by!**/v1.d.tsee/admin/src/lib/api/v1.d.tsis excluded by!**/v1.d.ts
📒 Files selected for processing (4)
apps/api/src/routes/activity.tsapps/ui/src/components/dashboard/activity-chart.tsxapps/ui/src/components/usage/model-usage-client.tsxapps/ui/src/types/activity.ts
| const uniqueSeries = getUniqueSeries(data.activity, groupBy); | ||
| const visibleSeries = showAllModels ? uniqueSeries : uniqueSeries.slice(0, 7); | ||
|
|
There was a problem hiding this comment.
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).
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>
There was a problem hiding this comment.
💡 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".
| modelBreakdown: modelBreakdownByDate.get(day.date) ?? [], | ||
| apiKeyBreakdown: [], |
There was a problem hiding this comment.
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 👍 / 👎.
Summary
/activitynow returns anapiKeyBreakdownalongsidemodelBreakdown(queried fromapi_key_hourly_statsjoined withapi_keyfor the human-readable description)Test plan
vitest run apps/api/src/routes/activity.spec.ts)🤖 Generated with Claude Code
Summary by CodeRabbit