@acarmisc/backstage-plugin-litellm 0.13.0 → 0.15.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/index.esm.js CHANGED
@@ -1350,10 +1350,18 @@ import { ContentCopy as ContentCopy2, Code } from "@mui/icons-material";
1350
1350
  function trimSlash(url) {
1351
1351
  return url.replace(/\/+$/, "");
1352
1352
  }
1353
+ function isModelAllowedByTeam(model, teamModels) {
1354
+ if (!teamModels || teamModels.length === 0) return true;
1355
+ if (teamModels.includes(ALL_PROXY_MODELS)) return true;
1356
+ if (teamModels.includes(model.model_name)) return true;
1357
+ return !!model.access_groups?.some((group) => teamModels.includes(group));
1358
+ }
1353
1359
  function buildSnippets(baseUrl, key, model) {
1354
1360
  const base = trimSlash(baseUrl);
1361
+ const apiBase = `${base}/v1`;
1355
1362
  return {
1356
- curl: `curl ${base}/v1/chat/completions \\
1363
+ publicEndpoint: apiBase,
1364
+ curl: `curl ${apiBase}/chat/completions \\
1357
1365
  -H "Authorization: Bearer ${key}" \\
1358
1366
  -H "Content-Type: application/json" \\
1359
1367
  -d '{
@@ -1364,14 +1372,61 @@ function buildSnippets(baseUrl, key, model) {
1364
1372
 
1365
1373
  client = OpenAI(
1366
1374
  api_key="${key}",
1367
- base_url="${base}/v1",
1375
+ base_url="${apiBase}",
1368
1376
  )
1369
1377
 
1370
1378
  response = client.chat.completions.create(
1371
1379
  model="${model}",
1372
1380
  messages=[{ "role": "user", "content": "Hello!" }],
1373
1381
  )
1374
- print(response.choices[0].message.content)`
1382
+ print(response.choices[0].message.content)`,
1383
+ opencode: `{
1384
+ "$schema": "https://opencode.ai/config.json",
1385
+ "provider": {
1386
+ "litellm": {
1387
+ "npm": "@ai-sdk/openai-compatible",
1388
+ "name": "LiteLLM",
1389
+ "options": {
1390
+ "baseURL": "${apiBase}",
1391
+ "apiKey": "${key}"
1392
+ },
1393
+ "models": {
1394
+ "${model}": {}
1395
+ }
1396
+ }
1397
+ }
1398
+ }`,
1399
+ pi: `{
1400
+ "litellm": {
1401
+ "baseUrl": "${apiBase}",
1402
+ "apiKey": "${key}",
1403
+ "api": "openai-completions",
1404
+ "models": [
1405
+ { "id": "${model}", "name": "${model}" }
1406
+ ]
1407
+ }
1408
+ }`,
1409
+ claudeCode: `# Option 1 \u2014 Static key (Method 1: Unified Endpoint)
1410
+ export ANTHROPIC_AUTH_TOKEN="${key}"
1411
+ export ANTHROPIC_BASE_URL="${base}"
1412
+ claude --model ${model}
1413
+
1414
+ # Option 2 \u2014 Dynamic key via helper script
1415
+ # 1. Create ~/bin/get-litellm-key.sh:
1416
+ cat > ~/bin/get-litellm-key.sh << 'SCRIPT'
1417
+ #!/bin/bash
1418
+ curl -s -X POST ${base}/key/generate \\
1419
+ -H "Authorization: Bearer ${key}" \\
1420
+ -H "Content-Type: application/json" \\
1421
+ -d '{}' | jq -r '.key'
1422
+ SCRIPT
1423
+ chmod +x ~/bin/get-litellm-key.sh
1424
+
1425
+ # 2. Add to ~/.claude/settings.json:
1426
+ # { "apiKeyHelper": "~/bin/get-litellm-key.sh" }
1427
+
1428
+ # 3. Set refresh interval (optional, default 1h):
1429
+ export CLAUDE_CODE_API_KEY_HELPER_TTL_MS=3600000`
1375
1430
  };
1376
1431
  }
1377
1432
  function aliasHelperText(aliasError, aliasDuplicate) {
@@ -1409,7 +1464,7 @@ function formatContextWindow2(maxInput, maxOutput) {
1409
1464
  if (outPart) return `ctx ${outPart} out`;
1410
1465
  return null;
1411
1466
  }
1412
- var generateDefaultAlias, emptyForm, SnippetTabs, GenerateKeyDialog;
1467
+ var generateDefaultAlias, emptyForm, ALL_PROXY_MODELS, SNIPPET_FILE_HINTS, SnippetTabs, GenerateKeyDialog;
1413
1468
  var init_GenerateKeyDialog = __esm({
1414
1469
  "src/components/GenerateKeyDialog.tsx"() {
1415
1470
  "use strict";
@@ -1429,10 +1484,18 @@ var init_GenerateKeyDialog = __esm({
1429
1484
  team_id: void 0,
1430
1485
  key_type: "llm_api"
1431
1486
  });
1487
+ ALL_PROXY_MODELS = "all-proxy-models";
1488
+ SNIPPET_FILE_HINTS = {
1489
+ opencode: "Add to ~/.config/opencode/opencode.json",
1490
+ pi: "Add to ~/.pi/agent/models.json",
1491
+ "claude-code": "Requires Claude Code CLI installed"
1492
+ };
1432
1493
  SnippetTabs = ({ snippets, model, onCopy }) => {
1433
1494
  const [tab, setTab] = useState2("curl");
1434
- const code = tab === "curl" ? snippets.curl : snippets.openai;
1435
- return /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Tabs, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 1 } }, /* @__PURE__ */ React4.createElement(Tab, { label: "curl", value: "curl" }), /* @__PURE__ */ React4.createElement(Tab, { label: "OpenAI SDK", value: "openai" })), /* @__PURE__ */ React4.createElement(
1495
+ const snippetKey = tab === "claude-code" ? "claudeCode" : tab;
1496
+ const code = snippets[snippetKey];
1497
+ const fileHint = SNIPPET_FILE_HINTS[tab];
1498
+ return /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Tabs, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 1 }, variant: "scrollable" }, /* @__PURE__ */ React4.createElement(Tab, { label: "curl", value: "curl" }), /* @__PURE__ */ React4.createElement(Tab, { label: "OpenAI SDK", value: "openai" }), /* @__PURE__ */ React4.createElement(Tab, { label: "opencode", value: "opencode" }), /* @__PURE__ */ React4.createElement(Tab, { label: "pi", value: "pi" }), /* @__PURE__ */ React4.createElement(Tab, { label: "Claude Code", value: "claude-code" })), fileHint && /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption", color: "text.secondary", display: "block", mb: 0.5 }, fileHint), /* @__PURE__ */ React4.createElement(
1436
1499
  Box4,
1437
1500
  {
1438
1501
  position: "relative",
@@ -1499,12 +1562,7 @@ var init_GenerateKeyDialog = __esm({
1499
1562
  const teamRequired = keyGenerationSettings?.teamRequired ?? true;
1500
1563
  const selectedTeam = teams.find((t) => t.team_id === formData.team_id) ?? null;
1501
1564
  const availableModels = useMemo3(() => {
1502
- const teamModels = selectedTeam?.models;
1503
- if (teamModels && teamModels.length > 0) {
1504
- const allowed = new Set(teamModels);
1505
- return models.filter((m) => allowed.has(m.model_name));
1506
- }
1507
- return models;
1565
+ return models.filter((m) => isModelAllowedByTeam(m, selectedTeam?.models));
1508
1566
  }, [models, selectedTeam]);
1509
1567
  const selectedModels = availableModels.filter((m) => (formData.models || []).includes(m.model_name));
1510
1568
  const aliasError = !(formData.alias || "").trim();
@@ -1528,12 +1586,12 @@ var init_GenerateKeyDialog = __esm({
1528
1586
  max_budget: unlimitedBudget ? null : formData.max_budget
1529
1587
  };
1530
1588
  const response = await onGenerateKey(request);
1589
+ const model = formData.models?.[0] ?? availableModels[0]?.model_name ?? "";
1531
1590
  setNewKeyValue(response.key);
1532
- setNewKeyModel(formData.models?.[0] ?? "");
1591
+ setNewKeyModel(model);
1533
1592
  setNewKeySnippets(null);
1534
1593
  try {
1535
1594
  const config = await onGetConfig();
1536
- const model = formData.models?.[0] ?? "";
1537
1595
  setNewKeySnippets(buildSnippets(config.baseUrl, response.key, model));
1538
1596
  } catch {
1539
1597
  }
@@ -1571,8 +1629,10 @@ var init_GenerateKeyDialog = __esm({
1571
1629
  getOptionLabel: (t) => t.team_alias || t.team_id,
1572
1630
  value: selectedTeam,
1573
1631
  onChange: (_e, team) => {
1574
- const teamModels = team?.models;
1575
- const restrictedModels = teamModels && teamModels.length > 0 ? (formData.models || []).filter((m) => teamModels.includes(m)) : formData.models;
1632
+ const restrictedModels = (formData.models || []).filter((name) => {
1633
+ const model = models.find((m) => m.model_name === name);
1634
+ return model ? isModelAllowedByTeam(model, team?.models) : false;
1635
+ });
1576
1636
  setFormData({ ...formData, team_id: team?.team_id, models: restrictedModels });
1577
1637
  },
1578
1638
  renderInput: (params) => /* @__PURE__ */ React4.createElement(
@@ -1619,7 +1679,31 @@ var init_GenerateKeyDialog = __esm({
1619
1679
  newKeyValue
1620
1680
  ),
1621
1681
  /* @__PURE__ */ React4.createElement(IconButton2, { onClick: () => copyToClipboard(newKeyValue) }, /* @__PURE__ */ React4.createElement(ContentCopy2, null))
1622
- ), newKeySnippets && /* @__PURE__ */ React4.createElement(Box4, { mt: 3 }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React4.createElement(Code, { fontSize: "small", color: "action" }), /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2" }, "Start calling the proxy \u2014 paste and run")), /* @__PURE__ */ React4.createElement(SnippetTabs, { snippets: newKeySnippets, model: newKeyModel, onCopy: copyToClipboard }))) : /* @__PURE__ */ React4.createElement(Box4, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, generateError && /* @__PURE__ */ React4.createElement(Alert, { severity: "error", onClose: () => setGenerateError(null) }, generateError), /* @__PURE__ */ React4.createElement(
1682
+ ), newKeySnippets && /* @__PURE__ */ React4.createElement(Box4, { mt: 2 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption", color: "text.secondary", display: "block", gutterBottom: true }, "Public endpoint \u2014 paste into any tool's base URL / API base field"), /* @__PURE__ */ React4.createElement(
1683
+ Box4,
1684
+ {
1685
+ display: "flex",
1686
+ alignItems: "center",
1687
+ gap: 1,
1688
+ p: 1.5,
1689
+ sx: {
1690
+ backgroundColor: "action.hover",
1691
+ border: "1px solid",
1692
+ borderColor: "divider",
1693
+ borderRadius: 1
1694
+ }
1695
+ },
1696
+ /* @__PURE__ */ React4.createElement(
1697
+ Typography4,
1698
+ {
1699
+ component: "code",
1700
+ color: "text.primary",
1701
+ sx: { fontFamily: "monospace", fontSize: 13, wordBreak: "break-all", flex: 1 }
1702
+ },
1703
+ newKeySnippets.publicEndpoint
1704
+ ),
1705
+ /* @__PURE__ */ React4.createElement(IconButton2, { size: "small", onClick: () => copyToClipboard(newKeySnippets.publicEndpoint) }, /* @__PURE__ */ React4.createElement(ContentCopy2, { fontSize: "small" }))
1706
+ )), newKeySnippets && /* @__PURE__ */ React4.createElement(Box4, { mt: 3 }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React4.createElement(Code, { fontSize: "small", color: "action" }), /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2" }, "Start calling the proxy \u2014 paste and run")), /* @__PURE__ */ React4.createElement(SnippetTabs, { snippets: newKeySnippets, model: newKeyModel, onCopy: copyToClipboard }))) : /* @__PURE__ */ React4.createElement(Box4, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, generateError && /* @__PURE__ */ React4.createElement(Alert, { severity: "error", onClose: () => setGenerateError(null) }, generateError), /* @__PURE__ */ React4.createElement(
1623
1707
  TextField2,
1624
1708
  {
1625
1709
  label: "Alias",
@@ -2339,24 +2423,169 @@ var init_TeamUsage = __esm({
2339
2423
  }
2340
2424
  });
2341
2425
 
2342
- // src/components/AuditLog.tsx
2343
- import React7, { useState as useState5, useCallback } from "react";
2426
+ // src/components/ModelsTable.tsx
2427
+ import React7, { useState as useState5, useMemo as useMemo5, useCallback } from "react";
2344
2428
  import Box7 from "@mui/material/Box";
2345
- import Typography7 from "@mui/material/Typography";
2346
2429
  import Table4 from "@mui/material/Table";
2347
2430
  import TableBody4 from "@mui/material/TableBody";
2348
2431
  import TableCell4 from "@mui/material/TableCell";
2349
2432
  import TableContainer4 from "@mui/material/TableContainer";
2350
2433
  import TableHead4 from "@mui/material/TableHead";
2351
2434
  import TableRow4 from "@mui/material/TableRow";
2435
+ import Typography7 from "@mui/material/Typography";
2436
+ import MenuItem3 from "@mui/material/MenuItem";
2437
+ import Select from "@mui/material/Select";
2438
+ import FormControl from "@mui/material/FormControl";
2439
+ import InputLabel from "@mui/material/InputLabel";
2440
+ import ContentCopyIcon from "@mui/icons-material/ContentCopy";
2441
+ import Tooltip3 from "@mui/material/Tooltip";
2442
+ import Snackbar from "@mui/material/Snackbar";
2443
+ import Alert2 from "@mui/material/Alert";
2444
+ import { alpha as alpha5, useTheme as useTheme2 } from "@mui/material/styles";
2445
+ function fmtCostPerToken(cost) {
2446
+ if (cost === void 0 || cost === null) return "\u2014";
2447
+ if (cost === 0) return "$0";
2448
+ if (cost < 1e-6) return `$${(cost * 1e6).toFixed(2)}/M`;
2449
+ if (cost < 1e-3) return `$${(cost * 1e3).toFixed(3)}/K`;
2450
+ return `$${cost.toFixed(4)}`;
2451
+ }
2452
+ function fmtTokens(n) {
2453
+ if (n === void 0 || n === null) return "\u2014";
2454
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
2455
+ if (n >= 1e3) return `${(n / 1e3).toFixed(0)}K`;
2456
+ return String(n);
2457
+ }
2458
+ var ModelsTable;
2459
+ var init_ModelsTable = __esm({
2460
+ "src/components/ModelsTable.tsx"() {
2461
+ "use strict";
2462
+ init_ui();
2463
+ ModelsTable = ({ allModels, teams, loading }) => {
2464
+ const theme = useTheme2();
2465
+ const [selectedTeamId, setSelectedTeamId] = useState5("");
2466
+ const [copiedSnackbar, setCopiedSnackbar] = useState5(false);
2467
+ const selectedTeam = useMemo5(
2468
+ () => teams.find((t) => t.team_id === selectedTeamId) ?? null,
2469
+ [teams, selectedTeamId]
2470
+ );
2471
+ const filteredModels = useMemo5(() => {
2472
+ 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));
2477
+ }, [allModels, selectedTeam]);
2478
+ const handleCopyModelId = useCallback((modelId) => {
2479
+ navigator.clipboard.writeText(modelId).then(() => setCopiedSnackbar(true));
2480
+ }, []);
2481
+ const modeColor = (mode) => {
2482
+ switch (mode) {
2483
+ case "chat":
2484
+ return theme.palette.info.main;
2485
+ case "embedding":
2486
+ return theme.palette.success.main;
2487
+ case "completion":
2488
+ return theme.palette.primary.main;
2489
+ case "image":
2490
+ return theme.palette.warning.main;
2491
+ default:
2492
+ return theme.palette.info.main;
2493
+ }
2494
+ };
2495
+ return /* @__PURE__ */ React7.createElement(
2496
+ SectionCard,
2497
+ {
2498
+ title: "Available Models",
2499
+ subtitle: selectedTeam ? `${filteredModels.length} model${filteredModels.length !== 1 ? "s" : ""} in ${selectedTeam.team_alias ?? selectedTeam.team_id}` : "Select a team to view its models"
2500
+ },
2501
+ /* @__PURE__ */ React7.createElement(Box7, { sx: { px: 2.5, pb: 2.5, pt: 1 } }, /* @__PURE__ */ React7.createElement(FormControl, { size: "small", sx: { minWidth: 240, mb: 2 } }, /* @__PURE__ */ React7.createElement(InputLabel, { id: "team-select-label" }, "Team"), /* @__PURE__ */ React7.createElement(
2502
+ Select,
2503
+ {
2504
+ labelId: "team-select-label",
2505
+ value: selectedTeamId,
2506
+ label: "Team",
2507
+ onChange: (e) => setSelectedTeamId(e.target.value)
2508
+ },
2509
+ teams.map((t) => /* @__PURE__ */ React7.createElement(MenuItem3, { key: t.team_id, value: t.team_id }, t.team_alias ?? t.team_id))
2510
+ ))),
2511
+ loading && /* @__PURE__ */ React7.createElement(EmptyState, { message: "Loading models\u2026" }),
2512
+ !loading && !selectedTeamId && /* @__PURE__ */ React7.createElement(
2513
+ EmptyState,
2514
+ {
2515
+ message: "Select a team to see available models",
2516
+ hint: "Each team may have a different set of models assigned"
2517
+ }
2518
+ ),
2519
+ !loading && selectedTeamId && filteredModels.length === 0 && /* @__PURE__ */ React7.createElement(
2520
+ EmptyState,
2521
+ {
2522
+ message: "No models available for this team",
2523
+ hint: allModels.length === 0 ? "No models are configured in the system" : "None of the team's assigned models were found in the catalog"
2524
+ }
2525
+ ),
2526
+ !loading && filteredModels.length > 0 && /* @__PURE__ */ React7.createElement(TableContainer4, null, /* @__PURE__ */ React7.createElement(Table4, { size: "small", sx: dataTableSx }, /* @__PURE__ */ React7.createElement(TableHead4, null, /* @__PURE__ */ React7.createElement(TableRow4, null, /* @__PURE__ */ React7.createElement(TableCell4, null, "Model ID"), /* @__PURE__ */ React7.createElement(TableCell4, null, "Mode"), /* @__PURE__ */ React7.createElement(TableCell4, { align: "right" }, "Input Cost"), /* @__PURE__ */ React7.createElement(TableCell4, { align: "right" }, "Output Cost"), /* @__PURE__ */ React7.createElement(TableCell4, { align: "right" }, "Max Input"), /* @__PURE__ */ React7.createElement(TableCell4, { align: "right" }, "Max Output"), /* @__PURE__ */ React7.createElement(TableCell4, null, "Capabilities"))), /* @__PURE__ */ React7.createElement(TableBody4, null, filteredModels.map((m) => /* @__PURE__ */ React7.createElement(TableRow4, { key: m.model_name, hover: true }, /* @__PURE__ */ React7.createElement(TableCell4, null, /* @__PURE__ */ React7.createElement(Box7, { sx: { display: "flex", alignItems: "center", gap: 0.5 } }, /* @__PURE__ */ React7.createElement(TagChip, { label: m.model_name, title: m.model_name }), /* @__PURE__ */ React7.createElement(Tooltip3, { title: "Copy model ID" }, /* @__PURE__ */ React7.createElement(
2527
+ Box7,
2528
+ {
2529
+ component: "span",
2530
+ role: "button",
2531
+ tabIndex: 0,
2532
+ onClick: () => handleCopyModelId(m.model_name),
2533
+ onKeyDown: (e) => e.key === "Enter" && handleCopyModelId(m.model_name),
2534
+ sx: quietIconButtonSx("accent"),
2535
+ style: { cursor: "pointer", display: "inline-flex" }
2536
+ },
2537
+ /* @__PURE__ */ React7.createElement(ContentCopyIcon, { sx: { fontSize: 14 } })
2538
+ )))), /* @__PURE__ */ React7.createElement(TableCell4, null, /* @__PURE__ */ React7.createElement(
2539
+ Box7,
2540
+ {
2541
+ component: "span",
2542
+ sx: {
2543
+ display: "inline-block",
2544
+ px: 0.75,
2545
+ py: 0.25,
2546
+ borderRadius: 1,
2547
+ fontSize: 11,
2548
+ fontWeight: 600,
2549
+ textTransform: "capitalize",
2550
+ bgcolor: alpha5(modeColor(m.mode), 0.1),
2551
+ color: modeColor(m.mode)
2552
+ }
2553
+ },
2554
+ m.mode
2555
+ )), /* @__PURE__ */ React7.createElement(TableCell4, { align: "right" }, /* @__PURE__ */ React7.createElement(Typography7, { variant: "body2", sx: { fontVariantNumeric: "tabular-nums" } }, fmtCostPerToken(m.input_cost_per_token))), /* @__PURE__ */ React7.createElement(TableCell4, { align: "right" }, /* @__PURE__ */ React7.createElement(Typography7, { variant: "body2", sx: { fontVariantNumeric: "tabular-nums" } }, fmtCostPerToken(m.output_cost_per_token))), /* @__PURE__ */ React7.createElement(TableCell4, { align: "right" }, /* @__PURE__ */ React7.createElement(Typography7, { variant: "body2", sx: { fontVariantNumeric: "tabular-nums" } }, fmtTokens(m.max_input_tokens))), /* @__PURE__ */ React7.createElement(TableCell4, { align: "right" }, /* @__PURE__ */ React7.createElement(Typography7, { variant: "body2", sx: { fontVariantNumeric: "tabular-nums" } }, fmtTokens(m.max_output_tokens))), /* @__PURE__ */ React7.createElement(TableCell4, null, /* @__PURE__ */ React7.createElement(Box7, { sx: { display: "flex", gap: 0.5, flexWrap: "wrap" } }, m.supports_function_calling && /* @__PURE__ */ React7.createElement(TagChip, { label: "Fn", title: "Function calling" }), m.supports_vision && /* @__PURE__ */ React7.createElement(TagChip, { label: "\u{1F441}", title: "Vision" })))))))),
2556
+ /* @__PURE__ */ React7.createElement(
2557
+ Snackbar,
2558
+ {
2559
+ open: copiedSnackbar,
2560
+ autoHideDuration: 2e3,
2561
+ onClose: () => setCopiedSnackbar(false),
2562
+ anchorOrigin: { vertical: "bottom", horizontal: "center" }
2563
+ },
2564
+ /* @__PURE__ */ React7.createElement(Alert2, { severity: "success", variant: "filled", sx: { fontSize: 13 } }, "Model ID copied to clipboard")
2565
+ )
2566
+ );
2567
+ };
2568
+ }
2569
+ });
2570
+
2571
+ // src/components/AuditLog.tsx
2572
+ import React8, { useState as useState6, useCallback as useCallback2 } from "react";
2573
+ import Box8 from "@mui/material/Box";
2574
+ import Typography8 from "@mui/material/Typography";
2575
+ import Table5 from "@mui/material/Table";
2576
+ import TableBody5 from "@mui/material/TableBody";
2577
+ import TableCell5 from "@mui/material/TableCell";
2578
+ import TableContainer5 from "@mui/material/TableContainer";
2579
+ import TableHead5 from "@mui/material/TableHead";
2580
+ import TableRow5 from "@mui/material/TableRow";
2352
2581
  import TablePagination from "@mui/material/TablePagination";
2353
2582
  import TextField4 from "@mui/material/TextField";
2354
- import MenuItem3 from "@mui/material/MenuItem";
2583
+ import MenuItem4 from "@mui/material/MenuItem";
2355
2584
  import Skeleton5 from "@mui/material/Skeleton";
2356
2585
  import Collapse2 from "@mui/material/Collapse";
2357
2586
  import IconButton4 from "@mui/material/IconButton";
2358
- import Alert2 from "@mui/material/Alert";
2359
- import { alpha as alpha5 } from "@mui/material/styles";
2587
+ import Alert3 from "@mui/material/Alert";
2588
+ import { alpha as alpha6 } from "@mui/material/styles";
2360
2589
  import { KeyboardArrowDown } from "@mui/icons-material";
2361
2590
  import { useAsync } from "react-use";
2362
2591
  function actionTone(action) {
@@ -2379,10 +2608,10 @@ function formatDateTime(iso) {
2379
2608
  }
2380
2609
  function renderAuditLogBody(loading, entries) {
2381
2610
  if (loading) {
2382
- return /* @__PURE__ */ React7.createElement(TableRow4, null, /* @__PURE__ */ React7.createElement(TableCell4, { colSpan: 6, sx: { py: 2 } }, /* @__PURE__ */ React7.createElement(Skeleton5, { variant: "rounded", height: 160 })));
2611
+ return /* @__PURE__ */ React8.createElement(TableRow5, null, /* @__PURE__ */ React8.createElement(TableCell5, { colSpan: 6, sx: { py: 2 } }, /* @__PURE__ */ React8.createElement(Skeleton5, { variant: "rounded", height: 160 })));
2383
2612
  }
2384
2613
  if (entries.length === 0) {
2385
- return /* @__PURE__ */ React7.createElement(TableRow4, null, /* @__PURE__ */ React7.createElement(TableCell4, { colSpan: 6 }, /* @__PURE__ */ React7.createElement(
2614
+ return /* @__PURE__ */ React8.createElement(TableRow5, null, /* @__PURE__ */ React8.createElement(TableCell5, { colSpan: 6 }, /* @__PURE__ */ React8.createElement(
2386
2615
  EmptyState,
2387
2616
  {
2388
2617
  message: "No audit events found",
@@ -2390,7 +2619,7 @@ function renderAuditLogBody(loading, entries) {
2390
2619
  }
2391
2620
  )));
2392
2621
  }
2393
- return entries.map((entry) => /* @__PURE__ */ React7.createElement(DetailRow, { key: entry.id, entry }));
2622
+ return entries.map((entry) => /* @__PURE__ */ React8.createElement(DetailRow, { key: entry.id, entry }));
2394
2623
  }
2395
2624
  var ACTION_TONES, TABLE_LABELS, DetailRow, AuditLog;
2396
2625
  var init_AuditLog = __esm({
@@ -2410,9 +2639,9 @@ var init_AuditLog = __esm({
2410
2639
  LiteLLM_UserTable: "User"
2411
2640
  };
2412
2641
  DetailRow = ({ entry }) => {
2413
- const [open, setOpen] = useState5(false);
2642
+ const [open, setOpen] = useState6(false);
2414
2643
  const hasDetail = entry.before_value || entry.updated_values;
2415
- return /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement(TableRow4, null, /* @__PURE__ */ React7.createElement(TableCell4, { sx: { width: 40, pr: 0 } }, hasDetail && /* @__PURE__ */ React7.createElement(
2644
+ return /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(TableRow5, null, /* @__PURE__ */ React8.createElement(TableCell5, { sx: { width: 40, pr: 0 } }, hasDetail && /* @__PURE__ */ React8.createElement(
2416
2645
  IconButton4,
2417
2646
  {
2418
2647
  size: "small",
@@ -2425,9 +2654,9 @@ var init_AuditLog = __esm({
2425
2654
  transition: theme.transitions.create("transform")
2426
2655
  })
2427
2656
  },
2428
- /* @__PURE__ */ React7.createElement(KeyboardArrowDown, { fontSize: "small" })
2429
- )), /* @__PURE__ */ React7.createElement(TableCell4, { sx: { whiteSpace: "nowrap" } }, /* @__PURE__ */ React7.createElement(Typography7, { variant: "body2", color: "text.secondary" }, formatDateTime(entry.updated_at))), /* @__PURE__ */ React7.createElement(TableCell4, null, entry.action && /* @__PURE__ */ React7.createElement(StatusPill, { label: entry.action, tone: actionTone(entry.action) })), /* @__PURE__ */ React7.createElement(TableCell4, null, /* @__PURE__ */ React7.createElement(Typography7, { variant: "body2" }, prettyTableName(entry.table_name))), /* @__PURE__ */ React7.createElement(TableCell4, null, /* @__PURE__ */ React7.createElement(
2430
- Typography7,
2657
+ /* @__PURE__ */ React8.createElement(KeyboardArrowDown, { fontSize: "small" })
2658
+ )), /* @__PURE__ */ React8.createElement(TableCell5, { sx: { whiteSpace: "nowrap" } }, /* @__PURE__ */ React8.createElement(Typography8, { variant: "body2", color: "text.secondary" }, formatDateTime(entry.updated_at))), /* @__PURE__ */ React8.createElement(TableCell5, null, entry.action && /* @__PURE__ */ React8.createElement(StatusPill, { label: entry.action, tone: actionTone(entry.action) })), /* @__PURE__ */ React8.createElement(TableCell5, null, /* @__PURE__ */ React8.createElement(Typography8, { variant: "body2" }, prettyTableName(entry.table_name))), /* @__PURE__ */ React8.createElement(TableCell5, null, /* @__PURE__ */ React8.createElement(
2659
+ Typography8,
2431
2660
  {
2432
2661
  variant: "body2",
2433
2662
  component: "code",
@@ -2436,8 +2665,8 @@ var init_AuditLog = __esm({
2436
2665
  sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 12 }
2437
2666
  },
2438
2667
  entry.object_id ? entry.object_id.slice(0, 20) + (entry.object_id.length > 20 ? "\u2026" : "") : "\u2014"
2439
- )), /* @__PURE__ */ React7.createElement(TableCell4, null, entry.changed_by ?? "\u2014")), hasDetail && /* @__PURE__ */ React7.createElement(TableRow4, { sx: { "&&:hover": { bgcolor: "transparent" } } }, /* @__PURE__ */ React7.createElement(TableCell4, { colSpan: 6, sx: { "&&": { py: 0, border: 0 } } }, /* @__PURE__ */ React7.createElement(Collapse2, { in: open, unmountOnExit: true }, /* @__PURE__ */ React7.createElement(
2440
- Box7,
2668
+ )), /* @__PURE__ */ React8.createElement(TableCell5, null, entry.changed_by ?? "\u2014")), hasDetail && /* @__PURE__ */ React8.createElement(TableRow5, { sx: { "&&:hover": { bgcolor: "transparent" } } }, /* @__PURE__ */ React8.createElement(TableCell5, { colSpan: 6, sx: { "&&": { py: 0, border: 0 } } }, /* @__PURE__ */ React8.createElement(Collapse2, { in: open, unmountOnExit: true }, /* @__PURE__ */ React8.createElement(
2669
+ Box8,
2441
2670
  {
2442
2671
  display: "flex",
2443
2672
  gap: 2,
@@ -2446,11 +2675,11 @@ var init_AuditLog = __esm({
2446
2675
  p: 2,
2447
2676
  mb: 1.5,
2448
2677
  borderRadius: 1.5,
2449
- bgcolor: alpha5(theme.palette.text.primary, theme.palette.mode === "dark" ? 0.05 : 0.03)
2678
+ bgcolor: alpha6(theme.palette.text.primary, theme.palette.mode === "dark" ? 0.05 : 0.03)
2450
2679
  })
2451
2680
  },
2452
- entry.before_value && /* @__PURE__ */ React7.createElement(Box7, { flex: 1, minWidth: 200 }, /* @__PURE__ */ React7.createElement(
2453
- Typography7,
2681
+ entry.before_value && /* @__PURE__ */ React8.createElement(Box8, { flex: 1, minWidth: 200 }, /* @__PURE__ */ React8.createElement(
2682
+ Typography8,
2454
2683
  {
2455
2684
  variant: "caption",
2456
2685
  color: "text.secondary",
@@ -2459,9 +2688,9 @@ var init_AuditLog = __esm({
2459
2688
  sx: { fontSize: 10.5, fontWeight: 700, letterSpacing: "0.07em", textTransform: "uppercase" }
2460
2689
  },
2461
2690
  "Before"
2462
- ), /* @__PURE__ */ React7.createElement(Typography7, { component: "pre", variant: "caption", sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", whiteSpace: "pre-wrap", wordBreak: "break-all", m: 0 } }, JSON.stringify(entry.before_value, null, 2))),
2463
- entry.updated_values && /* @__PURE__ */ React7.createElement(Box7, { flex: 1, minWidth: 200 }, /* @__PURE__ */ React7.createElement(
2464
- Typography7,
2691
+ ), /* @__PURE__ */ React8.createElement(Typography8, { component: "pre", variant: "caption", sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", whiteSpace: "pre-wrap", wordBreak: "break-all", m: 0 } }, JSON.stringify(entry.before_value, null, 2))),
2692
+ entry.updated_values && /* @__PURE__ */ React8.createElement(Box8, { flex: 1, minWidth: 200 }, /* @__PURE__ */ React8.createElement(
2693
+ Typography8,
2465
2694
  {
2466
2695
  variant: "caption",
2467
2696
  color: "text.secondary",
@@ -2470,14 +2699,14 @@ var init_AuditLog = __esm({
2470
2699
  sx: { fontSize: 10.5, fontWeight: 700, letterSpacing: "0.07em", textTransform: "uppercase" }
2471
2700
  },
2472
2701
  "After"
2473
- ), /* @__PURE__ */ React7.createElement(Typography7, { component: "pre", variant: "caption", sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", whiteSpace: "pre-wrap", wordBreak: "break-all", m: 0 } }, JSON.stringify(entry.updated_values, null, 2)))
2702
+ ), /* @__PURE__ */ React8.createElement(Typography8, { component: "pre", variant: "caption", sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", whiteSpace: "pre-wrap", wordBreak: "break-all", m: 0 } }, JSON.stringify(entry.updated_values, null, 2)))
2474
2703
  )))));
2475
2704
  };
2476
2705
  AuditLog = ({ api }) => {
2477
- const [page, setPage] = useState5(0);
2478
- const [pageSize, setPageSize] = useState5(25);
2479
- const [filters, setFilters] = useState5({});
2480
- const fetchParams = useCallback(
2706
+ const [page, setPage] = useState6(0);
2707
+ const [pageSize, setPageSize] = useState6(25);
2708
+ const [filters, setFilters] = useState6({});
2709
+ const fetchParams = useCallback2(
2481
2710
  () => ({ page: page + 1, page_size: pageSize, ...filters }),
2482
2711
  [page, pageSize, filters]
2483
2712
  )();
@@ -2487,13 +2716,13 @@ var init_AuditLog = __esm({
2487
2716
  );
2488
2717
  const entries = value?.audit_logs ?? [];
2489
2718
  const total = value?.total ?? 0;
2490
- return /* @__PURE__ */ React7.createElement(
2719
+ return /* @__PURE__ */ React8.createElement(
2491
2720
  SectionCard,
2492
2721
  {
2493
2722
  title: "Audit Log",
2494
2723
  subtitle: loading ? "Loading\u2026" : `${total.toLocaleString()} event${total === 1 ? "" : "s"}`,
2495
2724
  flush: true,
2496
- actions: /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement(
2725
+ actions: /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(
2497
2726
  TextField4,
2498
2727
  {
2499
2728
  size: "small",
@@ -2506,12 +2735,12 @@ var init_AuditLog = __esm({
2506
2735
  },
2507
2736
  sx: { minWidth: 150 }
2508
2737
  },
2509
- /* @__PURE__ */ React7.createElement(MenuItem3, { value: "" }, "All actions"),
2510
- /* @__PURE__ */ React7.createElement(MenuItem3, { value: "created" }, "Created"),
2511
- /* @__PURE__ */ React7.createElement(MenuItem3, { value: "updated" }, "Updated"),
2512
- /* @__PURE__ */ React7.createElement(MenuItem3, { value: "deleted" }, "Deleted"),
2513
- /* @__PURE__ */ React7.createElement(MenuItem3, { value: "blocked" }, "Blocked")
2514
- ), /* @__PURE__ */ React7.createElement(
2738
+ /* @__PURE__ */ React8.createElement(MenuItem4, { value: "" }, "All actions"),
2739
+ /* @__PURE__ */ React8.createElement(MenuItem4, { value: "created" }, "Created"),
2740
+ /* @__PURE__ */ React8.createElement(MenuItem4, { value: "updated" }, "Updated"),
2741
+ /* @__PURE__ */ React8.createElement(MenuItem4, { value: "deleted" }, "Deleted"),
2742
+ /* @__PURE__ */ React8.createElement(MenuItem4, { value: "blocked" }, "Blocked")
2743
+ ), /* @__PURE__ */ React8.createElement(
2515
2744
  TextField4,
2516
2745
  {
2517
2746
  size: "small",
@@ -2524,11 +2753,11 @@ var init_AuditLog = __esm({
2524
2753
  },
2525
2754
  sx: { minWidth: 150 }
2526
2755
  },
2527
- /* @__PURE__ */ React7.createElement(MenuItem3, { value: "" }, "All tables"),
2528
- /* @__PURE__ */ React7.createElement(MenuItem3, { value: "LiteLLM_VerificationToken" }, "Key"),
2529
- /* @__PURE__ */ React7.createElement(MenuItem3, { value: "LiteLLM_TeamTable" }, "Team"),
2530
- /* @__PURE__ */ React7.createElement(MenuItem3, { value: "LiteLLM_UserTable" }, "User")
2531
- ), /* @__PURE__ */ React7.createElement(
2756
+ /* @__PURE__ */ React8.createElement(MenuItem4, { value: "" }, "All tables"),
2757
+ /* @__PURE__ */ React8.createElement(MenuItem4, { value: "LiteLLM_VerificationToken" }, "Key"),
2758
+ /* @__PURE__ */ React8.createElement(MenuItem4, { value: "LiteLLM_TeamTable" }, "Team"),
2759
+ /* @__PURE__ */ React8.createElement(MenuItem4, { value: "LiteLLM_UserTable" }, "User")
2760
+ ), /* @__PURE__ */ React8.createElement(
2532
2761
  TextField4,
2533
2762
  {
2534
2763
  size: "small",
@@ -2542,9 +2771,9 @@ var init_AuditLog = __esm({
2542
2771
  }
2543
2772
  ))
2544
2773
  },
2545
- error && /* @__PURE__ */ React7.createElement(Box7, { px: 2.5, pb: 2 }, /* @__PURE__ */ React7.createElement(Alert2, { severity: "error" }, error.message)),
2546
- /* @__PURE__ */ React7.createElement(TableContainer4, null, /* @__PURE__ */ React7.createElement(Table4, { size: "small", sx: dataTableSx }, /* @__PURE__ */ React7.createElement(TableHead4, null, /* @__PURE__ */ React7.createElement(TableRow4, null, /* @__PURE__ */ React7.createElement(TableCell4, { sx: { width: 40 } }), /* @__PURE__ */ React7.createElement(TableCell4, null, "Time"), /* @__PURE__ */ React7.createElement(TableCell4, null, "Action"), /* @__PURE__ */ React7.createElement(TableCell4, null, "Table"), /* @__PURE__ */ React7.createElement(TableCell4, null, "Object ID"), /* @__PURE__ */ React7.createElement(TableCell4, null, "Changed By"))), /* @__PURE__ */ React7.createElement(TableBody4, null, renderAuditLogBody(loading, entries)))),
2547
- /* @__PURE__ */ React7.createElement(
2774
+ error && /* @__PURE__ */ React8.createElement(Box8, { px: 2.5, pb: 2 }, /* @__PURE__ */ React8.createElement(Alert3, { severity: "error" }, error.message)),
2775
+ /* @__PURE__ */ React8.createElement(TableContainer5, null, /* @__PURE__ */ React8.createElement(Table5, { size: "small", sx: dataTableSx }, /* @__PURE__ */ React8.createElement(TableHead5, null, /* @__PURE__ */ React8.createElement(TableRow5, null, /* @__PURE__ */ React8.createElement(TableCell5, { sx: { width: 40 } }), /* @__PURE__ */ React8.createElement(TableCell5, null, "Time"), /* @__PURE__ */ React8.createElement(TableCell5, null, "Action"), /* @__PURE__ */ React8.createElement(TableCell5, null, "Table"), /* @__PURE__ */ React8.createElement(TableCell5, null, "Object ID"), /* @__PURE__ */ React8.createElement(TableCell5, null, "Changed By"))), /* @__PURE__ */ React8.createElement(TableBody5, null, renderAuditLogBody(loading, entries)))),
2776
+ /* @__PURE__ */ React8.createElement(
2548
2777
  TablePagination,
2549
2778
  {
2550
2779
  component: "div",
@@ -2570,12 +2799,12 @@ var LiteLLMPage_exports = {};
2570
2799
  __export(LiteLLMPage_exports, {
2571
2800
  LiteLLMPage: () => LiteLLMPage
2572
2801
  });
2573
- import React8, { useState as useState6, useCallback as useCallback2, useMemo as useMemo5 } from "react";
2574
- import Box8 from "@mui/material/Box";
2575
- import Snackbar from "@mui/material/Snackbar";
2576
- import Alert3 from "@mui/material/Alert";
2802
+ import React9, { useState as useState7, useCallback as useCallback3, useMemo as useMemo6 } from "react";
2803
+ import Box9 from "@mui/material/Box";
2804
+ import Snackbar2 from "@mui/material/Snackbar";
2805
+ import Alert4 from "@mui/material/Alert";
2577
2806
  import CircularProgress4 from "@mui/material/CircularProgress";
2578
- import Typography8 from "@mui/material/Typography";
2807
+ import Typography9 from "@mui/material/Typography";
2579
2808
  import Paper5 from "@mui/material/Paper";
2580
2809
  import Tabs2 from "@mui/material/Tabs";
2581
2810
  import Tab2 from "@mui/material/Tab";
@@ -2604,17 +2833,18 @@ var init_LiteLLMPage = __esm({
2604
2833
  init_GenerateKeyDialog();
2605
2834
  init_UsageStats();
2606
2835
  init_TeamUsage();
2836
+ init_ModelsTable();
2607
2837
  init_AuditLog();
2608
2838
  init_api();
2609
2839
  PERIOD_LS_KEY2 = "litellm_usage_period";
2610
2840
  LiteLLMPage = () => {
2611
2841
  const api = useApi(liteLlmApiRef);
2612
- const [dateRange, setDateRange] = useState6(initDateRange);
2613
- const [activeTab, setActiveTab] = useState6("overview");
2614
- const [snackbar, setSnackbar] = useState6(null);
2615
- const [generateDialogOpen, setGenerateDialogOpen] = useState6(false);
2616
- const [teamUsageCache, setTeamUsageCache] = useState6({});
2617
- const [teamUsageLoading, setTeamUsageLoading] = useState6({});
2842
+ const [dateRange, setDateRange] = useState7(initDateRange);
2843
+ const [activeTab, setActiveTab] = useState7("overview");
2844
+ const [snackbar, setSnackbar] = useState7(null);
2845
+ const [generateDialogOpen, setGenerateDialogOpen] = useState7(false);
2846
+ const [teamUsageCache, setTeamUsageCache] = useState7({});
2847
+ const [teamUsageLoading, setTeamUsageLoading] = useState7({});
2618
2848
  const { value: userInfo, loading: userLoading, error: userError } = useAsync2(
2619
2849
  () => api.getUserInfo(),
2620
2850
  [api]
@@ -2639,7 +2869,7 @@ var init_LiteLLMPage = __esm({
2639
2869
  [api]
2640
2870
  );
2641
2871
  const { value: liteLlmConfig } = useAsync2(() => api.getConfig(), [api]);
2642
- const teams = useMemo5(() => {
2872
+ const teams = useMemo6(() => {
2643
2873
  if (!allTeams?.length) return [];
2644
2874
  if (!userInfo) return allTeams;
2645
2875
  const userId = userInfo.user_id;
@@ -2656,12 +2886,12 @@ var init_LiteLLMPage = __esm({
2656
2886
  const endDate = dateRange.end.toISOString().split("T")[0];
2657
2887
  return api.getUsage(startDate, endDate);
2658
2888
  }, [api, dateRange]);
2659
- const handleDateRangeChange = useCallback2((range) => {
2889
+ const handleDateRangeChange = useCallback3((range) => {
2660
2890
  setDateRange(range);
2661
2891
  setTeamUsageCache({});
2662
2892
  setTeamUsageLoading({});
2663
2893
  }, [setDateRange]);
2664
- const loadTeamUsage = useCallback2(async (teamId) => {
2894
+ const loadTeamUsage = useCallback3(async (teamId) => {
2665
2895
  if (teamUsageCache[teamId] !== void 0 || teamUsageLoading[teamId]) return;
2666
2896
  setTeamUsageLoading((prev) => ({ ...prev, [teamId]: true }));
2667
2897
  try {
@@ -2675,7 +2905,7 @@ var init_LiteLLMPage = __esm({
2675
2905
  setTeamUsageLoading((prev) => ({ ...prev, [teamId]: false }));
2676
2906
  }
2677
2907
  }, [api, dateRange, teamUsageCache, teamUsageLoading]);
2678
- const allowedModels = useMemo5(() => {
2908
+ const allowedModels = useMemo6(() => {
2679
2909
  if (!allModels?.length) return [];
2680
2910
  const userModels = userInfo?.models;
2681
2911
  const teamModels = teams?.flatMap((t) => t.models ?? []);
@@ -2688,7 +2918,7 @@ var init_LiteLLMPage = __esm({
2688
2918
  ]);
2689
2919
  return allModels.filter((m) => allowed.has(m.model_name));
2690
2920
  }, [allModels, userInfo, teams]);
2691
- const handleGenerateKey = useCallback2(
2921
+ const handleGenerateKey = useCallback3(
2692
2922
  async (request) => {
2693
2923
  try {
2694
2924
  const response = await api.generateKey(request);
@@ -2702,7 +2932,7 @@ var init_LiteLLMPage = __esm({
2702
2932
  },
2703
2933
  [api, refreshKeys]
2704
2934
  );
2705
- const handleUpdateKey = useCallback2(
2935
+ const handleUpdateKey = useCallback3(
2706
2936
  async (keyId, request) => {
2707
2937
  try {
2708
2938
  await api.updateKey(keyId, request);
@@ -2715,7 +2945,7 @@ var init_LiteLLMPage = __esm({
2715
2945
  },
2716
2946
  [api, refreshKeys]
2717
2947
  );
2718
- const handleBlockKey = useCallback2(
2948
+ const handleBlockKey = useCallback3(
2719
2949
  async (keyId) => {
2720
2950
  try {
2721
2951
  await api.blockKey(keyId);
@@ -2727,7 +2957,7 @@ var init_LiteLLMPage = __esm({
2727
2957
  },
2728
2958
  [api, refreshKeys]
2729
2959
  );
2730
- const handleUnblockKey = useCallback2(
2960
+ const handleUnblockKey = useCallback3(
2731
2961
  async (keyId) => {
2732
2962
  try {
2733
2963
  await api.unblockKey(keyId);
@@ -2739,7 +2969,7 @@ var init_LiteLLMPage = __esm({
2739
2969
  },
2740
2970
  [api, refreshKeys]
2741
2971
  );
2742
- const handleResetKeySpend = useCallback2(
2972
+ const handleResetKeySpend = useCallback3(
2743
2973
  async (keyId) => {
2744
2974
  try {
2745
2975
  await api.resetKeySpend(keyId);
@@ -2751,7 +2981,7 @@ var init_LiteLLMPage = __esm({
2751
2981
  },
2752
2982
  [api, refreshKeys]
2753
2983
  );
2754
- const handleDeleteKey = useCallback2(
2984
+ const handleDeleteKey = useCallback3(
2755
2985
  async (keyId) => {
2756
2986
  try {
2757
2987
  await api.deleteKey(keyId);
@@ -2768,7 +2998,7 @@ var init_LiteLLMPage = __esm({
2768
2998
  },
2769
2999
  [api, refreshKeys]
2770
3000
  );
2771
- const handlePruneExpiredKeys = useCallback2(
3001
+ const handlePruneExpiredKeys = useCallback3(
2772
3002
  async () => {
2773
3003
  try {
2774
3004
  const result = await api.pruneExpiredKeys();
@@ -2783,14 +3013,14 @@ var init_LiteLLMPage = __esm({
2783
3013
  );
2784
3014
  const isInitialLoading = userLoading && !userInfo;
2785
3015
  if (isInitialLoading) {
2786
- return /* @__PURE__ */ React8.createElement(Box8, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "50vh" }, /* @__PURE__ */ React8.createElement(CircularProgress4, null));
3016
+ return /* @__PURE__ */ React9.createElement(Box9, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "50vh" }, /* @__PURE__ */ React9.createElement(CircularProgress4, null));
2787
3017
  }
2788
3018
  if (userError || !userInfo) {
2789
3019
  const isProvisioningEnabled = userError?.body?.provisioning === true;
2790
3020
  const hint = userError?.body?.hint;
2791
- return /* @__PURE__ */ React8.createElement(Box8, { p: 3 }, /* @__PURE__ */ React8.createElement(Paper5, { sx: { p: 3 } }, /* @__PURE__ */ React8.createElement(Typography8, { variant: "h6", gutterBottom: true }, "Account not provisioned"), /* @__PURE__ */ React8.createElement(Typography8, { color: "text.secondary", paragraph: true }, "Your Backstage account is not linked to a LiteLLM user."), hint ? /* @__PURE__ */ React8.createElement(Typography8, { variant: "body2", color: "text.secondary" }, hint) : /* @__PURE__ */ React8.createElement(Typography8, { variant: "body2", color: "text.secondary" }, isProvisioningEnabled ? "Auto-provisioning is enabled but failed. Check the backend logs." : "Set litellm.provisioning.enabled: true in app-config.yaml to enable auto-provisioning, or ask your administrator to create the account manually.")));
3021
+ return /* @__PURE__ */ React9.createElement(Box9, { p: 3 }, /* @__PURE__ */ React9.createElement(Paper5, { sx: { p: 3 } }, /* @__PURE__ */ React9.createElement(Typography9, { variant: "h6", gutterBottom: true }, "Account not provisioned"), /* @__PURE__ */ React9.createElement(Typography9, { color: "text.secondary", paragraph: true }, "Your Backstage account is not linked to a LiteLLM user."), hint ? /* @__PURE__ */ React9.createElement(Typography9, { variant: "body2", color: "text.secondary" }, hint) : /* @__PURE__ */ React9.createElement(Typography9, { variant: "body2", color: "text.secondary" }, isProvisioningEnabled ? "Auto-provisioning is enabled but failed. Check the backend logs." : "Set litellm.provisioning.enabled: true in app-config.yaml to enable auto-provisioning, or ask your administrator to create the account manually.")));
2792
3022
  }
2793
- const pageTabs = /* @__PURE__ */ React8.createElement(
3023
+ const pageTabs = /* @__PURE__ */ React9.createElement(
2794
3024
  Tabs2,
2795
3025
  {
2796
3026
  value: activeTab,
@@ -2803,12 +3033,13 @@ var init_LiteLLMPage = __esm({
2803
3033
  '& [class*="MuiTab-root"]': { minHeight: 44, textTransform: "none", fontSize: 14 }
2804
3034
  }
2805
3035
  },
2806
- /* @__PURE__ */ React8.createElement(Tab2, { label: "Overview", value: "overview" }),
2807
- /* @__PURE__ */ React8.createElement(Tab2, { label: "Keys", value: "keys" }),
2808
- /* @__PURE__ */ React8.createElement(Tab2, { label: "Teams", value: "teams" }),
2809
- userInfo.can_view_audit && /* @__PURE__ */ React8.createElement(Tab2, { label: "Audit Log", value: "audit" })
3036
+ /* @__PURE__ */ React9.createElement(Tab2, { label: "Overview", value: "overview" }),
3037
+ /* @__PURE__ */ React9.createElement(Tab2, { label: "Keys", value: "keys" }),
3038
+ /* @__PURE__ */ React9.createElement(Tab2, { label: "Teams", value: "teams" }),
3039
+ /* @__PURE__ */ React9.createElement(Tab2, { label: "Models", value: "models" }),
3040
+ userInfo.can_view_audit && /* @__PURE__ */ React9.createElement(Tab2, { label: "Audit Log", value: "audit" })
2810
3041
  );
2811
- return /* @__PURE__ */ React8.createElement(Box8, { sx: { p: 3, display: "flex", flexDirection: "column", gap: 2 } }, /* @__PURE__ */ React8.createElement(
3042
+ return /* @__PURE__ */ React9.createElement(Box9, { sx: { p: 3, display: "flex", flexDirection: "column", gap: 2 } }, /* @__PURE__ */ React9.createElement(
2812
3043
  DashboardHeader,
2813
3044
  {
2814
3045
  userInfo,
@@ -2818,7 +3049,7 @@ var init_LiteLLMPage = __esm({
2818
3049
  onGenerateKeyClick: () => setGenerateDialogOpen(true),
2819
3050
  tabs: pageTabs
2820
3051
  }
2821
- ), activeTab === "overview" && /* @__PURE__ */ React8.createElement(
3052
+ ), activeTab === "overview" && /* @__PURE__ */ React9.createElement(
2822
3053
  UsageStats,
2823
3054
  {
2824
3055
  usage: usage ?? null,
@@ -2828,7 +3059,7 @@ var init_LiteLLMPage = __esm({
2828
3059
  loading: usageLoading,
2829
3060
  userInfo
2830
3061
  }
2831
- ), activeTab === "keys" && /* @__PURE__ */ React8.createElement(
3062
+ ), activeTab === "keys" && /* @__PURE__ */ React9.createElement(
2832
3063
  KeysTable,
2833
3064
  {
2834
3065
  keys: keys ?? [],
@@ -2842,7 +3073,7 @@ var init_LiteLLMPage = __esm({
2842
3073
  onDeleteKey: handleDeleteKey,
2843
3074
  onPruneExpiredKeys: handlePruneExpiredKeys
2844
3075
  }
2845
- ), activeTab === "teams" && /* @__PURE__ */ React8.createElement(
3076
+ ), activeTab === "teams" && /* @__PURE__ */ React9.createElement(
2846
3077
  TeamUsage,
2847
3078
  {
2848
3079
  teams: teams ?? [],
@@ -2853,7 +3084,14 @@ var init_LiteLLMPage = __esm({
2853
3084
  },
2854
3085
  getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false
2855
3086
  }
2856
- ), activeTab === "audit" && userInfo.can_view_audit && /* @__PURE__ */ React8.createElement(AuditLog, { api }), /* @__PURE__ */ React8.createElement(
3087
+ ), activeTab === "models" && /* @__PURE__ */ React9.createElement(
3088
+ ModelsTable,
3089
+ {
3090
+ allModels: allModels ?? [],
3091
+ teams: teams ?? [],
3092
+ loading: modelsLoading
3093
+ }
3094
+ ), activeTab === "audit" && userInfo.can_view_audit && /* @__PURE__ */ React9.createElement(AuditLog, { api }), /* @__PURE__ */ React9.createElement(
2857
3095
  GenerateKeyDialog,
2858
3096
  {
2859
3097
  open: generateDialogOpen,
@@ -2866,15 +3104,15 @@ var init_LiteLLMPage = __esm({
2866
3104
  onGenerateKey: handleGenerateKey,
2867
3105
  onGetConfig: () => api.getConfig()
2868
3106
  }
2869
- ), /* @__PURE__ */ React8.createElement(
2870
- Snackbar,
3107
+ ), /* @__PURE__ */ React9.createElement(
3108
+ Snackbar2,
2871
3109
  {
2872
3110
  open: !!snackbar,
2873
3111
  autoHideDuration: 5e3,
2874
3112
  onClose: () => setSnackbar(null),
2875
3113
  anchorOrigin: { vertical: "bottom", horizontal: "right" }
2876
3114
  },
2877
- snackbar ? /* @__PURE__ */ React8.createElement(Alert3, { severity: snackbar.severity, onClose: () => setSnackbar(null) }, snackbar.message) : void 0
3115
+ snackbar ? /* @__PURE__ */ React9.createElement(Alert4, { severity: snackbar.severity, onClose: () => setSnackbar(null) }, snackbar.message) : void 0
2878
3116
  ));
2879
3117
  };
2880
3118
  }
@@ -2882,7 +3120,7 @@ var init_LiteLLMPage = __esm({
2882
3120
 
2883
3121
  // src/plugin.tsx
2884
3122
  init_api();
2885
- import React9 from "react";
3123
+ import React10 from "react";
2886
3124
  import { TrendingUp as TrendingUpIcon } from "@mui/icons-material";
2887
3125
  import {
2888
3126
  createFrontendPlugin,
@@ -2901,10 +3139,10 @@ var liteLlmPage = PageBlueprint.make({
2901
3139
  params: {
2902
3140
  path: "/litellm",
2903
3141
  title: "LiteLLM",
2904
- icon: /* @__PURE__ */ React9.createElement(TrendingUpIcon, null),
3142
+ icon: /* @__PURE__ */ React10.createElement(TrendingUpIcon, null),
2905
3143
  loader: async () => {
2906
3144
  const { LiteLLMPage: LiteLLMPage2 } = await Promise.resolve().then(() => (init_LiteLLMPage(), LiteLLMPage_exports));
2907
- return /* @__PURE__ */ React9.createElement(LiteLLMPage2, null);
3145
+ return /* @__PURE__ */ React10.createElement(LiteLLMPage2, null);
2908
3146
  }
2909
3147
  }
2910
3148
  });
@@ -2923,16 +3161,16 @@ init_TeamUsage();
2923
3161
  // src/components/LiteLLMHomeWidget.tsx
2924
3162
  init_api();
2925
3163
  init_format();
2926
- import React10, { useState as useState7, useEffect as useEffect2 } from "react";
3164
+ import React11, { useState as useState8, useEffect as useEffect2 } from "react";
2927
3165
  import Paper6 from "@mui/material/Paper";
2928
- import Box9 from "@mui/material/Box";
2929
- import Typography9 from "@mui/material/Typography";
2930
- import FormControl from "@mui/material/FormControl";
2931
- import Select from "@mui/material/Select";
2932
- import MenuItem4 from "@mui/material/MenuItem";
3166
+ import Box10 from "@mui/material/Box";
3167
+ import Typography10 from "@mui/material/Typography";
3168
+ import FormControl2 from "@mui/material/FormControl";
3169
+ import Select2 from "@mui/material/Select";
3170
+ import MenuItem5 from "@mui/material/MenuItem";
2933
3171
  import Grid2 from "@mui/material/Grid";
2934
3172
  import CircularProgress5 from "@mui/material/CircularProgress";
2935
- import Alert4 from "@mui/material/Alert";
3173
+ import Alert5 from "@mui/material/Alert";
2936
3174
  import { AreaChart as AreaChart3, Area as Area3, ResponsiveContainer as ResponsiveContainer3 } from "recharts";
2937
3175
  import { useApi as useApi2 } from "@backstage/core-plugin-api";
2938
3176
  function presetToDateRange(preset) {
@@ -2947,18 +3185,18 @@ function presetToDateRange(preset) {
2947
3185
  }
2948
3186
  return { start, end };
2949
3187
  }
2950
- var Kpi = ({ label, value }) => /* @__PURE__ */ React10.createElement(Box9, null, /* @__PURE__ */ React10.createElement(Typography9, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ React10.createElement(Typography9, { variant: "subtitle1", fontWeight: 600 }, value));
3188
+ var Kpi = ({ label, value }) => /* @__PURE__ */ React11.createElement(Box10, null, /* @__PURE__ */ React11.createElement(Typography10, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ React11.createElement(Typography10, { variant: "subtitle1", fontWeight: 600 }, value));
2951
3189
  var LiteLLMHomeWidget = ({
2952
3190
  defaultPeriod = "7d",
2953
3191
  title = "LiteLLM Usage"
2954
3192
  }) => {
2955
3193
  const api = useApi2(liteLlmApiRef);
2956
- const [period, setPeriod] = useState7(defaultPeriod);
2957
- const [loading, setLoading] = useState7(true);
2958
- const [usageError, setUsageError] = useState7(null);
2959
- const [keysError, setKeysError] = useState7(null);
2960
- const [usage, setUsage] = useState7(null);
2961
- const [keys, setKeys] = useState7([]);
3194
+ const [period, setPeriod] = useState8(defaultPeriod);
3195
+ const [loading, setLoading] = useState8(true);
3196
+ const [usageError, setUsageError] = useState8(null);
3197
+ const [keysError, setKeysError] = useState8(null);
3198
+ const [usage, setUsage] = useState8(null);
3199
+ const [keys, setKeys] = useState8([]);
2962
3200
  useEffect2(() => {
2963
3201
  let cancelled = false;
2964
3202
  setLoading(true);
@@ -2996,17 +3234,17 @@ var LiteLLMHomeWidget = ({
2996
3234
  spend: d.spend
2997
3235
  }));
2998
3236
  const hasSparkline = dailyData.length > 0;
2999
- return /* @__PURE__ */ React10.createElement(Paper6, { sx: { p: 2 } }, /* @__PURE__ */ React10.createElement(Box9, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1.5 }, /* @__PURE__ */ React10.createElement(Typography9, { variant: "h6" }, title), /* @__PURE__ */ React10.createElement(FormControl, { size: "small", sx: { minWidth: 90 } }, /* @__PURE__ */ React10.createElement(
3000
- Select,
3237
+ return /* @__PURE__ */ React11.createElement(Paper6, { sx: { p: 2 } }, /* @__PURE__ */ React11.createElement(Box10, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1.5 }, /* @__PURE__ */ React11.createElement(Typography10, { variant: "h6" }, title), /* @__PURE__ */ React11.createElement(FormControl2, { size: "small", sx: { minWidth: 90 } }, /* @__PURE__ */ React11.createElement(
3238
+ Select2,
3001
3239
  {
3002
3240
  value: period,
3003
3241
  onChange: (e) => setPeriod(e.target.value),
3004
3242
  displayEmpty: true
3005
3243
  },
3006
- /* @__PURE__ */ React10.createElement(MenuItem4, { value: "today" }, "Today"),
3007
- /* @__PURE__ */ React10.createElement(MenuItem4, { value: "7d" }, "7d"),
3008
- /* @__PURE__ */ React10.createElement(MenuItem4, { value: "30d" }, "30d")
3009
- ))), loading && /* @__PURE__ */ React10.createElement(Box9, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ React10.createElement(CircularProgress5, { size: 32 })), !loading && totalFailure && /* @__PURE__ */ React10.createElement(Alert4, { severity: "error", sx: { mt: 1 } }, usageError ?? "Failed to load usage data"), !loading && !totalFailure && /* @__PURE__ */ React10.createElement(React10.Fragment, null, partialFailure && /* @__PURE__ */ React10.createElement(Alert4, { severity: "warning", sx: { mt: 1, mb: 1 } }, usageError ? `Usage data unavailable (${usageError}).` : "", keysError ? ` Key list unavailable (${keysError}).` : "", " Showing what loaded."), /* @__PURE__ */ React10.createElement(Grid2, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ React10.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React10.createElement(Kpi, { label: "USD Spent", value: fmtUsd(usage?.total_spend ?? 0) })), /* @__PURE__ */ React10.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React10.createElement(Kpi, { label: "Tokens In", value: fmtInt(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ React10.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React10.createElement(Kpi, { label: "Tokens Out", value: fmtInt(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ React10.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React10.createElement(Kpi, { label: "Keys", value: fmtInt(keys.length) }))), hasSparkline && /* @__PURE__ */ React10.createElement(Box9, { height: 120 }, /* @__PURE__ */ React10.createElement(ResponsiveContainer3, { width: "100%", height: "100%" }, /* @__PURE__ */ React10.createElement(AreaChart3, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 } }, /* @__PURE__ */ React10.createElement(
3244
+ /* @__PURE__ */ React11.createElement(MenuItem5, { value: "today" }, "Today"),
3245
+ /* @__PURE__ */ React11.createElement(MenuItem5, { value: "7d" }, "7d"),
3246
+ /* @__PURE__ */ React11.createElement(MenuItem5, { value: "30d" }, "30d")
3247
+ ))), loading && /* @__PURE__ */ React11.createElement(Box10, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ React11.createElement(CircularProgress5, { size: 32 })), !loading && totalFailure && /* @__PURE__ */ React11.createElement(Alert5, { severity: "error", sx: { mt: 1 } }, usageError ?? "Failed to load usage data"), !loading && !totalFailure && /* @__PURE__ */ React11.createElement(React11.Fragment, null, partialFailure && /* @__PURE__ */ React11.createElement(Alert5, { severity: "warning", sx: { mt: 1, mb: 1 } }, usageError ? `Usage data unavailable (${usageError}).` : "", keysError ? ` Key list unavailable (${keysError}).` : "", " Showing what loaded."), /* @__PURE__ */ React11.createElement(Grid2, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ React11.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React11.createElement(Kpi, { label: "USD Spent", value: fmtUsd(usage?.total_spend ?? 0) })), /* @__PURE__ */ React11.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React11.createElement(Kpi, { label: "Tokens In", value: fmtInt(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ React11.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React11.createElement(Kpi, { label: "Tokens Out", value: fmtInt(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ React11.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React11.createElement(Kpi, { label: "Keys", value: fmtInt(keys.length) }))), hasSparkline && /* @__PURE__ */ React11.createElement(Box10, { height: 120 }, /* @__PURE__ */ React11.createElement(ResponsiveContainer3, { width: "100%", height: "100%" }, /* @__PURE__ */ React11.createElement(AreaChart3, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 } }, /* @__PURE__ */ React11.createElement(
3010
3248
  Area3,
3011
3249
  {
3012
3250
  type: "monotone",