@acarmisc/backstage-plugin-litellm 0.38.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.esm.js CHANGED
@@ -306,6 +306,14 @@ var init_api = __esm({
306
306
  if (params.pageSize !== void 0) strParams.page_size = String(params.pageSize);
307
307
  return this.get("/insights/spend-logs", strParams);
308
308
  }
309
+ async getQuality(params) {
310
+ const strParams = {
311
+ start_date: params.startDate,
312
+ end_date: params.endDate
313
+ };
314
+ if (params.userId) strParams.userId = params.userId;
315
+ return this.get("/insights/quality", strParams);
316
+ }
309
317
  async getConfig() {
310
318
  return this.get("/config");
311
319
  }
@@ -704,7 +712,7 @@ var init_surfaces = __esm({
704
712
  "src/components/ui/surfaces.tsx"() {
705
713
  "use strict";
706
714
  init_tokens();
707
- SectionCard = ({ title, subtitle, actions, flush = false, children }) => /* @__PURE__ */ jsxs3(Paper2, { sx: { borderRadius: 2, overflow: "hidden" }, children: [
715
+ SectionCard = ({ title, subtitle, description, actions, flush = false, children }) => /* @__PURE__ */ jsxs3(Paper2, { sx: { borderRadius: 2, overflow: "hidden" }, children: [
708
716
  (title || actions) && /* @__PURE__ */ jsxs3(
709
717
  Box3,
710
718
  {
@@ -726,6 +734,7 @@ var init_surfaces = __esm({
726
734
  ]
727
735
  }
728
736
  ),
737
+ description && /* @__PURE__ */ jsx3(Typography2, { variant: "body2", color: "text.secondary", sx: { px: 2.5, pb: 1.5 }, children: description }),
729
738
  /* @__PURE__ */ jsx3(Box3, { sx: flush ? void 0 : { px: 2.5, pb: 2.5, pt: title ? 0 : 2.5 }, children })
730
739
  ] });
731
740
  ChartCard = ({ title, meta, height = 240, children }) => (
@@ -5682,7 +5691,7 @@ var init_insights = __esm({
5682
5691
  "use strict";
5683
5692
  costPer1k = (model, spend, tokens, models) => {
5684
5693
  const info = models?.find((m) => m.model_name === model);
5685
- const listed = info?.input_cost_per_token != null && info?.output_cost_per_token != null ? (info.input_cost_per_token + info.output_cost_per_token) / 2 * 1e3 : null;
5694
+ 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;
5686
5695
  if (listed !== null) return listed;
5687
5696
  return tokens > 0 ? spend / tokens * 1e3 : null;
5688
5697
  };
@@ -5733,11 +5742,17 @@ var init_insights = __esm({
5733
5742
  };
5734
5743
  };
5735
5744
  scorePractices = (summary, keyStats) => {
5736
- const waste = summary.retryWasteShare <= 0.01 ? 30 : summary.retryWasteShare <= 0.05 ? 20 : summary.retryWasteShare <= 0.1 ? 10 : 0;
5737
- const premium = summary.premiumShare <= 0.2 ? 25 : summary.premiumShare <= 0.5 ? 15 : summary.premiumShare <= 0.8 ? 5 : 0;
5738
- const conc = summary.topModelConcentration <= 0.5 ? 20 : summary.topModelConcentration <= 0.8 ? 10 : 0;
5745
+ const band = (v, limits) => {
5746
+ for (const [max, pts] of limits) {
5747
+ if (v <= max) return pts;
5748
+ }
5749
+ return 0;
5750
+ };
5751
+ const waste = band(summary.retryWasteShare, [[0.01, 30], [0.05, 20], [0.1, 10]]);
5752
+ const premium = band(summary.premiumShare, [[0.2, 25], [0.5, 15], [0.8, 5]]);
5753
+ const conc = band(summary.topModelConcentration, [[0.5, 20], [0.8, 10]]);
5739
5754
  const bad = keyStats.totalKeys > 0 ? (keyStats.idleKeys + keyStats.uncappedKeys) / keyStats.totalKeys : 0;
5740
- const hygiene = keyStats.totalKeys === 0 ? 25 : bad === 0 ? 25 : bad <= 0.2 ? 15 : bad <= 0.5 ? 5 : 0;
5755
+ const hygiene = keyStats.totalKeys === 0 ? 25 : band(bad, [[0, 25], [0.2, 15], [0.5, 5]]);
5741
5756
  const checks = [
5742
5757
  { id: "retry-waste", label: "Low retry waste", points: waste, maxPoints: 30, detail: `${(summary.retryWasteShare * 100).toFixed(1)}% of requests failed` },
5743
5758
  { id: "premium-mix", label: "Balanced model mix", points: premium, maxPoints: 25, detail: `${(summary.premiumShare * 100).toFixed(1)}% of spend on premium models` },
@@ -5864,6 +5879,67 @@ var init_InsightsTab = __esm({
5864
5879
  pageSize: 1e3
5865
5880
  });
5866
5881
  }, [api, dateRange]);
5882
+ const {
5883
+ value: quality,
5884
+ loading: qualityLoading,
5885
+ error: qualityError,
5886
+ retry: retryQuality
5887
+ } = useAsyncRetry(async () => {
5888
+ if (typeof api.getQuality !== "function") return { configured: false };
5889
+ return api.getQuality({
5890
+ startDate: toLocalDay(dateRange.start),
5891
+ endDate: toLocalDay(dateRange.end)
5892
+ });
5893
+ }, [api, dateRange]);
5894
+ const renderQualityBody = () => {
5895
+ if (qualityLoading) return /* @__PURE__ */ jsx17(Skeleton6, { variant: "rounded", height: 120 });
5896
+ if (qualityError) {
5897
+ return /* @__PURE__ */ jsxs16(Alert5, { severity: "error", children: [
5898
+ "Failed to load quality scores: ",
5899
+ qualityError.message || "Unknown error",
5900
+ /* @__PURE__ */ jsx17(Box16, { sx: { mt: 1 }, children: /* @__PURE__ */ jsx17(Button7, { size: "small", onClick: retryQuality, children: "Retry" }) })
5901
+ ] });
5902
+ }
5903
+ if (!quality || quality.configured === false) {
5904
+ return /* @__PURE__ */ jsx17(
5905
+ EmptyState,
5906
+ {
5907
+ message: "Quality scores \u2014 connect Langfuse",
5908
+ hint: "LLM-as-judge evaluation is not configured for this deployment."
5909
+ }
5910
+ );
5911
+ }
5912
+ if (quality.byModel.length === 0 && quality.scores.length === 0) {
5913
+ return /* @__PURE__ */ jsx17(EmptyState, { message: "No quality data in this period" });
5914
+ }
5915
+ return /* @__PURE__ */ jsxs16(Box16, { sx: { display: "flex", flexDirection: "column", gap: 2 }, children: [
5916
+ quality.byModel.length > 0 && /* @__PURE__ */ jsx17(Box16, { sx: { display: "flex", flexDirection: "column", gap: 1.5 }, children: quality.byModel.map((m) => /* @__PURE__ */ jsx17(Box16, { children: /* @__PURE__ */ jsxs16(Box16, { sx: { display: "flex", justifyContent: "space-between", gap: 1, mb: 0.5 }, children: [
5917
+ /* @__PURE__ */ jsx17(Typography14, { variant: "body2", sx: { fontWeight: 600 }, children: m.model }),
5918
+ /* @__PURE__ */ jsxs16(Typography14, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" }, children: [
5919
+ fmtUsd(m.cost),
5920
+ " \xB7 ",
5921
+ fmtInt(m.tokens),
5922
+ " tokens"
5923
+ ] })
5924
+ ] }) }, m.model)) }),
5925
+ quality.scores.length > 0 && /* @__PURE__ */ jsx17(Box16, { sx: { display: "flex", flexDirection: "column", gap: 1.5 }, children: quality.scores.map((s) => /* @__PURE__ */ jsxs16(Box16, { children: [
5926
+ /* @__PURE__ */ jsxs16(Box16, { sx: { display: "flex", justifyContent: "space-between", gap: 1, mb: 0.5 }, children: [
5927
+ /* @__PURE__ */ jsx17(Typography14, { variant: "body2", sx: { fontWeight: 600 }, children: s.name }),
5928
+ /* @__PURE__ */ jsxs16(Typography14, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" }, children: [
5929
+ s.avg.toFixed(2),
5930
+ s.count > 0 ? ` \xB7 n=${fmtInt(s.count)}` : ""
5931
+ ] })
5932
+ ] }),
5933
+ /* @__PURE__ */ jsx17(
5934
+ Meter,
5935
+ {
5936
+ value: s.avg <= 1 ? s.avg * 100 : s.avg,
5937
+ ariaLabel: `${s.name}: average ${s.avg.toFixed(2)}`
5938
+ }
5939
+ )
5940
+ ] }, s.name)) })
5941
+ ] });
5942
+ };
5867
5943
  const logStats = useMemo8(() => {
5868
5944
  const rows = spendLogs?.data ?? [];
5869
5945
  const hits = rows.filter((r) => r.cacheHit);
@@ -5944,64 +6020,90 @@ var init_InsightsTab = __esm({
5944
6020
  {
5945
6021
  title: "Efficiency",
5946
6022
  subtitle: `${dateRange.start.toLocaleDateString()} \u2013 ${dateRange.end.toLocaleDateString()}`,
6023
+ description: "What each token and request costs, and how much failed requests waste.",
5947
6024
  children: renderEfficiencyBody()
5948
6025
  }
5949
6026
  ),
5950
6027
  /* @__PURE__ */ jsxs16(Grid2, { container: true, spacing: 2, alignItems: "flex-start", children: [
5951
- /* @__PURE__ */ jsx17(Grid2, { item: true, xs: 12, md: 5, children: /* @__PURE__ */ jsx17(SectionCard, { title: "Practice score", subtitle: "Out of 100", children: loading ? /* @__PURE__ */ jsx17(Skeleton6, { variant: "rounded", height: 160 }) : /* @__PURE__ */ jsxs16(Box16, { sx: { display: "flex", flexDirection: "column", gap: 1.5 }, children: [
5952
- /* @__PURE__ */ jsxs16(Typography14, { sx: { fontSize: 30, fontWeight: 700, fontVariantNumeric: "tabular-nums" }, children: [
5953
- score.total,
5954
- /* @__PURE__ */ jsx17(Typography14, { component: "span", color: "text.secondary", sx: { fontSize: 16 }, children: "/100" })
5955
- ] }),
5956
- score.checks.map((c) => /* @__PURE__ */ jsxs16(Box16, { children: [
5957
- /* @__PURE__ */ jsxs16(Box16, { sx: { display: "flex", justifyContent: "space-between", gap: 1, mb: 0.5 }, children: [
5958
- /* @__PURE__ */ jsx17(Typography14, { variant: "body2", sx: { fontWeight: 600 }, children: c.label }),
5959
- /* @__PURE__ */ jsxs16(Typography14, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" }, children: [
5960
- c.points,
5961
- "/",
5962
- c.maxPoints
5963
- ] })
5964
- ] }),
5965
- /* @__PURE__ */ jsx17(
5966
- Meter,
5967
- {
5968
- value: c.maxPoints > 0 ? c.points / c.maxPoints * 100 : 0,
5969
- ariaLabel: `${c.label}: ${c.points} of ${c.maxPoints}`
5970
- }
5971
- ),
5972
- /* @__PURE__ */ jsx17(Typography14, { variant: "caption", color: "text.secondary", children: c.detail })
5973
- ] }, c.id))
5974
- ] }) }) }),
5975
- /* @__PURE__ */ jsx17(Grid2, { item: true, xs: 12, md: 7, children: /* @__PURE__ */ jsx17(SectionCard, { title: "Cache & errors", subtitle: "From spend logs (estimate)", children: renderSpendLogsBody() }) })
6028
+ /* @__PURE__ */ jsx17(Grid2, { item: true, xs: 12, md: 5, children: /* @__PURE__ */ jsx17(
6029
+ SectionCard,
6030
+ {
6031
+ title: "Practice score",
6032
+ subtitle: "Out of 100",
6033
+ description: "Five everyday habits in one number \u2014 higher means cheaper, safer usage.",
6034
+ children: loading ? /* @__PURE__ */ jsx17(Skeleton6, { variant: "rounded", height: 160 }) : /* @__PURE__ */ jsxs16(Box16, { sx: { display: "flex", flexDirection: "column", gap: 1.5 }, children: [
6035
+ /* @__PURE__ */ jsxs16(Typography14, { sx: { fontSize: 30, fontWeight: 700, fontVariantNumeric: "tabular-nums" }, children: [
6036
+ score.total,
6037
+ /* @__PURE__ */ jsx17(Typography14, { component: "span", color: "text.secondary", sx: { fontSize: 16 }, children: "/100" })
6038
+ ] }),
6039
+ score.checks.map((c) => /* @__PURE__ */ jsxs16(Box16, { children: [
6040
+ /* @__PURE__ */ jsxs16(Box16, { sx: { display: "flex", justifyContent: "space-between", gap: 1, mb: 0.5 }, children: [
6041
+ /* @__PURE__ */ jsx17(Typography14, { variant: "body2", sx: { fontWeight: 600 }, children: c.label }),
6042
+ /* @__PURE__ */ jsxs16(Typography14, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" }, children: [
6043
+ c.points,
6044
+ "/",
6045
+ c.maxPoints
6046
+ ] })
6047
+ ] }),
6048
+ /* @__PURE__ */ jsx17(
6049
+ Meter,
6050
+ {
6051
+ value: c.maxPoints > 0 ? c.points / c.maxPoints * 100 : 0,
6052
+ ariaLabel: `${c.label}: ${c.points} of ${c.maxPoints}`
6053
+ }
6054
+ ),
6055
+ /* @__PURE__ */ jsx17(Typography14, { variant: "caption", color: "text.secondary", children: c.detail })
6056
+ ] }, c.id))
6057
+ ] })
6058
+ }
6059
+ ) }),
6060
+ /* @__PURE__ */ jsx17(Grid2, { item: true, xs: 12, md: 7, children: /* @__PURE__ */ jsx17(
6061
+ SectionCard,
6062
+ {
6063
+ title: "Cache & errors",
6064
+ subtitle: "From spend logs (estimate)",
6065
+ description: "Repeated prompts served from cache save money; failures still bill.",
6066
+ children: renderSpendLogsBody()
6067
+ }
6068
+ ) })
5976
6069
  ] }),
5977
- /* @__PURE__ */ jsx17(SectionCard, { title: "Model opportunities", flush: true, children: loading ? /* @__PURE__ */ jsx17(Box16, { sx: { p: 2.5 }, children: /* @__PURE__ */ jsx17(Skeleton6, { variant: "rounded", height: 120 }) }) : /* @__PURE__ */ jsx17(TableContainer6, { children: /* @__PURE__ */ jsxs16(Table6, { size: "small", sx: dataTableSx, children: [
5978
- /* @__PURE__ */ jsx17(TableHead6, { children: /* @__PURE__ */ jsxs16(TableRow6, { children: [
5979
- /* @__PURE__ */ jsx17(TableCell6, { children: "Model" }),
5980
- /* @__PURE__ */ jsx17(TableCell6, { align: "right", children: "Spend" }),
5981
- /* @__PURE__ */ jsx17(TableCell6, { align: "right", children: "Requests" }),
5982
- /* @__PURE__ */ jsx17(TableCell6, { align: "right", children: "Failure" }),
5983
- /* @__PURE__ */ jsx17(TableCell6, { align: "right", children: "Avg / req" }),
5984
- /* @__PURE__ */ jsx17(TableCell6, { children: "Signal" })
5985
- ] }) }),
5986
- /* @__PURE__ */ jsx17(TableBody6, { children: opportunities.length === 0 ? /* @__PURE__ */ jsx17(TableRow6, { children: /* @__PURE__ */ jsx17(TableCell6, { colSpan: 6, children: /* @__PURE__ */ jsx17(EmptyState, { message: "No model activity in this period" }) }) }) : opportunities.map((o) => /* @__PURE__ */ jsxs16(TableRow6, { children: [
5987
- /* @__PURE__ */ jsx17(TableCell6, { children: /* @__PURE__ */ jsx17(Typography14, { variant: "body2", sx: { fontWeight: 600 }, children: o.model }) }),
5988
- /* @__PURE__ */ jsx17(TableCell6, { align: "right", children: fmtUsd(o.spend) }),
5989
- /* @__PURE__ */ jsx17(TableCell6, { align: "right", children: fmtInt(o.requests) }),
5990
- /* @__PURE__ */ jsx17(TableCell6, { align: "right", children: fmtPct1(o.failureRate) }),
5991
- /* @__PURE__ */ jsx17(TableCell6, { align: "right", children: fmtUsd(o.avgCostPerRequest) }),
5992
- /* @__PURE__ */ jsxs16(TableCell6, { children: [
5993
- /* @__PURE__ */ jsx17(StatusPill, { label: FLAG_LABEL[o.flag], tone: FLAG_TONE[o.flag], title: o.hint }),
5994
- /* @__PURE__ */ jsx17(Typography14, { variant: "caption", color: "text.secondary", sx: { display: "block", mt: 0.25 }, children: o.hint })
5995
- ] })
5996
- ] }, o.model)) })
5997
- ] }) }) }),
5998
- /* @__PURE__ */ jsx17(SectionCard, { title: "Quality scores", children: /* @__PURE__ */ jsx17(
5999
- EmptyState,
6070
+ /* @__PURE__ */ jsx17(
6071
+ SectionCard,
6000
6072
  {
6001
- message: "Quality scores \u2014 connect Langfuse",
6002
- hint: "LLM-as-judge evaluation is not configured for this deployment."
6073
+ title: "Model opportunities",
6074
+ description: "Where a cheaper model or a reliability fix would save the most.",
6075
+ flush: true,
6076
+ children: loading ? /* @__PURE__ */ jsx17(Box16, { sx: { p: 2.5 }, children: /* @__PURE__ */ jsx17(Skeleton6, { variant: "rounded", height: 120 }) }) : /* @__PURE__ */ jsx17(TableContainer6, { children: /* @__PURE__ */ jsxs16(Table6, { size: "small", sx: dataTableSx, children: [
6077
+ /* @__PURE__ */ jsx17(TableHead6, { children: /* @__PURE__ */ jsxs16(TableRow6, { children: [
6078
+ /* @__PURE__ */ jsx17(TableCell6, { children: "Model" }),
6079
+ /* @__PURE__ */ jsx17(TableCell6, { align: "right", children: "Spend" }),
6080
+ /* @__PURE__ */ jsx17(TableCell6, { align: "right", children: "Requests" }),
6081
+ /* @__PURE__ */ jsx17(TableCell6, { align: "right", children: "Failure" }),
6082
+ /* @__PURE__ */ jsx17(TableCell6, { align: "right", children: "Avg / req" }),
6083
+ /* @__PURE__ */ jsx17(TableCell6, { children: "Signal" })
6084
+ ] }) }),
6085
+ /* @__PURE__ */ jsx17(TableBody6, { children: opportunities.length === 0 ? /* @__PURE__ */ jsx17(TableRow6, { children: /* @__PURE__ */ jsx17(TableCell6, { colSpan: 6, children: /* @__PURE__ */ jsx17(EmptyState, { message: "No model activity in this period" }) }) }) : opportunities.map((o) => /* @__PURE__ */ jsxs16(TableRow6, { children: [
6086
+ /* @__PURE__ */ jsx17(TableCell6, { children: /* @__PURE__ */ jsx17(Typography14, { variant: "body2", sx: { fontWeight: 600 }, children: o.model }) }),
6087
+ /* @__PURE__ */ jsx17(TableCell6, { align: "right", children: fmtUsd(o.spend) }),
6088
+ /* @__PURE__ */ jsx17(TableCell6, { align: "right", children: fmtInt(o.requests) }),
6089
+ /* @__PURE__ */ jsx17(TableCell6, { align: "right", children: fmtPct1(o.failureRate) }),
6090
+ /* @__PURE__ */ jsx17(TableCell6, { align: "right", children: fmtUsd(o.avgCostPerRequest) }),
6091
+ /* @__PURE__ */ jsxs16(TableCell6, { children: [
6092
+ /* @__PURE__ */ jsx17(StatusPill, { label: FLAG_LABEL[o.flag], tone: FLAG_TONE[o.flag], title: o.hint }),
6093
+ /* @__PURE__ */ jsx17(Typography14, { variant: "caption", color: "text.secondary", sx: { display: "block", mt: 0.25 }, children: o.hint })
6094
+ ] })
6095
+ ] }, o.model)) })
6096
+ ] }) })
6003
6097
  }
6004
- ) })
6098
+ ),
6099
+ /* @__PURE__ */ jsx17(
6100
+ SectionCard,
6101
+ {
6102
+ title: "Quality scores",
6103
+ description: "Independent answer-quality scores from Langfuse, when connected.",
6104
+ children: renderQualityBody()
6105
+ }
6106
+ )
6005
6107
  ] });
6006
6108
  };
6007
6109
  }