@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/dist/index.d.ts CHANGED
@@ -2,11 +2,13 @@ export { litellmPlugin } from './plugin';
2
2
  export { litellmPlugin as default } from './plugin';
3
3
  export { createRouter } from './router';
4
4
  export * from './types';
5
- export { LiteLLMClient } from './client';
5
+ export { LiteLLMClient, normalizeRequestTags } from './client';
6
6
  export { resolveUserId, resolveUserProfile, toLiteLLMUserId, getOrProvisionUser, provisionUser, readProvisioningDefaults, readRoleConfigs, applyRoleOverrides, resolveUserRole, ProvisioningError, } from './provisioning';
7
7
  export type { BackstageUserProfile } from './provisioning';
8
8
  export { readTeamAdminConfig, isTeamManagementEnabled, assertTeamAdmin, validateTeamWriteInput, validateTeamPatchInput, } from './teamAdmin';
9
9
  export type { TeamAdminConfig, TeamAdminCheck, TeamWriteInput, TeamWriteValidation, TeamPatchValidation } from './teamAdmin';
10
+ export { readTeamBudgetVisibility, redactTeamBudget, redactTeamUsage, teamBudgetStatusFor, } from './teamBudgetVisibility';
11
+ export type { TeamBudgetVisibility, TeamBudgetStatus } from './teamBudgetVisibility';
10
12
  export { KeycloakJWTVerifier, newDefaultVerifier, readBridgeConfig, resolveBridgeUserId, getOrProvisionUserFromClaims, bridgeListKeys, bridgeGenerateKey, } from './bridge';
11
13
  export type { BridgeClaims, TokenVerifier, BridgeConfig, KeycloakJWTVerifierOptions, } from './bridge';
12
14
  export { litellmPermissions, litellmKeyCreatePermission, litellmKeyRevokePermission, litellmKeyManagePermission, litellmAuditReadPermission, litellmTeamCreatePermission, litellmTeamManagePermission, litellmTeamMembersManagePermission, litellmTeamKnowledgebaseManagePermission, litellmTeamMcpManagePermission, litellmTeamDeletePermission, } from './permissions';
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.litellmTeamDeletePermission = exports.litellmTeamMcpManagePermission = exports.litellmTeamKnowledgebaseManagePermission = exports.litellmTeamMembersManagePermission = exports.litellmTeamManagePermission = exports.litellmTeamCreatePermission = exports.litellmAuditReadPermission = exports.litellmKeyManagePermission = exports.litellmKeyRevokePermission = exports.litellmKeyCreatePermission = exports.litellmPermissions = exports.bridgeGenerateKey = exports.bridgeListKeys = exports.getOrProvisionUserFromClaims = exports.resolveBridgeUserId = exports.readBridgeConfig = exports.newDefaultVerifier = exports.KeycloakJWTVerifier = exports.validateTeamPatchInput = exports.validateTeamWriteInput = exports.assertTeamAdmin = exports.isTeamManagementEnabled = exports.readTeamAdminConfig = exports.ProvisioningError = exports.resolveUserRole = exports.applyRoleOverrides = exports.readRoleConfigs = exports.readProvisioningDefaults = exports.provisionUser = exports.getOrProvisionUser = exports.toLiteLLMUserId = exports.resolveUserProfile = exports.resolveUserId = exports.LiteLLMClient = exports.createRouter = exports.default = exports.litellmPlugin = void 0;
17
+ exports.litellmTeamDeletePermission = exports.litellmTeamMcpManagePermission = exports.litellmTeamKnowledgebaseManagePermission = exports.litellmTeamMembersManagePermission = exports.litellmTeamManagePermission = exports.litellmTeamCreatePermission = exports.litellmAuditReadPermission = exports.litellmKeyManagePermission = exports.litellmKeyRevokePermission = exports.litellmKeyCreatePermission = exports.litellmPermissions = exports.bridgeGenerateKey = exports.bridgeListKeys = exports.getOrProvisionUserFromClaims = exports.resolveBridgeUserId = exports.readBridgeConfig = exports.newDefaultVerifier = exports.KeycloakJWTVerifier = exports.teamBudgetStatusFor = exports.redactTeamUsage = exports.redactTeamBudget = exports.readTeamBudgetVisibility = exports.validateTeamPatchInput = exports.validateTeamWriteInput = exports.assertTeamAdmin = exports.isTeamManagementEnabled = exports.readTeamAdminConfig = exports.ProvisioningError = exports.resolveUserRole = exports.applyRoleOverrides = exports.readRoleConfigs = exports.readProvisioningDefaults = exports.provisionUser = exports.getOrProvisionUser = exports.toLiteLLMUserId = exports.resolveUserProfile = exports.resolveUserId = exports.normalizeRequestTags = exports.LiteLLMClient = exports.createRouter = exports.default = exports.litellmPlugin = void 0;
18
18
  var plugin_1 = require("./plugin");
19
19
  Object.defineProperty(exports, "litellmPlugin", { enumerable: true, get: function () { return plugin_1.litellmPlugin; } });
20
20
  var plugin_2 = require("./plugin");
