@acarmisc/backstage-plugin-litellm 0.23.0 → 0.24.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.d.ts CHANGED
@@ -8,6 +8,8 @@ export { LiteLLMHomeWidget } from './components/LiteLLMHomeWidget';
8
8
  export type { LiteLLMHomeWidgetProps } from './components/LiteLLMHomeWidget';
9
9
  export { LiteLLMBudgetWidget } from './components/LiteLLMBudgetWidget';
10
10
  export type { LiteLLMBudgetWidgetProps } from './components/LiteLLMBudgetWidget';
11
+ export { LiteLLMBudgetGauges } from './components/LiteLLMBudgetGauges';
12
+ export type { LiteLLMBudgetGaugesProps } from './components/LiteLLMBudgetGauges';
11
13
  export { GenerateKeyDialog } from './components/GenerateKeyDialog';
12
14
  export { ManageTeamDialog } from './components/ManageTeamDialog';
13
15
  export { LiteLlmApi, liteLlmApiRef } from './api';
package/dist/index.esm.js CHANGED
@@ -319,7 +319,7 @@ function SegmentedControl({
319
319
  })
320
320
  );
321
321
  }
322
- var MONTHS, fmtDateShort, fmtCompact, fmtUsdCompact, CHART_COLORS, SERIES, OTHER_COLOR, ChartTooltip, SeriesLegend, StatusPill, TagChip, SectionCard, ChartCard, MetricStrip, Stat, Meter, dataTableSx, quietIconButtonSx, EmptyState;
322
+ var MONTHS, fmtDateShort, fmtCompact, fmtUsdCompact, CHART_COLORS, SERIES, OTHER_COLOR, ChartTooltip, SeriesLegend, StatusPill, TagChip, SectionCard, ChartCard, MetricStrip, Stat, Meter, Gauge, dataTableSx, quietIconButtonSx, EmptyState;
323
323
  var init_ui = __esm({
324
324
  "src/components/ui.tsx"() {
325
325
  "use strict";
@@ -683,6 +683,102 @@ var init_ui = __esm({
683
683
  }
684
684
  )
685
685
  );
686
+ Gauge = ({ value, tone = "accent", size = 72, thickness = 6, label, caption }) => {
687
+ const clamped = Math.max(0, Math.min(100, value ?? 0));
688
+ const r = (size - thickness) / 2;
689
+ const c = 2 * Math.PI * r;
690
+ const dash = clamped / 100 * c;
691
+ return /* @__PURE__ */ React.createElement(
692
+ Box,
693
+ {
694
+ sx: (theme) => ({
695
+ position: "relative",
696
+ width: size,
697
+ height: size,
698
+ flexShrink: 0,
699
+ color: toneColor(theme, tone)
700
+ })
701
+ },
702
+ /* @__PURE__ */ React.createElement(
703
+ Box,
704
+ {
705
+ component: "svg",
706
+ viewBox: `0 0 ${size} ${size}`,
707
+ width: size,
708
+ height: size,
709
+ sx: { display: "block", transform: "rotate(-90deg)" }
710
+ },
711
+ /* @__PURE__ */ React.createElement(
712
+ Box,
713
+ {
714
+ component: "circle",
715
+ cx: size / 2,
716
+ cy: size / 2,
717
+ r,
718
+ fill: "none",
719
+ strokeWidth: thickness,
720
+ sx: (theme) => ({
721
+ stroke: alpha(
722
+ theme.palette.text.primary,
723
+ theme.palette.mode === "dark" ? 0.14 : 0.09
724
+ )
725
+ })
726
+ }
727
+ ),
728
+ /* @__PURE__ */ React.createElement(
729
+ Box,
730
+ {
731
+ component: "circle",
732
+ cx: size / 2,
733
+ cy: size / 2,
734
+ r,
735
+ fill: "none",
736
+ strokeWidth: thickness,
737
+ strokeLinecap: "round",
738
+ strokeDasharray: `${dash} ${c - dash}`,
739
+ sx: (theme) => ({
740
+ stroke: "currentColor",
741
+ transition: theme.transitions.create("stroke-dasharray")
742
+ })
743
+ }
744
+ )
745
+ ),
746
+ /* @__PURE__ */ React.createElement(
747
+ Box,
748
+ {
749
+ sx: {
750
+ position: "absolute",
751
+ inset: 0,
752
+ display: "flex",
753
+ flexDirection: "column",
754
+ alignItems: "center",
755
+ justifyContent: "center",
756
+ lineHeight: 1
757
+ }
758
+ },
759
+ /* @__PURE__ */ React.createElement(
760
+ Typography,
761
+ {
762
+ sx: {
763
+ fontSize: size <= 64 ? 15 : 18,
764
+ fontWeight: 700,
765
+ fontVariantNumeric: "tabular-nums",
766
+ color: "text.primary"
767
+ }
768
+ },
769
+ label ?? `${Math.round(value)}%`
770
+ ),
771
+ caption && /* @__PURE__ */ React.createElement(
772
+ Typography,
773
+ {
774
+ variant: "caption",
775
+ sx: { fontSize: 9.5, color: "text.secondary", mt: 0.25 }
776
+ },
777
+ caption
778
+ )
779
+ )
780
+ );
781
+ };
686
782
  dataTableSx = (theme) => ({
687
783
  // Element selectors, not `.MuiTableCell-head`: host apps may set a MUI
688
784
  // classname prefix (Backstage ships `v5-`), which breaks global class targeting.
@@ -2828,6 +2924,22 @@ function buildBudgetSummary(user, teams, keys, maxKeys = 3) {
2828
2924
  teams: teamLimits
2829
2925
  };
2830
2926
  }
2927
+ function buildBudgetGauges(summary) {
2928
+ const byLevel = {
2929
+ key: summary.keys,
2930
+ user: summary.user ? [summary.user] : [],
2931
+ team: summary.teams
2932
+ };
2933
+ return GAUGE_KINDS.map((kind) => {
2934
+ const limits = byLevel[kind];
2935
+ const count = kind === "key" ? limits.length + summary.hiddenBudgetedKeys : limits.length;
2936
+ return {
2937
+ kind,
2938
+ count,
2939
+ limit: limits.length ? limits.reduce((a, b) => b.pct > a.pct ? b : a) : void 0
2940
+ };
2941
+ });
2942
+ }
2831
2943
  function budgetHeadline(summary) {
2832
2944
  const all = [
2833
2945
  ...summary.keys,
@@ -2854,9 +2966,11 @@ function budgetTone2(pct) {
2854
2966
  if (pct >= 80) return "warning";
2855
2967
  return "accent";
2856
2968
  }
2969
+ var GAUGE_KINDS;
2857
2970
  var init_budget = __esm({
2858
2971
  "src/budget.ts"() {
2859
2972
  "use strict";
2973
+ GAUGE_KINDS = ["key", "user", "team"];
2860
2974
  }
2861
2975
  });
2862
2976
 
@@ -4516,6 +4630,201 @@ var LiteLLMHomeWidget = ({
4516
4630
 
4517
4631
  // src/index.ts
4518
4632
  init_LiteLLMBudgetWidget();
4633
+
4634
+ // src/components/LiteLLMBudgetGauges.tsx
4635
+ init_api();
4636
+ init_format();
4637
+ init_ui();
4638
+ init_budget();
4639
+ import React14, { useEffect as useEffect5, useMemo as useMemo10, useState as useState11 } from "react";
4640
+ import Paper9 from "@mui/material/Paper";
4641
+ import Box13 from "@mui/material/Box";
4642
+ import Typography13 from "@mui/material/Typography";
4643
+ import CircularProgress8 from "@mui/material/CircularProgress";
4644
+ import Alert8 from "@mui/material/Alert";
4645
+ import { useApi as useApi5 } from "@backstage/core-plugin-api";
4646
+ import { Link as Link2 } from "@backstage/core-components";
4647
+ var DEFAULT_KEYS_HREF = "/litellm?tab=keys";
4648
+ var LEVEL = {
4649
+ key: { name: "Key", none: "No key budget" },
4650
+ user: { name: "User", none: "No personal budget" },
4651
+ team: { name: "Team", none: "No team budget" }
4652
+ };
4653
+ function fmtUsdShort(n) {
4654
+ const v = n ?? 0;
4655
+ if (v >= 100 && Number.isInteger(v)) return `$${v}`;
4656
+ return fmtUsd(v);
4657
+ }
4658
+ function spendCaption(limit) {
4659
+ if (limit.hidden) return "hidden by admin";
4660
+ return `${fmtUsdShort(limit.spend)} / ${fmtUsdShort(limit.budget)}`;
4661
+ }
4662
+ function resetLabel(limit) {
4663
+ const window2 = fmtBudgetDuration(limit.budgetDuration);
4664
+ return window2 ? `resets ${window2}` : "never resets";
4665
+ }
4666
+ var LevelGauge = ({
4667
+ gauge,
4668
+ size,
4669
+ keysHref
4670
+ }) => {
4671
+ const { name, none } = LEVEL[gauge.kind];
4672
+ const limit = gauge.limit;
4673
+ const tone = limit ? budgetTone2(limit.pct) : "neutral";
4674
+ const extra = gauge.kind === "key" ? Math.max(0, gauge.count - 1) : 0;
4675
+ return /* @__PURE__ */ React14.createElement(
4676
+ Box13,
4677
+ {
4678
+ sx: {
4679
+ display: "flex",
4680
+ flexDirection: "column",
4681
+ alignItems: "center",
4682
+ minWidth: 0,
4683
+ flex: 1
4684
+ }
4685
+ },
4686
+ /* @__PURE__ */ React14.createElement(
4687
+ Gauge,
4688
+ {
4689
+ value: limit?.pct ?? 0,
4690
+ tone,
4691
+ size,
4692
+ label: limit ? `${Math.round(limit.pct)}%` : "\u2014"
4693
+ }
4694
+ ),
4695
+ /* @__PURE__ */ React14.createElement(
4696
+ Typography13,
4697
+ {
4698
+ sx: (theme) => ({
4699
+ mt: 0.75,
4700
+ fontSize: 10.5,
4701
+ fontWeight: 700,
4702
+ letterSpacing: "0.08em",
4703
+ textTransform: "uppercase",
4704
+ color: tone === "neutral" ? theme.palette.text.secondary : void 0
4705
+ })
4706
+ },
4707
+ name
4708
+ ),
4709
+ limit ? /* @__PURE__ */ React14.createElement(React14.Fragment, null, /* @__PURE__ */ React14.createElement(
4710
+ Typography13,
4711
+ {
4712
+ variant: "caption",
4713
+ title: limit.sublabel ? `${limit.label} \xB7 ${limit.sublabel}` : limit.label,
4714
+ sx: {
4715
+ mt: 0.25,
4716
+ fontSize: 11,
4717
+ fontWeight: 600,
4718
+ maxWidth: "100%",
4719
+ overflow: "hidden",
4720
+ textOverflow: "ellipsis",
4721
+ whiteSpace: "nowrap"
4722
+ }
4723
+ },
4724
+ limit.label
4725
+ ), /* @__PURE__ */ React14.createElement(
4726
+ Typography13,
4727
+ {
4728
+ variant: "caption",
4729
+ color: "text.secondary",
4730
+ sx: {
4731
+ fontSize: 10.5,
4732
+ fontVariantNumeric: "tabular-nums",
4733
+ textAlign: "center"
4734
+ }
4735
+ },
4736
+ spendCaption(limit)
4737
+ ), extra > 0 ? /* @__PURE__ */ React14.createElement(
4738
+ Typography13,
4739
+ {
4740
+ component: Link2,
4741
+ to: keysHref,
4742
+ variant: "caption",
4743
+ sx: {
4744
+ fontSize: 10.5,
4745
+ color: "primary.main",
4746
+ textDecoration: "none",
4747
+ "&:hover": { textDecoration: "underline" }
4748
+ }
4749
+ },
4750
+ "+",
4751
+ extra,
4752
+ " more"
4753
+ ) : /* @__PURE__ */ React14.createElement(Typography13, { variant: "caption", color: "text.secondary", sx: { fontSize: 10.5 } }, resetLabel(limit))) : /* @__PURE__ */ React14.createElement(
4754
+ Typography13,
4755
+ {
4756
+ variant: "caption",
4757
+ color: "text.secondary",
4758
+ sx: { mt: 0.25, fontSize: 10.5, textAlign: "center" }
4759
+ },
4760
+ none
4761
+ )
4762
+ );
4763
+ };
4764
+ var LiteLLMBudgetGauges = ({
4765
+ title = "Budget",
4766
+ size = 72,
4767
+ keysHref = DEFAULT_KEYS_HREF,
4768
+ action
4769
+ }) => {
4770
+ const api = useApi5(liteLlmApiRef);
4771
+ const [loading, setLoading] = useState11(true);
4772
+ const [error, setError] = useState11(null);
4773
+ const [user, setUser] = useState11(null);
4774
+ const [teams, setTeams] = useState11([]);
4775
+ const [keys, setKeys] = useState11([]);
4776
+ useEffect5(() => {
4777
+ let cancelled = false;
4778
+ setLoading(true);
4779
+ setError(null);
4780
+ Promise.allSettled([api.getUserInfo(), api.getTeams(), api.listKeys()]).then(
4781
+ ([userResult, teamsResult, keysResult]) => {
4782
+ if (cancelled) return;
4783
+ if (userResult.status === "fulfilled") {
4784
+ setUser(userResult.value);
4785
+ } else {
4786
+ setUser(null);
4787
+ setError(
4788
+ userResult.reason?.message ?? "Failed to load your LiteLLM profile"
4789
+ );
4790
+ }
4791
+ setTeams(teamsResult.status === "fulfilled" ? teamsResult.value : []);
4792
+ setKeys(keysResult.status === "fulfilled" ? keysResult.value : []);
4793
+ setLoading(false);
4794
+ }
4795
+ );
4796
+ return () => {
4797
+ cancelled = true;
4798
+ };
4799
+ }, [api]);
4800
+ const summary = useMemo10(
4801
+ () => buildBudgetSummary(user, teams, keys, keys.length),
4802
+ [user, teams, keys]
4803
+ );
4804
+ const gauges = useMemo10(() => buildBudgetGauges(summary), [summary]);
4805
+ const headline = useMemo10(() => budgetHeadline(summary), [summary]);
4806
+ const summaryText = headline.count === 0 ? "no limits apply" : `${headline.count} limit${headline.count === 1 ? "" : "s"}`;
4807
+ return /* @__PURE__ */ React14.createElement(Paper9, { sx: { p: 2 } }, /* @__PURE__ */ React14.createElement(Box13, { sx: { display: "flex", alignItems: "center", gap: 1, mb: 1.5, minWidth: 0 } }, /* @__PURE__ */ React14.createElement(Typography13, { variant: "h6", lineHeight: 1.2, sx: { minWidth: 0, flex: 1 } }, title), !loading && !error && /* @__PURE__ */ React14.createElement(
4808
+ StatusPill,
4809
+ {
4810
+ label: summaryText,
4811
+ tone: budgetTone2(headline.closest?.pct ?? 0),
4812
+ dot: headline.count > 0
4813
+ }
4814
+ )), loading && /* @__PURE__ */ React14.createElement(Box13, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 110 }, /* @__PURE__ */ React14.createElement(CircularProgress8, { size: 28 })), !loading && error && /* @__PURE__ */ React14.createElement(Alert8, { severity: "error", sx: { mt: 0.5 } }, error), !loading && !error && /* @__PURE__ */ React14.createElement(Box13, { sx: { display: "flex", alignItems: "flex-start", gap: 1 } }, gauges.map((gauge) => /* @__PURE__ */ React14.createElement(LevelGauge, { key: gauge.kind, gauge, size, keysHref }))), action && /* @__PURE__ */ React14.createElement(
4815
+ Box13,
4816
+ {
4817
+ sx: (theme) => ({
4818
+ mt: 2,
4819
+ pt: 2,
4820
+ borderTop: `1px solid ${theme.palette.divider}`
4821
+ })
4822
+ },
4823
+ action
4824
+ ));
4825
+ };
4826
+
4827
+ // src/index.ts
4519
4828
  init_GenerateKeyDialog();
4520
4829
  init_ManageTeamDialog();
4521
4830
  init_api();
@@ -4524,6 +4833,7 @@ export {
4524
4833
  DashboardHeader,
4525
4834
  GenerateKeyDialog,
4526
4835
  KeysTable,
4836
+ LiteLLMBudgetGauges,
4527
4837
  LiteLLMBudgetWidget,
4528
4838
  LiteLLMHomeWidget,
4529
4839
  LiteLLMPage,