@acarmisc/backstage-plugin-litellm-backend 0.17.7 → 0.19.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.
@@ -4,13 +4,12 @@ exports.registerTeamsRoutes = registerTeamsRoutes;
4
4
  const provisioning_1 = require("../provisioning");
5
5
  const backstage_plugin_litellm_common_1 = require("@acarmisc/backstage-plugin-litellm-common");
6
6
  const teamAdmin_1 = require("../teamAdmin");
7
- const teamBudgetVisibility_1 = require("../teamBudgetVisibility");
8
7
  const errors_1 = require("../errors");
9
8
  const withUser_1 = require("./middleware/withUser");
10
9
  const respondTeam_1 = require("../http/respondTeam");
11
10
  const teamFetch_1 = require("../http/teamFetch");
12
11
  function registerTeamsRoutes(router, ctx) {
13
- const { client, catalogClient, auth, permissions, logger, userIdDomain, provisioningEnabled, provisioningDefaults, roleConfigs, teamAdminCfg, teamBudgetVisibility, requireTeamMgmt, sendTeamError, authorizeTeamSubresource, } = ctx;
12
+ const { client, catalogClient, auth, permissions, logger, userIdDomain, provisioningEnabled, provisioningDefaults, roleConfigs, teamAdminCfg, requireTeamMgmt, sendTeamError, authorizeTeamSubresource, } = ctx;
14
13
  const requireUser = (0, withUser_1.createRequireUser)(ctx);
15
14
  router.get('/teams', requireUser, async (_req, res) => {
16
15
  try {
@@ -43,6 +42,12 @@ function registerTeamsRoutes(router, ctx) {
43
42
  res.status(500).json({ error: 'Team management is misconfigured (group is missing)' });
44
43
  return;
45
44
  }
45
+ if (teamAdminCfg.readOnly) {
46
+ res.status(403).json({
47
+ error: 'Team management is read-only (litellm.teamAdmin.readOnly): teams are managed outside Backstage',
48
+ });
49
+ return;
50
+ }
46
51
  const check = await (0, teamAdmin_1.assertTeamAdmin)({
47
52
  req,
48
53
  auth,
@@ -56,6 +61,16 @@ function registerTeamsRoutes(router, ctx) {
56
61
  res.status(check.status).json({ error: check.error });
57
62
  return;
58
63
  }
64
+ // createGroups narrows creation to a subset of the admin group.
65
+ if (teamAdminCfg.createGroups.length > 0) {
66
+ const memberships = await Promise.all(teamAdminCfg.createGroups.map(group => (0, provisioning_1.isUserMemberOfGroup)(check.userEntityRef, group, catalogClient, auth, logger)));
67
+ if (!memberships.some(Boolean)) {
68
+ res.status(403).json({
69
+ error: `Access denied: team creation is limited to ${teamAdminCfg.createGroups.join(', ')}`,
70
+ });
71
+ return;
72
+ }
73
+ }
59
74
  const v = (0, teamAdmin_1.validateTeamWriteInput)(req.body ?? {}, teamAdminCfg);
60
75
  if (!v.ok) {
61
76
  res.status(400).json({ error: v.error });
@@ -67,7 +82,7 @@ function registerTeamsRoutes(router, ctx) {
67
82
  logger.info(`Team creation already in flight for ${key} — joining`);
68
83
  try {
69
84
  const result = await pending;
70
- res.json(result);
85
+ (0, respondTeam_1.respondTeam)(res, result, ctx, 'manager');
71
86
  }
72
87
  catch (err) {
73
88
  sendTeamError(err, res);
@@ -100,10 +115,7 @@ function registerTeamsRoutes(router, ctx) {
100
115
  teamFetch_1.teamCreateInFlight.set(key, createPromise);
101
116
  try {
102
117
  const result = await createPromise;
103
- // Manager surface: redact dollars when hidden even from managers.
104
- res.json(teamBudgetVisibility.hideTeamBudgetForManagers
105
- ? (0, teamBudgetVisibility_1.redactTeamBudget)(result)
106
- : result);
118
+ (0, respondTeam_1.respondTeam)(res, result, ctx, 'manager');
107
119
  }
108
120
  catch (err) {
109
121
  sendTeamError(err, res);
@@ -150,9 +162,7 @@ function registerTeamsRoutes(router, ctx) {
150
162
  teamId,
151
163
  owningGroup,
152
164
  });
153
- res.json(teamBudgetVisibility.hideTeamBudgetForManagers
154
- ? (0, teamBudgetVisibility_1.redactTeamBudget)(r)
155
- : r);
165
+ (0, respondTeam_1.respondTeam)(res, r, ctx, 'manager');
156
166
  }
157
167
  catch (err) {
158
168
  sendTeamError(err, res);
@@ -224,10 +234,7 @@ function registerTeamsRoutes(router, ctx) {
224
234
  // the caller's group set and filter with includes().
225
235
  const owned = all.filter(t => typeof t.metadata?.owning_group === 'string' &&
226
236
  t.metadata.owning_group === teamAdminCfg.group);
227
- // Manager surface: strip dollar amounts when hidden even from managers.
228
- res.json(teamBudgetVisibility.hideTeamBudgetForManagers
229
- ? owned.map(teamBudgetVisibility_1.redactTeamBudget)
230
- : owned);
237
+ (0, respondTeam_1.respondTeamList)(res, owned, ctx, 'manager');
231
238
  }
232
239
  catch (err) {
233
240
  sendTeamError(err, res);
@@ -240,10 +247,10 @@ function registerTeamsRoutes(router, ctx) {
240
247
  // provision them on the spot when auto-provisioning is enabled. Only the
241
248
  // 'user' team role can be assigned from Backstage.
242
249
  router.post('/teams/:teamId/members', async (req, res) => {
243
- const authz = await authorizeTeamSubresource(req, res, backstage_plugin_litellm_common_1.litellmTeamMembersManagePermission);
250
+ const authz = await authorizeTeamSubresource(req, res, backstage_plugin_litellm_common_1.litellmTeamMembersManagePermission, { allowTeamRole: true });
244
251
  if (!authz)
245
252
  return;
246
- const { teamId, owningGroup, actor } = authz;
253
+ const { teamId, owningGroup, actor, via } = authz;
247
254
  const body = (req.body ?? {});
248
255
  const userEntityRef = (body.userEntityRef ?? '').trim();
249
256
  if (!userEntityRef) {
@@ -267,6 +274,12 @@ function registerTeamsRoutes(router, ctx) {
267
274
  return;
268
275
  }
269
276
  maxBudgetInTeam = body.maxBudgetInTeam;
277
+ if (via === 'teamRole') {
278
+ res.status(403).json({
279
+ error: 'Team member managers cannot set member budgets',
280
+ });
281
+ return;
282
+ }
270
283
  }
271
284
  // (a) the member must be a real User in the Backstage catalog
272
285
  try {
@@ -318,11 +331,10 @@ function registerTeamsRoutes(router, ctx) {
318
331
  teamId,
319
332
  member: litellmUserId,
320
333
  owningGroup,
334
+ via,
321
335
  });
322
336
  const updated = await (0, teamFetch_1.withTeamFetchRetry)(() => client.getTeamInfo(teamId));
323
- res.json(teamBudgetVisibility.hideTeamBudgetForManagers
324
- ? (0, teamBudgetVisibility_1.redactTeamBudget)(updated)
325
- : updated);
337
+ (0, respondTeam_1.respondTeam)(res, updated, ctx, 'manager');
326
338
  }
327
339
  catch (err) {
328
340
  sendTeamError(err, res);
@@ -332,10 +344,10 @@ function registerTeamsRoutes(router, ctx) {
332
344
  // from the `userEntityRef` query param (entity refs contain ':' and '/', so
333
345
  // they can't be a path segment). No catalog re-check on removal.
334
346
  router.delete('/teams/:teamId/members', async (req, res) => {
335
- const authz = await authorizeTeamSubresource(req, res, backstage_plugin_litellm_common_1.litellmTeamMembersManagePermission);
347
+ const authz = await authorizeTeamSubresource(req, res, backstage_plugin_litellm_common_1.litellmTeamMembersManagePermission, { allowTeamRole: true });
336
348
  if (!authz)
337
349
  return;
338
- const { teamId, owningGroup, actor } = authz;
350
+ const { teamId, owningGroup, actor, team, via } = authz;
339
351
  const userEntityRef = String(req.query.userEntityRef ?? '').trim();
340
352
  if (!userEntityRef) {
341
353
  res
@@ -344,6 +356,21 @@ function registerTeamsRoutes(router, ctx) {
344
356
  return;
345
357
  }
346
358
  const litellmUserId = (0, provisioning_1.toLiteLLMUserId)(userEntityRef, userIdDomain);
359
+ // Team-role managers change membership only: they cannot remove
360
+ // themselves nor a peer holding a manager role.
361
+ if (via === 'teamRole') {
362
+ if (litellmUserId === (0, provisioning_1.toLiteLLMUserId)(actor, userIdDomain)) {
363
+ res.status(400).json({ error: 'You cannot remove yourself from the team' });
364
+ return;
365
+ }
366
+ const role = team.members_with_roles?.find(m => m.user_id === litellmUserId)?.role;
367
+ if (role && teamAdminCfg.memberManagerRoles.includes(role)) {
368
+ res.status(403).json({
369
+ error: `Only platform team admins can remove a team ${role}`,
370
+ });
371
+ return;
372
+ }
373
+ }
347
374
  try {
348
375
  await client.teamMemberDelete({
349
376
  team_id: teamId,
@@ -354,11 +381,10 @@ function registerTeamsRoutes(router, ctx) {
354
381
  teamId,
355
382
  member: litellmUserId,
356
383
  owningGroup,
384
+ via,
357
385
  });
358
386
  const updated = await (0, teamFetch_1.withTeamFetchRetry)(() => client.getTeamInfo(teamId));
359
- res.json(teamBudgetVisibility.hideTeamBudgetForManagers
360
- ? (0, teamBudgetVisibility_1.redactTeamBudget)(updated)
361
- : updated);
387
+ (0, respondTeam_1.respondTeam)(res, updated, ctx, 'manager');
362
388
  }
363
389
  catch (err) {
364
390
  sendTeamError(err, res);
@@ -1,20 +1,23 @@
1
1
  /**
2
- * Key generation service — unified by PR-6.
2
+ * Key generation service.
3
3
  *
4
4
  * Extracts the common logic for creating API keys from both the UI (POST /keys/generate)
5
- * and the bridge (POST /bridge/keys). Handles validation, provisioning, team/model checks,
6
- * and explicit upstream request construction.
5
+ * and the bridge (POST /bridge/keys). Handles validation, team/model checks,
6
+ * and explicit upstream request construction. Callers resolve the user beforehand.
7
7
  */
8
8
  import { AuthService, LoggerService } from '@backstage/backend-plugin-api';
9
9
  import { CatalogClient } from '@backstage/catalog-client';
10
10
  import { LiteLLMClient } from '../client';
11
- import { GenerateKeyResponse, ProvisioningDefaults, RoleConfig } from '../types';
11
+ import { GenerateKeyResponse, UserInfo } from '../types';
12
12
  import { type KeyValidationConfig, type GenerateKeyInput } from '@acarmisc/backstage-plugin-litellm-common';
13
+ /** User identity for key creation — already resolved by the caller. */
13
14
  export interface KeyCreateUser {
14
15
  /** Backstage user entity ref, e.g. "user:default/alice" (may be empty for bridge). */
15
16
  tokenEntityRef?: string;
16
- /** Resolved LiteLLM user_id. Required. */
17
- userId: string;
17
+ /** Resolved user info from LiteLLM. Required. */
18
+ userInfo: UserInfo;
19
+ /** How the key was created (for metadata stamping). Defaults to 'backstage'. */
20
+ createdVia?: string;
18
21
  }
19
22
  /** Context needed to create a key — configuration, clients, and services. */
20
23
  export interface KeyCreateContext {
@@ -27,10 +30,6 @@ export interface KeyCreateContext {
27
30
  auth?: AuthService;
28
31
  logger: LoggerService;
29
32
  keyValidationConfig: KeyValidationConfig;
30
- provisioningEnabled: boolean;
31
- provisioningDefaults: ProvisioningDefaults;
32
- roleConfigs: RoleConfig[];
33
- userIdDomain?: string;
34
33
  }
35
34
  /** Error thrown during key creation — maps to HTTP responses. */
36
35
  export declare class KeyServiceError extends Error {
@@ -44,18 +43,16 @@ export declare class KeyServiceError extends Error {
44
43
  * Performs:
45
44
  * - Schema validation of the input
46
45
  * - Config flag enforcement (allowUnlimitedBudget, teamRequired)
47
- * - User provisioning (single-flight, role configs)
48
46
  * - Team membership validation
49
47
  * - Model subset validation
50
48
  * - Metadata stamping
51
49
  * - Explicit upstream request construction and call
52
50
  *
53
- * @param user Verified identity: tokenEntityRef (optional) + userId (required)
51
+ * @param user Resolved identity: tokenEntityRef (optional) + userInfo (required, pre-provisioned)
54
52
  * @param input Parsed GenerateKeyInput from the schema
55
53
  * @param ctx Configuration and service clients
56
54
  * @returns The new key response from LiteLLM
57
55
  * @throws KeyServiceError with status and body for HTTP response
58
- * @throws ProvisioningError for provisioning failures
59
56
  * @throws LiteLLMUpstreamError for upstream LiteLLM errors
60
57
  */
61
58
  export declare function createKeyForUser(user: KeyCreateUser, input: GenerateKeyInput, ctx: KeyCreateContext): Promise<GenerateKeyResponse>;
@@ -1,16 +1,15 @@
1
1
  "use strict";
2
2
  /**
3
- * Key generation service — unified by PR-6.
3
+ * Key generation service.
4
4
  *
5
5
  * Extracts the common logic for creating API keys from both the UI (POST /keys/generate)
6
- * and the bridge (POST /bridge/keys). Handles validation, provisioning, team/model checks,
7
- * and explicit upstream request construction.
6
+ * and the bridge (POST /bridge/keys). Handles validation, team/model checks,
7
+ * and explicit upstream request construction. Callers resolve the user beforehand.
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.KeyServiceError = void 0;
11
11
  exports.createKeyForUser = createKeyForUser;
12
12
  const provisioning_1 = require("../provisioning");
13
- /** Unified identity for key creation — resolved from either Backstage or JWT claims. */
14
13
  const modelAccess_1 = require("./modelAccess");
15
14
  /** Error thrown during key creation — maps to HTTP responses. */
16
15
  class KeyServiceError extends Error {
@@ -27,57 +26,21 @@ exports.KeyServiceError = KeyServiceError;
27
26
  * Performs:
28
27
  * - Schema validation of the input
29
28
  * - Config flag enforcement (allowUnlimitedBudget, teamRequired)
30
- * - User provisioning (single-flight, role configs)
31
29
  * - Team membership validation
32
30
  * - Model subset validation
33
31
  * - Metadata stamping
34
32
  * - Explicit upstream request construction and call
35
33
  *
36
- * @param user Verified identity: tokenEntityRef (optional) + userId (required)
34
+ * @param user Resolved identity: tokenEntityRef (optional) + userInfo (required, pre-provisioned)
37
35
  * @param input Parsed GenerateKeyInput from the schema
38
36
  * @param ctx Configuration and service clients
39
37
  * @returns The new key response from LiteLLM
40
38
  * @throws KeyServiceError with status and body for HTTP response
41
- * @throws ProvisioningError for provisioning failures
42
39
  * @throws LiteLLMUpstreamError for upstream LiteLLM errors
43
40
  */
44
41
  async function createKeyForUser(user, input, ctx) {
45
- const { client, config, catalogClient, auth, logger, provisioningEnabled, provisioningDefaults, roleConfigs, } = ctx;
46
- // ── Ensure user is provisioned ──────────────────────────────────────
47
- // For bridge (no catalogClient/auth), use direct provisioning from JWT;
48
- // for UI (with catalogClient/auth), use full provisioning with group roles.
49
- let userInfo;
50
- if (catalogClient !== undefined && catalogClient !== null && auth !== undefined && auth !== null) {
51
- userInfo = await (0, provisioning_1.getOrProvisionUser)(client, user.tokenEntityRef || '', user.userId, provisioningEnabled, provisioningDefaults, roleConfigs, catalogClient, auth, logger);
52
- }
53
- else {
54
- // Bridge path: just check if user exists, or provision with minimal info
55
- userInfo = await client.getUserInfo(user.userId);
56
- if (!userInfo) {
57
- if (!provisioningEnabled) {
58
- throw new KeyServiceError(404, {
59
- error: 'User not found in LiteLLM',
60
- detail: 'No LiteLLM user for this identity. Log in to Backstage once to be provisioned, or enable litellm.provisioning.enabled.',
61
- });
62
- }
63
- // Provision with minimal info (just user_id, will use defaults)
64
- const created = await client.createUser({
65
- user_id: user.userId,
66
- user_email: user.userId,
67
- max_budget: provisioningDefaults.maxBudget,
68
- budget_duration: provisioningDefaults.budgetDuration,
69
- models: provisioningDefaults.models,
70
- teams: provisioningDefaults.teams?.length ? [provisioningDefaults.teams[0]] : [],
71
- });
72
- if (!created) {
73
- throw new KeyServiceError(500, {
74
- error: 'User provisioning failed',
75
- detail: 'Failed to create user in LiteLLM',
76
- });
77
- }
78
- userInfo = await client.getUserInfo(user.userId);
79
- }
80
- }
42
+ const { client, config, catalogClient, auth, logger, } = ctx;
43
+ const userInfo = user.userInfo;
81
44
  // ── Enforce config flags ────────────────────────────────────────────
82
45
  if (!config.allowUnlimitedBudget && (input.max_budget === null || input.max_budget === undefined)) {
83
46
  throw new KeyServiceError(400, { error: 'max_budget is required when unlimited budgets are not allowed' });
@@ -127,13 +90,15 @@ async function createKeyForUser(user, input, ctx) {
127
90
  ? await (0, provisioning_1.resolveUserProfile)(user.tokenEntityRef, catalogClient, auth, logger)
128
91
  : {};
129
92
  const clientMetadata = input.metadata ?? {};
93
+ const createdVia = user.createdVia ?? 'backstage';
130
94
  const serverMetadata = {
131
- created_by_backstage_user: user.tokenEntityRef ?? 'unknown',
95
+ ...(user.tokenEntityRef && { created_by_backstage_user: user.tokenEntityRef }),
96
+ ...(!user.tokenEntityRef && { created_by: userInfo.user_id }),
132
97
  ...(profile.email && { created_by_email: profile.email }),
133
98
  ...(profile.displayName && {
134
99
  created_by_display_name: profile.displayName,
135
100
  }),
136
- created_via: 'backstage',
101
+ created_via: createdVia,
137
102
  created_at_iso: new Date().toISOString(),
138
103
  };
139
104
  // Server-owned keys override client values
@@ -145,7 +110,7 @@ async function createKeyForUser(user, input, ctx) {
145
110
  // Never spread the raw body; only include fields we've explicitly validated.
146
111
  const upstreamRequest = {
147
112
  alias: input.alias,
148
- user_id: user.userId,
113
+ user_id: userInfo.user_id,
149
114
  };
150
115
  if (input.models !== undefined) {
151
116
  upstreamRequest.models = input.models;
@@ -174,6 +139,6 @@ async function createKeyForUser(user, input, ctx) {
174
139
  upstreamRequest.metadata = enrichedMetadata;
175
140
  // ── Call upstream LiteLLM ───────────────────────────────────────────
176
141
  const result = await client.generateKey(upstreamRequest);
177
- logger.info('key.generate', { userId: user.userId, keyAlias: input.alias });
142
+ logger.info('key.generate', { userId: userInfo.user_id, keyAlias: input.alias });
178
143
  return result;
179
144
  }
@@ -61,6 +61,30 @@ export interface TeamAdminConfig {
61
61
  * Allow an admin to delete a team (vs. only block/deactivate).
62
62
  */
63
63
  allowTeamDelete: boolean;
64
+ /**
65
+ * When true, all team write operations return 403 even for admins:
66
+ * POST /teams, PATCH /teams/:id, DELETE /teams/:id,
67
+ * POST/DELETE /teams/:id/members, and PUT objectPermissions routes.
68
+ * Reads (GET /teams, GET /teams/managed) keep working.
69
+ * Fail-closed: defaults to false (writes allowed).
70
+ */
71
+ readOnly: boolean;
72
+ /**
73
+ * Backstage group refs that must include the caller for team creation.
74
+ * Empty => no group restriction, admin group check alone suffices.
75
+ * When non-empty, POST /teams additionally requires the caller to be
76
+ * a member of at least one of these groups, narrowing the permission.
77
+ * Fail-closed: defaults to [] (no extra restriction).
78
+ */
79
+ createGroups: string[];
80
+ /**
81
+ * LiteLLM team roles whose holders may add/remove members of their own team.
82
+ * Empty => feature off, only admins can manage members.
83
+ * When non-empty, callers with one of these roles in members_with_roles
84
+ * can POST/DELETE /teams/:teamId/members without owning the team's group.
85
+ * Fail-closed: defaults to [] (off).
86
+ */
87
+ memberManagerRoles: string[];
64
88
  }
65
89
  /**
66
90
  * Whether team-management routes should be mounted.
package/dist/teamAdmin.js CHANGED
@@ -112,6 +112,9 @@ function readTeamAdminConfig(config) {
112
112
  allowedMcpServers: config.getOptionalStringArray('litellm.teamAdmin.allowedMcpServers') ?? [],
113
113
  allowedMcpAccessGroups: config.getOptionalStringArray('litellm.teamAdmin.allowedMcpAccessGroups') ?? [],
114
114
  allowTeamDelete: config.getOptionalBoolean('litellm.teamAdmin.allowTeamDelete') ?? false,
115
+ readOnly: config.getOptionalBoolean('litellm.teamAdmin.readOnly') ?? false,
116
+ createGroups: config.getOptionalStringArray('litellm.teamAdmin.createGroups') ?? [],
117
+ memberManagerRoles: config.getOptionalStringArray('litellm.teamAdmin.memberManagerRoles') ?? [],
115
118
  };
116
119
  }
117
120
  function validateTeamWriteInput(input, cfg) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/types.ts"],
4
- "sourcesContent": ["// Re-export common types for backward compatibility\nexport {\n type UserInfo,\n type TeamMember,\n type TeamObjectPermission,\n type TeamInfo,\n type CreateTeamResponse,\n type VirtualKey,\n type ModelInfo,\n type UsageModelBreakdown,\n type UsageKeyBreakdown,\n type UsageDailyPoint,\n type UsageDailyModelPoint,\n type UsageMetrics,\n type GenerateKeyRequest,\n type UpdateKeyRequest,\n type GenerateKeyResponse,\n type AuditLogEntry,\n type PaginatedAuditLogs,\n} from '@acarmisc/backstage-plugin-litellm-common';\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?: {\n vector_stores?: string[];\n mcp_servers?: string[];\n mcp_access_groups?: string[];\n };\n}\n\nexport interface UpdateTeamRequest extends Partial<CreateTeamRequest> {\n team_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 /** Arbitrary metadata stored on the key record. */\n metadata?: Record<string, unknown> | null;\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\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"],
4
+ "sourcesContent": ["// Re-export common types for backward compatibility\nexport {\n type UserInfo,\n type TeamMember,\n type TeamObjectPermission,\n type TeamInfo,\n type CreateTeamResponse,\n type VirtualKey,\n type ModelInfo,\n type UsageModelBreakdown,\n type UsageKeyBreakdown,\n type UsageDailyPoint,\n type UsageDailyModelPoint,\n type UsageMetrics,\n type GenerateKeyRequest,\n type UpdateKeyRequest,\n type GenerateKeyResponse,\n type AuditLogEntry,\n type PaginatedAuditLogs,\n} from '@acarmisc/backstage-plugin-litellm-common';\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?: {\n vector_stores?: string[];\n mcp_servers?: string[];\n mcp_access_groups?: string[];\n };\n}\n\nexport interface UpdateTeamRequest extends Partial<CreateTeamRequest> {\n team_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 /** Arbitrary metadata stored on the key record. */\n metadata?: Record<string, unknown> | null;\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\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
  }
package/dist/types.d.ts CHANGED
@@ -96,42 +96,6 @@ export interface CreateUserResponse {
96
96
  models?: string[];
97
97
  teams?: string[];
98
98
  }
99
- /** One row from LiteLLM's `/spend/logs` (per-request spend log entry). */
100
- export interface SpendLogEntry {
101
- request_id?: string;
102
- /** ISO timestamp of the call. */
103
- startTime?: string;
104
- endTime?: string;
105
- spend?: number;
106
- total_tokens?: number;
107
- prompt_tokens?: number;
108
- completion_tokens?: number;
109
- model?: string;
110
- /** Virtual key alias or hash the request was billed to. */
111
- api_key?: string;
112
- user?: string;
113
- team_id?: string;
114
- /**
115
- * Tags attached to the request (e.g. `channel:backstage`,
116
- * `session:<thread>`, `invoked-by:<user>`). Shape varies across LiteLLM
117
- * versions (array or object) and is normalised to a string[].
118
- */
119
- request_tags?: string[] | Record<string, string>;
120
- /** LiteLLM `metadata` blob; carries trace ids and session grouping. */
121
- metadata?: Record<string, unknown> | string | null;
122
- }
123
- export interface SpendLogsParams {
124
- /** ISO date `YYYY-MM-DD` (inclusive). */
125
- start_date: string;
126
- /** ISO date `YYYY-MM-DD` (inclusive). */
127
- end_date: string;
128
- /** Restrict to the given virtual key. */
129
- api_key?: string;
130
- user_id?: string;
131
- team_id?: string;
132
- /** Max rows to return (LiteLLM caps at 1000 per page). */
133
- page_size?: number;
134
- }
135
99
  export interface AuditLogsParams {
136
100
  page?: number;
137
101
  page_size?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acarmisc/backstage-plugin-litellm-backend",
3
- "version": "0.17.7",
3
+ "version": "0.19.0",
4
4
  "description": "The Backstage backend plugin for LiteLLM governance",
5
5
  "backstage": {
6
6
  "role": "backend-plugin",
@@ -32,21 +32,17 @@
32
32
  "lint": "eslint src --ext .ts",
33
33
  "test": "tsc -p tsconfig.test.json && node --test \"dist-test/**/*.test.js\"",
34
34
  "api-report": "npm run build && api-extractor run --local --verbose",
35
- "prepack": "npm run build",
36
- "postpack": ""
35
+ "prepack": "npm run build"
37
36
  },
38
37
  "dependencies": {
39
38
  "@acarmisc/backstage-plugin-litellm-common": "0.1.0",
40
39
  "@backstage/backend-plugin-api": "^1.9.1",
41
40
  "@backstage/catalog-client": "^1.9.0",
42
- "@backstage/catalog-model": "^1.7.0",
43
41
  "@backstage/config": "^1.3.0",
44
42
  "@backstage/errors": "^1.3.0",
45
43
  "@backstage/plugin-permission-common": "^0.9.10",
46
- "@backstage/types": "^1.2.0",
47
44
  "express": "^4.18.2",
48
- "jose": "^5.9.6",
49
- "zod": "^3.22.0"
45
+ "jose": "^5.9.6"
50
46
  },
51
47
  "devDependencies": {
52
48
  "@backstage/cli": "^0.36.2",