@acarmisc/backstage-plugin-litellm-backend 0.11.1 → 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/client.js CHANGED
@@ -98,6 +98,7 @@ class LiteLLMClient {
98
98
  teams: teamIds,
99
99
  models: inner.models ?? raw?.models,
100
100
  max_budget: inner.max_budget ?? raw?.max_budget,
101
+ budget_duration: inner.budget_duration ?? raw?.budget_duration,
101
102
  spend: inner.spend ?? raw?.spend,
102
103
  current_spend: inner.current_spend ?? raw?.current_spend,
103
104
  soft_limit: inner.soft_limit ?? raw?.soft_limit,
@@ -162,6 +163,7 @@ class LiteLLMClient {
162
163
  expires_at: k.expires ?? undefined,
163
164
  spend: k.spend ?? 0,
164
165
  max_budget: k.max_budget ?? undefined,
166
+ budget_duration: k.budget_duration ?? undefined,
165
167
  tpm_limit: k.tpm_limit ?? undefined,
166
168
  rpm_limit: k.rpm_limit ?? undefined,
167
169
  models: k.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
@@ -8099,6 +8103,7 @@ var LiteLLMClient = class {
8099
8103
  teams: teamIds,
8100
8104
  models: inner.models ?? raw?.models,
8101
8105
  max_budget: inner.max_budget ?? raw?.max_budget,
8106
+ budget_duration: inner.budget_duration ?? raw?.budget_duration,
8102
8107
  spend: inner.spend ?? raw?.spend,
8103
8108
  current_spend: inner.current_spend ?? raw?.current_spend,
8104
8109
  soft_limit: inner.soft_limit ?? raw?.soft_limit,
@@ -8161,6 +8166,7 @@ var LiteLLMClient = class {
8161
8166
  expires_at: k.expires ?? void 0,
8162
8167
  spend: k.spend ?? 0,
8163
8168
  max_budget: k.max_budget ?? void 0,
8169
+ budget_duration: k.budget_duration ?? void 0,
8164
8170
  tpm_limit: k.tpm_limit ?? void 0,
8165
8171
  rpm_limit: k.rpm_limit ?? void 0,
8166
8172
  models: k.models ?? [],
@@ -8627,6 +8633,10 @@ var openApiSpec = {
8627
8633
  keyGeneration: { type: "object", properties: {
8628
8634
  allowUnlimitedBudget: { type: "boolean" },
8629
8635
  teamRequired: { type: "boolean" }
8636
+ } },
8637
+ display: { type: "object", properties: {
8638
+ hideTeamBudgetForMembers: { type: "boolean" },
8639
+ hideTeamBudgetForManagers: { type: "boolean" }
8630
8640
  } }
8631
8641
  } } } }
8632
8642
  }
@@ -8731,6 +8741,7 @@ var openApiSpec = {
8731
8741
  get: {
8732
8742
  tags: ["Teams"],
8733
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.",
8734
8745
  responses: { "200": { description: "Array of teams" } }
8735
8746
  }
8736
8747
  },
@@ -8738,6 +8749,7 @@ var openApiSpec = {
8738
8749
  get: {
8739
8750
  tags: ["Teams"],
8740
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).",
8741
8753
  parameters: [
8742
8754
  { name: "teamId", in: "path", required: true, schema: { type: "string" } },
8743
8755
  { name: "start_date", in: "query", required: true, schema: { type: "string" } },
@@ -10825,6 +10837,65 @@ function validateTeamPatchInput(input, cfg) {
10825
10837
  return { ok: true, value: result };
10826
10838
  }
10827
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
+
10828
10899
  // src/router.ts
10829
10900
  var teamCreateInFlight = /* @__PURE__ */ new Map();
10830
10901
  var TEAM_FETCH_RETRY_DELAYS_MS = [100, 300, 900];
@@ -10861,6 +10932,7 @@ async function createRouter(options) {
10861
10932
  const teamMgmtEnabled = isTeamManagementEnabled(config);
10862
10933
  const objectPermsEnabled = isObjectPermissionsEnabled(config);
10863
10934
  const teamAdminCfg = readTeamAdminConfig(config);
10935
+ const teamBudgetVisibility = readTeamBudgetVisibility(config);
10864
10936
  const catalogClient = options.catalogClient ?? new import_catalog_client.CatalogClient({ discoveryApi: discovery });
10865
10937
  if (provisioningEnabled) {
10866
10938
  logger.info(
@@ -10872,6 +10944,11 @@ async function createRouter(options) {
10872
10944
  "litellm.teamAdmin is configured but team management is disabled \u2014 set permission.enabled: true and litellm.teamAdmin.group to enable it."
10873
10945
  );
10874
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
+ }
10875
10952
  const router = (0, import_express.Router)();
10876
10953
  router.use(import_express.default.json());
10877
10954
  router.get("/health", (_req, res) => {
@@ -10886,6 +10963,10 @@ async function createRouter(options) {
10886
10963
  maxBudgetCeiling: teamAdminCfg.maxBudgetCeiling,
10887
10964
  allowUnlimitedBudget: teamAdminCfg.allowUnlimitedBudget,
10888
10965
  objectPermissionsEnabled: objectPermsEnabled
10966
+ },
10967
+ display: {
10968
+ hideTeamBudgetForMembers: teamBudgetVisibility.hideTeamBudgetForMembers,
10969
+ hideTeamBudgetForManagers: teamBudgetVisibility.hideTeamBudgetForManagers
10889
10970
  }
10890
10971
  });
10891
10972
  });
@@ -11285,7 +11366,10 @@ async function createRouter(options) {
11285
11366
  })
11286
11367
  )
