@acarmisc/backstage-plugin-litellm 0.36.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.cjs.js CHANGED
@@ -317,6 +317,17 @@ var init_api = __esm({
317
317
  end_date: endDate
318
318
  });
319
319
  }
320
+ async getSpendLogs(params) {
321
+ const strParams = {
322
+ start_date: params.startDate,
323
+ end_date: params.endDate
324
+ };
325
+ if (params.userId) strParams.userId = params.userId;
326
+ if (params.teamId) strParams.teamId = params.teamId;
327
+ if (params.page !== void 0) strParams.page = String(params.page);
328
+ if (params.pageSize !== void 0) strParams.page_size = String(params.pageSize);
329
+ return this.get("/insights/spend-logs", strParams);
330
+ }
320
331
  async getConfig() {
321
332
  return this.get("/config");
322
333
  }
@@ -5661,292 +5672,335 @@ var init_ModelsTable = __esm({
5661
5672
  }
5662
5673
  });
5663
5674
 
5664
- // src/components/AuditLog.tsx
5665
- function actionTone(action) {
5666
- if (!action) return "neutral";
5667
- for (const [key, tone] of Object.entries(ACTION_TONES)) {
5668
- if (action.toLowerCase().includes(key)) return tone;
5675
+ // src/insights.ts
5676
+ var costPer1k, summarizeUsage, scorePractices, modelOpportunities;
5677
+ var init_insights = __esm({
5678
+ "src/insights.ts"() {
5679
+ "use strict";
5680
+ costPer1k = (model, spend, tokens, models) => {
5681
+ const info = models?.find((m) => m.model_name === model);
5682
+ 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;
5683
+ if (listed !== null) return listed;
5684
+ return tokens > 0 ? spend / tokens * 1e3 : null;
5685
+ };
5686
+ summarizeUsage = (usage, models, premiumThresholdPer1k = 0.01) => {
5687
+ if (!usage) {
5688
+ return {
5689
+ costPer1kTokens: null,
5690
+ costPerRequest: null,
5691
+ outputInputRatio: null,
5692
+ retryWasteUsd: 0,
5693
+ retryWasteShare: 0,
5694
+ successRate: null,
5695
+ premiumShare: 0,
5696
+ premiumSpend: 0,
5697
+ topModelConcentration: 0,
5698
+ projectedMonthSpend: null,
5699
+ burnMultiple: null
5700
+ };
5701
+ }
5702
+ const {
5703
+ total_spend: spend,
5704
+ total_tokens: tokens,
5705
+ prompt_tokens: prompt,
5706
+ completion_tokens: completion,
5707
+ api_requests: reqs,
5708
+ successful_requests: ok,
5709
+ failed_requests: failed,
5710
+ usage_by_model: byModel
5711
+ } = usage;
5712
+ const wasteShare = reqs > 0 ? failed / reqs : 0;
5713
+ const entries = Object.entries(byModel ?? {});
5714
+ const topSpend = entries.reduce((m, [, d]) => Math.max(m, d.total_spend), 0);
5715
+ 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);
5716
+ const days = usage.daily_usage?.length ?? 0;
5717
+ const projected = days > 0 ? spend / days * 30 : null;
5718
+ return {
5719
+ costPer1kTokens: tokens > 0 ? spend / tokens * 1e3 : null,
5720
+ costPerRequest: reqs > 0 ? spend / reqs : null,
5721
+ outputInputRatio: prompt > 0 ? completion / prompt : null,
5722
+ retryWasteUsd: spend * wasteShare,
5723
+ retryWasteShare: wasteShare,
5724
+ successRate: reqs > 0 ? ok / reqs : null,
5725
+ premiumShare: spend > 0 ? premiumSpend / spend : 0,
5726
+ premiumSpend,
5727
+ topModelConcentration: spend > 0 ? topSpend / spend : 0,
5728
+ projectedMonthSpend: projected,
5729
+ burnMultiple: projected !== null && spend > 0 ? projected / spend : null
5730
+ };
5731
+ };
5732
+ scorePractices = (summary, keyStats) => {
5733
+ const waste = summary.retryWasteShare <= 0.01 ? 30 : summary.retryWasteShare <= 0.05 ? 20 : summary.retryWasteShare <= 0.1 ? 10 : 0;
5734
+ const premium = summary.premiumShare <= 0.2 ? 25 : summary.premiumShare <= 0.5 ? 15 : summary.premiumShare <= 0.8 ? 5 : 0;
5735
+ const conc = summary.topModelConcentration <= 0.5 ? 20 : summary.topModelConcentration <= 0.8 ? 10 : 0;
5736
+ const bad = keyStats.totalKeys > 0 ? (keyStats.idleKeys + keyStats.uncappedKeys) / keyStats.totalKeys : 0;
5737
+ const hygiene = keyStats.totalKeys === 0 ? 25 : bad === 0 ? 25 : bad <= 0.2 ? 15 : bad <= 0.5 ? 5 : 0;
5738
+ const checks = [
5739
+ { id: "retry-waste", label: "Low retry waste", points: waste, maxPoints: 30, detail: `${(summary.retryWasteShare * 100).toFixed(1)}% of requests failed` },
5740
+ { id: "premium-mix", label: "Balanced model mix", points: premium, maxPoints: 25, detail: `${(summary.premiumShare * 100).toFixed(1)}% of spend on premium models` },
5741
+ { id: "concentration", label: "No single-model lock-in", points: conc, maxPoints: 20, detail: `Top model is ${(summary.topModelConcentration * 100).toFixed(1)}% of spend` },
5742
+ { id: "key-hygiene", label: "Key hygiene", points: hygiene, maxPoints: 25, detail: `${keyStats.idleKeys} idle, ${keyStats.uncappedKeys} uncapped of ${keyStats.totalKeys} keys` }
5743
+ ];
5744
+ return { total: checks.reduce((s, c) => s + c.points, 0), checks };
5745
+ };
5746
+ modelOpportunities = (usage, models, premiumThresholdPer1k = 0.01) => {
5747
+ if (!usage) return [];
5748
+ const entries = Object.entries(usage.usage_by_model ?? {});
5749
+ const overallRate = usage.api_requests > 0 ? usage.failed_requests / usage.api_requests : 0;
5750
+ return entries.map(([model, d]) => {
5751
+ const failureRate = d.api_requests > 0 ? d.failed_requests / d.api_requests : null;
5752
+ const avgCostPerRequest = d.api_requests > 0 ? d.total_spend / d.api_requests : 0;
5753
+ const completionShare = d.total_tokens > 0 ? d.completion_tokens / d.total_tokens : 0;
5754
+ const per1k = costPer1k(model, d.total_spend, d.total_tokens, models) ?? 0;
5755
+ const spendShare = usage.total_spend > 0 ? d.total_spend / usage.total_spend : 0;
5756
+ if (failureRate !== null && overallRate > 0 && failureRate > 2 * overallRate && d.api_requests >= 10) {
5757
+ 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` };
5758
+ }
5759
+ if (per1k > premiumThresholdPer1k && completionShare < 0.3 && spendShare > 0.05) {
5760
+ 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` };
5761
+ }
5762
+ return { model, spend: d.total_spend, requests: d.api_requests, failureRate, avgCostPerRequest, flag: "ok", hint: "No action" };
5763
+ });
5764
+ };
5669
5765
  }
5670
- return "info";
5671
- }
5672
- function prettyTableName(name) {
5673
- if (!name) return "\u2014";
5674
- return TABLE_LABELS[name] ?? name;
5766
+ });
5767
+
5768
+ // src/dates.ts
5769
+ function toLocalDay(d) {
5770
+ const year = d.getFullYear();
5771
+ const month = String(d.getMonth() + 1).padStart(2, "0");
5772
+ const day = String(d.getDate()).padStart(2, "0");
5773
+ return `${year}-${month}-${day}`;
5675
5774
  }
