@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/dist/index.esm.js CHANGED
@@ -834,6 +834,7 @@ import TableCell from "@mui/material/TableCell";
834
834
  import TableContainer from "@mui/material/TableContainer";
835
835
  import TableHead from "@mui/material/TableHead";
836
836
  import TableRow from "@mui/material/TableRow";
837
+ import TableSortLabel from "@mui/material/TableSortLabel";
837
838
  import Button2 from "@mui/material/Button";
838
839
  import IconButton from "@mui/material/IconButton";
839
840
  import Box3 from "@mui/material/Box";
@@ -913,6 +914,24 @@ function formatContextWindow(maxInput, maxOutput) {
913
914
  if (outPart) return `ctx ${outPart} out`;
914
915
  return null;
915
916
  }
917
+ function compareKeys(a, b, sortKey) {
918
+ switch (sortKey) {
919
+ case "alias":
920
+ return (a.key_alias ?? "").localeCompare(b.key_alias ?? "");
921
+ case "created":
922
+ return a.created_at.localeCompare(b.created_at);
923
+ case "expires":
924
+ return (a.expires_at ?? "").localeCompare(b.expires_at ?? "");
925
+ case "budget":
926
+ return (a.max_budget ?? Infinity) - (b.max_budget ?? Infinity);
927
+ case "limits":
928
+ return (a.tpm_limit ?? Infinity) - (b.tpm_limit ?? Infinity) || (a.rpm_limit ?? Infinity) - (b.rpm_limit ?? Infinity);
929
+ case "models":
930
+ return (a.models ?? []).join(",").localeCompare((b.models ?? []).join(","));
931
+ default:
932
+ return 0;
933
+ }
934
+ }
916
935
  var maskKey, shortKeyId, formatDate, keyToEditForm, KeysTable;