11287
11368
  );
11288
- res.json(teams.filter(Boolean));
11369
+ const list = teams.filter(Boolean);
11370
+ res.json(
11371
+ teamBudgetVisibility.hideTeamBudgetForMembers ? list.map(redactTeamBudget) : list
11372
+ );
11289
11373
  } catch (error) {
11290
11374
  if (error instanceof ProvisioningError) {
11291
11375
  res.status(error.status).json(error.body);
@@ -11384,7 +11468,9 @@ async function createRouter(options) {
11384
11468
  teamCreateInFlight.set(key, createPromise);
11385
11469
  try {
11386
11470
  const result = await createPromise;
11387
- res.json(result);
11471
+ res.json(
11472
+ teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(result) : result
11473
+ );
11388
11474
  } catch (err) {
11389
11475
  sendTeamError(err, res);
11390
11476
  } finally {
@@ -11428,7 +11514,9 @@ async function createRouter(options) {
11428
11514
  teamId,
11429
11515
  owningGroup
11430
11516
  });
11431
- res.json(r);
11517
+ res.json(
11518
+ teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(r) : r
11519
+ );
11432
11520
  } catch (err) {
11433
11521
  sendTeamError(err, res);
11434
11522
  }
@@ -11486,7 +11574,9 @@ async function createRouter(options) {
11486
11574
  const owned = all.filter(
11487
11575
  (t) => typeof t.metadata?.owning_group === "string" && t.metadata.owning_group === teamAdminCfg.group
11488
11576
  );
11489
- res.json(owned);
11577
+ res.json(
11578
+ teamBudgetVisibility.hideTeamBudgetForManagers ? owned.map(redactTeamBudget) : owned
11579
+ );
11490
11580
  } catch (err) {
11491
11581
  sendTeamError(err, res);
11492
11582
  }
@@ -11628,7 +11718,9 @@ async function createRouter(options) {
11628
11718
  owningGroup
11629
11719
  });
11630
11720
  const updated = await withTeamFetchRetry(() => client.getTeamInfo(teamId));
11631
- res.json(updated);
11721
+ res.json(
11722
+ teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(updated) : updated
11723
+ );
11632
11724
  } catch (err) {
11633
11725
  sendTeamError(err, res);
11634
11726
  }
@@ -11659,7 +11751,9 @@ async function createRouter(options) {
11659
11751
  owningGroup
11660
11752
  });
11661
11753
  const updated = await withTeamFetchRetry(() => client.getTeamInfo(teamId));
11662
- res.json(updated);
11754
+ res.json(
11755
+ teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(updated) : updated
11756
+ );
11663
11757
  } catch (err) {
11664
11758
  sendTeamError(err, res);
11665
11759
  }
@@ -11738,7 +11832,9 @@ async function createRouter(options) {
11738
11832
  owningGroup,
11739
11833
  vector_stores: requested
11740
11834
  });
11741
- res.json(updated);
11835
+ res.json(
11836
+ teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(updated) : updated
11837
+ );
11742
11838
  } catch (err) {
11743
11839
  sendTeamError(err, res);
11744
11840
  }
@@ -11819,7 +11915,9 @@ async function createRouter(options) {
11819
11915
  before,
11820
11916
  after: requested
11821
11917
  });
11822
- res.json(updated);
11918
+ res.json(
11919
+ teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(updated) : updated
11920
+ );
11823
11921
  } catch (err) {
11824
11922
  sendTeamError(err, res);
11825
11923
  }
@@ -11838,7 +11936,24 @@ async function createRouter(options) {
11838
11936
  start_date,
11839
11937
  end_date
11840
11938
  );
11841
- 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);
11842
11957
  } catch (error) {
11843
11958
  logger.error("Failed to fetch team usage", error);
11844
11959
  res.status(500).json({ error: error.message });
@@ -12040,10 +12155,14 @@ var litellmPlugin = (0, import_backend_plugin_api.createBackendPlugin)({
12040
12155
  readProvisioningDefaults,
12041
12156
  readRoleConfigs,
12042
12157
  readTeamAdminConfig,
12158
+ readTeamBudgetVisibility,
12159
+ redactTeamBudget,
12160
+ redactTeamUsage,
12043
12161
  resolveBridgeUserId,
12044
12162
  resolveUserId,
12045
12163
  resolveUserProfile,
12046
12164
  resolveUserRole,
12165
+ teamBudgetStatusFor,
12047
12166
  toLiteLLMUserId,
12048
12167
  validateTeamPatchInput,
12049
12168
  validateTeamWriteInput