5676
- function formatDateTime(iso) {
5677
- try {
5678
- return new Date(iso).toLocaleString();
5679
- } catch {
5680
- return iso;
5681
- }
5775
+ function monthToDateRange(now = /* @__PURE__ */ new Date()) {
5776
+ const start = new Date(now.getFullYear(), now.getMonth(), 1);
5777
+ return {
5778
+ startDate: toLocalDay(start),
5779
+ endDate: toLocalDay(now)
5780
+ };
5682
5781
  }
5683
- function renderAuditLogBody(loading, entries) {
5684
- if (loading) {
5685
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableRow6.default, { children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { colSpan: 6, sx: { py: 2 }, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Skeleton6.default, { variant: "rounded", height: 160 }) }) });
5686
- }
5687
- if (entries.length === 0) {
5688
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableRow6.default, { children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { colSpan: 6, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
5689
- EmptyState,
5690
- {
5691
- message: "No audit events found",
5692
- hint: "Try widening the filters or a different time window."
5693
- }
5694
- ) }) });
5782
+ var init_dates = __esm({
5783
+ "src/dates.ts"() {
5784
+ "use strict";
5695
5785
  }
5696
- return entries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(DetailRow, { entry }, entry.id));
5697
- }
5698
- var import_react12, import_Box16, import_Typography14, import_Table6, import_TableBody6, import_TableCell6, import_TableContainer6, import_TableHead6, import_TableRow6, import_TablePagination, import_TextField6, import_MenuItem5, import_Skeleton6, import_Collapse3, import_IconButton7, import_Alert5, import_styles12, import_icons_material9, import_react_use, import_jsx_runtime17, ACTION_TONES, TABLE_LABELS, DetailRow, AuditLog;
5699
- var init_AuditLog = __esm({
5700
- "src/components/AuditLog.tsx"() {
5786
+ });
5787
+
5788
+ // src/components/InsightsTab.tsx
5789
+ var import_react12, import_Box16, import_Grid2, import_Typography14, import_Alert5, import_Button7, import_Table6, import_TableBody6, import_TableCell6, import_TableContainer6, import_TableHead6, import_TableRow6, import_Skeleton6, import_react_use, import_jsx_runtime17, FLAG_TONE, FLAG_LABEL, fmtRatio, fmtPct1, failedRow, InsightsTab;
5790
+ var init_InsightsTab = __esm({
5791
+ "src/components/InsightsTab.tsx"() {
5701
5792
  "use strict";
5702
5793
  import_react12 = require("react");
5703
5794
  import_Box16 = __toESM(require("@mui/material/Box"));
5795
+ import_Grid2 = __toESM(require("@mui/material/Grid"));
5704
5796
  import_Typography14 = __toESM(require("@mui/material/Typography"));
5797
+ import_Alert5 = __toESM(require("@mui/material/Alert"));
5798
+ import_Button7 = __toESM(require("@mui/material/Button"));
5705
5799
  import_Table6 = __toESM(require("@mui/material/Table"));
5706
5800
  import_TableBody6 = __toESM(require("@mui/material/TableBody"));
5707
5801
  import_TableCell6 = __toESM(require("@mui/material/TableCell"));
5708
5802
  import_TableContainer6 = __toESM(require("@mui/material/TableContainer"));
5709
5803
  import_TableHead6 = __toESM(require("@mui/material/TableHead"));
5710
5804
  import_TableRow6 = __toESM(require("@mui/material/TableRow"));
5711
- import_TablePagination = __toESM(require("@mui/material/TablePagination"));
5712
- import_TextField6 = __toESM(require("@mui/material/TextField"));
5713
- import_MenuItem5 = __toESM(require("@mui/material/MenuItem"));
5714
5805
  import_Skeleton6 = __toESM(require("@mui/material/Skeleton"));
5715
- import_Collapse3 = __toESM(require("@mui/material/Collapse"));
5716
- import_IconButton7 = __toESM(require("@mui/material/IconButton"));
5717
- import_Alert5 = __toESM(require("@mui/material/Alert"));
5718
- import_styles12 = require("@mui/material/styles");
5719
- import_icons_material9 = require("@mui/icons-material");
5720
5806
  import_react_use = require("react-use");
5807
+ init_insights();
5808
+ init_format();
5809
+ init_dates();
5721
5810
  init_ui();
5722
5811
  import_jsx_runtime17 = require("react/jsx-runtime");
5723
- ACTION_TONES = {
5724
- created: "success",
5725
- deleted: "danger",
5726
- updated: "warning",
5727
- blocked: "danger",
5728
- unblocked: "success"
5812
+ FLAG_TONE = {
5813
+ "failure-outlier": "danger",
5814
+ "downgrade-candidate": "warning",
5815
+ ok: "success"
5729
5816
  };
5730
- TABLE_LABELS = {
5731
- LiteLLM_VerificationToken: "Key",
5732
- LiteLLM_TeamTable: "Team",
5733
- LiteLLM_TeamMembership: "Team member",
5734
- LiteLLM_ObjectPermissionTable: "Team access (KB / MCP)",
5735
- LiteLLM_UserTable: "User"
5817
+ FLAG_LABEL = {
5818
+ "failure-outlier": "Failure outlier",
5819
+ "downgrade-candidate": "Downgrade candidate",
5820
+ ok: "OK"
5736
5821
  };
5737
- DetailRow = ({ entry }) => {
5738
- const [open, setOpen] = (0, import_react12.useState)(false);
5739
- const hasDetail = entry.before_value || entry.updated_values;
5740
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
5741
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_TableRow6.default, { children: [
5742
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { sx: { width: 40, pr: 0 }, children: hasDetail && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
5743
- import_IconButton7.default,
5744
- {
5745
- size: "small",
5746
- onClick: () => setOpen((o) => !o),
5747
- "aria-label": open ? "Hide changes" : "Show changes",
5748
- "aria-expanded": open,
5749
- sx: (theme) => ({
5750
- color: theme.palette.text.secondary,
5751
- transform: open ? "rotate(180deg)" : "none",
5752
- transition: theme.transitions.create("transform")
5753
- }),
5754
- children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_icons_material9.KeyboardArrowDown, { fontSize: "small" })
5755
- }
5756
- ) }),
5757
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { sx: { whiteSpace: "nowrap" }, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Typography14.default, { variant: "body2", color: "text.secondary", children: formatDateTime(entry.updated_at) }) }),
5758
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { children: entry.action && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(StatusPill, { label: entry.action, tone: actionTone(entry.action) }) }),
5759
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Typography14.default, { variant: "body2", children: prettyTableName(entry.table_name) }) }),
5760
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
5761
- import_Typography14.default,
5822
+ fmtRatio = (n) => n === null || Number.isNaN(n) ? "\u2014" : `${n.toFixed(2)}\xD7`;
5823
+ fmtPct1 = (n) => n === null || Number.isNaN(n) ? "\u2014" : `${(n * 100).toFixed(1)}%`;
5824
+ failedRow = (status) => /fail|error/i.test(status ?? "");
5825
+ InsightsTab = ({
5826
+ api,
5827
+ usage,
5828
+ models,
5829
+ keys,
5830
+ dateRange,
5831
+ loading
5832
+ }) => {
5833
+ const summary = (0, import_react12.useMemo)(() => summarizeUsage(usage, models), [usage, models]);
5834
+ const keyStats = (0, import_react12.useMemo)(
5835
+ () => ({
5836
+ idleKeys: keys.filter((k) => (k.spend ?? 0) === 0).length,
5837
+ uncappedKeys: keys.filter((k) => k.max_budget === void 0 || k.max_budget === null).length,
5838
+ totalKeys: keys.length
5839
+ }),
5840
+ [keys]
5841
+ );
5842
+ const score = (0, import_react12.useMemo)(
5843
+ () => scorePractices(summary, keyStats),
5844
+ [summary, keyStats]
5845
+ );
5846
+ const opportunities = (0, import_react12.useMemo)(() => {
5847
+ const opps = modelOpportunities(usage, models);
5848
+ return [...opps].sort(
5849
+ (a, b) => (a.flag === "ok" ? 1 : 0) - (b.flag === "ok" ? 1 : 0)
5850
+ );
5851
+ }, [usage, models]);
5852
+ const {
5853
+ value: spendLogs,
5854
+ loading: logsLoading,
5855
+ error: logsError,
5856
+ retry: retryLogs
5857
+ } = (0, import_react_use.useAsyncRetry)(async () => {
5858
+ return api.getSpendLogs({
5859
+ startDate: toLocalDay(dateRange.start),
5860
+ endDate: toLocalDay(dateRange.end),
5861
+ pageSize: 1e3
5862
+ });
5863
+ }, [api, dateRange]);
5864
+ const logStats = (0, import_react12.useMemo)(() => {
5865
+ const rows = spendLogs?.data ?? [];
5866
+ const hits = rows.filter((r) => r.cacheHit);
5867
+ const cachedPromptTokens = hits.reduce((s, r) => s + (r.promptTokens ?? 0), 0);
5868
+ const priced = models.map((m) => m.input_cost_per_token).filter((p) => p !== null && p !== void 0 && p > 0);
5869
+ const avgInputPrice = priced.length > 0 ? priced.reduce((s, p) => s + p, 0) / priced.length : null;
5870
+ return {
5871
+ total: spendLogs?.total ?? rows.length,
5872
+ hitRate: rows.length > 0 ? hits.length / rows.length : null,
5873
+ // Estimate: cached prompt tokens billed at the average listed input price.
5874
+ savedUsd: avgInputPrice !== null ? cachedPromptTokens * avgInputPrice : null,
5875
+ failedWasteUsd: rows.filter((r) => failedRow(r.status)).reduce((s, r) => s + (r.spend ?? 0), 0),
5876
+ truncated: spendLogs?.truncated ?? false
5877
+ };
5878
+ }, [spendLogs, models]);
5879
+ const renderSpendLogsBody = () => {
5880
+ if (logsLoading) return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Skeleton6.default, { variant: "rounded", height: 120 });
5881
+ if (logsError) {
5882
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_Alert5.default, { severity: "error", children: [
5883
+ "Failed to load spend logs: ",
5884
+ logsError.message || "Unknown error",
5885
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Box16.default, { sx: { mt: 1 }, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Button7.default, { size: "small", onClick: retryLogs, children: "Retry" }) })
5886
+ ] });
5887
+ }
5888
+ if ((spendLogs?.data ?? []).length === 0) {
5889
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(EmptyState, { message: "No spend logs in this period" });
5890
+ }
5891
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_Box16.default, { sx: { display: "flex", gap: 3, flexWrap: "wrap" }, children: [
5892
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Stat, { label: "Cache-hit rate", value: fmtPct1(logStats.hitRate) }),
5893
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
5894
+ Stat,
5762
5895
  {
5763
- variant: "body2",
5764
- component: "code",
5765
- color: "text.secondary",
5766
- title: entry.object_id ?? void 0,
5767
- sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 12 },
5768
- children: entry.object_id ? entry.object_id.slice(0, 20) + (entry.object_id.length > 20 ? "\u2026" : "") : "\u2014"
5896
+ label: "Cache savings (est.)",
5897
+ value: logStats.savedUsd !== null ? fmtUsd(logStats.savedUsd) : "\u2014"
5769
5898
  }