917
936
  var init_KeysTable = __esm({
918
937
  "src/components/KeysTable.tsx"() {
@@ -963,13 +982,29 @@ var init_KeysTable = __esm({
963
982
  const [resetSpendConfirm, setResetSpendConfirm] = useState(false);
964
983
  const [resetSpendSubmitting, setResetSpendSubmitting] = useState(false);
965
984
  const [filterText, setFilterText] = useState("");
985
+ const [sortKey, setSortKey] = useState("created");
986
+ const [sortDirection, setSortDirection] = useState("desc");
987
+ const handleSort = (nextSortKey) => {
988
+ if (sortKey === nextSortKey) {
989
+ setSortDirection((current) => current === "asc" ? "desc" : "asc");
990
+ } else {
991
+ setSortKey(nextSortKey);
992
+ setSortDirection("asc");
993
+ }
994
+ };
966
995
  const filteredKeys = useMemo2(() => {
967
- if (!filterText.trim()) return keys;
968
- const q = filterText.toLowerCase();
969
- return keys.filter(
970
- (k) => (k.key_alias ?? "").toLowerCase().includes(q) || k.models?.some((m) => m.toLowerCase().includes(q))
971
- );
972
- }, [keys, filterText]);
996
+ let result = keys;
997
+ if (filterText.trim()) {
998
+ const q = filterText.toLowerCase();
999
+ result = keys.filter(
1000
+ (k) => (k.key_alias ?? "").toLowerCase().includes(q) || k.models?.some((m) => m.toLowerCase().includes(q))
1001
+ );
1002
+ }
1003
+ return [...result].sort((a, b) => {
1004
+ const comparison = compareKeys(a, b, sortKey);
1005
+ return sortDirection === "asc" ? comparison : -comparison;
1006
+ });
1007
+ }, [keys, filterText, sortKey, sortDirection]);
973
1008
  const editSelectedModels = models.filter((m) => (editForm.models || []).includes(m.model_name));
974
1009
  const handleOpenEdit = (k) => {
975
1010
  setEditingKey(k);
@@ -1099,7 +1134,7 @@ var init_KeysTable = __esm({
1099
1134
  "& td": { color: theme.palette.text.secondary }
1100
1135
  }) : void 0
1101
1136
  },
1102
- /* @__PURE__ */ React3.createElement(TableCell, null, /* @__PURE__ */ React3.createElement(Box3, { display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "body2", sx: { fontWeight: 600 } }, key.key_alias || "\u2014"), key.blocked && /* @__PURE__ */ React3.createElement(StatusPill, { label: "Blocked", tone: "danger" }))),
1137
+ /* @__PURE__ */ React3.createElement(TableCell, null, /* @__PURE__ */ React3.createElement(Box3, { display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "body2" }, key.key_alias || "\u2014"), key.blocked && /* @__PURE__ */ React3.createElement(StatusPill, { label: "Blocked", tone: "danger" }))),
1103
1138
  /* @__PURE__ */ React3.createElement(TableCell, null, /* @__PURE__ */ React3.createElement(Box3, { display: "flex", alignItems: "center", gap: 0.5 }, /* @__PURE__ */ React3.createElement(
1104
1139
  Typography3,
1105
1140
  {
@@ -1181,7 +1216,8 @@ var init_KeysTable = __esm({
1181
1216
  TextField,
1182
1217
  {
1183
1218
  size: "small",
1184
- placeholder: "Filter by alias or model\u2026",
1219
+ label: "Search alias or model",
1220
+ placeholder: "Search alias or model\u2026",
1185
1221
  value: filterText,
1186
1222
  onChange: (e) => setFilterText(e.target.value),
1187
1223
  sx: { minWidth: 240 },
@@ -1221,7 +1257,7 @@ var init_KeysTable = __esm({
1221
1257
  "Generate New Key"
1222
1258
  ))
1223
1259
  },
1224
- /* @__PURE__ */ React3.createElement(TableContainer, null, /* @__PURE__ */ React3.createElement(Table, { size: "small", sx: dataTableSx }, /* @__PURE__ */ React3.createElement(TableHead, null, /* @__PURE__ */ React3.createElement(TableRow, null, /* @__PURE__ */ React3.createElement(TableCell, null, "Alias"), /* @__PURE__ */ React3.createElement(TableCell, null, "Key ID"), /* @__PURE__ */ React3.createElement(TableCell, null, "Created"), /* @__PURE__ */ React3.createElement(TableCell, null, "Expires"), /* @__PURE__ */ React3.createElement(TableCell, null, "Budget"), /* @__PURE__ */ React3.createElement(TableCell, null, "TPM / RPM"), /* @__PURE__ */ React3.createElement(TableCell, null, "Models"), /* @__PURE__ */ React3.createElement(TableCell, { align: "right" }, "Actions"))), /* @__PURE__ */ React3.createElement(TableBody, null, renderTableBody())))
1260
+ /* @__PURE__ */ React3.createElement(TableContainer, null, /* @__PURE__ */ React3.createElement(Table, { size: "small", sx: dataTableSx }, /* @__PURE__ */ React3.createElement(TableHead, null, /* @__PURE__ */ React3.createElement(TableRow, null, /* @__PURE__ */ React3.createElement(TableCell, { sortDirection: sortKey === "alias" ? sortDirection : false }, /* @__PURE__ */ React3.createElement(TableSortLabel, { active: sortKey === "alias", direction: sortKey === "alias" ? sortDirection : "asc", onClick: () => handleSort("alias") }, "Alias")), /* @__PURE__ */ React3.createElement(TableCell, null, "Key ID"), /* @__PURE__ */ React3.createElement(TableCell, { sortDirection: sortKey === "created" ? sortDirection : false }, /* @__PURE__ */ React3.createElement(TableSortLabel, { active: sortKey === "created", direction: sortKey === "created" ? sortDirection : "asc", onClick: () => handleSort("created") }, "Created")), /* @__PURE__ */ React3.createElement(TableCell, { sortDirection: sortKey === "expires" ? sortDirection : false }, /* @__PURE__ */ React3.createElement(TableSortLabel, { active: sortKey === "expires", direction: sortKey === "expires" ? sortDirection : "asc", onClick: () => handleSort("expires") }, "Expires")), /* @__PURE__ */ React3.createElement(TableCell, { sortDirection: sortKey === "budget" ? sortDirection : false }, /* @__PURE__ */ React3.createElement(TableSortLabel, { active: sortKey === "budget", direction: sortKey === "budget" ? sortDirection : "asc", onClick: () => handleSort("budget") }, "Budget")), /* @__PURE__ */ React3.createElement(TableCell, { sortDirection: sortKey === "limits" ? sortDirection : false }, /* @__PURE__ */ React3.createElement(TableSortLabel, { active: sortKey === "limits", direction: sortKey === "limits" ? sortDirection : "asc", onClick: () => handleSort("limits") }, "TPM / RPM")), /* @__PURE__ */ React3.createElement(TableCell, { sortDirection: sortKey === "models" ? sortDirection : false }, /* @__PURE__ */ React3.createElement(TableSortLabel, { active: sortKey === "models", direction: sortKey === "models" ? sortDirection : "asc", onClick: () => handleSort("models") }, "Models")), /* @__PURE__ */ React3.createElement(TableCell, { align: "right" }, "Actions"))), /* @__PURE__ */ React3.createElement(TableBody, null, renderTableBody())))
1225
1261
  ), /* @__PURE__ */ React3.createElement(Dialog, { open: !!editingKey, onClose: handleCloseEdit, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ React3.createElement(DialogTitle, null, "Edit Key"), /* @__PURE__ */ React3.createElement(DialogContent, null, editingKey && /* @__PURE__ */ React3.createElement(Box3, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "body2", color: "text.secondary" }, /* @__PURE__ */ React3.createElement("code", { style: { fontFamily: "monospace", color: "inherit" } }, maskKey(editingKey.key))), /* @__PURE__ */ React3.createElement(
1226
1262
  TextField,
1227
1263
  {
@@ -1344,9 +1380,12 @@ import Autocomplete2 from "@mui/material/Autocomplete";
1344
1380
  import Checkbox from "@mui/material/Checkbox";
1345
1381
  import FormControlLabel from "@mui/material/FormControlLabel";
1346
1382
  import Alert from "@mui/material/Alert";
1383
+ import Accordion from "@mui/material/Accordion";
1384
+ import AccordionSummary from "@mui/material/AccordionSummary";
1385
+ import AccordionDetails from "@mui/material/AccordionDetails";
1347
1386
  import Tabs from "@mui/material/Tabs";
1348
1387
  import Tab from "@mui/material/Tab";
1349
- import { ContentCopy as ContentCopy2, Code } from "@mui/icons-material";
1388
+ import { ContentCopy as ContentCopy2, Code, ExpandMore } from "@mui/icons-material";
1350
1389
  function trimSlash(url) {
1351
1390
  return url.replace(/\/+$/, "");
1352
1391
  }
@@ -1784,7 +1823,7 @@ var init_GenerateKeyDialog = __esm({
1784
1823
  required: true,
1785
1824
  fullWidth: true
1786
1825
  }
1787
- ), /* @__PURE__ */ React4.createElement(
1826
+ ), /* @__PURE__ */ React4.createElement(Accordion, { disableGutters: true, elevation: 0, sx: { border: "1px solid", borderColor: "divider", "&:before": { display: "none" } } }, /* @__PURE__ */ React4.createElement(AccordionSummary, { expandIcon: /* @__PURE__ */ React4.createElement(ExpandMore, null) }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "body2", fontWeight: 600 }, "Advanced")), /* @__PURE__ */ React4.createElement(AccordionDetails, { sx: { display: "flex", flexDirection: "column", gap: 2 } }, /* @__PURE__ */ React4.createElement(
1788
1827
  TextField2,
1789
1828
  {
1790
1829
  label: "TPM Limit",
@@ -1804,7 +1843,7 @@ var init_GenerateKeyDialog = __esm({
1804
1843
  helperText: "Max requests per minute this key can make across all models. Leave blank for no limit.",
1805
1844
  fullWidth: true
1806
1845
  }
1807
- ))), /* @__PURE__ */ React4.createElement(DialogActions2, null, newKeyValue ? /* @__PURE__ */ React4.createElement(Button3, { onClick: handleClose, variant: "contained", color: "success" }, "Done") : /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement(Button3, { onClick: handleClose }, "Cancel"), /* @__PURE__ */ React4.createElement(
1846
+ ))))), /* @__PURE__ */ React4.createElement(DialogActions2, null, newKeyValue ? /* @__PURE__ */ React4.createElement(Button3, { onClick: handleClose, variant: "contained", color: "success" }, "Done") : /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement(Button3, { onClick: handleClose }, "Cancel"), /* @__PURE__ */ React4.createElement(
1808
1847
  Button3,
1809
1848
  {
1810
1849
  onClick: handleGenerate,
@@ -1891,6 +1930,7 @@ var init_UsageStats = __esm({
1891
1930
  usage,
1892
1931
  models,
1893
1932
  dateRange,
1933
+ currentPreset,
1894
1934
  onDateRangeChange,
1895
1935
  loading,
1896
1936
  userInfo
@@ -1898,15 +1938,6 @@ var init_UsageStats = __esm({
1898
1938
  const chart = useChartTheme();
1899
1939
  const [selectedModel, setSelectedModel] = useState3("all");
1900
1940
  const [tab, setTab] = useState3("costs");
1901
- const selectedPreset = useMemo4(() => {
1902
- if (dateRange.start.toDateString() === dateRange.end.toDateString()) return "today";
1903
- const diffMs = dateRange.end.getTime() - dateRange.start.getTime();
1904
- const diffHours = diffMs / (1e3 * 60 * 60);
1905
- if (diffHours <= 26) return "24h";
1906
- const diffDays = Math.ceil(diffMs / (1e3 * 60 * 60 * 24));
1907
- if (diffDays <= 7) return "7d";
1908
- return "30d";
1909
- }, [dateRange]);
1910
1941
  const handlePresetChange = (preset) => {
1911
1942
  const end = /* @__PURE__ */ new Date();
1912
1943
  const start = /* @__PURE__ */ new Date();
@@ -1918,7 +1949,7 @@ var init_UsageStats = __esm({
1918
1949
  localStorage.setItem(PERIOD_LS_KEY, preset);
1919
1950
  } catch {
1920
1951
  }
1921
- onDateRangeChange({ start, end });
1952
+ onDateRangeChange({ start, end }, preset);
1922
1953
  };
1923
1954
  const dailyData = useMemo4(
1924
1955
  () => (usage?.daily_usage ?? []).map((d) => ({
@@ -2057,7 +2088,7 @@ var init_UsageStats = __esm({
2057
2088
  select: true,
2058
2089
  size: "small",
2059
2090
  label: "Period",
2060
- value: selectedPreset,
2091
+ value: currentPreset,
2061
2092
  onChange: (e) => handlePresetChange(e.target.value),
2062
2093
  sx: { minWidth: 160 }
2063
2094
  },
@@ -2067,7 +2098,7 @@ var init_UsageStats = __esm({
2067
2098
  SectionCard,
2068
2099
  {
2069
2100
  title: "Usage Analytics",
2070
- subtitle: `${PRESET_LABELS[selectedPreset]} \xB7 ${dateRange.start.toLocaleDateString()} \u2013 ${dateRange.end.toLocaleDateString()}`,
2101
+ subtitle: `${PRESET_LABELS[currentPreset]} \xB7 ${dateRange.start.toLocaleDateString()} \u2013 ${dateRange.end.toLocaleDateString()}`,
2071
2102
  actions: periodSelect
2072
2103
  },
2073
2104
  /* @__PURE__ */ React5.createElement(MetricStrip, { metrics }),
@@ -2234,7 +2265,7 @@ import Collapse from "@mui/material/Collapse";
2234
2265
  import IconButton3 from "@mui/material/IconButton";
2235
2266
  import CircularProgress3 from "@mui/material/CircularProgress";
2236
2267
  import { alpha as alpha4 } from "@mui/material/styles";
2237
- import { ExpandMore, Group } from "@mui/icons-material";
2268
+ import { ExpandMore as ExpandMore2, Group } from "@mui/icons-material";
2238
2269
  import {
2239
2270
  AreaChart as AreaChart2,
2240
2271
  Area as Area2,
@@ -2342,7 +2373,7 @@ var init_TeamUsage = __esm({
2342
2373
  transition: theme.transitions.create("transform")
2343
2374
  })
2344
2375
  },
2345
- /* @__PURE__ */ React6.createElement(ExpandMore, null)
2376
+ /* @__PURE__ */ React6.createElement(ExpandMore2, null)
2346
2377
  )), /* @__PURE__ */ React6.createElement(
2347
2378
  Box6,
2348
2379
  {
@@ -2442,6 +2473,10 @@ import Tooltip3 from "@mui/material/Tooltip";
2442
2473
  import Snackbar from "@mui/material/Snackbar";
2443
2474
  import Alert2 from "@mui/material/Alert";
2444
2475
  import { alpha as alpha5, useTheme as useTheme2 } from "@mui/material/styles";
2476
+ function isModelAllowedByTeam2(model, teamModels) {
2477
+ if (!teamModels || teamModels.length === 0 || teamModels.includes(ALL_PROXY_MODELS2)) return true;
2478
+ return teamModels.includes(model.model_name) || !!model.access_groups?.some((group) => teamModels.includes(group));
2479
+ }
2445
2480
  function fmtCostPerToken(cost) {
2446
2481
  if (cost === void 0 || cost === null) return "\u2014";
2447
2482
  if (cost === 0) return "$0";
@@ -2455,11 +2490,12 @@ function fmtTokens(n) {
2455
2490
  if (n >= 1e3) return `${(n / 1e3).toFixed(0)}K`;
2456
2491
  return String(n);
2457
2492
  }
2458
- var ModelsTable;
2493
+ var ALL_PROXY_MODELS2, ModelsTable;
2459
2494
  var init_ModelsTable = __esm({
2460
2495
  "src/components/ModelsTable.tsx"() {
2461
2496
  "use strict";
2462
2497
  init_ui();
2498
+ ALL_PROXY_MODELS2 = "all-proxy-models";
2463
2499
  ModelsTable = ({ allModels, teams, loading }) => {
2464
2500
  const theme = useTheme2();
2465
2501
  const [selectedTeamId, setSelectedTeamId] = useState5("");
@@ -2470,10 +2506,7 @@ var init_ModelsTable = __esm({
2470
2506
  );
2471
2507
  const filteredModels = useMemo5(() => {
2472
2508
  if (!selectedTeam) return [];
2473
- const teamModels = selectedTeam.models ?? [];
2474
- if (teamModels.length === 0) return allModels;
2475
- const allowed = new Set(teamModels);
2476
- return allModels.filter((m) => allowed.has(m.model_name));
2509
+ return allModels.filter((model) => isModelAllowedByTeam2(model, selectedTeam.models));
2477
2510
  }, [allModels, selectedTeam]);
2478
2511
  const handleCopyModelId = useCallback((modelId) => {
2479
2512
  navigator.clipboard.writeText(modelId).then(() => setCopiedSnackbar(true));
@@ -2840,6 +2873,13 @@ var init_LiteLLMPage = __esm({
2840
2873
  LiteLLMPage = () => {
2841
2874
  const api = useApi(liteLlmApiRef);
2842
2875
  const [dateRange, setDateRange] = useState7(initDateRange);
2876
+ const [currentPreset, setCurrentPreset] = useState7(() => {
2877
+ try {
2878
+ return localStorage.getItem(PERIOD_LS_KEY2) ?? "7d";
2879
+ } catch {
2880
+ return "7d";
2881
+ }
2882
+ });
2843
2883
  const [activeTab, setActiveTab] = useState7("overview");
2844
2884
  const [snackbar, setSnackbar] = useState7(null);
2845
2885
  const [generateDialogOpen, setGenerateDialogOpen] = useState7(false);
@@ -2886,11 +2926,14 @@ var init_LiteLLMPage = __esm({
2886
2926
  const endDate = dateRange.end.toISOString().split("T")[0];
2887
2927
  return api.getUsage(startDate, endDate);
2888
2928
  }, [api, dateRange]);
2889
- const handleDateRangeChange = useCallback3((range) => {
2929
+ const handleDateRangeChange = useCallback3((range, preset) => {
2890
2930
  setDateRange(range);
2931
+ if (preset) {
2932
+ setCurrentPreset(preset);
2933
+ }
2891
2934
  setTeamUsageCache({});
2892
2935
  setTeamUsageLoading({});
2893
- }, [setDateRange]);
2936
+ }, [setDateRange, setCurrentPreset]);
2894
2937
  const loadTeamUsage = useCallback3(async (teamId) => {
2895
2938
  if (teamUsageCache[teamId] !== void 0 || teamUsageLoading[teamId]) return;
2896
2939
  setTeamUsageLoading((prev) => ({ ...prev, [teamId]: true }));
@@ -3055,6 +3098,7 @@ var init_LiteLLMPage = __esm({
3055
3098
  usage: usage ?? null,
3056
3099
  models: allModels ?? [],
3057
3100
  dateRange,
3101
+ currentPreset,
3058
3102
  onDateRangeChange: handleDateRangeChange,
3059
3103
  loading: usageLoading,
3060
3104
  userInfo