@acarmisc/backstage-plugin-litellm 0.6.0 → 0.8.0

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/dist/index.esm.js CHANGED
@@ -15,6 +15,14 @@ var __export = (target, all) => {
15
15
 
16
16
  // src/api.ts
17
17
  import { createApiRef } from "@backstage/core-plugin-api";
18
+ function expiryStatus(expiresAt) {
19
+ if (!expiresAt) return null;
20
+ const diff = new Date(expiresAt).getTime() - Date.now();
21
+ const days = diff / (1e3 * 60 * 60 * 24);
22
+ if (days < 0) return "expired";
23
+ if (days < 7) return "soon";
24
+ return "ok";
25
+ }
18
26
  var ApiError, liteLlmApiRef, LiteLlmApi;
19
27
  var init_api = __esm({
20
28
  "src/api.ts"() {
@@ -100,6 +108,19 @@ var init_api = __esm({
100
108
  async resetKeySpend(keyId) {
101
109
  await this.post(`/keys/${encodeURIComponent(keyId)}/reset_spend`, {});
102
110
  }
111
+ async pruneExpiredKeys() {
112
+ const keys = await this.get("/keys");
113
+ const expired = keys.filter((k) => expiryStatus(k.expires_at) === "expired");
114
+ if (expired.length === 0) return { pruned: 0 };
115
+ for (const key of expired) {
116
+ try {
117
+ await this.del(`/keys/${encodeURIComponent(key.token ?? key.key)}`);
118
+ } catch (err) {
119
+ console.warn(`Failed to delete expired key ${key.token ?? key.key}:`, err);
120
+ }
121
+ }
122
+ return { pruned: expired.length };
123
+ }
103
124
  async getAuditLogs(params) {
104
125
  const strParams = {};
105
126
  if (params.page !== void 0) strParams.page = String(params.page);
@@ -183,14 +204,6 @@ import {
183
204
  InputAdornment
184
205
  } from "@mui/material";
185
206
  import { ContentCopy, Delete, Add, Edit, Autorenew, Search, Warning, Lock, LockOpen } from "@mui/icons-material";
186
- function expiryStatus(expiresAt) {
187
- if (!expiresAt) return null;
188
- const diff = new Date(expiresAt).getTime() - Date.now();
189
- const days = diff / (1e3 * 60 * 60 * 24);
190
- if (days < 0) return "expired";
191
- if (days < 7) return "soon";
192
- return "ok";
193
- }
194
207
  function ExpiryChip({ expiresAt }) {
195
208
  const status = expiryStatus(expiresAt);
196
209
  if (!status) return /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "text.secondary" }, "-");
@@ -214,6 +227,7 @@ var maskKey, shortKeyId, formatDate, emptyForm, keyToEditForm, KeysTable;
214
227
  var init_KeysTable = __esm({
215
228
  "src/components/KeysTable.tsx"() {
216
229
  "use strict";
230
+ init_api();
217
231
  maskKey = (key) => {
218
232
  if (key.length <= 8) return "***";
219
233
  return `${key.slice(0, 4)}...${key.slice(-4)}`;
@@ -258,7 +272,8 @@ var init_KeysTable = __esm({
258
272
  onBlockKey,
259
273
  onUnblockKey,
260
274
  onResetKeySpend,
261
- onDeleteKey
275
+ onDeleteKey,
276
+ onPruneExpiredKeys
262
277
  }) => {
263
278
  const [generateModalOpen, setGenerateModalOpen] = useState(false);
264
279
  const [newKeyValue, setNewKeyValue] = useState(null);
@@ -371,6 +386,23 @@ var init_KeysTable = __esm({
371
386
  const copyToClipboard = (text) => {
372
387
  navigator.clipboard.writeText(text);
373
388
  };
389
+ const [pruneConfirmCount, setPruneConfirmCount] = useState(null);
390
+ const [pruneSubmitting, setPruneSubmitting] = useState(false);
391
+ const expiredKeys = useMemo(() => {
392
+ return keys.filter((k) => expiryStatus(k.expires_at) === "expired");
393
+ }, [keys]);
394
+ const handlePruneExpired = async () => {
395
+ if (pruneConfirmCount === null || pruneConfirmCount === 0) return;
396
+ setPruneSubmitting(true);
397
+ try {
398
+ await onPruneExpiredKeys();
399
+ } catch (error) {
400
+ console.error("Failed to prune expired keys:", error);
401
+ } finally {
402
+ setPruneSubmitting(false);
403
+ setPruneConfirmCount(null);
404
+ }
405
+ };
374
406
  const modelOption = (m) => {
375
407
  const inCost = fmtCost(m.input_cost_per_token);
376
408
  const outCost = fmtCost(m.output_cost_per_token);
@@ -388,6 +420,17 @@ var init_KeysTable = __esm({
388
420
  startAdornment: /* @__PURE__ */ React2.createElement(InputAdornment, { position: "start" }, /* @__PURE__ */ React2.createElement(Search, { fontSize: "small" }))
389
421
  }
390
422
  }
423
+ ), expiredKeys.length > 0 && /* @__PURE__ */ React2.createElement(
424
+ Button,
425
+ {
426
+ variant: "outlined",
427
+ color: "error",
428
+ startIcon: /* @__PURE__ */ React2.createElement(Autorenew, null),
429
+ onClick: () => setPruneConfirmCount(expiredKeys.length)
430
+ },
431
+ "Prune expired (",
432
+ expiredKeys.length,
433
+ ")"
391
434
  ), /* @__PURE__ */ React2.createElement(
392
435
  Button,
393
436
  {
@@ -651,11 +694,30 @@ var init_KeysTable = __esm({
651
694
  disabled: deleteSubmitting
652
695
  },
653
696
  deleteSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 20 }) : "Revoke"
697
+ ))), /* @__PURE__ */ React2.createElement(Dialog, { open: pruneConfirmCount !== null, onClose: () => setPruneConfirmCount(null), maxWidth: "xs", fullWidth: true }, /* @__PURE__ */ React2.createElement(DialogTitle, null, "Prune Expired Keys?"), /* @__PURE__ */ React2.createElement(DialogContent, null, /* @__PURE__ */ React2.createElement(Typography2, null, "This will permanently delete ", pruneConfirmCount, " expired key", pruneConfirmCount !== 1 ? "s" : "", " from LiteLLM. Any integrations using them will stop working immediately.")), /* @__PURE__ */ React2.createElement(DialogActions, null, /* @__PURE__ */ React2.createElement(Button, { onClick: () => setPruneConfirmCount(null), disabled: pruneSubmitting }, "Cancel"), /* @__PURE__ */ React2.createElement(
698
+ Button,
699
+ {
700
+ onClick: handlePruneExpired,
701
+ variant: "contained",
702
+ color: "error",
703
+ disabled: pruneSubmitting
704
+ },
705
+ pruneSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 20 }) : "Prune"
654
706
  ))));
655
707
  };
656
708
  }
657
709
  });
658
710
 
711
+ // src/format.ts
712
+ var fmtUsd, fmtInt;
713
+ var init_format = __esm({
714
+ "src/format.ts"() {
715
+ "use strict";
716
+ fmtUsd = (n) => `$${(n ?? 0).toFixed(n < 1 ? 4 : 2)}`;
717
+ fmtInt = (n) => (n ?? 0).toLocaleString();
718
+ }
719
+ });
720
+
659
721
  // src/components/UsageStats.tsx
660
722
  import React3, { useMemo as useMemo2, useState as useState2 } from "react";
661
723
  import {
@@ -699,10 +761,11 @@ function modelColor(model) {
699
761
  for (let i = 0; i < model.length; i++) h = (h << 5) + h + model.charCodeAt(i) >>> 0;
700
762
  return MODEL_COLORS[h % MODEL_COLORS.length];
701
763
  }
702
- var TOP_N_MODELS, PERIOD_LS_KEY, MODEL_COLORS, fmtUsd, fmtInt, fmtPct, KpiCard, ChartSkeleton, EmptyChart, UsageStats;
764
+ var TOP_N_MODELS, PERIOD_LS_KEY, MODEL_COLORS, fmtPct, KpiCard, ChartSkeleton, EmptyChart, UsageStats;
703
765
  var init_UsageStats = __esm({
704
766
  "src/components/UsageStats.tsx"() {
705
767
  "use strict";
768
+ init_format();
706
769
  TOP_N_MODELS = 6;
707
770
  PERIOD_LS_KEY = "litellm_usage_period";
708
771
  MODEL_COLORS = [
@@ -717,8 +780,6 @@ var init_UsageStats = __esm({
717
780
  "#a4de6c",
718
781
  "#d0ed57"
719
782
  ];
720
- fmtUsd = (n) => `$${(n ?? 0).toFixed(n < 1 ? 4 : 2)}`;
721
- fmtInt = (n) => (n ?? 0).toLocaleString();
722
783
  fmtPct = (n) => `${(n * 100).toFixed(1)}%`;
723
784
  KpiCard = ({ label, value, hint }) => /* @__PURE__ */ React3.createElement(Paper3, { variant: "outlined", sx: { p: 2, height: "100%" } }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption", color: "text.secondary" }, label), /* @__PURE__ */ React3.createElement(Typography3, { variant: "h5", sx: { mt: 0.5 } }, value), hint ? /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption", color: "text.secondary" }, hint) : null);
724
785
  ChartSkeleton = ({ height = 260 }) => /* @__PURE__ */ React3.createElement(Skeleton, { variant: "rectangular", height, sx: { borderRadius: 1 } });
@@ -908,6 +969,8 @@ import {
908
969
  Typography as Typography4,
909
970
  LinearProgress as LinearProgress4,
910
971
  Chip as Chip4,
972
+ Divider,
973
+ Stack,
911
974
  Table as Table3,
912
975
  TableBody as TableBody3,
913
976
  TableCell as TableCell3,
@@ -918,7 +981,7 @@ import {
918
981
  IconButton as IconButton2,
919
982
  CircularProgress as CircularProgress2
920
983
  } from "@mui/material";
921
- import { ExpandMore, ExpandLess, Group } from "@mui/icons-material";
984
+ import { ExpandMore, ExpandLess, Group, Speed, Memory } from "@mui/icons-material";
922
985
  import {
923
986
  AreaChart as AreaChart2,
924
987
  Area as Area2,
@@ -928,10 +991,11 @@ import {
928
991
  Tooltip as Tooltip2,
929
992
  ResponsiveContainer as ResponsiveContainer2
930
993
  } from "recharts";
931
- var TeamCard, TeamUsage;
994
+ var Stat, TeamCard, TeamUsage;
932
995
  var init_TeamUsage = __esm({
933
996
  "src/components/TeamUsage.tsx"() {
934
997
  "use strict";
998
+ Stat = ({ label, value }) => /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ React4.createElement(Typography4, { variant: "body2", fontWeight: 600, sx: { fontFamily: "monospace" } }, value));
935
999
  TeamCard = ({ team, usage, usageLoading }) => {
936
1000
  const [expanded, setExpanded] = useState3(false);
937
1001
  const budget = team.max_budget ?? 0;
@@ -940,7 +1004,43 @@ var init_TeamUsage = __esm({
940
1004
  const isOver = budget > 0 && spend >= budget;
941
1005
  const isNear = budget > 0 && spend >= budget * 0.8 && !isOver;
942
1006
  const dailyData = usage?.daily_usage?.map((d) => ({ date: d.date, spend: d.spend })) ?? [];
943
- return /* @__PURE__ */ React4.createElement(Paper4, { variant: "outlined", sx: { p: 2, mb: 2 } }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React4.createElement(Group, { color: "action" }), /* @__PURE__ */ React4.createElement(Box4, { flexGrow: 1 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle1", fontWeight: 600 }, team.team_alias ?? team.team_id), team.models?.length ? /* @__PURE__ */ React4.createElement(Box4, { display: "flex", gap: 0.5, flexWrap: "wrap", mt: 0.5 }, team.models.map((m) => /* @__PURE__ */ React4.createElement(Chip4, { key: m, label: m, size: "small", variant: "outlined" }))) : /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption", color: "text.secondary" }, "All models")), /* @__PURE__ */ React4.createElement(IconButton2, { size: "small", onClick: () => setExpanded((e) => !e) }, expanded ? /* @__PURE__ */ React4.createElement(ExpandLess, null) : /* @__PURE__ */ React4.createElement(ExpandMore, null))), budget > 0 && /* @__PURE__ */ React4.createElement(Box4, { mt: 1.5 }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", justifyContent: "space-between", mb: 0.5 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "body2" }, "$", spend.toFixed(2), " / $", budget.toFixed(2)), isOver && /* @__PURE__ */ React4.createElement(Chip4, { label: "Over Budget", size: "small", color: "error" }), isNear && /* @__PURE__ */ React4.createElement(Chip4, { label: "Near Limit", size: "small", color: "warning" })), /* @__PURE__ */ React4.createElement(
1007
+ return /* @__PURE__ */ React4.createElement(Paper4, { variant: "outlined", sx: { p: 2, mb: 2 } }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React4.createElement(Group, { color: "action" }), /* @__PURE__ */ React4.createElement(Box4, { flexGrow: 1 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle1", fontWeight: 600 }, team.team_alias || "Untitled team"), /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption", color: "text.secondary", sx: { fontFamily: "monospace" } }, team.team_id)), /* @__PURE__ */ React4.createElement(IconButton2, { size: "small", onClick: () => setExpanded((e) => !e), "aria-label": "toggle details" }, expanded ? /* @__PURE__ */ React4.createElement(ExpandLess, null) : /* @__PURE__ */ React4.createElement(ExpandMore, null))), /* @__PURE__ */ React4.createElement(Stack, { direction: "row", spacing: 3, mt: 1.5, flexWrap: "wrap", useFlexGap: true, gap: 1.5 }, /* @__PURE__ */ React4.createElement(
1008
+ Stat,
1009
+ {
1010
+ label: "Members",
1011
+ value: team.members_with_roles?.length ? String(team.members_with_roles.length) : "\u2014"
1012
+ }
1013
+ ), /* @__PURE__ */ React4.createElement(
1014
+ Stat,
1015
+ {
1016
+ label: "Models",
1017
+ value: team.models?.length ? String(team.models.length) : "All"
1018
+ }
1019
+ ), /* @__PURE__ */ React4.createElement(
1020
+ Stat,
1021
+ {
1022
+ label: "Budget",
1023
+ value: budget > 0 ? `$${budget.toFixed(2)}` : "Unlimited"
1024
+ }
1025
+ ), /* @__PURE__ */ React4.createElement(
1026
+ Stat,
1027
+ {
1028
+ label: "Spend",
1029
+ value: `$${spend.toFixed(2)}`
1030
+ }
1031
+ ), /* @__PURE__ */ React4.createElement(
1032
+ Stat,
1033
+ {
1034
+ label: "TPM",
1035
+ value: team.tpm_limit != null && team.tpm_limit > 0 ? String(team.tpm_limit) : "\u2014"
1036
+ }
1037
+ ), /* @__PURE__ */ React4.createElement(
1038
+ Stat,
1039
+ {
1040
+ label: "RPM",
1041
+ value: team.rpm_limit != null && team.rpm_limit > 0 ? String(team.rpm_limit) : "\u2014"
1042
+ }
1043
+ )), budget > 0 && /* @__PURE__ */ React4.createElement(Box4, { mt: 1.5 }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", justifyContent: "space-between", mb: 0.5 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "body2" }, "$", spend.toFixed(2), " / $", budget.toFixed(2)), isOver && /* @__PURE__ */ React4.createElement(Chip4, { label: "Over Budget", size: "small", color: "error" }), isNear && /* @__PURE__ */ React4.createElement(Chip4, { label: "Near Limit", size: "small", color: "warning" })), /* @__PURE__ */ React4.createElement(
944
1044
  LinearProgress4,
945
1045
  {
946
1046
  variant: "determinate",
@@ -948,14 +1048,14 @@ var init_TeamUsage = __esm({
948
1048
  color: isOver ? "error" : isNear ? "warning" : "primary",
949
1049
  sx: { height: 6, borderRadius: 1 }
950
1050
  }
951
- )), /* @__PURE__ */ React4.createElement(Collapse, { in: expanded }, /* @__PURE__ */ React4.createElement(Box4, { mt: 2 }, usageLoading ? /* @__PURE__ */ React4.createElement(Box4, { display: "flex", justifyContent: "center", p: 2 }, /* @__PURE__ */ React4.createElement(CircularProgress2, { size: 24 })) : dailyData.length > 0 ? /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend"), /* @__PURE__ */ React4.createElement(Box4, { height: 160 }, /* @__PURE__ */ React4.createElement(ResponsiveContainer2, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(AreaChart2, { data: dailyData }, /* @__PURE__ */ React4.createElement(CartesianGrid2, { strokeDasharray: "3 3" }), /* @__PURE__ */ React4.createElement(XAxis2, { dataKey: "date", tick: { fontSize: 11 } }), /* @__PURE__ */ React4.createElement(YAxis2, { tick: { fontSize: 11 }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React4.createElement(Tooltip2, { formatter: (v) => [`$${v.toFixed(4)}`, "Spend"] }), /* @__PURE__ */ React4.createElement(Area2, { type: "monotone", dataKey: "spend", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.3 }))))) : null, team.members_with_roles?.length ? /* @__PURE__ */ React4.createElement(Box4, { mt: 2 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Members"), /* @__PURE__ */ React4.createElement(TableContainer3, null, /* @__PURE__ */ React4.createElement(Table3, { size: "small" }, /* @__PURE__ */ React4.createElement(TableHead3, null, /* @__PURE__ */ React4.createElement(TableRow3, null, /* @__PURE__ */ React4.createElement(TableCell3, null, "User"), /* @__PURE__ */ React4.createElement(TableCell3, null, "Role"))), /* @__PURE__ */ React4.createElement(TableBody3, null, team.members_with_roles.map((m) => /* @__PURE__ */ React4.createElement(TableRow3, { key: m.user_id }, /* @__PURE__ */ React4.createElement(TableCell3, { sx: { fontFamily: "monospace", fontSize: 12 } }, m.user_id), /* @__PURE__ */ React4.createElement(TableCell3, null, /* @__PURE__ */ React4.createElement(
1051
+ )), /* @__PURE__ */ React4.createElement(Collapse, { in: expanded }, /* @__PURE__ */ React4.createElement(Box4, { mt: 2 }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React4.createElement(Speed, { fontSize: "small", color: "action" }), /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary" }, "Rate limits")), /* @__PURE__ */ React4.createElement(Stack, { direction: "row", spacing: 3, flexWrap: "wrap", useFlexGap: true, gap: 1.5, mb: 2 }, /* @__PURE__ */ React4.createElement(Stat, { label: "TPM limit", value: team.tpm_limit != null && team.tpm_limit > 0 ? String(team.tpm_limit) : "Unlimited" }), /* @__PURE__ */ React4.createElement(Stat, { label: "RPM limit", value: team.rpm_limit != null && team.rpm_limit > 0 ? String(team.rpm_limit) : "Unlimited" }), /* @__PURE__ */ React4.createElement(Stat, { label: "Max budget", value: budget > 0 ? `$${budget.toFixed(2)}` : "Unlimited" })), /* @__PURE__ */ React4.createElement(Divider, { sx: { mb: 2 } }), /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Models"), team.models?.length ? /* @__PURE__ */ React4.createElement(Box4, { display: "flex", gap: 0.5, flexWrap: "wrap", mb: 2 }, team.models.map((m) => /* @__PURE__ */ React4.createElement(Chip4, { key: m, label: m, size: "small", variant: "outlined" }))) : /* @__PURE__ */ React4.createElement(Typography4, { variant: "body2", color: "text.secondary", mb: 2 }, "All models allowed"), /* @__PURE__ */ React4.createElement(Divider, { sx: { mb: 2 } }), usageLoading ? /* @__PURE__ */ React4.createElement(Box4, { display: "flex", justifyContent: "center", p: 2 }, /* @__PURE__ */ React4.createElement(CircularProgress2, { size: 24 })) : dailyData.length > 0 ? /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend"), /* @__PURE__ */ React4.createElement(Box4, { height: 160, mb: 2 }, /* @__PURE__ */ React4.createElement(ResponsiveContainer2, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(AreaChart2, { data: dailyData }, /* @__PURE__ */ React4.createElement(CartesianGrid2, { strokeDasharray: "3 3" }), /* @__PURE__ */ React4.createElement(XAxis2, { dataKey: "date", tick: { fontSize: 11 } }), /* @__PURE__ */ React4.createElement(YAxis2, { tick: { fontSize: 11 }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React4.createElement(Tooltip2, { formatter: (v) => [`$${v.toFixed(4)}`, "Spend"] }), /* @__PURE__ */ React4.createElement(Area2, { type: "monotone", dataKey: "spend", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.3 })))), /* @__PURE__ */ React4.createElement(Divider, { sx: { mb: 2 } })) : null, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React4.createElement(Memory, { fontSize: "small", color: "action" }), /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary" }, "Members")), team.members_with_roles?.length ? /* @__PURE__ */ React4.createElement(TableContainer3, null, /* @__PURE__ */ React4.createElement(Table3, { size: "small" }, /* @__PURE__ */ React4.createElement(TableHead3, null, /* @__PURE__ */ React4.createElement(TableRow3, null, /* @__PURE__ */ React4.createElement(TableCell3, null, "User"), /* @__PURE__ */ React4.createElement(TableCell3, null, "Role"))), /* @__PURE__ */ React4.createElement(TableBody3, null, team.members_with_roles.map((m) => /* @__PURE__ */ React4.createElement(TableRow3, { key: m.user_id }, /* @__PURE__ */ React4.createElement(TableCell3, { sx: { fontFamily: "monospace", fontSize: 12 } }, m.user_id), /* @__PURE__ */ React4.createElement(TableCell3, null, /* @__PURE__ */ React4.createElement(
952
1052
  Chip4,
953
1053
  {
954
1054
  label: m.role,
955
1055
  size: "small",
956
1056
  color: m.role === "admin" ? "primary" : "default"
957
1057
  }
958
- )))))))) : null)));
1058
+ ))))))) : /* @__PURE__ */ React4.createElement(Typography4, { variant: "body2", color: "text.secondary" }, "No members assigned."))));
959
1059
  };
960
1060
  TeamUsage = ({
961
1061
  teams,
@@ -1312,6 +1412,19 @@ var init_LiteLLMPage = __esm({
1312
1412
  },
1313
1413
  [api, refreshKeys]
1314
1414
  );
1415
+ const handlePruneExpiredKeys = useCallback2(
1416
+ async () => {
1417
+ try {
1418
+ const result = await api.pruneExpiredKeys();
1419
+ setSnackbar({ message: `Pruned ${result.pruned} expired key${result.pruned !== 1 ? "s" : ""}`, severity: "success" });
1420
+ refreshKeys();
1421
+ } catch (e) {
1422
+ setSnackbar({ message: `Failed to prune expired keys: ${e.message}`, severity: "error" });
1423
+ }
1424
+ return { pruned: 0 };
1425
+ },
1426
+ [api, refreshKeys]
1427
+ );
1315
1428
  const isInitialLoading = userLoading && !userInfo;
1316
1429
  if (isInitialLoading) {
1317
1430
  return /* @__PURE__ */ React6.createElement(Box6, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "50vh" }, /* @__PURE__ */ React6.createElement(CircularProgress4, null));
@@ -1355,14 +1468,14 @@ var init_LiteLLMPage = __esm({
1355
1468
  onBlockKey: handleBlockKey,
1356
1469
  onUnblockKey: handleUnblockKey,
1357
1470
  onResetKeySpend: handleResetKeySpend,
1358
- onDeleteKey: handleDeleteKey
1471
+ onDeleteKey: handleDeleteKey,
1472
+ onPruneExpiredKeys: handlePruneExpiredKeys
1359
1473
  }
1360
1474
  ), activeTab === "teams" && /* @__PURE__ */ React6.createElement(
1361
1475
  TeamUsage,
1362
1476
  {
1363
1477
  teams: teams ?? [],
1364
1478
  loading: teamsLoading,
1365
- dateRange,
1366
1479
  getTeamUsage: (teamId) => {
1367
1480
  if (teamUsageCache[teamId] === void 0) loadTeamUsage(teamId);
1368
1481
  return teamUsageCache[teamId] ?? null;
@@ -1425,6 +1538,7 @@ init_TeamUsage();
1425
1538
 
1426
1539
  // src/components/LiteLLMHomeWidget.tsx
1427
1540
  init_api();
1541
+ init_format();
1428
1542
  import React8, { useState as useState6, useEffect } from "react";
1429
1543
  import {
1430
1544
  Paper as Paper7,
@@ -1439,8 +1553,6 @@ import {
1439
1553
  } from "@mui/material";
1440
1554
  import { AreaChart as AreaChart3, Area as Area3, ResponsiveContainer as ResponsiveContainer3 } from "recharts";
1441
1555
  import { useApi as useApi2 } from "@backstage/core-plugin-api";
1442
- var fmtUsd2 = (n) => `$${(n ?? 0).toFixed(n < 1 ? 4 : 2)}`;
1443
- var fmtInt2 = (n) => (n ?? 0).toLocaleString();
1444
1556
  function presetToDateRange(preset) {
1445
1557
  const end = /* @__PURE__ */ new Date();
1446
1558
  const start = /* @__PURE__ */ new Date();
@@ -1502,7 +1614,7 @@ var LiteLLMHomeWidget = ({
1502
1614
  /* @__PURE__ */ React8.createElement(MenuItem4, { value: "today" }, "Today"),
1503
1615
  /* @__PURE__ */ React8.createElement(MenuItem4, { value: "7d" }, "7d"),
1504
1616
  /* @__PURE__ */ React8.createElement(MenuItem4, { value: "30d" }, "30d")
1505
- ))), loading && /* @__PURE__ */ React8.createElement(Box7, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ React8.createElement(CircularProgress5, { size: 32 })), !loading && error && /* @__PURE__ */ React8.createElement(Alert3, { severity: "error", sx: { mt: 1 } }, error), !loading && !error && /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(Grid2, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "USD Spent", value: fmtUsd2(usage?.total_spend ?? 0) })), /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "Tokens In", value: fmtInt2(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "Tokens Out", value: fmtInt2(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "Keys", value: fmtInt2(keys.length) }))), hasSparkline && /* @__PURE__ */ React8.createElement(Box7, { height: 120 }, /* @__PURE__ */ React8.createElement(ResponsiveContainer3, { width: "100%", height: "100%" }, /* @__PURE__ */ React8.createElement(AreaChart3, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 } }, /* @__PURE__ */ React8.createElement(
1617
+ ))), loading && /* @__PURE__ */ React8.createElement(Box7, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ React8.createElement(CircularProgress5, { size: 32 })), !loading && error && /* @__PURE__ */ React8.createElement(Alert3, { severity: "error", sx: { mt: 1 } }, error), !loading && !error && /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(Grid2, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "USD Spent", value: fmtUsd(usage?.total_spend ?? 0) })), /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "Tokens In", value: fmtInt(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "Tokens Out", value: fmtInt(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "Keys", value: fmtInt(keys.length) }))), hasSparkline && /* @__PURE__ */ React8.createElement(Box7, { height: 120 }, /* @__PURE__ */ React8.createElement(ResponsiveContainer3, { width: "100%", height: "100%" }, /* @__PURE__ */ React8.createElement(AreaChart3, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 } }, /* @__PURE__ */ React8.createElement(
1506
1618
  Area3,
1507
1619
  {
1508
1620
  type: "monotone",