5770
- ) }),
5771
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { children: entry.changed_by ?? "\u2014" })
5772
- ] }),
5773
- hasDetail && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableRow6.default, { sx: { "&&:hover": { bgcolor: "transparent" } }, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { colSpan: 6, sx: { "&&": { py: 0, border: 0 } }, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Collapse3.default, { in: open, unmountOnExit: true, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
5774
- import_Box16.default,
5899
+ ),
5900
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Stat, { label: "Failed-request waste", value: fmtUsd(logStats.failedWasteUsd) }),
5901
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Stat, { label: "Log rows", value: fmtInt(logStats.total) }),
5902
+ logStats.truncated && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Typography14.default, { variant: "caption", color: "text.secondary", children: "Showing the first 1,000 rows \u2014 figures are a lower bound." })
5903
+ ] });
5904
+ };
5905
+ const kpis = [
5906
+ {
5907
+ label: "Cost / 1k tokens",
5908
+ value: summary.costPer1kTokens !== null ? fmtUsd(summary.costPer1kTokens) : "\u2014",
5909
+ tone: "accent"
5910
+ },
5911
+ {
5912
+ label: "Cost / request",
5913
+ value: summary.costPerRequest !== null ? fmtUsd(summary.costPerRequest) : "\u2014",
5914
+ tone: "info"
5915
+ },
5916
+ {
5917
+ label: "Out : in ratio",
5918
+ value: fmtRatio(summary.outputInputRatio),
5919
+ tone: "neutral"
5920
+ },
5921
+ {
5922
+ label: "Retry waste",
5923
+ value: fmtUsd(summary.retryWasteUsd),
5924
+ hint: `${fmtPct1(summary.retryWasteShare)} of spend`,
5925
+ tone: "warning"
5926
+ },
5927
+ {
5928
+ label: "Success rate",
5929
+ value: fmtPct1(summary.successRate),
5930
+ tone: "success"
5931
+ }
5932
+ ];
5933
+ const renderEfficiencyBody = () => {
5934
+ if (loading) return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Skeleton6.default, { variant: "rounded", height: 96 });
5935
+ if (!usage) return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(EmptyState, { message: "No usage in this period" });
5936
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(MetricStrip, { metrics: kpis });
5937
+ };
5938
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_Box16.default, { sx: { display: "flex", flexDirection: "column", gap: 2 }, children: [
5939
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
5940
+ SectionCard,
5775
5941
  {
5776
- display: "flex",
5777
- gap: 2,
5778
- flexWrap: "wrap",
5779
- sx: (theme) => ({
5780
- p: 2,
5781
- mb: 1.5,
5782
- borderRadius: 1.5,
5783
- bgcolor: (0, import_styles12.alpha)(theme.palette.text.primary, theme.palette.mode === "dark" ? 0.05 : 0.03)
5784
- }),
5785
- children: [
5786
- entry.before_value && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_Box16.default, { flex: 1, minWidth: 200, children: [
5787
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
5788
- import_Typography14.default,
5789
- {
5790
- variant: "caption",
5791
- color: "text.secondary",
5792
- display: "block",
5793
- mb: 0.75,
5794
- sx: { fontSize: 10.5, fontWeight: 700, letterSpacing: "0.07em", textTransform: "uppercase" },
5795
- children: "Before"
5796
- }
5797
- ),
5798
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Typography14.default, { 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) })
5942
+ title: "Efficiency",
5943
+ subtitle: `${dateRange.start.toLocaleDateString()} \u2013 ${dateRange.end.toLocaleDateString()}`,
5944
+ children: renderEfficiencyBody()
5945
+ }
5946
+ ),
5947
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_Grid2.default, { container: true, spacing: 2, alignItems: "flex-start", children: [
5948
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Grid2.default, { item: true, xs: 12, md: 5, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(SectionCard, { title: "Practice score", subtitle: "Out of 100", children: loading ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Skeleton6.default, { variant: "rounded", height: 160 }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_Box16.default, { sx: { display: "flex", flexDirection: "column", gap: 1.5 }, children: [
5949
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_Typography14.default, { sx: { fontSize: 30, fontWeight: 700, fontVariantNumeric: "tabular-nums" }, children: [
5950
+ score.total,
5951
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Typography14.default, { component: "span", color: "text.secondary", sx: { fontSize: 16 }, children: "/100" })
5952
+ ] }),
5953
+ score.checks.map((c) => /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_Box16.default, { children: [
5954
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_Box16.default, { sx: { display: "flex", justifyContent: "space-between", gap: 1, mb: 0.5 }, children: [
5955
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Typography14.default, { variant: "body2", sx: { fontWeight: 600 }, children: c.label }),
5956
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_Typography14.default, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" }, children: [
5957
+ c.points,
5958
+ "/",
5959
+ c.maxPoints
5960
+ ] })
5799
5961
  ] }),
5800
- entry.updated_values && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_Box16.default, { flex: 1, minWidth: 200, children: [
5801
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
5802
- import_Typography14.default,
5803
- {
5804
- variant: "caption",
5805
- color: "text.secondary",
5806
- display: "block",
5807
- mb: 0.75,
5808
- sx: { fontSize: 10.5, fontWeight: 700, letterSpacing: "0.07em", textTransform: "uppercase" },
5809
- children: "After"
5810
- }
5811
- ),
5812
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Typography14.default, { 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) })
5813
- ] })
5814
- ]
5962
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
5963
+ Meter,
5964
+ {
5965
+ value: c.maxPoints > 0 ? c.points / c.maxPoints * 100 : 0,
5966
+ ariaLabel: `${c.label}: ${c.points} of ${c.maxPoints}`
5967
+ }
5968
+ ),
5969
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Typography14.default, { variant: "caption", color: "text.secondary", children: c.detail })
5970
+ ] }, c.id))
5971
+ ] }) }) }),
5972
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Grid2.default, { item: true, xs: 12, md: 7, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(SectionCard, { title: "Cache & errors", subtitle: "From spend logs (estimate)", children: renderSpendLogsBody() }) })
5973
+ ] }),
5974
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(SectionCard, { title: "Model opportunities", flush: true, children: loading ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Box16.default, { sx: { p: 2.5 }, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Skeleton6.default, { variant: "rounded", height: 120 }) }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableContainer6.default, { children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_Table6.default, { size: "small", sx: dataTableSx, children: [
5975
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableHead6.default, { children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_TableRow6.default, { children: [
5976
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { children: "Model" }),
5977
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { align: "right", children: "Spend" }),
5978
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { align: "right", children: "Requests" }),
5979
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { align: "right", children: "Failure" }),
5980
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { align: "right", children: "Avg / req" }),
5981
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { children: "Signal" })
5982
+ ] }) }),
5983
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableBody6.default, { children: opportunities.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableRow6.default, { children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { colSpan: 6, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(EmptyState, { message: "No model activity in this period" }) }) }) : opportunities.map((o) => /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_TableRow6.default, { children: [
5984
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Typography14.default, { variant: "body2", sx: { fontWeight: 600 }, children: o.model }) }),
5985
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { align: "right", children: fmtUsd(o.spend) }),
5986
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { align: "right", children: fmtInt(o.requests) }),
5987
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { align: "right", children: fmtPct1(o.failureRate) }),
5988
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { align: "right", children: fmtUsd(o.avgCostPerRequest) }),
5989
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_TableCell6.default, { children: [
5990
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(StatusPill, { label: FLAG_LABEL[o.flag], tone: FLAG_TONE[o.flag], title: o.hint }),
5991
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Typography14.default, { variant: "caption", color: "text.secondary", sx: { display: "block", mt: 0.25 }, children: o.hint })
5992
+ ] })
5993
+ ] }, o.model)) })
5994
+ ] }) }) }),
5995
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(SectionCard, { title: "Quality scores", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
5996
+ EmptyState,
5997
+ {
5998
+ message: "Quality scores \u2014 connect Langfuse",
5999
+ hint: "LLM-as-judge evaluation is not configured for this deployment."
5815
6000
  }
5816
- ) }) }) })
6001
+ ) })
5817
6002
  ] });
