@acarmisc/backstage-plugin-litellm 0.21.3 → 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/budget.d.ts CHANGED
@@ -17,6 +17,12 @@ export interface BudgetLimit {
17
17
  softLimit?: number;
18
18
  /** Share of the cap spent, 0..100+ (unclamped). */
19
19
  pct: number;
20
+ /**
21
+ * True when the backend redacted the dollar amounts (team budget hiding).
22
+ * `budget`/`spend` are then 0 and only `pct` carries the signal — render
23
+ * the meter without `$` figures.
24
+ */
25
+ hidden?: boolean;
20
26
  }
21
27
  /** The budgets that apply to the current user, grouped by enforcement level. */
22
28
  export interface BudgetSummary {
package/dist/index.cjs.js CHANGED
@@ -2023,6 +2023,8 @@ var init_ManageTeamDialog = __esm({
2023
2023
  const [mcpError, setMcpError] = (0, import_react5.useState)(null);
2024
2024
  const allowUnlimitedBudget = config?.teamManagement?.allowUnlimitedBudget ?? false;
2025
2025
  const maxBudgetCeiling = config?.teamManagement?.maxBudgetCeiling ?? FALLBACK_BUDGET_CEILING;
2026
+ const hideBudgetForManagers = config?.display?.hideTeamBudgetForManagers ?? false;
2027
+ const budgetWriteOnly = hideBudgetForManagers && mode === "edit";
2026
2028
  const members = (0, import_react5.useMemo)(() => team?.members_with_roles ?? [], [team]);
2027
2029
  const showMembers = mode === "edit" && !!canManageMembers;
2028
2030
  const catalogApi = (0, import_core_plugin_api2.useApi)(import_plugin_catalog_react.catalogApiRef);
@@ -2058,9 +2060,13 @@ var init_ManageTeamDialog = __esm({
2058
2060
  if (mode === "edit" && team) {
2059
2061
  setAlias(team.team_alias ?? "");
2060
2062
  setModels(team.models ?? []);
2061
- setMaxBudget(
2062
- team.max_budget ? String(team.max_budget) : String(maxBudgetCeiling)
2063
- );
2063
+ if (hideBudgetForManagers) {
2064
+ setMaxBudget("");
2065
+ } else {
2066
+ setMaxBudget(
2067
+ team.max_budget ? String(team.max_budget) : String(maxBudgetCeiling)
2068
+ );
2069
+ }
2064
2070
  setUnlimited(allowUnlimitedBudget && !team.max_budget);
2065
2071
  setBudgetDuration(team.budget_duration ?? DEFAULT_BUDGET_DURATION);
2066
2072
  } else {
@@ -2080,7 +2086,7 @@ var init_ManageTeamDialog = __esm({
2080
2086
  setMcpIds(team?.object_permission?.mcp_servers ?? []);
2081
2087
  setMcpError(null);
2082
2088
  }
2083
- }, [open, mode, team, allowUnlimitedBudget, maxBudgetCeiling]);
2089
+ }, [open, mode, team, allowUnlimitedBudget, maxBudgetCeiling, hideBudgetForManagers]);
2084
2090
  const handleSubmit = async () => {
2085
2091
  setError(null);
2086
2092
  if (!alias.trim()) {
@@ -2092,18 +2098,21 @@ var init_ManageTeamDialog = __esm({
2092
2098
  return;
2093
2099
  }
2094
2100
  if (!unlimited) {
2095
- if (!maxBudget) {
2096
- setError("Budget is required when unlimited budgets are disabled");
2097
- return;
2098
- }
2099
- const budget = parseFloat(maxBudget);
2100
- if (isNaN(budget) || budget <= 0) {
2101
- setError("Budget must be a positive number");
2102
- return;
2103
- }
2104
- if (budget > maxBudgetCeiling) {
2105
- setError(`Budget cannot exceed $${maxBudgetCeiling}`);
2106
- return;
2101
+ const budgetLeftBlank = budgetWriteOnly && !maxBudget.trim();
2102
+ if (!budgetLeftBlank) {
2103
+ if (!maxBudget) {
2104
+ setError("Budget is required when unlimited budgets are disabled");
2105
+ return;
2106
+ }
2107
+ const budget = parseFloat(maxBudget);
2108
+ if (isNaN(budget) || budget <= 0) {
2109
+ setError("Budget must be a positive number");
2110
+ return;
2111
+ }
2112
+ if (budget > maxBudgetCeiling) {
2113
+ setError(`Budget cannot exceed $${maxBudgetCeiling}`);
2114
+ return;
2115
+ }
2107
2116
  }
2108
2117
  }
2109
2118
  try {
@@ -2117,10 +2126,16 @@ var init_ManageTeamDialog = __esm({
2117
2126
  ...budgetDuration && { budget_duration: budgetDuration }
2118
2127
  };
2119
2128
  } else {
2129
+ let budgetField = {};
2130
+ if (unlimited) {
2131
+ budgetField = { max_budget: null };
2132
+ } else if (maxBudget.trim()) {
2133
+ budgetField = { max_budget: parseFloat(maxBudget) };
2134
+ }
2120
2135
  payload = {
2121
2136
  team_alias: alias.trim(),
2122
2137
  models,
2123
- ...unlimited ? { max_budget: null } : { max_budget: parseFloat(maxBudget) },
2138
+ ...budgetField,
2124
2139
  ...budgetDuration && { budget_duration: budgetDuration }
2125
2140
  };
2126
2141
  }
@@ -2226,13 +2241,13 @@ var init_ManageTeamDialog = __esm({
2226
2241
  ), /* @__PURE__ */ import_react5.default.createElement(
2227
2242
  import_TextField3.default,
2228
2243
  {
2229
- label: "Max Budget ($)",
2244
+ label: budgetWriteOnly ? "New Max Budget ($)" : "Max Budget ($)",
2230
2245
  type: "number",
2231
2246
  value: maxBudget,
2232
2247
  onChange: (e) => setMaxBudget(e.target.value),
2233
2248
  disabled: submitting || unlimited,
2234
2249
  inputProps: { min: 0, max: maxBudgetCeiling },
2235
- helperText: `Maximum: $${maxBudgetCeiling}`,
2250
+ helperText: budgetWriteOnly ? "Current budget is hidden \u2014 leave blank to keep it, or enter a new value" : `Maximum: $${maxBudgetCeiling}`,
2236
2251
  fullWidth: true
2237
2252
  }
2238
2253
  ), allowUnlimitedBudget && /* @__PURE__ */ import_react5.default.createElement(
@@ -2788,7 +2803,19 @@ function buildBudgetSummary(user, teams, keys, maxKeys = 3) {
2788
2803
  pct: limitPct(spend, budget)
2789
2804
  };
2790
2805
  })() : void 0;
2791
- const teamLimits = teams.filter((t) => (t.max_budget ?? 0) > 0).map((t) => {
2806
+ const teamLimits = teams.filter((t) => (t.max_budget ?? 0) > 0 || t.budget_hidden === true).map((t) => {
2807
+ if (t.budget_hidden === true) {
2808
+ return {
2809
+ kind: "team",
2810
+ label: t.team_alias || "Untitled team",
2811
+ sublabel: t.team_id,
2812
+ budget: 0,
2813
+ spend: 0,
2814
+ budgetDuration: t.budget_duration,
2815
+ pct: Math.min(100, Math.max(0, t.budget_pct ?? 0)),
2816
+ hidden: true
2817
+ };
2818
+ }
2792
2819
  const spend = t.spend ?? 0;
2793
2820
  return {
2794
2821
  kind: "team",
@@ -2964,7 +2991,7 @@ var init_LiteLLMBudgetWidget = __esm({
2964
2991
  }
2965
2992
  },
2966
2993
  limit.sublabel
2967
- )), /* @__PURE__ */ import_react7.default.createElement(import_Typography7.default, { variant: "body2", sx: { fontWeight: 600, fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap" } }, fmtUsd(limit.spend), /* @__PURE__ */ import_react7.default.createElement(import_Box7.default, { component: "span", color: "text.secondary", sx: { fontWeight: 400 } }, " / ", fmtUsd(limit.budget)))), /* @__PURE__ */ import_react7.default.createElement(import_Box7.default, { sx: { mt: 1 } }, /* @__PURE__ */ import_react7.default.createElement(Meter, { value: limit.pct, tone, height: 6 })), /* @__PURE__ */ import_react7.default.createElement(
2994
+ )), limit.hidden ? /* @__PURE__ */ import_react7.default.createElement(import_Typography7.default, { variant: "body2", sx: { fontWeight: 600, fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap" } }, pct, "% used") : /* @__PURE__ */ import_react7.default.createElement(import_Typography7.default, { variant: "body2", sx: { fontWeight: 600, fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap" } }, fmtUsd(limit.spend), /* @__PURE__ */ import_react7.default.createElement(import_Box7.default, { component: "span", color: "text.secondary", sx: { fontWeight: 400 } }, " / ", fmtUsd(limit.budget)))), /* @__PURE__ */ import_react7.default.createElement(import_Box7.default, { sx: { mt: 1 } }, /* @__PURE__ */ import_react7.default.createElement(Meter, { value: limit.pct, tone, height: 6 })), /* @__PURE__ */ import_react7.default.createElement(
2968
2995
  import_Box7.default,
2969
2996
  {
2970
2997
  sx: {
@@ -2975,7 +3002,7 @@ var init_LiteLLMBudgetWidget = __esm({
2975
3002
  mt: 0.75
2976
3003
  }
2977
3004
  },
2978
- /* @__PURE__ */ import_react7.default.createElement(import_Typography7.default, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, pct, "% of budget", closeTo && ` \xB7 soft-limit warning`),
3005
+ /* @__PURE__ */ import_react7.default.createElement(import_Typography7.default, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, limit.hidden ? /* @__PURE__ */ import_react7.default.createElement(import_react7.default.Fragment, null, "Hidden by admin") : /* @__PURE__ */ import_react7.default.createElement(import_react7.default.Fragment, null, pct, "% of budget", closeTo && ` \xB7 soft-limit warning`)),
2979
3006
  /* @__PURE__ */ import_react7.default.createElement(import_Typography7.default, { variant: "caption", color: "text.secondary" }, fmtBudgetDuration(limit.budgetDuration) ? `resets ${fmtBudgetDuration(limit.budgetDuration)}` : "never resets")
2980
3007
  ));
2981
3008
  };
@@ -3156,6 +3183,15 @@ function budgetTone3(isOver, isNear) {
3156
3183
  if (isNear) return "warning";
3157
3184
  return "accent";
3158
3185
  }
3186
+ function visibleBudgetPct(spend, budget) {
3187
+ if (budget <= 0) return 0;
3188
+ return Math.min(spend / budget * 100, 100);
3189
+ }
3190
+ function budgetStatValue(hidden, budget) {
3191
+ if (hidden) return "Hidden";
3192
+ if (budget > 0) return fmtUsd2(budget);
3193
+ return "Unlimited";
3194
+ }
3159
3195
  var import_react8, import_Paper6, import_Box8, import_Typography8, import_Skeleton4, import_Divider3, import_Table4, import_TableBody4, import_TableCell4, import_TableHead4, import_TableRow4, import_TableContainer4, import_Collapse2, import_IconButton5, import_Button5, import_CircularProgress5, import_styles5, import_icons_material6, import_recharts2, fmtUsd2, TeamCard, TeamUsage;
3160
3196
  var init_TeamUsage = __esm({
3161
3197
  "src/components/TeamUsage.tsx"() {
@@ -3184,13 +3220,15 @@ var init_TeamUsage = __esm({
3184
3220
  TeamCard = ({ team, usage, usageLoading, canManage, onEditTeam }) => {
3185
3221
  const [expanded, setExpanded] = (0, import_react8.useState)(false);
3186
3222
  const chart = useChartTheme();
3187
- const budget = team.max_budget ?? 0;
3188
- const spend = team.spend ?? 0;
3189
- const budgetPct = budget > 0 ? Math.min(spend / budget * 100, 100) : 0;
3190
- const isOver = budget > 0 && spend >= budget;
3191
- const isNear = budget > 0 && spend >= budget * 0.8 && !isOver;
3223
+ const hidden = team.budget_hidden === true;
3224
+ const budget = hidden ? 0 : team.max_budget ?? 0;
3225
+ const spend = hidden ? 0 : team.spend ?? 0;
3226
+ const budgetPct = hidden ? Math.min(100, Math.max(0, team.budget_pct ?? 0)) : visibleBudgetPct(spend, budget);
3227
+ const isOver = hidden ? team.budget_status === "over" : budget > 0 && spend >= budget;
3228
+ const isNear = hidden ? team.budget_status === "near" : budget > 0 && spend >= budget * 0.8 && !isOver;
3192
3229
  const dailyData = usage?.daily_usage?.map((d) => ({ date: d.date, spend: d.spend })) ?? [];
3193
3230
  const renderDailySpendSection = () => {
3231
+ if (hidden) return null;
3194
3232
  if (usageLoading) {
3195
3233
  return /* @__PURE__ */ import_react8.default.createElement(import_Box8.default, { display: "flex", justifyContent: "center", py: 3 }, /* @__PURE__ */ import_react8.default.createElement(import_CircularProgress5.default, { size: 22 }));
3196
3234
  }
@@ -3291,11 +3329,11 @@ var init_TeamUsage = __esm({
3291
3329
  },
3292
3330
  /* @__PURE__ */ import_react8.default.createElement(Stat, { label: "Members", value: team.members_with_roles?.length ?? "\u2014" }),
3293
3331
  /* @__PURE__ */ import_react8.default.createElement(Stat, { label: "Models", value: team.models?.length ? team.models.length : "All" }),
3294
- /* @__PURE__ */ import_react8.default.createElement(Stat, { label: "Budget", value: budget > 0 ? fmtUsd2(budget) : "Unlimited" }),
3295
- /* @__PURE__ */ import_react8.default.createElement(Stat, { label: "Spend", value: fmtUsd2(spend) }),
3332
+ /* @__PURE__ */ import_react8.default.createElement(Stat, { label: "Budget", value: budgetStatValue(hidden, budget) }),
3333
+ /* @__PURE__ */ import_react8.default.createElement(Stat, { label: "Spend", value: hidden ? "\u2014" : fmtUsd2(spend) }),
3296
3334
  /* @__PURE__ */ import_react8.default.createElement(Stat, { label: "TPM", value: team.tpm_limit && team.tpm_limit > 0 ? team.tpm_limit.toLocaleString() : "\u2014" }),
3297
3335
  /* @__PURE__ */ import_react8.default.createElement(Stat, { label: "RPM", value: team.rpm_limit && team.rpm_limit > 0 ? team.rpm_limit.toLocaleString() : "\u2014" })
3298
- ), budget > 0 && /* @__PURE__ */ import_react8.default.createElement(import_Box8.default, { mt: 2, maxWidth: 640 }, /* @__PURE__ */ import_react8.default.createElement(import_Box8.default, { display: "flex", justifyContent: "space-between", alignItems: "baseline", mb: 0.75 }, /* @__PURE__ */ import_react8.default.createElement(import_Typography8.default, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, fmtUsd2(spend), " of ", fmtUsd2(budget)), /* @__PURE__ */ import_react8.default.createElement(import_Typography8.default, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, budgetPct.toFixed(0), "%")), /* @__PURE__ */ import_react8.default.createElement(Meter, { value: budgetPct, tone: budgetTone3(isOver, isNear), height: 5 })), /* @__PURE__ */ import_react8.default.createElement(import_Collapse2.default, { in: expanded, unmountOnExit: true }, /* @__PURE__ */ import_react8.default.createElement(import_Divider3.default, { sx: { my: 2.5 } }), /* @__PURE__ */ import_react8.default.createElement(import_Box8.default, { mb: 2.5 }, sectionLabel("Models"), team.models?.length ? /* @__PURE__ */ import_react8.default.createElement(import_Box8.default, { display: "flex", gap: 0.5, flexWrap: "wrap" }, team.models.map((m) => /* @__PURE__ */ import_react8.default.createElement(TagChip, { key: m, label: m, title: m }))) : /* @__PURE__ */ import_react8.default.createElement(import_Typography8.default, { variant: "body2", color: "text.secondary" }, "All models allowed")), renderDailySpendSection(), /* @__PURE__ */ import_react8.default.createElement(import_Box8.default, null, sectionLabel("Members"), team.members_with_roles?.length ? /* @__PURE__ */ import_react8.default.createElement(
3336
+ ), hidden && /* @__PURE__ */ import_react8.default.createElement(import_Box8.default, { mt: 2, maxWidth: 640 }, /* @__PURE__ */ import_react8.default.createElement(import_Box8.default, { display: "flex", justifyContent: "space-between", alignItems: "baseline", mb: 0.75 }, /* @__PURE__ */ import_react8.default.createElement(import_Typography8.default, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, budgetPct.toFixed(0), "% used"), /* @__PURE__ */ import_react8.default.createElement(import_Typography8.default, { variant: "caption", color: "text.secondary" }, "Hidden by admin")), /* @__PURE__ */ import_react8.default.createElement(Meter, { value: budgetPct, tone: budgetTone3(isOver, isNear), height: 5 }), /* @__PURE__ */ import_react8.default.createElement(import_Typography8.default, { 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__ */ import_react8.default.createElement(import_Box8.default, { mt: 2, maxWidth: 640 }, /* @__PURE__ */ import_react8.default.createElement(import_Box8.default, { display: "flex", justifyContent: "space-between", alignItems: "baseline", mb: 0.75 }, /* @__PURE__ */ import_react8.default.createElement(import_Typography8.default, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, fmtUsd2(spend), " of ", fmtUsd2(budget)), /* @__PURE__ */ import_react8.default.createElement(import_Typography8.default, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, budgetPct.toFixed(0), "%")), /* @__PURE__ */ import_react8.default.createElement(Meter, { value: budgetPct, tone: budgetTone3(isOver, isNear), height: 5 })), /* @__PURE__ */ import_react8.default.createElement(import_Collapse2.default, { in: expanded, unmountOnExit: true }, /* @__PURE__ */ import_react8.default.createElement(import_Divider3.default, { sx: { my: 2.5 } }), /* @__PURE__ */ import_react8.default.createElement(import_Box8.default, { mb: 2.5 }, sectionLabel("Models"), team.models?.length ? /* @__PURE__ */ import_react8.default.createElement(import_Box8.default, { display: "flex", gap: 0.5, flexWrap: "wrap" }, team.models.map((m) => /* @__PURE__ */ import_react8.default.createElement(TagChip, { key: m, label: m, title: m }))) : /* @__PURE__ */ import_react8.default.createElement(import_Typography8.default, { variant: "body2", color: "text.secondary" }, "All models allowed")), renderDailySpendSection(), /* @__PURE__ */ import_react8.default.createElement(import_Box8.default, null, sectionLabel("Members"), team.members_with_roles?.length ? /* @__PURE__ */ import_react8.default.createElement(
3299
3337
  import_TableContainer4.default,
3300
3338
  {
3301
3339
  component: import_Paper6.default,
@@ -3763,10 +3801,10 @@ var init_AuditLog = __esm({
3763
3801
  const [page, setPage] = (0, import_react10.useState)(0);
3764
3802
  const [pageSize, setPageSize] = (0, import_react10.useState)(25);
3765
3803
  const [filters, setFilters] = (0, import_react10.useState)({});
3766
- const fetchParams = (0, import_react10.useCallback)(
3804
+ const fetchParams = (0, import_react10.useMemo)(
3767
3805
  () => ({ page: page + 1, page_size: pageSize, ...filters }),
3768
3806
  [page, pageSize, filters]
3769
- )();
3807
+ );
3770
3808
  const { value, loading, error } = (0, import_react_use2.useAsync)(
3771
3809
  () => api.getAuditLogs(fetchParams),
3772
3810
  [api, fetchParams]