@acarmisc/backstage-plugin-litellm 0.15.1 → 0.15.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,6 +8,17 @@ Pair this package with the backend, [`@acarmisc/backstage-plugin-litellm-backend
8
8
 
9
9
  Full setup docs, configuration reference, and architecture notes live in the [project README](https://github.com/acarmisc/backstage-plugin-litellm-govai#readme).
10
10
 
11
+ ## Screenshots
12
+
13
+ | View | Preview |
14
+ |------|---------|
15
+ | Home widget — `LiteLLMHomeWidget` | ![Home widget](../../docs/screenshots/home-widget.png) |
16
+ | Overview / Usage Analytics | ![Usage analytics](../../docs/screenshots/usage-analytics.png) |
17
+ | Keys tab — virtual key inventory | ![Keys tab](../../docs/screenshots/keys-tab.png) |
18
+ | Generate New Key dialog | ![Generate New Key dialog](../../docs/screenshots/generate-key-dialog.png) |
19
+ | Models tab | ![Models tab](../../docs/screenshots/models-tab.png) |
20
+ | Compact "Create Key" card | ![Create Key card](../../docs/screenshots/key-card.png) |
21
+
11
22
  ## Installation
12
23
 
13
24
  This plugin is designed to be used **within a Backstage monorepo**.
@@ -22,6 +33,8 @@ You'll also need the backend package installed and configured — see its [READM
22
33
 
23
34
  ### Full page
24
35
 
36
+ The `LiteLLMPage` component renders the four-tab governance view: **Overview** (usage KPIs + charts), **Keys** (virtual key inventory), **Teams** (team-scoped usage), and **Models** (proxy model catalog).
37
+
25
38
  ```tsx
26
39
  import { litellmPlugin, LiteLLMPage } from '@acarmisc/backstage-plugin-litellm';
27
40
 
@@ -29,9 +42,11 @@ import { litellmPlugin, LiteLLMPage } from '@acarmisc/backstage-plugin-litellm';
29
42
  <Route path="/litellm" element={<LiteLLMPage />} />
30
43
  ```
31
44
 
45
+ ![Usage Analytics full page](../../docs/screenshots/usage-analytics-overview.png)
46
+
32
47
  ### Home page widget
33
48
 
34
- `LiteLLMHomeWidget` is a compact card for the Backstage homepage. It shows the signed-in user's own usage — identity is resolved server-side from the Backstage token, so no `userId` prop is required.
49
+ `LiteLLMHomeWidget` is a compact card for the Backstage homepage. It shows the signed-in user's own usage — identity is resolved server-side from the Backstage token, so no `userId` prop is required. The header includes a `Today` / `7d` / `30d` period selector and a daily-spend sparkline.
35
50
 
36
51
  ```tsx
37
52
  import { LiteLLMHomeWidget } from '@acarmisc/backstage-plugin-litellm';
@@ -44,6 +59,28 @@ import { LiteLLMHomeWidget } from '@acarmisc/backstage-plugin-litellm';
44
59
  | `defaultPeriod` | `'today' \| '7d' \| '30d'` | `'7d'` | Period shown on first render |
45
60
  | `title` | `string` | `'LiteLLM Usage'` | Card title override |
46
61
 
62
+ A slimmer **Create Key** card variant is available for surfaces that only need a one-click shortcut into the key-mint flow:
63
+
64
+ ![Create Key card](../../docs/screenshots/key-card.png)
65
+
66
+ ### Keys tab
67
+
68
+ The **Keys** tab lists every virtual key you own with its alias, key ID, creation and expiry dates, budget bar (`$spent / $limit`), TPM/RPM, and model scope. Each row exposes inline actions: edit, block/unblock, reset spend, and delete.
69
+
70
+ ![Keys tab](../../docs/screenshots/keys-tab.png)
71
+
72
+ ### Generate New Key dialog
73
+
74
+ The dialog walks the user through aliasing the key, picking a duration, binding a team, restricting model access, setting a max budget, and configuring per-key TPM/RPM caps. Failures (e.g. duplicate alias) surface inline with the upstream error code preserved.
75
+
76
+ ![Generate New Key dialog](../../docs/screenshots/generate-key-dialog.png)
77
+
78
+ ### Models tab
79
+
80
+ The **Models** tab lists every model the LiteLLM proxy exposes, with per-model input/output cost, max input/output token limits, and capabilities. The team selector scopes the list to the models the bound team is allowed to call.
81
+
82
+ ![Models tab](../../docs/screenshots/models-tab.png)
83
+
47
84
  ## Exports
48
85
 
49
86
  - `litellmPlugin` — the frontend plugin instance
@@ -4,9 +4,11 @@ interface UsageStatsProps {
4
4
  usage: UsageMetrics | null;
5
5
  models: ModelInfo[];
6
6
  dateRange: DateRange;
7
- onDateRangeChange: (range: DateRange) => void;
7
+ currentPreset: DatePreset;
8
+ onDateRangeChange: (range: DateRange, preset?: DatePreset) => void;
8
9
  loading: boolean;
9
10
  userInfo?: UserInfo;
10
11
  }
12
+ type DatePreset = 'today' | '24h' | '7d' | '30d';
11
13
  export declare const UsageStats: React.FC<UsageStatsProps>;
12
14
  export {};
package/dist/index.cjs.js CHANGED
@@ -913,7 +913,25 @@ function formatContextWindow(maxInput, maxOutput) {
913
913
  if (outPart) return `ctx ${outPart} out`;
914
914
  return null;
915
915
  }
916
- var import_react3, import_Table, import_TableBody, import_TableCell, import_TableContainer, import_TableHead, import_TableRow, import_Button2, import_IconButton, import_Box3, import_Typography3, import_Dialog, import_DialogTitle, import_DialogContent, import_DialogActions, import_TextField, import_CircularProgress, import_Autocomplete, import_Skeleton2, import_InputAdornment, import_styles3, import_icons_material2, maskKey, shortKeyId, formatDate, keyToEditForm, KeysTable;
916
+ function compareKeys(a, b, sortKey) {
917
+ switch (sortKey) {
918
+ case "alias":
919
+ return (a.key_alias ?? "").localeCompare(b.key_alias ?? "");
920
+ case "created":
921
+ return a.created_at.localeCompare(b.created_at);
922
+ case "expires":
923
+ return (a.expires_at ?? "").localeCompare(b.expires_at ?? "");
924
+ case "budget":
925
+ return (a.max_budget ?? Infinity) - (b.max_budget ?? Infinity);
926
+ case "limits":
927
+ return (a.tpm_limit ?? Infinity) - (b.tpm_limit ?? Infinity) || (a.rpm_limit ?? Infinity) - (b.rpm_limit ?? Infinity);
928
+ case "models":
929
+ return (a.models ?? []).join(",").localeCompare((b.models ?? []).join(","));
930
+ default:
931
+ return 0;
932
+ }
933
+ }
934
+ var import_react3, import_Table, import_TableBody, import_TableCell, import_TableContainer, import_TableHead, import_TableRow, import_TableSortLabel, import_Button2, import_IconButton, import_Box3, import_Typography3, import_Dialog, import_DialogTitle, import_DialogContent, import_DialogActions, import_TextField, import_CircularProgress, import_Autocomplete, import_Skeleton2, import_InputAdornment, import_styles3, import_icons_material2, maskKey, shortKeyId, formatDate, keyToEditForm, KeysTable;
917
935
  var init_KeysTable = __esm({
918
936
  "src/components/KeysTable.tsx"() {
919
937
  "use strict";
@@ -924,6 +942,7 @@ var init_KeysTable = __esm({
924
942
  import_TableContainer = __toESM(require("@mui/material/TableContainer"));
925
943
  import_TableHead = __toESM(require("@mui/material/TableHead"));
926
944
  import_TableRow = __toESM(require("@mui/material/TableRow"));
945
+ import_TableSortLabel = __toESM(require("@mui/material/TableSortLabel"));
927
946
  import_Button2 = __toESM(require("@mui/material/Button"));
928
947
  import_IconButton = __toESM(require("@mui/material/IconButton"));
929
948
  import_Box3 = __toESM(require("@mui/material/Box"));
@@ -985,13 +1004,29 @@ var init_KeysTable = __esm({
985
1004
  const [resetSpendConfirm, setResetSpendConfirm] = (0, import_react3.useState)(false);
986
1005
  const [resetSpendSubmitting, setResetSpendSubmitting] = (0, import_react3.useState)(false);
987
1006
  const [filterText, setFilterText] = (0, import_react3.useState)("");
1007
+ const [sortKey, setSortKey] = (0, import_react3.useState)("created");
1008
+ const [sortDirection, setSortDirection] = (0, import_react3.useState)("desc");
1009
+ const handleSort = (nextSortKey) => {
1010
+ if (sortKey === nextSortKey) {
1011
+ setSortDirection((current) => current === "asc" ? "desc" : "asc");
1012
+ } else {
1013
+ setSortKey(nextSortKey);
1014
+ setSortDirection("asc");
1015
+ }
1016
+ };
988
1017
  const filteredKeys = (0, import_react3.useMemo)(() => {
989
- if (!filterText.trim()) return keys;
990
- const q = filterText.toLowerCase();
991
- return keys.filter(
992
- (k) => (k.key_alias ?? "").toLowerCase().includes(q) || k.models?.some((m) => m.toLowerCase().includes(q))
993
- );
994
- }, [keys, filterText]);
1018
+ let result = keys;
1019
+ if (filterText.trim()) {
1020
+ const q = filterText.toLowerCase();
1021
+ result = keys.filter(
1022
+ (k) => (k.key_alias ?? "").toLowerCase().includes(q) || k.models?.some((m) => m.toLowerCase().includes(q))
1023
+ );
1024
+ }
1025
+ return [...result].sort((a, b) => {
1026
+ const comparison = compareKeys(a, b, sortKey);
1027
+ return sortDirection === "asc" ? comparison : -comparison;
1028
+ });
1029
+ }, [keys, filterText, sortKey, sortDirection]);
995
1030
  const editSelectedModels = models.filter((m) => (editForm.models || []).includes(m.model_name));
996
1031
  const handleOpenEdit = (k) => {
997
1032
  setEditingKey(k);
@@ -1121,7 +1156,7 @@ var init_KeysTable = __esm({
1121
1156
  "& td": { color: theme.palette.text.secondary }
1122
1157
  }) : void 0
1123
1158
  },
1124
- /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, /* @__PURE__ */ import_react3.default.createElement(import_Box3.default, { display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }, /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, { variant: "body2", sx: { fontWeight: 600 } }, key.key_alias || "\u2014"), key.blocked && /* @__PURE__ */ import_react3.default.createElement(StatusPill, { label: "Blocked", tone: "danger" }))),
1159
+ /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, /* @__PURE__ */ import_react3.default.createElement(import_Box3.default, { display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }, /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, { variant: "body2" }, key.key_alias || "\u2014"), key.blocked && /* @__PURE__ */ import_react3.default.createElement(StatusPill, { label: "Blocked", tone: "danger" }))),
1125
1160
  /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, /* @__PURE__ */ import_react3.default.createElement(import_Box3.default, { display: "flex", alignItems: "center", gap: 0.5 }, /* @__PURE__ */ import_react3.default.createElement(
1126
1161
  import_Typography3.default,
1127
1162
  {
@@ -1203,7 +1238,8 @@ var init_KeysTable = __esm({
1203
1238
  import_TextField.default,
1204
1239
  {
1205
1240
  size: "small",
1206
- placeholder: "Filter by alias or model\u2026",
1241
+ label: "Search alias or model",
1242
+ placeholder: "Search alias or model\u2026",
1207
1243
  value: filterText,
1208
1244
  onChange: (e) => setFilterText(e.target.value),
1209
1245
  sx: { minWidth: 240 },
@@ -1243,7 +1279,7 @@ var init_KeysTable = __esm({
1243
1279
  "Generate New Key"
1244
1280
  ))
1245
1281
  },
1246
- /* @__PURE__ */ import_react3.default.createElement(import_TableContainer.default, null, /* @__PURE__ */ import_react3.default.createElement(import_Table.default, { size: "small", sx: dataTableSx }, /* @__PURE__ */ import_react3.default.createElement(import_TableHead.default, null, /* @__PURE__ */ import_react3.default.createElement(import_TableRow.default, null, /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, "Alias"), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, "Key ID"), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, "Created"), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, "Expires"), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, "Budget"), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, "TPM / RPM"), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, "Models"), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, { align: "right" }, "Actions"))), /* @__PURE__ */ import_react3.default.createElement(import_TableBody.default, null, renderTableBody())))
1282
+ /* @__PURE__ */ import_react3.default.createElement(import_TableContainer.default, null, /* @__PURE__ */ import_react3.default.createElement(import_Table.default, { size: "small", sx: dataTableSx }, /* @__PURE__ */ import_react3.default.createElement(import_TableHead.default, null, /* @__PURE__ */ import_react3.default.createElement(import_TableRow.default, null, /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, { sortDirection: sortKey === "alias" ? sortDirection : false }, /* @__PURE__ */ import_react3.default.createElement(import_TableSortLabel.default, { active: sortKey === "alias", direction: sortKey === "alias" ? sortDirection : "asc", onClick: () => handleSort("alias") }, "Alias")), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, "Key ID"), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, { sortDirection: sortKey === "created" ? sortDirection : false }, /* @__PURE__ */ import_react3.default.createElement(import_TableSortLabel.default, { active: sortKey === "created", direction: sortKey === "created" ? sortDirection : "asc", onClick: () => handleSort("created") }, "Created")), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, { sortDirection: sortKey === "expires" ? sortDirection : false }, /* @__PURE__ */ import_react3.default.createElement(import_TableSortLabel.default, { active: sortKey === "expires", direction: sortKey === "expires" ? sortDirection : "asc", onClick: () => handleSort("expires") }, "Expires")), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, { sortDirection: sortKey === "budget" ? sortDirection : false }, /* @__PURE__ */ import_react3.default.createElement(import_TableSortLabel.default, { active: sortKey === "budget", direction: sortKey === "budget" ? sortDirection : "asc", onClick: () => handleSort("budget") }, "Budget")), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, { sortDirection: sortKey === "limits" ? sortDirection : false }, /* @__PURE__ */ import_react3.default.createElement(import_TableSortLabel.default, { active: sortKey === "limits", direction: sortKey === "limits" ? sortDirection : "asc", onClick: () => handleSort("limits") }, "TPM / RPM")), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, { sortDirection: sortKey === "models" ? sortDirection : false }, /* @__PURE__ */ import_react3.default.createElement(import_TableSortLabel.default, { active: sortKey === "models", direction: sortKey === "models" ? sortDirection : "asc", onClick: () => handleSort("models") }, "Models")), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, { align: "right" }, "Actions"))), /* @__PURE__ */ import_react3.default.createElement(import_TableBody.default, null, renderTableBody())))
1247
1283
  ), /* @__PURE__ */ import_react3.default.createElement(import_Dialog.default, { open: !!editingKey, onClose: handleCloseEdit, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ import_react3.default.createElement(import_DialogTitle.default, null, "Edit Key"), /* @__PURE__ */ import_react3.default.createElement(import_DialogContent.default, null, editingKey && /* @__PURE__ */ import_react3.default.createElement(import_Box3.default, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, { variant: "body2", color: "text.secondary" }, /* @__PURE__ */ import_react3.default.createElement("code", { style: { fontFamily: "monospace", color: "inherit" } }, maskKey(editingKey.key))), /* @__PURE__ */ import_react3.default.createElement(
1248
1284
  import_TextField.default,
1249
1285
  {
@@ -1467,7 +1503,7 @@ function formatContextWindow2(maxInput, maxOutput) {
1467
1503
  if (outPart) return `ctx ${outPart} out`;
1468
1504
  return null;
1469
1505
  }
1470
- var import_react4, import_Box4, import_Typography4, import_Dialog2, import_DialogTitle2, import_DialogContent2, import_DialogActions2, import_TextField2, import_MenuItem, import_Button3, import_IconButton2, import_CircularProgress2, import_Autocomplete2, import_Checkbox, import_FormControlLabel, import_Alert, import_Tabs, import_Tab, import_icons_material3, generateDefaultAlias, emptyForm, ALL_PROXY_MODELS, SNIPPET_FILE_HINTS, SnippetTabs, GenerateKeyDialog;
1506
+ var import_react4, import_Box4, import_Typography4, import_Dialog2, import_DialogTitle2, import_DialogContent2, import_DialogActions2, import_TextField2, import_MenuItem, import_Button3, import_IconButton2, import_CircularProgress2, import_Autocomplete2, import_Checkbox, import_FormControlLabel, import_Alert, import_Accordion, import_AccordionSummary, import_AccordionDetails, import_Tabs, import_Tab, import_icons_material3, generateDefaultAlias, emptyForm, ALL_PROXY_MODELS, SNIPPET_FILE_HINTS, SnippetTabs, GenerateKeyDialog;
1471
1507
  var init_GenerateKeyDialog = __esm({
1472
1508
  "src/components/GenerateKeyDialog.tsx"() {
1473
1509
  "use strict";
@@ -1487,6 +1523,9 @@ var init_GenerateKeyDialog = __esm({
1487
1523
  import_Checkbox = __toESM(require("@mui/material/Checkbox"));
1488
1524
  import_FormControlLabel = __toESM(require("@mui/material/FormControlLabel"));
1489
1525
  import_Alert = __toESM(require("@mui/material/Alert"));
1526
+ import_Accordion = __toESM(require("@mui/material/Accordion"));
1527
+ import_AccordionSummary = __toESM(require("@mui/material/AccordionSummary"));
1528
+ import_AccordionDetails = __toESM(require("@mui/material/AccordionDetails"));
1490
1529
  import_Tabs = __toESM(require("@mui/material/Tabs"));
1491
1530
  import_Tab = __toESM(require("@mui/material/Tab"));
1492
1531
  import_icons_material3 = require("@mui/icons-material");
@@ -1806,7 +1845,7 @@ var init_GenerateKeyDialog = __esm({
1806
1845
  required: true,
1807
1846
  fullWidth: true
1808
1847
  }
1809
- ), /* @__PURE__ */ import_react4.default.createElement(
1848
+ ), /* @__PURE__ */ import_react4.default.createElement(import_Accordion.default, { disableGutters: true, elevation: 0, sx: { border: "1px solid", borderColor: "divider", "&:before": { display: "none" } } }, /* @__PURE__ */ import_react4.default.createElement(import_AccordionSummary.default, { expandIcon: /* @__PURE__ */ import_react4.default.createElement(import_icons_material3.ExpandMore, null) }, /* @__PURE__ */ import_react4.default.createElement(import_Typography4.default, { variant: "body2", fontWeight: 600 }, "Advanced")), /* @__PURE__ */ import_react4.default.createElement(import_AccordionDetails.default, { sx: { display: "flex", flexDirection: "column", gap: 2 } }, /* @__PURE__ */ import_react4.default.createElement(
1810
1849
  import_TextField2.default,
1811
1850
  {
1812
1851
  label: "TPM Limit",
@@ -1826,7 +1865,7 @@ var init_GenerateKeyDialog = __esm({
1826
1865
  helperText: "Max requests per minute this key can make across all models. Leave blank for no limit.",
1827
1866
  fullWidth: true
1828
1867
  }
1829
- ))), /* @__PURE__ */ import_react4.default.createElement(import_DialogActions2.default, null, newKeyValue ? /* @__PURE__ */ import_react4.default.createElement(import_Button3.default, { onClick: handleClose, variant: "contained", color: "success" }, "Done") : /* @__PURE__ */ import_react4.default.createElement(import_react4.default.Fragment, null, /* @__PURE__ */ import_react4.default.createElement(import_Button3.default, { onClick: handleClose }, "Cancel"), /* @__PURE__ */ import_react4.default.createElement(
1868
+ ))))), /* @__PURE__ */ import_react4.default.createElement(import_DialogActions2.default, null, newKeyValue ? /* @__PURE__ */ import_react4.default.createElement(import_Button3.default, { onClick: handleClose, variant: "contained", color: "success" }, "Done") : /* @__PURE__ */ import_react4.default.createElement(import_react4.default.Fragment, null, /* @__PURE__ */ import_react4.default.createElement(import_Button3.default, { onClick: handleClose }, "Cancel"), /* @__PURE__ */ import_react4.default.createElement(
1830
1869
  import_Button3.default,
1831
1870
  {
1832
1871
  onClick: handleGenerate,
@@ -1898,6 +1937,7 @@ var init_UsageStats = __esm({
1898
1937
  usage,
1899
1938
  models,
1900
1939
  dateRange,
1940
+ currentPreset,
1901
1941
  onDateRangeChange,
1902
1942
  loading,
1903
1943
  userInfo
@@ -1905,15 +1945,6 @@ var init_UsageStats = __esm({
1905
1945
  const chart = useChartTheme();
1906
1946
  const [selectedModel, setSelectedModel] = (0, import_react5.useState)("all");
1907
1947
  const [tab, setTab] = (0, import_react5.useState)("costs");
1908
- const selectedPreset = (0, import_react5.useMemo)(() => {
1909
- if (dateRange.start.toDateString() === dateRange.end.toDateString()) return "today";
1910
- const diffMs = dateRange.end.getTime() - dateRange.start.getTime();
1911
- const diffHours = diffMs / (1e3 * 60 * 60);
1912
- if (diffHours <= 26) return "24h";
1913
- const diffDays = Math.ceil(diffMs / (1e3 * 60 * 60 * 24));
1914
- if (diffDays <= 7) return "7d";
1915
- return "30d";
1916
- }, [dateRange]);
1917
1948
  const handlePresetChange = (preset) => {
1918
1949
  const end = /* @__PURE__ */ new Date();
1919
1950
  const start = /* @__PURE__ */ new Date();
@@ -1925,7 +1956,7 @@ var init_UsageStats = __esm({
1925
1956
  localStorage.setItem(PERIOD_LS_KEY, preset);
1926
1957
  } catch {
1927
1958
  }
1928
- onDateRangeChange({ start, end });
1959
+ onDateRangeChange({ start, end }, preset);
1929
1960
  };
1930
1961
  const dailyData = (0, import_react5.useMemo)(
1931
1962
  () => (usage?.daily_usage ?? []).map((d) => ({
@@ -2064,7 +2095,7 @@ var init_UsageStats = __esm({
2064
2095
  select: true,
2065
2096
  size: "small",
2066
2097
  label: "Period",
2067
- value: selectedPreset,
2098
+ value: currentPreset,
2068
2099
  onChange: (e) => handlePresetChange(e.target.value),
2069
2100
  sx: { minWidth: 160 }
2070
2101
  },
@@ -2074,7 +2105,7 @@ var init_UsageStats = __esm({
2074
2105
  SectionCard,
2075
2106
  {
2076
2107
  title: "Usage Analytics",
2077
- subtitle: `${PRESET_LABELS[selectedPreset]} \xB7 ${dateRange.start.toLocaleDateString()} \u2013 ${dateRange.end.toLocaleDateString()}`,
2108
+ subtitle: `${PRESET_LABELS[currentPreset]} \xB7 ${dateRange.start.toLocaleDateString()} \u2013 ${dateRange.end.toLocaleDateString()}`,
2078
2109
  actions: periodSelect
2079
2110
  },
2080
2111
  /* @__PURE__ */ import_react5.default.createElement(MetricStrip, { metrics }),
@@ -2423,6 +2454,10 @@ var init_TeamUsage = __esm({
2423
2454
  });
2424
2455
 
2425
2456
  // src/components/ModelsTable.tsx
2457
+ function isModelAllowedByTeam2(model, teamModels) {
2458
+ if (!teamModels || teamModels.length === 0 || teamModels.includes(ALL_PROXY_MODELS2)) return true;
2459
+ return teamModels.includes(model.model_name) || !!model.access_groups?.some((group) => teamModels.includes(group));
2460
+ }
2426
2461
  function fmtCostPerToken(cost) {
2427
2462
  if (cost === void 0 || cost === null) return "\u2014";
2428
2463
  if (cost === 0) return "$0";
@@ -2436,7 +2471,7 @@ function fmtTokens(n) {
2436
2471
  if (n >= 1e3) return `${(n / 1e3).toFixed(0)}K`;
2437
2472
  return String(n);
2438
2473
  }
2439
- var import_react7, import_Box7, import_Table4, import_TableBody4, import_TableCell4, import_TableContainer4, import_TableHead4, import_TableRow4, import_Typography7, import_MenuItem3, import_Select, import_FormControl, import_InputLabel, import_ContentCopy, import_Tooltip, import_Snackbar, import_Alert2, import_styles5, ModelsTable;
2474
+ var import_react7, import_Box7, import_Table4, import_TableBody4, import_TableCell4, import_TableContainer4, import_TableHead4, import_TableRow4, import_Typography7, import_MenuItem3, import_Select, import_FormControl, import_InputLabel, import_ContentCopy, import_Tooltip, import_Snackbar, import_Alert2, import_styles5, ALL_PROXY_MODELS2, ModelsTable;
2440
2475
  var init_ModelsTable = __esm({
2441
2476
  "src/components/ModelsTable.tsx"() {
2442
2477
  "use strict";
@@ -2459,6 +2494,7 @@ var init_ModelsTable = __esm({
2459
2494
  import_Alert2 = __toESM(require("@mui/material/Alert"));
2460
2495
  init_ui();
2461
2496
  import_styles5 = require("@mui/material/styles");
2497
+ ALL_PROXY_MODELS2 = "all-proxy-models";
2462
2498
  ModelsTable = ({ allModels, teams, loading }) => {
2463
2499
  const theme = (0, import_styles5.useTheme)();
2464
2500
  const [selectedTeamId, setSelectedTeamId] = (0, import_react7.useState)("");
@@ -2469,10 +2505,7 @@ var init_ModelsTable = __esm({
2469
2505
  );
2470
2506
  const filteredModels = (0, import_react7.useMemo)(() => {
2471
2507
  if (!selectedTeam) return [];
2472
- const teamModels = selectedTeam.models ?? [];
2473
- if (teamModels.length === 0) return allModels;
2474
- const allowed = new Set(teamModels);
2475
- return allModels.filter((m) => allowed.has(m.model_name));
2508
+ return allModels.filter((model) => isModelAllowedByTeam2(model, selectedTeam.models));
2476
2509
  }, [allModels, selectedTeam]);
2477
2510
  const handleCopyModelId = (0, import_react7.useCallback)((modelId) => {
2478
2511
  navigator.clipboard.writeText(modelId).then(() => setCopiedSnackbar(true));
@@ -2839,6 +2872,13 @@ var init_LiteLLMPage = __esm({
2839
2872
  LiteLLMPage = () => {
2840
2873
  const api = (0, import_core_plugin_api2.useApi)(liteLlmApiRef);
2841
2874
  const [dateRange, setDateRange] = (0, import_react9.useState)(initDateRange);
2875
+ const [currentPreset, setCurrentPreset] = (0, import_react9.useState)(() => {
2876
+ try {
2877
+ return localStorage.getItem(PERIOD_LS_KEY2) ?? "7d";
2878
+ } catch {
2879
+ return "7d";
2880
+ }
2881
+ });
2842
2882
  const [activeTab, setActiveTab] = (0, import_react9.useState)("overview");
2843
2883
  const [snackbar, setSnackbar] = (0, import_react9.useState)(null);
2844
2884
  const [generateDialogOpen, setGenerateDialogOpen] = (0, import_react9.useState)(false);
@@ -2885,11 +2925,14 @@ var init_LiteLLMPage = __esm({
2885
2925
  const endDate = dateRange.end.toISOString().split("T")[0];
2886
2926
  return api.getUsage(startDate, endDate);
2887
2927
  }, [api, dateRange]);
2888
- const handleDateRangeChange = (0, import_react9.useCallback)((range) => {
2928
+ const handleDateRangeChange = (0, import_react9.useCallback)((range, preset) => {
2889
2929
  setDateRange(range);
2930
+ if (preset) {
2931
+ setCurrentPreset(preset);
2932
+ }
2890
2933
  setTeamUsageCache({});
2891
2934
  setTeamUsageLoading({});
2892
- }, [setDateRange]);
2935
+ }, [setDateRange, setCurrentPreset]);
2893
2936
  const loadTeamUsage = (0, import_react9.useCallback)(async (teamId) => {
2894
2937
  if (teamUsageCache[teamId] !== void 0 || teamUsageLoading[teamId]) return;
2895
2938
  setTeamUsageLoading((prev) => ({ ...prev, [teamId]: true }));
@@ -3054,6 +3097,7 @@ var init_LiteLLMPage = __esm({
3054
3097
  usage: usage ?? null,
3055
3098
  models: allModels ?? [],
3056
3099
  dateRange,
3100
+ currentPreset,
3057
3101
  onDateRangeChange: handleDateRangeChange,
3058
3102
  loading: usageLoading,
3059
3103
  userInfo