@acarmisc/backstage-plugin-litellm 0.37.0 → 0.39.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,25 @@ 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
+ }
331
+ async getQuality(params) {
332
+ const strParams = {
333
+ start_date: params.startDate,
334
+ end_date: params.endDate
335
+ };
336
+ if (params.userId) strParams.userId = params.userId;
337
+ return this.get("/insights/quality", strParams);
338
+ }
320
339
  async getConfig() {
321
340
  return this.get("/config");
322
341
  }
@@ -715,7 +734,7 @@ var init_surfaces = __esm({
715
734
  import_styles3 = require("@mui/material/styles");
716
735
  init_tokens();
717
736
  import_jsx_runtime3 = require("react/jsx-runtime");
718
- SectionCard = ({ title, subtitle, actions, flush = false, children }) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_Paper2.default, { sx: { borderRadius: 2, overflow: "hidden" }, children: [
737
+ SectionCard = ({ title, subtitle, description, actions, flush = false, children }) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_Paper2.default, { sx: { borderRadius: 2, overflow: "hidden" }, children: [
719
738
  (title || actions) && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
720
739
  import_Box3.default,
721
740
  {
@@ -737,6 +756,7 @@ var init_surfaces = __esm({
737
756
  ]
738
757
  }
739
758
  ),
759
+ description && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_Typography2.default, { variant: "body2", color: "text.secondary", sx: { px: 2.5, pb: 1.5 }, children: description }),
740
760
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_Box3.default, { sx: flush ? void 0 : { px: 2.5, pb: 2.5, pt: title ? 0 : 2.5 }, children })
741
761
  ] });
742
762
  ChartCard = ({ title, meta, height = 240, children }) => (
@@ -5661,295 +5681,431 @@ var init_ModelsTable = __esm({
5661
5681
  }
5662
5682
  });
5663
5683
 
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;
5684
+ // src/insights.ts
5685
+ var costPer1k, summarizeUsage, scorePractices, modelOpportunities;
5686
+ var init_insights = __esm({
5687
+ "src/insights.ts"() {
5688
+ "use strict";
5689
+ costPer1k = (model, spend, tokens, models) => {
5690
+ const info = models?.find((m) => m.model_name === model);
5691
+ const listed = info?.input_cost_per_token !== void 0 && info?.input_cost_per_token !== null && info?.output_cost_per_token !== void 0 && info?.output_cost_per_token !== null ? (info.input_cost_per_token + info.output_cost_per_token) / 2 * 1e3 : null;
5692
+ if (listed !== null) return listed;
5693
+ return tokens > 0 ? spend / tokens * 1e3 : null;
5694
+ };
5695
+ summarizeUsage = (usage, models, premiumThresholdPer1k = 0.01) => {
5696
+ if (!usage) {
5697
+ return {
5698
+ costPer1kTokens: null,
5699
+ costPerRequest: null,
5700
+ outputInputRatio: null,
5701
+ retryWasteUsd: 0,
5702
+ retryWasteShare: 0,
5703
+ successRate: null,
5704
+ premiumShare: 0,
5705
+ premiumSpend: 0,
5706
+ topModelConcentration: 0,
5707
+ projectedMonthSpend: null,
5708
+ burnMultiple: null
5709
+ };
5710
+ }
5711
+ const {
5712
+ total_spend: spend,
5713
+ total_tokens: tokens,
5714
+ prompt_tokens: prompt,
5715
+ completion_tokens: completion,
5716
+ api_requests: reqs,
5717
+ successful_requests: ok,
5718
+ failed_requests: failed,
5719
+ usage_by_model: byModel
5720
+ } = usage;
5721
+ const wasteShare = reqs > 0 ? failed / reqs : 0;
5722
+ const entries = Object.entries(byModel ?? {});
5723
+ const topSpend = entries.reduce((m, [, d]) => Math.max(m, d.total_spend), 0);
5724
+ 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);
5725
+ const days = usage.daily_usage?.length ?? 0;
5726
+ const projected = days > 0 ? spend / days * 30 : null;
5727
+ return {
5728
+ costPer1kTokens: tokens > 0 ? spend / tokens * 1e3 : null,
5729
+ costPerRequest: reqs > 0 ? spend / reqs : null,
5730
+ outputInputRatio: prompt > 0 ? completion / prompt : null,
5731
+ retryWasteUsd: spend * wasteShare,
5732
+ retryWasteShare: wasteShare,
5733
+ successRate: reqs > 0 ? ok / reqs : null,
5734
+ premiumShare: spend > 0 ? premiumSpend / spend : 0,
5735
+ premiumSpend,
5736
+ topModelConcentration: spend > 0 ? topSpend / spend : 0,
5737
+ projectedMonthSpend: projected,
5738
+ burnMultiple: projected !== null && spend > 0 ? projected / spend : null
5739
+ };
5740
+ };
5741
+ scorePractices = (summary, keyStats) => {
5742
+ const band = (v, limits) => {
5743
+ for (const [max, pts] of limits) {
5744
+ if (v <= max) return pts;
5745
+ }
5746
+ return 0;
5747
+ };
5748
+ const waste = band(summary.retryWasteShare, [[0.01, 30], [0.05, 20], [0.1, 10]]);
5749
+ const premium = band(summary.premiumShare, [[0.2, 25], [0.5, 15], [0.8, 5]]);
5750
+ const conc = band(summary.topModelConcentration, [[0.5, 20], [0.8, 10]]);
5751
+ const bad = keyStats.totalKeys > 0 ? (keyStats.idleKeys + keyStats.uncappedKeys) / keyStats.totalKeys : 0;
5752
+ const hygiene = keyStats.totalKeys === 0 ? 25 : band(bad, [[0, 25], [0.2, 15], [0.5, 5]]);
5753
+ const checks = [
5754
+ { id: "retry-waste", label: "Low retry waste", points: waste, maxPoints: 30, detail: `${(summary.retryWasteShare * 100).toFixed(1)}% of requests failed` },
5755
+ { id: "premium-mix", label: "Balanced model mix", points: premium, maxPoints: 25, detail: `${(summary.premiumShare * 100).toFixed(1)}% of spend on premium models` },
5756
+ { id: "concentration", label: "No single-model lock-in", points: conc, maxPoints: 20, detail: `Top model is ${(summary.topModelConcentration * 100).toFixed(1)}% of spend` },
5757
+ { id: "key-hygiene", label: "Key hygiene", points: hygiene, maxPoints: 25, detail: `${keyStats.idleKeys} idle, ${keyStats.uncappedKeys} uncapped of ${keyStats.totalKeys} keys` }
5758
+ ];
5759
+ return { total: checks.reduce((s, c) => s + c.points, 0), checks };
5760
+ };
5761
+ modelOpportunities = (usage, models, premiumThresholdPer1k = 0.01) => {
5762
+ if (!usage) return [];
5763
+ const entries = Object.entries(usage.usage_by_model ?? {});
5764
+ const overallRate = usage.api_requests > 0 ? usage.failed_requests / usage.api_requests : 0;
5765
+ return entries.map(([model, d]) => {
5766
+ const failureRate = d.api_requests > 0 ? d.failed_requests / d.api_requests : null;
5767
+ const avgCostPerRequest = d.api_requests > 0 ? d.total_spend / d.api_requests : 0;
5768
+ const completionShare = d.total_tokens > 0 ? d.completion_tokens / d.total_tokens : 0;
5769
+ const per1k = costPer1k(model, d.total_spend, d.total_tokens, models) ?? 0;
5770
+ const spendShare = usage.total_spend > 0 ? d.total_spend / usage.total_spend : 0;
5771
+ if (failureRate !== null && overallRate > 0 && failureRate > 2 * overallRate && d.api_requests >= 10) {
5772
+ 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` };
5773
+ }
5774
+ if (per1k > premiumThresholdPer1k && completionShare < 0.3 && spendShare > 0.05) {
5775
+ 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` };
5776
+ }
5777
+ return { model, spend: d.total_spend, requests: d.api_requests, failureRate, avgCostPerRequest, flag: "ok", hint: "No action" };
5778
+ });
5779
+ };
5669
5780
  }
5670
- return "info";
5671
- }
5672
- function prettyTableName(name) {
5673
- if (!name) return "\u2014";
5674
- return TABLE_LABELS[name] ?? name;
5781
+ });
5782
+
5783
+ // src/dates.ts
5784
+ function toLocalDay(d) {
5785
+ const year = d.getFullYear();
5786
+ const month = String(d.getMonth() + 1).padStart(2, "0");
5787
+ const day = String(d.getDate()).padStart(2, "0");
5788
+ return `${year}-${month}-${day}`;
5675
5789
  }
