@acarmisc/backstage-plugin-litellm 0.37.0 → 0.38.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
@@ -295,6 +295,17 @@ var init_api = __esm({
295
295
  end_date: endDate
296
296
  });
297
297
  }
298
+ async getSpendLogs(params) {
299
+ const strParams = {
300
+ start_date: params.startDate,
301
+ end_date: params.endDate
302
+ };
303
+ if (params.userId) strParams.userId = params.userId;
304
+ if (params.teamId) strParams.teamId = params.teamId;
305
+ if (params.page !== void 0) strParams.page = String(params.page);
306
+ if (params.pageSize !== void 0) strParams.page_size = String(params.pageSize);
307
+ return this.get("/insights/spend-logs", strParams);
308
+ }
298
309
  async getConfig() {
299
310
  return this.get("/config");
300
311
  }
@@ -5664,271 +5675,95 @@ var init_ModelsTable = __esm({
5664
5675
  }
5665
5676
  });
5666
5677
 
5667
- // src/components/AuditLog.tsx
5668
- import { useMemo as useMemo8, useState as useState11 } from "react";
5669
- import Box16 from "@mui/material/Box";
5670
- import Typography14 from "@mui/material/Typography";
5671
- import Table6 from "@mui/material/Table";
5672
- import TableBody6 from "@mui/material/TableBody";
5673
- import TableCell6 from "@mui/material/TableCell";
5674
- import TableContainer6 from "@mui/material/TableContainer";
5675
- import TableHead6 from "@mui/material/TableHead";
5676
- import TableRow6 from "@mui/material/TableRow";
5677
- import TablePagination from "@mui/material/TablePagination";
5678
- import TextField6 from "@mui/material/TextField";
5679
- import MenuItem5 from "@mui/material/MenuItem";
5680
- import Skeleton6 from "@mui/material/Skeleton";
5681
- import Collapse3 from "@mui/material/Collapse";
5682
- import IconButton7 from "@mui/material/IconButton";
5683
- import Alert5 from "@mui/material/Alert";
5684
- import { alpha as alpha12 } from "@mui/material/styles";
5685
- import { KeyboardArrowDown } from "@mui/icons-material";
5686
- import { useAsync } from "react-use";
5687
- import { Fragment as Fragment9, jsx as jsx17, jsxs as jsxs16 } from "react/jsx-runtime";
5688
- function actionTone(action) {
5689
- if (!action) return "neutral";
5690
- for (const [key, tone] of Object.entries(ACTION_TONES)) {
5691
- if (action.toLowerCase().includes(key)) return tone;
5692
- }
5693
- return "info";
5694
- }
5695
- function prettyTableName(name) {
5696
- if (!name) return "\u2014";
5697
- return TABLE_LABELS[name] ?? name;
5698
- }
5699
- function formatDateTime(iso) {
5700
- try {
5701
- return new Date(iso).toLocaleString();
5702
- } catch {
5703
- return iso;
5704
- }
5705
- }
5706
- function renderAuditLogBody(loading, entries) {
5707
- if (loading) {
5708
- return /* @__PURE__ */ jsx17(TableRow6, { children: /* @__PURE__ */ jsx17(TableCell6, { colSpan: 6, sx: { py: 2 }, children: /* @__PURE__ */ jsx17(Skeleton6, { variant: "rounded", height: 160 }) }) });
5709
- }
5710
- if (entries.length === 0) {
5711
- return /* @__PURE__ */ jsx17(TableRow6, { children: /* @__PURE__ */ jsx17(TableCell6, { colSpan: 6, children: /* @__PURE__ */ jsx17(
5712
- EmptyState,
5713
- {
5714
- message: "No audit events found",
5715
- hint: "Try widening the filters or a different time window."
5716
- }
5717
- ) }) });
5718
- }
5719
- return entries.map((entry) => /* @__PURE__ */ jsx17(DetailRow, { entry }, entry.id));
5720
- }
5721
- var ACTION_TONES, TABLE_LABELS, DetailRow, AuditLog;
5722
- var init_AuditLog = __esm({
5723
- "src/components/AuditLog.tsx"() {
5678
+ // src/insights.ts
5679
+ var costPer1k, summarizeUsage, scorePractices, modelOpportunities;
5680
+ var init_insights = __esm({
5681
+ "src/insights.ts"() {
5724
5682
  "use strict";
5725
- init_ui();
5726
- ACTION_TONES = {
5727
- created: "success",
5728
- deleted: "danger",
5729
- updated: "warning",
5730
- blocked: "danger",
5731
- unblocked: "success"
5683
+ costPer1k = (model, spend, tokens, models) => {
5684
+ const info = models?.find((m) => m.model_name === model);
5685
+ const listed = info?.input_cost_per_token != null && info?.output_cost_per_token != null ? (info.input_cost_per_token + info.output_cost_per_token) / 2 * 1e3 : null;
5686
+ if (listed !== null) return listed;
5687
+ return tokens > 0 ? spend / tokens * 1e3 : null;
5732
5688
  };
5733
- TABLE_LABELS = {
5734
- LiteLLM_VerificationToken: "Key",
5735
- LiteLLM_TeamTable: "Team",
5736
- LiteLLM_TeamMembership: "Team member",
5737
- LiteLLM_ObjectPermissionTable: "Team access (KB / MCP)",
5738
- LiteLLM_UserTable: "User"
5689
+ summarizeUsage = (usage, models, premiumThresholdPer1k = 0.01) => {
5690
+ if (!usage) {
5691
+ return {
5692
+ costPer1kTokens: null,
5693
+ costPerRequest: null,
5694
+ outputInputRatio: null,
5695
+ retryWasteUsd: 0,
5696
+ retryWasteShare: 0,
5697
+ successRate: null,
5698
+ premiumShare: 0,
5699
+ premiumSpend: 0,
5700
+ topModelConcentration: 0,
5701
+ projectedMonthSpend: null,
5702
+ burnMultiple: null
5703
+ };
5704
+ }
5705
+ const {
5706
+ total_spend: spend,
5707
+ total_tokens: tokens,
5708
+ prompt_tokens: prompt,
5709
+ completion_tokens: completion,
5710
+ api_requests: reqs,
5711
+ successful_requests: ok,
5712
+ failed_requests: failed,
5713
+ usage_by_model: byModel
5714
+ } = usage;
5715
+ const wasteShare = reqs > 0 ? failed / reqs : 0;
5716
+ const entries = Object.entries(byModel ?? {});
5717
+ const topSpend = entries.reduce((m, [, d]) => Math.max(m, d.total_spend), 0);
5718
+ const premiumSpend = entries.filter(([name, d]) => (costPer1k(name, d.total_spend, d.total_tokens, models) ?? 0) > premiumThresholdPer1k).reduce((s, [, d]) => s + d.total_spend, 0);
5719
+ const days = usage.daily_usage?.length ?? 0;
5720
+ const projected = days > 0 ? spend / days * 30 : null;
5721
+ return {
5722
+ costPer1kTokens: tokens > 0 ? spend / tokens * 1e3 : null,
5723
+ costPerRequest: reqs > 0 ? spend / reqs : null,
5724
+ outputInputRatio: prompt > 0 ? completion / prompt : null,
5725
+ retryWasteUsd: spend * wasteShare,
5726
+ retryWasteShare: wasteShare,
5727
+ successRate: reqs > 0 ? ok / reqs : null,
5728
+ premiumShare: spend > 0 ? premiumSpend / spend : 0,
5729
+ premiumSpend,
5730
+ topModelConcentration: spend > 0 ? topSpend / spend : 0,
5731
+ projectedMonthSpend: projected,
5732
+ burnMultiple: projected !== null && spend > 0 ? projected / spend : null
5733
+ };
5739
5734
  };
5740
- DetailRow = ({ entry }) => {
5741
- const [open, setOpen] = useState11(false);
5742
- const hasDetail = entry.before_value || entry.updated_values;
5743
- return /* @__PURE__ */ jsxs16(Fragment9, { children: [
5744
- /* @__PURE__ */ jsxs16(TableRow6, { children: [
5745
- /* @__PURE__ */ jsx17(TableCell6, { sx: { width: 40, pr: 0 }, children: hasDetail && /* @__PURE__ */ jsx17(
5746
- IconButton7,
5747
- {
5748
- size: "small",
5749
- onClick: () => setOpen((o) => !o),
5750
- "aria-label": open ? "Hide changes" : "Show changes",
5751
- "aria-expanded": open,
5752
- sx: (theme) => ({
5753
- color: theme.palette.text.secondary,
5754
- transform: open ? "rotate(180deg)" : "none",
5755
- transition: theme.transitions.create("transform")
5756
- }),
5757
- children: /* @__PURE__ */ jsx17(KeyboardArrowDown, { fontSize: "small" })
5758
- }
5759
- ) }),
5760
- /* @__PURE__ */ jsx17(TableCell6, { sx: { whiteSpace: "nowrap" }, children: /* @__PURE__ */ jsx17(Typography14, { variant: "body2", color: "text.secondary", children: formatDateTime(entry.updated_at) }) }),
5761
- /* @__PURE__ */ jsx17(TableCell6, { children: entry.action && /* @__PURE__ */ jsx17(StatusPill, { label: entry.action, tone: actionTone(entry.action) }) }),
5762
- /* @__PURE__ */ jsx17(TableCell6, { children: /* @__PURE__ */ jsx17(Typography14, { variant: "body2", children: prettyTableName(entry.table_name) }) }),
5763
- /* @__PURE__ */ jsx17(TableCell6, { children: /* @__PURE__ */ jsx17(
5764
- Typography14,
5765
- {
5766
- variant: "body2",
5767
- component: "code",
5768
- color: "text.secondary",
5769
- title: entry.object_id ?? void 0,
5770
- sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 12 },
5771
- children: entry.object_id ? entry.object_id.slice(0, 20) + (entry.object_id.length > 20 ? "\u2026" : "") : "\u2014"
5772
- }
5773
- ) }),
5774
- /* @__PURE__ */ jsx17(TableCell6, { children: entry.changed_by ?? "\u2014" })
5775
- ] }),
5776
- hasDetail && /* @__PURE__ */ jsx17(TableRow6, { sx: { "&&:hover": { bgcolor: "transparent" } }, children: /* @__PURE__ */ jsx17(TableCell6, { colSpan: 6, sx: { "&&": { py: 0, border: 0 } }, children: /* @__PURE__ */ jsx17(Collapse3, { in: open, unmountOnExit: true, children: /* @__PURE__ */ jsxs16(
5777
- Box16,
5778
- {
5779
- display: "flex",
5780
- gap: 2,
5781
- flexWrap: "wrap",
5782
- sx: (theme) => ({
5783
- p: 2,
5784
- mb: 1.5,
5785
- borderRadius: 1.5,
5786
- bgcolor: alpha12(theme.palette.text.primary, theme.palette.mode === "dark" ? 0.05 : 0.03)
5787
- }),
5788
- children: [
5789
- entry.before_value && /* @__PURE__ */ jsxs16(Box16, { flex: 1, minWidth: 200, children: [
5790
- /* @__PURE__ */ jsx17(
5791
- Typography14,
5792
- {
5793
- variant: "caption",
5794
- color: "text.secondary",
5795
- display: "block",
5796
- mb: 0.75,
5797
- sx: { fontSize: 10.5, fontWeight: 700, letterSpacing: "0.07em", textTransform: "uppercase" },
5798
- children: "Before"
5799
- }
5800
- ),
5801
- /* @__PURE__ */ jsx17(Typography14, { component: "pre", variant: "caption", sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", whiteSpace: "pre-wrap", wordBreak: "break-all", m: 0 }, children: JSON.stringify(entry.before_value, null, 2) })
5802
- ] }),
5803
- entry.updated_values && /* @__PURE__ */ jsxs16(Box16, { flex: 1, minWidth: 200, children: [
5804
- /* @__PURE__ */ jsx17(
5805
- Typography14,
5806
- {
5807
- variant: "caption",
5808
- color: "text.secondary",
5809
- display: "block",
5810
- mb: 0.75,
5811
- sx: { fontSize: 10.5, fontWeight: 700, letterSpacing: "0.07em", textTransform: "uppercase" },
5812
- children: "After"
5813
- }
5814
- ),
5815
- /* @__PURE__ */ jsx17(Typography14, { component: "pre", variant: "caption", sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", whiteSpace: "pre-wrap", wordBreak: "break-all", m: 0 }, children: JSON.stringify(entry.updated_values, null, 2) })
5816
- ] })
5817
- ]
5818
- }
5819
- ) }) }) })
5820
- ] });
5735
+ scorePractices = (summary, keyStats) => {
5736
+ const waste = summary.retryWasteShare <= 0.01 ? 30 : summary.retryWasteShare <= 0.05 ? 20 : summary.retryWasteShare <= 0.1 ? 10 : 0;
5737
+ const premium = summary.premiumShare <= 0.2 ? 25 : summary.premiumShare <= 0.5 ? 15 : summary.premiumShare <= 0.8 ? 5 : 0;
5738
+ const conc = summary.topModelConcentration <= 0.5 ? 20 : summary.topModelConcentration <= 0.8 ? 10 : 0;
5739
+ const bad = keyStats.totalKeys > 0 ? (keyStats.idleKeys + keyStats.uncappedKeys) / keyStats.totalKeys : 0;
5740
+ const hygiene = keyStats.totalKeys === 0 ? 25 : bad === 0 ? 25 : bad <= 0.2 ? 15 : bad <= 0.5 ? 5 : 0;
5741
+ const checks = [
5742
+ { id: "retry-waste", label: "Low retry waste", points: waste, maxPoints: 30, detail: `${(summary.retryWasteShare * 100).toFixed(1)}% of requests failed` },
5743
+ { id: "premium-mix", label: "Balanced model mix", points: premium, maxPoints: 25, detail: `${(summary.premiumShare * 100).toFixed(1)}% of spend on premium models` },
5744
+ { id: "concentration", label: "No single-model lock-in", points: conc, maxPoints: 20, detail: `Top model is ${(summary.topModelConcentration * 100).toFixed(1)}% of spend` },
5745
+ { id: "key-hygiene", label: "Key hygiene", points: hygiene, maxPoints: 25, detail: `${keyStats.idleKeys} idle, ${keyStats.uncappedKeys} uncapped of ${keyStats.totalKeys} keys` }
5746
+ ];
5747
+ return { total: checks.reduce((s, c) => s + c.points, 0), checks };
5821
5748
  };
5822
- AuditLog = ({ api }) => {
5823
- const [page, setPage] = useState11(0);
5824
- const [pageSize, setPageSize] = useState11(25);
5825
- const [filters, setFilters] = useState11({});
5826
- const fetchParams = useMemo8(
5827
- () => ({ page: page + 1, page_size: pageSize, ...filters }),
5828
- [page, pageSize, filters]
5829
- );
5830
- const { value, loading, error } = useAsync(
5831
- () => api.getAuditLogs(fetchParams),
5832
- [api, fetchParams]
5833
- );
5834
- const entries = value?.audit_logs ?? [];
5835
- const total = value?.total ?? 0;
5836
- return /* @__PURE__ */ jsxs16(
5837
- SectionCard,
5838
- {
5839
- title: "Audit Log",
5840
- subtitle: loading ? "Loading\u2026" : `${total.toLocaleString()} event${total === 1 ? "" : "s"}`,
5841
- flush: true,
5842
- actions: /* @__PURE__ */ jsxs16(Fragment9, { children: [
5843
- /* @__PURE__ */ jsxs16(
5844
- TextField6,
5845
- {
5846
- size: "small",
5847
- label: "Action",
5848
- select: true,
5849
- value: filters.action ?? "",
5850
- onChange: (e) => {
5851
- setFilters((f) => ({ ...f, action: e.target.value || void 0 }));
5852
- setPage(0);
5853
- },
5854
- sx: { minWidth: 150 },
5855
- children: [
5856
- /* @__PURE__ */ jsx17(MenuItem5, { value: "", children: "All actions" }),
5857
- /* @__PURE__ */ jsx17(MenuItem5, { value: "created", children: "Created" }),
5858
- /* @__PURE__ */ jsx17(MenuItem5, { value: "updated", children: "Updated" }),
5859
- /* @__PURE__ */ jsx17(MenuItem5, { value: "deleted", children: "Deleted" }),
5860
- /* @__PURE__ */ jsx17(MenuItem5, { value: "blocked", children: "Blocked" })
5861
- ]
5862
- }
5863
- ),
5864
- /* @__PURE__ */ jsxs16(
5865
- TextField6,
5866
- {
5867
- size: "small",
5868
- label: "Table",
5869
- select: true,
5870
- value: filters.table_name ?? "",
5871
- onChange: (e) => {
5872
- setFilters((f) => ({ ...f, table_name: e.target.value || void 0 }));
5873
- setPage(0);
5874
- },
5875
- sx: { minWidth: 150 },
5876
- children: [
5877
- /* @__PURE__ */ jsx17(MenuItem5, { value: "", children: "All tables" }),
5878
- /* @__PURE__ */ jsx17(MenuItem5, { value: "LiteLLM_VerificationToken", children: "Key" }),
5879
- /* @__PURE__ */ jsx17(MenuItem5, { value: "LiteLLM_TeamTable", children: "Team" }),
5880
- /* @__PURE__ */ jsx17(MenuItem5, { value: "LiteLLM_TeamMembership", children: "Team member" }),
5881
- /* @__PURE__ */ jsx17(MenuItem5, { value: "LiteLLM_ObjectPermissionTable", children: "Team access (KB / MCP)" }),
5882
- /* @__PURE__ */ jsx17(MenuItem5, { value: "LiteLLM_UserTable", children: "User" })
5883
- ]
5884
- }
5885
- ),
5886
- /* @__PURE__ */ jsx17(
5887
- TextField6,
5888
- {
5889
- size: "small",
5890
- label: "Changed by",
5891
- value: filters.changed_by ?? "",
5892
- onChange: (e) => {
5893
- setFilters((f) => ({ ...f, changed_by: e.target.value || void 0 }));
5894
- setPage(0);
5895
- },
5896
- sx: { minWidth: 180 }
5897
- }
5898
- )
5899
- ] }),
5900
- children: [
5901
- error && /* @__PURE__ */ jsx17(Box16, { px: 2.5, pb: 2, children: /* @__PURE__ */ jsx17(Alert5, { severity: "error", children: error.message }) }),
5902
- /* @__PURE__ */ jsx17(TableContainer6, { children: /* @__PURE__ */ jsxs16(Table6, { size: "small", sx: dataTableSx, children: [
5903
- /* @__PURE__ */ jsx17(TableHead6, { children: /* @__PURE__ */ jsxs16(TableRow6, { children: [
5904
- /* @__PURE__ */ jsx17(TableCell6, { sx: { width: 40 } }),
5905
- /* @__PURE__ */ jsx17(TableCell6, { children: "Time" }),
5906
- /* @__PURE__ */ jsx17(TableCell6, { children: "Action" }),
5907
- /* @__PURE__ */ jsx17(TableCell6, { children: "Table" }),
5908
- /* @__PURE__ */ jsx17(TableCell6, { children: "Object ID" }),
5909
- /* @__PURE__ */ jsx17(TableCell6, { children: "Changed By" })
5910
- ] }) }),
5911
- /* @__PURE__ */ jsx17(TableBody6, { children: renderAuditLogBody(loading, entries) })
5912
- ] }) }),
5913
- /* @__PURE__ */ jsx17(
5914
- TablePagination,
5915
- {
5916
- component: "div",
5917
- count: total,
5918
- page,
5919
- onPageChange: (_, p) => setPage(p),
5920
- rowsPerPage: pageSize,
5921
- onRowsPerPageChange: (e) => {
5922
- setPageSize(Number(e.target.value));
5923
- setPage(0);
5924
- },
5925
- rowsPerPageOptions: [10, 25, 50],
5926
- sx: (theme) => ({ borderTop: `1px solid ${theme.palette.divider}` })
5927
- }
5928
- )
5929
- ]
5749
+ modelOpportunities = (usage, models, premiumThresholdPer1k = 0.01) => {
5750
+ if (!usage) return [];
5751
+ const entries = Object.entries(usage.usage_by_model ?? {});
5752
+ const overallRate = usage.api_requests > 0 ? usage.failed_requests / usage.api_requests : 0;
5753
+ return entries.map(([model, d]) => {
5754
+ const failureRate = d.api_requests > 0 ? d.failed_requests / d.api_requests : null;
5755
+ const avgCostPerRequest = d.api_requests > 0 ? d.total_spend / d.api_requests : 0;
5756
+ const completionShare = d.total_tokens > 0 ? d.completion_tokens / d.total_tokens : 0;
5757
+ const per1k = costPer1k(model, d.total_spend, d.total_tokens, models) ?? 0;
5758
+ const spendShare = usage.total_spend > 0 ? d.total_spend / usage.total_spend : 0;
5759
+ if (failureRate !== null && overallRate > 0 && failureRate > 2 * overallRate && d.api_requests >= 10) {
5760
+ return { model, spend: d.total_spend, requests: d.api_requests, failureRate, avgCostPerRequest, flag: "failure-outlier", hint: `Fails ${(failureRate * 100).toFixed(1)}% vs ${(overallRate * 100).toFixed(1)}% overall` };
5930
5761
  }
5931
- );
5762
+ if (per1k > premiumThresholdPer1k && completionShare < 0.3 && spendShare > 0.05) {
5763
+ return { model, spend: d.total_spend, requests: d.api_requests, failureRate, avgCostPerRequest, flag: "downgrade-candidate", hint: `$${per1k.toFixed(3)}/1k with ${(completionShare * 100).toFixed(0)}% output \u2014 try a cheaper model` };
5764
+ }
5765
+ return { model, spend: d.total_spend, requests: d.api_requests, failureRate, avgCostPerRequest, flag: "ok", hint: "No action" };
5766
+ });
5932
5767
  };
5933
5768
  }
5934
5769
  });
@@ -5953,6 +5788,225 @@ var init_dates = __esm({
5953
5788
  }
5954
5789
  });
5955
5790
 
5791
+ // src/components/InsightsTab.tsx
5792
+ import { useMemo as useMemo8 } from "react";
5793
+ import Box16 from "@mui/material/Box";
5794
+ import Grid2 from "@mui/material/Grid";
5795
+ import Typography14 from "@mui/material/Typography";
5796
+ import Alert5 from "@mui/material/Alert";
5797
+ import Button7 from "@mui/material/Button";
5798
+ import Table6 from "@mui/material/Table";
5799
+ import TableBody6 from "@mui/material/TableBody";
5800
+ import TableCell6 from "@mui/material/TableCell";
5801
+ import TableContainer6 from "@mui/material/TableContainer";
5802
+ import TableHead6 from "@mui/material/TableHead";
5803
+ import TableRow6 from "@mui/material/TableRow";
5804
+ import Skeleton6 from "@mui/material/Skeleton";
5805
+ import { useAsyncRetry } from "react-use";
5806
+ import { jsx as jsx17, jsxs as jsxs16 } from "react/jsx-runtime";
5807
+ var FLAG_TONE, FLAG_LABEL, fmtRatio, fmtPct1, failedRow, InsightsTab;
5808
+ var init_InsightsTab = __esm({
5809
+ "src/components/InsightsTab.tsx"() {
5810
+ "use strict";
5811
+ init_insights();
5812
+ init_format();
5813
+ init_dates();
5814
+ init_ui();
5815
+ FLAG_TONE = {
5816
+ "failure-outlier": "danger",
5817
+ "downgrade-candidate": "warning",
5818
+ ok: "success"
5819
+ };
5820
+ FLAG_LABEL = {
5821
+ "failure-outlier": "Failure outlier",
5822
+ "downgrade-candidate": "Downgrade candidate",
5823
+ ok: "OK"
5824
+ };
5825
+ fmtRatio = (n) => n === null || Number.isNaN(n) ? "\u2014" : `${n.toFixed(2)}\xD7`;
5826
+ fmtPct1 = (n) => n === null || Number.isNaN(n) ? "\u2014" : `${(n * 100).toFixed(1)}%`;
5827
+ failedRow = (status) => /fail|error/i.test(status ?? "");
5828
+ InsightsTab = ({
5829
+ api,
5830
+ usage,
5831
+ models,
5832
+ keys,
5833
+ dateRange,
5834
+ loading
5835
+ }) => {
5836
+ const summary = useMemo8(() => summarizeUsage(usage, models), [usage, models]);
5837
+ const keyStats = useMemo8(
5838
+ () => ({
5839
+ idleKeys: keys.filter((k) => (k.spend ?? 0) === 0).length,
5840
+ uncappedKeys: keys.filter((k) => k.max_budget === void 0 || k.max_budget === null).length,
5841
+ totalKeys: keys.length
5842
+ }),
5843
+ [keys]
5844
+ );
5845
+ const score = useMemo8(
5846
+ () => scorePractices(summary, keyStats),
5847
+ [summary, keyStats]
5848
+ );
5849
+ const opportunities = useMemo8(() => {
5850
+ const opps = modelOpportunities(usage, models);
5851
+ return [...opps].sort(
5852
+ (a, b) => (a.flag === "ok" ? 1 : 0) - (b.flag === "ok" ? 1 : 0)
5853
+ );
5854
+ }, [usage, models]);
5855
+ const {
5856
+ value: spendLogs,
5857
+ loading: logsLoading,
5858
+ error: logsError,
5859
+ retry: retryLogs
5860
+ } = useAsyncRetry(async () => {
5861
+ return api.getSpendLogs({
5862
+ startDate: toLocalDay(dateRange.start),
5863
+ endDate: toLocalDay(dateRange.end),
5864
+ pageSize: 1e3
5865
+ });
5866
+ }, [api, dateRange]);
5867
+ const logStats = useMemo8(() => {
5868
+ const rows = spendLogs?.data ?? [];
5869
+ const hits = rows.filter((r) => r.cacheHit);
5870
+ const cachedPromptTokens = hits.reduce((s, r) => s + (r.promptTokens ?? 0), 0);
5871
+ const priced = models.map((m) => m.input_cost_per_token).filter((p) => p !== null && p !== void 0 && p > 0);
5872
+ const avgInputPrice = priced.length > 0 ? priced.reduce((s, p) => s + p, 0) / priced.length : null;
5873
+ return {
5874
+ total: spendLogs?.total ?? rows.length,
5875
+ hitRate: rows.length > 0 ? hits.length / rows.length : null,
5876
+ // Estimate: cached prompt tokens billed at the average listed input price.
5877
+ savedUsd: avgInputPrice !== null ? cachedPromptTokens * avgInputPrice : null,
5878
+ failedWasteUsd: rows.filter((r) => failedRow(r.status)).reduce((s, r) => s + (r.spend ?? 0), 0),
5879
+ truncated: spendLogs?.truncated ?? false
5880
+ };
5881
+ }, [spendLogs, models]);
5882
+ const renderSpendLogsBody = () => {
5883
+ if (logsLoading) return /* @__PURE__ */ jsx17(Skeleton6, { variant: "rounded", height: 120 });
5884
+ if (logsError) {
5885
+ return /* @__PURE__ */ jsxs16(Alert5, { severity: "error", children: [
5886
+ "Failed to load spend logs: ",
5887
+ logsError.message || "Unknown error",
5888
+ /* @__PURE__ */ jsx17(Box16, { sx: { mt: 1 }, children: /* @__PURE__ */ jsx17(Button7, { size: "small", onClick: retryLogs, children: "Retry" }) })
5889
+ ] });
5890
+ }
5891
+ if ((spendLogs?.data ?? []).length === 0) {
5892
+ return /* @__PURE__ */ jsx17(EmptyState, { message: "No spend logs in this period" });
5893
+ }
5894
+ return /* @__PURE__ */ jsxs16(Box16, { sx: { display: "flex", gap: 3, flexWrap: "wrap" }, children: [
5895
+ /* @__PURE__ */ jsx17(Stat, { label: "Cache-hit rate", value: fmtPct1(logStats.hitRate) }),
5896
+ /* @__PURE__ */ jsx17(
5897
+ Stat,
5898
+ {
5899
+ label: "Cache savings (est.)",
5900
+ value: logStats.savedUsd !== null ? fmtUsd(logStats.savedUsd) : "\u2014"
5901
+ }
5902
+ ),
5903
+ /* @__PURE__ */ jsx17(Stat, { label: "Failed-request waste", value: fmtUsd(logStats.failedWasteUsd) }),
5904
+ /* @__PURE__ */ jsx17(Stat, { label: "Log rows", value: fmtInt(logStats.total) }),
5905
+ logStats.truncated && /* @__PURE__ */ jsx17(Typography14, { variant: "caption", color: "text.secondary", children: "Showing the first 1,000 rows \u2014 figures are a lower bound." })
5906
+ ] });
5907
+ };
5908
+ const kpis = [
5909
+ {
5910
+ label: "Cost / 1k tokens",
5911
+ value: summary.costPer1kTokens !== null ? fmtUsd(summary.costPer1kTokens) : "\u2014",
5912
+ tone: "accent"
5913
+ },
5914
+ {
5915
+ label: "Cost / request",
5916
+ value: summary.costPerRequest !== null ? fmtUsd(summary.costPerRequest) : "\u2014",
5917
+ tone: "info"
5918
+ },
5919
+ {
5920
+ label: "Out : in ratio",
5921
+ value: fmtRatio(summary.outputInputRatio),
5922
+ tone: "neutral"
5923
+ },
5924
+ {
5925
+ label: "Retry waste",
5926
+ value: fmtUsd(summary.retryWasteUsd),
5927
+ hint: `${fmtPct1(summary.retryWasteShare)} of spend`,
5928
+ tone: "warning"
5929
+ },
5930
+ {
5931
+ label: "Success rate",
5932
+ value: fmtPct1(summary.successRate),
5933
+ tone: "success"
5934
+ }
5935
+ ];
5936
+ const renderEfficiencyBody = () => {
5937
+ if (loading) return /* @__PURE__ */ jsx17(Skeleton6, { variant: "rounded", height: 96 });
5938
+ if (!usage) return /* @__PURE__ */ jsx17(EmptyState, { message: "No usage in this period" });
5939
+ return /* @__PURE__ */ jsx17(MetricStrip, { metrics: kpis });
5940
+ };
5941
+ return /* @__PURE__ */ jsxs16(Box16, { sx: { display: "flex", flexDirection: "column", gap: 2 }, children: [
5942
+ /* @__PURE__ */ jsx17(
5943
+ SectionCard,
5944
+ {
5945
+ title: "Efficiency",
5946
+ subtitle: `${dateRange.start.toLocaleDateString()} \u2013 ${dateRange.end.toLocaleDateString()}`,
5947
+ children: renderEfficiencyBody()
5948
+ }
5949
+ ),
5950
+ /* @__PURE__ */ jsxs16(Grid2, { container: true, spacing: 2, alignItems: "flex-start", children: [
5951
+ /* @__PURE__ */ jsx17(Grid2, { item: true, xs: 12, md: 5, children: /* @__PURE__ */ jsx17(SectionCard, { title: "Practice score", subtitle: "Out of 100", children: loading ? /* @__PURE__ */ jsx17(Skeleton6, { variant: "rounded", height: 160 }) : /* @__PURE__ */ jsxs16(Box16, { sx: { display: "flex", flexDirection: "column", gap: 1.5 }, children: [
5952
+ /* @__PURE__ */ jsxs16(Typography14, { sx: { fontSize: 30, fontWeight: 700, fontVariantNumeric: "tabular-nums" }, children: [
5953
+ score.total,
5954
+ /* @__PURE__ */ jsx17(Typography14, { component: "span", color: "text.secondary", sx: { fontSize: 16 }, children: "/100" })
5955
+ ] }),
5956
+ score.checks.map((c) => /* @__PURE__ */ jsxs16(Box16, { children: [
5957
+ /* @__PURE__ */ jsxs16(Box16, { sx: { display: "flex", justifyContent: "space-between", gap: 1, mb: 0.5 }, children: [
5958
+ /* @__PURE__ */ jsx17(Typography14, { variant: "body2", sx: { fontWeight: 600 }, children: c.label }),
5959
+ /* @__PURE__ */ jsxs16(Typography14, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" }, children: [
5960
+ c.points,
5961
+ "/",
5962
+ c.maxPoints
5963
+ ] })
5964
+ ] }),
5965
+ /* @__PURE__ */ jsx17(
5966
+ Meter,
5967
+ {
5968
+ value: c.maxPoints > 0 ? c.points / c.maxPoints * 100 : 0,
5969
+ ariaLabel: `${c.label}: ${c.points} of ${c.maxPoints}`
5970
+ }
5971
+ ),
5972
+ /* @__PURE__ */ jsx17(Typography14, { variant: "caption", color: "text.secondary", children: c.detail })
5973
+ ] }, c.id))
5974
+ ] }) }) }),
5975
+ /* @__PURE__ */ jsx17(Grid2, { item: true, xs: 12, md: 7, children: /* @__PURE__ */ jsx17(SectionCard, { title: "Cache & errors", subtitle: "From spend logs (estimate)", children: renderSpendLogsBody() }) })
5976
+ ] }),
5977
+ /* @__PURE__ */ jsx17(SectionCard, { title: "Model opportunities", flush: true, children: loading ? /* @__PURE__ */ jsx17(Box16, { sx: { p: 2.5 }, children: /* @__PURE__ */ jsx17(Skeleton6, { variant: "rounded", height: 120 }) }) : /* @__PURE__ */ jsx17(TableContainer6, { children: /* @__PURE__ */ jsxs16(Table6, { size: "small", sx: dataTableSx, children: [
5978
+ /* @__PURE__ */ jsx17(TableHead6, { children: /* @__PURE__ */ jsxs16(TableRow6, { children: [
5979
+ /* @__PURE__ */ jsx17(TableCell6, { children: "Model" }),
5980
+ /* @__PURE__ */ jsx17(TableCell6, { align: "right", children: "Spend" }),
5981
+ /* @__PURE__ */ jsx17(TableCell6, { align: "right", children: "Requests" }),
5982
+ /* @__PURE__ */ jsx17(TableCell6, { align: "right", children: "Failure" }),
5983
+ /* @__PURE__ */ jsx17(TableCell6, { align: "right", children: "Avg / req" }),
5984
+ /* @__PURE__ */ jsx17(TableCell6, { children: "Signal" })
5985
+ ] }) }),
5986
+ /* @__PURE__ */ jsx17(TableBody6, { children: opportunities.length === 0 ? /* @__PURE__ */ jsx17(TableRow6, { children: /* @__PURE__ */ jsx17(TableCell6, { colSpan: 6, children: /* @__PURE__ */ jsx17(EmptyState, { message: "No model activity in this period" }) }) }) : opportunities.map((o) => /* @__PURE__ */ jsxs16(TableRow6, { children: [
5987
+ /* @__PURE__ */ jsx17(TableCell6, { children: /* @__PURE__ */ jsx17(Typography14, { variant: "body2", sx: { fontWeight: 600 }, children: o.model }) }),
5988
+ /* @__PURE__ */ jsx17(TableCell6, { align: "right", children: fmtUsd(o.spend) }),
5989
+ /* @__PURE__ */ jsx17(TableCell6, { align: "right", children: fmtInt(o.requests) }),
5990
+ /* @__PURE__ */ jsx17(TableCell6, { align: "right", children: fmtPct1(o.failureRate) }),
5991
+ /* @__PURE__ */ jsx17(TableCell6, { align: "right", children: fmtUsd(o.avgCostPerRequest) }),
5992
+ /* @__PURE__ */ jsxs16(TableCell6, { children: [
5993
+ /* @__PURE__ */ jsx17(StatusPill, { label: FLAG_LABEL[o.flag], tone: FLAG_TONE[o.flag], title: o.hint }),
5994
+ /* @__PURE__ */ jsx17(Typography14, { variant: "caption", color: "text.secondary", sx: { display: "block", mt: 0.25 }, children: o.hint })
5995
+ ] })
5996
+ ] }, o.model)) })
5997
+ ] }) }) }),
5998
+ /* @__PURE__ */ jsx17(SectionCard, { title: "Quality scores", children: /* @__PURE__ */ jsx17(
5999
+ EmptyState,
6000
+ {
6001
+ message: "Quality scores \u2014 connect Langfuse",
6002
+ hint: "LLM-as-judge evaluation is not configured for this deployment."
6003
+ }
6004
+ ) })
6005
+ ] });
6006
+ };
6007
+ }
6008
+ });
6009
+
5956
6010
  // src/feedback.ts
5957
6011
  function toastFor(kind, errorMsg) {
5958
6012
  switch (kind) {
@@ -6047,20 +6101,20 @@ var LiteLLMPage_exports = {};
6047
6101
  __export(LiteLLMPage_exports, {
6048
6102
  LiteLLMPage: () => LiteLLMPage
6049
6103
  });
6050
- import { useCallback as useCallback4, useEffect as useEffect6, useMemo as useMemo9, useState as useState12 } from "react";
6104
+ import { useCallback as useCallback4, useEffect as useEffect6, useMemo as useMemo9, useState as useState11 } from "react";
6051
6105
  import Box17 from "@mui/material/Box";
6052
6106
  import Alert6 from "@mui/material/Alert";
6053
- import Button7 from "@mui/material/Button";
6107
+ import Button8 from "@mui/material/Button";
6054
6108
  import Typography15 from "@mui/material/Typography";
6055
6109
  import Tabs2 from "@mui/material/Tabs";
6056
6110
  import Tab2 from "@mui/material/Tab";
6057
- import Grid2 from "@mui/material/Grid";
6111
+ import Grid3 from "@mui/material/Grid";
6058
6112
  import Accordion2 from "@mui/material/Accordion";
6059
6113
  import AccordionSummary2 from "@mui/material/AccordionSummary";
6060
6114
  import AccordionDetails2 from "@mui/material/AccordionDetails";
6061
6115
  import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
6062
6116
  import { useSearchParams } from "react-router-dom";
6063
- import { useAsync as useAsync2, useAsyncRetry } from "react-use";
6117
+ import { useAsync, useAsyncRetry as useAsyncRetry2 } from "react-use";
6064
6118
  import { useApi as useApi4, alertApiRef as alertApiRef2 } from "@backstage/core-plugin-api";
6065
6119
  import { Content, Progress } from "@backstage/core-components";
6066
6120
  import { usePermission as usePermission3 } from "@backstage/plugin-permission-react";
@@ -6098,20 +6152,20 @@ var init_LiteLLMPage = __esm({
6098
6152
  init_LiteLLMBudgetWidget();
6099
6153
  init_TeamUsage();
6100
6154
  init_ModelsTable();
6101
- init_AuditLog();
6155
+ init_InsightsTab();
6102
6156
  init_api();
6103
6157
  init_dates();
6104
6158
  init_feedback();
6105
6159
  init_teamMemberManager();
6106
6160
  init_unprovisionedCopy();
6107
6161
  PERIOD_LS_KEY2 = "litellm_usage_period";
6108
- PAGE_TABS = ["overview", "keys", "teams", "models", "audit"];
6162
+ PAGE_TABS = ["overview", "keys", "teams", "models", "insights"];
6109
6163
  isPageTab = (v) => !!v && PAGE_TABS.includes(v);
6110
6164
  LiteLLMPage = () => {
6111
6165
  const api = useApi4(liteLlmApiRef);
6112
6166
  const alertApi = useApi4(alertApiRef2);
6113
- const [dateRange, setDateRange] = useState12(initDateRange);
6114
- const [currentPreset, setCurrentPreset] = useState12(() => {
6167
+ const [dateRange, setDateRange] = useState11(initDateRange);
6168
+ const [currentPreset, setCurrentPreset] = useState11(() => {
6115
6169
  try {
6116
6170
  return localStorage.getItem(PERIOD_LS_KEY2) ?? "7d";
6117
6171
  } catch {
@@ -6119,15 +6173,15 @@ var init_LiteLLMPage = __esm({
6119
6173
  }
6120
6174
  });
6121
6175
  const [searchParams, setSearchParams] = useSearchParams();
6122
- const [activeTab, setActiveTab] = useState12(() => {
6176
+ const [activeTab, setActiveTab] = useState11(() => {
6123
6177
  const t = searchParams.get("tab");
6124
6178
  return isPageTab(t) ? t : "overview";
6125
6179
  });
6126
- const [filter, setFilter] = useState12(() => {
6180
+ const [filter, setFilter] = useState11(() => {
6127
6181
  const f = searchParams.get("filter");
6128
6182
  return f === "expired" || f === "expiring" ? f : void 0;
6129
6183
  });
6130
- const [generateDialogOpen, setGenerateDialogOpen] = useState12(
6184
+ const [generateDialogOpen, setGenerateDialogOpen] = useState11(
6131
6185
  () => searchParams.get("generate") === "1"
6132
6186
  );
6133
6187
  useEffect6(() => {
@@ -6183,31 +6237,31 @@ var init_LiteLLMPage = __esm({
6183
6237
  { replace: true }
6184
6238
  );
6185
6239
  }, [setSearchParams]);
6186
- const [manageTeam, setManageTeam] = useState12(null);
6187
- const [keyToEdit, setKeyToEdit] = useState12(null);
6240
+ const [manageTeam, setManageTeam] = useState11(null);
6241
+ const [keyToEdit, setKeyToEdit] = useState11(null);
6188
6242
  const keyFormOpen = generateDialogOpen || !!keyToEdit;
6189
- const [teamUsageCache, setTeamUsageCache] = useState12({});
6190
- const [teamUsageLoading, setTeamUsageLoading] = useState12({});
6191
- const [memberUsageCache, setMemberUsageCache] = useState12({});
6192
- const [memberUsageLoading, setMemberUsageLoading] = useState12({});
6193
- const { value: userInfo, loading: userLoading, error: userError } = useAsync2(
6243
+ const [teamUsageCache, setTeamUsageCache] = useState11({});
6244
+ const [teamUsageLoading, setTeamUsageLoading] = useState11({});
6245
+ const [memberUsageCache, setMemberUsageCache] = useState11({});
6246
+ const [memberUsageLoading, setMemberUsageLoading] = useState11({});
6247
+ const { value: userInfo, loading: userLoading, error: userError } = useAsync(
6194
6248
  () => api.getUserInfo(),
6195
6249
  [api]
6196
6250
  );
6197
- const { value: keys, loading: keysLoading, error: keysError, retry: refreshKeys } = useAsyncRetry(
6251
+ const { value: keys, loading: keysLoading, error: keysError, retry: refreshKeys } = useAsyncRetry2(
6198
6252
  () => api.listKeys(),
6199
6253
  [api]
6200
6254
  );
6201
- const { value: allModels, loading: modelsLoading, error: modelsError, retry: refreshModels } = useAsyncRetry(
6255
+ const { value: allModels, loading: modelsLoading, error: modelsError, retry: refreshModels } = useAsyncRetry2(
6202
6256
  () => api.listModels(),
6203
6257
  [api]
6204
6258
  );
6205
- const { value: allTeams, loading: teamsLoading, error: teamsError, retry: refreshTeams } = useAsyncRetry(
6259
+ const { value: allTeams, loading: teamsLoading, error: teamsError, retry: refreshTeams } = useAsyncRetry2(
6206
6260
  () => api.getTeams(),
6207
6261
  [api]
6208
6262
  );
6209
- const { value: liteLlmConfig } = useAsync2(() => api.getConfig(), [api]);
6210
- const { value: managedTeams } = useAsync2(
6263
+ const { value: liteLlmConfig } = useAsync(() => api.getConfig(), [api]);
6264
+ const { value: managedTeams } = useAsync(
6211
6265
  async () => liteLlmConfig?.teamManagement?.enabled ? api.getManagedTeams().catch(() => []) : [],
6212
6266
  [api, liteLlmConfig]
6213
6267
  );
@@ -6218,11 +6272,11 @@ var init_LiteLLMPage = __esm({
6218
6272
  const { allowed: canManageMcpServers } = usePermission3({ permission: litellmTeamMcpManagePermission });
6219
6273
  const teamMgmtEnabled = liteLlmConfig?.teamManagement?.enabled ?? false;
6220
6274
  const objectPermsEnabled = liteLlmConfig?.teamManagement?.objectPermissionsEnabled ?? false;
6221
- const { value: vectorStores } = useAsync2(
6275
+ const { value: vectorStores } = useAsync(
6222
6276
  async () => objectPermsEnabled ? api.getVectorStores().catch(() => []) : [],
6223
6277
  [api, objectPermsEnabled]
6224
6278
  );
6225
- const { value: mcpServers } = useAsync2(
6279
+ const { value: mcpServers } = useAsync(
6226
6280
  async () => objectPermsEnabled ? api.getMcpServers().catch(() => []) : [],
6227
6281
  [api, objectPermsEnabled]
6228
6282
  );
@@ -6238,7 +6292,7 @@ var init_LiteLLMPage = __esm({
6238
6292
  (t) => t.members_with_roles?.some((m) => m.user_id === userId)
6239
6293
  );
6240
6294
  }, [allTeams, userInfo]);
6241
- const { value: usage, loading: usageLoading, error: usageError, retry: refreshUsage } = useAsyncRetry(async () => {
6295
+ const { value: usage, loading: usageLoading, error: usageError, retry: refreshUsage } = useAsyncRetry2(async () => {
6242
6296
  const startDate = toLocalDay(dateRange.start);
6243
6297
  const endDate = toLocalDay(dateRange.end);
6244
6298
  return api.getUsage(startDate, endDate);
@@ -6446,7 +6500,7 @@ var init_LiteLLMPage = __esm({
6446
6500
  /* @__PURE__ */ jsx18(Tab2, { label: "Keys", value: "keys" }),
6447
6501
  /* @__PURE__ */ jsx18(Tab2, { label: "Teams", value: "teams" }),
6448
6502
  /* @__PURE__ */ jsx18(Tab2, { label: "Models", value: "models" }),
6449
- userInfo.can_view_audit && /* @__PURE__ */ jsx18(Tab2, { label: "Audit Log", value: "audit" })
6503
+ /* @__PURE__ */ jsx18(Tab2, { label: "Insights", value: "insights" })
6450
6504
  ]
6451
6505
  }
6452
6506
  );
@@ -6500,7 +6554,7 @@ var init_LiteLLMPage = __esm({
6500
6554
  }
6501
6555
  ),
6502
6556
  /* @__PURE__ */ jsx18(
6503
- Button7,
6557
+ Button8,
6504
6558
  {
6505
6559
  variant: "contained",
6506
6560
  color: "primary",
@@ -6512,8 +6566,8 @@ var init_LiteLLMPage = __esm({
6512
6566
  }
6513
6567
  ) });
6514
6568
  }
6515
- return /* @__PURE__ */ jsxs17(Grid2, { container: true, spacing: 2, alignItems: "flex-start", children: [
6516
- /* @__PURE__ */ jsx18(Grid2, { item: true, xs: 12, lg: 8, children: /* @__PURE__ */ jsx18(
6569
+ return /* @__PURE__ */ jsxs17(Grid3, { container: true, spacing: 2, alignItems: "flex-start", children: [
6570
+ /* @__PURE__ */ jsx18(Grid3, { item: true, xs: 12, lg: 8, children: /* @__PURE__ */ jsx18(
6517
6571
  UsageStats,
6518
6572
  {
6519
6573
  usage: usage ?? null,
@@ -6527,7 +6581,7 @@ var init_LiteLLMPage = __esm({
6527
6581
  userInfo
6528
6582
  }
6529
6583
  ) }),
6530
- /* @__PURE__ */ jsx18(Grid2, { item: true, xs: 12, lg: 4, children: /* @__PURE__ */ jsx18(LiteLLMBudgetWidget, { compact: true, collapsible: true, userInfo: userInfo ?? null, teams, keys: keys ?? [] }) })
6584
+ /* @__PURE__ */ jsx18(Grid3, { item: true, xs: 12, lg: 4, children: /* @__PURE__ */ jsx18(LiteLLMBudgetWidget, { compact: true, collapsible: true, userInfo: userInfo ?? null, teams, keys: keys ?? [] }) })
6531
6585
  ] });
6532
6586
  })(),
6533
6587
  activeTab === "keys" && (() => {
@@ -6535,7 +6589,7 @@ var init_LiteLLMPage = __esm({
6535
6589
  return /* @__PURE__ */ jsxs17(Alert6, { severity: "error", children: [
6536
6590
  "Failed to load keys: ",
6537
6591
  keysError.message || "Unknown error",
6538
- /* @__PURE__ */ jsx18(Box17, { sx: { mt: 1 }, children: /* @__PURE__ */ jsx18(Button7, { size: "small", onClick: refreshKeys, children: "Retry" }) })
6592
+ /* @__PURE__ */ jsx18(Box17, { sx: { mt: 1 }, children: /* @__PURE__ */ jsx18(Button8, { size: "small", onClick: refreshKeys, children: "Retry" }) })
6539
6593
  ] });
6540
6594
  }
6541
6595
  return /* @__PURE__ */ jsx18(
@@ -6559,7 +6613,7 @@ var init_LiteLLMPage = __esm({
6559
6613
  return /* @__PURE__ */ jsxs17(Alert6, { severity: "error", children: [
6560
6614
  "Failed to load teams: ",
6561
6615
  teamsError.message || "Unknown error",
6562
- /* @__PURE__ */ jsx18(Box17, { sx: { mt: 1 }, children: /* @__PURE__ */ jsx18(Button7, { size: "small", onClick: refreshTeams, children: "Retry" }) })
6616
+ /* @__PURE__ */ jsx18(Box17, { sx: { mt: 1 }, children: /* @__PURE__ */ jsx18(Button8, { size: "small", onClick: refreshTeams, children: "Retry" }) })
6563
6617
  ] });
6564
6618
  }
6565
6619
  const teamsById = /* @__PURE__ */ new Map();
@@ -6596,7 +6650,7 @@ var init_LiteLLMPage = __esm({
6596
6650
  return /* @__PURE__ */ jsxs17(Alert6, { severity: "error", children: [
6597
6651
  "Failed to load models: ",
6598
6652
  modelsError.message || "Unknown error",
6599
- /* @__PURE__ */ jsx18(Box17, { sx: { mt: 1 }, children: /* @__PURE__ */ jsx18(Button7, { size: "small", onClick: refreshModels, children: "Retry" }) })
6653
+ /* @__PURE__ */ jsx18(Box17, { sx: { mt: 1 }, children: /* @__PURE__ */ jsx18(Button8, { size: "small", onClick: refreshModels, children: "Retry" }) })
6600
6654
  ] });
6601
6655
  }
6602
6656
  return /* @__PURE__ */ jsx18(
@@ -6608,7 +6662,17 @@ var init_LiteLLMPage = __esm({
6608
6662
  }
6609
6663
  );
6610
6664
  })(),
6611
- activeTab === "audit" && userInfo.can_view_audit && /* @__PURE__ */ jsx18(AuditLog, { api })
6665
+ activeTab === "insights" && /* @__PURE__ */ jsx18(
6666
+ InsightsTab,
6667
+ {
6668
+ api,
6669
+ usage: usage ?? null,
6670
+ models: allModels ?? [],
6671
+ keys: keys ?? [],
6672
+ dateRange,
6673
+ loading: usageLoading
6674
+ }
6675
+ )
6612
6676
  ] }),
6613
6677
  /* @__PURE__ */ jsx18(
6614
6678
  KeyFormDialog,
@@ -6782,17 +6846,17 @@ var ConnectPage_exports = {};
6782
6846
  __export(ConnectPage_exports, {
6783
6847
  ConnectPage: () => ConnectPage
6784
6848
  });
6785
- import { useState as useState13 } from "react";
6849
+ import { useState as useState12 } from "react";
6786
6850
  import Alert7 from "@mui/material/Alert";
6787
6851
  import Box18 from "@mui/material/Box";
6788
- import Button8 from "@mui/material/Button";
6852
+ import Button9 from "@mui/material/Button";
6789
6853
  import Checkbox3 from "@mui/material/Checkbox";
6790
6854
  import FormControlLabel3 from "@mui/material/FormControlLabel";
6791
6855
  import FormGroup from "@mui/material/FormGroup";
6792
6856
  import Paper9 from "@mui/material/Paper";
6793
6857
  import Typography16 from "@mui/material/Typography";
6794
6858
  import { useSearchParams as useSearchParams2 } from "react-router-dom";
6795
- import { useAsync as useAsync3 } from "react-use";
6859
+ import { useAsync as useAsync2 } from "react-use";
6796
6860
  import { useApi as useApi5 } from "@backstage/core-plugin-api";
6797
6861
  import { Content as Content2, Progress as Progress2 } from "@backstage/core-components";
6798
6862
  import { jsx as jsx19, jsxs as jsxs18 } from "react/jsx-runtime";
@@ -6809,10 +6873,10 @@ var init_ConnectPage = __esm({
6809
6873
  const state = params.get("state") ?? "";
6810
6874
  const client = params.get("client") || "a local application";
6811
6875
  const have = new Set((params.get("have") ?? "").split(",").filter(Boolean));
6812
- const teams = useAsync3(() => api.getTeams(), [api]);
6813
- const [selected, setSelected] = useState13();
6814
- const [busy, setBusy] = useState13(false);
6815
- const [error, setError] = useState13();
6876
+ const teams = useAsync2(() => api.getTeams(), [api]);
6877
+ const [selected, setSelected] = useState12();
6878
+ const [busy, setBusy] = useState12(false);
6879
+ const [error, setError] = useState12();
6816
6880
  if (!isLoopbackCallback(redirectUri)) {
6817
6881
  return /* @__PURE__ */ jsx19(Content2, { children: /* @__PURE__ */ jsx19(Alert7, { severity: "error", children: "Invalid connection link: redirect_uri must be http://localhost:<port>/callback." }) });
6818
6882
  }
@@ -6860,7 +6924,7 @@ var init_ConnectPage = __esm({
6860
6924
  )) }),
6861
6925
  error && /* @__PURE__ */ jsx19(Alert7, { severity: "error", sx: { mb: 2 }, children: error }),
6862
6926
  /* @__PURE__ */ jsx19(
6863
- Button8,
6927
+ Button9,
6864
6928
  {
6865
6929
  variant: "contained",
6866
6930
  onClick: connect,
@@ -6937,13 +7001,13 @@ init_format();
6937
7001
  init_dates();
6938
7002
  init_ui();
6939
7003
  init_widgetState();
6940
- import { useEffect as useEffect7, useMemo as useMemo10, useState as useState14 } from "react";
7004
+ import { useEffect as useEffect7, useMemo as useMemo10, useState as useState13 } from "react";
6941
7005
  import Paper10 from "@mui/material/Paper";
6942
7006
  import Box19 from "@mui/material/Box";
6943
7007
  import Typography17 from "@mui/material/Typography";
6944
7008
  import FormControl2 from "@mui/material/FormControl";
6945
7009
  import Select2 from "@mui/material/Select";
6946
- import MenuItem6 from "@mui/material/MenuItem";
7010
+ import MenuItem5 from "@mui/material/MenuItem";
6947
7011
  import Skeleton7 from "@mui/material/Skeleton";
6948
7012
  import { AreaChart as AreaChart3, Area as Area3, XAxis as XAxis3, Tooltip as Tooltip8, ResponsiveContainer as ResponsiveContainer3 } from "recharts";
6949
7013
  import { useApi as useApi6 } from "@backstage/core-plugin-api";
@@ -6993,7 +7057,7 @@ function usageSummary(usage) {
6993
7057
  }
6994
7058
 
6995
7059
  // src/components/LiteLLMHomeWidget.tsx
6996
- import { Fragment as Fragment10, jsx as jsx21, jsxs as jsxs19 } from "react/jsx-runtime";
7060
+ import { Fragment as Fragment9, jsx as jsx21, jsxs as jsxs19 } from "react/jsx-runtime";
6997
7061
  function presetToDateRange(preset) {
6998
7062
  const end = /* @__PURE__ */ new Date();
6999
7063
  const start = /* @__PURE__ */ new Date();
@@ -7014,10 +7078,10 @@ var LiteLLMHomeWidget = ({
7014
7078
  const api = useApi6(liteLlmApiRef);
7015
7079
  const moduleRouteRef = useRouteRef2(rootRouteRef);
7016
7080
  const { userInfo, keys, loading: profileLoading, error: profileError } = useLiteLLMProfile();
7017
- const [period, setPeriod] = useState14(defaultPeriod);
7018
- const [usageLoading, setUsageLoading] = useState14(true);
7019
- const [usageError, setUsageError] = useState14(null);
7020
- const [usage, setUsage] = useState14(null);
7081
+ const [period, setPeriod] = useState13(defaultPeriod);
7082
+ const [usageLoading, setUsageLoading] = useState13(true);
7083
+ const [usageError, setUsageError] = useState13(null);
7084
+ const [usage, setUsage] = useState13(null);
7021
7085
  useEffect7(() => {
7022
7086
  let cancelled = false;
7023
7087
  setUsageLoading(true);
@@ -7073,9 +7137,9 @@ var LiteLLMHomeWidget = ({
7073
7137
  displayEmpty: true,
7074
7138
  inputProps: { "aria-label": "Usage period" },
7075
7139
  children: [
7076
- /* @__PURE__ */ jsx21(MenuItem6, { value: "today", children: "Today" }),
7077
- /* @__PURE__ */ jsx21(MenuItem6, { value: "7d", children: "7d" }),
7078
- /* @__PURE__ */ jsx21(MenuItem6, { value: "30d", children: "30d" })
7140
+ /* @__PURE__ */ jsx21(MenuItem5, { value: "today", children: "Today" }),
7141
+ /* @__PURE__ */ jsx21(MenuItem5, { value: "7d", children: "7d" }),
7142
+ /* @__PURE__ */ jsx21(MenuItem5, { value: "30d", children: "30d" })
7079
7143
  ]
7080
7144
  }
7081
7145
  ) })
@@ -7087,7 +7151,7 @@ var LiteLLMHomeWidget = ({
7087
7151
  viewState.kind === "error" && /* @__PURE__ */ jsx21(Typography17, { variant: "body2", color: "text.secondary", sx: { mt: 1 }, children: viewState.message }),
7088
7152
  viewState.kind === "unprovisioned" && /* @__PURE__ */ jsx21(Typography17, { variant: "body2", color: "text.secondary", sx: { mt: 1 }, children: UNPROVISIONED_MSG }),
7089
7153
  viewState.kind === "ready" && partialFailure && /* @__PURE__ */ jsx21(Box19, { sx: { mt: 1, mb: 1, p: 1, backgroundColor: "action.hover", borderRadius: 1 }, children: /* @__PURE__ */ jsx21(Typography17, { variant: "body2", color: "text.secondary", children: USAGE_UNAVAILABLE_MSG }) }),
7090
- viewState.kind === "ready" && /* @__PURE__ */ jsxs19(Fragment10, { children: [
7154
+ viewState.kind === "ready" && /* @__PURE__ */ jsxs19(Fragment9, { children: [
7091
7155
  /* @__PURE__ */ jsx21(
7092
7156
  Typography17,
7093
7157
  {
@@ -7100,7 +7164,7 @@ var LiteLLMHomeWidget = ({
7100
7164
  children: viewState.usageUnavailable ? USAGE_UNAVAILABLE_MSG : usageSummary(usage)
7101
7165
  }
7102
7166
  ),
7103
- hasSparkline && !viewState.usageUnavailable && /* @__PURE__ */ jsxs19(Fragment10, { children: [
7167
+ hasSparkline && !viewState.usageUnavailable && /* @__PURE__ */ jsxs19(Fragment9, { children: [
7104
7168
  /* @__PURE__ */ jsx21(
7105
7169
  Box19,
7106
7170
  {
@@ -7146,14 +7210,14 @@ init_useLiteLLMProfile();
7146
7210
  init_api();
7147
7211
  init_format();
7148
7212
  init_dates();
7149
- import { useCallback as useCallback5, useEffect as useEffect8, useId as useId2, useMemo as useMemo11, useState as useState15 } from "react";
7213
+ import { useCallback as useCallback5, useEffect as useEffect8, useId as useId2, useMemo as useMemo11, useState as useState14 } from "react";
7150
7214
  import { useRouteRef as useRouteRef3 } from "@backstage/frontend-plugin-api";
7151
7215
  import Paper11 from "@mui/material/Paper";
7152
7216
  import Box20 from "@mui/material/Box";
7153
- import Button9 from "@mui/material/Button";
7217
+ import Button10 from "@mui/material/Button";
7154
7218
  import Typography18 from "@mui/material/Typography";
7155
7219
  import Skeleton8 from "@mui/material/Skeleton";
7156
- import Collapse4 from "@mui/material/Collapse";
7220
+ import Collapse3 from "@mui/material/Collapse";
7157
7221
  import { useTheme as useTheme4 } from "@mui/material/styles";
7158
7222
  import useMediaQuery from "@mui/material/useMediaQuery";
7159
7223
  import { ExpandMore as ExpandMore4 } from "@mui/icons-material";
@@ -7186,7 +7250,7 @@ init_GenerateKeyButton();
7186
7250
  init_BudgetLimitList();
7187
7251
  init_budget();
7188
7252
  init_routes();
7189
- import { Fragment as Fragment11, jsx as jsx22, jsxs as jsxs20 } from "react/jsx-runtime";
7253
+ import { Fragment as Fragment10, jsx as jsx22, jsxs as jsxs20 } from "react/jsx-runtime";
7190
7254
  var LEVEL = {
7191
7255
  key: { name: "Key", none: "No cap (unlimited)" },
7192
7256
  user: { name: "User", none: "No personal budget" },
@@ -7339,7 +7403,7 @@ var LevelRow = ({
7339
7403
  ),
7340
7404
  /* @__PURE__ */ jsxs20(Box20, { sx: { minWidth: 0 }, children: [
7341
7405
  /* @__PURE__ */ jsx22(Tag, { tone: tone === "warning" || tone === "danger" ? tone : void 0, children: name }),
7342
- limit ? /* @__PURE__ */ jsxs20(Fragment11, { children: [
7406
+ limit ? /* @__PURE__ */ jsxs20(Fragment10, { children: [
7343
7407
  /* @__PURE__ */ jsx22(
7344
7408
  Typography18,
7345
7409
  {
@@ -7570,17 +7634,17 @@ var LiteLLMBudgetGauges = ({
7570
7634
  const moduleRouteRef = useRouteRef3(rootRouteRef);
7571
7635
  const moduleHref = propModuleHref ?? moduleRouteRef?.();
7572
7636
  const { userInfo: hookUserInfo, teams: hookTeams, keys: hookKeys, loading: profileLoading, error: profileError } = useLiteLLMProfile();
7573
- const [usageLoading, setUsageLoading] = useState15(true);
7574
- const [usageError, setUsageError] = useState15(null);
7575
- const [dailySpend, setDailySpend] = useState15([]);
7576
- const [mtdSpend, setMtdSpend] = useState15(0);
7637
+ const [usageLoading, setUsageLoading] = useState14(true);
7638
+ const [usageError, setUsageError] = useState14(null);
7639
+ const [dailySpend, setDailySpend] = useState14([]);
7640
+ const [mtdSpend, setMtdSpend] = useState14(0);
7577
7641
  const user = propUserInfo !== void 0 ? propUserInfo : hookUserInfo;
7578
7642
  const teams = propTeams !== void 0 ? propTeams : hookTeams;
7579
7643
  const keys = propKeys !== void 0 ? propKeys : hookKeys;
7580
7644
  const loading = propUserInfo === void 0 ? profileLoading : usageLoading;
7581
7645
  const error = propUserInfo === void 0 ? profileError : usageError;
7582
7646
  const isControlled = expanded !== void 0;
7583
- const [uncontrolledExpanded, setUncontrolledExpanded] = useState15(defaultExpanded);
7647
+ const [uncontrolledExpanded, setUncontrolledExpanded] = useState14(defaultExpanded);
7584
7648
  const isExpanded = isControlled ? expanded : uncontrolledExpanded;
7585
7649
  const toggleExpanded = useCallback5(() => {
7586
7650
  const next = !isExpanded;
@@ -7664,10 +7728,10 @@ var LiteLLMBudgetGauges = ({
7664
7728
  return onCreateKey ? /* @__PURE__ */ jsx22(GenerateKeyButton, { size: "small", label, onClick: onCreateKey }, key) : /* @__PURE__ */ jsx22(GenerateKeyButton, { size: "small", label, to: `${moduleHref}?generate=1` }, key);
7665
7729
  case "module":
7666
7730
  if (!moduleHref) return null;
7667
- return /* @__PURE__ */ jsx22(Button9, { size: "small", variant: "text", component: Link4, to: moduleHref, children: label }, key);
7731
+ return /* @__PURE__ */ jsx22(Button10, { size: "small", variant: "text", component: Link4, to: moduleHref, children: label }, key);
7668
7732
  case "all-limits":
7669
7733
  return /* @__PURE__ */ jsx22(
7670
- Button9,
7734
+ Button10,
7671
7735
  {
7672
7736
  size: "small",
7673
7737
  variant: "text",
@@ -7716,7 +7780,7 @@ var LiteLLMBudgetGauges = ({
7716
7780
  ] }, i)) }),
7717
7781
  viewState.kind === "error" && /* @__PURE__ */ jsx22(Typography18, { variant: "body2", color: "text.secondary", sx: { mt: 0.5 }, children: viewState.message }),
7718
7782
  viewState.kind === "unprovisioned" && /* @__PURE__ */ jsx22(Typography18, { variant: "body2", color: "text.secondary", sx: { mt: 0.5 }, children: UNPROVISIONED_MSG }),
7719
- viewState.kind === "ready" && /* @__PURE__ */ jsxs20(Fragment11, { children: [
7783
+ viewState.kind === "ready" && /* @__PURE__ */ jsxs20(Fragment10, { children: [
7720
7784
  /* @__PURE__ */ jsxs20(
7721
7785
  Box20,
7722
7786
  {
@@ -7770,7 +7834,7 @@ var LiteLLMBudgetGauges = ({
7770
7834
  maxBudget: user?.max_budget ?? void 0
7771
7835
  }
7772
7836
  ) }),
7773
- /* @__PURE__ */ jsx22(Collapse4, { in: isExpanded, unmountOnExit: true, children: /* @__PURE__ */ jsx22(
7837
+ /* @__PURE__ */ jsx22(Collapse3, { in: isExpanded, unmountOnExit: true, children: /* @__PURE__ */ jsx22(
7774
7838
  Box20,
7775
7839
  {
7776
7840
  id: expandedRegionId,