@@ -24,6 +24,7 @@ Object.defineProperty(exports, "createRouter", { enumerable: true, get: function
24
24
  __exportStar(require("./types"), exports);
25
25
  var client_1 = require("./client");
26
26
  Object.defineProperty(exports, "LiteLLMClient", { enumerable: true, get: function () { return client_1.LiteLLMClient; } });
27
+ Object.defineProperty(exports, "normalizeRequestTags", { enumerable: true, get: function () { return client_1.normalizeRequestTags; } });
27
28
  var provisioning_1 = require("./provisioning");
28
29
  Object.defineProperty(exports, "resolveUserId", { enumerable: true, get: function () { return provisioning_1.resolveUserId; } });
29
30
  Object.defineProperty(exports, "resolveUserProfile", { enumerable: true, get: function () { return provisioning_1.resolveUserProfile; } });
@@ -41,6 +42,11 @@ Object.defineProperty(exports, "isTeamManagementEnabled", { enumerable: true, ge
41
42
  Object.defineProperty(exports, "assertTeamAdmin", { enumerable: true, get: function () { return teamAdmin_1.assertTeamAdmin; } });
42
43
  Object.defineProperty(exports, "validateTeamWriteInput", { enumerable: true, get: function () { return teamAdmin_1.validateTeamWriteInput; } });
43
44
  Object.defineProperty(exports, "validateTeamPatchInput", { enumerable: true, get: function () { return teamAdmin_1.validateTeamPatchInput; } });
45
+ var teamBudgetVisibility_1 = require("./teamBudgetVisibility");
46
+ Object.defineProperty(exports, "readTeamBudgetVisibility", { enumerable: true, get: function () { return teamBudgetVisibility_1.readTeamBudgetVisibility; } });
47
+ Object.defineProperty(exports, "redactTeamBudget", { enumerable: true, get: function () { return teamBudgetVisibility_1.redactTeamBudget; } });
48
+ Object.defineProperty(exports, "redactTeamUsage", { enumerable: true, get: function () { return teamBudgetVisibility_1.redactTeamUsage; } });
49
+ Object.defineProperty(exports, "teamBudgetStatusFor", { enumerable: true, get: function () { return teamBudgetVisibility_1.teamBudgetStatusFor; } });
44
50
  var bridge_1 = require("./bridge");
45
51
  Object.defineProperty(exports, "KeycloakJWTVerifier", { enumerable: true, get: function () { return bridge_1.KeycloakJWTVerifier; } });
46
52
  Object.defineProperty(exports, "newDefaultVerifier", { enumerable: true, get: function () { return bridge_1.newDefaultVerifier; } });
package/dist/openapi.js CHANGED
@@ -66,6 +66,10 @@ exports.openApiSpec = {
66
66
  allowUnlimitedBudget: { type: 'boolean' },
67
67
  teamRequired: { type: 'boolean' },
68
68
  } },
69
+ display: { type: 'object', properties: {
70
+ hideTeamBudgetForMembers: { type: 'boolean' },
71
+ hideTeamBudgetForManagers: { type: 'boolean' },
72
+ } },
69
73
  } } } },
70
74
  },
71
75
  },
@@ -169,6 +173,9 @@ exports.openApiSpec = {
169
173
  get: {
170
174
  tags: ['Teams'],
171
175
  summary: 'List teams the current user belongs to',
176
+ description: 'When litellm.display.hideTeamBudgetForMembers is set, max_budget/spend ' +
177
+ 'are stripped and budget_pct/budget_status/budget_hidden carry the ' +
178
+ 'consumption level instead.',
172
179
  responses: { '200': { description: 'Array of teams' } },
173
180
  },
174
181
  },
