@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/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
|
}
|
|
@@ -1121,7 +1173,7 @@ __export(LiteLLMPage_exports, {
|
|
|
1121
1173
|
LiteLLMPage: () => LiteLLMPage
|
|
1122
1174
|
});
|
|
1123
1175
|
import React6, { useState as useState5, useCallback as useCallback2, useMemo as useMemo3 } from "react";
|
|
1124
|
-
import {
|
|
1176
|
+
import { Box as Box6, Snackbar, Alert as Alert2, CircularProgress as CircularProgress4, Typography as Typography6, Paper as Paper6, Tabs as Tabs2, Tab as Tab2 } from "@mui/material";
|
|
1125
1177
|
import { useAsync as useAsync2, useAsyncRetry } from "react-use";
|
|
1126
1178
|
import { useApi } from "@backstage/core-plugin-api";
|
|
1127
1179
|
function initDateRange() {
|
|
@@ -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));
|
|
@@ -1321,7 +1386,7 @@ var init_LiteLLMPage = __esm({
|
|
|
1321
1386
|
const hint = userError?.body?.hint;
|
|
1322
1387
|
return /* @__PURE__ */ React6.createElement(Box6, { p: 3 }, /* @__PURE__ */ React6.createElement(Paper6, { sx: { p: 3 } }, /* @__PURE__ */ React6.createElement(Typography6, { variant: "h6", gutterBottom: true }, "Account not provisioned"), /* @__PURE__ */ React6.createElement(Typography6, { color: "text.secondary", paragraph: true }, "Your Backstage account is not linked to a LiteLLM user."), hint ? /* @__PURE__ */ React6.createElement(Typography6, { variant: "body2", color: "text.secondary" }, hint) : /* @__PURE__ */ React6.createElement(Typography6, { 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.")));
|
|
1323
1388
|
}
|
|
1324
|
-
return /* @__PURE__ */ React6.createElement(Box6, { p: 3 },
|
|
1389
|
+
return /* @__PURE__ */ React6.createElement(Box6, { p: 3 }, /* @__PURE__ */ React6.createElement(Box6, { mb: 2 }, /* @__PURE__ */ React6.createElement(DashboardHeader, { userInfo, teams: teams ?? [], loading: userLoading || teamsLoading })), /* @__PURE__ */ React6.createElement(
|
|
1325
1390
|
Tabs2,
|
|
1326
1391
|
{
|
|
1327
1392
|
value: activeTab,
|
|
@@ -1329,20 +1394,10 @@ var init_LiteLLMPage = __esm({
|
|
|
1329
1394
|
sx: { mb: 2, borderBottom: 1, borderColor: "divider" }
|
|
1330
1395
|
},
|
|
1331
1396
|
/* @__PURE__ */ React6.createElement(Tab2, { label: "Overview", value: "overview" }),
|
|
1332
|
-
/* @__PURE__ */ React6.createElement(Tab2, { label: "
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
teams: teams ?? [],
|
|
1337
|
-
loading: teamsLoading,
|
|
1338
|
-
dateRange,
|
|
1339
|
-
getTeamUsage: (teamId) => {
|
|
1340
|
-
if (teamUsageCache[teamId] === void 0) loadTeamUsage(teamId);
|
|
1341
|
-
return teamUsageCache[teamId] ?? null;
|
|
1342
|
-
},
|
|
1343
|
-
getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false
|
|
1344
|
-
}
|
|
1345
|
-
)), /* @__PURE__ */ React6.createElement(Grid2, { item: true, xs: 12 }, /* @__PURE__ */ React6.createElement(
|
|
1397
|
+
/* @__PURE__ */ React6.createElement(Tab2, { label: "Keys", value: "keys" }),
|
|
1398
|
+
/* @__PURE__ */ React6.createElement(Tab2, { label: "Teams", value: "teams" }),
|
|
1399
|
+
userInfo.can_view_audit && /* @__PURE__ */ React6.createElement(Tab2, { label: "Audit Log", value: "audit" })
|
|
1400
|
+
), activeTab === "overview" && /* @__PURE__ */ React6.createElement(
|
|
1346
1401
|
UsageStats,
|
|
1347
1402
|
{
|
|
1348
1403
|
usage: usage ?? null,
|
|
@@ -1352,7 +1407,7 @@ var init_LiteLLMPage = __esm({
|
|
|
1352
1407
|
loading: usageLoading,
|
|
1353
1408
|
userInfo
|
|
1354
1409
|
}
|
|
1355
|
-
)
|
|
1410
|
+
), activeTab === "keys" && /* @__PURE__ */ React6.createElement(
|
|
1356
1411
|
KeysTable,
|
|
1357
1412
|
{
|
|
1358
1413
|
keys: keys ?? [],
|
|
@@ -1365,9 +1420,22 @@ var init_LiteLLMPage = __esm({
|
|
|
1365
1420
|
onBlockKey: handleBlockKey,
|
|
1366
1421
|
onUnblockKey: handleUnblockKey,
|
|
1367
1422
|
onResetKeySpend: handleResetKeySpend,
|
|
1368
|
-
onDeleteKey: handleDeleteKey
|
|
1423
|
+
onDeleteKey: handleDeleteKey,
|
|
1424
|
+
onPruneExpiredKeys: handlePruneExpiredKeys
|
|
1425
|
+
}
|
|
1426
|
+
), activeTab === "teams" && /* @__PURE__ */ React6.createElement(
|
|
1427
|
+
TeamUsage,
|
|
1428
|
+
{
|
|
1429
|
+
teams: teams ?? [],
|
|
1430
|
+
loading: teamsLoading,
|
|
1431
|
+
dateRange,
|
|
1432
|
+
getTeamUsage: (teamId) => {
|
|
1433
|
+
if (teamUsageCache[teamId] === void 0) loadTeamUsage(teamId);
|
|
1434
|
+
return teamUsageCache[teamId] ?? null;
|
|
1435
|
+
},
|
|
1436
|
+
getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false
|
|
1369
1437
|
}
|
|
1370
|
-
))
|
|
1438
|
+
), activeTab === "audit" && userInfo.can_view_audit && /* @__PURE__ */ React6.createElement(AuditLog, { api }), /* @__PURE__ */ React6.createElement(
|
|
1371
1439
|
Snackbar,
|
|
1372
1440
|
{
|
|
1373
1441
|
open: !!snackbar,
|
|
@@ -1431,7 +1499,7 @@ import {
|
|
|
1431
1499
|
FormControl as FormControl2,
|
|
1432
1500
|
Select as Select2,
|
|
1433
1501
|
MenuItem as MenuItem4,
|
|
1434
|
-
Grid as
|
|
1502
|
+
Grid as Grid2,
|
|
1435
1503
|
CircularProgress as CircularProgress5,
|
|
1436
1504
|
Alert as Alert3
|
|
1437
1505
|
} from "@mui/material";
|
|
@@ -1500,7 +1568,7 @@ var LiteLLMHomeWidget = ({
|
|
|
1500
1568
|
/* @__PURE__ */ React8.createElement(MenuItem4, { value: "today" }, "Today"),
|
|
1501
1569
|
/* @__PURE__ */ React8.createElement(MenuItem4, { value: "7d" }, "7d"),
|
|
1502
1570
|
/* @__PURE__ */ React8.createElement(MenuItem4, { value: "30d" }, "30d")
|
|
1503
|
-
))), loading && /* @__PURE__ */ React8.createElement(Box7, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ React8.createElement(CircularProgress5, { size: 32 })), !loading && error && /* @__PURE__ */ React8.createElement(Alert3, { severity: "error", sx: { mt: 1 } }, error), !loading && !error && /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(
|
|
1571
|
+
))), loading && /* @__PURE__ */ React8.createElement(Box7, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ React8.createElement(CircularProgress5, { size: 32 })), !loading && error && /* @__PURE__ */ React8.createElement(Alert3, { severity: "error", sx: { mt: 1 } }, error), !loading && !error && /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(Grid2, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "USD Spent", value: fmtUsd2(usage?.total_spend ?? 0) })), /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "Tokens In", value: fmtInt2(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "Tokens Out", value: fmtInt2(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "Keys", value: fmtInt2(keys.length) }))), hasSparkline && /* @__PURE__ */ React8.createElement(Box7, { height: 120 }, /* @__PURE__ */ React8.createElement(ResponsiveContainer3, { width: "100%", height: "100%" }, /* @__PURE__ */ React8.createElement(AreaChart3, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 } }, /* @__PURE__ */ React8.createElement(
|
|
1504
1572
|
Area3,
|
|
1505
1573
|
{
|
|
1506
1574
|
type: "monotone",
|