5676
- function formatDateTime(iso) {
5677
- try {
5678
- return new Date(iso).toLocaleString();
5679
- } catch {
5680
- return iso;
5681
- }
5790
+ function monthToDateRange(now = /* @__PURE__ */ new Date()) {
5791
+ const start = new Date(now.getFullYear(), now.getMonth(), 1);
5792
+ return {
5793
+ startDate: toLocalDay(start),
5794
+ endDate: toLocalDay(now)
5795
+ };
5682
5796
  }
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
- ) }) });
5797
+ var init_dates = __esm({
5798
+ "src/dates.ts"() {
5799
+ "use strict";
5695
5800
  }
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"() {
5801
+ });
5802
+
5803
+ // src/components/InsightsTab.tsx
5804
+ 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;
5805
+ var init_InsightsTab = __esm({
5806
+ "src/components/InsightsTab.tsx"() {
5701
5807
  "use strict";
5702
5808
  import_react12 = require("react");
5703
5809
  import_Box16 = __toESM(require("@mui/material/Box"));
5810
+ import_Grid2 = __toESM(require("@mui/material/Grid"));
5704
5811
  import_Typography14 = __toESM(require("@mui/material/Typography"));
5812
+ import_Alert5 = __toESM(require("@mui/material/Alert"));
5813
+ import_Button7 = __toESM(require("@mui/material/Button"));
5705
5814
  import_Table6 = __toESM(require("@mui/material/Table"));
5706
5815
  import_TableBody6 = __toESM(require("@mui/material/TableBody"));
5707
5816
  import_TableCell6 = __toESM(require("@mui/material/TableCell"));
5708
5817
  import_TableContainer6 = __toESM(require("@mui/material/TableContainer"));
5709
5818
  import_TableHead6 = __toESM(require("@mui/material/TableHead"));
5710
5819
  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
5820
  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
5821
  import_react_use = require("react-use");
5822
+ init_insights();
5823
+ init_format();
5824
+ init_dates();
5721
5825
  init_ui();
5722
5826
  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"
5827
+ FLAG_TONE = {
5828
+ "failure-outlier": "danger",
5829
+ "downgrade-candidate": "warning",
5830
+ ok: "success"
5729
5831
  };
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"
5832
+ FLAG_LABEL = {
5833
+ "failure-outlier": "Failure outlier",
5834
+ "downgrade-candidate": "Downgrade candidate",
5835
+ ok: "OK"
5736
5836
  };
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,
5837
+ fmtRatio = (n) => n === null || Number.isNaN(n) ? "\u2014" : `${n.toFixed(2)}\xD7`;
5838
+ fmtPct1 = (n) => n === null || Number.isNaN(n) ? "\u2014" : `${(n * 100).toFixed(1)}%`;
5839
+ failedRow = (status) => /fail|error/i.test(status ?? "");
5840
+ InsightsTab = ({
5841
+ api,
5842
+ usage,
5843
+ models,
5844
+ keys,
5845
+ dateRange,
5846
+ loading
5847
+ }) => {
5848
+ const summary = (0, import_react12.useMemo)(() => summarizeUsage(usage, models), [usage, models]);
5849
+ const keyStats = (0, import_react12.useMemo)(
5850
+ () => ({
5851
+ idleKeys: keys.filter((k) => (k.spend ?? 0) === 0).length,
5852
+ uncappedKeys: keys.filter((k) => k.max_budget === void 0 || k.max_budget === null).length,
5853
+ totalKeys: keys.length
5854
+ }),
5855
+ [keys]
5856
+ );
5857
+ const score = (0, import_react12.useMemo)(
5858
+ () => scorePractices(summary, keyStats),
5859
+ [summary, keyStats]
5860
+ );
5861
+ const opportunities = (0, import_react12.useMemo)(() => {
5862
+ const opps = modelOpportunities(usage, models);
5863
+ return [...opps].sort(
5864
+ (a, b) => (a.flag === "ok" ? 1 : 0) - (b.flag === "ok" ? 1 : 0)
5865
+ );
5866
+ }, [usage, models]);
5867
+ const {
5868
+ value: spendLogs,
5869
+ loading: logsLoading,
5870
+ error: logsError,
5871
+ retry: retryLogs
5872
+ } = (0, import_react_use.useAsyncRetry)(async () => {
5873
+ return api.getSpendLogs({
5874
+ startDate: toLocalDay(dateRange.start),
5875
+ endDate: toLocalDay(dateRange.end),
5876
+ pageSize: 1e3
5877
+ });
5878
+ }, [api, dateRange]);
5879
+ const {
5880
+ value: quality,
5881
+ loading: qualityLoading,
5882
+ error: qualityError,
5883
+ retry: retryQuality
5884
+ } = (0, import_react_use.useAsyncRetry)(async () => {
5885
+ if (typeof api.getQuality !== "function") return { configured: false };
5886
+ return api.getQuality({
5887
+ startDate: toLocalDay(dateRange.start),
5888
+ endDate: toLocalDay(dateRange.end)
5889
+ });
5890
+ }, [api, dateRange]);
5891
+ const renderQualityBody = () => {
5892
+ if (qualityLoading) return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Skeleton6.default, { variant: "rounded", height: 120 });
5893
+ if (qualityError) {
5894
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_Alert5.default, { severity: "error", children: [
5895
+ "Failed to load quality scores: ",
5896
+ qualityError.message || "Unknown error",
5897
+ /* @__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: retryQuality, children: "Retry" }) })
5898
+ ] });
5899
+ }
5900
+ if (!quality || quality.configured === false) {
5901
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
5902
+ EmptyState,
5762
5903
  {
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"
5904
+ message: "Quality scores \u2014 connect Langfuse",
5905
+ hint: "LLM-as-judge evaluation is not configured for this deployment."
5769
5906
  }
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,
5775
- {
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) })
5799
- ] }),
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) })
5907
+ );
5908
+ }
5909
+ if (quality.byModel.length === 0 && quality.scores.length === 0) {
5910
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(EmptyState, { message: "No quality data in this period" });
5911
+ }
5912
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_Box16.default, { sx: { display: "flex", flexDirection: "column", gap: 2 }, children: [
5913
+ quality.byModel.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Box16.default, { sx: { display: "flex", flexDirection: "column", gap: 1.5 }, children: quality.byModel.map((m) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Box16.default, { children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_Box16.default, { sx: { display: "flex", justifyContent: "space-between", gap: 1, mb: 0.5 }, children: [
5914
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Typography14.default, { variant: "body2", sx: { fontWeight: 600 }, children: m.model }),
5915
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_Typography14.default, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" }, children: [
5916
+ fmtUsd(m.cost),
5917
+ " \xB7 ",
5918
+ fmtInt(m.tokens),
5919
+ " tokens"
5920
+ ] })
5921
+ ] }) }, m.model)) }),
5922
+ quality.scores.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Box16.default, { sx: { display: "flex", flexDirection: "column", gap: 1.5 }, children: quality.scores.map((s) => /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_Box16.default, { children: [
5923
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_Box16.default, { sx: { display: "flex", justifyContent: "space-between", gap: 1, mb: 0.5 }, children: [
5924
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Typography14.default, { variant: "body2", sx: { fontWeight: 600 }, children: s.name }),
5925
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_Typography14.default, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" }, children: [
5926
+ s.avg.toFixed(2),
5927
+ s.count > 0 ? ` \xB7 n=${fmtInt(s.count)}` : ""
5813
5928
  ] })
5814
- ]
5815
- }
5816
- ) }) }) })
5817
- ] });
5818
- };
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
- ),
5929
+ ] }),
5883
5930
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
5884
- import_TextField6.default,
5931
+ Meter,
5885
5932
  {
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 }
5933
+ value: s.avg <= 1 ? s.avg * 100 : s.avg,
5934
+ ariaLabel: `${s.name}: average ${s.avg.toFixed(2)}`
5894
5935
  }
5895
5936
  )
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: [
5937
+ ] }, s.name)) })
5938
+ ] });
5939
+ };
5940
+ const logStats = (0, import_react12.useMemo)(() => {
5941
+ const rows = spendLogs?.data ?? [];
5942
+ const hits = rows.filter((r) => r.cacheHit);
5943
+ const cachedPromptTokens = hits.reduce((s, r) => s + (r.promptTokens ?? 0), 0);
5944
+ const priced = models.map((m) => m.input_cost_per_token).filter((p) => p !== null && p !== void 0 && p > 0);
5945
+ const avgInputPrice = priced.length > 0 ? priced.reduce((s, p) => s + p, 0) / priced.length : null;
5946
+ return {
5947
+ total: spendLogs?.total ?? rows.length,
5948
+ hitRate: rows.length > 0 ? hits.length / rows.length : null,
5949
+ // Estimate: cached prompt tokens billed at the average listed input price.
5950
+ savedUsd: avgInputPrice !== null ? cachedPromptTokens * avgInputPrice : null,
5951
+ failedWasteUsd: rows.filter((r) => failedRow(r.status)).reduce((s, r) => s + (r.spend ?? 0), 0),
5952
+ truncated: spendLogs?.truncated ?? false
5953
+ };
5954
+ }, [spendLogs, models]);
5955
+ const renderSpendLogsBody = () => {
5956
+ if (logsLoading) return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Skeleton6.default, { variant: "rounded", height: 120 });
5957
+ if (logsError) {
5958
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_Alert5.default, { severity: "error", children: [
5959
+ "Failed to load spend logs: ",
5960
+ logsError.message || "Unknown error",
5961
+ /* @__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" }) })
5962
+ ] });
5963
+ }
5964
+ if ((spendLogs?.data ?? []).length === 0) {
5965
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(EmptyState, { message: "No spend logs in this period" });
5966
+ }
5967
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_Box16.default, { sx: { display: "flex", gap: 3, flexWrap: "wrap" }, children: [
5968
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Stat, { label: "Cache-hit rate", value: fmtPct1(logStats.hitRate) }),
5969
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
5970
+ Stat,
5971
+ {
5972
+ label: "Cache savings (est.)",
5973
+ value: logStats.savedUsd !== null ? fmtUsd(logStats.savedUsd) : "\u2014"
5974
+ }
5975
+ ),
5976
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Stat, { label: "Failed-request waste", value: fmtUsd(logStats.failedWasteUsd) }),
5977
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Stat, { label: "Log rows", value: fmtInt(logStats.total) }),
5978
+ 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." })
5979
+ ] });
5980
+ };
5981
+ const kpis = [
5982
+ {
5983
+ label: "Cost / 1k tokens",
5984
+ value: summary.costPer1kTokens !== null ? fmtUsd(summary.costPer1kTokens) : "\u2014",
5985
+ tone: "accent"
5986
+ },
5987
+ {
5988
+ label: "Cost / request",
5989
+ value: summary.costPerRequest !== null ? fmtUsd(summary.costPerRequest) : "\u2014",
5990
+ tone: "info"
5991
+ },
5992
+ {
5993
+ label: "Out : in ratio",
5994
+ value: fmtRatio(summary.outputInputRatio),
5995
+ tone: "neutral"
5996
+ },
5997
+ {
5998
+ label: "Retry waste",
5999
+ value: fmtUsd(summary.retryWasteUsd),
6000
+ hint: `${fmtPct1(summary.retryWasteShare)} of spend`,
6001
+ tone: "warning"
6002
+ },
6003
+ {
6004
+ label: "Success rate",
6005
+ value: fmtPct1(summary.successRate),
6006
+ tone: "success"
6007
+ }
6008
+ ];
6009
+ const renderEfficiencyBody = () => {
6010
+ if (loading) return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Skeleton6.default, { variant: "rounded", height: 96 });
6011
+ if (!usage) return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(EmptyState, { message: "No usage in this period" });
6012
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(MetricStrip, { metrics: kpis });
6013
+ };
6014
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_Box16.default, { sx: { display: "flex", flexDirection: "column", gap: 2 }, children: [
6015
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
6016
+ SectionCard,
6017
+ {
6018
+ title: "Efficiency",
6019
+ subtitle: `${dateRange.start.toLocaleDateString()} \u2013 ${dateRange.end.toLocaleDateString()}`,
6020
+ description: "What each token and request costs, and how much failed requests waste.",
6021
+ children: renderEfficiencyBody()
6022
+ }
6023
+ ),
6024
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_Grid2.default, { container: true, spacing: 2, alignItems: "flex-start", children: [
6025
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Grid2.default, { item: true, xs: 12, md: 5, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
6026
+ SectionCard,
6027
+ {
6028
+ title: "Practice score",
6029
+ subtitle: "Out of 100",
6030
+ description: "Five everyday habits in one number \u2014 higher means cheaper, safer usage.",
6031
+ 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: [
6032
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_Typography14.default, { sx: { fontSize: 30, fontWeight: 700, fontVariantNumeric: "tabular-nums" }, children: [
6033
+ score.total,
6034
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Typography14.default, { component: "span", color: "text.secondary", sx: { fontSize: 16 }, children: "/100" })
6035
+ ] }),
6036
+ score.checks.map((c) => /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_Box16.default, { children: [
6037
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_Box16.default, { sx: { display: "flex", justifyContent: "space-between", gap: 1, mb: 0.5 }, children: [
6038
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Typography14.default, { variant: "body2", sx: { fontWeight: 600 }, children: c.label }),
6039
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_Typography14.default, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" }, children: [
6040
+ c.points,
6041
+ "/",
6042
+ c.maxPoints
6043
+ ] })
6044
+ ] }),
6045
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
6046
+ Meter,
6047
+ {
6048
+ value: c.maxPoints > 0 ? c.points / c.maxPoints * 100 : 0,
6049
+ ariaLabel: `${c.label}: ${c.points} of ${c.maxPoints}`
6050
+ }
6051
+ ),
6052
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Typography14.default, { variant: "caption", color: "text.secondary", children: c.detail })
6053
+ ] }, c.id))
6054
+ ] })
6055
+ }
6056
+ ) }),
6057
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Grid2.default, { item: true, xs: 12, md: 7, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
6058
+ SectionCard,
6059
+ {
6060
+ title: "Cache & errors",
6061
+ subtitle: "From spend logs (estimate)",
6062
+ description: "Repeated prompts served from cache save money; failures still bill.",
6063
+ children: renderSpendLogsBody()
6064
+ }
6065
+ ) })
6066
+ ] }),
6067
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
6068
+ SectionCard,
6069
+ {
6070
+ title: "Model opportunities",
6071
+ description: "Where a cheaper model or a reliability fix would save the most.",
6072
+ flush: true,
6073
+ 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: [
5900
6074
  /* @__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" })
6075
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { children: "Model" }),
6076
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { align: "right", children: "Spend" }),
6077
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { align: "right", children: "Requests" }),
6078
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { align: "right", children: "Failure" }),
6079
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { align: "right", children: "Avg / req" }),
6080
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { children: "Signal" })
5907
6081
  ] }) }),
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
- );
6082
+ /* @__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: [
6083
+ /* @__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 }) }),
6084
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { align: "right", children: fmtUsd(o.spend) }),
6085
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { align: "right", children: fmtInt(o.requests) }),
6086
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { align: "right", children: fmtPct1(o.failureRate) }),
6087
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableCell6.default, { align: "right", children: fmtUsd(o.avgCostPerRequest) }),
6088
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_TableCell6.default, { children: [
6089
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(StatusPill, { label: FLAG_LABEL[o.flag], tone: FLAG_TONE[o.flag], title: o.hint }),
6090
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_Typography14.default, { variant: "caption", color: "text.secondary", sx: { display: "block", mt: 0.25 }, children: o.hint })
6091
+ ] })
6092
+ ] }, o.model)) })
6093
+ ] }) })
6094
+ }
6095
+ ),
6096
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
6097
+ SectionCard,
6098
+ {
6099
+ title: "Quality scores",
6100
+ description: "Independent answer-quality scores from Langfuse, when connected.",
6101
+ children: renderQualityBody()
6102
+ }
6103
+ )
6104
+ ] });
5929
6105
  };
5930
6106
  }
5931
6107
  });
5932
6108
 
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
- }
5951
- });
5952
-
5953
6109
  // src/feedback.ts
5954
6110
  function toastFor(kind, errorMsg) {
5955
6111
  switch (kind) {
@@ -6058,18 +6214,18 @@ function initDateRange() {
6058
6214
  else start.setDate(start.getDate() - 7);
6059
6215
  return { start, end };
6060
6216
  }
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;
6217
+ 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
6218
  var init_LiteLLMPage = __esm({
6063
6219
  "src/components/LiteLLMPage.tsx"() {
6064
6220
  "use strict";
6065
6221
  import_react13 = require("react");
6066
6222
  import_Box17 = __toESM(require("@mui/material/Box"));
6067
6223
  import_Alert6 = __toESM(require("@mui/material/Alert"));
6068
- import_Button7 = __toESM(require("@mui/material/Button"));
6224
+ import_Button8 = __toESM(require("@mui/material/Button"));
6069
6225
  import_Typography15 = __toESM(require("@mui/material/Typography"));
6070
6226
  import_Tabs2 = __toESM(require("@mui/material/Tabs"));
6071
6227
  import_Tab2 = __toESM(require("@mui/material/Tab"));
6072
- import_Grid2 = __toESM(require("@mui/material/Grid"));
6228
+ import_Grid3 = __toESM(require("@mui/material/Grid"));
6073
6229
  import_Accordion2 = __toESM(require("@mui/material/Accordion"));
6074
6230
  import_AccordionSummary2 = __toESM(require("@mui/material/AccordionSummary"));
6075
6231
  import_AccordionDetails2 = __toESM(require("@mui/material/AccordionDetails"));
@@ -6087,7 +6243,7 @@ var init_LiteLLMPage = __esm({
6087
6243
  init_LiteLLMBudgetWidget();
6088
6244
  init_TeamUsage();
6089
6245
  init_ModelsTable();
6090
- init_AuditLog();
6246
+ init_InsightsTab();
6091
6247
  init_api();
6092
6248
  init_dates();
6093
6249
  import_backstage_plugin_litellm_common5 = require("@acarmisc/backstage-plugin-litellm-common");
@@ -6096,7 +6252,7 @@ var init_LiteLLMPage = __esm({
6096
6252
  init_unprovisionedCopy();
6097
6253
  import_jsx_runtime18 = require("react/jsx-runtime");
6098
6254
  PERIOD_LS_KEY2 = "litellm_usage_period";
6099
- PAGE_TABS = ["overview", "keys", "teams", "models", "audit"];
6255
+ PAGE_TABS = ["overview", "keys", "teams", "models", "insights"];
6100
6256
  isPageTab = (v) => !!v && PAGE_TABS.includes(v);
6101
6257
  LiteLLMPage = () => {
6102
6258
  const api = (0, import_core_plugin_api5.useApi)(liteLlmApiRef);
@@ -6437,7 +6593,7 @@ var init_LiteLLMPage = __esm({
6437
6593
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_Tab2.default, { label: "Keys", value: "keys" }),
6438
6594
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_Tab2.default, { label: "Teams", value: "teams" }),
6439
6595
  /* @__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" })
6596
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_Tab2.default, { label: "Insights", value: "insights" })
6441
6597
  ]
6442
6598
  }
6443
6599
  );
@@ -6491,7 +6647,7 @@ var init_LiteLLMPage = __esm({
6491
6647
  }
6492
6648
  ),
6493
6649
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
6494
- import_Button7.default,
6650
+ import_Button8.default,
6495
6651
  {
6496
6652
  variant: "contained",
6497
6653
  color: "primary",
@@ -6503,8 +6659,8 @@ var init_LiteLLMPage = __esm({
6503
6659
  }
6504
6660
  ) });
6505
6661
  }
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)(
6662
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_Grid3.default, { container: true, spacing: 2, alignItems: "flex-start", children: [
6663
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_Grid3.default, { item: true, xs: 12, lg: 8, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
6508
6664
  UsageStats,
6509
6665
  {
6510
6666
  usage: usage ?? null,
@@ -6518,7 +6674,7 @@ var init_LiteLLMPage = __esm({
6518
6674
  userInfo
6519
6675
  }
6520
6676
  ) }),
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 ?? [] }) })
6677
+ /* @__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
6678
  ] });
6523
6679
  })(),
6524
6680
  activeTab === "keys" && (() => {
@@ -6526,7 +6682,7 @@ var init_LiteLLMPage = __esm({
6526
6682
  return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_Alert6.default, { severity: "error", children: [
6527
6683
  "Failed to load keys: ",
6528
6684
  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" }) })
6685
+ /* @__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
6686
  ] });
6531
6687
  }
6532
6688
  return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
@@ -6550,7 +6706,7 @@ var init_LiteLLMPage = __esm({
6550
6706
  return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_Alert6.default, { severity: "error", children: [
6551
6707
  "Failed to load teams: ",
6552
6708
  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" }) })
6709
+ /* @__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
6710
  ] });
6555
6711
  }
6556
6712
  const teamsById = /* @__PURE__ */ new Map();
@@ -6587,7 +6743,7 @@ var init_LiteLLMPage = __esm({
6587
6743
  return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_Alert6.default, { severity: "error", children: [
6588
6744
  "Failed to load models: ",
6589
6745
  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" }) })
6746
+ /* @__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
6747
  ] });
6592
6748
  }
6593
6749
  return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
@@ -6599,7 +6755,17 @@ var init_LiteLLMPage = __esm({
6599
6755
  }
6600
6756
  );
6601
6757
  })(),
6602
- activeTab === "audit" && userInfo.can_view_audit && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(AuditLog, { api })
6758
+ activeTab === "insights" && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
6759
+ InsightsTab,
6760
+ {
6761
+ api,
6762
+ usage: usage ?? null,
6763
+ models: allModels ?? [],
6764
+ keys: keys ?? [],
6765
+ dateRange,
6766
+ loading: usageLoading
6767
+ }
6768
+ )
6603
6769
  ] }),
6604
6770
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
6605
6771
  KeyFormDialog,
@@ -6773,14 +6939,14 @@ var ConnectPage_exports = {};
6773
6939
  __export(ConnectPage_exports, {
6774
6940
  ConnectPage: () => ConnectPage
6775
6941
  });
6776
- 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;
6942
+ 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;
6777
6943
  var init_ConnectPage = __esm({
6778
6944
  "src/components/ConnectPage.tsx"() {
6779
6945
  "use strict";
6780
6946
  import_react14 = require("react");
6781
6947
  import_Alert7 = __toESM(require("@mui/material/Alert"));
6782
6948
  import_Box18 = __toESM(require("@mui/material/Box"));
6783
- import_Button8 = __toESM(require("@mui/material/Button"));
6949
+ import_Button9 = __toESM(require("@mui/material/Button"));
6784
6950
  import_Checkbox3 = __toESM(require("@mui/material/Checkbox"));
6785
6951
  import_FormControlLabel3 = __toESM(require("@mui/material/FormControlLabel"));
6786
6952
  import_FormGroup = __toESM(require("@mui/material/FormGroup"));
@@ -6851,7 +7017,7 @@ var init_ConnectPage = __esm({
6851
7017
  )) }),
6852
7018
  error && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_Alert7.default, { severity: "error", sx: { mb: 2 }, children: error }),
6853
7019
  /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
6854
- import_Button8.default,
7020
+ import_Button9.default,
6855
7021
  {
6856
7022
  variant: "contained",
6857
7023
  onClick: connect,
@@ -6891,7 +7057,7 @@ __export(index_exports, {
6891
7057
  module.exports = __toCommonJS(index_exports);
6892
7058
 
6893
7059
  // src/plugin.tsx
6894
- var import_icons_material10 = require("@mui/icons-material");
7060
+ var import_icons_material9 = require("@mui/icons-material");
6895
7061
  var import_frontend_plugin_api4 = require("@backstage/frontend-plugin-api");
6896
7062
  init_api();
6897
7063
  init_routes();
@@ -6907,7 +7073,7 @@ var liteLlmPage = import_frontend_plugin_api4.PageBlueprint.make({
6907
7073
  params: {
6908
7074
  path: "/litellm",
6909
7075
  title: "LiteLLM",
6910
- icon: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_icons_material10.TrendingUp, {}),
7076
+ icon: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_icons_material9.TrendingUp, {}),
6911
7077
  routeRef: rootRouteRef,
6912
7078
  loader: async () => {
6913
7079
  const { LiteLLMPage: LiteLLMPage2 } = await Promise.resolve().then(() => (init_LiteLLMPage(), LiteLLMPage_exports));
@@ -6947,7 +7113,7 @@ var import_Box19 = __toESM(require("@mui/material/Box"));
6947
7113
  var import_Typography17 = __toESM(require("@mui/material/Typography"));
6948
7114
  var import_FormControl2 = __toESM(require("@mui/material/FormControl"));
6949
7115
  var import_Select2 = __toESM(require("@mui/material/Select"));
6950
- var import_MenuItem6 = __toESM(require("@mui/material/MenuItem"));
7116
+ var import_MenuItem5 = __toESM(require("@mui/material/MenuItem"));
6951
7117
  var import_Skeleton7 = __toESM(require("@mui/material/Skeleton"));
6952
7118
  var import_recharts3 = require("recharts");
6953
7119
  var import_core_plugin_api7 = require("@backstage/core-plugin-api");
@@ -7084,9 +7250,9 @@ var LiteLLMHomeWidget = ({
7084
7250
  displayEmpty: true,
7085
7251
  inputProps: { "aria-label": "Usage period" },
7086
7252
  children: [
7087
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_MenuItem6.default, { value: "today", children: "Today" }),
7088
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_MenuItem6.default, { value: "7d", children: "7d" }),
7089
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_MenuItem6.default, { value: "30d", children: "30d" })
7253
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_MenuItem5.default, { value: "today", children: "Today" }),
7254
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_MenuItem5.default, { value: "7d", children: "7d" }),
7255
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_MenuItem5.default, { value: "30d", children: "30d" })
7090
7256
  ]
7091
7257
  }
7092
7258
  ) })
@@ -7157,13 +7323,13 @@ var import_react16 = require("react");
7157
7323
  var import_frontend_plugin_api6 = require("@backstage/frontend-plugin-api");
7158
7324
  var import_Paper11 = __toESM(require("@mui/material/Paper"));
7159
7325
  var import_Box20 = __toESM(require("@mui/material/Box"));
7160
- var import_Button9 = __toESM(require("@mui/material/Button"));
7326
+ var import_Button10 = __toESM(require("@mui/material/Button"));
7161
7327
  var import_Typography18 = __toESM(require("@mui/material/Typography"));
7162
7328
  var import_Skeleton8 = __toESM(require("@mui/material/Skeleton"));
7163
- var import_Collapse4 = __toESM(require("@mui/material/Collapse"));
7164
- var import_styles13 = require("@mui/material/styles");
7329
+ var import_Collapse3 = __toESM(require("@mui/material/Collapse"));
7330
+ var import_styles12 = require("@mui/material/styles");
7165
7331
  var import_useMediaQuery = __toESM(require("@mui/material/useMediaQuery"));
7166
- var import_icons_material11 = require("@mui/icons-material");
7332
+ var import_icons_material10 = require("@mui/icons-material");
7167
7333
  var import_recharts4 = require("recharts");
7168
7334
  var import_core_plugin_api8 = require("@backstage/core-plugin-api");
7169
7335
  var import_core_components6 = require("@backstage/core-components");
@@ -7574,7 +7740,7 @@ var LiteLLMBudgetGauges = ({
7574
7740
  keys: propKeys
7575
7741
  }) => {
7576
7742
  const api = (0, import_core_plugin_api8.useApi)(liteLlmApiRef);
7577
- const theme = (0, import_styles13.useTheme)();
7743
+ const theme = (0, import_styles12.useTheme)();
7578
7744
  const isSmallScreen = (0, import_useMediaQuery.default)(theme.breakpoints.down("sm"));
7579
7745
  const gaugeSize = isSmallScreen ? Math.min(48, size) : size;
7580
7746
  const expandedRegionId = (0, import_react16.useId)();
@@ -7675,10 +7841,10 @@ var LiteLLMBudgetGauges = ({
7675
7841
  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);
7676
7842
  case "module":
7677
7843
  if (!moduleHref) return null;
7678
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_Button9.default, { size: "small", variant: "text", component: import_core_components6.Link, to: moduleHref, children: label }, key);
7844
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_Button10.default, { size: "small", variant: "text", component: import_core_components6.Link, to: moduleHref, children: label }, key);
7679
7845
  case "all-limits":
7680
7846
  return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
7681
- import_Button9.default,
7847
+ import_Button10.default,
7682
7848
  {
7683
7849
  size: "small",
7684
7850
  variant: "text",
@@ -7686,7 +7852,7 @@ var LiteLLMBudgetGauges = ({
7686
7852
  "aria-expanded": isExpanded,
7687
7853
  "aria-controls": expandedRegionId,
7688
7854
  endIcon: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
7689
- import_icons_material11.ExpandMore,
7855
+ import_icons_material10.ExpandMore,
7690
7856
  {
7691
7857
  sx: (muiTheme) => ({
7692
7858
  transform: isExpanded ? "rotate(180deg)" : "none",
@@ -7781,7 +7947,7 @@ var LiteLLMBudgetGauges = ({
7781
7947
  maxBudget: user?.max_budget ?? void 0
7782
7948
  }
7783
7949
  ) }),
7784
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_Collapse4.default, { in: isExpanded, unmountOnExit: true, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
7950
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_Collapse3.default, { in: isExpanded, unmountOnExit: true, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
7785
7951
  import_Box20.default,
7786
7952
  {
7787
7953
  id: expandedRegionId,