@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/api.d.ts CHANGED
@@ -12,6 +12,9 @@ export interface LiteLlmApiInterface {
12
12
  blockKey(keyId: string): Promise<void>;
13
13
  unblockKey(keyId: string): Promise<void>;
14
14
  resetKeySpend(keyId: string): Promise<void>;
15
+ pruneExpiredKeys(): Promise<{
16
+ pruned: number;
17
+ }>;
15
18
  listModels(): Promise<ModelInfo[]>;
16
19
  getTeams(): Promise<TeamInfo[]>;
17
20
  getUsage(startDate: string, endDate: string): Promise<UsageMetrics>;
@@ -19,6 +22,8 @@ export interface LiteLlmApiInterface {
19
22
  getAuditLogs(params: AuditLogsParams): Promise<PaginatedAuditLogs>;
20
23
  }
21
24
  export declare const liteLlmApiRef: import("@backstage/core-plugin-api").ApiRef<LiteLlmApiInterface>;
25
+ export type ExpiryStatus = 'expired' | 'soon' | 'ok';
26
+ export declare function expiryStatus(expiresAt?: string): ExpiryStatus | null;
22
27
  export declare class LiteLlmApi implements LiteLlmApiInterface {
23
28
  private fetchApi;
24
29
  private basePath;
@@ -38,6 +43,9 @@ export declare class LiteLlmApi implements LiteLlmApiInterface {
38
43
  blockKey(keyId: string): Promise<void>;
39
44
  unblockKey(keyId: string): Promise<void>;
40
45
  resetKeySpend(keyId: string): Promise<void>;
46
+ pruneExpiredKeys(): Promise<{
47
+ pruned: number;
48
+ }>;
41
49
  getAuditLogs(params: AuditLogsParams): Promise<PaginatedAuditLogs>;
42
50
  listModels(): Promise<ModelInfo[]>;
43
51
  getTeams(): Promise<TeamInfo[]>;
@@ -0,0 +1 @@
1
+ export {};
@@ -12,6 +12,9 @@ interface KeysTableProps {
12
12
  onUnblockKey: (keyId: string) => Promise<void>;
13
13
  onResetKeySpend: (keyId: string) => Promise<void>;
14
14
  onDeleteKey: (keyId: string) => Promise<void>;
15
+ onPruneExpiredKeys: () => Promise<{
16
+ pruned: number;
17
+ }>;
15
18
  }
16
19
  export declare const KeysTable: React.FC<KeysTableProps>;
17
20
  export {};
@@ -1,9 +1,8 @@
1
1
  import React from 'react';
2
- import { TeamInfo, UsageMetrics, DateRange } from '../types';
2
+ import { TeamInfo, UsageMetrics } from '../types';
3
3
  interface TeamUsageProps {
4
4
  teams: TeamInfo[];
5
5
  loading: boolean;
6
- dateRange: DateRange;
7
6
  getTeamUsage: (teamId: string) => UsageMetrics | null;
8
7
  getTeamUsageLoading: (teamId: string) => boolean;
9
8
  }
@@ -0,0 +1,2 @@
1
+ export declare const fmtUsd: (n: number) => string;
2
+ export declare const fmtInt: (n: number) => string;
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.cjs.js CHANGED
@@ -36,6 +36,14 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
36
36
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
37
37
 
38
38
  // src/api.ts
39
+ function expiryStatus(expiresAt) {
40
+ if (!expiresAt) return null;
41
+ const diff = new Date(expiresAt).getTime() - Date.now();
42
+ const days = diff / (1e3 * 60 * 60 * 24);
43
+ if (days < 0) return "expired";
44
+ if (days < 7) return "soon";
45
+ return "ok";
46
+ }
39
47
  var import_core_plugin_api, ApiError, liteLlmApiRef, LiteLlmApi;
40
48
  var init_api = __esm({
41
49
  "src/api.ts"() {
@@ -122,6 +130,19 @@ var init_api = __esm({
122
130
  async resetKeySpend(keyId) {
123
131
  await this.post(`/keys/${encodeURIComponent(keyId)}/reset_spend`, {});
124
132
  }
133
+ async pruneExpiredKeys() {
134
+ const keys = await this.get("/keys");
135
+ const expired = keys.filter((k) => expiryStatus(k.expires_at) === "expired");
136
+ if (expired.length === 0) return { pruned: 0 };
137
+ for (const key of expired) {
138
+ try {
139
+ await this.del(`/keys/${encodeURIComponent(key.token ?? key.key)}`);
140
+ } catch (err) {
141
+ console.warn(`Failed to delete expired key ${key.token ?? key.key}:`, err);
142
+ }
143
+ }
144
+ return { pruned: expired.length };
145
+ }
125
146
  async getAuditLogs(params) {
126
147
  const strParams = {};
127
148
  if (params.page !== void 0) strParams.page = String(params.page);
@@ -179,14 +200,6 @@ var init_DashboardHeader = __esm({
179
200
  });
180
201
 
181
202
  // src/components/KeysTable.tsx
182
- function expiryStatus(expiresAt) {
183
- if (!expiresAt) return null;
184
- const diff = new Date(expiresAt).getTime() - Date.now();
185
- const days = diff / (1e3 * 60 * 60 * 24);
186
- if (days < 0) return "expired";
187
- if (days < 7) return "soon";
188
- return "ok";
189
- }
190
203
  function ExpiryChip({ expiresAt }) {
191
204
  const status = expiryStatus(expiresAt);
192
205
  if (!status) return /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2", color: "text.secondary" }, "-");
@@ -213,6 +226,7 @@ var init_KeysTable = __esm({
213
226
  import_react2 = __toESM(require("react"));
214
227
  import_material2 = require("@mui/material");
215
228
  import_icons_material = require("@mui/icons-material");
229
+ init_api();
216
230
  maskKey = (key) => {
217
231
  if (key.length <= 8) return "***";
218
232
  return `${key.slice(0, 4)}...${key.slice(-4)}`;
@@ -257,7 +271,8 @@ var init_KeysTable = __esm({
257
271
  onBlockKey,
258
272
  onUnblockKey,
259
273
  onResetKeySpend,
260
- onDeleteKey
274
+ onDeleteKey,
275
+ onPruneExpiredKeys
261
276
  }) => {
262
277
  const [generateModalOpen, setGenerateModalOpen] = (0, import_react2.useState)(false);
263
278
  const [newKeyValue, setNewKeyValue] = (0, import_react2.useState)(null);
@@ -370,6 +385,23 @@ var init_KeysTable = __esm({
370
385
  const copyToClipboard = (text) => {
371
386
  navigator.clipboard.writeText(text);
372
387
  };
388
+ const [pruneConfirmCount, setPruneConfirmCount] = (0, import_react2.useState)(null);
389
+ const [pruneSubmitting, setPruneSubmitting] = (0, import_react2.useState)(false);
390
+ const expiredKeys = (0, import_react2.useMemo)(() => {
391
+ return keys.filter((k) => expiryStatus(k.expires_at) === "expired");
392
+ }, [keys]);
393
+ const handlePruneExpired = async () => {
394
+ if (pruneConfirmCount === null || pruneConfirmCount === 0) return;
395
+ setPruneSubmitting(true);
396
+ try {
397
+ await onPruneExpiredKeys();
398
+ } catch (error) {
399
+ console.error("Failed to prune expired keys:", error);
400
+ } finally {
401
+ setPruneSubmitting(false);
402
+ setPruneConfirmCount(null);
403
+ }
404
+ };
373
405
  const modelOption = (m) => {
374
406
  const inCost = fmtCost(m.input_cost_per_token);
375
407
  const outCost = fmtCost(m.output_cost_per_token);
@@ -387,6 +419,17 @@ var init_KeysTable = __esm({
387
419
  startAdornment: /* @__PURE__ */ import_react2.default.createElement(import_material2.InputAdornment, { position: "start" }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Search, { fontSize: "small" }))
388
420
  }
389
421
  }
422
+ ), expiredKeys.length > 0 && /* @__PURE__ */ import_react2.default.createElement(
423
+ import_material2.Button,
424
+ {
425
+ variant: "outlined",
426
+ color: "error",
427
+ startIcon: /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Autorenew, null),
428
+ onClick: () => setPruneConfirmCount(expiredKeys.length)
429
+ },
430
+ "Prune expired (",
431
+ expiredKeys.length,
432
+ ")"
390
433
  ), /* @__PURE__ */ import_react2.default.createElement(
391
434
  import_material2.Button,
392
435
  {
@@ -650,24 +693,44 @@ var init_KeysTable = __esm({
650
693
  disabled: deleteSubmitting
651
694
  },
652
695
  deleteSubmitting ? /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 20 }) : "Revoke"
696
+ ))), /* @__PURE__ */ import_react2.default.createElement(import_material2.Dialog, { open: pruneConfirmCount !== null, onClose: () => setPruneConfirmCount(null), maxWidth: "xs", fullWidth: true }, /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogTitle, null, "Prune Expired Keys?"), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogContent, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, null, "This will permanently delete ", pruneConfirmCount, " expired key", pruneConfirmCount !== 1 ? "s" : "", " from LiteLLM. Any integrations using them will stop working immediately.")), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogActions, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: () => setPruneConfirmCount(null), disabled: pruneSubmitting }, "Cancel"), /* @__PURE__ */ import_react2.default.createElement(
697
+ import_material2.Button,
698
+ {
699
+ onClick: handlePruneExpired,
700
+ variant: "contained",
701
+ color: "error",
702
+ disabled: pruneSubmitting
703
+ },
704
+ pruneSubmitting ? /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 20 }) : "Prune"
653
705
  ))));
654
706
  };
655
707
  }
656
708
  });
