@acarmisc/backstage-plugin-litellm 0.6.0 → 0.8.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/api.test.d.ts +1 -0
- package/dist/components/KeysTable.d.ts +3 -0
- package/dist/components/TeamUsage.d.ts +1 -2
- package/dist/format.d.ts +2 -0
- package/dist/format.test.d.ts +1 -0
- package/dist/index.cjs.js +131 -21
- package/dist/index.cjs.js.map +4 -4
- package/dist/index.esm.js +134 -22
- package/dist/index.esm.js.map +4 -4
- package/package.json +3 -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,11 +694,30 @@ 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
|
}
|
|
657
709
|
});
|
|
658
710
|
|
|
711
|
+
// src/format.ts
|
|
712
|
+
var fmtUsd, fmtInt;
|
|
713
|
+
var init_format = __esm({
|
|
714
|
+
"src/format.ts"() {
|
|
715
|
+
"use strict";
|
|
716
|
+
fmtUsd = (n) => `$${(n ?? 0).toFixed(n < 1 ? 4 : 2)}`;
|
|
717
|
+
fmtInt = (n) => (n ?? 0).toLocaleString();
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
|
|
659
721
|
// src/components/UsageStats.tsx
|
|
660
722
|
import React3, { useMemo as useMemo2, useState as useState2 } from "react";
|
|
661
723
|
import {
|
|
@@ -699,10 +761,11 @@ function modelColor(model) {
|
|
|
699
761
|
for (let i = 0; i < model.length; i++) h = (h << 5) + h + model.charCodeAt(i) >>> 0;
|
|
700
762
|
return MODEL_COLORS[h % MODEL_COLORS.length];
|
|
701
763
|
}
|
|
702
|
-
var TOP_N_MODELS, PERIOD_LS_KEY, MODEL_COLORS,
|
|
764
|
+
var TOP_N_MODELS, PERIOD_LS_KEY, MODEL_COLORS, fmtPct, KpiCard, ChartSkeleton, EmptyChart, UsageStats;
|
|
703
765
|
var init_UsageStats = __esm({
|
|
704
766
|
"src/components/UsageStats.tsx"() {
|
|
705
767
|
"use strict";
|
|
768
|
+
init_format();
|
|
706
769
|
TOP_N_MODELS = 6;
|
|
707
770
|
PERIOD_LS_KEY = "litellm_usage_period";
|
|
708
771
|
MODEL_COLORS = [
|
|
@@ -717,8 +780,6 @@ var init_UsageStats = __esm({
|
|
|
717
780
|
"#a4de6c",
|
|
718
781
|
"#d0ed57"
|
|
719
782
|
];
|
|
720
|
-
fmtUsd = (n) => `$${(n ?? 0).toFixed(n < 1 ? 4 : 2)}`;
|
|
721
|
-
fmtInt = (n) => (n ?? 0).toLocaleString();
|
|
722
783
|
fmtPct = (n) => `${(n * 100).toFixed(1)}%`;
|
|
723
784
|
KpiCard = ({ label, value, hint }) => /* @__PURE__ */ React3.createElement(Paper3, { variant: "outlined", sx: { p: 2, height: "100%" } }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption", color: "text.secondary" }, label), /* @__PURE__ */ React3.createElement(Typography3, { variant: "h5", sx: { mt: 0.5 } }, value), hint ? /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption", color: "text.secondary" }, hint) : null);
|
|
724
785
|
ChartSkeleton = ({ height = 260 }) => /* @__PURE__ */ React3.createElement(Skeleton, { variant: "rectangular", height, sx: { borderRadius: 1 } });
|
|
@@ -908,6 +969,8 @@ import {
|
|
|
908
969
|
Typography as Typography4,
|
|
909
970
|
LinearProgress as LinearProgress4,
|
|
910
971
|
Chip as Chip4,
|
|
972
|
+
Divider,
|
|
973
|
+
Stack,
|
|
911
974
|
Table as Table3,
|
|
912
975
|
TableBody as TableBody3,
|
|
913
976
|
TableCell as TableCell3,
|
|
@@ -918,7 +981,7 @@ import {
|
|
|
918
981
|
IconButton as IconButton2,
|
|
919
982
|
CircularProgress as CircularProgress2
|
|
920
983
|
} from "@mui/material";
|
|
921
|
-
import { ExpandMore, ExpandLess, Group } from "@mui/icons-material";
|
|
984
|
+
import { ExpandMore, ExpandLess, Group, Speed, Memory } from "@mui/icons-material";
|
|
922
985
|
import {
|
|
923
986
|
AreaChart as AreaChart2,
|
|
924
987
|
Area as Area2,
|
|
@@ -928,10 +991,11 @@ import {
|
|
|
928
991
|
Tooltip as Tooltip2,
|
|
929
992
|
ResponsiveContainer as ResponsiveContainer2
|
|
930
993
|
} from "recharts";
|
|
931
|
-
var TeamCard, TeamUsage;
|
|
994
|
+
var Stat, TeamCard, TeamUsage;
|
|
932
995
|
var init_TeamUsage = __esm({
|
|
933
996
|
"src/components/TeamUsage.tsx"() {
|
|
934
997
|
"use strict";
|
|
998
|
+
Stat = ({ label, value }) => /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ React4.createElement(Typography4, { variant: "body2", fontWeight: 600, sx: { fontFamily: "monospace" } }, value));
|
|
935
999
|
TeamCard = ({ team, usage, usageLoading }) => {
|
|
936
1000
|
const [expanded, setExpanded] = useState3(false);
|
|
937
1001
|
const budget = team.max_budget ?? 0;
|
|
@@ -940,7 +1004,43 @@ var init_TeamUsage = __esm({
|
|
|
940
1004
|
const isOver = budget > 0 && spend >= budget;
|
|
941
1005
|
const isNear = budget > 0 && spend >= budget * 0.8 && !isOver;
|
|
942
1006
|
const dailyData = usage?.daily_usage?.map((d) => ({ date: d.date, spend: d.spend })) ?? [];
|
|
943
|
-
return /* @__PURE__ */ React4.createElement(Paper4, { variant: "outlined", sx: { p: 2, mb: 2 } }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React4.createElement(Group, { color: "action" }), /* @__PURE__ */ React4.createElement(Box4, { flexGrow: 1 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle1", fontWeight: 600 }, team.team_alias
|
|
1007
|
+
return /* @__PURE__ */ React4.createElement(Paper4, { variant: "outlined", sx: { p: 2, mb: 2 } }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React4.createElement(Group, { color: "action" }), /* @__PURE__ */ React4.createElement(Box4, { flexGrow: 1 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle1", fontWeight: 600 }, team.team_alias || "Untitled team"), /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption", color: "text.secondary", sx: { fontFamily: "monospace" } }, team.team_id)), /* @__PURE__ */ React4.createElement(IconButton2, { size: "small", onClick: () => setExpanded((e) => !e), "aria-label": "toggle details" }, expanded ? /* @__PURE__ */ React4.createElement(ExpandLess, null) : /* @__PURE__ */ React4.createElement(ExpandMore, null))), /* @__PURE__ */ React4.createElement(Stack, { direction: "row", spacing: 3, mt: 1.5, flexWrap: "wrap", useFlexGap: true, gap: 1.5 }, /* @__PURE__ */ React4.createElement(
|
|
1008
|
+
Stat,
|
|
1009
|
+
{
|
|
1010
|
+
label: "Members",
|
|
1011
|
+
value: team.members_with_roles?.length ? String(team.members_with_roles.length) : "\u2014"
|
|
1012
|
+
}
|
|
1013
|
+
), /* @__PURE__ */ React4.createElement(
|
|
1014
|
+
Stat,
|
|
1015
|
+
{
|
|
1016
|
+
label: "Models",
|
|
1017
|
+
value: team.models?.length ? String(team.models.length) : "All"
|
|
1018
|
+
}
|
|
1019
|
+
), /* @__PURE__ */ React4.createElement(
|
|
1020
|
+
Stat,
|
|
1021
|
+
{
|
|
1022
|
+
label: "Budget",
|
|
1023
|
+
value: budget > 0 ? `$${budget.toFixed(2)}` : "Unlimited"
|
|
1024
|
+
}
|
|
1025
|
+
), /* @__PURE__ */ React4.createElement(
|
|
1026
|
+
Stat,
|
|
1027
|
+
{
|
|
1028
|
+
label: "Spend",
|
|
1029
|
+
value: `$${spend.toFixed(2)}`
|
|
1030
|
+
}
|
|
1031
|
+
), /* @__PURE__ */ React4.createElement(
|
|
1032
|
+
Stat,
|
|
1033
|
+
{
|
|
1034
|
+
label: "TPM",
|
|
1035
|
+
value: team.tpm_limit != null && team.tpm_limit > 0 ? String(team.tpm_limit) : "\u2014"
|
|
1036
|
+
}
|
|
1037
|
+
), /* @__PURE__ */ React4.createElement(
|
|
1038
|
+
Stat,
|
|
1039
|
+
{
|
|
1040
|
+
label: "RPM",
|
|
1041
|
+
value: team.rpm_limit != null && team.rpm_limit > 0 ? String(team.rpm_limit) : "\u2014"
|
|
1042
|
+
}
|
|
1043
|
+
)), budget > 0 && /* @__PURE__ */ React4.createElement(Box4, { mt: 1.5 }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", justifyContent: "space-between", mb: 0.5 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "body2" }, "$", spend.toFixed(2), " / $", budget.toFixed(2)), isOver && /* @__PURE__ */ React4.createElement(Chip4, { label: "Over Budget", size: "small", color: "error" }), isNear && /* @__PURE__ */ React4.createElement(Chip4, { label: "Near Limit", size: "small", color: "warning" })), /* @__PURE__ */ React4.createElement(
|
|
944
1044
|
LinearProgress4,
|
|
945
1045
|
{
|
|
946
1046
|
variant: "determinate",
|
|
@@ -948,14 +1048,14 @@ var init_TeamUsage = __esm({
|
|
|
948
1048
|
color: isOver ? "error" : isNear ? "warning" : "primary",
|
|
949
1049
|
sx: { height: 6, borderRadius: 1 }
|
|
950
1050
|
}
|
|
951
|
-
)), /* @__PURE__ */ React4.createElement(Collapse, { in: expanded }, /* @__PURE__ */ React4.createElement(Box4, { mt: 2 }, usageLoading ? /* @__PURE__ */ React4.createElement(Box4, { display: "flex", justifyContent: "center", p: 2 }, /* @__PURE__ */ React4.createElement(CircularProgress2, { size: 24 })) : dailyData.length > 0 ? /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend"), /* @__PURE__ */ React4.createElement(Box4, { height: 160 }, /* @__PURE__ */ React4.createElement(ResponsiveContainer2, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(AreaChart2, { data: dailyData }, /* @__PURE__ */ React4.createElement(CartesianGrid2, { strokeDasharray: "3 3" }), /* @__PURE__ */ React4.createElement(XAxis2, { dataKey: "date", tick: { fontSize: 11 } }), /* @__PURE__ */ React4.createElement(YAxis2, { tick: { fontSize: 11 }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React4.createElement(Tooltip2, { formatter: (v) => [`$${v.toFixed(4)}`, "Spend"] }), /* @__PURE__ */ React4.createElement(Area2, { type: "monotone", dataKey: "spend", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.3 }))))) : null,
|
|
1051
|
+
)), /* @__PURE__ */ React4.createElement(Collapse, { in: expanded }, /* @__PURE__ */ React4.createElement(Box4, { mt: 2 }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React4.createElement(Speed, { fontSize: "small", color: "action" }), /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary" }, "Rate limits")), /* @__PURE__ */ React4.createElement(Stack, { direction: "row", spacing: 3, flexWrap: "wrap", useFlexGap: true, gap: 1.5, mb: 2 }, /* @__PURE__ */ React4.createElement(Stat, { label: "TPM limit", value: team.tpm_limit != null && team.tpm_limit > 0 ? String(team.tpm_limit) : "Unlimited" }), /* @__PURE__ */ React4.createElement(Stat, { label: "RPM limit", value: team.rpm_limit != null && team.rpm_limit > 0 ? String(team.rpm_limit) : "Unlimited" }), /* @__PURE__ */ React4.createElement(Stat, { label: "Max budget", value: budget > 0 ? `$${budget.toFixed(2)}` : "Unlimited" })), /* @__PURE__ */ React4.createElement(Divider, { sx: { mb: 2 } }), /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Models"), team.models?.length ? /* @__PURE__ */ React4.createElement(Box4, { display: "flex", gap: 0.5, flexWrap: "wrap", mb: 2 }, team.models.map((m) => /* @__PURE__ */ React4.createElement(Chip4, { key: m, label: m, size: "small", variant: "outlined" }))) : /* @__PURE__ */ React4.createElement(Typography4, { variant: "body2", color: "text.secondary", mb: 2 }, "All models allowed"), /* @__PURE__ */ React4.createElement(Divider, { sx: { mb: 2 } }), usageLoading ? /* @__PURE__ */ React4.createElement(Box4, { display: "flex", justifyContent: "center", p: 2 }, /* @__PURE__ */ React4.createElement(CircularProgress2, { size: 24 })) : dailyData.length > 0 ? /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend"), /* @__PURE__ */ React4.createElement(Box4, { height: 160, mb: 2 }, /* @__PURE__ */ React4.createElement(ResponsiveContainer2, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(AreaChart2, { data: dailyData }, /* @__PURE__ */ React4.createElement(CartesianGrid2, { strokeDasharray: "3 3" }), /* @__PURE__ */ React4.createElement(XAxis2, { dataKey: "date", tick: { fontSize: 11 } }), /* @__PURE__ */ React4.createElement(YAxis2, { tick: { fontSize: 11 }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React4.createElement(Tooltip2, { formatter: (v) => [`$${v.toFixed(4)}`, "Spend"] }), /* @__PURE__ */ React4.createElement(Area2, { type: "monotone", dataKey: "spend", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.3 })))), /* @__PURE__ */ React4.createElement(Divider, { sx: { mb: 2 } })) : null, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React4.createElement(Memory, { fontSize: "small", color: "action" }), /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary" }, "Members")), team.members_with_roles?.length ? /* @__PURE__ */ React4.createElement(TableContainer3, null, /* @__PURE__ */ React4.createElement(Table3, { size: "small" }, /* @__PURE__ */ React4.createElement(TableHead3, null, /* @__PURE__ */ React4.createElement(TableRow3, null, /* @__PURE__ */ React4.createElement(TableCell3, null, "User"), /* @__PURE__ */ React4.createElement(TableCell3, null, "Role"))), /* @__PURE__ */ React4.createElement(TableBody3, null, team.members_with_roles.map((m) => /* @__PURE__ */ React4.createElement(TableRow3, { key: m.user_id }, /* @__PURE__ */ React4.createElement(TableCell3, { sx: { fontFamily: "monospace", fontSize: 12 } }, m.user_id), /* @__PURE__ */ React4.createElement(TableCell3, null, /* @__PURE__ */ React4.createElement(
|
|
952
1052
|
Chip4,
|
|
953
1053
|
{
|
|
954
1054
|
label: m.role,
|
|
955
1055
|
size: "small",
|
|
956
1056
|
color: m.role === "admin" ? "primary" : "default"
|
|
957
1057
|
}
|
|
958
|
-
)))))))
|
|
1058
|
+
))))))) : /* @__PURE__ */ React4.createElement(Typography4, { variant: "body2", color: "text.secondary" }, "No members assigned."))));
|
|
959
1059
|
};
|
|
960
1060
|
TeamUsage = ({
|
|
961
1061
|
teams,
|
|
@@ -1312,6 +1412,19 @@ var init_LiteLLMPage = __esm({
|
|
|
1312
1412
|
},
|
|
1313
1413
|
[api, refreshKeys]
|
|
1314
1414
|
);
|
|
1415
|
+
const handlePruneExpiredKeys = useCallback2(
|
|
1416
|
+
async () => {
|
|
1417
|
+
try {
|
|
1418
|
+
const result = await api.pruneExpiredKeys();
|
|
1419
|
+
setSnackbar({ message: `Pruned ${result.pruned} expired key${result.pruned !== 1 ? "s" : ""}`, severity: "success" });
|
|
1420
|
+
refreshKeys();
|
|
1421
|
+
} catch (e) {
|
|
1422
|
+
setSnackbar({ message: `Failed to prune expired keys: ${e.message}`, severity: "error" });
|
|
1423
|
+
}
|
|
1424
|
+
return { pruned: 0 };
|
|
1425
|
+
},
|
|
1426
|
+
[api, refreshKeys]
|
|
1427
|
+
);
|
|
1315
1428
|
const isInitialLoading = userLoading && !userInfo;
|
|
1316
1429
|
if (isInitialLoading) {
|
|
1317
1430
|
return /* @__PURE__ */ React6.createElement(Box6, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "50vh" }, /* @__PURE__ */ React6.createElement(CircularProgress4, null));
|
|
@@ -1355,14 +1468,14 @@ var init_LiteLLMPage = __esm({
|
|
|
1355
1468
|
onBlockKey: handleBlockKey,
|
|
1356
1469
|
onUnblockKey: handleUnblockKey,
|
|
1357
1470
|
onResetKeySpend: handleResetKeySpend,
|
|
1358
|
-
onDeleteKey: handleDeleteKey
|
|
1471
|
+
onDeleteKey: handleDeleteKey,
|
|
1472
|
+
onPruneExpiredKeys: handlePruneExpiredKeys
|
|
1359
1473
|
}
|
|
1360
1474
|
), activeTab === "teams" && /* @__PURE__ */ React6.createElement(
|
|
1361
1475
|
TeamUsage,
|
|
1362
1476
|
{
|
|
1363
1477
|
teams: teams ?? [],
|
|
1364
1478
|
loading: teamsLoading,
|
|
1365
|
-
dateRange,
|
|
1366
1479
|
getTeamUsage: (teamId) => {
|
|
1367
1480
|
if (teamUsageCache[teamId] === void 0) loadTeamUsage(teamId);
|
|
1368
1481
|
return teamUsageCache[teamId] ?? null;
|
|
@@ -1425,6 +1538,7 @@ init_TeamUsage();
|
|
|
1425
1538
|
|
|
1426
1539
|
// src/components/LiteLLMHomeWidget.tsx
|
|
1427
1540
|
init_api();
|
|
1541
|
+
init_format();
|
|
1428
1542
|
import React8, { useState as useState6, useEffect } from "react";
|
|
1429
1543
|
import {
|
|
1430
1544
|
Paper as Paper7,
|
|
@@ -1439,8 +1553,6 @@ import {
|
|
|
1439
1553
|
} from "@mui/material";
|
|
1440
1554
|
import { AreaChart as AreaChart3, Area as Area3, ResponsiveContainer as ResponsiveContainer3 } from "recharts";
|
|
1441
1555
|
import { useApi as useApi2 } from "@backstage/core-plugin-api";
|
|
1442
|
-
var fmtUsd2 = (n) => `$${(n ?? 0).toFixed(n < 1 ? 4 : 2)}`;
|
|
1443
|
-
var fmtInt2 = (n) => (n ?? 0).toLocaleString();
|
|
1444
1556
|
function presetToDateRange(preset) {
|
|
1445
1557
|
const end = /* @__PURE__ */ new Date();
|
|
1446
1558
|
const start = /* @__PURE__ */ new Date();
|
|
@@ -1502,7 +1614,7 @@ var LiteLLMHomeWidget = ({
|
|
|
1502
1614
|
/* @__PURE__ */ React8.createElement(MenuItem4, { value: "today" }, "Today"),
|
|
1503
1615
|
/* @__PURE__ */ React8.createElement(MenuItem4, { value: "7d" }, "7d"),
|
|
1504
1616
|
/* @__PURE__ */ React8.createElement(MenuItem4, { value: "30d" }, "30d")
|
|
1505
|
-
))), 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:
|
|
1617
|
+
))), 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: fmtUsd(usage?.total_spend ?? 0) })), /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "Tokens In", value: fmtInt(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "Tokens Out", value: fmtInt(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "Keys", value: fmtInt(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(
|
|
1506
1618
|
Area3,
|
|
1507
1619
|
{
|
|
1508
1620
|
type: "monotone",
|