@acarmisc/backstage-plugin-litellm-backend 0.12.0 → 0.14.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.d.ts CHANGED
@@ -1,4 +1,10 @@
1
- import { LiteLLMConfig, UserInfo, VirtualKey, ModelInfo, UsageMetrics, TeamInfo, GenerateKeyRequest, GenerateKeyResponse, UpdateKeyRequest, DeleteKeyRequest, CreateUserRequest, CreateUserResponse, CreateTeamRequest, CreateTeamResponse, UpdateTeamRequest, AuditLogsParams, PaginatedAuditLogs } from './types';
1
+ import { LiteLLMConfig, UserInfo, VirtualKey, ModelInfo, UsageMetrics, TeamInfo, GenerateKeyRequest, GenerateKeyResponse, UpdateKeyRequest, DeleteKeyRequest, CreateUserRequest, CreateUserResponse, CreateTeamRequest, CreateTeamResponse, UpdateTeamRequest, AuditLogsParams, PaginatedAuditLogs, SpendLogEntry, SpendLogsParams } from './types';
2
+ /**
3
+ * LiteLLM versions differ on `request_tags`: an array of `k:v` strings in
4
+ * current builds, but historically an object map. Normalise both to a flat
5
+ * string array so callers can `includes('session:...')` uniformly.
6
+ */
7
+ export declare function normalizeRequestTags(tags: SpendLogEntry['request_tags']): string[];
2
8
  /**
3
9
  * Typed error for failed upstream LiteLLM responses. Preserves the HTTP
4
10
  * status and the structured `param` (e.g. `key_alias`) from the upstream
@@ -149,6 +155,17 @@ export declare class LiteLLMClient {
149
155
  name?: string;
150
156
  url?: string;
151
157
  }>>;
158
+ /**
159
+ * Reads per-request spend logs from LiteLLM (`GET /spend/logs`), filtered
160
+ * by date range and optionally by key/user/team. Rows are normalised so
161
+ * `request_tags` is always a `string[]` (LiteLLM returns either an array
162
+ * or an object depending on version) — callers group by tag to attribute
163
+ * spend to a conversation or caller.
164
+ *
165
+ * This is the admin/DB-backed endpoint: it can return a large number of
166
+ * rows, so callers should keep the date window tight and cap `page_size`.
167
+ */
168
+ getSpendLogs(params: SpendLogsParams): Promise<SpendLogEntry[]>;
152
169
  private emptyUsage;
