@acarmisc/backstage-plugin-litellm 0.5.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 +95 -27
- package/dist/index.cjs.js.map +2 -2
- package/dist/index.esm.js +98 -30
- package/dist/index.esm.js.map +3 -3
- 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));
|
|
@@ -1245,7 +1310,7 @@ var init_LiteLLMPage = __esm({
|
|
|
1245
1310
|
const hint = userError?.body?.hint;
|
|
1246
1311
|
return /* @__PURE__ */ import_react6.default.createElement(import_material6.Box, { p: 3 }, /* @__PURE__ */ import_react6.default.createElement(import_material6.Paper, { sx: { p: 3 } }, /* @__PURE__ */ import_react6.default.createElement(import_material6.Typography, { variant: "h6", gutterBottom: true }, "Account not provisioned"), /* @__PURE__ */ import_react6.default.createElement(import_material6.Typography, { color: "text.secondary", paragraph: true }, "Your Backstage account is not linked to a LiteLLM user."), hint ? /* @__PURE__ */ import_react6.default.createElement(import_material6.Typography, { variant: "body2", color: "text.secondary" }, hint) : /* @__PURE__ */ import_react6.default.createElement(import_material6.Typography, { variant: "body2", color: "text.secondary" }, isProvisioningEnabled ? "Auto-provisioning is enabled but failed. Check the backend logs." : "Set litellm.provisioning.enabled: true in app-config.yaml to enable auto-provisioning, or ask your administrator to create the account manually.")));
|
|
1247
1312
|
}
|
|
1248
|
-
return /* @__PURE__ */ import_react6.default.createElement(import_material6.Box, { p: 3 },
|
|
1313
|
+
return /* @__PURE__ */ import_react6.default.createElement(import_material6.Box, { p: 3 }, /* @__PURE__ */ import_react6.default.createElement(import_material6.Box, { mb: 2 }, /* @__PURE__ */ import_react6.default.createElement(DashboardHeader, { userInfo, teams: teams ?? [], loading: userLoading || teamsLoading })), /* @__PURE__ */ import_react6.default.createElement(
|
|
1249
1314
|
import_material6.Tabs,
|
|
1250
1315
|
{
|
|
1251
1316
|
value: activeTab,
|
|
@@ -1253,20 +1318,10 @@ var init_LiteLLMPage = __esm({
|
|
|
1253
1318
|
sx: { mb: 2, borderBottom: 1, borderColor: "divider" }
|
|
1254
1319
|
},
|
|
1255
1320
|
/* @__PURE__ */ import_react6.default.createElement(import_material6.Tab, { label: "Overview", value: "overview" }),
|
|
1256
|
-
/* @__PURE__ */ import_react6.default.createElement(import_material6.Tab, { label: "
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
teams: teams ?? [],
|
|
1261
|
-
loading: teamsLoading,
|
|
1262
|
-
dateRange,
|
|
1263
|
-
getTeamUsage: (teamId) => {
|
|
1264
|
-
if (teamUsageCache[teamId] === void 0) loadTeamUsage(teamId);
|
|
1265
|
-
return teamUsageCache[teamId] ?? null;
|
|
1266
|
-
},
|
|
1267
|
-
getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false
|
|
1268
|
-
}
|
|
1269
|
-
)), /* @__PURE__ */ import_react6.default.createElement(import_material6.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react6.default.createElement(
|
|
1321
|
+
/* @__PURE__ */ import_react6.default.createElement(import_material6.Tab, { label: "Keys", value: "keys" }),
|
|
1322
|
+
/* @__PURE__ */ import_react6.default.createElement(import_material6.Tab, { label: "Teams", value: "teams" }),
|
|
1323
|
+
userInfo.can_view_audit && /* @__PURE__ */ import_react6.default.createElement(import_material6.Tab, { label: "Audit Log", value: "audit" })
|
|
1324
|
+
), activeTab === "overview" && /* @__PURE__ */ import_react6.default.createElement(
|
|
1270
1325
|
UsageStats,
|
|
1271
1326
|
{
|
|
1272
1327
|
usage: usage ?? null,
|
|
@@ -1276,7 +1331,7 @@ var init_LiteLLMPage = __esm({
|
|
|
1276
1331
|
loading: usageLoading,
|
|
1277
1332
|
userInfo
|
|
1278
1333
|
}
|
|
1279
|
-
)
|
|
1334
|
+
), activeTab === "keys" && /* @__PURE__ */ import_react6.default.createElement(
|
|
1280
1335
|
KeysTable,
|
|
1281
1336
|
{
|
|
1282
1337
|
keys: keys ?? [],
|
|
@@ -1289,9 +1344,22 @@ var init_LiteLLMPage = __esm({
|
|
|
1289
1344
|
onBlockKey: handleBlockKey,
|
|
1290
1345
|
onUnblockKey: handleUnblockKey,
|
|
1291
1346
|
onResetKeySpend: handleResetKeySpend,
|
|
1292
|
-
onDeleteKey: handleDeleteKey
|
|
1347
|
+
onDeleteKey: handleDeleteKey,
|
|
1348
|
+
onPruneExpiredKeys: handlePruneExpiredKeys
|
|
1349
|
+
}
|
|
1350
|
+
), activeTab === "teams" && /* @__PURE__ */ import_react6.default.createElement(
|
|
1351
|
+
TeamUsage,
|
|
1352
|
+
{
|
|
1353
|
+
teams: teams ?? [],
|
|
1354
|
+
loading: teamsLoading,
|
|
1355
|
+
dateRange,
|
|
1356
|
+
getTeamUsage: (teamId) => {
|
|
1357
|
+
if (teamUsageCache[teamId] === void 0) loadTeamUsage(teamId);
|
|
1358
|
+
return teamUsageCache[teamId] ?? null;
|
|
1359
|
+
},
|
|
1360
|
+
getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false
|
|
1293
1361
|
}
|
|
1294
|
-
))
|
|
1362
|
+
), activeTab === "audit" && userInfo.can_view_audit && /* @__PURE__ */ import_react6.default.createElement(AuditLog, { api }), /* @__PURE__ */ import_react6.default.createElement(
|
|
1295
1363
|
import_material6.Snackbar,
|
|
1296
1364
|
{
|
|
1297
1365
|
open: !!snackbar,
|