@acarmisc/backstage-plugin-litellm 0.38.0 → 0.40.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/api.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type FetchApi, type DiscoveryApi } from '@backstage/frontend-plugin-api';
2
- import { UserInfo, VirtualKey, ModelInfo, UsageMetrics, TeamMemberUsage, TeamInfo, GenerateKeyRequest, GenerateKeyResponse, UpdateKeyRequest, AuditLogsParams, PaginatedAuditLogs, SpendLogsParams, PaginatedSpendLogs, LiteLlmConfig, VectorStoreInfo, McpServerInfo, CreateTeamRequest, CreateTeamResponse, UpdateTeamRequest } from './types';
2
+ import { UserInfo, VirtualKey, ModelInfo, UsageMetrics, TeamMemberUsage, TeamInfo, GenerateKeyRequest, GenerateKeyResponse, UpdateKeyRequest, AuditLogsParams, PaginatedAuditLogs, SpendLogsParams, PaginatedSpendLogs, LiteLlmConfig, QualityResponse, VectorStoreInfo, McpServerInfo, CreateTeamRequest, CreateTeamResponse, UpdateTeamRequest } from './types';
3
3
  export declare class ApiError extends Error {
4
4
  body: unknown;
5
5
  status: number;
@@ -46,6 +46,8 @@ export interface LiteLlmApiInterface {
46
46
  getTeamMemberUsage(teamId: string, startDate: string, endDate: string): Promise<TeamMemberUsage>;
47
47
  getAuditLogs(params: AuditLogsParams): Promise<PaginatedAuditLogs>;
48
48
  getSpendLogs(params: SpendLogsParams): Promise<PaginatedSpendLogs>;
49
+ /** Langfuse quality summary; `{configured:false}` when Langfuse is off. */
50
+ getQuality(params: SpendLogsParams): Promise<QualityResponse>;
49
51
  getConfig(): Promise<LiteLlmConfig>;
50
52
  createTeam(request: CreateTeamRequest): Promise<CreateTeamResponse>;
51
53
  updateTeam(teamId: string, request: UpdateTeamRequest): Promise<TeamInfo>;
@@ -110,6 +112,7 @@ export declare class LiteLlmApi implements LiteLlmApiInterface {
110
112
  getTeamUsage(teamId: string, startDate: string, endDate: string): Promise<UsageMetrics>;
111
113
  getTeamMemberUsage(teamId: string, startDate: string, endDate: string): Promise<TeamMemberUsage>;
112
114
  getSpendLogs(params: SpendLogsParams): Promise<PaginatedSpendLogs>;
115
+ getQuality(params: SpendLogsParams): Promise<QualityResponse>;
113
116
  getConfig(): Promise<LiteLlmConfig>;
114
117
  createTeam(request: CreateTeamRequest): Promise<CreateTeamResponse>;
115
118
  updateTeam(teamId: string, request: UpdateTeamRequest): Promise<TeamInfo>;
@@ -5,6 +5,8 @@ import { type Tone } from './tokens';
5
5
  export declare const SectionCard: FC<{
6
6
  title?: ReactNode;
7
7
  subtitle?: ReactNode;
8
+ /** One-line explainer under the title row. */
9
+ description?: ReactNode;
8
10
  actions?: ReactNode;
9
11
  /** Set when the body renders its own edge-to-edge content (e.g. a table). */
10
12
  flush?: boolean;
package/dist/index.cjs.js CHANGED
@@ -328,6 +328,14 @@ var init_api = __esm({
328
328
  if (params.pageSize !== void 0) strParams.page_size = String(params.pageSize);
329
329
  return this.get("/insights/spend-logs", strParams);
330
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
+ }
331
339
  async getConfig() {
332
340
  return this.get("/config");
333
341
  }
@@ -726,7 +734,7 @@ var init_surfaces = __esm({
726
734
  import_styles3 = require("@mui/material/styles");
727
735
  init_tokens();
728
736
  import_jsx_runtime3 = require("react/jsx-runtime");
729
- 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: [
730
738
  (title || actions) && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
731
739
  import_Box3.default,
732
740
  {
@@ -748,6 +756,7 @@ var init_surfaces = __esm({
748
756
  ]
749
757
  }
750
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 }),
751
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 })
752
761
  ] });
753
762
  ChartCard = ({ title, meta, height = 240, children }) => (
@@ -5679,7 +5688,7 @@ var init_insights = __esm({
5679
5688
  "use strict";
5680
5689
  costPer1k = (model, spend, tokens, models) => {
5681
5690
  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;
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;
5683
5692
  if (listed !== null) return listed;
5684
5693
  return tokens > 0 ? spend / tokens * 1e3 : null;
5685
5694
  };
@@ -5730,11 +5739,17 @@ var init_insights = __esm({
5730
5739
  };
5731
5740
  };
5732
5741
  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;
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]]);
5736
5751
  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;
5752
+ const hygiene = keyStats.totalKeys === 0 ? 25 : band(bad, [[0, 25], [0.2, 15], [0.5, 5]]);
5738
5753
  const checks = [
5739
5754
  { id: "retry-waste", label: "Low retry waste", points: waste, maxPoints: 30, detail: `${(summary.retryWasteShare * 100).toFixed(1)}% of requests failed` },
5740
5755
  { id: "premium-mix", label: "Balanced model mix", points: premium, maxPoints: 25, detail: `${(summary.premiumShare * 100).toFixed(1)}% of spend on premium models` },
@@ -5861,6 +5876,67 @@ var init_InsightsTab = __esm({
5861
5876
  pageSize: 1e3
5862
5877
  });
5863
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,
5903
+ {
5904
+ message: "Quality scores \u2014 connect Langfuse",
5905
+ hint: "LLM-as-judge evaluation is not configured for this deployment."
5906
+ }
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)}` : ""
5928
+ ] })
5929
+ ] }),
5930
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
5931
+ Meter,
5932
+ {
5933
+ value: s.avg <= 1 ? s.avg * 100 : s.avg,
5934
+ ariaLabel: `${s.name}: average ${s.avg.toFixed(2)}`
5935
+ }
5936
+ )
5937
+ ] }, s.name)) })
5938
+ ] });
5939
+ };
5864
5940
  const logStats = (0, import_react12.useMemo)(() => {
5865
5941
  const rows = spendLogs?.data ?? [];
5866
5942
  const hits = rows.filter((r) => r.cacheHit);
@@ -5941,64 +6017,90 @@ var init_InsightsTab = __esm({
5941
6017
  {
5942
6018
  title: "Efficiency",
5943
6019
  subtitle: `${dateRange.start.toLocaleDateString()} \u2013 ${dateRange.end.toLocaleDateString()}`,
6020
+ description: "What each token and request costs, and how much failed requests waste.",
5944
6021
  children: renderEfficiencyBody()
5945
6022
  }
5946
6023
  ),
5947
6024
  /* @__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
- ] })
5961
- ] }),
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() }) })
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
+ ) })
5973
6066
  ] }),
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,
6067
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
6068
+ SectionCard,
5997
6069
  {
5998
- message: "Quality scores \u2014 connect Langfuse",
5999
- hint: "LLM-as-judge evaluation is not configured for this deployment."
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: [
6074
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_TableHead6.default, { children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_TableRow6.default, { children: [
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" })
6081
+ ] }) }),
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
+ ] }) })
6000
6094
  }
6001
- ) })
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
+ )
6002
6104
  ] });
6003
6105
  };
6004
6106
  }