5818
6003
  };
5819
- AuditLog = ({ api }) => {
5820
- const [page, setPage] = (0, import_react12.useState)(0);
5821
- const [pageSize, setPageSize] = (0, import_react12.useState)(25);
5822
- const [filters, setFilters] = (0, import_react12.useState)({});
5823
- const fetchParams = (0, import_react12.useMemo)(
5824
- () => ({ page: page + 1, page_size: pageSize, ...filters }),
5825
- [page, pageSize, filters]
5826
- );
5827
- const { value, loading, error } = (0, import_react_use.useAsync)(
5828
- () => api.getAuditLogs(fetchParams),
5829
- [api, fetchParams]
5830
- );
5831
- const entries = value?.audit_logs ?? [];
5832
- const total = value?.total ?? 0;
5833
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
5834
- SectionCard,
5835
- {
5836
- title: "Audit Log",
5837
- subtitle: loading ? "Loading\u2026" : `${total.toLocaleString()} event${total === 1 ? "" : "s"}`,
5838
- flush: true,
5839
- actions: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
5840
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
5841
- import_TextField6.default,
5842
- {
5843
- size: "small",
5844
- label: "Action",
5845
- select: true,
5846
- value: filters.action ?? "",
5847
- onChange: (e) => {
5848
- setFilters((f) => ({ ...f, action: e.target.value || void 0 }));
5849
- setPage(0);
5850
- },
5851
- sx: { minWidth: 150 },
5852
- children: [
5853
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_MenuItem5.default, { value: "", children: "All actions" }),
5854
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_MenuItem5.default, { value: "created", children: "Created" }),
5855
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_MenuItem5.default, { value: "updated", children: "Updated" }),
5856
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_MenuItem5.default, { value: "deleted", children: "Deleted" }),
5857
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_MenuItem5.default, { value: "blocked", children: "Blocked" })
5858
- ]
5859
- }
5860
- ),
5861
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
5862
- import_TextField6.default,
5863
- {
5864
- size: "small",
5865
- label: "Table",
5866
- select: true,
5867
- value: filters.table_name ?? "",
5868
- onChange: (e) => {
5869
- setFilters((f) => ({ ...f, table_name: e.target.value || void 0 }));
5870
- setPage(0);
5871
- },
5872
- sx: { minWidth: 150 },
5873
- children: [
5874
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_MenuItem5.default, { value: "", children: "All tables" }),
5875
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_MenuItem5.default, { value: "LiteLLM_VerificationToken", children: "Key" }),
5876
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_MenuItem5.default, { value: "LiteLLM_TeamTable", children: "Team" }),
5877
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_MenuItem5.default, { value: "LiteLLM_TeamMembership", children: "Team member" }),
5878
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_MenuItem5.default, { value: "LiteLLM_ObjectPermissionTable", children: "Team access (KB / MCP)" }),
5879
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_MenuItem5.default, { value: "LiteLLM_UserTable", children: "User" })
5880
- ]
5881
- }
5882
- ),
5883
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
5884
- import_TextField6.default,
5885
- {
5886
- size: "small",
5887
- label: "Changed by",
5888
- value: filters.changed_by ?? "",
5889
- onChange: (e) => {
5890
- setFilters((f) => ({ ...f, changed_by: e.target.value || void 0 }));
5891
- setPage(0);
5892
- },
5893
- sx: { minWidth: 180 }
5894
- }
5895
- )
5896
- ] }),
5897
- children: [
5898
- error && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Box16.default, { px: 2.5, pb: 2, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Alert5.default, { severity: "error", children: error.message }) }),
5899
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableContainer6.default, { children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_Table6.default, { size: "small", sx: dataTableSx, children: [
5900
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableHead6.default, { children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_TableRow6.default, { children: [
5901
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { sx: { width: 40 } }),
5902
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { children: "Time" }),
5903
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { children: "Action" }),
5904
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { children: "Table" }),
5905
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { children: "Object ID" }),
5906
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { children: "Changed By" })
5907
- ] }) }),
5908
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableBody6.default, { children: renderAuditLogBody(loading, entries) })
5909
- ] }) }),
5910
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
5911
- import_TablePagination.default,
5912
- {
5913
- component: "div",
5914
- count: total,
5915
- page,
5916
- onPageChange: (_, p) => setPage(p),
5917
- rowsPerPage: pageSize,
5918
- onRowsPerPageChange: (e) => {
5919
- setPageSize(Number(e.target.value));
5920
- setPage(0);
5921
- },
5922
- rowsPerPageOptions: [10, 25, 50],
5923
- sx: (theme) => ({ borderTop: `1px solid ${theme.palette.divider}` })
5924
- }
5925
- )
5926
- ]
5927
- }
5928
- );
5929
- };
5930
- }
5931
- });
5932
-
5933
- // src/dates.ts
5934
- function toLocalDay(d) {
5935
- const year = d.getFullYear();
5936
- const month = String(d.getMonth() + 1).padStart(2, "0");
5937
- const day = String(d.getDate()).padStart(2, "0");
5938
- return `${year}-${month}-${day}`;
5939
- }
5940
- function monthToDateRange(now = /* @__PURE__ */ new Date()) {
5941
- const start = new Date(now.getFullYear(), now.getMonth(), 1);
5942
- return {
5943
- startDate: toLocalDay(start),
5944
- endDate: toLocalDay(now)
5945
- };
5946
- }
5947
- var init_dates = __esm({
5948
- "src/dates.ts"() {
5949
- "use strict";
5950
6004
  }
5951
6005
  });
