@acarmisc/backstage-plugin-litellm 0.15.0 → 0.15.2

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.d.ts CHANGED
@@ -6,6 +6,7 @@ export { UsageStats } from './components/UsageStats';
6
6
  export { TeamUsage } from './components/TeamUsage';
7
7
  export { LiteLLMHomeWidget } from './components/LiteLLMHomeWidget';
8
8
  export type { LiteLLMHomeWidgetProps } from './components/LiteLLMHomeWidget';
9
+ export { GenerateKeyDialog } from './components/GenerateKeyDialog';
9
10
  export { LiteLlmApi, liteLlmApiRef } from './api';
10
11
  export type { LiteLlmApiInterface } from './api';
11
12
  export * from './types';
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,
@@ -2234,7 +2273,7 @@ import Collapse from "@mui/material/Collapse";
2234
2273
  import IconButton3 from "@mui/material/IconButton";
2235
2274
  import CircularProgress3 from "@mui/material/CircularProgress";
2236
2275
  import { alpha as alpha4 } from "@mui/material/styles";
2237
- import { ExpandMore, Group } from "@mui/icons-material";
2276
+ import { ExpandMore as ExpandMore2, Group } from "@mui/icons-material";
2238
2277
  import {
2239
2278
  AreaChart as AreaChart2,
2240
2279
  Area as Area2,
@@ -2342,7 +2381,7 @@ var init_TeamUsage = __esm({
2342
2381
  transition: theme.transitions.create("transform")
2343
2382
  })
2344
2383
  },
2345
- /* @__PURE__ */ React6.createElement(ExpandMore, null)
2384
+ /* @__PURE__ */ React6.createElement(ExpandMore2, null)
2346
2385
  )), /* @__PURE__ */ React6.createElement(
2347
2386
  Box6,
2348
2387
  {
@@ -2442,6 +2481,10 @@ import Tooltip3 from "@mui/material/Tooltip";
2442
2481
  import Snackbar from "@mui/material/Snackbar";
2443
2482
  import Alert2 from "@mui/material/Alert";
2444
2483
  import { alpha as alpha5, useTheme as useTheme2 } from "@mui/material/styles";
2484
+ function isModelAllowedByTeam2(model, teamModels) {
2485
+ if (!teamModels || teamModels.length === 0 || teamModels.includes(ALL_PROXY_MODELS2)) return true;
2486
+ return teamModels.includes(model.model_name) || !!model.access_groups?.some((group) => teamModels.includes(group));
2487
+ }
2445
2488
  function fmtCostPerToken(cost) {
2446
2489
  if (cost === void 0 || cost === null) return "\u2014";
2447
2490
  if (cost === 0) return "$0";
@@ -2455,11 +2498,12 @@ function fmtTokens(n) {
2455
2498
  if (n >= 1e3) return `${(n / 1e3).toFixed(0)}K`;
2456
2499
  return String(n);
2457
2500
  }
2458
- var ModelsTable;
2501
+ var ALL_PROXY_MODELS2, ModelsTable;
2459
2502
  var init_ModelsTable = __esm({
2460
2503
  "src/components/ModelsTable.tsx"() {
2461
2504
  "use strict";
2462
2505
  init_ui();
2506
+ ALL_PROXY_MODELS2 = "all-proxy-models";
2463
2507
  ModelsTable = ({ allModels, teams, loading }) => {
2464
2508
  const theme = useTheme2();
2465
2509
  const [selectedTeamId, setSelectedTeamId] = useState5("");
@@ -2470,10 +2514,7 @@ var init_ModelsTable = __esm({
2470
2514
  );
2471
2515
  const filteredModels = useMemo5(() => {
2472
2516
  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));
2517
+ return allModels.filter((model) => isModelAllowedByTeam2(model, selectedTeam.models));
2477
2518
  }, [allModels, selectedTeam]);
2478
2519
  const handleCopyModelId = useCallback((modelId) => {
2479
2520
  navigator.clipboard.writeText(modelId).then(() => setCopiedSnackbar(true));
@@ -3259,9 +3300,11 @@ var LiteLLMHomeWidget = ({
3259
3300
  };
3260
3301
 
3261
3302
  // src/index.ts
3303
+ init_GenerateKeyDialog();
3262
3304
  init_api();
3263
3305
  export {
3264
3306
  DashboardHeader,
3307
+ GenerateKeyDialog,
3265
3308
  KeysTable,
3266
3309
  LiteLLMHomeWidget,
3267
3310
  LiteLLMPage,