feat(ptu): admin UI for PTU reservations + Usage page column + CSV export - #33302
feat(ptu): admin UI for PTU reservations + Usage page column + CSV export#33302yucheng-berri wants to merge 4 commits into
Conversation
Greptile SummaryThis PR delivers stage 4 of the PTU-reservation feature: an admin UI page for managing reservations, two new stat cards on the team Usage view, and CSV/JSON export enrichment — all fully gated behind
Confidence Score: 5/5Safe to merge — all changes are additive, fully gated behind a feature flag, and the two issues flagged in the prior review are correctly resolved. Both previously flagged findings are fixed: the CSV/JSON export inconsistency (hasFlatCost guard now applied uniformly in both generateDailyData and generateMetadata) and the unused networking export (removed). The remaining changes introduce a new admin page, stat cards, and networking calls that follow existing codebase patterns and are backed by 166 passing tests. No correctness or data-integrity issues were found on a fresh read of the diff. No files require special attention.
|
| Filename | Overview |
|---|---|
| ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts | Introduces hasFlatCost guard; now applied consistently to both generateDailyData and generateMetadata — the prior export inconsistency is resolved. |
| ui/litellm-dashboard/src/components/networking.tsx | Adds ptuReservationListCall, ptuReservationCreateCall, ptuReservationCloseCall. The previously flagged dead ptuReservationInfoCall has been removed. |
| ui/litellm-dashboard/src/app/(dashboard)/ptu-reservations/_components/ptu_reservation_panel.tsx | New admin panel with feature-flag gating, role gating, and status badge logic. |
| ui/litellm-dashboard/src/app/(dashboard)/hooks/ptuReservations/usePtuReservations.ts | Clean React Query hooks for list, create, and close mutations. Mutations correctly invalidate ptuReservationKeys.all on success. |
| ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx | Adds Flat Cost and Total Cost stat cards for team entities behind the PTU flag. Grid expands 5→7 only when flag is on. |
Reviews (2): Last reviewed commit: "fix(ptu-ui): CI + greptile p1/p2 - align..." | Re-trigger Greptile
| if (entityType === "team") { | ||
| const flatCost = spendData.metadata.total_flat_cost ?? 0; | ||
| summary.total_flat_cost = flatCost; | ||
| summary.total_cost = spendData.metadata.total_spend + flatCost; |
There was a problem hiding this comment.
Inconsistent flat-cost detection between CSV and JSON export
generateDailyData adds flat cost columns to the CSV only when hasFlatCost(spendData) returns true (data-shape driven), but generateMetadata always adds total_flat_cost and total_cost to the JSON summary for any team entity, even when the backend never returned total_flat_cost (it defaults to ?? 0). A team export with PTU disabled will therefore produce a JSON summary containing total_flat_cost: 0 and total_cost === total_spend while the CSV has no flat cost columns at all — two halves of the same export that disagree. The PR description explicitly states "Detection is data-shape driven: spendData.metadata.total_flat_cost presence, not entityType", but generateMetadata violates that contract. Aligning both to hasFlatCost would make the exports consistent.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
4454840 to
66aca9a
Compare
…eck; dead code - generateMetadata now gates total_flat_cost/total_cost on hasFlatCost(spendData) same as generateDailyData, so CSV and JSON stay consistent for the same request. Addresses greptile P1 on #33302. - Removes ptuReservationInfoCall from networking.tsx; unused in this stack and greptile flagged it as dead code (P2). - Fixes 'Type PtuReservationListFilters has no properties in common with ListParams' next.js build error by adapting the filters shape to the createQueryKeys ListParams contract. - Types summary as ExportMetadata['summary'] instead of Record<string, any> so Next.js typecheck accepts it. - Prettier reformatted ptu_reservation_panel.tsx.
893c60e to
2519d21
Compare
|
@greptileai fixed both findings:
Also fixed a Next.js typecheck failure surfaced by CI: Rebased the stack onto latest |
66aca9a to
69dd1e1
Compare
…eck; dead code - generateMetadata now gates total_flat_cost/total_cost on hasFlatCost(spendData) same as generateDailyData, so CSV and JSON stay consistent for the same request. Addresses greptile P1 on #33302. - Removes ptuReservationInfoCall from networking.tsx; unused in this stack and greptile flagged it as dead code (P2). - Fixes 'Type PtuReservationListFilters has no properties in common with ListParams' next.js build error by adapting the filters shape to the createQueryKeys ListParams contract. - Types summary as ExportMetadata['summary'] instead of Record<string, any> so Next.js typecheck accepts it. - Prettier reformatted ptu_reservation_panel.tsx.
2519d21 to
fbad97e
Compare
69dd1e1 to
e724ae5
Compare
…eck; dead code - generateMetadata now gates total_flat_cost/total_cost on hasFlatCost(spendData) same as generateDailyData, so CSV and JSON stay consistent for the same request. Addresses greptile P1 on #33302. - Removes ptuReservationInfoCall from networking.tsx; unused in this stack and greptile flagged it as dead code (P2). - Fixes 'Type PtuReservationListFilters has no properties in common with ListParams' next.js build error by adapting the filters shape to the createQueryKeys ListParams contract. - Types summary as ExportMetadata['summary'] instead of Record<string, any> so Next.js typecheck accepts it. - Prettier reformatted ptu_reservation_panel.tsx.
fbad97e to
f80a058
Compare
…eck; dead code - generateMetadata now gates total_flat_cost/total_cost on hasFlatCost(spendData) same as generateDailyData, so CSV and JSON stay consistent for the same request. Addresses greptile P1 on #33302. - Removes ptuReservationInfoCall from networking.tsx; unused in this stack and greptile flagged it as dead code (P2). - Fixes 'Type PtuReservationListFilters has no properties in common with ListParams' next.js build error by adapting the filters shape to the createQueryKeys ListParams contract. - Types summary as ExportMetadata['summary'] instead of Record<string, any> so Next.js typecheck accepts it. - Prettier reformatted ptu_reservation_panel.tsx.
f80a058 to
4100e34
Compare
…port Adds three UI surfaces gated by enable_ptu_cost_attribution: 1. PTU Reservations admin page under /ptu-reservations. Feature-flagged panel with a list table + create modal + close modal. Only proxy admin can create/close; non-admin sees a friendly denial. Uses antd components (tremor phase-out) and TableIconActionButton for row actions. 2. Usage page > Team view: adds Flat Cost and Total Cost stat cards next to Total Spend when the flag is on. Non-team entities are unaffected; grid reflows from 5 to 7 items. 3. EntityUsageExport: CSV daily export gains Flat Cost ($) and Total Cost ($) columns when the response carries total_flat_cost. JSON export summary gains total_flat_cost and total_cost for teams. Networking: - ptuReservationListCall / InfoCall / CreateCall / CloseCall in networking.tsx. - usePtuReservations + useCreatePtuReservation + useClosePtuReservation React Query hooks mirroring useBudgets. - useIsPtuCostAttributionEnabled reads /config/list general_settings. Nav: - 'PTU Reservations' item next to Budgets under the admin group. - migratedPages entry so deep-links land on the path-based route. UI-side detection of flat cost is data-shape driven: exports check for spendData.metadata.total_flat_cost != null rather than plumbing entityType through every helper. Backend only populates that field for team responses today, so the effect is identical without the extra parameter across the daily-export chain. Tests (166 passed): - ptu_reservation_panel.test.tsx: 6 tests — flag-off state, loading, empty state, table render, create-modal open, non-admin denial. - utils.test.ts: 2 new tests covering the flat-cost column shape and metadata inclusion for team exports. - EntityUsage.test.tsx: mocked the new hook so the existing 25 tests keep passing without needing a QueryClientProvider wrapper. No backend or hot-path changes. No CSV export changes for non-team entities. No changes to budgets or LiteLLM_TeamTable.spend.
…eck; dead code - generateMetadata now gates total_flat_cost/total_cost on hasFlatCost(spendData) same as generateDailyData, so CSV and JSON stay consistent for the same request. Addresses greptile P1 on #33302. - Removes ptuReservationInfoCall from networking.tsx; unused in this stack and greptile flagged it as dead code (P2). - Fixes 'Type PtuReservationListFilters has no properties in common with ListParams' next.js build error by adapting the filters shape to the createQueryKeys ListParams contract. - Types summary as ExportMetadata['summary'] instead of Record<string, any> so Next.js typecheck accepts it. - Prettier reformatted ptu_reservation_panel.tsx.
e724ae5 to
48d46cc
Compare
4100e34 to
06d8b45
Compare
The useIsPtuCostAttributionEnabled hook was reading /config/list?config_type=general_settings, which is built from the fixed ConfigGeneralSettings schema and does not contain enable_ptu_cost_attribution. The flag lives in the ui_settings surface (/get/ui_settings) and is synced into the in-process general_settings dict at runtime. Point the hook at useUISettings so the panel and Usage Flat Cost column reflect the flag state. Thread enablePtuCostAttribution through SidebarProvider so the PTU Reservations nav item appears and disappears with the flag, matching the enable_chat_ui / enable_projects_ui pattern.
|
Follow-up fix pushed as |
The Team Usage view's Daily Spend chart previously visualized only per-request spend, so a team with sizeable PTU flat cost saw the Flat Cost card show a large aggregate while the per-day bars stayed flat at fractional cents. Backend data was correct; this was a chart-only readability gap. Bars now stack Request spend (cyan) on top of Flat cost (violet), with the legend on so the two series are labeled. The custom tooltip splits into three lines: Request spend, Flat cost, Total cost. Data is projected to friendly top-level keys before it hits the chart so the legend reads "Flat cost" and "Request spend" instead of "metrics.spend" / "metrics.flat_cost". Scope is Team Usage only, matching where flat_cost is exposed today. Global Usage / Project Spend view does not surface flat_cost and is intentionally untouched.
| data={[...spendData.results] | ||
| .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()) | ||
| .map((row) => ({ | ||
| ...row, | ||
| "Request spend": row.metrics.spend ?? 0, | ||
| "Flat cost": row.metrics.flat_cost ?? 0, | ||
| }))} | ||
| index="date" | ||
| categories={["metrics.spend"]} | ||
| colors={["cyan"]} | ||
| categories={["Request spend", "Flat cost"]} | ||
| colors={["cyan", "violet"]} | ||
| stack={true} | ||
| valueFormatter={valueFormatterSpend} | ||
| yAxisWidth={100} | ||
| showLegend={false} | ||
| showLegend={true} |
There was a problem hiding this comment.
🟡 Daily spend chart always shows an empty flat-cost series even when the feature is off
The daily spend chart is rebuilt to always include a second "Flat cost" bar series, a legend, and a relabeled tooltip (categories={["Request spend", "Flat cost"]} at ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx:585-598) without checking the feature flag, so views that should look unchanged now display an extra always-zero cost bar.
Impact: Every non-team usage view, and team views when PTU cost attribution is disabled, show a misleading empty "Flat cost" series and altered tooltip labels instead of the classic single-spend chart.
Feature-flag gating applied to stat cards but not the chart
The stat cards correctly gate on showFlatCost (EntityUsage.tsx:522 and :529), which is entityType === "team" && ptuAttributionEnabled (EntityUsage.tsx:120). The chart block below (EntityUsage.tsx:585-611) is inside the same Cost TabPanel rendered for all entity types, but it unconditionally maps in "Flat cost": row.metrics.flat_cost ?? 0, sets categories to two series, stack={true}, showLegend={true}, and rewrites the tooltip to show "Request spend / Flat cost / Total cost". For non-team entities and for teams with the flag off, flat_cost is absent so the extra series is always $0, contradicting the PR's stated intent that these views render exactly as before. Gating the chart categories/legend/tooltip on showFlatCost restores the prior single-series behavior.
Prompt for agents
The Daily Spend BarChart in EntityUsage.tsx (around lines 584-611) was changed to always render two stacked series ("Request spend" and "Flat cost"), enable the legend, and use a tooltip that shows Request spend / Flat cost / Total cost. Unlike the stat cards above it (which are gated by the showFlatCost boolean = entityType === 'team' && ptuAttributionEnabled), this chart is not gated, so it renders for all entity types and for teams when the PTU cost-attribution flag is off. The result is an always-zero 'Flat cost' bar plus a legend and changed tooltip labels in views that the PR says should be unchanged. Gate the chart's flat-cost behavior on showFlatCost: when showFlatCost is false, keep the original single 'metrics.spend' cyan series with showLegend={false} and the original 'Total Spend' tooltip; when true, use the new stacked two-series chart with legend and the new tooltip. Consider selecting the data map, categories, colors, stack, showLegend, and customTooltip conditionally based on showFlatCost.
Was this helpful? React with 👍 or 👎 to provide feedback.
|
Closing as superseded. This is part of the v1 PTU design, which stored PTU config in a separate reservation table. The shipped design puts that config on the model deployment instead, merged as #35341, #35343, #35391, #35393 and #36829. The admin UI landed as #35393, with the PTU inputs moved into the model form rather than a reservations page. The branch is kept, so nothing here is lost. |
Relevant issues
Linear ticket
Resolves LIT-1697 (stage 4 of 5)
Stacks on #33266. Merge that first.
Pre-Submission checklist
Screenshots / Proof of Fix
To reproduce end-to-end you need stages 1-3 applied, a reservation created, and a real Azure gpt-4 request on the same team+day.
Post screenshots in this PR once staging deploy lands. Locally verified via unit tests + component tests.
Type
New Feature
Changes
Adds the admin UI, the Usage page cost breakdown, and CSV export enrichment for the PTU-reservation feature stack. Everything is additive and gated behind
enable_ptu_cost_attributioningeneral_settings; consumers who don't opt in see zero behavior change.PTU Reservations admin page (
/ptu-reservations):active(live now),scheduled(starts later),ended(already closed). Only active/scheduled rows show a Close button; ended rows have no action.general_settings. Fully rendered inside the panel so the nav item behavior stays consistent even without hook plumbing in the sidebar.Usage page team pane extension:
Flat CostandTotal Coststat cards next toTotal Spend. Non-team entity types (user, org, tag, agent, customer) render exactly as before — the grid stays 5 items wide instead of 7.CSV / JSON export enrichment:
dailyexport addsFlat Cost ($)andTotal Cost ($)columns when the response includestotal_flat_cost. Detection is data-shape driven:spendData.metadata.total_flat_costpresence, notentityType.total_flat_costandtotal_costfor team exports; other entities' summaries are unchanged.daily_with_keysanddaily_with_modelsscopes are unchanged — flat cost is a per-team-per-model concept, and folding it into per-key or per-model exports would require sentinel handling in the aggregator that stage 3 already keeps out of api_key breakdowns.Networking layer:
ptuReservationListCall,ptuReservationInfoCall,ptuReservationCreateCall,ptuReservationCloseCall.usePtuReservations,useCreatePtuReservation,useClosePtuReservation, all invalidating on mutation success.useIsPtuCostAttributionEnabledreads/config/list?config_type=general_settingsand returns a stable{ enabled, isLoading }shape.Deviation from the admin-entity pattern
Stage 1's rationale for skipping
/updatestill applies; the UI mirrors that by offering only Create + Close. No Edit button on the reservations table.The panel itself uses
antd+ a lightweight styled<table>rather than tremor, breaking pattern with the Budgets panel. The codebase is actively migrating off tremor (the eslint ruleno-restricted-importsbans it in new files), so this is the right direction going forward rather than a deviation.Behavior changes
/ui/?page=ptu-reservations(viamigratedPages, actual URL/ui/ptu-reservations). Feature-flag gated. Nav item visible to any admin role.total_flat_costis present in the response. Consumers of the old CSV shape who parse by header lose nothing (columns are appended,Spend ($)still means per-request); consumers who parse by index will break — flagging for anyone with fragile scripts.Boilerplate note
The panel + modals mirror the Budgets pattern (
budgets/_components/) but with antd instead of tremor and a single Close verb instead of Edit/Delete. The overall CRUD scaffolding is still copy-paste; extracting a generic admin-entity panel component is filed as a follow-up rather than blocking this PR (per the pattern discussed in stage 1).Files changed
ui/litellm-dashboard/src/app/(dashboard)/ptu-reservations/page.tsxui/litellm-dashboard/src/app/(dashboard)/ptu-reservations/_components/ptu_reservation_panel.tsxui/litellm-dashboard/src/app/(dashboard)/ptu-reservations/_components/ptu_reservation_modal.tsxui/litellm-dashboard/src/app/(dashboard)/ptu-reservations/_components/close_reservation_modal.tsxui/litellm-dashboard/src/app/(dashboard)/ptu-reservations/_components/ptu_reservation_panel.test.tsxui/litellm-dashboard/src/app/(dashboard)/hooks/ptuReservations/useIsPtuCostAttributionEnabled.tsui/litellm-dashboard/src/app/(dashboard)/hooks/ptuReservations/usePtuReservations.tsui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx: showFlatCost stat cards for team viewui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.test.tsx: mock the new hook so existing tests passui/litellm-dashboard/src/components/EntityUsageExport/utils.ts: hasFlatCost detector + Flat Cost / Total Cost columns + metadata summaryui/litellm-dashboard/src/components/EntityUsageExport/utils.test.ts: 4 new tests for the new column + metadata behaviorui/litellm-dashboard/src/components/EntityUsageExport/types.ts: optionaltotal_flat_costonEntitySpendData.metadata+ optional fields onExportMetadata.summaryui/litellm-dashboard/src/components/UsagePage/types.ts: optionalflat_costonSpendMetricsui/litellm-dashboard/src/components/networking.tsx: 4 new ptuReservation* callsui/litellm-dashboard/src/components/leftnav.tsx: PTU Reservations nav item +Coinsiconui/litellm-dashboard/src/utils/migratedPages.ts:ptu-reservationsroute mappingTests
166 UI tests pass across three directly-affected test files:
ptu_reservation_panel.test.tsx(6): flag-off render, loading state, empty state, populated table, create-modal open, non-admin denial.utils.test.ts(72): existing suite + 4 new tests — team metadata summary includes total_flat_cost / total_cost, non-team summary omits both, CSV daily rows for team include Flat Cost / Total Cost, non-team daily rows omit both.EntityUsage.test.tsx(25): existing 25 tests still pass after mocking the new hook.Prettier + ESLint clean on all changed files. Python lint clean (no backend changes; verified).