153
170
  /**
154
171
  * Transforms LiteLLM's SpendAnalyticsPaginatedResponse into the flatter
package/dist/client.js CHANGED
@@ -1,7 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LiteLLMClient = exports.LiteLLMUpstreamError = void 0;
4
+ exports.normalizeRequestTags = normalizeRequestTags;
4
5
  const DEFAULT_TIMEOUT = 30000;
6
+ /**
7
+ * LiteLLM versions differ on `request_tags`: an array of `k:v` strings in
8
+ * current builds, but historically an object map. Normalise both to a flat
9
+ * string array so callers can `includes('session:...')` uniformly.
10
+ */
11
+ function normalizeRequestTags(tags) {
12
+ if (!tags)
13
+ return [];
14
+ if (Array.isArray(tags))
15
+ return tags.filter((t) => typeof t === 'string');
16
+ return Object.entries(tags).map(([k, v]) => `${k}:${v}`);
17
+ }
5
18
  /**
6
19
  * Typed error for failed upstream LiteLLM responses. Preserves the HTTP
7
20
  * status and the structured `param` (e.g. `key_alias`) from the upstream
@@ -451,6 +464,47 @@ class LiteLLMClient {
451
464
  }))
452
465
  .filter(r => r.id);
453
466
  }
467
+ /**
468
+ * Reads per-request spend logs from LiteLLM (`GET /spend/logs`), filtered
469
+ * by date range and optionally by key/user/team. Rows are normalised so
470
+ * `request_tags` is always a `string[]` (LiteLLM returns either an array
471
+ * or an object depending on version) — callers group by tag to attribute
472
+ * spend to a conversation or caller.
473
+ *
474
+ * This is the admin/DB-backed endpoint: it can return a large number of
475
+ * rows, so callers should keep the date window tight and cap `page_size`.
476
+ */
477
+ async getSpendLogs(params) {
478
+ const query = new URLSearchParams({
479
+ start_date: params.start_date,
480
+ end_date: params.end_date,
481
+ });
482
+ if (params.api_key)
483
+ query.append('api_key', params.api_key);
484
+ if (params.user_id)
485
+ query.append('user_id', params.user_id);
486
+ if (params.team_id)
487
+ query.append('team_id', params.team_id);
488
+ if (params.page_size)
489
+ query.append('page_size', String(params.page_size));
490
+ const raw = await this.request(`/spend/logs?${query.toString()}`);
491
+ const rows = Array.isArray(raw) ? raw : raw?.data ?? [];
492
+ return rows.map(row => ({
493
+ request_id: row?.request_id,
494
+ startTime: row?.startTime ?? row?.start_time,
495
+ endTime: row?.endTime ?? row?.end_time,
496
+ spend: row?.spend ?? 0,
497
+ total_tokens: row?.total_tokens ?? 0,
498
+ prompt_tokens: row?.prompt_tokens ?? 0,
499
+ completion_tokens: row?.completion_tokens ?? 0,
500
+ model: row?.model,
501
+ api_key: row?.api_key,
502
+ user: row?.user,
503
+ team_id: row?.team_id,
504
+ request_tags: normalizeRequestTags(row?.request_tags),
505
+ metadata: row?.metadata,
506
+ }));
507
+ }
454
508
  emptyUsage() {
455
509
  return {
456
510
  total_spend: 0,
package/dist/index.cjs.js CHANGED
@@ -7997,15 +7997,20 @@ __export(index_exports, {
7997
7997
  litellmTeamMcpManagePermission: () => litellmTeamMcpManagePermission,
7998
7998
  litellmTeamMembersManagePermission: () => litellmTeamMembersManagePermission,
7999
7999
  newDefaultVerifier: () => newDefaultVerifier,
8000
+ normalizeRequestTags: () => normalizeRequestTags,
8000
8001
  provisionUser: () => provisionUser,
8001
8002
  readBridgeConfig: () => readBridgeConfig,
8002
8003
  readProvisioningDefaults: () => readProvisioningDefaults,
8003
8004
  readRoleConfigs: () => readRoleConfigs,
8004
8005
  readTeamAdminConfig: () => readTeamAdminConfig,
8006
+ readTeamBudgetVisibility: () => readTeamBudgetVisibility,
8007
+ redactTeamBudget: () => redactTeamBudget,
8008
+ redactTeamUsage: () => redactTeamUsage,
8005
8009
  resolveBridgeUserId: () => resolveBridgeUserId,
8006
8010
  resolveUserId: () => resolveUserId,
8007
8011
  resolveUserProfile: () => resolveUserProfile,
8008
8012
  resolveUserRole: () => resolveUserRole,
8013
+ teamBudgetStatusFor: () => teamBudgetStatusFor,
8009
8014
  toLiteLLMUserId: () => toLiteLLMUserId,
8010
8015
  validateTeamPatchInput: () => validateTeamPatchInput,
8011
8016
  validateTeamWriteInput: () => validateTeamWriteInput
@@ -8022,6 +8027,11 @@ var import_catalog_client = require("@backstage/catalog-client");
8022
8027
 
8023
8028
  // src/client.ts
8024
8029
  var DEFAULT_TIMEOUT = 3e4;
8030
+ function normalizeRequestTags(tags) {
8031
+ if (!tags) return [];
8032
+ if (Array.isArray(tags)) return tags.filter((t) => typeof t === "string");
8033
+ return Object.entries(tags).map(([k, v]) => `${k}:${v}`);
8034
+ }
8025
8035
  var LiteLLMUpstreamError = class extends Error {
8026
8036
  constructor(status, statusText, body) {
8027
8037
  let message2 = `LiteLLM API error: ${status} ${statusText} - ${body}`;
@@ -8425,6 +8435,43 @@ var LiteLLMClient = class {
8425
8435
  url: r?.url ?? void 0
8426
8436
  })).filter((r) => r.id);
8427
8437
  }
8438
+ /**
8439
+ * Reads per-request spend logs from LiteLLM (`GET /spend/logs`), filtered
8440
+ * by date range and optionally by key/user/team. Rows are normalised so
8441
+ * `request_tags` is always a `string[]` (LiteLLM returns either an array
8442
+ * or an object depending on version) — callers group by tag to attribute
8443
+ * spend to a conversation or caller.
8444
+ *
8445
+ * This is the admin/DB-backed endpoint: it can return a large number of
8446
+ * rows, so callers should keep the date window tight and cap `page_size`.
8447
+ */
8448
+ async getSpendLogs(params) {
8449
+ const query = new URLSearchParams({
8450
+ start_date: params.start_date,
8451
+ end_date: params.end_date
8452
+ });
8453
+ if (params.api_key) query.append("api_key", params.api_key);
8454
+ if (params.user_id) query.append("user_id", params.user_id);
8455
+ if (params.team_id) query.append("team_id", params.team_id);
8456
+ if (params.page_size) query.append("page_size", String(params.page_size));
8457
+ const raw = await this.request(`/spend/logs?${query.toString()}`);
8458
+ const rows = Array.isArray(raw) ? raw : raw?.data ?? [];
8459
+ return rows.map((row) => ({
8460
+ request_id: row?.request_id,
8461
+ startTime: row?.startTime ?? row?.start_time,
8462
+ endTime: row?.endTime ?? row?.end_time,
8463
+ spend: row?.spend ?? 0,
8464
+ total_tokens: row?.total_tokens ?? 0,
8465
+ prompt_tokens: row?.prompt_tokens ?? 0,
8466
+ completion_tokens: row?.completion_tokens ?? 0,
8467
+ model: row?.model,
8468
+ api_key: row?.api_key,
8469
+ user: row?.user,
8470
+ team_id: row?.team_id,
8471
+ request_tags: normalizeRequestTags(row?.request_tags),
8472
+ metadata: row?.metadata
8473
+ }));
8474
+ }
8428
8475
  emptyUsage() {
8429
8476
  return {
8430
8477
  total_spend: 0,
@@ -8629,6 +8676,10 @@ var openApiSpec = {
8629
8676
  keyGeneration: { type: "object", properties: {
8630
8677
  allowUnlimitedBudget: { type: "boolean" },
8631
8678
  teamRequired: { type: "boolean" }
8679
+ } },
8680
+ display: { type: "object", properties: {
8681
+ hideTeamBudgetForMembers: { type: "boolean" },
8682
+ hideTeamBudgetForManagers: { type: "boolean" }
8632
8683
  } }
8633
8684
  } } } }
8634
8685
  }
@@ -8733,6 +8784,7 @@ var openApiSpec = {
8733
8784
  get: {
8734
8785
  tags: ["Teams"],
8735
8786
  summary: "List teams the current user belongs to",
8787
+ 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
8788
  responses: { "200": { description: "Array of teams" } }
8737
8789
  }
8738
8790
  },
@@ -8740,6 +8792,7 @@ var openApiSpec = {
8740
8792
  get: {
8741
8793
  tags: ["Teams"],
8742
8794
  summary: "Usage metrics for a team",
8795
+ description: "Spend fields are zeroed when the caller is subject to team budget hiding (member or manager flag, whichever applies to them).",
8743
8796
  parameters: [
8744
8797
  { name: "teamId", in: "path", required: true, schema: { type: "string" } },
8745
8798
  { name: "start_date", in: "query", required: true, schema: { type: "string" } },
@@ -10827,6 +10880,79 @@ function validateTeamPatchInput(input, cfg) {
10827
10880
  return { ok: true, value: result };
10828
10881
  }
10829
10882
 
10883
+ // src/teamBudgetVisibility.ts
10884
+ function teamBudgetStatusFor(spend, budget) {
10885
+ if (budget > 0 && spend >= budget) return "over";
10886
+ if (budget > 0 && spend >= budget * 0.8) return "near";
10887
+ return "ok";
10888
+ }
10889
+ function readTeamBudgetVisibility(config) {
10890
+ return {
10891
+ hideTeamBudgetForMembers: config.getOptionalBoolean(
10892
+ "litellm.display.hideTeamBudgetForMembers"
10893
+ ) ?? false,
10894
+ hideTeamBudgetForManagers: config.getOptionalBoolean(
10895
+ "litellm.display.hideTeamBudgetForManagers"
10896
+ ) ?? false
10897
+ };
10898
+ }
10899
+ function redactTeamBudget(team) {
10900
+ const budget = team.max_budget ?? 0;
10901
+ if (budget <= 0) return team;
10902
+ const spend = team.spend ?? 0;
10903
+ const pct = Math.min(100, Math.max(0, spend / budget * 100));
10904
+ const { max_budget: _dropped, ...rest } = team;
10905
+ return {
10906
+ ...rest,
10907
+ spend: 0,
10908
+ budget_pct: pct,
10909
+ budget_status: teamBudgetStatusFor(spend, budget),
10910
+ budget_hidden: true
10911
+ };
10912
+ }
10913
+ function redactTeamUsage(usage) {
10914
+ const zeroSpend = (p) => ({
10915
+ ...p,
10916
+ spend: 0
10917
+ });
10918
+ const zeroTotalSpend = (p) => ({
10919
+ ...p,
10920
+ total_spend: 0
10921
+ });
10922
+ return {
10923
+ ...usage,
10924
+ total_spend: 0,
10925
+ usage_by_model: Object.fromEntries(
10926
+ Object.entries(usage.usage_by_model ?? {}).map(([k, v]) => [
10927
+ k,
10928
+ zeroTotalSpend(v)
10929
+ ])
10930
+ ),
10931
+ usage_by_key: Object.fromEntries(
10932
+ Object.entries(usage.usage_by_key ?? {}).map(([k, v]) => [
10933
+ k,
10934
+ zeroTotalSpend(v)
10935
+ ])
10936
+ ),
10937
+ daily_usage: (usage.daily_usage ?? []).map(zeroSpend),
10938
+ daily_by_model: (usage.daily_by_model ?? []).map(zeroSpend)
10939
+ };
10940
+ }
10941
+
10942
+ // src/opencode.ts
10943
+ function readOpencodeConfig(config) {
10944
+ const enabled = config.getOptionalBoolean("litellm.opencode.enabled") ?? false;
10945
+ return {
10946
+ enabled,
10947
+ keyDuration: config.getOptionalString("litellm.opencode.keyDuration") ?? "30d",
10948
+ maxBudget: config.getOptionalNumber("litellm.opencode.maxBudget") ?? 50,
10949
+ requireTeam: config.getOptionalBoolean("litellm.opencode.requireTeam") ?? false,
10950
+ metadata: config.getOptional(
10951
+ "litellm.opencode.metadata"
10952
+ ) ?? {}
10953
+ };
10954
+ }
10955
+
10830
10956
  // src/router.ts
10831
10957
  var teamCreateInFlight = /* @__PURE__ */ new Map();
10832
10958
  var TEAM_FETCH_RETRY_DELAYS_MS = [100, 300, 900];
@@ -10848,6 +10974,15 @@ async function withTeamFetchRetry(fn, delaysMs = TEAM_FETCH_RETRY_DELAYS_MS) {
10848
10974
  }
10849
10975
  }
10850
10976
  }
10977
+ function isValidRedirectUri(uri) {
10978
+ if (!uri) return false;
10979
+ try {
10980
+ const url = new URL(uri);
10981
+ return url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1") && url.pathname === "/callback";
10982
+ } catch {
10983
+ return false;
10984
+ }
10985
+ }
10851
10986
  async function createRouter(options) {
10852
10987
  const { config, logger, auth, discovery, permissions } = options;
10853
10988
  const baseUrl = config.getString("litellm.baseUrl");
@@ -10863,6 +10998,8 @@ async function createRouter(options) {
10863
10998
  const teamMgmtEnabled = isTeamManagementEnabled(config);
10864
10999
  const objectPermsEnabled = isObjectPermissionsEnabled(config);
10865
11000
  const teamAdminCfg = readTeamAdminConfig(config);
11001
+ const teamBudgetVisibility = readTeamBudgetVisibility(config);
11002
+ const opencodeCfg = readOpencodeConfig(config);
10866
11003
  const catalogClient = options.catalogClient ?? new import_catalog_client.CatalogClient({ discoveryApi: discovery });
10867
11004
  if (provisioningEnabled) {
10868
11005
  logger.info(
@@ -10874,6 +11011,11 @@ async function createRouter(options) {
10874
11011
  "litellm.teamAdmin is configured but team management is disabled \u2014 set permission.enabled: true and litellm.teamAdmin.group to enable it."
10875
11012
  );
10876
11013
  }
11014
+ if (teamBudgetVisibility.hideTeamBudgetForMembers || teamBudgetVisibility.hideTeamBudgetForManagers) {
11015
+ logger.info(
11016
+ `LiteLLM team budget hiding enabled \u2014 members: ${teamBudgetVisibility.hideTeamBudgetForMembers}, managers: ${teamBudgetVisibility.hideTeamBudgetForManagers}`
11017
+ );
11018
+ }
10877
11019
  const router = (0, import_express.Router)();
10878
11020
  router.use(import_express.default.json());
10879
11021
  router.get("/health", (_req, res) => {
@@ -10883,11 +11025,16 @@ async function createRouter(options) {
10883
11025
  res.json({
10884
11026
  baseUrl: publicBaseUrl,
10885
11027
  keyGeneration: { allowUnlimitedBudget, teamRequired },
11028
+ opencode: { enabled: opencodeCfg.enabled },
10886
11029
  teamManagement: {
10887
11030
  enabled: teamMgmtEnabled,
10888
11031
  maxBudgetCeiling: teamAdminCfg.maxBudgetCeiling,
10889
11032
  allowUnlimitedBudget: teamAdminCfg.allowUnlimitedBudget,
10890
11033
  objectPermissionsEnabled: objectPermsEnabled
11034
+ },
11035
+ display: {
11036
+ hideTeamBudgetForMembers: teamBudgetVisibility.hideTeamBudgetForMembers,
11037
+ hideTeamBudgetForManagers: teamBudgetVisibility.hideTeamBudgetForManagers
10891
11038
  }
10892
11039
  });
10893
11040
  });
@@ -11115,6 +11262,109 @@ async function createRouter(options) {
11115
11262
  res.status(500).json({ error: error.message });
11116
11263
  }
11117
11264
  });
11265
+ if (opencodeCfg.enabled) {
11266
+ logger.info(
11267
+ `OpenCode connect endpoint enabled \u2014 duration=${opencodeCfg.keyDuration}, maxBudget=$${opencodeCfg.maxBudget}`
11268
+ );
11269
+ router.get("/opencode/connect", async (req, res) => {
11270
+ try {
11271
+ const tokenEntityRef = await resolveUserId(req, auth);
11272
+ if (!tokenEntityRef) {
11273
+ res.status(401).json({ error: "Authentication required" });
11274
+ return;
11275
+ }
11276
+ const redirectUri = req.query.redirect_uri;
11277
+ if (!isValidRedirectUri(redirectUri)) {
11278
+ res.status(400).json({
11279
+ error: "Invalid redirect_uri \u2014 expected http://localhost:<port>/callback"
11280
+ });
11281
+ return;
11282
+ }
11283
+ if (!await assertPermission(req, litellmKeyCreatePermission)) {
11284
+ sendPermissionDenied(res, litellmKeyCreatePermission);
11285
+ return;
11286
+ }
11287
+ const userId = toLiteLLMUserId(tokenEntityRef, userIdDomain);
11288
+ const userInfo = await getOrProvisionUser(
11289
+ client,
11290
+ tokenEntityRef,
11291
+ userId,
11292
+ provisioningEnabled,
11293
+ provisioningDefaults,
11294
+ roleConfigs,
11295
+ catalogClient,
11296
+ auth,
11297
+ logger
11298
+ );
11299
+ const teamId = req.query.team;
11300
+ if (teamId) {
11301
+ const userTeams = userInfo?.teams ?? [];
11302
+ if (!userTeams.includes(teamId)) {
11303
+ res.status(403).json({
11304
+ error: "Access denied: team is not one of your teams",
11305
+ team: teamId
11306
+ });
11307
+ return;
11308
+ }
11309
+ } else if (opencodeCfg.requireTeam) {
11310
+ res.status(400).json({ error: "A team is required for this connection" });
11311
+ return;
11312
+ }
11313
+ const alias = teamId ? `opencode-${userId}-${teamId}` : `opencode-${userId}`;
11314
+ const existing = await client.listKeys(userId).catch(() => []);
11315
+ const reusable = existing.find(
11316
+ (k) => k.key_alias === alias && !k.blocked && k.user_id === userId
11317
+ );
11318
+ let key;
11319
+ if (reusable?.token ?? reusable?.key) {
11320
+ key = reusable.token ?? reusable.key;
11321
+ } else {
11322
+ const profile = await resolveUserProfile(
11323
+ tokenEntityRef,
11324
+ catalogClient,
11325
+ auth,
11326
+ logger
11327
+ );
11328
+ const result = await client.generateKey({
11329
+ alias,
11330
+ duration: opencodeCfg.keyDuration,
11331
+ max_budget: opencodeCfg.maxBudget,
11332
+ team_id: teamId,
11333
+ metadata: {
11334
+ ...opencodeCfg.metadata,
11335
+ created_via: "opencode-connect",
11336
+ created_by_backstage_user: tokenEntityRef,
11337
+ ...profile.email && { created_by_email: profile.email },
11338
+ ...teamId && { opencode_team: teamId }
11339
+ },
11340
+ user_id: userId
11341
+ });
11342
+ if (!result.key) {
11343
+ res.status(502).json({ error: "LiteLLM returned no key material" });
11344
+ return;
11345
+ }
11346
+ key = result.key;
11347
+ }
11348
+ const url = new URL(redirectUri);
11349
+ url.searchParams.set("key", key);
11350
+ if (teamId) url.searchParams.set("team", teamId);
11351
+ logger.info({
11352
+ action: "opencode.connect",
11353
+ userId,
11354
+ team: teamId ?? null,
11355
+ reused: Boolean(reusable?.token ?? reusable?.key)
11356
+ });
11357
+ res.redirect(302, url.href);
11358
+ } catch (error) {
11359
+ if (error instanceof ProvisioningError) {
11360
+ res.status(error.status).json(error.body);
11361
+ return;
11362
+ }
11363
+ logger.error("OpenCode connect failed", error);
11364
+ res.status(500).json({ error: error.message });
11365
+ }
11366
+ });
11367
+ }
11118
11368
  router.post("/keys/:keyId/update", async (req, res) => {
11119
11369
  try {
11120
11370
  const { keyId } = req.params;
@@ -11287,7 +11537,10 @@ async function createRouter(options) {
11287
11537
  })
11288
11538
  )
11289
11539
  );
11290
- res.json(teams.filter(Boolean));
11540
+ const list = teams.filter(Boolean);
11541
+ res.json(
11542
+ teamBudgetVisibility.hideTeamBudgetForMembers ? list.map(redactTeamBudget) : list
11543
+ );
11291
11544
  } catch (error) {
11292
11545
  if (error instanceof ProvisioningError) {
11293
11546
  res.status(error.status).json(error.body);
@@ -11386,7 +11639,9 @@ async function createRouter(options) {
11386
11639
  teamCreateInFlight.set(key, createPromise);
11387
11640
  try {
11388
11641
  const result = await createPromise;
11389
- res.json(result);
11642
+ res.json(
11643
+ teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(result) : result
11644
+ );
11390
11645
  } catch (err) {
11391
11646
  sendTeamError(err, res);
11392
11647
  } finally {
@@ -11430,7 +11685,9 @@ async function createRouter(options) {
11430
11685
  teamId,
11431
11686
  owningGroup
11432
11687
  });
11433
- res.json(r);
11688
+ res.json(
11689
+ teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(r) : r
11690
+ );
11434
11691
  } catch (err) {
11435
11692
  sendTeamError(err, res);
11436
11693
  }
@@ -11488,7 +11745,9 @@ async function createRouter(options) {
11488
11745
  const owned = all.filter(
11489
11746
  (t) => typeof t.metadata?.owning_group === "string" && t.metadata.owning_group === teamAdminCfg.group
11490
11747
  );
11491
- res.json(owned);
11748
+ res.json(
11749
+ teamBudgetVisibility.hideTeamBudgetForManagers ? owned.map(redactTeamBudget) : owned
11750
+ );
11492
11751
  } catch (err) {
11493
11752
  sendTeamError(err, res);
11494
11753
  }
@@ -11630,7 +11889,9 @@ async function createRouter(options) {
11630
11889
  owningGroup
11631
11890
  });
11632
11891
  const updated = await withTeamFetchRetry(() => client.getTeamInfo(teamId));
11633
- res.json(updated);
11892
+ res.json(
11893
+ teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(updated) : updated
11894
+ );
11634
11895
  } catch (err) {
11635
11896
  sendTeamError(err, res);
11636
11897
  }
@@ -11661,7 +11922,9 @@ async function createRouter(options) {
11661
11922
  owningGroup
11662
11923
  });
11663
11924
  const updated = await withTeamFetchRetry(() => client.getTeamInfo(teamId));
11664
- res.json(updated);
11925
+ res.json(
11926
+ teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(updated) : updated
11927
+ );
11665
11928
  } catch (err) {
11666
11929
  sendTeamError(err, res);
11667
11930
  }
@@ -11740,7 +12003,9 @@ async function createRouter(options) {
11740
12003
  owningGroup,
11741
12004
  vector_stores: requested
11742
12005
  });