657
709
 
710
+ // src/format.ts
711
+ var fmtUsd, fmtInt;
712
+ var init_format = __esm({
713
+ "src/format.ts"() {
714
+ "use strict";
715
+ fmtUsd = (n) => `$${(n ?? 0).toFixed(n < 1 ? 4 : 2)}`;
716
+ fmtInt = (n) => (n ?? 0).toLocaleString();
717
+ }
718
+ });
719
+
658
720
  // src/components/UsageStats.tsx
659
721
  function modelColor(model) {
660
722
  let h = 5381;
661
723
  for (let i = 0; i < model.length; i++) h = (h << 5) + h + model.charCodeAt(i) >>> 0;
662
724
  return MODEL_COLORS[h % MODEL_COLORS.length];
663
725
  }
664
- var import_react3, import_material3, import_recharts, TOP_N_MODELS, PERIOD_LS_KEY, MODEL_COLORS, fmtUsd, fmtInt, fmtPct, KpiCard, ChartSkeleton, EmptyChart, UsageStats;
726
+ var import_react3, import_material3, import_recharts, TOP_N_MODELS, PERIOD_LS_KEY, MODEL_COLORS, fmtPct, KpiCard, ChartSkeleton, EmptyChart, UsageStats;
665
727
  var init_UsageStats = __esm({
666
728
  "src/components/UsageStats.tsx"() {
667
729
  "use strict";
668
730
  import_react3 = __toESM(require("react"));
669
731
  import_material3 = require("@mui/material");
670
732
  import_recharts = require("recharts");
733
+ init_format();
671
734
  TOP_N_MODELS = 6;
672
735
  PERIOD_LS_KEY = "litellm_usage_period";
673
736
  MODEL_COLORS = [
@@ -682,8 +745,6 @@ var init_UsageStats = __esm({
682
745
  "#a4de6c",
683
746
  "#d0ed57"
684
747
  ];
685
- fmtUsd = (n) => `$${(n ?? 0).toFixed(n < 1 ? 4 : 2)}`;
686
- fmtInt = (n) => (n ?? 0).toLocaleString();
687
748
  fmtPct = (n) => `${(n * 100).toFixed(1)}%`;
688
749
  KpiCard = ({ label, value, hint }) => /* @__PURE__ */ import_react3.default.createElement(import_material3.Paper, { variant: "outlined", sx: { p: 2, height: "100%" } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "caption", color: "text.secondary" }, label), /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "h5", sx: { mt: 0.5 } }, value), hint ? /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "caption", color: "text.secondary" }, hint) : null);
