Skip to content

feat: split BYOK vs credits in usage reporting - #3400

Merged
steebchen merged 4 commits into
mainfrom
split-byok-credits-reporting
Aug 4, 2026
Merged

steebchen merged 4 commits into
mainfrom
split-byok-credits-reporting

Conversation

@steebchen

@steebchen steebchen commented Aug 3, 2026 •

Copy link
Copy Markdown
Member

Problem

BYOK ("api-keys" mode) and credits usage were blended in every reported spend number, in both the user dashboard and the admin dashboard. Billing itself was correct — the worker only debits credits for usedMode="credits" rows (plus storage cost for BYOK rows) — but reporting showed the blended total everywhere, so neither users nor admins could tell billed credits spend apart from unbilled BYOK usage.

The data layer was already fully instrumented: log.usedMode records the billing mode per request, and every hourly rollup table has carried creditsCost / apiKeysCost / creditsRequestCount / apiKeysRequestCount since the tables were created (PR #1612), so the split is complete for all history and no migration or backfill is needed. The gaps were purely in the reporting endpoints and the UIs.

Approach

API (additive, backward compatible): every aggregation endpoint now returns the four split fields next to each blended cost/requestCount — /activity nested breakdowns (model/apiKey/user) and /activity/sources, all four /analytics/* endpoints, and the admin metrics, org list, org/project metrics, cost-by-model, timeseries, model-provider-stats and global-stats endpoints. Existing blended fields keep their semantics. /logs gains a server-side usedMode filter.

UI: a shared All / Credits / BYOK segmented selector (persisted in the mode URL param) on every relevant page. Since the server returns all variants at once, switching is a pure client-side view toggle — no extra fetches. Rows are normalized once at the fetch boundary (applyUsageMode / applyUsageModeToDaily), so all downstream charts and reducers work unchanged. Wired in: dashboard, Usage & Metrics, model usage, project analytics, org analytics, team + member detail, developer "my usage", API key stats, agents/sources, and a "Billing" filter on the logs list. Same pattern in ee/admin: global metrics, org list, org/project metrics, cost-by-model tables + timeseries, global stats.

Real bugs fixed along the way

  1. Credits runway (GET /orgs/{id}/credits-runway) divided the balance by blended 7-day spend, understating runway for BYOK-heavy orgs. It now uses what the worker actually debits: creditsCost + apiKeysDataStorageCost.
  2. Admin unusedCredits/overage subtracted blended spend from topped-up credits; now derived from debited spend only (the blended totalSpent field is unchanged, with totalCreditsSpent/totalApiKeysSpent added alongside).
  3. DevPass/Chat Plan margin counted BYOK usage as "real provider cost" LLM Gateway paid, understating margin — those queries now count credits-mode cost only (own commit).

Known limitations (deliberate)

  • The rollups only split cost, request counts, and storage cost by mode. Tokens, errors, cache and latency were never mode-split and can't be reconstructed for history, so those metrics always show all traffic (labeled as such when a mode filter is active). Splitting them going forward would be a follow-up rollup-column addition.
  • Member spend-limit enforcement (team.ts / gateway api-key-usage-limits.ts) still counts BYOK cost toward limits — changing that is a behavior decision left out of scope.
  • The worker comment claims credits-mode storage cost is debited but no code path debits creditsDataStorageCost — billing question, untouched here, flagged as follow-up.

Verification

  • pnpm build — all 17 tasks pass.
  • pnpm test:unit — 222 files, 3698 tests pass, including new coverage: mixed-mode seeds asserting the splits on /activity (day totals, model/apiKey/user/source breakdowns), all four /analytics/* endpoints, the credits-runway formula ((7 + 0.7)/7 = 1.1, explicitly not 707.7/7), the /logs?usedMode= filter, and a new admin-metrics-mode-split.spec.ts (metrics split + debited-spend unusedCredits, org list/org/project splits, cost-by-model splits, DevPass margin excluding BYOK).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added All, Credits, and BYOK usage views across dashboards, analytics, activity, and admin metrics.
    • Added billing-mode filtering for logs.
    • Usage charts, tables, cost breakdowns, and summaries now show mode-specific requests and costs.
    • Admin views distinguish credit spending from BYOK spending and clarify credit balance and margin calculations.
  • Bug Fixes
    • Improved credits runway calculations by excluding BYOK provider costs while including applicable storage costs.
  • Tests
    • Expanded coverage for mode-specific activity, analytics, metrics, filtering, and runway calculations.

Screenshots

User dashboard — All view (split subtitle on Total Spend)

Dashboard with All/Credits/BYOK selector; Total Spend shows $1044.07 credits • $696.05 BYOK (not billed)

User dashboard — BYOK view

Dashboard filtered to BYOK: card retitled BYOK Usage, requests narrowed to BYOK traffic

Usage & Metrics — Costs tab with Credits (billed) / BYOK (not billed) footer

Cost breakdown pie with Credits (billed) and BYOK keys (not billed) totals under the legend

Org analytics — selector in header

Organization analytics with All/Credits/BYOK selector next to the date range

Admin — global dashboard (Credit flow spent split, Cost by Model selector)

Admin dashboard Credit flow card showing Spent, Spent (credits), Spent (BYOK, not billed), Unused; Cost by Model card with mode selector

Admin — org detail metrics, All view

Admin org metrics with mode selector; Total Requests and Total Cost cards show credits/BYOK split subtitles

Admin — org detail metrics, Credits view

Admin org metrics filtered to credits: Credits Requests and Credits Cost cards; Cost by Model totals narrowed

Admin — organizations list (Total Spent split line)

Organizations table with credits • BYOK breakdown under each Total Spent value

steebchen and others added 4 commits August 3, 2026 19:49
Adds creditsCost/apiKeysCost/creditsRequestCount/apiKeysRequestCount to
activity breakdowns, sources, analytics, admin metrics, cost-by-model,
timeseries and global stats. Fixes credits-runway and admin
unusedCredits to use debited spend (creditsCost + BYOK storage), makes
DevPass/ChatPlan margin count credits-mode cost only, and adds a
usedMode filter to /logs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 3, 2026 19:27
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The PR adds credits-versus-BYOK request and cost metrics across API responses, analytics, admin metrics, billing calculations, and dashboards. It also adds URL-based usage-mode selectors and a usedMode filter for logs.

Changes

Credits and BYOK API metrics

Layer / File(s) Summary
Shared aggregation and activity analytics
apps/api/src/lib/*, apps/api/src/routes/activity.ts, apps/api/src/routes/analytics.ts
Adds shared SQL aggregation, validation, numeric mapping, and credits/API-key fields to activity and analytics responses.
Admin metrics and billing calculations
apps/api/src/routes/admin.ts
Adds mode-split metrics to global, organization, project, model, provider, and timeseries responses. Credit balances and subscriber provider costs use credits-only values where specified.
Log filtering and runway calculations
apps/api/src/routes/logs.ts, apps/api/src/routes/organization.ts
Adds the usedMode filter and separates credit spend from BYOK data-storage costs in runway calculations.
API validation coverage
apps/api/src/routes/*.spec.ts
Adds coverage for activity, analytics, admin metrics, log filtering, and credits runway behavior.

Usage-mode presentation

Layer / File(s) Summary
Shared application mode handling
apps/ui/src/lib/usage-mode.ts, apps/ui/src/components/shared/usage-mode-selector.tsx, apps/ui/src/types/activity.ts
Adds total, credits, and BYOK parsing, selection, normalization, URL navigation, and activity types.
Application dashboards and usage views
apps/ui/src/components/**, apps/ui/src/app/dashboard/**
Adds usage-mode selectors and applies mode-specific metrics to dashboards, analytics, model usage, API-key usage, agents, charts, and logs.
Admin dashboards and charts
ee/admin/src/lib/usage-mode.ts, ee/admin/src/components/*, ee/admin/src/app/**
Adds mode-aware selectors, labels, totals, breakdowns, and chart data for organization, project, global, model, and subscriber metrics.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant UsageModeSelector
  participant Dashboard
  participant API
  User->>UsageModeSelector: select total, credits, or BYOK
  UsageModeSelector->>Dashboard: update mode URL parameter
  Dashboard->>API: request activity and metric data
  API-->>Dashboard: blended and mode-split metrics
  Dashboard->>Dashboard: normalize and render selected metrics
Loading

Possibly related PRs

Suggested reviewers: copilot, amineace

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.65% 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
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: separating BYOK and credits in usage reporting.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch split-byok-credits-reporting

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.

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 updates LLM Gateway’s reporting stack to clearly separate credits-billed usage from BYOK (“api-keys”) usage across API aggregation endpoints and both the user and admin dashboards, while keeping existing “blended” totals backward compatible.

Changes:

  • API: Add per-mode split fields (creditsCost, apiKeysCost, creditsRequestCount, apiKeysRequestCount) across activity/analytics/admin aggregation endpoints, plus a /logs usedMode server-side filter.
  • Fix reporting correctness bugs where “debited spend” must exclude BYOK provider cost (e.g., credits runway and admin unused credits/overage).
  • UI + Admin UI: Add a shared All / Credits / BYOK mode selector (persisted via mode URL param) and normalize rows at the fetch boundary so charts/tables can toggle modes client-side without refetching.

Reviewed changes

Copilot reviewed 43 out of 43 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
ee/admin/src/lib/usage-mode.ts Adds admin-side usage-mode parsing and helpers for selecting per-mode cost/request values.
ee/admin/src/components/usage-mode-selector.tsx Introduces admin usage-mode selector persisted in mode URL param.
ee/admin/src/components/cost-by-model-timeseries-chart.tsx Switches cost/request series to respect selected usage mode while keeping tokens blended.
ee/admin/src/components/cost-by-model-chart.tsx Adds per-mode totals/rows rendering and selector for cost-by-model view.
ee/admin/src/app/page.tsx Shows total spent plus split spent (credits vs BYOK) in admin overview.
ee/admin/src/app/organizations/page.tsx Displays per-org split totals (credits vs BYOK) alongside blended total.
ee/admin/src/app/organizations/[orgId]/projects/[projectId]/project-metrics.tsx Adds usage-mode selector and per-mode metric cards for project metrics.
ee/admin/src/app/organizations/[orgId]/org-metrics.tsx Adds usage-mode selector and per-mode metric cards for org metrics.
ee/admin/src/app/global-stats/client.tsx Updates global stats subtitle to show credits vs BYOK split when present.
ee/admin/src/app/devpass/[orgId]/page.tsx Clarifies “real provider cost” is credits-mode only.
ee/admin/src/app/chat-plans/[orgId]/page.tsx Clarifies “real provider cost” is credits-mode only.
apps/ui/src/types/activity.ts Extends activity types to include per-mode split fields and storage split.
apps/ui/src/lib/usage-mode.ts Adds shared UI usage-mode model + normalization helpers (applyUsageMode*).
apps/ui/src/components/usage/usage-client.tsx Adds usage-mode selector and all-traffic note for blended-only metrics.
apps/ui/src/components/usage/usage-chart.tsx Uses per-mode request counts for the requests-over-time chart.
apps/ui/src/components/usage/model-usage-table.tsx Applies mode normalization so model rows reflect selected billing view.
apps/ui/src/components/usage/model-usage-client.tsx Adds selector to model usage page header.
apps/ui/src/components/usage/cost-breakdown-chart.tsx Applies mode normalization for breakdown and shows split totals in total mode.
apps/ui/src/components/shared/usage-mode-selector.tsx Introduces reusable UI usage-mode segmented control stored in URL.
apps/ui/src/components/enterprise/feature-showcase.tsx Updates mock analytics rows to include split fields for compatibility.
apps/ui/src/components/dashboard/developer-dashboard-client.tsx Adds selector; narrows cost/requests to selected mode for developer dashboard.
apps/ui/src/components/dashboard/dashboard-client.tsx Adds selector and mode-normalizes daily rows; shows split totals in total mode.
apps/ui/src/components/dashboard/activity-chart.tsx Mode-normalizes /activity response for charts without extra fetches.
apps/ui/src/components/api-keys/api-key-stats-client.tsx Adds selector; adjusts error-rate computation to remain blended while costs/requests can be per-mode.
apps/ui/src/components/analytics/chart-helpers.ts Extends analytics row shapes to include per-mode split fields.
apps/ui/src/components/analytics/analytics-client.tsx Adds selector and mode-normalizes analytics activity rows.
apps/ui/src/components/activity/recent-logs.tsx Adds UI filter for usedMode and passes it to /logs.
apps/ui/src/components/activity/agents-view.tsx Adds selector and mode-normalizes agent/source rows.
apps/ui/src/app/dashboard/[orgId]/org/team/team-client.tsx Adds selector and mode-normalizes per-member usage rows.
apps/ui/src/app/dashboard/[orgId]/org/team/[userId]/member-detail-client.tsx Adds selector and mode-normalizes member detail breakdowns/top lists.
apps/ui/src/app/dashboard/[orgId]/org/analytics/org-analytics-client.tsx Adds selector; mode-normalizes org activity rows and breakdowns.
apps/api/src/routes/organization.ts Fixes credits-runway burn-rate to use debited spend (credits + BYOK storage), not blended cost.
apps/api/src/routes/organization.spec.ts Adds regression test for credits-runway debited-spend calculation.
apps/api/src/routes/logs.ts Adds usedMode query param and SQL filter support for logs listing.
apps/api/src/routes/logs.spec.ts Adds tests validating usedMode filtering and “all” behavior.
apps/api/src/routes/analytics.ts Adds mode-split fields to analytics endpoints and response schemas.
apps/api/src/routes/analytics.spec.ts Adds coverage for mode split across analytics endpoints.
apps/api/src/routes/admin.ts Adds mode-split metrics; fixes unusedCredits/overage to use debited spend; excludes BYOK from “real provider cost” for plans.
apps/api/src/routes/admin-metrics-mode-split.spec.ts Adds admin regression tests for splits and debited-spend computations.
apps/api/src/routes/activity.ts Adds mode-split fields to activity endpoints (daily + breakdowns + sources).
apps/api/src/routes/activity.spec.ts Adds tests covering activity mode split (day totals, breakdowns, sources).
apps/api/src/lib/user-usage-breakdown.ts Extends user usage breakdown helper to include mode split fields.
apps/api/src/lib/mode-split.ts Introduces shared helper for selecting + mapping split fields in rollup queries.

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

Comment on lines +49 to +59
<button
key={option.value}
type="button"
onClick={() => setMode(option.value)}
title={
option.value === "api-keys"
? "Usage served by your own provider keys (not billed to credits)"
: option.value === "credits"
? "Usage billed against your credit balance"
: "All traffic"
}
Comment on lines +47 to +52
<Button
key={option.value}
variant={mode === option.value ? "default" : "outline"}
size="sm"
onClick={() => setMode(option.value)}
>

@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

Caution

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

⚠️ Outside diff range comments (3)
apps/api/src/routes/analytics.ts (1)

536-582: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Make top-N model/provider cards mode-aware.

modelRows are ordered by blended cost, then topModels and topProviders are sliced from that blended set. The client only re-sorts the returned rows by cost after applying the selected mode, so Credits or BYOK top spenders can be hidden if they are not in the blended top-N. Include a mode-aware ordering, or fetch enough candidates before the mode-specific mapModeSplit(...).slice(...)/sort(...).slice(...).

🤖 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/api/src/routes/analytics.ts` around lines 536 - 582, Update the
topModels and topProviders selection in the analytics route to rank candidates
using the selected mode’s cost rather than only blended r.cost. Apply
mapModeSplit-derived mode costs before slicing, or retain enough
modelRows/provider entries to perform mode-specific sorting and top-N selection
so Credits and BYOK leaders are not excluded.
apps/ui/src/components/dashboard/dashboard-client.tsx (1)

305-369: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the blended request count for reliability rates.

applyUsageModeToDaily keeps cacheCount and errorCount blended across all traffic, but it rewrites requestCount for credits and api-keys views. dashboard-client.tsx reports cacheHitRate = totalCached / totalRequests, and ErrorsReliabilityCard computes both cacheRate and errorRate from the mode-adjusted requestCount. With mode filtering, these can exceed 100% or otherwise misreport blended metrics. Compute the denominator from the untransformed activity data for these rates.

🤖 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/dashboard-client.tsx` around lines 305 -
369, Use the untransformed rawActivityData requestCount total as the denominator
for blended reliability metrics, while retaining activityData for mode-specific
cost and request totals. Update cacheHitRate and the request-count value passed
to ErrorsReliabilityCard so cache and error rates use the raw blended request
count, and keep the existing zero-denominator safeguards.
apps/ui/src/components/usage/model-usage-table.tsx (1)

134-151: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Sum mode-split fields when merging model entries across days.

The merge loop sums requestCount, inputTokens, outputTokens, totalTokens, and cost for repeated provider|id keys, but does not sum creditsRequestCount, apiKeysRequestCount, creditsCost, or apiKeysCost. For a model that appears across multiple days, the aggregated entry keeps only the first day's values for these four fields instead of the total across all days.

This does not affect the current table, because it never reads these fields. If any future consumer of this aggregated map reads them, it gets an incorrect partial value.

🐛 Proposed fix to sum the mode-split fields
 	data.activity.forEach((day) => {
 		day.modelBreakdown.forEach((rawModel) => {
 			const model = applyUsageMode(rawModel, usageMode);
 			const key = `${model.provider}|${model.id}`;
 			if (modelMap.has(key)) {
 				const existing = modelMap.get(key)!;
 				existing.requestCount += model.requestCount;
 				existing.inputTokens += model.inputTokens;
 				existing.outputTokens += model.outputTokens;
 				existing.totalTokens += model.totalTokens;
 				existing.cost += model.cost;
+				existing.creditsRequestCount += model.creditsRequestCount;
+				existing.apiKeysRequestCount += model.apiKeysRequestCount;
+				existing.creditsCost += model.creditsCost;
+				existing.apiKeysCost += model.apiKeysCost;
 			} else {
 				modelMap.set(key, { ...model });
 			}
 		});
 	});
🤖 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/usage/model-usage-table.tsx` around lines 134 - 151,
Update the existing-entry merge branch in the modelMap aggregation loop to also
add creditsRequestCount, apiKeysRequestCount, creditsCost, and apiKeysCost from
each repeated model entry, alongside the existing totals. Keep the current
provider|id grouping and other field aggregation unchanged.
🧹 Nitpick comments (1)
apps/api/src/lib/mode-split.ts (1)

51-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

mapModeSplit isn't null-safe, so four call sites in analytics.ts re-implement its logic manually. mapModeSplit requires a non-nullable ModeSplitRow. Wherever the aggregate row can be undefined (a single-row .select() result, or a Map.get() lookup), the code falls back to four repeated Number(row?.field ?? 0) conversions instead of reusing the helper.

  • apps/api/src/lib/mode-split.ts#L51-L58: widen mapModeSplit to accept ModeSplitRow | null | undefined and default each field to 0 internally, so callers can pass a possibly-undefined row directly.
  • apps/api/src/routes/analytics.ts#L502-L505: replace the four Number(summaryRow?.field ?? 0) lines with ...mapModeSplit(summaryRow).
  • apps/api/src/routes/analytics.ts#L825-L828: replace the four Number(s?.field ?? 0) lines with ...mapModeSplit(s).
  • apps/api/src/routes/analytics.ts#L867-L870: replace the four Number(r?.field ?? 0) lines with ...mapModeSplit(r).
  • apps/api/src/routes/analytics.ts#L1286-L1289: replace the four Number(totals?.field ?? 0) lines with ...mapModeSplit(totals).
🤖 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/api/src/lib/mode-split.ts` around lines 51 - 58, Make mapModeSplit
null-safe by accepting ModeSplitRow, null, or undefined and defaulting each
mapped field to 0. In apps/api/src/lib/mode-split.ts#L51-L58 update mapModeSplit
accordingly; in apps/api/src/routes/analytics.ts#L502-L505, `#L825-L828`,
`#L867-L870`, and `#L1286-L1289` replace the repeated Number(field ?? 0) conversions
with mapModeSplit applied to the corresponding row.

Source: Coding guidelines

🤖 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 329-341: Mark token metrics as all-traffic values whenever the
selected usage mode is not total, without changing costs or request counts.
Update the Tokens metric in apps/ui/src/components/dashboard/activity-chart.tsx
lines 329-341, the Tokens summary in
apps/ui/src/app/dashboard/[orgId]/org/analytics/org-analytics-client.tsx lines
221-225, Total Tokens and Error Rate in
apps/ui/src/app/dashboard/[orgId]/org/team/[userId]/member-detail-client.tsx
lines 108-145, agent token totals in
apps/ui/src/components/activity/agents-view.tsx lines 703-708, and the Tokens
summary in apps/ui/src/components/dashboard/developer-dashboard-client.tsx lines
107-129; use the existing usage-mode state and either add an all-traffic label
or disable the metric as appropriate.

In `@apps/ui/src/components/dashboard/dashboard-client.tsx`:
- Around line 546-571: Update the subtitle construction in the spend summary
block to avoid presenting blended totalRequestCost as mode-specific when
usageMode is "credits" or "api-keys". Either omit the requests segment outside
"total" mode or explicitly label it as covering all traffic, while preserving
the existing total-mode wording and other mode-adjusted values.

In `@apps/ui/src/components/shared/usage-mode-selector.tsx`:
- Around line 48-68: Update the buttons rendered by USAGE_MODE_OPTIONS.map in
the usage mode selector to expose selection semantics: add the appropriate group
semantics to the surrounding control and set aria-pressed on each button based
on whether option.value matches mode, while preserving the existing visual
styling and click behavior.

---

Outside diff comments:
In `@apps/api/src/routes/analytics.ts`:
- Around line 536-582: Update the topModels and topProviders selection in the
analytics route to rank candidates using the selected mode’s cost rather than
only blended r.cost. Apply mapModeSplit-derived mode costs before slicing, or
retain enough modelRows/provider entries to perform mode-specific sorting and
top-N selection so Credits and BYOK leaders are not excluded.

In `@apps/ui/src/components/dashboard/dashboard-client.tsx`:
- Around line 305-369: Use the untransformed rawActivityData requestCount total
as the denominator for blended reliability metrics, while retaining activityData
for mode-specific cost and request totals. Update cacheHitRate and the
request-count value passed to ErrorsReliabilityCard so cache and error rates use
the raw blended request count, and keep the existing zero-denominator
safeguards.

In `@apps/ui/src/components/usage/model-usage-table.tsx`:
- Around line 134-151: Update the existing-entry merge branch in the modelMap
aggregation loop to also add creditsRequestCount, apiKeysRequestCount,
creditsCost, and apiKeysCost from each repeated model entry, alongside the
existing totals. Keep the current provider|id grouping and other field
aggregation unchanged.

---

Nitpick comments:
In `@apps/api/src/lib/mode-split.ts`:
- Around line 51-58: Make mapModeSplit null-safe by accepting ModeSplitRow,
null, or undefined and defaulting each mapped field to 0. In
apps/api/src/lib/mode-split.ts#L51-L58 update mapModeSplit accordingly; in
apps/api/src/routes/analytics.ts#L502-L505, `#L825-L828`, `#L867-L870`, and
`#L1286-L1289` replace the repeated Number(field ?? 0) conversions with
mapModeSplit applied to the corresponding row.
🪄 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 Plus

Run ID: d3c6fe9b-1145-4589-a4f0-43923402602a

📥 Commits

Reviewing files that changed from the base of the PR and between 75fef5c and 81ac8da.

📒 Files selected for processing (43)
  • apps/api/src/lib/mode-split.ts
  • apps/api/src/lib/user-usage-breakdown.ts
  • apps/api/src/routes/activity.spec.ts
  • apps/api/src/routes/activity.ts
  • apps/api/src/routes/admin-metrics-mode-split.spec.ts
  • apps/api/src/routes/admin.ts
  • apps/api/src/routes/analytics.spec.ts
  • apps/api/src/routes/analytics.ts
  • apps/api/src/routes/logs.spec.ts
  • apps/api/src/routes/logs.ts
  • apps/api/src/routes/organization.spec.ts
  • apps/api/src/routes/organization.ts
  • apps/ui/src/app/dashboard/[orgId]/org/analytics/org-analytics-client.tsx
  • apps/ui/src/app/dashboard/[orgId]/org/team/[userId]/member-detail-client.tsx
  • apps/ui/src/app/dashboard/[orgId]/org/team/team-client.tsx
  • apps/ui/src/components/activity/agents-view.tsx
  • apps/ui/src/components/activity/recent-logs.tsx
  • apps/ui/src/components/analytics/analytics-client.tsx
  • apps/ui/src/components/analytics/chart-helpers.ts
  • apps/ui/src/components/api-keys/api-key-stats-client.tsx
  • apps/ui/src/components/dashboard/activity-chart.tsx
  • apps/ui/src/components/dashboard/dashboard-client.tsx
  • apps/ui/src/components/dashboard/developer-dashboard-client.tsx
  • apps/ui/src/components/enterprise/feature-showcase.tsx
  • apps/ui/src/components/shared/usage-mode-selector.tsx
  • apps/ui/src/components/usage/cost-breakdown-chart.tsx
  • apps/ui/src/components/usage/model-usage-client.tsx
  • apps/ui/src/components/usage/model-usage-table.tsx
  • apps/ui/src/components/usage/usage-chart.tsx
  • apps/ui/src/components/usage/usage-client.tsx
  • apps/ui/src/lib/usage-mode.ts
  • apps/ui/src/types/activity.ts
  • ee/admin/src/app/chat-plans/[orgId]/page.tsx
  • ee/admin/src/app/devpass/[orgId]/page.tsx
  • ee/admin/src/app/global-stats/client.tsx
  • ee/admin/src/app/organizations/[orgId]/org-metrics.tsx
  • ee/admin/src/app/organizations/[orgId]/projects/[projectId]/project-metrics.tsx
  • ee/admin/src/app/organizations/page.tsx
  • ee/admin/src/app/page.tsx
  • ee/admin/src/components/cost-by-model-chart.tsx
  • ee/admin/src/components/cost-by-model-timeseries-chart.tsx
  • ee/admin/src/components/usage-mode-selector.tsx
  • ee/admin/src/lib/usage-mode.ts

Comment on lines +329 to +341
const usageMode = useUsageMode();
const data = useMemo(
() =>
rawData
? {
...rawData,
activity: rawData.activity.map((day) =>
applyUsageModeToDaily(day, usageMode),
),
}
: rawData,
[rawData, usageMode],
);

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Mark metrics that remain blended across usage modes.

The selected mode changes costs and request counts. Tokens and error rates remain all-traffic values. Do not present these values as mode-specific metrics.

  • apps/ui/src/components/dashboard/activity-chart.tsx#L329-L341: When mode is not total, label the Tokens metric as all traffic or disable it.
  • apps/ui/src/app/dashboard/[orgId]/org/analytics/org-analytics-client.tsx#L221-L225: Label the Tokens summary as all traffic when a non-total mode is active.
  • apps/ui/src/app/dashboard/[orgId]/org/team/[userId]/member-detail-client.tsx#L108-L145: Label Total Tokens and Error Rate as all traffic when a non-total mode is active.
  • apps/ui/src/components/activity/agents-view.tsx#L703-L708: Label agent token totals as all traffic when a non-total mode is active.
  • apps/ui/src/components/dashboard/developer-dashboard-client.tsx#L107-L129: Label the Tokens summary as all traffic when a non-total mode is active.
📍 Affects 5 files
  • apps/ui/src/components/dashboard/activity-chart.tsx#L329-L341 (this comment)
  • apps/ui/src/app/dashboard/[orgId]/org/analytics/org-analytics-client.tsx#L221-L225
  • apps/ui/src/app/dashboard/[orgId]/org/team/[userId]/member-detail-client.tsx#L108-L145
  • apps/ui/src/components/activity/agents-view.tsx#L703-L708
  • apps/ui/src/components/dashboard/developer-dashboard-client.tsx#L107-L129
🤖 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 329 - 341,
Mark token metrics as all-traffic values whenever the selected usage mode is not
total, without changing costs or request counts. Update the Tokens metric in
apps/ui/src/components/dashboard/activity-chart.tsx lines 329-341, the Tokens
summary in
apps/ui/src/app/dashboard/[orgId]/org/analytics/org-analytics-client.tsx lines
221-225, Total Tokens and Error Rate in
apps/ui/src/app/dashboard/[orgId]/org/team/[userId]/member-detail-client.tsx
lines 108-145, agent token totals in
apps/ui/src/components/activity/agents-view.tsx lines 703-708, and the Tokens
summary in apps/ui/src/components/dashboard/developer-dashboard-client.tsx lines
107-129; use the existing usage-mode state and either add an all-traffic label
or disable the metric as appropriate.

Comment on lines +546 to +571
label={
usageMode === "credits"
? "Credits Spend"
: usageMode === "api-keys"
? "BYOK Usage"
: "Total Spend"
}
value={`$${totalCost.toFixed(2)}`}
subtitle={
totalRequests > 0
? `avg $${avgCostPerRequest.toFixed(4)} per request${
totalRequestCost > 0
? ` • $${totalRequestCost.toFixed(2)} requests`
: ""
}${
totalDataStorageCost > 0
? ` • $${totalDataStorageCost.toFixed(4)} storage`
: ""
}`
: `${format(from, "MMM d")} – ${format(to, "MMM d")}`
usageMode === "total" &&
totalCreditsCost > 0 &&
totalApiKeysCost > 0
? `$${totalCreditsCost.toFixed(2)} credits • $${totalApiKeysCost.toFixed(2)} BYOK (not billed)`
: usageMode === "api-keys" && totalCost > 0
? "Served by your provider keys — not billed to credits"
: totalRequests > 0
? `avg $${avgCostPerRequest.toFixed(4)} per request${
totalRequestCost > 0
? ` • $${totalRequestCost.toFixed(2)} requests`
: ""
}${
totalDataStorageCost > 0
? ` • $${totalDataStorageCost.toFixed(4)} storage`
: ""
}`
: `${format(from, "MMM d")} – ${format(to, "MMM d")}`

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Spend subtitle mixes a mode-adjusted figure with a blended figure.

In Credits or BYOK mode, the subtitle falls through to avg $X per request • $Y requests • $Z storage (Line 562-570). totalDataStorageCost ($Z storage) is mode-adjusted, because dataStorageCost has per-mode columns and gets rewritten by applyUsageModeToDaily. totalRequestCost ($Y requests, Line 340-341) has no per-mode column and always stays blended.

The result: within the same subtitle, $Z storage reflects only the selected mode, but $Y requests reflects all traffic. Combined with the mode-specific label ("Credits Spend" / "BYOK Usage") and the mode-specific $X per request average, this makes the request-fee figure look like it belongs to the selected mode when it does not.

Either omit the blended $Y requests segment when usageMode !== "total", or label it explicitly as blended (for example, "$Y requests (all traffic)").

🤖 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/dashboard-client.tsx` around lines 546 -
571, Update the subtitle construction in the spend summary block to avoid
presenting blended totalRequestCost as mode-specific when usageMode is "credits"
or "api-keys". Either omit the requests segment outside "total" mode or
explicitly label it as covering all traffic, while preserving the existing
total-mode wording and other mode-adjusted values.

Comment on lines +48 to +68
{USAGE_MODE_OPTIONS.map((option) => (
<button
key={option.value}
type="button"
onClick={() => setMode(option.value)}
title={
option.value === "api-keys"
? "Usage served by your own provider keys (not billed to credits)"
: option.value === "credits"
? "Usage billed against your credit balance"
: "All traffic"
}
className={cn(
"rounded-md px-3 py-1 text-xs font-medium transition-colors",
mode === option.value
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground",
)}
>
{option.label}
</button>

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Expose the selected mode to assistive technology.

Line 62 applies selected state only through CSS. Add group semantics and aria-pressed to each button. This lets screen readers identify the active usage mode.

Proposed fix
 		<div
+			role="group"
+			aria-label="Usage mode"
 			className={cn(
 				"inline-flex items-center rounded-lg border border-border/60 bg-muted/40 p-0.5",
 				className,
@@
 					key={option.value}
 					type="button"
+					aria-pressed={mode === option.value}
 					onClick={() => setMode(option.value)}
🤖 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/shared/usage-mode-selector.tsx` around lines 48 - 68,
Update the buttons rendered by USAGE_MODE_OPTIONS.map in the usage mode selector
to expose selection semantics: add the appropriate group semantics to the
surrounding control and set aria-pressed on each button based on whether
option.value matches mode, while preserving the existing visual styling and
click behavior.

@steebchen
steebchen merged commit 19d6b0d into main Aug 4, 2026
14 checks passed
@steebchen
steebchen deleted the split-byok-credits-reporting branch August 4, 2026 17:05
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