@acarmisc/backstage-plugin-litellm-backend 0.27.0 → 0.29.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
@@ -450,6 +450,17 @@ export interface Config {
450
450
  * @default false
451
451
  */
452
452
  hideTeamBudgetForManagers?: boolean;
453
+
454
+ /**
455
+ * Key-alias prefixes that identify system / service keys (e.g.
456
+ * ["svc-", "system-"]). The Keys table hides keys whose alias starts
457
+ * with one of these prefixes behind a "Hide service keys" toggle that
458
+ * is on by default; users can switch it off to see every key.
459
+ * Matching is case-insensitive. Display-only: the keys are still
460
+ * returned by the API and nothing is enforced server-side.
461
+ * @default []
462
+ */
463
+ serviceKeyPrefixes?: string[];
453
464
  };
454
465
 
455
466
  /**
@@ -463,6 +474,8 @@ export interface Config {
463
474
  * reaches:
464
475
  * - managers of the team (members of its metadata.owning_group, with
465
476
  * team management enabled);
477
+ * - team leads (members of the catalog group `<team_alias><leadGroupSuffix>`,
478
+ * when leadGroupSuffix is set);
466
479
  * - members of the team holding one of `viewerRoles` in it;
467
480
  * - members of the team granted `litellm.team.usage.read` (only
468
481
  * consulted when permission.enabled is true).
@@ -480,6 +493,13 @@ export interface Config {
480
493
  * @default []
481
494
  */
482
495
  viewerRoles?: string[];
496
+
497
+ /**
498
+ * Suffix of a catalog group named `<team_alias><suffix>` whose members
499
+ * are team leads (e.g. "-leads" -> group:default/abstrid-leads). For
500
+ * teams synced without metadata.owning_group. Unset: no lead access.
501
+ */
502
+ leadGroupSuffix?: string;
483
503
  };
484
504
  };
485
505
 
package/dist/index.cjs.js CHANGED
@@ -9449,7 +9449,8 @@ function readMemberBreakdownConfig(config) {
9449
9449
  return {
9450
9450
  enabled: config.getOptionalBoolean("litellm.teamUsage.memberBreakdown.enabled") ?? false,
9451
9451
  viewerRoles: config.getOptionalStringArray("litellm.teamUsage.memberBreakdown.viewerRoles") ?? [],
9452
- permissionEnabled: config.getOptionalBoolean("permission.enabled") ?? false
9452
+ permissionEnabled: config.getOptionalBoolean("permission.enabled") ?? false,
9453
+ leadGroupSuffix: config.getOptionalString("litellm.teamUsage.memberBreakdown.leadGroupSuffix") || void 0
9453
9454
  };
9454
9455
  }
9455
9456
  function memberEmails(team, keys) {
@@ -9785,7 +9786,8 @@ var openApiSpec = {
9785
9786
  } },
9786
9787
  display: { type: "object", properties: {
9787
9788
  hideTeamBudgetForMembers: { type: "boolean" },
9788
- hideTeamBudgetForManagers: { type: "boolean" }
9789
+ hideTeamBudgetForManagers: { type: "boolean" },
9790
+ serviceKeyPrefixes: { type: "array", items: { type: "string" } }
9789
9791
  } }
9790
9792
  } } } }
9791
9793
  }
@@ -10053,7 +10055,8 @@ function registerConfigRoutes(router, ctx) {
10053
10055
  },
10054
10056
  display: {
10055
10057
  hideTeamBudgetForMembers: ctx.teamBudgetVisibility.hideTeamBudgetForMembers,
10056
- hideTeamBudgetForManagers: ctx.teamBudgetVisibility.hideTeamBudgetForManagers
10058
+ hideTeamBudgetForManagers: ctx.teamBudgetVisibility.hideTeamBudgetForManagers,
10059
+ serviceKeyPrefixes: ctx.serviceKeyPrefixes
10057
10060
  }
10058
10061
  });
10059
10062
  });
@@ -11521,6 +11524,22 @@ function registerTeamUsageRoutes(router, ctx) {
11521
11524
  teamBudgetVisibility
11522
11525
  } = ctx;
11523
11526
  const requireUser = createRequireUser(ctx);