689
750
  ChartSkeleton = ({ height = 260 }) => /* @__PURE__ */ import_react3.default.createElement(import_material3.Skeleton, { variant: "rectangular", height, sx: { borderRadius: 1 } });
@@ -866,7 +927,7 @@ var init_UsageStats = __esm({
866
927
  });
867
928
 
868
929
  // src/components/TeamUsage.tsx
869
- var import_react4, import_material4, import_icons_material2, import_recharts2, TeamCard, TeamUsage;
930
+ var import_react4, import_material4, import_icons_material2, import_recharts2, Stat, TeamCard, TeamUsage;
870
931
  var init_TeamUsage = __esm({
871
932
  "src/components/TeamUsage.tsx"() {
872
933
  "use strict";
@@ -874,6 +935,7 @@ var init_TeamUsage = __esm({
874
935
  import_material4 = require("@mui/material");
875
936
  import_icons_material2 = require("@mui/icons-material");
876
937
  import_recharts2 = require("recharts");
938
+ Stat = ({ label, value }) => /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "body2", fontWeight: 600, sx: { fontFamily: "monospace" } }, value));
877
939
  TeamCard = ({ team, usage, usageLoading }) => {
878
940
  const [expanded, setExpanded] = (0, import_react4.useState)(false);
879
941
  const budget = team.max_budget ?? 0;
@@ -882,7 +944,43 @@ var init_TeamUsage = __esm({
882
944
  const isOver = budget > 0 && spend >= budget;
883
945
  const isNear = budget > 0 && spend >= budget * 0.8 && !isOver;
884
946
  const dailyData = usage?.daily_usage?.map((d) => ({ date: d.date, spend: d.spend })) ?? [];
885
- return /* @__PURE__ */ import_react4.default.createElement(import_material4.Paper, { variant: "outlined", sx: { p: 2, mb: 2 } }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_icons_material2.Group, { color: "action" }), /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { flexGrow: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle1", fontWeight: 600 }, team.team_alias ?? team.team_id), team.models?.length ? /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", gap: 0.5, flexWrap: "wrap", mt: 0.5 }, team.models.map((m) => /* @__PURE__ */ import_react4.default.createElement(import_material4.Chip, { key: m, label: m, size: "small", variant: "outlined" }))) : /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "caption", color: "text.secondary" }, "All models")), /* @__PURE__ */ import_react4.default.createElement(import_material4.IconButton, { size: "small", onClick: () => setExpanded((e) => !e) }, expanded ? /* @__PURE__ */ import_react4.default.createElement(import_icons_material2.ExpandLess, null) : /* @__PURE__ */ import_react4.default.createElement(import_icons_material2.ExpandMore, null))), budget > 0 && /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { mt: 1.5 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", justifyContent: "space-between", mb: 0.5 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "body2" }, "$", spend.toFixed(2), " / $", budget.toFixed(2)), isOver && /* @__PURE__ */ import_react4.default.createElement(import_material4.Chip, { label: "Over Budget", size: "small", color: "error" }), isNear && /* @__PURE__ */ import_react4.default.createElement(import_material4.Chip, { label: "Near Limit", size: "small", color: "warning" })), /* @__PURE__ */ import_react4.default.createElement(
947
+ return /* @__PURE__ */ import_react4.default.createElement(import_material4.Paper, { variant: "outlined", sx: { p: 2, mb: 2 } }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_icons_material2.Group, { color: "action" }), /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { flexGrow: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle1", fontWeight: 600 }, team.team_alias || "Untitled team"), /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "caption", color: "text.secondary", sx: { fontFamily: "monospace" } }, team.team_id)), /* @__PURE__ */ import_react4.default.createElement(import_material4.IconButton, { size: "small", onClick: () => setExpanded((e) => !e), "aria-label": "toggle details" }, expanded ? /* @__PURE__ */ import_react4.default.createElement(import_icons_material2.ExpandLess, null) : /* @__PURE__ */ import_react4.default.createElement(import_icons_material2.ExpandMore, null))), /* @__PURE__ */ import_react4.default.createElement(import_material4.Stack, { direction: "row", spacing: 3, mt: 1.5, flexWrap: "wrap", useFlexGap: true, gap: 1.5 }, /* @__PURE__ */ import_react4.default.createElement(
948
+ Stat,
949
+ {
950
+ label: "Members",
951
+ value: team.members_with_roles?.length ? String(team.members_with_roles.length) : "\u2014"
952
+ }
953
+ ), /* @__PURE__ */ import_react4.default.createElement(
954
+ Stat,
955
+ {
956
+ label: "Models",
957
+ value: team.models?.length ? String(team.models.length) : "All"
958
+ }
959
+ ), /* @__PURE__ */ import_react4.default.createElement(
960
+ Stat,
961
+ {
962
+ label: "Budget",
963
+ value: budget > 0 ? `$${budget.toFixed(2)}` : "Unlimited"
964
+ }
965
+ ), /* @__PURE__ */ import_react4.default.createElement(
966
+ Stat,
967
+ {
968
+ label: "Spend",
969
+ value: `$${spend.toFixed(2)}`
970
+ }
971
+ ), /* @__PURE__ */ import_react4.default.createElement(
972
+ Stat,
973
+ {
974
+ label: "TPM",
975
+ value: team.tpm_limit != null && team.tpm_limit > 0 ? String(team.tpm_limit) : "\u2014"
976
+ }
977
+ ), /* @__PURE__ */ import_react4.default.createElement(
978
+ Stat,
979
+ {
980
+ label: "RPM",
981
+ value: team.rpm_limit != null && team.rpm_limit > 0 ? String(team.rpm_limit) : "\u2014"
982
+ }
983
+ )), budget > 0 && /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { mt: 1.5 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", justifyContent: "space-between", mb: 0.5 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "body2" }, "$", spend.toFixed(2), " / $", budget.toFixed(2)), isOver && /* @__PURE__ */ import_react4.default.createElement(import_material4.Chip, { label: "Over Budget", size: "small", color: "error" }), isNear && /* @__PURE__ */ import_react4.default.createElement(import_material4.Chip, { label: "Near Limit", size: "small", color: "warning" })), /* @__PURE__ */ import_react4.default.createElement(
886
984
  import_material4.LinearProgress,
887
985
  {
888
986
  variant: "determinate",
@@ -890,14 +988,14 @@ var init_TeamUsage = __esm({
890
988
  color: isOver ? "error" : isNear ? "warning" : "primary",
891
989
  sx: { height: 6, borderRadius: 1 }
892
990
  }
893
- )), /* @__PURE__ */ import_react4.default.createElement(import_material4.Collapse, { in: expanded }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { mt: 2 }, usageLoading ? /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", justifyContent: "center", p: 2 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.CircularProgress, { size: 24 })) : dailyData.length > 0 ? /* @__PURE__ */ import_react4.default.createElement(import_react4.default.Fragment, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend"), /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { height: 160 }, /* @__PURE__ */ import_react4.default.createElement(import_recharts2.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react4.default.createElement(import_recharts2.AreaChart, { data: dailyData }, /* @__PURE__ */ import_react4.default.createElement(import_recharts2.CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.XAxis, { dataKey: "date", tick: { fontSize: 11 } }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.YAxis, { tick: { fontSize: 11 }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.Tooltip, { formatter: (v) => [`$${v.toFixed(4)}`, "Spend"] }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.Area, { type: "monotone", dataKey: "spend", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.3 }))))) : null, team.members_with_roles?.length ? /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { mt: 2 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Members"), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableContainer, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Table, { size: "small" }, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableHead, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableRow, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, "User"), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, "Role"))), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableBody, null, team.members_with_roles.map((m) => /* @__PURE__ */ import_react4.default.createElement(import_material4.TableRow, { key: m.user_id }, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { sx: { fontFamily: "monospace", fontSize: 12 } }, m.user_id), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, /* @__PURE__ */ import_react4.default.createElement(
991
+ )), /* @__PURE__ */ import_react4.default.createElement(import_material4.Collapse, { in: expanded }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { mt: 2 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_icons_material2.Speed, { fontSize: "small", color: "action" }), /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary" }, "Rate limits")), /* @__PURE__ */ import_react4.default.createElement(import_material4.Stack, { direction: "row", spacing: 3, flexWrap: "wrap", useFlexGap: true, gap: 1.5, mb: 2 }, /* @__PURE__ */ import_react4.default.createElement(Stat, { label: "TPM limit", value: team.tpm_limit != null && team.tpm_limit > 0 ? String(team.tpm_limit) : "Unlimited" }), /* @__PURE__ */ import_react4.default.createElement(Stat, { label: "RPM limit", value: team.rpm_limit != null && team.rpm_limit > 0 ? String(team.rpm_limit) : "Unlimited" }), /* @__PURE__ */ import_react4.default.createElement(Stat, { label: "Max budget", value: budget > 0 ? `$${budget.toFixed(2)}` : "Unlimited" })), /* @__PURE__ */ import_react4.default.createElement(import_material4.Divider, { sx: { mb: 2 } }), /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Models"), team.models?.length ? /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", gap: 0.5, flexWrap: "wrap", mb: 2 }, team.models.map((m) => /* @__PURE__ */ import_react4.default.createElement(import_material4.Chip, { key: m, label: m, size: "small", variant: "outlined" }))) : /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "body2", color: "text.secondary", mb: 2 }, "All models allowed"), /* @__PURE__ */ import_react4.default.createElement(import_material4.Divider, { sx: { mb: 2 } }), usageLoading ? /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", justifyContent: "center", p: 2 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.CircularProgress, { size: 24 })) : dailyData.length > 0 ? /* @__PURE__ */ import_react4.default.createElement(import_react4.default.Fragment, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend"), /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { height: 160, mb: 2 }, /* @__PURE__ */ import_react4.default.createElement(import_recharts2.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react4.default.createElement(import_recharts2.AreaChart, { data: dailyData }, /* @__PURE__ */ import_react4.default.createElement(import_recharts2.CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.XAxis, { dataKey: "date", tick: { fontSize: 11 } }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.YAxis, { tick: { fontSize: 11 }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.Tooltip, { formatter: (v) => [`$${v.toFixed(4)}`, "Spend"] }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.Area, { type: "monotone", dataKey: "spend", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.3 })))), /* @__PURE__ */ import_react4.default.createElement(import_material4.Divider, { sx: { mb: 2 } })) : null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_icons_material2.Memory, { fontSize: "small", color: "action" }), /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary" }, "Members")), team.members_with_roles?.length ? /* @__PURE__ */ import_react4.default.createElement(import_material4.TableContainer, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Table, { size: "small" }, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableHead, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableRow, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, "User"), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, "Role"))), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableBody, null, team.members_with_roles.map((m) => /* @__PURE__ */ import_react4.default.createElement(import_material4.TableRow, { key: m.user_id }, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { sx: { fontFamily: "monospace", fontSize: 12 } }, m.user_id), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, /* @__PURE__ */ import_react4.default.createElement(
894
992
  import_material4.Chip,
895
993
  {
896
994
  label: m.role,
897
995
  size: "small",
898
996
  color: m.role === "admin" ? "primary" : "default"
899
997
  }
900
- )))))))) : null)));
998
+ ))))))) : /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "body2", color: "text.secondary" }, "No members assigned."))));
901
999
  };
