@acarmisc/backstage-plugin-litellm 0.6.0 → 0.7.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,6 +694,15 @@ 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
  }
@@ -1312,6 +1364,19 @@ var init_LiteLLMPage = __esm({
1312
1364
  },
1313
1365
  [api, refreshKeys]
1314
1366
  );
1367
+ const handlePruneExpiredKeys = useCallback2(
1368
+ async () => {
1369
+ try {
1370
+ const result = await api.pruneExpiredKeys();
1371
+ setSnackbar({ message: `Pruned ${result.pruned} expired key${result.pruned !== 1 ? "s" : ""}`, severity: "success" });
1372
+ refreshKeys();
1373
+ } catch (e) {
1374
+ setSnackbar({ message: `Failed to prune expired keys: ${e.message}`, severity: "error" });
1375
+ }
1376
+ return { pruned: 0 };
1377
+ },
1378
+ [api, refreshKeys]
1379
+ );
1315
1380
  const isInitialLoading = userLoading && !userInfo;
1316
1381
  if (isInitialLoading) {
1317
1382
  return /* @__PURE__ */ React6.createElement(Box6, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "50vh" }, /* @__PURE__ */ React6.createElement(CircularProgress4, null));
@@ -1355,7 +1420,8 @@ var init_LiteLLMPage = __esm({
1355
1420
  onBlockKey: handleBlockKey,
1356
1421
  onUnblockKey: handleUnblockKey,
1357
1422
  onResetKeySpend: handleResetKeySpend,
1358
- onDeleteKey: handleDeleteKey
1423
+ onDeleteKey: handleDeleteKey,
1424
+ onPruneExpiredKeys: handlePruneExpiredKeys
1359
1425
  }
1360
1426
  ), activeTab === "teams" && /* @__PURE__ */ React6.createElement(
1361
1427
  TeamUsage,