5952
6006
 
@@ -6058,18 +6112,18 @@ function initDateRange() {
6058
6112
  else start.setDate(start.getDate() - 7);
6059
6113
  return { start, end };
6060
6114
  }
6061
- var import_react13, import_Box17, import_Alert6, import_Button7, import_Typography15, import_Tabs2, import_Tab2, import_Grid2, import_Accordion2, import_AccordionSummary2, import_AccordionDetails2, import_ExpandMore, import_react_router_dom, import_react_use2, import_core_plugin_api5, import_core_components3, import_plugin_permission_react3, import_backstage_plugin_litellm_common5, import_jsx_runtime18, PERIOD_LS_KEY2, PAGE_TABS, isPageTab, LiteLLMPage;
6115
+ var import_react13, import_Box17, import_Alert6, import_Button8, import_Typography15, import_Tabs2, import_Tab2, import_Grid3, import_Accordion2, import_AccordionSummary2, import_AccordionDetails2, import_ExpandMore, import_react_router_dom, import_react_use2, import_core_plugin_api5, import_core_components3, import_plugin_permission_react3, import_backstage_plugin_litellm_common5, import_jsx_runtime18, PERIOD_LS_KEY2, PAGE_TABS, isPageTab, LiteLLMPage;
6062
6116
  var init_LiteLLMPage = __esm({
6063
6117
  "src/components/LiteLLMPage.tsx"() {
6064
6118
  "use strict";
6065
6119
  import_react13 = require("react");
6066
6120
  import_Box17 = __toESM(require("@mui/material/Box"));
6067
6121
  import_Alert6 = __toESM(require("@mui/material/Alert"));
6068
- import_Button7 = __toESM(require("@mui/material/Button"));
6122
+ import_Button8 = __toESM(require("@mui/material/Button"));
6069
6123
  import_Typography15 = __toESM(require("@mui/material/Typography"));
6070
6124
  import_Tabs2 = __toESM(require("@mui/material/Tabs"));
6071
6125
  import_Tab2 = __toESM(require("@mui/material/Tab"));
6072
- import_Grid2 = __toESM(require("@mui/material/Grid"));
6126
+ import_Grid3 = __toESM(require("@mui/material/Grid"));
6073
6127
  import_Accordion2 = __toESM(require("@mui/material/Accordion"));
6074
6128
  import_AccordionSummary2 = __toESM(require("@mui/material/AccordionSummary"));
6075
6129
  import_AccordionDetails2 = __toESM(require("@mui/material/AccordionDetails"));
@@ -6087,7 +6141,7 @@ var init_LiteLLMPage = __esm({
6087
6141
  init_LiteLLMBudgetWidget();
6088
6142
  init_TeamUsage();
6089
6143
  init_ModelsTable();
6090
- init_AuditLog();
6144
+ init_InsightsTab();
6091
6145
  init_api();
6092
6146
  init_dates();
6093
6147
  import_backstage_plugin_litellm_common5 = require("@acarmisc/backstage-plugin-litellm-common");
@@ -6096,7 +6150,7 @@ var init_LiteLLMPage = __esm({
6096
6150
  init_unprovisionedCopy();
6097
6151
  import_jsx_runtime18 = require("react/jsx-runtime");
6098
6152
  PERIOD_LS_KEY2 = "litellm_usage_period";
6099
- PAGE_TABS = ["overview", "keys", "teams", "models", "audit"];
6153
+ PAGE_TABS = ["overview", "keys", "teams", "models", "insights"];
6100
6154
  isPageTab = (v) => !!v && PAGE_TABS.includes(v);
6101
6155
  LiteLLMPage = () => {
6102
6156
  const api = (0, import_core_plugin_api5.useApi)(liteLlmApiRef);
@@ -6437,7 +6491,7 @@ var init_LiteLLMPage = __esm({
6437
6491
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_Tab2.default, { label: "Keys", value: "keys" }),
6438
6492
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_Tab2.default, { label: "Teams", value: "teams" }),
6439
6493
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_Tab2.default, { label: "Models", value: "models" }),
6440
- userInfo.can_view_audit && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_Tab2.default, { label: "Audit Log", value: "audit" })
6494
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_Tab2.default, { label: "Insights", value: "insights" })
6441
6495
  ]
6442
6496
  }
6443
6497
  );
@@ -6491,7 +6545,7 @@ var init_LiteLLMPage = __esm({
6491
6545
  }
6492
6546
  ),
6493
6547
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
6494
- import_Button7.default,
6548
+ import_Button8.default,
6495
6549
  {
6496
6550
  variant: "contained",
6497
6551
  color: "primary",
@@ -6503,8 +6557,8 @@ var init_LiteLLMPage = __esm({
6503
6557
  }
6504
6558
  ) });
6505
6559
  }