902
1000
  TeamUsage = ({
903
1001
  teams,
@@ -1236,6 +1334,19 @@ var init_LiteLLMPage = __esm({
1236
1334
  },
1237
1335
  [api, refreshKeys]
1238
1336
  );
1337
+ const handlePruneExpiredKeys = (0, import_react6.useCallback)(
1338
+ async () => {
1339
+ try {
1340
+ const result = await api.pruneExpiredKeys();
1341
+ setSnackbar({ message: `Pruned ${result.pruned} expired key${result.pruned !== 1 ? "s" : ""}`, severity: "success" });
1342
+ refreshKeys();
1343
+ } catch (e) {
1344
+ setSnackbar({ message: `Failed to prune expired keys: ${e.message}`, severity: "error" });
1345
+ }
1346
+ return { pruned: 0 };
1347
+ },
1348
+ [api, refreshKeys]
1349
+ );
1239
1350
  const isInitialLoading = userLoading && !userInfo;
1240
1351
  if (isInitialLoading) {
1241
1352
  return /* @__PURE__ */ import_react6.default.createElement(import_material6.Box, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "50vh" }, /* @__PURE__ */ import_react6.default.createElement(import_material6.CircularProgress, null));
@@ -1279,14 +1390,14 @@ var init_LiteLLMPage = __esm({
1279
1390
  onBlockKey: handleBlockKey,
1280
1391
  onUnblockKey: handleUnblockKey,
1281
1392
  onResetKeySpend: handleResetKeySpend,
1282
- onDeleteKey: handleDeleteKey
1393
+ onDeleteKey: handleDeleteKey,
1394
+ onPruneExpiredKeys: handlePruneExpiredKeys
1283
1395
  }
1284
1396
  ), activeTab === "teams" && /* @__PURE__ */ import_react6.default.createElement(
1285
1397
  TeamUsage,
1286
1398
  {
1287
1399
  teams: teams ?? [],
1288
1400
  loading: teamsLoading,
1289
- dateRange,
1290
1401
  getTeamUsage: (teamId) => {
1291
1402
  if (teamUsageCache[teamId] === void 0) loadTeamUsage(teamId);
1292
1403
  return teamUsageCache[teamId] ?? null;
@@ -1363,8 +1474,7 @@ var import_material7 = require("@mui/material");
1363
1474
  var import_recharts3 = require("recharts");
1364
1475
  var import_core_plugin_api3 = require("@backstage/core-plugin-api");
1365
1476
  init_api();
1366
- var fmtUsd2 = (n) => `$${(n ?? 0).toFixed(n < 1 ? 4 : 2)}`;
1367
- var fmtInt2 = (n) => (n ?? 0).toLocaleString();
1477
+ init_format();
1368
1478
  function presetToDateRange(preset) {
1369
1479
  const end = /* @__PURE__ */ new Date();
1370
1480
  const start = /* @__PURE__ */ new Date();
@@ -1426,7 +1536,7 @@ var LiteLLMHomeWidget = ({
1426
1536
  /* @__PURE__ */ import_react8.default.createElement(import_material7.MenuItem, { value: "today" }, "Today"),
1427
1537
  /* @__PURE__ */ import_react8.default.createElement(import_material7.MenuItem, { value: "7d" }, "7d"),
1428
1538
  /* @__PURE__ */ import_react8.default.createElement(import_material7.MenuItem, { value: "30d" }, "30d")
1429
- ))), loading && /* @__PURE__ */ import_react8.default.createElement(import_material7.Box, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ import_react8.default.createElement(import_material7.CircularProgress, { size: 32 })), !loading && error && /* @__PURE__ */ import_react8.default.createElement(import_material7.Alert, { severity: "error", sx: { mt: 1 } }, error), !loading && !error && /* @__PURE__ */ import_react8.default.createElement(import_react8.default.Fragment, null, /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "USD Spent", value: fmtUsd2(usage?.total_spend ?? 0) })), /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "Tokens In", value: fmtInt2(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "Tokens Out", value: fmtInt2(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "Keys", value: fmtInt2(keys.length) }))), hasSparkline && /* @__PURE__ */ import_react8.default.createElement(import_material7.Box, { height: 120 }, /* @__PURE__ */ import_react8.default.createElement(import_recharts3.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react8.default.createElement(import_recharts3.AreaChart, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 } }, /* @__PURE__ */ import_react8.default.createElement(
1539
+ ))), loading && /* @__PURE__ */ import_react8.default.createElement(import_material7.Box, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ import_react8.default.createElement(import_material7.CircularProgress, { size: 32 })), !loading && error && /* @__PURE__ */ import_react8.default.createElement(import_material7.Alert, { severity: "error", sx: { mt: 1 } }, error), !loading && !error && /* @__PURE__ */ import_react8.default.createElement(import_react8.default.Fragment, null, /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "USD Spent", value: fmtUsd(usage?.total_spend ?? 0) })), /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "Tokens In", value: fmtInt(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "Tokens Out", value: fmtInt(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "Keys", value: fmtInt(keys.length) }))), hasSparkline && /* @__PURE__ */ import_react8.default.createElement(import_material7.Box, { height: 120 }, /* @__PURE__ */ import_react8.default.createElement(import_recharts3.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react8.default.createElement(import_recharts3.AreaChart, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 } }, /* @__PURE__ */ import_react8.default.createElement(
1430
1540
  import_recharts3.Area,
1431
1541
  {
1432
1542
  type: "monotone",