@acarmisc/backstage-plugin-litellm-backend 0.12.0 → 0.13.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/config.d.ts CHANGED
@@ -248,6 +248,34 @@ export interface Config {
248
248
  };
249
249
  };
250
250
 
251
+ /**
252
+ * Display controls for team budget amounts in the frontend.
253
+ * Each flag independently hides real dollar budgets while the backend
254
+ * still exposes the consumption level (percent of cap + ok/near/over
255
+ * status + reset window). Enforcement is server-side: the backend
256
+ * redacts max_budget/spend from the corresponding endpoints, so the
257
+ * dollars are not merely hidden in CSS.
258
+ */
259
+ display?: {
260
+ /**
261
+ * Hide dollar budgets from regular team members (GET /teams, Teams
262
+ * cards, TEAM section of the budget widget). Team managers still see
263
+ * dollars unless hideTeamBudgetForManagers is also set.
264
+ * @default false
265
+ * @visibility frontend
266
+ */
267
+ hideTeamBudgetForMembers?: boolean;
268
+
269
+ /**
270
+ * Hide dollar budgets even from team managers (GET /teams/managed,
271
+ * team write responses, ManageTeamDialog budget field which becomes
272
+ * write-only). Useful when budgets are finance-sensitive.
273
+ * @default false
274
+ * @visibility frontend
275
+ */
276
+ hideTeamBudgetForManagers?: boolean;
277
+ };
278
+
251
279
  bridge?: {
252
280
  /**
253
281
  * When true, mount the /bridge/keys, /bridge/keys (POST), /bridge/models
package/dist/index.cjs.js CHANGED
@@ -8002,10 +8002,14 @@ __export(index_exports, {
8002
8002
  readProvisioningDefaults: () => readProvisioningDefaults,
8003
8003
  readRoleConfigs: () => readRoleConfigs,
8004
8004
  readTeamAdminConfig: () => readTeamAdminConfig,
8005
+ readTeamBudgetVisibility: () => readTeamBudgetVisibility,
8006
+ redactTeamBudget: () => redactTeamBudget,
8007
+ redactTeamUsage: () => redactTeamUsage,
8005
8008
  resolveBridgeUserId: () => resolveBridgeUserId,
8006
8009
  resolveUserId: () => resolveUserId,
8007
8010
  resolveUserProfile: () => resolveUserProfile,
8008
8011
  resolveUserRole: () => resolveUserRole,
8012
+ teamBudgetStatusFor: () => teamBudgetStatusFor,
8009
8013
  toLiteLLMUserId: () => toLiteLLMUserId,
8010
8014
  validateTeamPatchInput: () => validateTeamPatchInput,
8011
8015
  validateTeamWriteInput: () => validateTeamWriteInput
@@ -8629,6 +8633,10 @@ var openApiSpec = {
8629
8633
  keyGeneration: { type: "object", properties: {
8630
8634
  allowUnlimitedBudget: { type: "boolean" },
8631
8635
  teamRequired: { type: "boolean" }
8636
+ } },
8637
+ display: { type: "object", properties: {
8638
+ hideTeamBudgetForMembers: { type: "boolean" },
8639
+ hideTeamBudgetForManagers: { type: "boolean" }
8632
8640
  } }
8633
8641
  } } } }
8634
8642
  }
@@ -8733,6 +8741,7 @@ var openApiSpec = {
8733
8741
  get: {
8734
8742
  tags: ["Teams"],
8735
8743
  summary: "List teams the current user belongs to",
8744
+ description: "When litellm.display.hideTeamBudgetForMembers is set, max_budget/spend are stripped and budget_pct/budget_status/budget_hidden carry the consumption level instead.",
8736
8745
  responses: { "200": { description: "Array of teams" } }
8737
8746
  }
8738
8747
  },
@@ -8740,6 +8749,7 @@ var openApiSpec = {
8740
8749
  get: {
8741
8750
  tags: ["Teams"],
8742
8751
  summary: "Usage metrics for a team",
8752
+ description: "Spend fields are zeroed when the caller is subject to team budget hiding (member or manager flag, whichever applies to them).",
8743
8753
  parameters: [
8744
8754
  { name: "teamId", in: "path", required: true, schema: { type: "string" } },
8745
8755
  { name: "start_date", in: "query", required: true, schema: { type: "string" } },
@@ -10827,6 +10837,65 @@ function validateTeamPatchInput(input, cfg) {
10827
10837
  return { ok: true, value: result };
10828
10838
  }
10829
10839
 
10840
+ // src/teamBudgetVisibility.ts
10841
+ function teamBudgetStatusFor(spend, budget) {
10842
+ if (budget > 0 && spend >= budget) return "over";
10843
+ if (budget > 0 && spend >= budget * 0.8) return "near";
10844
+ return "ok";
10845
+ }
10846
+ function readTeamBudgetVisibility(config) {
10847
+ return {
10848
+ hideTeamBudgetForMembers: config.getOptionalBoolean(
10849
+ "litellm.display.hideTeamBudgetForMembers"
10850
+ ) ?? false,
10851
+ hideTeamBudgetForManagers: config.getOptionalBoolean(
10852
+ "litellm.display.hideTeamBudgetForManagers"
10853
+ ) ?? false
10854
+ };
10855
+ }
10856
+ function redactTeamBudget(team) {
10857
+ const budget = team.max_budget ?? 0;
10858
+ if (budget <= 0) return team;
10859
+ const spend = team.spend ?? 0;
10860
+ const pct = Math.min(100, Math.max(0, spend / budget * 100));
10861
+ const { max_budget: _dropped, ...rest } = team;
10862
+ return {
10863
+ ...rest,
10864
+ spend: 0,
10865
+ budget_pct: pct,
10866
+ budget_status: teamBudgetStatusFor(spend, budget),
10867
+ budget_hidden: true
10868
+ };
10869
+ }
10870
+ function redactTeamUsage(usage) {
10871
+ const zeroSpend = (p) => ({
10872
+ ...p,
10873
+ spend: 0
10874
+ });
10875
+ const zeroTotalSpend = (p) => ({
10876
+ ...p,
10877
+ total_spend: 0
10878
+ });
10879
+ return {
10880
+ ...usage,
10881
+ total_spend: 0,
10882
+ usage_by_model: Object.fromEntries(
10883
+ Object.entries(usage.usage_by_model ?? {}).map(([k, v]) => [
10884
+ k,
10885
+ zeroTotalSpend(v)
10886
+ ])
10887
+ ),
10888
+ usage_by_key: Object.fromEntries(
10889
+ Object.entries(usage.usage_by_key ?? {}).map(([k, v]) => [
10890
+ k,
10891
+ zeroTotalSpend(v)
10892
+ ])
10893
+ ),
10894
+ daily_usage: (usage.daily_usage ?? []).map(zeroSpend),
10895
+ daily_by_model: (usage.daily_by_model ?? []).map(zeroSpend)
10896
+ };
10897
+ }
10898
+
10830
10899
  // src/router.ts
10831
10900
  var teamCreateInFlight = /* @__PURE__ */ new Map();
10832
10901
  var TEAM_FETCH_RETRY_DELAYS_MS = [100, 300, 900];
@@ -10863,6 +10932,7 @@ async function createRouter(options) {
10863
10932
  const teamMgmtEnabled = isTeamManagementEnabled(config);
10864
10933
  const objectPermsEnabled = isObjectPermissionsEnabled(config);
10865
10934
  const teamAdminCfg = readTeamAdminConfig(config);
10935
+ const teamBudgetVisibility = readTeamBudgetVisibility(config);
10866
10936
  const catalogClient = options.catalogClient ?? new import_catalog_client.CatalogClient({ discoveryApi: discovery });
10867
10937
  if (provisioningEnabled) {
10868
10938
  logger.info(
@@ -10874,6 +10944,11 @@ async function createRouter(options) {
10874
10944
  "litellm.teamAdmin is configured but team management is disabled \u2014 set permission.enabled: true and litellm.teamAdmin.group to enable it."
10875
10945
  );
10876
10946
  }
10947
+ if (teamBudgetVisibility.hideTeamBudgetForMembers || teamBudgetVisibility.hideTeamBudgetForManagers) {
10948
+ logger.info(
10949
+ `LiteLLM team budget hiding enabled \u2014 members: ${teamBudgetVisibility.hideTeamBudgetForMembers}, managers: ${teamBudgetVisibility.hideTeamBudgetForManagers}`
10950
+ );
10951
+ }
10877
10952
  const router = (0, import_express.Router)();
10878
10953
  router.use(import_express.default.json());
10879
10954
  router.get("/health", (_req, res) => {
@@ -10888,6 +10963,10 @@ async function createRouter(options) {
10888
10963
  maxBudgetCeiling: teamAdminCfg.maxBudgetCeiling,
10889
10964
  allowUnlimitedBudget: teamAdminCfg.allowUnlimitedBudget,
10890
10965
  objectPermissionsEnabled: objectPermsEnabled
10966
+ },
10967
+ display: {
10968
+ hideTeamBudgetForMembers: teamBudgetVisibility.hideTeamBudgetForMembers,
10969
+ hideTeamBudgetForManagers: teamBudgetVisibility.hideTeamBudgetForManagers
10891
10970
  }
10892
10971
  });
10893
10972
  });
@@ -11287,7 +11366,10 @@ async function createRouter(options) {
11287
11366
  })
