@acarmisc/backstage-plugin-litellm 0.22.0 → 0.23.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
@@ -2001,6 +2001,8 @@ var init_ManageTeamDialog = __esm({
2001
2001
  const [mcpError, setMcpError] = useState3(null);
2002
2002
  const allowUnlimitedBudget = config?.teamManagement?.allowUnlimitedBudget ?? false;
2003
2003
  const maxBudgetCeiling = config?.teamManagement?.maxBudgetCeiling ?? FALLBACK_BUDGET_CEILING;
2004
+ const hideBudgetForManagers = config?.display?.hideTeamBudgetForManagers ?? false;
2005
+ const budgetWriteOnly = hideBudgetForManagers && mode === "edit";
2004
2006
  const members = useMemo4(() => team?.members_with_roles ?? [], [team]);
2005
2007
  const showMembers = mode === "edit" && !!canManageMembers;
2006
2008
  const catalogApi = useApi(catalogApiRef);
@@ -2036,9 +2038,13 @@ var init_ManageTeamDialog = __esm({
2036
2038
  if (mode === "edit" && team) {
2037
2039
  setAlias(team.team_alias ?? "");
2038
2040
  setModels(team.models ?? []);
2039
- setMaxBudget(
2040
- team.max_budget ? String(team.max_budget) : String(maxBudgetCeiling)
2041
- );
2041
+ if (hideBudgetForManagers) {
2042
+ setMaxBudget("");
2043
+ } else {
2044
+ setMaxBudget(
2045
+ team.max_budget ? String(team.max_budget) : String(maxBudgetCeiling)
2046
+ );
2047
+ }
2042
2048
  setUnlimited(allowUnlimitedBudget && !team.max_budget);
2043
2049
  setBudgetDuration(team.budget_duration ?? DEFAULT_BUDGET_DURATION);
2044
2050
  } else {
@@ -2058,7 +2064,7 @@ var init_ManageTeamDialog = __esm({
2058
2064
  setMcpIds(team?.object_permission?.mcp_servers ?? []);
2059
2065
  setMcpError(null);
2060
2066
  }
2061
- }, [open, mode, team, allowUnlimitedBudget, maxBudgetCeiling]);
2067
+ }, [open, mode, team, allowUnlimitedBudget, maxBudgetCeiling, hideBudgetForManagers]);
2062
2068
  const handleSubmit = async () => {
2063
2069
  setError(null);
2064
2070
  if (!alias.trim()) {
@@ -2070,18 +2076,21 @@ var init_ManageTeamDialog = __esm({
2070
2076
  return;
2071
2077
  }
2072
2078
  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;
2079
+ const budgetLeftBlank = budgetWriteOnly && !maxBudget.trim();
2080
+ if (!budgetLeftBlank) {
2081
+ if (!maxBudget) {
2082
+ setError("Budget is required when unlimited budgets are disabled");
2083
+ return;
2084
+ }
2085
+ const budget = parseFloat(maxBudget);
2086
+ if (isNaN(budget) || budget <= 0) {
2087
+ setError("Budget must be a positive number");
2088
+ return;
2089
+ }
2090
+ if (budget > maxBudgetCeiling) {
2091
+ setError(`Budget cannot exceed $${maxBudgetCeiling}`);
2092
+ return;
2093
+ }
2085
2094
  }
2086
2095
  }
2087
2096
  try {
@@ -2095,10 +2104,16 @@ var init_ManageTeamDialog = __esm({
2095
2104
  ...budgetDuration && { budget_duration: budgetDuration }
2096
2105
  };
2097
2106
  } else {
2107
+ let budgetField = {};
2108
+ if (unlimited) {
2109
+ budgetField = { max_budget: null };
2110
+ } else if (maxBudget.trim()) {
2111
+ budgetField = { max_budget: parseFloat(maxBudget) };
2112
+ }
2098
2113
  payload = {
2099
2114
  team_alias: alias.trim(),
2100
2115
  models,
2101
- ...unlimited ? { max_budget: null } : { max_budget: parseFloat(maxBudget) },
2116
+ ...budgetField,
2102
2117
  ...budgetDuration && { budget_duration: budgetDuration }
2103
2118
  };
2104
2119
  }
@@ -2204,13 +2219,13 @@ var init_ManageTeamDialog = __esm({
2204
2219
  ), /* @__PURE__ */ React5.createElement(
2205
2220
  TextField3,
2206
2221
  {
2207
- label: "Max Budget ($)",
2222
+ label: budgetWriteOnly ? "New Max Budget ($)" : "Max Budget ($)",
2208
2223
  type: "number",
2209
2224
  value: maxBudget,
2210
2225
  onChange: (e) => setMaxBudget(e.target.value),
2211
2226
  disabled: submitting || unlimited,
2212
2227
  inputProps: { min: 0, max: maxBudgetCeiling },
2213
- helperText: `Maximum: $${maxBudgetCeiling}`,
2228
+ helperText: budgetWriteOnly ? "Current budget is hidden \u2014 leave blank to keep it, or enter a new value" : `Maximum: $${maxBudgetCeiling}`,
2214
2229
  fullWidth: true
2215
2230
  }
2216
2231
  ), allowUnlimitedBudget && /* @__PURE__ */ React5.createElement(
@@ -2781,7 +2796,19 @@ function buildBudgetSummary(user, teams, keys, maxKeys = 3) {
2781
2796
  pct: limitPct(spend, budget)
2782
2797
  };
2783
2798
  })() : void 0;
2784
- const teamLimits = teams.filter((t) => (t.max_budget ?? 0) > 0).map((t) => {
2799
+ const teamLimits = teams.filter((t) => (t.max_budget ?? 0) > 0 || t.budget_hidden === true).map((t) => {
2800
+ if (t.budget_hidden === true) {
2801
+ return {
2802
+ kind: "team",
2803
+ label: t.team_alias || "Untitled team",
2804
+ sublabel: t.team_id,
2805
+ budget: 0,
2806
+ spend: 0,
2807
+ budgetDuration: t.budget_duration,
2808
+ pct: Math.min(100, Math.max(0, t.budget_pct ?? 0)),
2809
+ hidden: true
2810
+ };
2811
+ }
2785
2812
  const spend = t.spend ?? 0;
2786
2813
  return {
2787
2814
  kind: "team",
@@ -2957,7 +2984,7 @@ var init_LiteLLMBudgetWidget = __esm({
2957
2984
  }
2958
2985
  },
2959
2986
  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(
2987
+ )), 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
2988
  Box7,
2962
2989
  {
2963
2990
  sx: {
@@ -2968,7 +2995,7 @@ var init_LiteLLMBudgetWidget = __esm({
2968
2995
  mt: 0.75
2969
2996
  }
2970
2997
  },
2971
- /* @__PURE__ */ React7.createElement(Typography7, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, pct, "% of budget", closeTo && ` \xB7 soft-limit warning`),
2998
+ /* @__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
2999
  /* @__PURE__ */ React7.createElement(Typography7, { variant: "caption", color: "text.secondary" }, fmtBudgetDuration(limit.budgetDuration) ? `resets ${fmtBudgetDuration(limit.budgetDuration)}` : "never resets")
2973
3000
  ));
2974
3001
  };
@@ -3176,6 +3203,15 @@ function budgetTone3(isOver, isNear) {
3176
3203
  if (isNear) return "warning";
3177
3204
  return "accent";
3178
3205
  }
3206
+ function visibleBudgetPct(spend, budget) {
3207
+ if (budget <= 0) return 0;
3208
+ return Math.min(spend / budget * 100, 100);
3209
+ }
3210
+ function budgetStatValue(hidden, budget) {
3211
+ if (hidden) return "Hidden";
3212
+ if (budget > 0) return fmtUsd2(budget);
3213
+ return "Unlimited";
3214
+ }
3179
3215
  var fmtUsd2, TeamCard, TeamUsage;
3180
3216
  var init_TeamUsage = __esm({
3181
3217
  "src/components/TeamUsage.tsx"() {
@@ -3185,13 +3221,15 @@ var init_TeamUsage = __esm({
3185
3221
  TeamCard = ({ team, usage, usageLoading, canManage, onEditTeam }) => {
3186
3222
  const [expanded, setExpanded] = useState6(false);
3187
3223
  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;
3224
+ const hidden = team.budget_hidden === true;
3225
+ const budget = hidden ? 0 : team.max_budget ?? 0;
3226
+ const spend = hidden ? 0 : team.spend ?? 0;
3227
+ const budgetPct = hidden ? Math.min(100, Math.max(0, team.budget_pct ?? 0)) : visibleBudgetPct(spend, budget);
3228
+ const isOver = hidden ? team.budget_status === "over" : budget > 0 && spend >= budget;
3229
+ const isNear = hidden ? team.budget_status === "near" : budget > 0 && spend >= budget * 0.8 && !isOver;
3193
3230
  const dailyData = usage?.daily_usage?.map((d) => ({ date: d.date, spend: d.spend })) ?? [];
3194
3231
  const renderDailySpendSection = () => {
3232
+ if (hidden) return null;
3195
3233
  if (usageLoading) {
3196
3234
  return /* @__PURE__ */ React8.createElement(Box8, { display: "flex", justifyContent: "center", py: 3 }, /* @__PURE__ */ React8.createElement(CircularProgress5, { size: 22 }));
3197
3235
  }
@@ -3292,11 +3330,11 @@ var init_TeamUsage = __esm({
3292
3330
  },
3293
3331
  /* @__PURE__ */ React8.createElement(Stat, { label: "Members", value: team.members_with_roles?.length ?? "\u2014" }),
3294
3332
  /* @__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) }),
3333
+ /* @__PURE__ */ React8.createElement(Stat, { label: "Budget", value: budgetStatValue(hidden, budget) }),
3334
+ /* @__PURE__ */ React8.createElement(Stat, { label: "Spend", value: hidden ? "\u2014" : fmtUsd2(spend) }),
3297
3335
  /* @__PURE__ */ React8.createElement(Stat, { label: "TPM", value: team.tpm_limit && team.tpm_limit > 0 ? team.tpm_limit.toLocaleString() : "\u2014" }),
3298
3336
  /* @__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(
3337
+ ), 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
3338
  TableContainer4,
3301
3339
  {
3302
3340
  component: Paper6,