@acarmisc/backstage-plugin-litellm 0.22.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.
@@ -2001,6 +2097,8 @@ var init_ManageTeamDialog = __esm({
2001
2097
  const [mcpError, setMcpError] = useState3(null);
2002
2098
  const allowUnlimitedBudget = config?.teamManagement?.allowUnlimitedBudget ?? false;
2003
2099
  const maxBudgetCeiling = config?.teamManagement?.maxBudgetCeiling ?? FALLBACK_BUDGET_CEILING;
2100
+ const hideBudgetForManagers = config?.display?.hideTeamBudgetForManagers ?? false;
2101
+ const budgetWriteOnly = hideBudgetForManagers && mode === "edit";
2004
2102
  const members = useMemo4(() => team?.members_with_roles ?? [], [team]);
2005
2103
  const showMembers = mode === "edit" && !!canManageMembers;
2006
2104
  const catalogApi = useApi(catalogApiRef);
@@ -2036,9 +2134,13 @@ var init_ManageTeamDialog = __esm({
2036
2134
  if (mode === "edit" && team) {
2037
2135
  setAlias(team.team_alias ?? "");
2038
2136
  setModels(team.models ?? []);
2039
- setMaxBudget(
2040
- team.max_budget ? String(team.max_budget) : String(maxBudgetCeiling)
2041
- );
2137
+ if (hideBudgetForManagers) {
2138
+ setMaxBudget("");
2139
+ } else {
2140
+ setMaxBudget(
2141
+ team.max_budget ? String(team.max_budget) : String(maxBudgetCeiling)
2142
+ );
2143
+ }
2042
2144
  setUnlimited(allowUnlimitedBudget && !team.max_budget);
2043
2145
  setBudgetDuration(team.budget_duration ?? DEFAULT_BUDGET_DURATION);
2044
2146
  } else {
@@ -2058,7 +2160,7 @@ var init_ManageTeamDialog = __esm({
2058
2160
  setMcpIds(team?.object_permission?.mcp_servers ?? []);
2059
2161
  setMcpError(null);
2060
2162
  }
2061
- }, [open, mode, team, allowUnlimitedBudget, maxBudgetCeiling]);
2163
+ }, [open, mode, team, allowUnlimitedBudget, maxBudgetCeiling, hideBudgetForManagers]);
2062
2164
  const handleSubmit = async () => {
2063
2165
  setError(null);
2064
2166
  if (!alias.trim()) {
@@ -2070,18 +2172,21 @@ var init_ManageTeamDialog = __esm({
2070
2172
  return;
2071
2173
  }
2072
2174
  if (!unlimited) {
2073
- if (!maxBudget) {
2074
- setError("Budget is required when unlimited budgets are disabled");
2075
- return;
2076
- }
2077
- const budget = parseFloat(maxBudget);
2078
- if (isNaN(budget) || budget <= 0) {
2079
- setError("Budget must be a positive number");
2080
- return;
2081
- }
2082
- if (budget > maxBudgetCeiling) {
2083
- setError(`Budget cannot exceed $${maxBudgetCeiling}`);
2084
- return;
2175
+ const budgetLeftBlank = budgetWriteOnly && !maxBudget.trim();
2176
+ if (!budgetLeftBlank) {
2177
+ if (!maxBudget) {
2178
+ setError("Budget is required when unlimited budgets are disabled");
2179
+ return;
2180
+ }
2181
+ const budget = parseFloat(maxBudget);
2182
+ if (isNaN(budget) || budget <= 0) {
2183
+ setError("Budget must be a positive number");
2184
+ return;
2185
+ }
2186
+ if (budget > maxBudgetCeiling) {
2187
+ setError(`Budget cannot exceed $${maxBudgetCeiling}`);
2188
+ return;
2189
+ }
2085
2190
  }
2086
2191
  }
2087
2192
  try {
@@ -2095,10 +2200,16 @@ var init_ManageTeamDialog = __esm({
2095
2200
  ...budgetDuration && { budget_duration: budgetDuration }
2096
2201
  };
2097
2202
  } else {
2203
+ let budgetField = {};
2204
+ if (unlimited) {
2205
+ budgetField = { max_budget: null };
2206
+ } else if (maxBudget.trim()) {
2207
+ budgetField = { max_budget: parseFloat(maxBudget) };
2208
+ }
2098
2209
  payload = {
2099
2210
  team_alias: alias.trim(),
2100
2211
  models,
2101
- ...unlimited ? { max_budget: null } : { max_budget: parseFloat(maxBudget) },
2212
+ ...budgetField,
2102
2213
  ...budgetDuration && { budget_duration: budgetDuration }
2103
2214
  };
2104
2215
  }
@@ -2204,13 +2315,13 @@ var init_ManageTeamDialog = __esm({
2204
2315
  ), /* @__PURE__ */ React5.createElement(
2205
2316
  TextField3,
2206
2317
  {
2207
- label: "Max Budget ($)",
2318
+ label: budgetWriteOnly ? "New Max Budget ($)" : "Max Budget ($)",
2208
2319
  type: "number",
2209
2320
  value: maxBudget,
2210
2321
  onChange: (e) => setMaxBudget(e.target.value),
2211
2322
  disabled: submitting || unlimited,
2212
2323
  inputProps: { min: 0, max: maxBudgetCeiling },
2213
- helperText: `Maximum: $${maxBudgetCeiling}`,
2324
+ helperText: budgetWriteOnly ? "Current budget is hidden \u2014 leave blank to keep it, or enter a new value" : `Maximum: $${maxBudgetCeiling}`,
2214
2325
  fullWidth: true
2215
2326
  }
2216
2327
  ), allowUnlimitedBudget && /* @__PURE__ */ React5.createElement(
@@ -2781,7 +2892,19 @@ function buildBudgetSummary(user, teams, keys, maxKeys = 3) {
2781
2892
  pct: limitPct(spend, budget)
2782
2893
  };
2783
2894
  })() : void 0;
2784
- const teamLimits = teams.filter((t) => (t.max_budget ?? 0) > 0).map((t) => {
2895
+ const teamLimits = teams.filter((t) => (t.max_budget ?? 0) > 0 || t.budget_hidden === true).map((t) => {
2896
+ if (t.budget_hidden === true) {
2897
+ return {
2898
+ kind: "team",
2899
+ label: t.team_alias || "Untitled team",
2900
+ sublabel: t.team_id,
2901
+ budget: 0,
2902
+ spend: 0,
2903
+ budgetDuration: t.budget_duration,
2904
+ pct: Math.min(100, Math.max(0, t.budget_pct ?? 0)),
2905
+ hidden: true
2906
+ };
2907
+ }
2785
2908
  const spend = t.spend ?? 0;
2786
2909
  return {
2787
2910
  kind: "team",
@@ -2801,6 +2924,22 @@ function buildBudgetSummary(user, teams, keys, maxKeys = 3) {
2801
2924
  teams: teamLimits
2802
2925
  };
2803
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
+ }
2804
2943
  function budgetHeadline(summary) {
2805
2944
  const all = [
2806
2945
  ...summary.keys,
@@ -2827,9 +2966,11 @@ function budgetTone2(pct) {
2827
2966
  if (pct >= 80) return "warning";
2828
2967
  return "accent";
2829
2968
  }
2969
+ var GAUGE_KINDS;
2830
2970
  var init_budget = __esm({
2831
2971
  "src/budget.ts"() {
2832
2972
  "use strict";
2973
+ GAUGE_KINDS = ["key", "user", "team"];
2833
2974
  }
2834
2975
  });
2835
2976
 
@@ -2957,7 +3098,7 @@ var init_LiteLLMBudgetWidget = __esm({
2957
3098
  }
2958
3099
  },
2959
3100
  limit.sublabel
2960
- )), /* @__PURE__ */ React7.createElement(Typography7, { variant: "body2", sx: { fontWeight: 600, fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap" } }, fmtUsd(limit.spend), /* @__PURE__ */ React7.createElement(Box7, { component: "span", color: "text.secondary", sx: { fontWeight: 400 } }, " / ", fmtUsd(limit.budget)))), /* @__PURE__ */ React7.createElement(Box7, { sx: { mt: 1 } }, /* @__PURE__ */ React7.createElement(Meter, { value: limit.pct, tone, height: 6 })), /* @__PURE__ */ React7.createElement(
3101
+ )), limit.hidden ? /* @__PURE__ */ React7.createElement(Typography7, { variant: "body2", sx: { fontWeight: 600, fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap" } }, pct, "% used") : /* @__PURE__ */ React7.createElement(Typography7, { variant: "body2", sx: { fontWeight: 600, fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap" } }, fmtUsd(limit.spend), /* @__PURE__ */ React7.createElement(Box7, { component: "span", color: "text.secondary", sx: { fontWeight: 400 } }, " / ", fmtUsd(limit.budget)))), /* @__PURE__ */ React7.createElement(Box7, { sx: { mt: 1 } }, /* @__PURE__ */ React7.createElement(Meter, { value: limit.pct, tone, height: 6 })), /* @__PURE__ */ React7.createElement(
2961
3102
  Box7,
2962
3103
  {
2963
3104
  sx: {
@@ -2968,7 +3109,7 @@ var init_LiteLLMBudgetWidget = __esm({
2968
3109
  mt: 0.75
2969
3110
  }
2970
3111
  },
2971
- /* @__PURE__ */ React7.createElement(Typography7, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, pct, "% of budget", closeTo && ` \xB7 soft-limit warning`),
3112
+ /* @__PURE__ */ React7.createElement(Typography7, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, limit.hidden ? /* @__PURE__ */ React7.createElement(React7.Fragment, null, "Hidden by admin") : /* @__PURE__ */ React7.createElement(React7.Fragment, null, pct, "% of budget", closeTo && ` \xB7 soft-limit warning`)),
2972
3113
  /* @__PURE__ */ React7.createElement(Typography7, { variant: "caption", color: "text.secondary" }, fmtBudgetDuration(limit.budgetDuration) ? `resets ${fmtBudgetDuration(limit.budgetDuration)}` : "never resets")
2973
3114
  ));
2974
3115
  };
@@ -3176,6 +3317,15 @@ function budgetTone3(isOver, isNear) {
3176
3317
  if (isNear) return "warning";
3177
3318
  return "accent";
3178
3319
  }
3320
+ function visibleBudgetPct(spend, budget) {
3321
+ if (budget <= 0) return 0;
3322
+ return Math.min(spend / budget * 100, 100);
3323
+ }
3324
+ function budgetStatValue(hidden, budget) {
3325
+ if (hidden) return "Hidden";
3326
+ if (budget > 0) return fmtUsd2(budget);
3327
+ return "Unlimited";
3328
+ }
3179
3329
  var fmtUsd2, TeamCard, TeamUsage;
3180
3330
  var init_TeamUsage = __esm({
3181
3331
  "src/components/TeamUsage.tsx"() {
@@ -3185,13 +3335,15 @@ var init_TeamUsage = __esm({
3185
3335
  TeamCard = ({ team, usage, usageLoading, canManage, onEditTeam }) => {
3186
3336
  const [expanded, setExpanded] = useState6(false);
3187
3337
  const chart = useChartTheme();
3188
- const budget = team.max_budget ?? 0;
3189
- const spend = team.spend ?? 0;
3190
- const budgetPct = budget > 0 ? Math.min(spend / budget * 100, 100) : 0;
3191
- const isOver = budget > 0 && spend >= budget;
3192
- const isNear = budget > 0 && spend >= budget * 0.8 && !isOver;
3338
+ const hidden = team.budget_hidden === true;
3339
+ const budget = hidden ? 0 : team.max_budget ?? 0;
3340
+ const spend = hidden ? 0 : team.spend ?? 0;
3341
+ const budgetPct = hidden ? Math.min(100, Math.max(0, team.budget_pct ?? 0)) : visibleBudgetPct(spend, budget);
3342
+ const isOver = hidden ? team.budget_status === "over" : budget > 0 && spend >= budget;
3343
+ const isNear = hidden ? team.budget_status === "near" : budget > 0 && spend >= budget * 0.8 && !isOver;
3193
3344
  const dailyData = usage?.daily_usage?.map((d) => ({ date: d.date, spend: d.spend })) ?? [];
3194
3345
  const renderDailySpendSection = () => {
3346
+ if (hidden) return null;
3195
3347
  if (usageLoading) {
3196
3348
  return /* @__PURE__ */ React8.createElement(Box8, { display: "flex", justifyContent: "center", py: 3 }, /* @__PURE__ */ React8.createElement(CircularProgress5, { size: 22 }));
3197
3349
  }
@@ -3292,11 +3444,11 @@ var init_TeamUsage = __esm({
3292
3444
  },
3293
3445
  /* @__PURE__ */ React8.createElement(Stat, { label: "Members", value: team.members_with_roles?.length ?? "\u2014" }),
3294
3446
  /* @__PURE__ */ React8.createElement(Stat, { label: "Models", value: team.models?.length ? team.models.length : "All" }),
3295
- /* @__PURE__ */ React8.createElement(Stat, { label: "Budget", value: budget > 0 ? fmtUsd2(budget) : "Unlimited" }),
3296
- /* @__PURE__ */ React8.createElement(Stat, { label: "Spend", value: fmtUsd2(spend) }),
3447
+ /* @__PURE__ */ React8.createElement(Stat, { label: "Budget", value: budgetStatValue(hidden, budget) }),
3448
+ /* @__PURE__ */ React8.createElement(Stat, { label: "Spend", value: hidden ? "\u2014" : fmtUsd2(spend) }),
3297
3449
  /* @__PURE__ */ React8.createElement(Stat, { label: "TPM", value: team.tpm_limit && team.tpm_limit > 0 ? team.tpm_limit.toLocaleString() : "\u2014" }),
3298
3450
  /* @__PURE__ */ React8.createElement(Stat, { label: "RPM", value: team.rpm_limit && team.rpm_limit > 0 ? team.rpm_limit.toLocaleString() : "\u2014" })
3299
- ), budget > 0 && /* @__PURE__ */ React8.createElement(Box8, { mt: 2, maxWidth: 640 }, /* @__PURE__ */ React8.createElement(Box8, { display: "flex", justifyContent: "space-between", alignItems: "baseline", mb: 0.75 }, /* @__PURE__ */ React8.createElement(Typography8, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, fmtUsd2(spend), " of ", fmtUsd2(budget)), /* @__PURE__ */ React8.createElement(Typography8, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, budgetPct.toFixed(0), "%")), /* @__PURE__ */ React8.createElement(Meter, { value: budgetPct, tone: budgetTone3(isOver, isNear), height: 5 })), /* @__PURE__ */ React8.createElement(Collapse2, { in: expanded, unmountOnExit: true }, /* @__PURE__ */ React8.createElement(Divider3, { sx: { my: 2.5 } }), /* @__PURE__ */ React8.createElement(Box8, { mb: 2.5 }, sectionLabel("Models"), team.models?.length ? /* @__PURE__ */ React8.createElement(Box8, { display: "flex", gap: 0.5, flexWrap: "wrap" }, team.models.map((m) => /* @__PURE__ */ React8.createElement(TagChip, { key: m, label: m, title: m }))) : /* @__PURE__ */ React8.createElement(Typography8, { variant: "body2", color: "text.secondary" }, "All models allowed")), renderDailySpendSection(), /* @__PURE__ */ React8.createElement(Box8, null, sectionLabel("Members"), team.members_with_roles?.length ? /* @__PURE__ */ React8.createElement(
3451
+ ), hidden && /* @__PURE__ */ React8.createElement(Box8, { mt: 2, maxWidth: 640 }, /* @__PURE__ */ React8.createElement(Box8, { display: "flex", justifyContent: "space-between", alignItems: "baseline", mb: 0.75 }, /* @__PURE__ */ React8.createElement(Typography8, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, budgetPct.toFixed(0), "% used"), /* @__PURE__ */ React8.createElement(Typography8, { variant: "caption", color: "text.secondary" }, "Hidden by admin")), /* @__PURE__ */ React8.createElement(Meter, { value: budgetPct, tone: budgetTone3(isOver, isNear), height: 5 }), /* @__PURE__ */ React8.createElement(Typography8, { variant: "caption", color: "text.secondary", sx: { display: "block", mt: 0.75 } }, "Team budget is hidden \u2014 showing usage level only", team.budget_duration ? ` \xB7 resets every ${team.budget_duration}` : "", ".")), !hidden && budget > 0 && /* @__PURE__ */ React8.createElement(Box8, { mt: 2, maxWidth: 640 }, /* @__PURE__ */ React8.createElement(Box8, { display: "flex", justifyContent: "space-between", alignItems: "baseline", mb: 0.75 }, /* @__PURE__ */ React8.createElement(Typography8, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, fmtUsd2(spend), " of ", fmtUsd2(budget)), /* @__PURE__ */ React8.createElement(Typography8, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, budgetPct.toFixed(0), "%")), /* @__PURE__ */ React8.createElement(Meter, { value: budgetPct, tone: budgetTone3(isOver, isNear), height: 5 })), /* @__PURE__ */ React8.createElement(Collapse2, { in: expanded, unmountOnExit: true }, /* @__PURE__ */ React8.createElement(Divider3, { sx: { my: 2.5 } }), /* @__PURE__ */ React8.createElement(Box8, { mb: 2.5 }, sectionLabel("Models"), team.models?.length ? /* @__PURE__ */ React8.createElement(Box8, { display: "flex", gap: 0.5, flexWrap: "wrap" }, team.models.map((m) => /* @__PURE__ */ React8.createElement(TagChip, { key: m, label: m, title: m }))) : /* @__PURE__ */ React8.createElement(Typography8, { variant: "body2", color: "text.secondary" }, "All models allowed")), renderDailySpendSection(), /* @__PURE__ */ React8.createElement(Box8, null, sectionLabel("Members"), team.members_with_roles?.length ? /* @__PURE__ */ React8.createElement(
3300
3452
  TableContainer4,
3301
3453
  {
3302
3454
  component: Paper6,
@@ -4478,6 +4630,201 @@ var LiteLLMHomeWidget = ({
4478
4630
 
4479
4631
  // src/index.ts
4480
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
4481
4828
  init_GenerateKeyDialog();
4482
4829
  init_ManageTeamDialog();
4483
4830
  init_api();
@@ -4486,6 +4833,7 @@ export {
4486
4833
  DashboardHeader,
4487
4834
  GenerateKeyDialog,
4488
4835
  KeysTable,
4836
+ LiteLLMBudgetGauges,
4489
4837
  LiteLLMBudgetWidget,
4490
4838
  LiteLLMHomeWidget,
4491
4839
  LiteLLMPage,