11288
11367
  )
11289
11368
  );
11290
- res.json(teams.filter(Boolean));
11369
+ const list = teams.filter(Boolean);
11370
+ res.json(
11371
+ teamBudgetVisibility.hideTeamBudgetForMembers ? list.map(redactTeamBudget) : list
11372
+ );
11291
11373
  } catch (error) {
11292
11374
  if (error instanceof ProvisioningError) {
11293
11375
  res.status(error.status).json(error.body);
@@ -11386,7 +11468,9 @@ async function createRouter(options) {
11386
11468
  teamCreateInFlight.set(key, createPromise);
11387
11469
  try {
11388
11470
  const result = await createPromise;
11389
- res.json(result);
11471
+ res.json(
11472
+ teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(result) : result
11473
+ );
11390
11474
  } catch (err) {
11391
11475
  sendTeamError(err, res);
11392
11476
  } finally {
@@ -11430,7 +11514,9 @@ async function createRouter(options) {
11430
11514
  teamId,
11431
11515
  owningGroup
11432
11516
  });
11433
- res.json(r);
11517
+ res.json(
11518
+ teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(r) : r
11519
+ );
11434
11520
  } catch (err) {
11435
11521
  sendTeamError(err, res);
11436
11522
  }
@@ -11488,7 +11574,9 @@ async function createRouter(options) {
11488
11574
  const owned = all.filter(
11489
11575
  (t) => typeof t.metadata?.owning_group === "string" && t.metadata.owning_group === teamAdminCfg.group
11490
11576
  );
11491
- res.json(owned);
11577
+ res.json(
11578
+ teamBudgetVisibility.hideTeamBudgetForManagers ? owned.map(redactTeamBudget) : owned
11579
+ );
11492
11580
  } catch (err) {
11493
11581
  sendTeamError(err, res);
11494
11582
  }
@@ -11630,7 +11718,9 @@ async function createRouter(options) {
11630
11718
  owningGroup
11631
11719
  });
11632
11720
  const updated = await withTeamFetchRetry(() => client.getTeamInfo(teamId));
11633
- res.json(updated);
11721
+ res.json(
11722
+ teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(updated) : updated
11723
+ );
11634
11724
  } catch (err) {
11635
11725
  sendTeamError(err, res);
11636
11726
  }
