@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/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[]>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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 {};
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { TeamInfo, UsageMetrics
|
|
2
|
+
import { TeamInfo, UsageMetrics } from '../types';
|
|
3
3
|
interface TeamUsageProps {
|
|
4
4
|
teams: TeamInfo[];
|
|
5
5
|
loading: boolean;
|
|
6
|
-
dateRange: DateRange;
|
|
7
6
|
getTeamUsage: (teamId: string) => UsageMetrics | null;
|
|
8
7
|
getTeamUsageLoading: (teamId: string) => boolean;
|
|
9
8
|
}
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
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,24 +693,44 @@ 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
|
}
|
|
656
708
|
});
|
|
657
709
|
|
|
710
|
+
// src/format.ts
|
|
711
|
+
var fmtUsd, fmtInt;
|
|
712
|
+
var init_format = __esm({
|
|
713
|
+
"src/format.ts"() {
|
|
714
|
+
"use strict";
|
|
715
|
+
fmtUsd = (n) => `$${(n ?? 0).toFixed(n < 1 ? 4 : 2)}`;
|
|
716
|
+
fmtInt = (n) => (n ?? 0).toLocaleString();
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
|
|
658
720
|
// src/components/UsageStats.tsx
|
|
659
721
|
function modelColor(model) {
|
|
660
722
|
let h = 5381;
|
|
661
723
|
for (let i = 0; i < model.length; i++) h = (h << 5) + h + model.charCodeAt(i) >>> 0;
|
|
662
724
|
return MODEL_COLORS[h % MODEL_COLORS.length];
|
|
663
725
|
}
|
|
664
|
-
var import_react3, import_material3, import_recharts, TOP_N_MODELS, PERIOD_LS_KEY, MODEL_COLORS,
|
|
726
|
+
var import_react3, import_material3, import_recharts, TOP_N_MODELS, PERIOD_LS_KEY, MODEL_COLORS, fmtPct, KpiCard, ChartSkeleton, EmptyChart, UsageStats;
|
|
665
727
|
var init_UsageStats = __esm({
|
|
666
728
|
"src/components/UsageStats.tsx"() {
|
|
667
729
|
"use strict";
|
|
668
730
|
import_react3 = __toESM(require("react"));
|
|
669
731
|
import_material3 = require("@mui/material");
|
|
670
732
|
import_recharts = require("recharts");
|
|
733
|
+
init_format();
|
|
671
734
|
TOP_N_MODELS = 6;
|
|
672
735
|
PERIOD_LS_KEY = "litellm_usage_period";
|
|
673
736
|
MODEL_COLORS = [
|
|
@@ -682,8 +745,6 @@ var init_UsageStats = __esm({
|
|
|
682
745
|
"#a4de6c",
|
|
683
746
|
"#d0ed57"
|
|
684
747
|
];
|
|
685
|
-
fmtUsd = (n) => `$${(n ?? 0).toFixed(n < 1 ? 4 : 2)}`;
|
|
686
|
-
fmtInt = (n) => (n ?? 0).toLocaleString();
|
|
687
748
|
fmtPct = (n) => `${(n * 100).toFixed(1)}%`;
|
|
688
749
|
KpiCard = ({ label, value, hint }) => /* @__PURE__ */ import_react3.default.createElement(import_material3.Paper, { variant: "outlined", sx: { p: 2, height: "100%" } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "caption", color: "text.secondary" }, label), /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "h5", sx: { mt: 0.5 } }, value), hint ? /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "caption", color: "text.secondary" }, hint) : null);
|
|
689
750
|
ChartSkeleton = ({ height = 260 }) => /* @__PURE__ */ import_react3.default.createElement(import_material3.Skeleton, { variant: "rectangular", height, sx: { borderRadius: 1 } });
|
|
@@ -866,7 +927,7 @@ var init_UsageStats = __esm({
|
|
|
866
927
|
});
|
|
867
928
|
|
|
868
929
|
// src/components/TeamUsage.tsx
|
|
869
|
-
var import_react4, import_material4, import_icons_material2, import_recharts2, TeamCard, TeamUsage;
|
|
930
|
+
var import_react4, import_material4, import_icons_material2, import_recharts2, Stat, TeamCard, TeamUsage;
|
|
870
931
|
var init_TeamUsage = __esm({
|
|
871
932
|
"src/components/TeamUsage.tsx"() {
|
|
872
933
|
"use strict";
|
|
@@ -874,6 +935,7 @@ var init_TeamUsage = __esm({
|
|
|
874
935
|
import_material4 = require("@mui/material");
|
|
875
936
|
import_icons_material2 = require("@mui/icons-material");
|
|
876
937
|
import_recharts2 = require("recharts");
|
|
938
|
+
Stat = ({ label, value }) => /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "body2", fontWeight: 600, sx: { fontFamily: "monospace" } }, value));
|
|
877
939
|
TeamCard = ({ team, usage, usageLoading }) => {
|
|
878
940
|
const [expanded, setExpanded] = (0, import_react4.useState)(false);
|
|
879
941
|
const budget = team.max_budget ?? 0;
|
|
@@ -882,7 +944,43 @@ var init_TeamUsage = __esm({
|
|
|
882
944
|
const isOver = budget > 0 && spend >= budget;
|
|
883
945
|
const isNear = budget > 0 && spend >= budget * 0.8 && !isOver;
|
|
884
946
|
const dailyData = usage?.daily_usage?.map((d) => ({ date: d.date, spend: d.spend })) ?? [];
|
|
885
|
-
return /* @__PURE__ */ import_react4.default.createElement(import_material4.Paper, { variant: "outlined", sx: { p: 2, mb: 2 } }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_icons_material2.Group, { color: "action" }), /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { flexGrow: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle1", fontWeight: 600 }, team.team_alias
|
|
947
|
+
return /* @__PURE__ */ import_react4.default.createElement(import_material4.Paper, { variant: "outlined", sx: { p: 2, mb: 2 } }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_icons_material2.Group, { color: "action" }), /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { flexGrow: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle1", fontWeight: 600 }, team.team_alias || "Untitled team"), /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "caption", color: "text.secondary", sx: { fontFamily: "monospace" } }, team.team_id)), /* @__PURE__ */ import_react4.default.createElement(import_material4.IconButton, { size: "small", onClick: () => setExpanded((e) => !e), "aria-label": "toggle details" }, expanded ? /* @__PURE__ */ import_react4.default.createElement(import_icons_material2.ExpandLess, null) : /* @__PURE__ */ import_react4.default.createElement(import_icons_material2.ExpandMore, null))), /* @__PURE__ */ import_react4.default.createElement(import_material4.Stack, { direction: "row", spacing: 3, mt: 1.5, flexWrap: "wrap", useFlexGap: true, gap: 1.5 }, /* @__PURE__ */ import_react4.default.createElement(
|
|
948
|
+
Stat,
|
|
949
|
+
{
|
|
950
|
+
label: "Members",
|
|
951
|
+
value: team.members_with_roles?.length ? String(team.members_with_roles.length) : "\u2014"
|
|
952
|
+
}
|
|
953
|
+
), /* @__PURE__ */ import_react4.default.createElement(
|
|
954
|
+
Stat,
|
|
955
|
+
{
|
|
956
|
+
label: "Models",
|
|
957
|
+
value: team.models?.length ? String(team.models.length) : "All"
|
|
958
|
+
}
|
|
959
|
+
), /* @__PURE__ */ import_react4.default.createElement(
|
|
960
|
+
Stat,
|
|
961
|
+
{
|
|
962
|
+
label: "Budget",
|
|
963
|
+
value: budget > 0 ? `$${budget.toFixed(2)}` : "Unlimited"
|
|
964
|
+
}
|
|
965
|
+
), /* @__PURE__ */ import_react4.default.createElement(
|
|
966
|
+
Stat,
|
|
967
|
+
{
|
|
968
|
+
label: "Spend",
|
|
969
|
+
value: `$${spend.toFixed(2)}`
|
|
970
|
+
}
|
|
971
|
+
), /* @__PURE__ */ import_react4.default.createElement(
|
|
972
|
+
Stat,
|
|
973
|
+
{
|
|
974
|
+
label: "TPM",
|
|
975
|
+
value: team.tpm_limit != null && team.tpm_limit > 0 ? String(team.tpm_limit) : "\u2014"
|
|
976
|
+
}
|
|
977
|
+
), /* @__PURE__ */ import_react4.default.createElement(
|
|
978
|
+
Stat,
|
|
979
|
+
{
|
|
980
|
+
label: "RPM",
|
|
981
|
+
value: team.rpm_limit != null && team.rpm_limit > 0 ? String(team.rpm_limit) : "\u2014"
|
|
982
|
+
}
|
|
983
|
+
)), budget > 0 && /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { mt: 1.5 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", justifyContent: "space-between", mb: 0.5 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "body2" }, "$", spend.toFixed(2), " / $", budget.toFixed(2)), isOver && /* @__PURE__ */ import_react4.default.createElement(import_material4.Chip, { label: "Over Budget", size: "small", color: "error" }), isNear && /* @__PURE__ */ import_react4.default.createElement(import_material4.Chip, { label: "Near Limit", size: "small", color: "warning" })), /* @__PURE__ */ import_react4.default.createElement(
|
|
886
984
|
import_material4.LinearProgress,
|
|
887
985
|
{
|
|
888
986
|
variant: "determinate",
|
|
@@ -890,14 +988,14 @@ var init_TeamUsage = __esm({
|
|
|
890
988
|
color: isOver ? "error" : isNear ? "warning" : "primary",
|
|
891
989
|
sx: { height: 6, borderRadius: 1 }
|
|
892
990
|
}
|
|
893
|
-
)), /* @__PURE__ */ import_react4.default.createElement(import_material4.Collapse, { in: expanded }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { mt: 2 }, usageLoading ? /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", justifyContent: "center", p: 2 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.CircularProgress, { size: 24 })) : dailyData.length > 0 ? /* @__PURE__ */ import_react4.default.createElement(import_react4.default.Fragment, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend"), /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { height: 160 }, /* @__PURE__ */ import_react4.default.createElement(import_recharts2.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react4.default.createElement(import_recharts2.AreaChart, { data: dailyData }, /* @__PURE__ */ import_react4.default.createElement(import_recharts2.CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.XAxis, { dataKey: "date", tick: { fontSize: 11 } }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.YAxis, { tick: { fontSize: 11 }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.Tooltip, { formatter: (v) => [`$${v.toFixed(4)}`, "Spend"] }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.Area, { type: "monotone", dataKey: "spend", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.3 }))))) : null,
|
|
991
|
+
)), /* @__PURE__ */ import_react4.default.createElement(import_material4.Collapse, { in: expanded }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { mt: 2 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_icons_material2.Speed, { fontSize: "small", color: "action" }), /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary" }, "Rate limits")), /* @__PURE__ */ import_react4.default.createElement(import_material4.Stack, { direction: "row", spacing: 3, flexWrap: "wrap", useFlexGap: true, gap: 1.5, mb: 2 }, /* @__PURE__ */ import_react4.default.createElement(Stat, { label: "TPM limit", value: team.tpm_limit != null && team.tpm_limit > 0 ? String(team.tpm_limit) : "Unlimited" }), /* @__PURE__ */ import_react4.default.createElement(Stat, { label: "RPM limit", value: team.rpm_limit != null && team.rpm_limit > 0 ? String(team.rpm_limit) : "Unlimited" }), /* @__PURE__ */ import_react4.default.createElement(Stat, { label: "Max budget", value: budget > 0 ? `$${budget.toFixed(2)}` : "Unlimited" })), /* @__PURE__ */ import_react4.default.createElement(import_material4.Divider, { sx: { mb: 2 } }), /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Models"), team.models?.length ? /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", gap: 0.5, flexWrap: "wrap", mb: 2 }, team.models.map((m) => /* @__PURE__ */ import_react4.default.createElement(import_material4.Chip, { key: m, label: m, size: "small", variant: "outlined" }))) : /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "body2", color: "text.secondary", mb: 2 }, "All models allowed"), /* @__PURE__ */ import_react4.default.createElement(import_material4.Divider, { sx: { mb: 2 } }), usageLoading ? /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", justifyContent: "center", p: 2 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.CircularProgress, { size: 24 })) : dailyData.length > 0 ? /* @__PURE__ */ import_react4.default.createElement(import_react4.default.Fragment, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend"), /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { height: 160, mb: 2 }, /* @__PURE__ */ import_react4.default.createElement(import_recharts2.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react4.default.createElement(import_recharts2.AreaChart, { data: dailyData }, /* @__PURE__ */ import_react4.default.createElement(import_recharts2.CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.XAxis, { dataKey: "date", tick: { fontSize: 11 } }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.YAxis, { tick: { fontSize: 11 }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.Tooltip, { formatter: (v) => [`$${v.toFixed(4)}`, "Spend"] }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.Area, { type: "monotone", dataKey: "spend", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.3 })))), /* @__PURE__ */ import_react4.default.createElement(import_material4.Divider, { sx: { mb: 2 } })) : null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_icons_material2.Memory, { fontSize: "small", color: "action" }), /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary" }, "Members")), team.members_with_roles?.length ? /* @__PURE__ */ import_react4.default.createElement(import_material4.TableContainer, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Table, { size: "small" }, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableHead, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableRow, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, "User"), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, "Role"))), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableBody, null, team.members_with_roles.map((m) => /* @__PURE__ */ import_react4.default.createElement(import_material4.TableRow, { key: m.user_id }, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { sx: { fontFamily: "monospace", fontSize: 12 } }, m.user_id), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, /* @__PURE__ */ import_react4.default.createElement(
|
|
894
992
|
import_material4.Chip,
|
|
895
993
|
{
|
|
896
994
|
label: m.role,
|
|
897
995
|
size: "small",
|
|
898
996
|
color: m.role === "admin" ? "primary" : "default"
|
|
899
997
|
}
|
|
900
|
-
)))))))
|
|
998
|
+
))))))) : /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "body2", color: "text.secondary" }, "No members assigned."))));
|
|
901
999
|
};
|
|
902
1000
|
TeamUsage = ({
|
|
903
1001
|
teams,
|
|
@@ -1236,6 +1334,19 @@ var init_LiteLLMPage = __esm({
|
|
|
1236
1334
|
},
|
|
1237
1335
|
[api, refreshKeys]
|
|
1238
1336
|
);
|
|
1337
|
+
const handlePruneExpiredKeys = (0, import_react6.useCallback)(
|
|
1338
|
+
async () => {
|
|
1339
|
+
try {
|
|
1340
|
+
const result = await api.pruneExpiredKeys();
|
|
1341
|
+
setSnackbar({ message: `Pruned ${result.pruned} expired key${result.pruned !== 1 ? "s" : ""}`, severity: "success" });
|
|
1342
|
+
refreshKeys();
|
|
1343
|
+
} catch (e) {
|
|
1344
|
+
setSnackbar({ message: `Failed to prune expired keys: ${e.message}`, severity: "error" });
|
|
1345
|
+
}
|
|
1346
|
+
return { pruned: 0 };
|
|
1347
|
+
},
|
|
1348
|
+
[api, refreshKeys]
|
|
1349
|
+
);
|
|
1239
1350
|
const isInitialLoading = userLoading && !userInfo;
|
|
1240
1351
|
if (isInitialLoading) {
|
|
1241
1352
|
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,14 +1390,14 @@ var init_LiteLLMPage = __esm({
|
|
|
1279
1390
|
onBlockKey: handleBlockKey,
|
|
1280
1391
|
onUnblockKey: handleUnblockKey,
|
|
1281
1392
|
onResetKeySpend: handleResetKeySpend,
|
|
1282
|
-
onDeleteKey: handleDeleteKey
|
|
1393
|
+
onDeleteKey: handleDeleteKey,
|
|
1394
|
+
onPruneExpiredKeys: handlePruneExpiredKeys
|
|
1283
1395
|
}
|
|
1284
1396
|
), activeTab === "teams" && /* @__PURE__ */ import_react6.default.createElement(
|
|
1285
1397
|
TeamUsage,
|
|
1286
1398
|
{
|
|
1287
1399
|
teams: teams ?? [],
|
|
1288
1400
|
loading: teamsLoading,
|
|
1289
|
-
dateRange,
|
|
1290
1401
|
getTeamUsage: (teamId) => {
|
|
1291
1402
|
if (teamUsageCache[teamId] === void 0) loadTeamUsage(teamId);
|
|
1292
1403
|
return teamUsageCache[teamId] ?? null;
|
|
@@ -1363,8 +1474,7 @@ var import_material7 = require("@mui/material");
|
|
|
1363
1474
|
var import_recharts3 = require("recharts");
|
|
1364
1475
|
var import_core_plugin_api3 = require("@backstage/core-plugin-api");
|
|
1365
1476
|
init_api();
|
|
1366
|
-
|
|
1367
|
-
var fmtInt2 = (n) => (n ?? 0).toLocaleString();
|
|
1477
|
+
init_format();
|
|
1368
1478
|
function presetToDateRange(preset) {
|
|
1369
1479
|
const end = /* @__PURE__ */ new Date();
|
|
1370
1480
|
const start = /* @__PURE__ */ new Date();
|
|
@@ -1426,7 +1536,7 @@ var LiteLLMHomeWidget = ({
|
|
|
1426
1536
|
/* @__PURE__ */ import_react8.default.createElement(import_material7.MenuItem, { value: "today" }, "Today"),
|
|
1427
1537
|
/* @__PURE__ */ import_react8.default.createElement(import_material7.MenuItem, { value: "7d" }, "7d"),
|
|
1428
1538
|
/* @__PURE__ */ import_react8.default.createElement(import_material7.MenuItem, { value: "30d" }, "30d")
|
|
1429
|
-
))), loading && /* @__PURE__ */ import_react8.default.createElement(import_material7.Box, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ import_react8.default.createElement(import_material7.CircularProgress, { size: 32 })), !loading && error && /* @__PURE__ */ import_react8.default.createElement(import_material7.Alert, { severity: "error", sx: { mt: 1 } }, error), !loading && !error && /* @__PURE__ */ import_react8.default.createElement(import_react8.default.Fragment, null, /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "USD Spent", value:
|
|
1539
|
+
))), loading && /* @__PURE__ */ import_react8.default.createElement(import_material7.Box, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ import_react8.default.createElement(import_material7.CircularProgress, { size: 32 })), !loading && error && /* @__PURE__ */ import_react8.default.createElement(import_material7.Alert, { severity: "error", sx: { mt: 1 } }, error), !loading && !error && /* @__PURE__ */ import_react8.default.createElement(import_react8.default.Fragment, null, /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "USD Spent", value: fmtUsd(usage?.total_spend ?? 0) })), /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "Tokens In", value: fmtInt(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "Tokens Out", value: fmtInt(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "Keys", value: fmtInt(keys.length) }))), hasSparkline && /* @__PURE__ */ import_react8.default.createElement(import_material7.Box, { height: 120 }, /* @__PURE__ */ import_react8.default.createElement(import_recharts3.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react8.default.createElement(import_recharts3.AreaChart, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 } }, /* @__PURE__ */ import_react8.default.createElement(
|
|
1430
1540
|
import_recharts3.Area,
|
|
1431
1541
|
{
|
|
1432
1542
|
type: "monotone",
|