@acarmisc/backstage-plugin-litellm 0.15.1 → 0.15.3
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/README.md +38 -1
- package/dist/components/UsageStats.d.ts +3 -1
- package/dist/index.cjs.js +76 -32
- package/dist/index.cjs.js.map +3 -3
- package/dist/index.esm.js +77 -33
- package/dist/index.esm.js.map +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,17 @@ Pair this package with the backend, [`@acarmisc/backstage-plugin-litellm-backend
|
|
|
8
8
|
|
|
9
9
|
Full setup docs, configuration reference, and architecture notes live in the [project README](https://github.com/acarmisc/backstage-plugin-litellm-govai#readme).
|
|
10
10
|
|
|
11
|
+
## Screenshots
|
|
12
|
+
|
|
13
|
+
| View | Preview |
|
|
14
|
+
|------|---------|
|
|
15
|
+
| Home widget — `LiteLLMHomeWidget` |  |
|
|
16
|
+
| Overview / Usage Analytics |  |
|
|
17
|
+
| Keys tab — virtual key inventory |  |
|
|
18
|
+
| Generate New Key dialog |  |
|
|
19
|
+
| Models tab |  |
|
|
20
|
+
| Compact "Create Key" card |  |
|
|
21
|
+
|
|
11
22
|
## Installation
|
|
12
23
|
|
|
13
24
|
This plugin is designed to be used **within a Backstage monorepo**.
|
|
@@ -22,6 +33,8 @@ You'll also need the backend package installed and configured — see its [READM
|
|
|
22
33
|
|
|
23
34
|
### Full page
|
|
24
35
|
|
|
36
|
+
The `LiteLLMPage` component renders the four-tab governance view: **Overview** (usage KPIs + charts), **Keys** (virtual key inventory), **Teams** (team-scoped usage), and **Models** (proxy model catalog).
|
|
37
|
+
|
|
25
38
|
```tsx
|
|
26
39
|
import { litellmPlugin, LiteLLMPage } from '@acarmisc/backstage-plugin-litellm';
|
|
27
40
|
|
|
@@ -29,9 +42,11 @@ import { litellmPlugin, LiteLLMPage } from '@acarmisc/backstage-plugin-litellm';
|
|
|
29
42
|
<Route path="/litellm" element={<LiteLLMPage />} />
|
|
30
43
|
```
|
|
31
44
|
|
|
45
|
+

|
|
46
|
+
|
|
32
47
|
### Home page widget
|
|
33
48
|
|
|
34
|
-
`LiteLLMHomeWidget` is a compact card for the Backstage homepage. It shows the signed-in user's own usage — identity is resolved server-side from the Backstage token, so no `userId` prop is required.
|
|
49
|
+
`LiteLLMHomeWidget` is a compact card for the Backstage homepage. It shows the signed-in user's own usage — identity is resolved server-side from the Backstage token, so no `userId` prop is required. The header includes a `Today` / `7d` / `30d` period selector and a daily-spend sparkline.
|
|
35
50
|
|
|
36
51
|
```tsx
|
|
37
52
|
import { LiteLLMHomeWidget } from '@acarmisc/backstage-plugin-litellm';
|
|
@@ -44,6 +59,28 @@ import { LiteLLMHomeWidget } from '@acarmisc/backstage-plugin-litellm';
|
|
|
44
59
|
| `defaultPeriod` | `'today' \| '7d' \| '30d'` | `'7d'` | Period shown on first render |
|
|
45
60
|
| `title` | `string` | `'LiteLLM Usage'` | Card title override |
|
|
46
61
|
|
|
62
|
+
A slimmer **Create Key** card variant is available for surfaces that only need a one-click shortcut into the key-mint flow:
|
|
63
|
+
|
|
64
|
+

|
|
65
|
+
|
|
66
|
+
### Keys tab
|
|
67
|
+
|
|
68
|
+
The **Keys** tab lists every virtual key you own with its alias, key ID, creation and expiry dates, budget bar (`$spent / $limit`), TPM/RPM, and model scope. Each row exposes inline actions: edit, block/unblock, reset spend, and delete.
|
|
69
|
+
|
|
70
|
+

|
|
71
|
+
|
|
72
|
+
### Generate New Key dialog
|
|
73
|
+
|
|
74
|
+
The dialog walks the user through aliasing the key, picking a duration, binding a team, restricting model access, setting a max budget, and configuring per-key TPM/RPM caps. Failures (e.g. duplicate alias) surface inline with the upstream error code preserved.
|
|
75
|
+
|
|
76
|
+

|
|
77
|
+
|
|
78
|
+
### Models tab
|
|
79
|
+
|
|
80
|
+
The **Models** tab lists every model the LiteLLM proxy exposes, with per-model input/output cost, max input/output token limits, and capabilities. The team selector scopes the list to the models the bound team is allowed to call.
|
|
81
|
+
|
|
82
|
+

|
|
83
|
+
|
|
47
84
|
## Exports
|
|
48
85
|
|
|
49
86
|
- `litellmPlugin` — the frontend plugin instance
|
|
@@ -4,9 +4,11 @@ interface UsageStatsProps {
|
|
|
4
4
|
usage: UsageMetrics | null;
|
|
5
5
|
models: ModelInfo[];
|
|
6
6
|
dateRange: DateRange;
|
|
7
|
-
|
|
7
|
+
currentPreset: DatePreset;
|
|
8
|
+
onDateRangeChange: (range: DateRange, preset?: DatePreset) => void;
|
|
8
9
|
loading: boolean;
|
|
9
10
|
userInfo?: UserInfo;
|
|
10
11
|
}
|
|
12
|
+
type DatePreset = 'today' | '24h' | '7d' | '30d';
|
|
11
13
|
export declare const UsageStats: React.FC<UsageStatsProps>;
|
|
12
14
|
export {};
|
package/dist/index.cjs.js
CHANGED
|
@@ -913,7 +913,25 @@ function formatContextWindow(maxInput, maxOutput) {
|
|
|
913
913
|
if (outPart) return `ctx ${outPart} out`;
|
|
914
914
|
return null;
|
|
915
915
|
}
|
|
916
|
-
|
|
916
|
+
function compareKeys(a, b, sortKey) {
|
|
917
|
+
switch (sortKey) {
|
|
918
|
+
case "alias":
|
|
919
|
+
return (a.key_alias ?? "").localeCompare(b.key_alias ?? "");
|
|
920
|
+
case "created":
|
|
921
|
+
return a.created_at.localeCompare(b.created_at);
|
|
922
|
+
case "expires":
|
|
923
|
+
return (a.expires_at ?? "").localeCompare(b.expires_at ?? "");
|
|
924
|
+
case "budget":
|
|
925
|
+
return (a.max_budget ?? Infinity) - (b.max_budget ?? Infinity);
|
|
926
|
+
case "limits":
|
|
927
|
+
return (a.tpm_limit ?? Infinity) - (b.tpm_limit ?? Infinity) || (a.rpm_limit ?? Infinity) - (b.rpm_limit ?? Infinity);
|
|
928
|
+
case "models":
|
|
929
|
+
return (a.models ?? []).join(",").localeCompare((b.models ?? []).join(","));
|
|
930
|
+
default:
|
|
931
|
+
return 0;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
var import_react3, import_Table, import_TableBody, import_TableCell, import_TableContainer, import_TableHead, import_TableRow, import_TableSortLabel, import_Button2, import_IconButton, import_Box3, import_Typography3, import_Dialog, import_DialogTitle, import_DialogContent, import_DialogActions, import_TextField, import_CircularProgress, import_Autocomplete, import_Skeleton2, import_InputAdornment, import_styles3, import_icons_material2, maskKey, shortKeyId, formatDate, keyToEditForm, KeysTable;
|
|
917
935
|
var init_KeysTable = __esm({
|
|
918
936
|
"src/components/KeysTable.tsx"() {
|
|
919
937
|
"use strict";
|
|
@@ -924,6 +942,7 @@ var init_KeysTable = __esm({
|
|
|
924
942
|
import_TableContainer = __toESM(require("@mui/material/TableContainer"));
|
|
925
943
|
import_TableHead = __toESM(require("@mui/material/TableHead"));
|
|
926
944
|
import_TableRow = __toESM(require("@mui/material/TableRow"));
|
|
945
|
+
import_TableSortLabel = __toESM(require("@mui/material/TableSortLabel"));
|
|
927
946
|
import_Button2 = __toESM(require("@mui/material/Button"));
|
|
928
947
|
import_IconButton = __toESM(require("@mui/material/IconButton"));
|
|
929
948
|
import_Box3 = __toESM(require("@mui/material/Box"));
|
|
@@ -985,13 +1004,29 @@ var init_KeysTable = __esm({
|
|
|
985
1004
|
const [resetSpendConfirm, setResetSpendConfirm] = (0, import_react3.useState)(false);
|
|
986
1005
|
const [resetSpendSubmitting, setResetSpendSubmitting] = (0, import_react3.useState)(false);
|
|
987
1006
|
const [filterText, setFilterText] = (0, import_react3.useState)("");
|
|
1007
|
+
const [sortKey, setSortKey] = (0, import_react3.useState)("created");
|
|
1008
|
+
const [sortDirection, setSortDirection] = (0, import_react3.useState)("desc");
|
|
1009
|
+
const handleSort = (nextSortKey) => {
|
|
1010
|
+
if (sortKey === nextSortKey) {
|
|
1011
|
+
setSortDirection((current) => current === "asc" ? "desc" : "asc");
|
|
1012
|
+
} else {
|
|
1013
|
+
setSortKey(nextSortKey);
|
|
1014
|
+
setSortDirection("asc");
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
988
1017
|
const filteredKeys = (0, import_react3.useMemo)(() => {
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1018
|
+
let result = keys;
|
|
1019
|
+
if (filterText.trim()) {
|
|
1020
|
+
const q = filterText.toLowerCase();
|
|
1021
|
+
result = keys.filter(
|
|
1022
|
+
(k) => (k.key_alias ?? "").toLowerCase().includes(q) || k.models?.some((m) => m.toLowerCase().includes(q))
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
return [...result].sort((a, b) => {
|
|
1026
|
+
const comparison = compareKeys(a, b, sortKey);
|
|
1027
|
+
return sortDirection === "asc" ? comparison : -comparison;
|
|
1028
|
+
});
|
|
1029
|
+
}, [keys, filterText, sortKey, sortDirection]);
|
|
995
1030
|
const editSelectedModels = models.filter((m) => (editForm.models || []).includes(m.model_name));
|
|
996
1031
|
const handleOpenEdit = (k) => {
|
|
997
1032
|
setEditingKey(k);
|
|
@@ -1121,7 +1156,7 @@ var init_KeysTable = __esm({
|
|
|
1121
1156
|
"& td": { color: theme.palette.text.secondary }
|
|
1122
1157
|
}) : void 0
|
|
1123
1158
|
},
|
|
1124
|
-
/* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, /* @__PURE__ */ import_react3.default.createElement(import_Box3.default, { display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }, /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, { variant: "body2"
|
|
1159
|
+
/* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, /* @__PURE__ */ import_react3.default.createElement(import_Box3.default, { display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }, /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, { variant: "body2" }, key.key_alias || "\u2014"), key.blocked && /* @__PURE__ */ import_react3.default.createElement(StatusPill, { label: "Blocked", tone: "danger" }))),
|
|
1125
1160
|
/* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, /* @__PURE__ */ import_react3.default.createElement(import_Box3.default, { display: "flex", alignItems: "center", gap: 0.5 }, /* @__PURE__ */ import_react3.default.createElement(
|
|
1126
1161
|
import_Typography3.default,
|
|
1127
1162
|
{
|
|
@@ -1203,7 +1238,8 @@ var init_KeysTable = __esm({
|
|
|
1203
1238
|
import_TextField.default,
|
|
1204
1239
|
{
|
|
1205
1240
|
size: "small",
|
|
1206
|
-
|
|
1241
|
+
label: "Search alias or model",
|
|
1242
|
+
placeholder: "Search alias or model\u2026",
|
|
1207
1243
|
value: filterText,
|
|
1208
1244
|
onChange: (e) => setFilterText(e.target.value),
|
|
1209
1245
|
sx: { minWidth: 240 },
|
|
@@ -1243,7 +1279,7 @@ var init_KeysTable = __esm({
|
|
|
1243
1279
|
"Generate New Key"
|
|
1244
1280
|
))
|
|
1245
1281
|
},
|
|
1246
|
-
/* @__PURE__ */ import_react3.default.createElement(import_TableContainer.default, null, /* @__PURE__ */ import_react3.default.createElement(import_Table.default, { size: "small", sx: dataTableSx }, /* @__PURE__ */ import_react3.default.createElement(import_TableHead.default, null, /* @__PURE__ */ import_react3.default.createElement(import_TableRow.default, null, /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default,
|
|
1282
|
+
/* @__PURE__ */ import_react3.default.createElement(import_TableContainer.default, null, /* @__PURE__ */ import_react3.default.createElement(import_Table.default, { size: "small", sx: dataTableSx }, /* @__PURE__ */ import_react3.default.createElement(import_TableHead.default, null, /* @__PURE__ */ import_react3.default.createElement(import_TableRow.default, null, /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, { sortDirection: sortKey === "alias" ? sortDirection : false }, /* @__PURE__ */ import_react3.default.createElement(import_TableSortLabel.default, { active: sortKey === "alias", direction: sortKey === "alias" ? sortDirection : "asc", onClick: () => handleSort("alias") }, "Alias")), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, "Key ID"), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, { sortDirection: sortKey === "created" ? sortDirection : false }, /* @__PURE__ */ import_react3.default.createElement(import_TableSortLabel.default, { active: sortKey === "created", direction: sortKey === "created" ? sortDirection : "asc", onClick: () => handleSort("created") }, "Created")), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, { sortDirection: sortKey === "expires" ? sortDirection : false }, /* @__PURE__ */ import_react3.default.createElement(import_TableSortLabel.default, { active: sortKey === "expires", direction: sortKey === "expires" ? sortDirection : "asc", onClick: () => handleSort("expires") }, "Expires")), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, { sortDirection: sortKey === "budget" ? sortDirection : false }, /* @__PURE__ */ import_react3.default.createElement(import_TableSortLabel.default, { active: sortKey === "budget", direction: sortKey === "budget" ? sortDirection : "asc", onClick: () => handleSort("budget") }, "Budget")), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, { sortDirection: sortKey === "limits" ? sortDirection : false }, /* @__PURE__ */ import_react3.default.createElement(import_TableSortLabel.default, { active: sortKey === "limits", direction: sortKey === "limits" ? sortDirection : "asc", onClick: () => handleSort("limits") }, "TPM / RPM")), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, { sortDirection: sortKey === "models" ? sortDirection : false }, /* @__PURE__ */ import_react3.default.createElement(import_TableSortLabel.default, { active: sortKey === "models", direction: sortKey === "models" ? sortDirection : "asc", onClick: () => handleSort("models") }, "Models")), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, { align: "right" }, "Actions"))), /* @__PURE__ */ import_react3.default.createElement(import_TableBody.default, null, renderTableBody())))
|
|
1247
1283
|
), /* @__PURE__ */ import_react3.default.createElement(import_Dialog.default, { open: !!editingKey, onClose: handleCloseEdit, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ import_react3.default.createElement(import_DialogTitle.default, null, "Edit Key"), /* @__PURE__ */ import_react3.default.createElement(import_DialogContent.default, null, editingKey && /* @__PURE__ */ import_react3.default.createElement(import_Box3.default, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, { variant: "body2", color: "text.secondary" }, /* @__PURE__ */ import_react3.default.createElement("code", { style: { fontFamily: "monospace", color: "inherit" } }, maskKey(editingKey.key))), /* @__PURE__ */ import_react3.default.createElement(
|
|
1248
1284
|
import_TextField.default,
|
|
1249
1285
|
{
|
|
@@ -1467,7 +1503,7 @@ function formatContextWindow2(maxInput, maxOutput) {
|
|
|
1467
1503
|
if (outPart) return `ctx ${outPart} out`;
|
|
1468
1504
|
return null;
|
|
1469
1505
|
}
|
|
1470
|
-
var import_react4, import_Box4, import_Typography4, import_Dialog2, import_DialogTitle2, import_DialogContent2, import_DialogActions2, import_TextField2, import_MenuItem, import_Button3, import_IconButton2, import_CircularProgress2, import_Autocomplete2, import_Checkbox, import_FormControlLabel, import_Alert, import_Tabs, import_Tab, import_icons_material3, generateDefaultAlias, emptyForm, ALL_PROXY_MODELS, SNIPPET_FILE_HINTS, SnippetTabs, GenerateKeyDialog;
|
|
1506
|
+
var import_react4, import_Box4, import_Typography4, import_Dialog2, import_DialogTitle2, import_DialogContent2, import_DialogActions2, import_TextField2, import_MenuItem, import_Button3, import_IconButton2, import_CircularProgress2, import_Autocomplete2, import_Checkbox, import_FormControlLabel, import_Alert, import_Accordion, import_AccordionSummary, import_AccordionDetails, import_Tabs, import_Tab, import_icons_material3, generateDefaultAlias, emptyForm, ALL_PROXY_MODELS, SNIPPET_FILE_HINTS, SnippetTabs, GenerateKeyDialog;
|
|
1471
1507
|
var init_GenerateKeyDialog = __esm({
|
|
1472
1508
|
"src/components/GenerateKeyDialog.tsx"() {
|
|
1473
1509
|
"use strict";
|
|
@@ -1487,6 +1523,9 @@ var init_GenerateKeyDialog = __esm({
|
|
|
1487
1523
|
import_Checkbox = __toESM(require("@mui/material/Checkbox"));
|
|
1488
1524
|
import_FormControlLabel = __toESM(require("@mui/material/FormControlLabel"));
|
|
1489
1525
|
import_Alert = __toESM(require("@mui/material/Alert"));
|
|
1526
|
+
import_Accordion = __toESM(require("@mui/material/Accordion"));
|
|
1527
|
+
import_AccordionSummary = __toESM(require("@mui/material/AccordionSummary"));
|
|
1528
|
+
import_AccordionDetails = __toESM(require("@mui/material/AccordionDetails"));
|
|
1490
1529
|
import_Tabs = __toESM(require("@mui/material/Tabs"));
|
|
1491
1530
|
import_Tab = __toESM(require("@mui/material/Tab"));
|
|
1492
1531
|
import_icons_material3 = require("@mui/icons-material");
|
|
@@ -1806,7 +1845,7 @@ var init_GenerateKeyDialog = __esm({
|
|
|
1806
1845
|
required: true,
|
|
1807
1846
|
fullWidth: true
|
|
1808
1847
|
}
|
|
1809
|
-
), /* @__PURE__ */ import_react4.default.createElement(
|
|
1848
|
+
), /* @__PURE__ */ import_react4.default.createElement(import_Accordion.default, { disableGutters: true, elevation: 0, sx: { border: "1px solid", borderColor: "divider", "&:before": { display: "none" } } }, /* @__PURE__ */ import_react4.default.createElement(import_AccordionSummary.default, { expandIcon: /* @__PURE__ */ import_react4.default.createElement(import_icons_material3.ExpandMore, null) }, /* @__PURE__ */ import_react4.default.createElement(import_Typography4.default, { variant: "body2", fontWeight: 600 }, "Advanced")), /* @__PURE__ */ import_react4.default.createElement(import_AccordionDetails.default, { sx: { display: "flex", flexDirection: "column", gap: 2 } }, /* @__PURE__ */ import_react4.default.createElement(
|
|
1810
1849
|
import_TextField2.default,
|
|
1811
1850
|
{
|
|
1812
1851
|
label: "TPM Limit",
|
|
@@ -1826,7 +1865,7 @@ var init_GenerateKeyDialog = __esm({
|
|
|
1826
1865
|
helperText: "Max requests per minute this key can make across all models. Leave blank for no limit.",
|
|
1827
1866
|
fullWidth: true
|
|
1828
1867
|
}
|
|
1829
|
-
))), /* @__PURE__ */ import_react4.default.createElement(import_DialogActions2.default, null, newKeyValue ? /* @__PURE__ */ import_react4.default.createElement(import_Button3.default, { onClick: handleClose, variant: "contained", color: "success" }, "Done") : /* @__PURE__ */ import_react4.default.createElement(import_react4.default.Fragment, null, /* @__PURE__ */ import_react4.default.createElement(import_Button3.default, { onClick: handleClose }, "Cancel"), /* @__PURE__ */ import_react4.default.createElement(
|
|
1868
|
+
))))), /* @__PURE__ */ import_react4.default.createElement(import_DialogActions2.default, null, newKeyValue ? /* @__PURE__ */ import_react4.default.createElement(import_Button3.default, { onClick: handleClose, variant: "contained", color: "success" }, "Done") : /* @__PURE__ */ import_react4.default.createElement(import_react4.default.Fragment, null, /* @__PURE__ */ import_react4.default.createElement(import_Button3.default, { onClick: handleClose }, "Cancel"), /* @__PURE__ */ import_react4.default.createElement(
|
|
1830
1869
|
import_Button3.default,
|
|
1831
1870
|
{
|
|
1832
1871
|
onClick: handleGenerate,
|
|
@@ -1898,6 +1937,7 @@ var init_UsageStats = __esm({
|
|
|
1898
1937
|
usage,
|
|
1899
1938
|
models,
|
|
1900
1939
|
dateRange,
|
|
1940
|
+
currentPreset,
|
|
1901
1941
|
onDateRangeChange,
|
|
1902
1942
|
loading,
|
|
1903
1943
|
userInfo
|
|
@@ -1905,15 +1945,6 @@ var init_UsageStats = __esm({
|
|
|
1905
1945
|
const chart = useChartTheme();
|
|
1906
1946
|
const [selectedModel, setSelectedModel] = (0, import_react5.useState)("all");
|
|
1907
1947
|
const [tab, setTab] = (0, import_react5.useState)("costs");
|
|
1908
|
-
const selectedPreset = (0, import_react5.useMemo)(() => {
|
|
1909
|
-
if (dateRange.start.toDateString() === dateRange.end.toDateString()) return "today";
|
|
1910
|
-
const diffMs = dateRange.end.getTime() - dateRange.start.getTime();
|
|
1911
|
-
const diffHours = diffMs / (1e3 * 60 * 60);
|
|
1912
|
-
if (diffHours <= 26) return "24h";
|
|
1913
|
-
const diffDays = Math.ceil(diffMs / (1e3 * 60 * 60 * 24));
|
|
1914
|
-
if (diffDays <= 7) return "7d";
|
|
1915
|
-
return "30d";
|
|
1916
|
-
}, [dateRange]);
|
|
1917
1948
|
const handlePresetChange = (preset) => {
|
|
1918
1949
|
const end = /* @__PURE__ */ new Date();
|
|
1919
1950
|
const start = /* @__PURE__ */ new Date();
|
|
@@ -1925,7 +1956,7 @@ var init_UsageStats = __esm({
|
|
|
1925
1956
|
localStorage.setItem(PERIOD_LS_KEY, preset);
|
|
1926
1957
|
} catch {
|
|
1927
1958
|
}
|
|
1928
|
-
onDateRangeChange({ start, end });
|
|
1959
|
+
onDateRangeChange({ start, end }, preset);
|
|
1929
1960
|
};
|
|
1930
1961
|
const dailyData = (0, import_react5.useMemo)(
|
|
1931
1962
|
() => (usage?.daily_usage ?? []).map((d) => ({
|
|
@@ -2064,7 +2095,7 @@ var init_UsageStats = __esm({
|
|
|
2064
2095
|
select: true,
|
|
2065
2096
|
size: "small",
|
|
2066
2097
|
label: "Period",
|
|
2067
|
-
value:
|
|
2098
|
+
value: currentPreset,
|
|
2068
2099
|
onChange: (e) => handlePresetChange(e.target.value),
|
|
2069
2100
|
sx: { minWidth: 160 }
|
|
2070
2101
|
},
|
|
@@ -2074,7 +2105,7 @@ var init_UsageStats = __esm({
|
|
|
2074
2105
|
SectionCard,
|
|
2075
2106
|
{
|
|
2076
2107
|
title: "Usage Analytics",
|
|
2077
|
-
subtitle: `${PRESET_LABELS[
|
|
2108
|
+
subtitle: `${PRESET_LABELS[currentPreset]} \xB7 ${dateRange.start.toLocaleDateString()} \u2013 ${dateRange.end.toLocaleDateString()}`,
|
|
2078
2109
|
actions: periodSelect
|
|
2079
2110
|
},
|
|
2080
2111
|
/* @__PURE__ */ import_react5.default.createElement(MetricStrip, { metrics }),
|
|
@@ -2423,6 +2454,10 @@ var init_TeamUsage = __esm({
|
|
|
2423
2454
|
});
|
|
2424
2455
|
|
|
2425
2456
|
// src/components/ModelsTable.tsx
|
|
2457
|
+
function isModelAllowedByTeam2(model, teamModels) {
|
|
2458
|
+
if (!teamModels || teamModels.length === 0 || teamModels.includes(ALL_PROXY_MODELS2)) return true;
|
|
2459
|
+
return teamModels.includes(model.model_name) || !!model.access_groups?.some((group) => teamModels.includes(group));
|
|
2460
|
+
}
|
|
2426
2461
|
function fmtCostPerToken(cost) {
|
|
2427
2462
|
if (cost === void 0 || cost === null) return "\u2014";
|
|
2428
2463
|
if (cost === 0) return "$0";
|
|
@@ -2436,7 +2471,7 @@ function fmtTokens(n) {
|
|
|
2436
2471
|
if (n >= 1e3) return `${(n / 1e3).toFixed(0)}K`;
|
|
2437
2472
|
return String(n);
|
|
2438
2473
|
}
|
|
2439
|
-
var import_react7, import_Box7, import_Table4, import_TableBody4, import_TableCell4, import_TableContainer4, import_TableHead4, import_TableRow4, import_Typography7, import_MenuItem3, import_Select, import_FormControl, import_InputLabel, import_ContentCopy, import_Tooltip, import_Snackbar, import_Alert2, import_styles5, ModelsTable;
|
|
2474
|
+
var import_react7, import_Box7, import_Table4, import_TableBody4, import_TableCell4, import_TableContainer4, import_TableHead4, import_TableRow4, import_Typography7, import_MenuItem3, import_Select, import_FormControl, import_InputLabel, import_ContentCopy, import_Tooltip, import_Snackbar, import_Alert2, import_styles5, ALL_PROXY_MODELS2, ModelsTable;
|
|
2440
2475
|
var init_ModelsTable = __esm({
|
|
2441
2476
|
"src/components/ModelsTable.tsx"() {
|
|
2442
2477
|
"use strict";
|
|
@@ -2459,6 +2494,7 @@ var init_ModelsTable = __esm({
|
|
|
2459
2494
|
import_Alert2 = __toESM(require("@mui/material/Alert"));
|
|
2460
2495
|
init_ui();
|
|
2461
2496
|
import_styles5 = require("@mui/material/styles");
|
|
2497
|
+
ALL_PROXY_MODELS2 = "all-proxy-models";
|
|
2462
2498
|
ModelsTable = ({ allModels, teams, loading }) => {
|
|
2463
2499
|
const theme = (0, import_styles5.useTheme)();
|
|
2464
2500
|
const [selectedTeamId, setSelectedTeamId] = (0, import_react7.useState)("");
|
|
@@ -2469,10 +2505,7 @@ var init_ModelsTable = __esm({
|
|
|
2469
2505
|
);
|
|
2470
2506
|
const filteredModels = (0, import_react7.useMemo)(() => {
|
|
2471
2507
|
if (!selectedTeam) return [];
|
|
2472
|
-
|
|
2473
|
-
if (teamModels.length === 0) return allModels;
|
|
2474
|
-
const allowed = new Set(teamModels);
|
|
2475
|
-
return allModels.filter((m) => allowed.has(m.model_name));
|
|
2508
|
+
return allModels.filter((model) => isModelAllowedByTeam2(model, selectedTeam.models));
|
|
2476
2509
|
}, [allModels, selectedTeam]);
|
|
2477
2510
|
const handleCopyModelId = (0, import_react7.useCallback)((modelId) => {
|
|
2478
2511
|
navigator.clipboard.writeText(modelId).then(() => setCopiedSnackbar(true));
|
|
@@ -2839,6 +2872,13 @@ var init_LiteLLMPage = __esm({
|
|
|
2839
2872
|
LiteLLMPage = () => {
|
|
2840
2873
|
const api = (0, import_core_plugin_api2.useApi)(liteLlmApiRef);
|
|
2841
2874
|
const [dateRange, setDateRange] = (0, import_react9.useState)(initDateRange);
|
|
2875
|
+
const [currentPreset, setCurrentPreset] = (0, import_react9.useState)(() => {
|
|
2876
|
+
try {
|
|
2877
|
+
return localStorage.getItem(PERIOD_LS_KEY2) ?? "7d";
|
|
2878
|
+
} catch {
|
|
2879
|
+
return "7d";
|
|
2880
|
+
}
|
|
2881
|
+
});
|
|
2842
2882
|
const [activeTab, setActiveTab] = (0, import_react9.useState)("overview");
|
|
2843
2883
|
const [snackbar, setSnackbar] = (0, import_react9.useState)(null);
|
|
2844
2884
|
const [generateDialogOpen, setGenerateDialogOpen] = (0, import_react9.useState)(false);
|
|
@@ -2885,11 +2925,14 @@ var init_LiteLLMPage = __esm({
|
|
|
2885
2925
|
const endDate = dateRange.end.toISOString().split("T")[0];
|
|
2886
2926
|
return api.getUsage(startDate, endDate);
|
|
2887
2927
|
}, [api, dateRange]);
|
|
2888
|
-
const handleDateRangeChange = (0, import_react9.useCallback)((range) => {
|
|
2928
|
+
const handleDateRangeChange = (0, import_react9.useCallback)((range, preset) => {
|
|
2889
2929
|
setDateRange(range);
|
|
2930
|
+
if (preset) {
|
|
2931
|
+
setCurrentPreset(preset);
|
|
2932
|
+
}
|
|
2890
2933
|
setTeamUsageCache({});
|
|
2891
2934
|
setTeamUsageLoading({});
|
|
2892
|
-
}, [setDateRange]);
|
|
2935
|
+
}, [setDateRange, setCurrentPreset]);
|
|
2893
2936
|
const loadTeamUsage = (0, import_react9.useCallback)(async (teamId) => {
|
|
2894
2937
|
if (teamUsageCache[teamId] !== void 0 || teamUsageLoading[teamId]) return;
|
|
2895
2938
|
setTeamUsageLoading((prev) => ({ ...prev, [teamId]: true }));
|
|
@@ -3054,6 +3097,7 @@ var init_LiteLLMPage = __esm({
|
|
|
3054
3097
|
usage: usage ?? null,
|
|
3055
3098
|
models: allModels ?? [],
|
|
3056
3099
|
dateRange,
|
|
3100
|
+
currentPreset,
|
|
3057
3101
|
onDateRangeChange: handleDateRangeChange,
|
|
3058
3102
|
loading: usageLoading,
|
|
3059
3103
|
userInfo
|