@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/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[]>;
@@ -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 {};
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,6 +693,15 @@ 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
  }
@@ -1236,6 +1288,19 @@ var init_LiteLLMPage = __esm({
1236
1288
  },
1237
1289
  [api, refreshKeys]
1238
1290
  );
1291
+ const handlePruneExpiredKeys = (0, import_react6.useCallback)(
1292
+ async () => {
1293
+ try {
1294
+ const result = await api.pruneExpiredKeys();
1295
+ setSnackbar({ message: `Pruned ${result.pruned} expired key${result.pruned !== 1 ? "s" : ""}`, severity: "success" });
1296
+ refreshKeys();
1297
+ } catch (e) {
1298
+ setSnackbar({ message: `Failed to prune expired keys: ${e.message}`, severity: "error" });
1299
+ }
1300
+ return { pruned: 0 };
1301
+ },
1302
+ [api, refreshKeys]
1303
+ );
1239
1304
  const isInitialLoading = userLoading && !userInfo;
1240
1305
  if (isInitialLoading) {
1241
1306
  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,7 +1344,8 @@ var init_LiteLLMPage = __esm({
1279
1344
  onBlockKey: handleBlockKey,
1280
1345
  onUnblockKey: handleUnblockKey,
1281
1346
  onResetKeySpend: handleResetKeySpend,
1282
- onDeleteKey: handleDeleteKey
1347
+ onDeleteKey: handleDeleteKey,
1348
+ onPruneExpiredKeys: handlePruneExpiredKeys
1283
1349
  }
1284
1350
  ), activeTab === "teams" && /* @__PURE__ */ import_react6.default.createElement(
1285
1351
  TeamUsage,