@@ -11661,7 +11751,9 @@ async function createRouter(options) {
11661
11751
  owningGroup
11662
11752
  });
11663
11753
  const updated = await withTeamFetchRetry(() => client.getTeamInfo(teamId));
11664
- res.json(updated);
11754
+ res.json(
11755
+ teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(updated) : updated
11756
+ );
11665
11757
  } catch (err) {
11666
11758
  sendTeamError(err, res);
11667
11759
  }
@@ -11740,7 +11832,9 @@ async function createRouter(options) {
11740
11832
  owningGroup,
11741
11833
  vector_stores: requested
11742
11834
  });
11743
- res.json(updated);
11835
+ res.json(
11836
+ teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(updated) : updated
11837
+ );
11744
11838
  } catch (err) {
11745
11839
  sendTeamError(err, res);
11746
11840
  }
@@ -11821,7 +11915,9 @@ async function createRouter(options) {
11821
11915
  before,
11822
11916
  after: requested
11823
11917
  });
11824
- res.json(updated);
11918
+ res.json(
11919
+ teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(updated) : updated
11920
+ );
11825
11921
  } catch (err) {
11826
11922
  sendTeamError(err, res);
11827
11923
  }
@@ -11840,7 +11936,24 @@ async function createRouter(options) {
11840
11936
  start_date,
11841
11937
  end_date
11842
11938
  );
11843
- res.json(usage);
11939
+ let hide = teamBudgetVisibility.hideTeamBudgetForMembers;
11940
+ if (teamAdminCfg.group) {
11941
+ try {
11942
+ const tokenEntityRef = await resolveUserId(req, auth);
11943
+ const isManager = !!tokenEntityRef && await isUserMemberOfGroup(
11944
+ tokenEntityRef,
11945
+ teamAdminCfg.group,
11946
+ catalogClient,
11947
+ auth,
11948
+ logger
11949
+ );
11950
+ hide = isManager ? teamBudgetVisibility.hideTeamBudgetForManagers : teamBudgetVisibility.hideTeamBudgetForMembers;
11951
+ } catch (err) {
11952
+ logger.warn(`Team-manager check failed, hiding team spend: ${err.message}`);
11953
+ hide = teamBudgetVisibility.hideTeamBudgetForMembers;
11954
+ }
11955
+ }
11956
+ res.json(hide ? redactTeamUsage(usage) : usage);
11844
11957
  } catch (error) {
11845
11958
  logger.error("Failed to fetch team usage", error);
11846
11959
  res.status(500).json({ error: error.message });
@@ -12042,10 +12155,14 @@ var litellmPlugin = (0, import_backend_plugin_api.createBackendPlugin)({
12042
12155
  readProvisioningDefaults,
12043
12156
  readRoleConfigs,
12044
12157
  readTeamAdminConfig,
12158
+ readTeamBudgetVisibility,
12159
+ redactTeamBudget,
12160
+ redactTeamUsage,
12045
12161
  resolveBridgeUserId,
12046
12162
  resolveUserId,
12047
12163
  resolveUserProfile,
12048
12164
  resolveUserRole,
12165
+ teamBudgetStatusFor,
12049
12166
  toLiteLLMUserId,
12050
12167
  validateTeamPatchInput,
12051
12168
  validateTeamWriteInput