6506
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_Grid2.default, { container: true, spacing: 2, alignItems: "flex-start", children: [
6507
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_Grid2.default, { item: true, xs: 12, lg: 8, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
6560
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_Grid3.default, { container: true, spacing: 2, alignItems: "flex-start", children: [
6561
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_Grid3.default, { item: true, xs: 12, lg: 8, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
6508
6562
  UsageStats,
6509
6563
  {
6510
6564
  usage: usage ?? null,
@@ -6518,7 +6572,7 @@ var init_LiteLLMPage = __esm({
6518
6572
  userInfo
6519
6573
  }
6520
6574
  ) }),
6521
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_Grid2.default, { item: true, xs: 12, lg: 4, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(LiteLLMBudgetWidget, { compact: true, collapsible: true, userInfo: userInfo ?? null, teams, keys: keys ?? [] }) })
6575
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_Grid3.default, { item: true, xs: 12, lg: 4, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(LiteLLMBudgetWidget, { compact: true, collapsible: true, userInfo: userInfo ?? null, teams, keys: keys ?? [] }) })
6522
6576
  ] });
6523
6577
  })(),
6524
6578
  activeTab === "keys" && (() => {
@@ -6526,7 +6580,7 @@ var init_LiteLLMPage = __esm({
6526
6580
  return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_Alert6.default, { severity: "error", children: [
6527
6581
  "Failed to load keys: ",
6528
6582
  keysError.message || "Unknown error",
6529
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_Box17.default, { sx: { mt: 1 }, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_Button7.default, { size: "small", onClick: refreshKeys, children: "Retry" }) })
6583
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_Box17.default, { sx: { mt: 1 }, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_Button8.default, { size: "small", onClick: refreshKeys, children: "Retry" }) })
6530
6584
  ] });
6531
6585
  }
6532
6586
  return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
