@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.
- package/README.md +58 -0
- package/config.d.ts +28 -0
- package/dist/bridge.d.ts +4 -6
- package/dist/bridge.js +10 -28
- package/dist/client.d.ts +7 -20
- package/dist/client.js +29 -107
- package/dist/errors.d.ts +2 -1
- package/dist/errors.js +4 -1
- package/dist/index.cjs.js +366 -480
- package/dist/index.cjs.js.map +4 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -3
- package/dist/provisioning.d.ts +2 -0
- package/dist/provisioning.js +11 -20
- package/dist/router.js +56 -34
- package/dist/routes/bridge.js +10 -19
- package/dist/routes/config.js +2 -0
- package/dist/routes/context.d.ts +4 -2
- package/dist/routes/keys.js +8 -21
- package/dist/routes/middleware/withUser.d.ts +3 -2
- package/dist/routes/middleware/withUser.js +1 -1
- package/dist/routes/objectPermissions.js +4 -8
- package/dist/routes/teamUsage.js +2 -3
- package/dist/routes/teams.js +50 -24
- package/dist/services/keyService.d.ts +10 -13
- package/dist/services/keyService.js +12 -47
- package/dist/teamAdmin.d.ts +24 -0
- package/dist/teamAdmin.js +3 -0
- package/dist/types.cjs.js.map +1 -1
- package/dist/types.d.ts +0 -36
- package/package.json +3 -7
package/dist/routes/teams.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
17
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
46
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
142
|
+
logger.info('key.generate', { userId: userInfo.user_id, keyAlias: input.alias });
|
|
178
143
|
return result;
|
|
179
144
|
}
|
package/dist/teamAdmin.d.ts
CHANGED
|
@@ -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) {
|
package/dist/types.cjs.js.map
CHANGED
|
@@ -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
|
|
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.
|
|
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",
|