@@ -176,6 +183,8 @@ exports.openApiSpec = {
176
183
  get: {
177
184
  tags: ['Teams'],
178
185
  summary: 'Usage metrics for a team',
186
+ description: 'Spend fields are zeroed when the caller is subject to team budget ' +
187
+ 'hiding (member or manager flag, whichever applies to them).',
179
188
  parameters: [
180
189
  { name: 'teamId', in: 'path', required: true, schema: { type: 'string' } },
181
190
  { name: 'start_date', in: 'query', required: true, schema: { type: 'string' } },
@@ -0,0 +1,30 @@
1
+ import { Config } from '@backstage/config';
2
+ /**
3
+ * Configuration for the OpenCode SSO-connect integration (litellm.opencode.*).
4
+ *
5
+ * The connect flow is served by the opencode-portal-auth plugin client-side:
6
+ * the user runs /connect in OpenCode, a browser opens
7
+ * GET {baseUrl}/api/litellm/opencode/connect?team=...&redirect_uri=...
8
+ * and this backend generates (or reuses) a LiteLLM virtual key bound to the
9
+ * signed-in Backstage user and, optionally, one of their teams, then 302s
10
+ * back to the local OpenCode callback listener.
11
+ */
12
+ export interface OpencodeConfig {
13
+ /** Route is mounted only when true. Default false. */
14
+ enabled: boolean;
15
+ /**
16
+ * Default duration for freshly generated keys, in LiteLLM format
17
+ * (e.g. "30d"). Users can re-connect to rotate. Default '30d'.
18
+ */
19
+ keyDuration: string;
20
+ /** Default max budget (USD) for keys created via the connect flow. Default 50. */
21
+ maxBudget: number;
22
+ /**
23
+ * When true, a team query parameter must reference a team the user is a
24
+ * member of (or manage); otherwise the key is personal-only.
25
+ */
26
+ requireTeam: boolean;
27
+ /** Extra metadata stamped on keys created via this flow. */
28
+ metadata: Record<string, string>;
29
+ }
30
+ export declare function readOpencodeConfig(config: Config): OpencodeConfig;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readOpencodeConfig = readOpencodeConfig;
4
+ function readOpencodeConfig(config) {
5
+ const enabled = config.getOptionalBoolean('litellm.opencode.enabled') ?? false;
6
+ return {
7
+ enabled,
8
+ keyDuration: config.getOptionalString('litellm.opencode.keyDuration') ?? '30d',
9
+ maxBudget: config.getOptionalNumber('litellm.opencode.maxBudget') ?? 50,
10
+ requireTeam: config.getOptionalBoolean('litellm.opencode.requireTeam') ?? false,
11
+ metadata: config.getOptional('litellm.opencode.metadata') ?? {},
12
+ };
13
+ }
package/dist/router.js CHANGED
@@ -45,6 +45,8 @@ Object.defineProperty(exports, "ProvisioningError", { enumerable: true, get: fun
45
45
  const bridge_1 = require("./bridge");
46
46
  const permissions_1 = require("./permissions");
47
47
  const teamAdmin_1 = require("./teamAdmin");
48
+ const teamBudgetVisibility_1 = require("./teamBudgetVisibility");
49
+ const opencode_1 = require("./opencode");
48
50
  const teamCreateInFlight = new Map();
49
51
  /** Backoff schedule for `withTeamFetchRetry` — 3 retries over ~1.3s total. */
50
52
  const TEAM_FETCH_RETRY_DELAYS_MS = [100, 300, 900];
@@ -84,6 +86,24 @@ async function withTeamFetchRetry(fn, delaysMs = TEAM_FETCH_RETRY_DELAYS_MS) {
84
86
  }
85
87
  }
86
88
  }
89
+ /**
90
+ * Only loopback http://localhost:<port>/callback redirect URIs are accepted —
91
+ * the OpenCode plugin runs its callback listener on 127.0.0.1. This
92
+ * keeps the flow from being usable as an open redirect.
93
+ */
94
+ function isValidRedirectUri(uri) {
95
+ if (!uri)
96
+ return false;
97
+ try {
98
+ const url = new URL(uri);
99
+ return (url.protocol === 'http:' &&
100
+ (url.hostname === 'localhost' || url.hostname === '127.0.0.1') &&
101
+ url.pathname === '/callback');
102
+ }
103
+ catch {
104
+ return false;
105
+ }
106
+ }
87
107
  async function createRouter(options) {
88
108
  const { config, logger, auth, discovery, permissions } = options;
89
109
  const baseUrl = config.getString('litellm.baseUrl');
@@ -102,6 +122,8 @@ async function createRouter(options) {
102
122
  const teamMgmtEnabled = (0, teamAdmin_1.isTeamManagementEnabled)(config);
103
123
  const objectPermsEnabled = (0, teamAdmin_1.isObjectPermissionsEnabled)(config);
104
124
  const teamAdminCfg = (0, teamAdmin_1.readTeamAdminConfig)(config);
125
+ const teamBudgetVisibility = (0, teamBudgetVisibility_1.readTeamBudgetVisibility)(config);
126
+ const opencodeCfg = (0, opencode_1.readOpencodeConfig)(config);
105
127
  const catalogClient = options.catalogClient ?? new catalog_client_1.CatalogClient({ discoveryApi: discovery });
106
128
  if (provisioningEnabled) {
107
129
  logger.info(`LiteLLM auto-provisioning enabled — defaults: budget=$${provisioningDefaults.maxBudget}/${provisioningDefaults.budgetDuration}, models=${provisioningDefaults.models.length
@@ -111,6 +133,10 @@ async function createRouter(options) {
111
133
  if (config.getOptional('litellm.teamAdmin') && !teamMgmtEnabled) {
112
134
  logger.warn('litellm.teamAdmin is configured but team management is disabled — set permission.enabled: true and litellm.teamAdmin.group to enable it.');
113
135
  }
136
+ if (teamBudgetVisibility.hideTeamBudgetForMembers ||
137
+ teamBudgetVisibility.hideTeamBudgetForManagers) {
138
+ logger.info(`LiteLLM team budget hiding enabled — members: ${teamBudgetVisibility.hideTeamBudgetForMembers}, managers: ${teamBudgetVisibility.hideTeamBudgetForManagers}`);
139
+ }
114
140
  const router = (0, express_1.Router)();
115
141
  // JSON body parser. Without this, every POST/PUT endpoint sees an empty
116
142
  // req.body. Backstage's httpRouter does not apply a body parser at the
@@ -125,12 +151,17 @@ async function createRouter(options) {
125
151
  res.json({
126
152
  baseUrl: publicBaseUrl,
127
153
  keyGeneration: { allowUnlimitedBudget, teamRequired },
154
+ opencode: { enabled: opencodeCfg.enabled },
128
155
  teamManagement: {
129
156
  enabled: teamMgmtEnabled,
130
157
  maxBudgetCeiling: teamAdminCfg.maxBudgetCeiling,
131
158
  allowUnlimitedBudget: teamAdminCfg.allowUnlimitedBudget,
132
159
  objectPermissionsEnabled: objectPermsEnabled,
133
160
  },
161
+ display: {
162
+ hideTeamBudgetForMembers: teamBudgetVisibility.hideTeamBudgetForMembers,
163
+ hideTeamBudgetForManagers: teamBudgetVisibility.hideTeamBudgetForManagers,
164
+ },
134
165
  });
135
166
  });
136
167
  // Self-hosted OpenAPI 3.1 contract — lets integrators read a spec instead
@@ -386,6 +417,122 @@ async function createRouter(options) {
386
417
  res.status(500).json({ error: error.message });
387
418
  }
388
419
  });
420
+ // ── OpenCode SSO-connect ─────────────────────────────────────────────────
421
+ //
422
+ // Client side: the opencode-portal-auth plugin. Handshake contract (see
423
+ // that package's README):
424
+ //
425
+ // GET /opencode/connect?team=<id>&redirect_uri=http://localhost:<port>/callback
426
+ // → (SSO-authenticated) generate/reuse a virtual key bound to the
427
+ // user and optional team → 302 {redirect_uri}?key=<api-key>
428
+ //
429
+ // Mounted only when litellm.opencode.enabled is true. Key creation is
430
+ // gated by the same litellmKeyCreatePermission as POST /keys/generate.
431
+ if (opencodeCfg.enabled) {
432
+ logger.info(`OpenCode connect endpoint enabled — duration=${opencodeCfg.keyDuration}, maxBudget=$${opencodeCfg.maxBudget}`);
433
+ router.get('/opencode/connect', async (req, res) => {
434
+ try {
435
+ // The request comes from the browser (user session), so SSO
436
+ // auth applies exactly like for any other UI route.
437
+ const tokenEntityRef = await (0, provisioning_1.resolveUserId)(req, auth);
438
+ if (!tokenEntityRef) {
439
+ res.status(401).json({ error: 'Authentication required' });
440
+ return;
441
+ }
442
+ const redirectUri = req.query.redirect_uri;
443
+ if (!isValidRedirectUri(redirectUri)) {
444
+ res.status(400).json({
445
+ error: 'Invalid redirect_uri — expected http://localhost:<port>/callback',
446
+ });
447
+ return;
448
+ }
449
+ if (!(await assertPermission(req, permissions_1.litellmKeyCreatePermission))) {
450
+ sendPermissionDenied(res, permissions_1.litellmKeyCreatePermission);
451
+ return;
452
+ }
453
+ const userId = (0, provisioning_1.toLiteLLMUserId)(tokenEntityRef, userIdDomain);
454
+ const userInfo = await (0, provisioning_1.getOrProvisionUser)(client, tokenEntityRef, userId, provisioningEnabled, provisioningDefaults, roleConfigs, catalogClient, auth, logger);
455
+ // Optional team binding: must be one of the user's own teams so
456
+ // the connect flow can never mint a key billed to someone else.
457
+ const teamId = req.query.team;
458
+ if (teamId) {
459
+ const userTeams = userInfo?.teams ?? [];
460
+ if (!userTeams.includes(teamId)) {
461
+ res.status(403).json({
462
+ error: 'Access denied: team is not one of your teams',
463
+ team: teamId,
464
+ });
465
+ return;
466
+ }
467
+ }
468
+ else if (opencodeCfg.requireTeam) {
469
+ res.status(400).json({ error: 'A team is required for this connection' });
470
+ return;
471
+ }
472
+ // Alias is stable per user+team so re-connecting rotates the same
473
+ // logical key slot instead of accumulating duplicates. LiteLLM
474
+ // rejects duplicate aliases, so on conflict we look up the existing
475
+ // key and reuse it (the user gets a working connection either way).
476
+ const alias = teamId
477
+ ? `opencode-${userId}-${teamId}`
478
+ : `opencode-${userId}`;
479
+ const existing = await client
480
+ .listKeys(userId)
481
+ .catch(() => []);
482
+ // Reuse a healthy existing key for this slot rather than failing
483
+ // on the alias collision LiteLLM would raise. Blocked keys are
484
+ // skipped so a blocked slot can be recovered by re-connecting.
485
+ const reusable = existing.find(k => k.key_alias === alias &&
486
+ !k.blocked &&
487
+ k.user_id === userId);
488
+ let key;
489
+ if (reusable?.token ?? reusable?.key) {
490
+ key = reusable.token ?? reusable.key;
491
+ }
492
+ else {
493
+ const profile = await (0, provisioning_1.resolveUserProfile)(tokenEntityRef, catalogClient, auth, logger);
494
+ const result = await client.generateKey({
495
+ alias,
496
+ duration: opencodeCfg.keyDuration,
497
+ max_budget: opencodeCfg.maxBudget,
498
+ team_id: teamId,
499
+ metadata: {
500
+ ...opencodeCfg.metadata,
501
+ created_via: 'opencode-connect',
502
+ created_by_backstage_user: tokenEntityRef,
503
+ ...(profile.email && { created_by_email: profile.email }),
504
+ ...(teamId && { opencode_team: teamId }),
505
+ },
506
+ user_id: userId,
507
+ });
508
+ if (!result.key) {
509
+ res.status(502).json({ error: 'LiteLLM returned no key material' });
510
+ return;
511
+ }
512
+ key = result.key;
513
+ }
514
+ const url = new URL(redirectUri);
515
+ url.searchParams.set('key', key);
516
+ if (teamId)
517
+ url.searchParams.set('team', teamId);
518
+ logger.info({
519
+ action: 'opencode.connect',
520
+ userId,
521
+ team: teamId ?? null,
522
+ reused: Boolean(reusable?.token ?? reusable?.key),
523
+ });
524
+ res.redirect(302, url.href);
525
+ }
526
+ catch (error) {
527
+ if (error instanceof provisioning_1.ProvisioningError) {
528
+ res.status(error.status).json(error.body);
529
+ return;
530
+ }
531
+ logger.error('OpenCode connect failed', error);
532
+ res.status(500).json({ error: error.message });
533
+ }
534
+ });
535
+ }
389
536
  router.post('/keys/:keyId/update', async (req, res) => {
390
537
  try {
391
538
  const { keyId } = req.params;
@@ -552,7 +699,12 @@ async function createRouter(options) {
552
699
  logger.warn(`Failed to fetch team ${teamId} after retries: ${err.message}`);
553
700
  return null;
554
701
  })));
555
- res.json(teams.filter(Boolean));
702
+ const list = teams.filter(Boolean);
703
+ // Member surface: strip dollar amounts when the operator hides them
704
+ // from members. Managers who need the numbers use /teams/managed.
705
+ res.json(teamBudgetVisibility.hideTeamBudgetForMembers
706
+ ? list.map(teamBudgetVisibility_1.redactTeamBudget)
707
+ : list);
556
708
  }
557
709
  catch (error) {
558
710
  if (error instanceof provisioning_1.ProvisioningError) {
@@ -656,7 +808,10 @@ async function createRouter(options) {
656
808
  teamCreateInFlight.set(key, createPromise);
657
809
  try {
658
810
  const result = await createPromise;
659
- res.json(result);
811
+ // Manager surface: redact dollars when hidden even from managers.
812
+ res.json(teamBudgetVisibility.hideTeamBudgetForManagers
813
+ ? (0, teamBudgetVisibility_1.redactTeamBudget)(result)
814
+ : result);
660
815
  }
661
816
  catch (err) {
662
817
  sendTeamError(err, res);
@@ -704,7 +859,9 @@ async function createRouter(options) {
704
859
  teamId,
705
860
  owningGroup,
706
861
  });
707
- res.json(r);
862
+ res.json(teamBudgetVisibility.hideTeamBudgetForManagers
863
+ ? (0, teamBudgetVisibility_1.redactTeamBudget)(r)
864
+ : r);
708
865
  }
709
866
  catch (err) {
710
867
  sendTeamError(err, res);
@@ -772,7 +929,10 @@ async function createRouter(options) {
772
929
  // the caller's group set and filter with includes().
773
930
  const owned = all.filter(t => typeof t.metadata?.owning_group === 'string' &&
774
931
  t.metadata.owning_group === teamAdminCfg.group);
775
- res.json(owned);
932
+ // Manager surface: strip dollar amounts when hidden even from managers.
933
+ res.json(teamBudgetVisibility.hideTeamBudgetForManagers
934
+ ? owned.map(teamBudgetVisibility_1.redactTeamBudget)
935
+ : owned);
776
936
  }
777
937
  catch (err) {
778
938
  sendTeamError(err, res);
@@ -924,7 +1084,9 @@ async function createRouter(options) {
924
1084
  owningGroup,
925
1085
  });
926
1086
  const updated = await withTeamFetchRetry(() => client.getTeamInfo(teamId));
927
- res.json(updated);
1087
+ res.json(teamBudgetVisibility.hideTeamBudgetForManagers
1088
+ ? (0, teamBudgetVisibility_1.redactTeamBudget)(updated)
1089
+ : updated);
928
1090
  }
929
1091
  catch (err) {
930
1092
  sendTeamError(err, res);
@@ -959,7 +1121,9 @@ async function createRouter(options) {
959
1121
  owningGroup,
960
1122
  });
961
1123
  const updated = await withTeamFetchRetry(() => client.getTeamInfo(teamId));
962
- res.json(updated);
1124
+ res.json(teamBudgetVisibility.hideTeamBudgetForManagers
1125
+ ? (0, teamBudgetVisibility_1.redactTeamBudget)(updated)
1126
+ : updated);
963
1127
  }
964
1128
  catch (err) {
965
1129
  sendTeamError(err, res);
@@ -1047,7 +1211,9 @@ async function createRouter(options) {
1047
1211
  owningGroup,
1048
1212
  vector_stores: requested,
1049
1213
  });
1050
- res.json(updated);
1214
+ res.json(teamBudgetVisibility.hideTeamBudgetForManagers
1215
+ ? (0, teamBudgetVisibility_1.redactTeamBudget)(updated)
1216
+ : updated);
1051
1217
  }
1052
1218
  catch (err) {
1053
1219
  sendTeamError(err, res);
@@ -1136,7 +1302,9 @@ async function createRouter(options) {
1136
1302
  before,
1137
1303
  after: requested,
1138
1304
  });
1139
- res.json(updated);
1305
+ res.json(teamBudgetVisibility.hideTeamBudgetForManagers
1306
+ ? (0, teamBudgetVisibility_1.redactTeamBudget)(updated)
1307
+ : updated);
1140
1308
  }
1141
1309
  catch (err) {
1142
1310
  sendTeamError(err, res);
@@ -1151,7 +1319,26 @@ async function createRouter(options) {
1151
1319
  return;
1152
1320
  }
1153
1321
  const usage = await client.getTeamUsage(teamId, start_date, end_date);
1154
- res.json(usage);
1322
+ // Spend figures would let a client derive a hidden budget
1323
+ // (budget = spend / pct), so they follow the same hiding flags.
1324
+ // Which flag applies depends on whether the caller is a team manager;
1325
+ // catalog failures fail closed to the member rule.
1326
+ let hide = teamBudgetVisibility.hideTeamBudgetForMembers;
1327
+ if (teamAdminCfg.group) {
1328
+ try {
1329
+ const tokenEntityRef = await (0, provisioning_1.resolveUserId)(req, auth);
1330
+ const isManager = !!tokenEntityRef &&
1331
+ (await (0, provisioning_1.isUserMemberOfGroup)(tokenEntityRef, teamAdminCfg.group, catalogClient, auth, logger));
1332
+ hide = isManager
1333
+ ? teamBudgetVisibility.hideTeamBudgetForManagers
1334
+ : teamBudgetVisibility.hideTeamBudgetForMembers;
1335
+ }
1336
+ catch (err) {
1337
+ logger.warn(`Team-manager check failed, hiding team spend: ${err.message}`);
1338
+ hide = teamBudgetVisibility.hideTeamBudgetForMembers;
1339
+ }
1340
+ }
1341
+ res.json(hide ? (0, teamBudgetVisibility_1.redactTeamUsage)(usage) : usage);
1155
1342
  }
1156
1343
  catch (error) {
1157
1344
  logger.error('Failed to fetch team usage', error);
@@ -0,0 +1,48 @@
1
+ import { Config } from '@backstage/config';
2
+ import { TeamInfo, UsageMetrics } from './types';
3
+ /**
4
+ * Independent "hide team budget" switches for the two team audiences.
5
+ *
6
+ * - `hideTeamBudgetForMembers` redacts dollar amounts on the member surface:
7
+ * `GET /teams`, the Teams cards, and the `TEAM` section of the budget widget.
8
+ * Members still see the consumption level (percent of cap + ok/near/over
9
+ * status + reset window).
10
+ * - `hideTeamBudgetForManagers` does the same on the manager surface:
11
+ * `GET /teams/managed`, team write responses, and the ManageTeamDialog
12
+ * budget field (which becomes write-only).
13
+ *
14
+ * Both default to false (current behavior: dollars visible). Splitting them
15
+ * lets an operator hide real budgets from regular members while team managers
16
+ * keep the numbers they need to govern — or hide from both.
17
+ */
18
+ export interface TeamBudgetVisibility {
19
+ hideTeamBudgetForMembers: boolean;
20
+ hideTeamBudgetForManagers: boolean;
21
+ }
22
+ /**
23
+ * Consumption status derived from spend vs. cap. Mirrors the frontend
24
+ * `budgetTone` thresholds (80% near, 100% over) so both sides agree.
25
+ */
26
+ export type TeamBudgetStatus = 'ok' | 'near' | 'over';
27
+ export declare function teamBudgetStatusFor(spend: number, budget: number): TeamBudgetStatus;
28
+ export declare function readTeamBudgetVisibility(config: Config): TeamBudgetVisibility;
29
+ /**
30
+ * Strips dollar amounts from a team record while preserving the consumption
31
+ * signal. `max_budget` is dropped, `spend` is zeroed (the field is required
32
+ * downstream, so it cannot be omitted — `budget_hidden: true` marks the
33
+ * record as redacted), and `budget_pct` + `budget_status` carry the level of
34
+ * consumption. `budget_duration`, models, members, and rate limits are kept:
35
+ * none of them reveal dollars.
36
+ *
37
+ * Teams without a positive budget (unlimited) carry no dollars to hide and
38
+ * are returned untouched.
39
+ */
40
+ export declare function redactTeamBudget(team: TeamInfo): TeamInfo;
41
+ /**
42
+ * Strips dollar amounts from team usage metrics while keeping the
43
+ * non-monetary signal (tokens, requests). Returning zeroed spend (rather
44
+ * than omitting the fields) keeps the response shape stable for existing
45
+ * clients; the frontend hides spend charts whenever the owning team record
46
+ * is redacted.
47
+ */
48
+ export declare function redactTeamUsage(usage: UsageMetrics): UsageMetrics;
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.teamBudgetStatusFor = teamBudgetStatusFor;
4
+ exports.readTeamBudgetVisibility = readTeamBudgetVisibility;
5
+ exports.redactTeamBudget = redactTeamBudget;
6
+ exports.redactTeamUsage = redactTeamUsage;
7
+ function teamBudgetStatusFor(spend, budget) {
8
+ if (budget > 0 && spend >= budget)
9
+ return 'over';
10
+ if (budget > 0 && spend >= budget * 0.8)
11
+ return 'near';
12
+ return 'ok';
13
+ }
14
+ function readTeamBudgetVisibility(config) {
15
+ return {
16
+ hideTeamBudgetForMembers: config.getOptionalBoolean('litellm.display.hideTeamBudgetForMembers') ?? false,
17
+ hideTeamBudgetForManagers: config.getOptionalBoolean('litellm.display.hideTeamBudgetForManagers') ?? false,
18
+ };
19
+ }
20
+ /**
21
+ * Strips dollar amounts from a team record while preserving the consumption
22
+ * signal. `max_budget` is dropped, `spend` is zeroed (the field is required
23
+ * downstream, so it cannot be omitted — `budget_hidden: true` marks the
24
+ * record as redacted), and `budget_pct` + `budget_status` carry the level of
25
+ * consumption. `budget_duration`, models, members, and rate limits are kept:
26
+ * none of them reveal dollars.
27
+ *
28
+ * Teams without a positive budget (unlimited) carry no dollars to hide and
29
+ * are returned untouched.
30
+ */
31
+ function redactTeamBudget(team) {
32
+ const budget = team.max_budget ?? 0;
33
+ if (budget <= 0)
34
+ return team;
35
+ const spend = team.spend ?? 0;
36
+ const pct = Math.min(100, Math.max(0, (spend / budget) * 100));
37
+ const { max_budget: _dropped, ...rest } = team;
38
+ return {
39
+ ...rest,
40
+ spend: 0,
41
+ budget_pct: pct,
42
+ budget_status: teamBudgetStatusFor(spend, budget),
43
+ budget_hidden: true,
44
+ };
45
+ }
46
+ /**
47
+ * Strips dollar amounts from team usage metrics while keeping the
48
+ * non-monetary signal (tokens, requests). Returning zeroed spend (rather
49
+ * than omitting the fields) keeps the response shape stable for existing
50
+ * clients; the frontend hides spend charts whenever the owning team record
51
+ * is redacted.
52
+ */
53
+ function redactTeamUsage(usage) {
54
+ const zeroSpend = (p) => ({
55
+ ...p,
56
+ spend: 0,
57
+ });
58
+ const zeroTotalSpend = (p) => ({
59
+ ...p,
60
+ total_spend: 0,
61
+ });
62
+ return {
63
+ ...usage,
64
+ total_spend: 0,
65
+ usage_by_model: Object.fromEntries(Object.entries(usage.usage_by_model ?? {}).map(([k, v]) => [
66
+ k,
67
+ zeroTotalSpend(v),
68
+ ])),
69
+ usage_by_key: Object.fromEntries(Object.entries(usage.usage_by_key ?? {}).map(([k, v]) => [
70
+ k,
71
+ zeroTotalSpend(v),
72
+ ])),
73
+ daily_usage: (usage.daily_usage ?? []).map(zeroSpend),
74
+ daily_by_model: (usage.daily_by_model ?? []).map(zeroSpend),
75
+ };
76
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/types.ts"],
4
- "sourcesContent": ["export interface UserInfo {\n user_id: string;\n user_email?: string;\n email?: string;\n teams?: string[];\n models?: string[];\n max_budget?: number;\n spend?: number;\n current_spend?: number;\n soft_limit?: number;\n hard_limit?: number;\n /** Spend-reset period for max_budget, e.g. \"30d\". */\n budget_duration?: string;\n /** Backstage-computed: true when the user is a member of litellm.audit.group */\n can_view_audit?: boolean;\n}\n\nexport interface TeamMember {\n user_id: string;\n user_email?: string;\n role: 'admin' | 'user';\n}\n\nexport interface TeamObjectPermission {\n /** Vector store ids/names attached to the team. */\n vector_stores?: string[];\n /** MCP server ids/names attached to the team. */\n mcp_servers?: string[];\n /** MCP access group names attached to the team. */\n mcp_access_groups?: string[];\n}\n\nexport interface TeamInfo {\n team_id: string;\n team_alias?: string;\n max_budget?: number;\n /** Spend-reset period for max_budget, e.g. \"30d\". */\n budget_duration?: string;\n spend: number;\n members_with_roles?: TeamMember[];\n models?: string[];\n tpm_limit?: number;\n rpm_limit?: number;\n /** Arbitrary metadata stored on the team record. */\n metadata?: Record<string, unknown>;\n /** Knowledge bases (vector stores) and MCP servers attached to the team. */\n object_permission?: TeamObjectPermission;\n /** Whether the team is blocked/deactivated. */\n blocked?: boolean;\n /** Max budget available to individual team members (if set per-member). */\n team_member_budget?: number;\n}\n\nexport interface CreateTeamRequest {\n team_alias: string;\n models?: string[];\n max_budget?: number;\n budget_duration?: string;\n tpm_limit?: number;\n rpm_limit?: number;\n metadata?: Record<string, unknown>;\n object_permission?: TeamObjectPermission;\n}\n\nexport interface UpdateTeamRequest extends Partial<CreateTeamRequest> {\n team_id: string;\n blocked?: boolean;\n}\n\nexport interface CreateTeamResponse {\n team_id: string;\n team_alias?: string;\n}\n\nexport interface VirtualKey {\n key: string;\n token: string;\n key_alias?: string;\n created_at: string;\n expires_at?: string;\n spend: number;\n max_budget?: number;\n budget_duration?: string;\n tpm_limit?: number;\n rpm_limit?: number;\n models?: string[];\n user_id?: string;\n blocked?: boolean;\n}\n\n/**\n * Shape of a single entry inside LiteLLM's `/user/info` `keys` array.\n * Differs from VirtualKey: uses `expires` (not `expires_at`), exposes\n * both a hashed `token` and a masked `key_name`, and fields are nullable\n * rather than optional.\n */\nexport interface LiteLLMUserKey {\n token: string;\n key_name?: string;\n key_alias?: string | null;\n spend?: number;\n expires?: string | null;\n models?: string[];\n tpm_limit?: number | null;\n rpm_limit?: number | null;\n max_budget?: number | null;\n budget_duration?: string | null;\n user_id?: string | null;\n team_id?: string | null;\n created_at: string;\n blocked?: boolean | null;\n}\n\nexport interface ModelInfo {\n model_name: string;\n mode: string;\n supports_function_calling?: boolean;\n supports_vision?: boolean;\n input_cost_per_token?: number;\n output_cost_per_token?: number;\n max_input_tokens?: number;\n max_output_tokens?: number;\n /** Access group names this model belongs to (litellm.model_info.access_groups). A team's `models` list can reference a group name instead of a literal model_name. */\n access_groups?: string[];\n}\n\nexport interface UsageModelBreakdown {\n total_spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n}\n\nexport interface UsageKeyBreakdown {\n key_alias?: string;\n team_id?: string | null;\n models: string[];\n total_spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n}\n\nexport interface UsageDailyPoint {\n date: string;\n spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n}\n\nexport interface UsageDailyModelPoint {\n date: string;\n model: string;\n spend: number;\n prompt_tokens: number;\n completion_tokens: number;\n total_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n}\n\nexport interface UsageMetrics {\n total_spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n usage_by_model: Record<string, UsageModelBreakdown>;\n usage_by_key: Record<string, UsageKeyBreakdown>;\n daily_usage: UsageDailyPoint[];\n daily_by_model: UsageDailyModelPoint[];\n}\n\nexport interface GenerateKeyRequest {\n alias?: string;\n models?: string[];\n duration?: string;\n max_budget?: number;\n tpm_limit?: number;\n rpm_limit?: number;\n user_id?: string;\n team_id?: string;\n key_type?: string;\n metadata?: Record<string, string>;\n}\n\nexport interface UpdateKeyRequest {\n key: string;\n key_alias?: string;\n models?: string[];\n max_budget?: number;\n tpm_limit?: number;\n rpm_limit?: number;\n team_id?: string;\n duration?: string;\n}\n\nexport interface GenerateKeyResponse {\n key: string;\n key_alias?: string;\n expires_at?: string;\n max_budget?: number;\n tpm_limit?: number;\n rpm_limit?: number;\n models?: string[];\n}\n\nexport interface DeleteKeyRequest {\n keys: string[];\n}\n\nexport interface LiteLLMConfig {\n baseUrl: string;\n masterKey: string;\n}\n\nexport interface ProvisioningDefaults {\n maxBudget: number;\n budgetDuration: string;\n models: string[];\n teams: string[];\n tpmLimit?: number;\n rpmLimit?: number;\n /**\n * LiteLLM user role applied on /user/new. Defaults to \"internal_user\"\n * which grants self-service Create/Delete/View on the user's own keys.\n * Valid values: proxy_admin, proxy_admin_viewer, internal_user,\n * internal_user_viewer, team.\n */\n userRole?: string;\n metadata: Record<string, string>;\n}\n\nexport interface RoleConfig {\n group: string;\n maxBudget?: number;\n budgetDuration?: string;\n models?: string[];\n teams?: string[];\n tpmLimit?: number;\n rpmLimit?: number;\n userRole?: string;\n metadata?: Record<string, string>;\n}\n\nexport interface CreateUserRequest {\n user_id: string;\n user_email?: string;\n user_alias?: string;\n user_role?: string;\n max_budget?: number;\n budget_duration?: string;\n models?: string[];\n teams?: string[];\n tpm_limit?: number;\n rpm_limit?: number;\n metadata?: Record<string, string>;\n auto_create_key?: boolean;\n}\n\nexport interface CreateUserResponse {\n user_id: string;\n user_email?: string;\n max_budget?: number;\n models?: string[];\n teams?: string[];\n}\n\nexport interface AuditLogEntry {\n id: string;\n updated_at: string;\n changed_by?: string;\n changed_by_api_key?: string;\n action?: string;\n table_name?: string;\n object_id?: string;\n before_value?: Record<string, unknown> | null;\n updated_values?: Record<string, unknown> | null;\n}\n\nexport interface PaginatedAuditLogs {\n audit_logs: AuditLogEntry[];\n total: number;\n page: number;\n page_size: number;\n total_pages: number;\n}\n\nexport interface AuditLogsParams {\n page?: number;\n page_size?: number;\n start_date?: string;\n end_date?: string;\n action?: string;\n table_name?: string;\n changed_by?: string;\n sort_by?: string;\n sort_order?: 'asc' | 'desc';\n}\n"],
4
+ "sourcesContent": ["export interface UserInfo {\n user_id: string;\n user_email?: string;\n email?: string;\n teams?: string[];\n models?: string[];\n max_budget?: number;\n spend?: number;\n current_spend?: number;\n soft_limit?: number;\n hard_limit?: number;\n /** Spend-reset period for max_budget, e.g. \"30d\". */\n budget_duration?: string;\n /** Backstage-computed: true when the user is a member of litellm.audit.group */\n can_view_audit?: boolean;\n}\n\nexport interface TeamMember {\n user_id: string;\n user_email?: string;\n role: 'admin' | 'user';\n}\n\nexport interface TeamObjectPermission {\n /** Vector store ids/names attached to the team. */\n vector_stores?: string[];\n /** MCP server ids/names attached to the team. */\n mcp_servers?: string[];\n /** MCP access group names attached to the team. */\n mcp_access_groups?: string[];\n}\n\nexport interface TeamInfo {\n team_id: string;\n team_alias?: string;\n max_budget?: number;\n /** Spend-reset period for max_budget, e.g. \"30d\". */\n budget_duration?: string;\n spend: number;\n /**\n * Share of max_budget consumed (0-100), present only on redacted records.\n * Set by the backend when litellm.display.hideTeamBudgetFor* is enabled so\n * clients can render the consumption level without dollar amounts.\n */\n budget_pct?: number;\n /** ok | near (>=80%) | over (>=100%), present only on redacted records. */\n budget_status?: 'ok' | 'near' | 'over';\n /** True when max_budget/spend were stripped by budget hiding. */\n budget_hidden?: boolean;\n members_with_roles?: TeamMember[];\n models?: string[];\n tpm_limit?: number;\n rpm_limit?: number;\n /** Arbitrary metadata stored on the team record. */\n metadata?: Record<string, unknown>;\n /** Knowledge bases (vector stores) and MCP servers attached to the team. */\n object_permission?: TeamObjectPermission;\n /** Whether the team is blocked/deactivated. */\n blocked?: boolean;\n /** Max budget available to individual team members (if set per-member). */\n team_member_budget?: number;\n}\n\nexport interface CreateTeamRequest {\n team_alias: string;\n models?: string[];\n max_budget?: number;\n budget_duration?: string;\n tpm_limit?: number;\n rpm_limit?: number;\n metadata?: Record<string, unknown>;\n object_permission?: TeamObjectPermission;\n}\n\nexport interface UpdateTeamRequest extends Partial<CreateTeamRequest> {\n team_id: string;\n blocked?: boolean;\n}\n\nexport interface CreateTeamResponse {\n team_id: string;\n team_alias?: string;\n}\n\nexport interface VirtualKey {\n key: string;\n token: string;\n key_alias?: string;\n created_at: string;\n expires_at?: string;\n spend: number;\n max_budget?: number;\n budget_duration?: string;\n tpm_limit?: number;\n rpm_limit?: number;\n models?: string[];\n user_id?: string;\n blocked?: boolean;\n}\n\n/**\n * Shape of a single entry inside LiteLLM's `/user/info` `keys` array.\n * Differs from VirtualKey: uses `expires` (not `expires_at`), exposes\n * both a hashed `token` and a masked `key_name`, and fields are nullable\n * rather than optional.\n */\nexport interface LiteLLMUserKey {\n token: string;\n key_name?: string;\n key_alias?: string | null;\n spend?: number;\n expires?: string | null;\n models?: string[];\n tpm_limit?: number | null;\n rpm_limit?: number | null;\n max_budget?: number | null;\n budget_duration?: string | null;\n user_id?: string | null;\n team_id?: string | null;\n created_at: string;\n blocked?: boolean | null;\n}\n\nexport interface ModelInfo {\n model_name: string;\n mode: string;\n supports_function_calling?: boolean;\n supports_vision?: boolean;\n input_cost_per_token?: number;\n output_cost_per_token?: number;\n max_input_tokens?: number;\n max_output_tokens?: number;\n /** Access group names this model belongs to (litellm.model_info.access_groups). A team's `models` list can reference a group name instead of a literal model_name. */\n access_groups?: string[];\n}\n\nexport interface UsageModelBreakdown {\n total_spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n}\n\nexport interface UsageKeyBreakdown {\n key_alias?: string;\n team_id?: string | null;\n models: string[];\n total_spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n}\n\nexport interface UsageDailyPoint {\n date: string;\n spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n}\n\nexport interface UsageDailyModelPoint {\n date: string;\n model: string;\n spend: number;\n prompt_tokens: number;\n completion_tokens: number;\n total_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n}\n\nexport interface UsageMetrics {\n total_spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n usage_by_model: Record<string, UsageModelBreakdown>;\n usage_by_key: Record<string, UsageKeyBreakdown>;\n daily_usage: UsageDailyPoint[];\n daily_by_model: UsageDailyModelPoint[];\n}\n\nexport interface GenerateKeyRequest {\n alias?: string;\n models?: string[];\n duration?: string;\n max_budget?: number;\n tpm_limit?: number;\n rpm_limit?: number;\n user_id?: string;\n team_id?: string;\n key_type?: string;\n metadata?: Record<string, string>;\n}\n\nexport interface UpdateKeyRequest {\n key: string;\n key_alias?: string;\n models?: string[];\n max_budget?: number;\n tpm_limit?: number;\n rpm_limit?: number;\n team_id?: string;\n duration?: string;\n}\n\nexport interface GenerateKeyResponse {\n key: string;\n key_alias?: string;\n expires_at?: string;\n max_budget?: number;\n tpm_limit?: number;\n rpm_limit?: number;\n models?: string[];\n}\n\nexport interface DeleteKeyRequest {\n keys: string[];\n}\n\nexport interface LiteLLMConfig {\n baseUrl: string;\n masterKey: string;\n}\n\nexport interface ProvisioningDefaults {\n maxBudget: number;\n budgetDuration: string;\n models: string[];\n teams: string[];\n tpmLimit?: number;\n rpmLimit?: number;\n /**\n * LiteLLM user role applied on /user/new. Defaults to \"internal_user\"\n * which grants self-service Create/Delete/View on the user's own keys.\n * Valid values: proxy_admin, proxy_admin_viewer, internal_user,\n * internal_user_viewer, team.\n */\n userRole?: string;\n metadata: Record<string, string>;\n}\n\nexport interface RoleConfig {\n group: string;\n maxBudget?: number;\n budgetDuration?: string;\n models?: string[];\n teams?: string[];\n tpmLimit?: number;\n rpmLimit?: number;\n userRole?: string;\n metadata?: Record<string, string>;\n}\n\nexport interface CreateUserRequest {\n user_id: string;\n user_email?: string;\n user_alias?: string;\n user_role?: string;\n max_budget?: number;\n budget_duration?: string;\n models?: string[];\n teams?: string[];\n tpm_limit?: number;\n rpm_limit?: number;\n metadata?: Record<string, string>;\n auto_create_key?: boolean;\n}\n\nexport interface CreateUserResponse {\n user_id: string;\n user_email?: string;\n max_budget?: number;\n models?: string[];\n teams?: string[];\n}\n\nexport interface AuditLogEntry {\n id: string;\n updated_at: string;\n changed_by?: string;\n changed_by_api_key?: string;\n action?: string;\n table_name?: string;\n object_id?: string;\n before_value?: Record<string, unknown> | null;\n updated_values?: Record<string, unknown> | null;\n}\n\nexport interface PaginatedAuditLogs {\n audit_logs: AuditLogEntry[];\n total: number;\n page: number;\n page_size: number;\n total_pages: number;\n}\n\n/** One row from LiteLLM's `/spend/logs` (per-request spend log entry). */\nexport interface SpendLogEntry {\n request_id?: string;\n /** ISO timestamp of the call. */\n startTime?: string;\n endTime?: string;\n spend?: number;\n total_tokens?: number;\n prompt_tokens?: number;\n completion_tokens?: number;\n model?: string;\n /** Virtual key alias or hash the request was billed to. */\n api_key?: string;\n user?: string;\n team_id?: string;\n /**\n * Tags attached to the request (e.g. `channel:backstage`,\n * `session:<thread>`, `invoked-by:<user>`). Shape varies across LiteLLM\n * versions (array or object) and is normalised to a string[].\n */\n request_tags?: string[] | Record<string, string>;\n /** LiteLLM `metadata` blob; carries trace ids and session grouping. */\n metadata?: Record<string, unknown> | string | null;\n}\n\nexport interface SpendLogsParams {\n /** ISO date `YYYY-MM-DD` (inclusive). */\n start_date: string;\n /** ISO date `YYYY-MM-DD` (inclusive). */\n end_date: string;\n /** Restrict to the given virtual key. */\n api_key?: string;\n user_id?: string;\n team_id?: string;\n /** Max rows to return (LiteLLM caps at 1000 per page). */\n page_size?: number;\n}\n\nexport interface AuditLogsParams {\n page?: number;\n page_size?: number;\n start_date?: string;\n end_date?: string;\n action?: string;\n table_name?: string;\n changed_by?: string;\n sort_by?: string;\n sort_order?: 'asc' | 'desc';\n}\n"],
5
5
  "mappings": ";;;;;;;;;;;;;;;;AAAA;AAAA;",
6
6
  "names": []
7
7
  }