@@ -6550,7 +6604,7 @@ var init_LiteLLMPage = __esm({
6550
6604
  return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_Alert6.default, { severity: "error", children: [
6551
6605
  "Failed to load teams: ",
6552
6606
  teamsError.message || "Unknown error",
6553
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_Box17.default, { sx: { mt: 1 }, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_Button7.default, { size: "small", onClick: refreshTeams, children: "Retry" }) })
6607
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_Box17.default, { sx: { mt: 1 }, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_Button8.default, { size: "small", onClick: refreshTeams, children: "Retry" }) })
6554
6608
  ] });
6555
6609
  }
6556
6610
  const teamsById = /* @__PURE__ */ new Map();
@@ -6587,7 +6641,7 @@ var init_LiteLLMPage = __esm({
6587
6641
  return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_Alert6.default, { severity: "error", children: [
6588
6642
  "Failed to load models: ",
6589
6643
  modelsError.message || "Unknown error",
6590
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_Box17.default, { sx: { mt: 1 }, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_Button7.default, { size: "small", onClick: refreshModels, children: "Retry" }) })
6644
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_Box17.default, { sx: { mt: 1 }, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_Button8.default, { size: "small", onClick: refreshModels, children: "Retry" }) })
6591
6645
  ] });
6592
6646
  }
6593
6647
  return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
@@ -6599,7 +6653,17 @@ var init_LiteLLMPage = __esm({
6599
6653
  }
6600
6654
  );
6601
6655
  })(),
6602
- activeTab === "audit" && userInfo.can_view_audit && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(AuditLog, { api })
6656
+ activeTab === "insights" && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
6657
+ InsightsTab,
6658
+ {
6659
+ api,
6660
+ usage: usage ?? null,
6661
+ models: allModels ?? [],
6662
+ keys: keys ?? [],
6663
+ dateRange,
6664
+ loading: usageLoading
6665
+ }
6666
+ )
6603
6667
  ] }),
6604
6668
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
6605
6669
  KeyFormDialog,
@@ -6758,6 +6822,10 @@ function postToLoopback(redirectUri, fields) {
6758
6822
  document.body.appendChild(form);
6759
6823
  form.submit();
6760
6824
  }