11527
+ async function isTeamManager(team, tokenEntityRef) {
11528
+ const owningGroup = typeof team.metadata?.owning_group === "string" ? team.metadata.owning_group : void 0;
11529
+ if (ctx.teamMgmtEnabled && owningGroup && await isUserMemberOfGroup(tokenEntityRef, owningGroup, catalogClient, auth, logger)) {
11530
+ return true;
11531
+ }
11532
+ const suffix = ctx.memberBreakdownCfg.leadGroupSuffix;
11533
+ if (!suffix || !team.team_alias) return false;
11534
+ const slug = team.team_alias.toLowerCase().replace(/[^a-z0-9\-_.]/g, "-");
11535
+ return isUserMemberOfGroup(
11536
+ tokenEntityRef,
11537
+ `group:default/${slug}${suffix}`,
11538
+ catalogClient,
11539
+ auth,
11540
+ logger
11541
+ );
11542
+ }
11524
11543
  router.get("/teams/:teamId/usage", requireUser, async (req, res) => {
11525
11544
  try {
11526
11545
  const { teamId } = req.params;
@@ -11533,20 +11552,11 @@ function registerTeamUsageRoutes(router, ctx) {
11533
11552
  const userInfo = await getProvisionedUser(ctx, res);
11534
11553
  const isMember = userInfo?.teams?.includes(teamId) ?? false;
11535
11554
  let isManager = false;
11536
- const managerCheckNeeded = ctx.teamMgmtEnabled && (!isMember || teamBudgetVisibility.hideTeamBudgetForMembers !== teamBudgetVisibility.hideTeamBudgetForManagers);
11555
+ const managerCheckNeeded = (ctx.teamMgmtEnabled || !!ctx.memberBreakdownCfg.leadGroupSuffix) && (!isMember || teamBudgetVisibility.hideTeamBudgetForMembers !== teamBudgetVisibility.hideTeamBudgetForManagers);
11537
11556
  if (managerCheckNeeded) {
11538
11557
  try {
11539
11558
  const team = await withTeamFetchRetry(() => client.getTeamInfo(teamId));
11540
- const owningGroup = typeof team.metadata?.owning_group === "string" ? team.metadata.owning_group : void 0;
11541
- if (owningGroup) {
11542
- isManager = await isUserMemberOfGroup(
11543
- tokenEntityRef,
11544
- owningGroup,
11545
- catalogClient,
11546
- auth,
11547
- logger
11548
- );
11549
- }
11559
+ isManager = await isTeamManager(team, tokenEntityRef);
11550
11560
  } catch (err) {
11551
11561
  const message2 = err instanceof Error ? err.message : String(err);
11552
11562
  logger.warn(`Team-manager check failed: ${message2}`);
@@ -11600,8 +11610,7 @@ function registerTeamUsageRoutes(router, ctx) {
11600
11610
  }
11601
11611
  throw err;
11602
11612
  }
11603
- const owningGroup = typeof team.metadata?.owning_group === "string" ? team.metadata.owning_group : void 0;
11604
- const isManager = ctx.teamMgmtEnabled && !!owningGroup && await isUserMemberOfGroup(tokenEntityRef, owningGroup, catalogClient, auth, logger);
11613
+ const isManager = await isTeamManager(team, tokenEntityRef);
11605
11614
  if (!isMember && !isManager) {
11606
11615
  res.status(404).json({ error: "Team not found" });
11607
11616
  return null;
@@ -13663,6 +13672,7 @@ async function createRouter(options) {
13663
13672
  const userIdDomain = config.getOptionalString("litellm.userIdDomain");
13664
13673
  const publicBaseUrl = config.getOptionalString("litellm.publicBaseUrl") ?? null;
13665
13674
  const supportContact = config.getOptionalString("litellm.supportContact");
13675
+ const serviceKeyPrefixes = (config.getOptionalStringArray("litellm.display.serviceKeyPrefixes") ?? []).map((p) => p.trim()).filter((p) => p.length > 0);
13666
13676
  const baseClient = options.client ?? new LiteLLMClient({ baseUrl, masterKey });
13667
13677
  const userInfoCacheTtlSeconds = config.getOptionalNumber("litellm.cache.userInfoTtlSeconds") ?? 10;
13668
13678
  const client = userInfoCacheTtlSeconds > 0 ? withUserInfoCache(baseClient, { ttlMs: userInfoCacheTtlSeconds * 1e3 }) : baseClient;
@@ -13721,6 +13731,7 @@ async function createRouter(options) {
13721
13731
  baseUrl,
13722
13732
  publicBaseUrl,
13723
13733
  supportContact,
13734
+ serviceKeyPrefixes,
13724
13735
  userIdDomain,
13725
13736
  provisioningEnabled,
13726
13737
  provisioningDefaults,