@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 +8 -0
- package/dist/components/KeysTable.d.ts +3 -0
- package/dist/index.cjs.js +76 -10
- package/dist/index.cjs.js.map +2 -2
- package/dist/index.esm.js +76 -10
- package/dist/index.esm.js.map +2 -2
- package/package.json +1 -1
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,
|