11743
- res.json(updated);
12006
+ res.json(
12007
+ teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(updated) : updated
12008
+ );
11744
12009
  } catch (err) {
11745
12010
  sendTeamError(err, res);
11746
12011
  }
@@ -11821,7 +12086,9 @@ async function createRouter(options) {
11821
12086
  before,
11822
12087
  after: requested
11823
12088
  });
11824
- res.json(updated);
12089
+ res.json(
12090
+ teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(updated) : updated
12091
+ );
11825
12092
  } catch (err) {
11826
12093
  sendTeamError(err, res);
11827
12094
  }
@@ -11840,7 +12107,24 @@ async function createRouter(options) {
11840
12107
  start_date,
11841
12108
  end_date
11842
12109
  );
11843
- res.json(usage);
12110
+ let hide = teamBudgetVisibility.hideTeamBudgetForMembers;
12111
+ if (teamAdminCfg.group) {
12112
+ try {
12113
+ const tokenEntityRef = await resolveUserId(req, auth);
12114
+ const isManager = !!tokenEntityRef && await isUserMemberOfGroup(
12115
+ tokenEntityRef,
12116
+ teamAdminCfg.group,
12117
+ catalogClient,
12118
+ auth,
12119
+ logger
12120
+ );
12121
+ hide = isManager ? teamBudgetVisibility.hideTeamBudgetForManagers : teamBudgetVisibility.hideTeamBudgetForMembers;
12122
+ } catch (err) {
12123
+ logger.warn(`Team-manager check failed, hiding team spend: ${err.message}`);
12124
+ hide = teamBudgetVisibility.hideTeamBudgetForMembers;
12125
+ }
12126
+ }
12127
+ res.json(hide ? redactTeamUsage(usage) : usage);
11844
12128
  } catch (error) {
11845
12129
  logger.error("Failed to fetch team usage", error);
11846
12130
  res.status(500).json({ error: error.message });
@@ -12037,15 +12321,20 @@ var litellmPlugin = (0, import_backend_plugin_api.createBackendPlugin)({
12037
12321
  litellmTeamMcpManagePermission,
12038
12322
  litellmTeamMembersManagePermission,
12039
12323
  newDefaultVerifier,
12324
+ normalizeRequestTags,
12040
12325
  provisionUser,
12041
12326
  readBridgeConfig,
12042
12327
  readProvisioningDefaults,
12043
12328
  readRoleConfigs,
12044
12329
  readTeamAdminConfig,
12330
+ readTeamBudgetVisibility,
12331
+ redactTeamBudget,
12332
+ redactTeamUsage,
12045
12333
  resolveBridgeUserId,
12046
12334
  resolveUserId,
12047
12335
  resolveUserProfile,
12048
12336
  resolveUserRole,
12337
+ teamBudgetStatusFor,
12049
12338
  toLiteLLMUserId,
12050
12339
  validateTeamPatchInput,
12051
12340
  validateTeamWriteInput