6825
+ function initialTeams(all, have) {
6826
+ const missing = all.filter((id) => !have.has(id));
6827
+ return new Set(have.size ? missing : all);
6828
+ }
6761
6829
  var init_connect = __esm({
6762
6830
  "src/connect.ts"() {
6763
6831
  "use strict";
@@ -6769,14 +6837,14 @@ var ConnectPage_exports = {};
6769
6837
  __export(ConnectPage_exports, {
6770
6838
  ConnectPage: () => ConnectPage
6771
6839
  });
6772
- var import_react14, import_Alert7, import_Box18, import_Button8, import_Checkbox3, import_FormControlLabel3, import_FormGroup, import_Paper9, import_Typography16, import_react_router_dom2, import_react_use3, import_core_plugin_api6, import_core_components4, import_jsx_runtime19, ConnectPage;
6840
+ var import_react14, import_Alert7, import_Box18, import_Button9, import_Checkbox3, import_FormControlLabel3, import_FormGroup, import_Paper9, import_Typography16, import_react_router_dom2, import_react_use3, import_core_plugin_api6, import_core_components4, import_jsx_runtime19, ConnectPage;
6773
6841
  var init_ConnectPage = __esm({
6774
6842
  "src/components/ConnectPage.tsx"() {
6775
6843
  "use strict";
6776
6844
  import_react14 = require("react");
6777
6845
  import_Alert7 = __toESM(require("@mui/material/Alert"));
6778
6846
  import_Box18 = __toESM(require("@mui/material/Box"));
6779
- import_Button8 = __toESM(require("@mui/material/Button"));
6847
+ import_Button9 = __toESM(require("@mui/material/Button"));
6780
6848
  import_Checkbox3 = __toESM(require("@mui/material/Checkbox"));
6781
6849
  import_FormControlLabel3 = __toESM(require("@mui/material/FormControlLabel"));
6782
6850
  import_FormGroup = __toESM(require("@mui/material/FormGroup"));
@@ -6795,6 +6863,7 @@ var init_ConnectPage = __esm({
6795
6863
  const redirectUri = params.get("redirect_uri");
6796
6864
  const state = params.get("state") ?? "";
6797
6865
  const client = params.get("client") || "a local application";
6866
+ const have = new Set((params.get("have") ?? "").split(",").filter(Boolean));
6798
6867
  const teams = (0, import_react_use3.useAsync)(() => api.getTeams(), [api]);
6799
6868
  const [selected, setSelected] = (0, import_react14.useState)();
6800
6869
  const [busy, setBusy] = (0, import_react14.useState)(false);
@@ -6804,7 +6873,7 @@ var init_ConnectPage = __esm({
6804
6873
  }
6805
6874
  if (teams.loading) return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_core_components4.Progress, {});
6806
6875
  const all = teams.value ?? [];
6807
- const chosen = selected ?? new Set(all.map((t) => t.team_id));
6876
+ const chosen = selected ?? initialTeams(all.map((t) => t.team_id), have);
6808
6877
  const toggle = (id) => {
6809
6878
  const next = new Set(chosen);
6810
6879
  if (!next.delete(id)) next.add(id);
@@ -6830,7 +6899,7 @@ var init_ConnectPage = __esm({
6830
6899
  ] }),
6831
6900
  /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_Typography16.default, { variant: "body2", color: "text.secondary", paragraph: true, children: [
6832
6901
  client,
6833
- " will receive an API key for each selected team. Connecting again replaces the keys created by a previous connection."
6902
+ " will receive an API key for each selected team. Selecting a team that is already connected replaces its key, also on your other devices."
6834
6903
  ] }),
6835
6904
  teams.error && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_Alert7.default, { severity: "error", children: [
6836
6905
  "Could not load your teams: ",
@@ -6840,13 +6909,13 @@ var init_ConnectPage = __esm({
6840
6909
  import_FormControlLabel3.default,
6841
6910
  {
6842
6911
  control: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_Checkbox3.default, { checked: chosen.has(t.team_id), onChange: () => toggle(t.team_id) }),
6843
- label: t.team_alias ?? t.team_id
6912
+ label: `${t.team_alias ?? t.team_id}${have.has(t.team_id) ? " (already connected)" : ""}`
6844
6913
  },
6845
6914
  t.team_id
6846
6915
  )) }),
6847
6916
  error && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_Alert7.default, { severity: "error", sx: { mb: 2 }, children: error }),
6848
6917
  /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
6849
- import_Button8.default,
6918
+ import_Button9.default,
6850
6919
  {
6851
6920
  variant: "contained",
6852
6921
  onClick: connect,
@@ -6886,7 +6955,7 @@ __export(index_exports, {
6886
6955
  module.exports = __toCommonJS(index_exports);
6887
6956
 
6888
6957
  // src/plugin.tsx
6889
- var import_icons_material10 = require("@mui/icons-material");
6958
+ var import_icons_material9 = require("@mui/icons-material");
6890
6959
  var import_frontend_plugin_api4 = require("@backstage/frontend-plugin-api");
6891
6960
  init_api();
6892
6961
  init_routes();
@@ -6902,7 +6971,7 @@ var liteLlmPage = import_frontend_plugin_api4.PageBlueprint.make({
6902
6971
  params: {
6903
6972
  path: "/litellm",
6904
6973
  title: "LiteLLM",
6905
- icon: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_icons_material10.TrendingUp, {}),
6974
+ icon: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_icons_material9.TrendingUp, {}),
6906
6975
  routeRef: rootRouteRef,
6907
6976
  loader: async () => {
6908
6977
  const { LiteLLMPage: LiteLLMPage2 } = await Promise.resolve().then(() => (init_LiteLLMPage(), LiteLLMPage_exports));
@@ -6942,7 +7011,7 @@ var import_Box19 = __toESM(require("@mui/material/Box"));
6942
7011
  var import_Typography17 = __toESM(require("@mui/material/Typography"));
6943
7012
  var import_FormControl2 = __toESM(require("@mui/material/FormControl"));
6944
7013
  var import_Select2 = __toESM(require("@mui/material/Select"));
6945
- var import_MenuItem6 = __toESM(require("@mui/material/MenuItem"));
7014
+ var import_MenuItem5 = __toESM(require("@mui/material/MenuItem"));
6946
7015
  var import_Skeleton7 = __toESM(require("@mui/material/Skeleton"));
6947
7016
  var import_recharts3 = require("recharts");
6948
7017
  var import_core_plugin_api7 = require("@backstage/core-plugin-api");
@@ -7079,9 +7148,9 @@ var LiteLLMHomeWidget = ({
7079
7148
  displayEmpty: true,
7080
7149
  inputProps: { "aria-label": "Usage period" },
7081
7150
  children: [
7082
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_MenuItem6.default, { value: "today", children: "Today" }),
7083
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_MenuItem6.default, { value: "7d", children: "7d" }),
7084
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_MenuItem6.default, { value: "30d", children: "30d" })
7151
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_MenuItem5.default, { value: "today", children: "Today" }),
7152
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_MenuItem5.default, { value: "7d", children: "7d" }),
7153
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_MenuItem5.default, { value: "30d", children: "30d" })
7085
7154
  ]
7086
7155
  }
7087
7156
  ) })
@@ -7152,13 +7221,13 @@ var import_react16 = require("react");
7152
7221
  var import_frontend_plugin_api6 = require("@backstage/frontend-plugin-api");
7153
7222
  var import_Paper11 = __toESM(require("@mui/material/Paper"));
7154
7223
  var import_Box20 = __toESM(require("@mui/material/Box"));
7155
- var import_Button9 = __toESM(require("@mui/material/Button"));
7224
+ var import_Button10 = __toESM(require("@mui/material/Button"));
7156
7225
  var import_Typography18 = __toESM(require("@mui/material/Typography"));
7157
7226
  var import_Skeleton8 = __toESM(require("@mui/material/Skeleton"));
7158
- var import_Collapse4 = __toESM(require("@mui/material/Collapse"));
7159
- var import_styles13 = require("@mui/material/styles");
7227
+ var import_Collapse3 = __toESM(require("@mui/material/Collapse"));
7228
+ var import_styles12 = require("@mui/material/styles");
7160
7229
  var import_useMediaQuery = __toESM(require("@mui/material/useMediaQuery"));
7161
- var import_icons_material11 = require("@mui/icons-material");
7230
+ var import_icons_material10 = require("@mui/icons-material");
7162
7231
  var import_recharts4 = require("recharts");
7163
7232
  var import_core_plugin_api8 = require("@backstage/core-plugin-api");
7164
7233
  var import_core_components6 = require("@backstage/core-components");
@@ -7569,7 +7638,7 @@ var LiteLLMBudgetGauges = ({
7569
7638
  keys: propKeys
7570
7639
  }) => {
7571
7640
  const api = (0, import_core_plugin_api8.useApi)(liteLlmApiRef);
7572
- const theme = (0, import_styles13.useTheme)();
7641
+ const theme = (0, import_styles12.useTheme)();
7573
7642
  const isSmallScreen = (0, import_useMediaQuery.default)(theme.breakpoints.down("sm"));
7574
7643
  const gaugeSize = isSmallScreen ? Math.min(48, size) : size;
7575
7644
  const expandedRegionId = (0, import_react16.useId)();
@@ -7670,10 +7739,10 @@ var LiteLLMBudgetGauges = ({
7670
7739
  return onCreateKey ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(GenerateKeyButton, { size: "small", label, onClick: onCreateKey }, key) : /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(GenerateKeyButton, { size: "small", label, to: `${moduleHref}?generate=1` }, key);
7671
7740
  case "module":
7672
7741
  if (!moduleHref) return null;
7673
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_Button9.default, { size: "small", variant: "text", component: import_core_components6.Link, to: moduleHref, children: label }, key);
7742
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_Button10.default, { size: "small", variant: "text", component: import_core_components6.Link, to: moduleHref, children: label }, key);
7674
7743
  case "all-limits":
7675
7744
  return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
7676
- import_Button9.default,
7745
+ import_Button10.default,
7677
7746
  {
7678
7747
  size: "small",
7679
7748
  variant: "text",
@@ -7681,7 +7750,7 @@ var LiteLLMBudgetGauges = ({
7681
7750
  "aria-expanded": isExpanded,
7682
7751
  "aria-controls": expandedRegionId,
7683
7752
  endIcon: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
7684
- import_icons_material11.ExpandMore,
7753
+ import_icons_material10.ExpandMore,
7685
7754
  {
7686
7755
  sx: (muiTheme) => ({
7687
7756
  transform: isExpanded ? "rotate(180deg)" : "none",
@@ -7776,7 +7845,7 @@ var LiteLLMBudgetGauges = ({
7776
7845
  maxBudget: user?.max_budget ?? void 0
7777
7846
  }
7778
7847
  ) }),
7779
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_Collapse4.default, { in: isExpanded, unmountOnExit: true, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
7848
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_Collapse3.default, { in: isExpanded, unmountOnExit: true, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
7780
7849
  import_Box20.default,
7781
7850
  {
7782
7851
  id: expandedRegionId,