@acarmisc/backstage-plugin-litellm-backend 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config.d.ts +101 -0
- package/dist/client.d.ts +61 -1
- package/dist/client.js +137 -1
- package/dist/index.cjs.js +931 -11
- package/dist/index.cjs.js.map +4 -4
- package/dist/index.d.ts +3 -1
- package/dist/index.js +13 -1
- package/dist/permissions.d.ts +6 -0
- package/dist/permissions.js +32 -1
- package/dist/router.d.ts +3 -0
- package/dist/router.js +594 -1
- package/dist/teamAdmin.d.ts +150 -0
- package/dist/teamAdmin.js +235 -0
- package/dist/types.cjs.js.map +1 -1
- package/dist/types.d.ts +34 -0
- package/package.json +1 -1
package/dist/index.cjs.js
CHANGED
|
@@ -38,7 +38,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
38
38
|
var require_api_cjs = __commonJS({
|
|
39
39
|
"node_modules/@backstage/plugin-permission-common/dist/types/api.cjs.js"(exports2) {
|
|
40
40
|
"use strict";
|
|
41
|
-
var
|
|
41
|
+
var AuthorizeResult3 = {
|
|
42
42
|
/**
|
|
43
43
|
* The authorization request is denied.
|
|
44
44
|
*/
|
|
@@ -52,7 +52,7 @@ var require_api_cjs = __commonJS({
|
|
|
52
52
|
*/
|
|
53
53
|
CONDITIONAL: "CONDITIONAL"
|
|
54
54
|
};
|
|
55
|
-
exports2.AuthorizeResult =
|
|
55
|
+
exports2.AuthorizeResult = AuthorizeResult3;
|
|
56
56
|
}
|
|
57
57
|
});
|
|
58
58
|
|
|
@@ -7976,28 +7976,39 @@ __export(index_exports, {
|
|
|
7976
7976
|
LiteLLMClient: () => LiteLLMClient,
|
|
7977
7977
|
ProvisioningError: () => ProvisioningError,
|
|
7978
7978
|
applyRoleOverrides: () => applyRoleOverrides,
|
|
7979
|
+
assertTeamAdmin: () => assertTeamAdmin,
|
|
7979
7980
|
bridgeGenerateKey: () => bridgeGenerateKey,
|
|
7980
7981
|
bridgeListKeys: () => bridgeListKeys,
|
|
7981
7982
|
createRouter: () => createRouter,
|
|
7982
7983
|
default: () => litellmPlugin,
|
|
7983
7984
|
getOrProvisionUser: () => getOrProvisionUser,
|
|
7984
7985
|
getOrProvisionUserFromClaims: () => getOrProvisionUserFromClaims,
|
|
7986
|
+
isTeamManagementEnabled: () => isTeamManagementEnabled,
|
|
7985
7987
|
litellmAuditReadPermission: () => litellmAuditReadPermission,
|
|
7986
7988
|
litellmKeyCreatePermission: () => litellmKeyCreatePermission,
|
|
7987
7989
|
litellmKeyManagePermission: () => litellmKeyManagePermission,
|
|
7988
7990
|
litellmKeyRevokePermission: () => litellmKeyRevokePermission,
|
|
7989
7991
|
litellmPermissions: () => litellmPermissions,
|
|
7990
7992
|
litellmPlugin: () => litellmPlugin,
|
|
7993
|
+
litellmTeamCreatePermission: () => litellmTeamCreatePermission,
|
|
7994
|
+
litellmTeamDeletePermission: () => litellmTeamDeletePermission,
|
|
7995
|
+
litellmTeamKnowledgebaseManagePermission: () => litellmTeamKnowledgebaseManagePermission,
|
|
7996
|
+
litellmTeamManagePermission: () => litellmTeamManagePermission,
|
|
7997
|
+
litellmTeamMcpManagePermission: () => litellmTeamMcpManagePermission,
|
|
7998
|
+
litellmTeamMembersManagePermission: () => litellmTeamMembersManagePermission,
|
|
7991
7999
|
newDefaultVerifier: () => newDefaultVerifier,
|
|
7992
8000
|
provisionUser: () => provisionUser,
|
|
7993
8001
|
readBridgeConfig: () => readBridgeConfig,
|
|
7994
8002
|
readProvisioningDefaults: () => readProvisioningDefaults,
|
|
7995
8003
|
readRoleConfigs: () => readRoleConfigs,
|
|
8004
|
+
readTeamAdminConfig: () => readTeamAdminConfig,
|
|
7996
8005
|
resolveBridgeUserId: () => resolveBridgeUserId,
|
|
7997
8006
|
resolveUserId: () => resolveUserId,
|
|
7998
8007
|
resolveUserProfile: () => resolveUserProfile,
|
|
7999
8008
|
resolveUserRole: () => resolveUserRole,
|
|
8000
|
-
toLiteLLMUserId: () => toLiteLLMUserId
|
|
8009
|
+
toLiteLLMUserId: () => toLiteLLMUserId,
|
|
8010
|
+
validateTeamPatchInput: () => validateTeamPatchInput,
|
|
8011
|
+
validateTeamWriteInput: () => validateTeamWriteInput
|
|
8001
8012
|
});
|
|
8002
8013
|
module.exports = __toCommonJS(index_exports);
|
|
8003
8014
|
|
|
@@ -8006,7 +8017,7 @@ var import_backend_plugin_api = require("@backstage/backend-plugin-api");
|
|
|
8006
8017
|
|
|
8007
8018
|
// src/router.ts
|
|
8008
8019
|
var import_express = __toESM(require("express"));
|
|
8009
|
-
var
|
|
8020
|
+
var import_plugin_permission_common3 = __toESM(require_index_cjs2());
|
|
8010
8021
|
var import_catalog_client = require("@backstage/catalog-client");
|
|
8011
8022
|
|
|
8012
8023
|
// src/client.ts
|
|
@@ -8276,18 +8287,141 @@ var LiteLLMClient = class {
|
|
|
8276
8287
|
const raw = await this.request(
|
|
8277
8288
|
`/team/info?team_id=${encodeURIComponent(teamId)}`
|
|
8278
8289
|
);
|
|
8290
|
+
return this.toTeamInfo(raw, teamId);
|
|
8291
|
+
}
|
|
8292
|
+
/**
|
|
8293
|
+
* Normalises a single team row into the TeamInfo contract. Tolerates both
|
|
8294
|
+
* the `/team/info` shape (row wrapped in a `team_info` envelope alongside
|
|
8295
|
+
* sibling arrays) and a bare row as returned per-item by `/team/list`.
|
|
8296
|
+
* `metadata` is always surfaced so callers can read `owning_group`.
|
|
8297
|
+
*/
|
|
8298
|
+
toTeamInfo(raw, fallbackId) {
|
|
8279
8299
|
const inner = raw?.team_info ?? {};
|
|
8280
8300
|
return {
|
|
8281
|
-
team_id: raw?.team_id ?? inner.team_id ??
|
|
8301
|
+
team_id: raw?.team_id ?? inner.team_id ?? fallbackId ?? "",
|
|
8282
8302
|
team_alias: inner.team_alias ?? raw?.team_alias,
|
|
8283
8303
|
max_budget: inner.max_budget ?? raw?.max_budget,
|
|
8284
8304
|
spend: inner.spend ?? raw?.spend ?? 0,
|
|
8285
8305
|
members_with_roles: inner.members_with_roles ?? raw?.members_with_roles,
|
|
8286
8306
|
models: inner.models ?? raw?.models,
|
|
8287
8307
|
tpm_limit: inner.tpm_limit ?? raw?.tpm_limit,
|
|
8288
|
-
rpm_limit: inner.rpm_limit ?? raw?.rpm_limit
|
|
8308
|
+
rpm_limit: inner.rpm_limit ?? raw?.rpm_limit,
|
|
8309
|
+
metadata: inner.metadata ?? raw?.metadata,
|
|
8310
|
+
object_permission: inner.object_permission ?? raw?.object_permission,
|
|
8311
|
+
blocked: inner.blocked ?? raw?.blocked,
|
|
8312
|
+
team_member_budget: inner.team_member_budget ?? raw?.team_member_budget
|
|
8289
8313
|
};
|
|
8290
8314
|
}
|
|
8315
|
+
/**
|
|
8316
|
+
* Lists every team known to the LiteLLM proxy. Tolerates the observed
|
|
8317
|
+
* response shapes — a bare array, `{ teams: [...] }`, or `{ data: [...] }` —
|
|
8318
|
+
* and normalises each row through the same unwrap as `getTeamInfo`.
|
|
8319
|
+
*
|
|
8320
|
+
* This returns the GLOBAL team list and must never be exposed directly to
|
|
8321
|
+
* end users; callers are responsible for scoping the result (e.g. to teams
|
|
8322
|
+
* whose `metadata.owning_group` matches the caller's admin group).
|
|
8323
|
+
*/
|
|
8324
|
+
async listTeams() {
|
|
8325
|
+
const raw = await this.request("/team/list");
|
|
8326
|
+
let rows = [];
|
|
8327
|
+
if (Array.isArray(raw)) rows = raw;
|
|
8328
|
+
else if (Array.isArray(raw?.teams)) rows = raw.teams;
|
|
8329
|
+
else if (Array.isArray(raw?.data)) rows = raw.data;
|
|
8330
|
+
return rows.map((row) => this.toTeamInfo(row));
|
|
8331
|
+
}
|
|
8332
|
+
async createTeam(payload) {
|
|
8333
|
+
return this.request("/team/new", {
|
|
8334
|
+
method: "POST",
|
|
8335
|
+
body: JSON.stringify(payload)
|
|
8336
|
+
});
|
|
8337
|
+
}
|
|
8338
|
+
async updateTeam(payload) {
|
|
8339
|
+
return this.request("/team/update", {
|
|
8340
|
+
method: "POST",
|
|
8341
|
+
body: JSON.stringify(payload)
|
|
8342
|
+
});
|
|
8343
|
+
}
|
|
8344
|
+
async deleteTeam(teamId) {
|
|
8345
|
+
return this.request("/team/delete", {
|
|
8346
|
+
method: "POST",
|
|
8347
|
+
body: JSON.stringify({ team_ids: [teamId] })
|
|
8348
|
+
});
|
|
8349
|
+
}
|
|
8350
|
+
async blockTeam(teamId) {
|
|
8351
|
+
return this.request("/team/block", {
|
|
8352
|
+
method: "POST",
|
|
8353
|
+
body: JSON.stringify({ team_id: teamId })
|
|
8354
|
+
});
|
|
8355
|
+
}
|
|
8356
|
+
async unblockTeam(teamId) {
|
|
8357
|
+
return this.request("/team/unblock", {
|
|
8358
|
+
method: "POST",
|
|
8359
|
+
body: JSON.stringify({ team_id: teamId })
|
|
8360
|
+
});
|
|
8361
|
+
}
|
|
8362
|
+
/**
|
|
8363
|
+
* Adds a member to a team. LiteLLM's `/team/member_add` nests the member
|
|
8364
|
+
* under a `member` object; `role` is 'user' or 'admin' (we only ever send
|
|
8365
|
+
* 'user' from Backstage). `max_budget_in_team` optionally caps that member's
|
|
8366
|
+
* spend within the team.
|
|
8367
|
+
*/
|
|
8368
|
+
async teamMemberAdd(payload) {
|
|
8369
|
+
return this.request("/team/member_add", {
|
|
8370
|
+
method: "POST",
|
|
8371
|
+
body: JSON.stringify({
|
|
8372
|
+
team_id: payload.team_id,
|
|
8373
|
+
member: { user_id: payload.user_id, role: payload.role ?? "user" },
|
|
8374
|
+
...payload.max_budget_in_team !== void 0 && {
|
|
8375
|
+
max_budget_in_team: payload.max_budget_in_team
|
|
8376
|
+
}
|
|
8377
|
+
})
|
|
8378
|
+
});
|
|
8379
|
+
}
|
|
8380
|
+
/** Removes a member from a team via LiteLLM's `/team/member_delete`. */
|
|
8381
|
+
async teamMemberDelete(payload) {
|
|
8382
|
+
return this.request("/team/member_delete", {
|
|
8383
|
+
method: "POST",
|
|
8384
|
+
body: JSON.stringify({
|
|
8385
|
+
team_id: payload.team_id,
|
|
8386
|
+
user_id: payload.user_id
|
|
8387
|
+
})
|
|
8388
|
+
});
|
|
8389
|
+
}
|
|
8390
|
+
/**
|
|
8391
|
+
* Lists the vector stores (knowledge bases) registered on the LiteLLM proxy.
|
|
8392
|
+
* Tolerates a bare array, `{ data: [...] }`, or `{ vector_stores: [...] }`.
|
|
8393
|
+
* Each entry is normalised to `{ id, name? }` — `id` prefers
|
|
8394
|
+
* `vector_store_id` then `id` then `name`.
|
|
8395
|
+
*/
|
|
8396
|
+
async listVectorStores() {
|
|
8397
|
+
const raw = await this.request("/vector_store/list");
|
|
8398
|
+
let rows = [];
|
|
8399
|
+
if (Array.isArray(raw)) rows = raw;
|
|
8400
|
+
else if (Array.isArray(raw?.data)) rows = raw.data;
|
|
8401
|
+
else if (Array.isArray(raw?.vector_stores)) rows = raw.vector_stores;
|
|
8402
|
+
return rows.map((r) => ({
|
|
8403
|
+
id: r?.vector_store_id ?? r?.id ?? r?.name ?? "",
|
|
8404
|
+
name: r?.vector_store_name ?? r?.name ?? void 0
|
|
8405
|
+
})).filter((r) => r.id);
|
|
8406
|
+
}
|
|
8407
|
+
/**
|
|
8408
|
+
* Lists the MCP servers registered on the LiteLLM proxy. Path is
|
|
8409
|
+
* `/mcp/server/list`; tolerates a bare array, `{ data: [...] }` or
|
|
8410
|
+
* `{ servers: [...] }`. Normalised to `{ id, name?, url? }` — `id` prefers
|
|
8411
|
+
* `server_id` then `id` then `alias`/`name`.
|
|
8412
|
+
*/
|
|
8413
|
+
async listMcpServers() {
|
|
8414
|
+
const raw = await this.request("/mcp/server/list");
|
|
8415
|
+
let rows = [];
|
|
8416
|
+
if (Array.isArray(raw)) rows = raw;
|
|
8417
|
+
else if (Array.isArray(raw?.data)) rows = raw.data;
|
|
8418
|
+
else if (Array.isArray(raw?.servers)) rows = raw.servers;
|
|
8419
|
+
return rows.map((r) => ({
|
|
8420
|
+
id: r?.server_id ?? r?.id ?? r?.alias ?? r?.name ?? "",
|
|
8421
|
+
name: r?.alias ?? r?.name ?? void 0,
|
|
8422
|
+
url: r?.url ?? void 0
|
|
8423
|
+
})).filter((r) => r.id);
|
|
8424
|
+
}
|
|
8291
8425
|
emptyUsage() {
|
|
8292
8426
|
return {
|
|
8293
8427
|
total_spend: 0,
|
|
@@ -10457,14 +10591,245 @@ var litellmAuditReadPermission = (0, import_plugin_permission_common.createPermi
|
|
|
10457
10591
|
name: "litellm.audit.read",
|
|
10458
10592
|
attributes: { action: "read" }
|
|
10459
10593
|
});
|
|
10594
|
+
var litellmTeamCreatePermission = (0, import_plugin_permission_common.createPermission)({
|
|
10595
|
+
name: "litellm.team.create",
|
|
10596
|
+
attributes: { action: "create" }
|
|
10597
|
+
});
|
|
10598
|
+
var litellmTeamManagePermission = (0, import_plugin_permission_common.createPermission)({
|
|
10599
|
+
name: "litellm.team.manage",
|
|
10600
|
+
attributes: { action: "update" }
|
|
10601
|
+
});
|
|
10602
|
+
var litellmTeamMembersManagePermission = (0, import_plugin_permission_common.createPermission)({
|
|
10603
|
+
name: "litellm.team.members.manage",
|
|
10604
|
+
attributes: { action: "update" }
|
|
10605
|
+
});
|
|
10606
|
+
var litellmTeamKnowledgebaseManagePermission = (0, import_plugin_permission_common.createPermission)({
|
|
10607
|
+
name: "litellm.team.knowledgebase.manage",
|
|
10608
|
+
attributes: { action: "update" }
|
|
10609
|
+
});
|
|
10610
|
+
var litellmTeamMcpManagePermission = (0, import_plugin_permission_common.createPermission)({
|
|
10611
|
+
name: "litellm.team.mcp.manage",
|
|
10612
|
+
attributes: { action: "update" }
|
|
10613
|
+
});
|
|
10614
|
+
var litellmTeamDeletePermission = (0, import_plugin_permission_common.createPermission)({
|
|
10615
|
+
name: "litellm.team.delete",
|
|
10616
|
+
attributes: { action: "delete" }
|
|
10617
|
+
});
|
|
10460
10618
|
var litellmPermissions = [
|
|
10461
10619
|
litellmKeyCreatePermission,
|
|
10462
10620
|
litellmKeyRevokePermission,
|
|
10463
10621
|
litellmKeyManagePermission,
|
|
10464
|
-
litellmAuditReadPermission
|
|
10622
|
+
litellmAuditReadPermission,
|
|
10623
|
+
litellmTeamCreatePermission,
|
|
10624
|
+
litellmTeamManagePermission,
|
|
10625
|
+
litellmTeamMembersManagePermission,
|
|
10626
|
+
litellmTeamKnowledgebaseManagePermission,
|
|
10627
|
+
litellmTeamMcpManagePermission,
|
|
10628
|
+
litellmTeamDeletePermission
|
|
10465
10629
|
];
|
|
10466
10630
|
|
|
10631
|
+
// src/teamAdmin.ts
|
|
10632
|
+
var import_plugin_permission_common2 = __toESM(require_index_cjs2());
|
|
10633
|
+
function isTeamManagementEnabled(config) {
|
|
10634
|
+
const permEnabled = config.getOptionalBoolean("permission.enabled") ?? false;
|
|
10635
|
+
const adminConfig = readTeamAdminConfig(config);
|
|
10636
|
+
return permEnabled && !!adminConfig.group;
|
|
10637
|
+
}
|
|
10638
|
+
function isObjectPermissionsEnabled(config) {
|
|
10639
|
+
return isTeamManagementEnabled(config) && (config.getOptionalBoolean("litellm.teamAdmin.objectPermissions.enabled") ?? false);
|
|
10640
|
+
}
|
|
10641
|
+
async function assertTeamAdmin(opts) {
|
|
10642
|
+
const {
|
|
10643
|
+
req,
|
|
10644
|
+
auth,
|
|
10645
|
+
permissions,
|
|
10646
|
+
catalogClient,
|
|
10647
|
+
teamAdminGroup,
|
|
10648
|
+
permission,
|
|
10649
|
+
logger
|
|
10650
|
+
} = opts;
|
|
10651
|
+
const userEntityRef = await resolveUserId(req, auth);
|
|
10652
|
+
if (!userEntityRef) {
|
|
10653
|
+
return { ok: false, status: 401, error: "Authentication required" };
|
|
10654
|
+
}
|
|
10655
|
+
const credentials = await resolveCredentials(req, auth);
|
|
10656
|
+
if (!credentials) {
|
|
10657
|
+
return { ok: false, status: 401, error: "Authentication required" };
|
|
10658
|
+
}
|
|
10659
|
+
const isMember = await isUserMemberOfGroup(
|
|
10660
|
+
userEntityRef,
|
|
10661
|
+
teamAdminGroup,
|
|
10662
|
+
catalogClient,
|
|
10663
|
+
auth,
|
|
10664
|
+
logger
|
|
10665
|
+
);
|
|
10666
|
+
if (!isMember) {
|
|
10667
|
+
return {
|
|
10668
|
+
ok: false,
|
|
10669
|
+
status: 403,
|
|
10670
|
+
error: `Access denied: not a member of ${teamAdminGroup}`
|
|
10671
|
+
};
|
|
10672
|
+
}
|
|
10673
|
+
const [decision] = await permissions.authorize([{ permission }], {
|
|
10674
|
+
credentials
|
|
10675
|
+
});
|
|
10676
|
+
if (decision.result !== import_plugin_permission_common2.AuthorizeResult.ALLOW) {
|
|
10677
|
+
return {
|
|
10678
|
+
ok: false,
|
|
10679
|
+
status: 403,
|
|
10680
|
+
error: `Access denied: missing permission "${permission.name}"`
|
|
10681
|
+
};
|
|
10682
|
+
}
|
|
10683
|
+
return { ok: true, userEntityRef };
|
|
10684
|
+
}
|
|
10685
|
+
function readTeamAdminConfig(config) {
|
|
10686
|
+
return {
|
|
10687
|
+
group: config.getOptionalString("litellm.teamAdmin.group"),
|
|
10688
|
+
allowedModels: config.getOptionalStringArray("litellm.teamAdmin.allowedModels") ?? [],
|
|
10689
|
+
allowedModelAccessGroups: config.getOptionalStringArray(
|
|
10690
|
+
"litellm.teamAdmin.allowedModelAccessGroups"
|
|
10691
|
+
) ?? [],
|
|
10692
|
+
maxBudgetCeiling: config.getOptionalNumber(
|
|
10693
|
+
"litellm.teamAdmin.maxBudgetCeiling"
|
|
10694
|
+
),
|
|
10695
|
+
allowUnlimitedBudget: config.getOptionalBoolean("litellm.teamAdmin.allowUnlimitedBudget") ?? false,
|
|
10696
|
+
allowedVectorStores: config.getOptionalStringArray("litellm.teamAdmin.allowedVectorStores") ?? [],
|
|
10697
|
+
allowedMcpServers: config.getOptionalStringArray("litellm.teamAdmin.allowedMcpServers") ?? [],
|
|
10698
|
+
allowedMcpAccessGroups: config.getOptionalStringArray(
|
|
10699
|
+
"litellm.teamAdmin.allowedMcpAccessGroups"
|
|
10700
|
+
) ?? [],
|
|
10701
|
+
allowTeamDelete: config.getOptionalBoolean("litellm.teamAdmin.allowTeamDelete") ?? false
|
|
10702
|
+
};
|
|
10703
|
+
}
|
|
10704
|
+
function validateTeamWriteInput(input, cfg) {
|
|
10705
|
+
const trimmedAlias = (input.team_alias ?? "").trim();
|
|
10706
|
+
if (!trimmedAlias) {
|
|
10707
|
+
return { ok: false, error: "team_alias is required" };
|
|
10708
|
+
}
|
|
10709
|
+
const models = input.models ?? [];
|
|
10710
|
+
if (!Array.isArray(models) || models.length === 0) {
|
|
10711
|
+
return { ok: false, error: "models must list at least one allowed model (a team with no explicit models grants access to every proxy model)" };
|
|
10712
|
+
}
|
|
10713
|
+
const allowedSet = /* @__PURE__ */ new Set([...cfg.allowedModels, ...cfg.allowedModelAccessGroups]);
|
|
10714
|
+
if (allowedSet.size === 0) {
|
|
10715
|
+
return { ok: false, error: "models are not allowed (team admin has configured no allowlist)" };
|
|
10716
|
+
}
|
|
10717
|
+
for (const model of models) {
|
|
10718
|
+
if (!allowedSet.has(model)) {
|
|
10719
|
+
return { ok: false, error: `model "${model}" is not in the allowed set for team admins` };
|
|
10720
|
+
}
|
|
10721
|
+
}
|
|
10722
|
+
let maxBudget;
|
|
10723
|
+
if (input.max_budget !== void 0 && input.max_budget !== null) {
|
|
10724
|
+
if (!Number.isFinite(input.max_budget) || input.max_budget <= 0) {
|
|
10725
|
+
return { ok: false, error: "max_budget must be a positive number" };
|
|
10726
|
+
}
|
|
10727
|
+
maxBudget = input.max_budget;
|
|
10728
|
+
}
|
|
10729
|
+
if (!cfg.allowUnlimitedBudget) {
|
|
10730
|
+
if (maxBudget === void 0) {
|
|
10731
|
+
return { ok: false, error: "max_budget is required" };
|
|
10732
|
+
}
|
|
10733
|
+
if (cfg.maxBudgetCeiling === void 0) {
|
|
10734
|
+
return { ok: false, error: "team budget ceiling is not configured (litellm.teamAdmin.maxBudgetCeiling)" };
|
|
10735
|
+
}
|
|
10736
|
+
if (maxBudget > cfg.maxBudgetCeiling) {
|
|
10737
|
+
return { ok: false, error: `max_budget $${maxBudget} exceeds the ceiling $${cfg.maxBudgetCeiling}` };
|
|
10738
|
+
}
|
|
10739
|
+
} else if (maxBudget !== void 0 && cfg.maxBudgetCeiling !== void 0 && maxBudget > cfg.maxBudgetCeiling) {
|
|
10740
|
+
return { ok: false, error: `max_budget $${maxBudget} exceeds the ceiling $${cfg.maxBudgetCeiling}` };
|
|
10741
|
+
}
|
|
10742
|
+
let budgetDuration = input.budget_duration;
|
|
10743
|
+
if (!budgetDuration || typeof budgetDuration !== "string" || budgetDuration.trim() === "") {
|
|
10744
|
+
if (maxBudget !== void 0) {
|
|
10745
|
+
budgetDuration = "30d";
|
|
10746
|
+
}
|
|
10747
|
+
}
|
|
10748
|
+
const tpmLimit = input.tpm_limit !== void 0 && Number.isFinite(input.tpm_limit) && input.tpm_limit >= 0 ? input.tpm_limit : void 0;
|
|
10749
|
+
const rpmLimit = input.rpm_limit !== void 0 && Number.isFinite(input.rpm_limit) && input.rpm_limit >= 0 ? input.rpm_limit : void 0;
|
|
10750
|
+
return {
|
|
10751
|
+
ok: true,
|
|
10752
|
+
value: {
|
|
10753
|
+
team_alias: trimmedAlias,
|
|
10754
|
+
models,
|
|
10755
|
+
...maxBudget !== void 0 && { max_budget: maxBudget },
|
|
10756
|
+
...budgetDuration && { budget_duration: budgetDuration },
|
|
10757
|
+
...tpmLimit !== void 0 && { tpm_limit: tpmLimit },
|
|
10758
|
+
...rpmLimit !== void 0 && { rpm_limit: rpmLimit }
|
|
10759
|
+
}
|
|
10760
|
+
};
|
|
10761
|
+
}
|
|
10762
|
+
function validateTeamPatchInput(input, cfg) {
|
|
10763
|
+
const result = {};
|
|
10764
|
+
let hasRecognizedField = false;
|
|
10765
|
+
if (input.team_alias !== void 0) {
|
|
10766
|
+
hasRecognizedField = true;
|
|
10767
|
+
const trimmedAlias = (input.team_alias ?? "").trim();
|
|
10768
|
+
if (!trimmedAlias) {
|
|
10769
|
+
return { ok: false, error: "team_alias must not be empty" };
|
|
10770
|
+
}
|
|
10771
|
+
result.team_alias = trimmedAlias;
|
|
10772
|
+
}
|
|
10773
|
+
if (input.models !== void 0) {
|
|
10774
|
+
hasRecognizedField = true;
|
|
10775
|
+
if (!Array.isArray(input.models) || input.models.length === 0) {
|
|
10776
|
+
return { ok: false, error: "models must list at least one allowed model (a team with no explicit models grants access to every proxy model)" };
|
|
10777
|
+
}
|
|
10778
|
+
const allowedSet = /* @__PURE__ */ new Set([...cfg.allowedModels, ...cfg.allowedModelAccessGroups]);
|
|
10779
|
+
if (allowedSet.size === 0) {
|
|
10780
|
+
return { ok: false, error: "models are not allowed (team admin has configured no allowlist)" };
|
|
10781
|
+
}
|
|
10782
|
+
for (const model of input.models) {
|
|
10783
|
+
if (!allowedSet.has(model)) {
|
|
10784
|
+
return { ok: false, error: `model "${model}" is not in the allowed set for team admins` };
|
|
10785
|
+
}
|
|
10786
|
+
}
|
|
10787
|
+
result.models = input.models;
|
|
10788
|
+
}
|
|
10789
|
+
if (input.max_budget !== void 0) {
|
|
10790
|
+
hasRecognizedField = true;
|
|
10791
|
+
if (input.max_budget === null) {
|
|
10792
|
+
if (!cfg.allowUnlimitedBudget) {
|
|
10793
|
+
return { ok: false, error: "max_budget cannot be cleared (unlimited budgets are disabled)" };
|
|
10794
|
+
}
|
|
10795
|
+
result.max_budget = null;
|
|
10796
|
+
} else {
|
|
10797
|
+
if (!Number.isFinite(input.max_budget) || input.max_budget <= 0) {
|
|
10798
|
+
return { ok: false, error: "max_budget must be a positive number" };
|
|
10799
|
+
}
|
|
10800
|
+
if (cfg.maxBudgetCeiling !== void 0 && input.max_budget > cfg.maxBudgetCeiling) {
|
|
10801
|
+
return { ok: false, error: `max_budget $${input.max_budget} exceeds the ceiling $${cfg.maxBudgetCeiling}` };
|
|
10802
|
+
}
|
|
10803
|
+
result.max_budget = input.max_budget;
|
|
10804
|
+
}
|
|
10805
|
+
}
|
|
10806
|
+
if (input.blocked !== void 0) {
|
|
10807
|
+
hasRecognizedField = true;
|
|
10808
|
+
if (typeof input.blocked !== "boolean") {
|
|
10809
|
+
return { ok: false, error: "blocked must be a boolean" };
|
|
10810
|
+
}
|
|
10811
|
+
result.blocked = input.blocked;
|
|
10812
|
+
}
|
|
10813
|
+
if (input.budget_duration !== void 0 && typeof input.budget_duration === "string" && input.budget_duration.trim() !== "") {
|
|
10814
|
+
hasRecognizedField = true;
|
|
10815
|
+
result.budget_duration = input.budget_duration;
|
|
10816
|
+
}
|
|
10817
|
+
if (input.tpm_limit !== void 0 && Number.isFinite(input.tpm_limit) && input.tpm_limit >= 0) {
|
|
10818
|
+
hasRecognizedField = true;
|
|
10819
|
+
result.tpm_limit = input.tpm_limit;
|
|
10820
|
+
}
|
|
10821
|
+
if (input.rpm_limit !== void 0 && Number.isFinite(input.rpm_limit) && input.rpm_limit >= 0) {
|
|
10822
|
+
hasRecognizedField = true;
|
|
10823
|
+
result.rpm_limit = input.rpm_limit;
|
|
10824
|
+
}
|
|
10825
|
+
if (!hasRecognizedField) {
|
|
10826
|
+
return { ok: false, error: "no updatable fields provided" };
|
|
10827
|
+
}
|
|
10828
|
+
return { ok: true, value: result };
|
|
10829
|
+
}
|
|
10830
|
+
|
|
10467
10831
|
// src/router.ts
|
|
10832
|
+
var teamCreateInFlight = /* @__PURE__ */ new Map();
|
|
10468
10833
|
async function createRouter(options) {
|
|
10469
10834
|
const { config, logger, auth, discovery, permissions } = options;
|
|
10470
10835
|
const baseUrl = config.getString("litellm.baseUrl");
|
|
@@ -10477,12 +10842,20 @@ async function createRouter(options) {
|
|
|
10477
10842
|
const auditGroup = config.getOptionalString("litellm.audit.group");
|
|
10478
10843
|
const allowUnlimitedBudget = config.getOptionalBoolean("litellm.keyGeneration.allowUnlimitedBudget") ?? false;
|
|
10479
10844
|
const teamRequired = config.getOptionalBoolean("litellm.keyGeneration.teamRequired") ?? true;
|
|
10480
|
-
const
|
|
10845
|
+
const teamMgmtEnabled = isTeamManagementEnabled(config);
|
|
10846
|
+
const objectPermsEnabled = isObjectPermissionsEnabled(config);
|
|
10847
|
+
const teamAdminCfg = readTeamAdminConfig(config);
|
|
10848
|
+
const catalogClient = options.catalogClient ?? new import_catalog_client.CatalogClient({ discoveryApi: discovery });
|
|
10481
10849
|
if (provisioningEnabled) {
|
|
10482
10850
|
logger.info(
|
|
10483
10851
|
`LiteLLM auto-provisioning enabled \u2014 defaults: budget=$${provisioningDefaults.maxBudget}/${provisioningDefaults.budgetDuration}, models=${provisioningDefaults.models.length ? provisioningDefaults.models.join(",") : "all"}, teams=[${provisioningDefaults.teams.join(",")}]`
|
|
10484
10852
|
);
|
|
10485
10853
|
}
|
|
10854
|
+
if (config.getOptional("litellm.teamAdmin") && !teamMgmtEnabled) {
|
|
10855
|
+
logger.warn(
|
|
10856
|
+
"litellm.teamAdmin is configured but team management is disabled \u2014 set permission.enabled: true and litellm.teamAdmin.group to enable it."
|
|
10857
|
+
);
|
|
10858
|
+
}
|
|
10486
10859
|
const router = (0, import_express.Router)();
|
|
10487
10860
|
router.use(import_express.default.json());
|
|
10488
10861
|
router.get("/health", (_req, res) => {
|
|
@@ -10491,7 +10864,13 @@ async function createRouter(options) {
|
|
|
10491
10864
|
router.get("/config", (_req, res) => {
|
|
10492
10865
|
res.json({
|
|
10493
10866
|
baseUrl: publicBaseUrl,
|
|
10494
|
-
keyGeneration: { allowUnlimitedBudget, teamRequired }
|
|
10867
|
+
keyGeneration: { allowUnlimitedBudget, teamRequired },
|
|
10868
|
+
teamManagement: {
|
|
10869
|
+
enabled: teamMgmtEnabled,
|
|
10870
|
+
maxBudgetCeiling: teamAdminCfg.maxBudgetCeiling ?? null,
|
|
10871
|
+
allowUnlimitedBudget: teamAdminCfg.allowUnlimitedBudget,
|
|
10872
|
+
objectPermissionsEnabled: objectPermsEnabled
|
|
10873
|
+
}
|
|
10495
10874
|
});
|
|
10496
10875
|
});
|
|
10497
10876
|
router.get("/openapi.json", (_req, res) => {
|
|
@@ -10635,7 +11014,7 @@ async function createRouter(options) {
|
|
|
10635
11014
|
const [decision] = await permissions.authorize([{ permission }], {
|
|
10636
11015
|
credentials
|
|
10637
11016
|
});
|
|
10638
|
-
return decision.result ===
|
|
11017
|
+
return decision.result === import_plugin_permission_common3.AuthorizeResult.ALLOW;
|
|
10639
11018
|
}
|
|
10640
11019
|
function sendPermissionDenied(res, permission) {
|
|
10641
11020
|
res.status(403).json({
|
|
@@ -10900,6 +11279,536 @@ async function createRouter(options) {
|
|
|
10900
11279
|
res.status(500).json({ error: error.message });
|
|
10901
11280
|
}
|
|
10902
11281
|
});
|
|
11282
|
+
function requireTeamMgmt(res) {
|
|
11283
|
+
if (!teamMgmtEnabled) {
|
|
11284
|
+
res.status(403).json({
|
|
11285
|
+
error: "Team management is disabled (requires permission.enabled and litellm.teamAdmin.group)"
|
|
11286
|
+
});
|
|
11287
|
+
return false;
|
|
11288
|
+
}
|
|
11289
|
+
return true;
|
|
11290
|
+
}
|
|
11291
|
+
function requireObjectPerms(res) {
|
|
11292
|
+
if (!requireTeamMgmt(res)) return false;
|
|
11293
|
+
if (!objectPermsEnabled) {
|
|
11294
|
+
res.status(403).json({
|
|
11295
|
+
error: "Knowledge-base / MCP management is disabled (set litellm.teamAdmin.objectPermissions.enabled: true, with a permission policy and the allowlists in place)"
|
|
11296
|
+
});
|
|
11297
|
+
return false;
|
|
11298
|
+
}
|
|
11299
|
+
return true;
|
|
11300
|
+
}
|
|
11301
|
+
function sendTeamError(err, res) {
|
|
11302
|
+
if (err instanceof LiteLLMUpstreamError) {
|
|
11303
|
+
res.status(err.status).json({
|
|
11304
|
+
error: err.message,
|
|
11305
|
+
...err.param ? { param: err.param } : {}
|
|
11306
|
+
});
|
|
11307
|
+
} else {
|
|
11308
|
+
logger.error("Team operation failed", err);
|
|
11309
|
+
res.status(500).json({ error: err.message });
|
|
11310
|
+
}
|
|
11311
|
+
}
|
|
11312
|
+
router.post("/teams", async (req, res) => {
|
|
11313
|
+
if (!requireTeamMgmt(res)) return;
|
|
11314
|
+
const check = await assertTeamAdmin({
|
|
11315
|
+
req,
|
|
11316
|
+
auth,
|
|
11317
|
+
permissions,
|
|
11318
|
+
catalogClient,
|
|
11319
|
+
teamAdminGroup: teamAdminCfg.group,
|
|
11320
|
+
permission: litellmTeamCreatePermission,
|
|
11321
|
+
logger
|
|
11322
|
+
});
|
|
11323
|
+
if (!check.ok) {
|
|
11324
|
+
res.status(check.status).json({ error: check.error });
|
|
11325
|
+
return;
|
|
11326
|
+
}
|
|
11327
|
+
const v = validateTeamWriteInput(req.body ?? {}, teamAdminCfg);
|
|
11328
|
+
if (!v.ok) {
|
|
11329
|
+
res.status(400).json({ error: v.error });
|
|
11330
|
+
return;
|
|
11331
|
+
}
|
|
11332
|
+
const key = v.value.team_alias.trim().toLowerCase();
|
|
11333
|
+
const pending = teamCreateInFlight.get(key);
|
|
11334
|
+
if (pending) {
|
|
11335
|
+
logger.info(`Team creation already in flight for ${key} \u2014 joining`);
|
|
11336
|
+
try {
|
|
11337
|
+
const result = await pending;
|
|
11338
|
+
res.json(result);
|
|
11339
|
+
} catch (err) {
|
|
11340
|
+
sendTeamError(err, res);
|
|
11341
|
+
}
|
|
11342
|
+
return;
|
|
11343
|
+
}
|
|
11344
|
+
const payload = {
|
|
11345
|
+
team_alias: v.value.team_alias,
|
|
11346
|
+
models: v.value.models,
|
|
11347
|
+
...v.value.max_budget !== void 0 && { max_budget: v.value.max_budget },
|
|
11348
|
+
...v.value.budget_duration && { budget_duration: v.value.budget_duration },
|
|
11349
|
+
...v.value.tpm_limit !== void 0 && { tpm_limit: v.value.tpm_limit },
|
|
11350
|
+
...v.value.rpm_limit !== void 0 && { rpm_limit: v.value.rpm_limit },
|
|
11351
|
+
metadata: {
|
|
11352
|
+
owning_group: teamAdminCfg.group,
|
|
11353
|
+
created_by_backstage_user: check.userEntityRef,
|
|
11354
|
+
created_via: "backstage",
|
|
11355
|
+
created_at_iso: (/* @__PURE__ */ new Date()).toISOString()
|
|
11356
|
+
}
|
|
11357
|
+
};
|
|
11358
|
+
const createPromise = (async () => {
|
|
11359
|
+
const result = await client.createTeam(payload);
|
|
11360
|
+
logger.info({
|
|
11361
|
+
action: "team.create",
|
|
11362
|
+
actor: check.userEntityRef,
|
|
11363
|
+
teamAlias: v.value.team_alias,
|
|
11364
|
+
owningGroup: teamAdminCfg.group
|
|
11365
|
+
});
|
|
11366
|
+
return result;
|
|
11367
|
+
})();
|
|
11368
|
+
teamCreateInFlight.set(key, createPromise);
|
|
11369
|
+
try {
|
|
11370
|
+
const result = await createPromise;
|
|
11371
|
+
res.json(result);
|
|
11372
|
+
} catch (err) {
|
|
11373
|
+
sendTeamError(err, res);
|
|
11374
|
+
} finally {
|
|
11375
|
+
teamCreateInFlight.delete(key);
|
|
11376
|
+
}
|
|
11377
|
+
});
|
|
11378
|
+
router.patch("/teams/:teamId", async (req, res) => {
|
|
11379
|
+
const authz = await authorizeTeamSubresource(
|
|
11380
|
+
req,
|
|
11381
|
+
res,
|
|
11382
|
+
litellmTeamManagePermission
|
|
11383
|
+
);
|
|
11384
|
+
if (!authz) return;
|
|
11385
|
+
const { teamId, owningGroup, actor, team: existing } = authz;
|
|
11386
|
+
const v = validateTeamPatchInput(req.body ?? {}, teamAdminCfg);
|
|
11387
|
+
if (!v.ok) {
|
|
11388
|
+
res.status(400).json({ error: v.error });
|
|
11389
|
+
return;
|
|
11390
|
+
}
|
|
11391
|
+
const expected = (req.body ?? {}).expectedUpdatedAtIso;
|
|
11392
|
+
if (expected !== void 0 && expected !== existing.metadata?.updated_at_iso) {
|
|
11393
|
+
res.status(409).json({
|
|
11394
|
+
error: "This team was modified since you loaded it. Reload and re-apply your change."
|
|
11395
|
+
});
|
|
11396
|
+
return;
|
|
11397
|
+
}
|
|
11398
|
+
const payload = {
|
|
11399
|
+
team_id: teamId,
|
|
11400
|
+
...v.value,
|
|
11401
|
+
metadata: {
|
|
11402
|
+
...existing.metadata ?? {},
|
|
11403
|
+
updated_by_backstage_user: actor,
|
|
11404
|
+
updated_at_iso: (/* @__PURE__ */ new Date()).toISOString()
|
|
11405
|
+
}
|
|
11406
|
+
};
|
|
11407
|
+
try {
|
|
11408
|
+
const r = await client.updateTeam(payload);
|
|
11409
|
+
logger.info({
|
|
11410
|
+
action: "team.update",
|
|
11411
|
+
actor,
|
|
11412
|
+
teamId,
|
|
11413
|
+
owningGroup
|
|
11414
|
+
});
|
|
11415
|
+
res.json(r);
|
|
11416
|
+
} catch (err) {
|
|
11417
|
+
sendTeamError(err, res);
|
|
11418
|
+
}
|
|
11419
|
+
});
|
|
11420
|
+
router.delete("/teams/:teamId", async (req, res) => {
|
|
11421
|
+
const authz = await authorizeTeamSubresource(
|
|
11422
|
+
req,
|
|
11423
|
+
res,
|
|
11424
|
+
litellmTeamDeletePermission
|
|
11425
|
+
);
|
|
11426
|
+
if (!authz) return;
|
|
11427
|
+
const { teamId, owningGroup, actor } = authz;
|
|
11428
|
+
if (!teamAdminCfg.allowTeamDelete) {
|
|
11429
|
+
res.status(403).json({
|
|
11430
|
+
error: "Team deletion is disabled (set litellm.teamAdmin.allowTeamDelete: true). Block the team instead."
|
|
11431
|
+
});
|
|
11432
|
+
return;
|
|
11433
|
+
}
|
|
11434
|
+
const provisioningRefs = [
|
|
11435
|
+
...provisioningDefaults.teams,
|
|
11436
|
+
...roleConfigs.flatMap((r) => r.teams ?? [])
|
|
11437
|
+
];
|
|
11438
|
+
const force = String(req.query.force ?? "") === "true";
|
|
11439
|
+
if (provisioningRefs.includes(teamId) && !force) {
|
|
11440
|
+
res.status(409).json({
|
|
11441
|
+
error: `Team ${teamId} is referenced by litellm.provisioning config; deleting it will break user provisioning. Re-send with ?force=true to override.`
|
|
11442
|
+
});
|
|
11443
|
+
return;
|
|
11444
|
+
}
|
|
11445
|
+
try {
|
|
11446
|
+
await client.deleteTeam(teamId);
|
|
11447
|
+
logger.info({ action: "team.delete", actor, teamId, owningGroup, force });
|
|
11448
|
+
res.json({ success: true });
|
|
11449
|
+
} catch (err) {
|
|
11450
|
+
sendTeamError(err, res);
|
|
11451
|
+
}
|
|
11452
|
+
});
|
|
11453
|
+
router.get("/teams/managed", async (req, res) => {
|
|
11454
|
+
if (!requireTeamMgmt(res)) return;
|
|
11455
|
+
const check = await assertTeamAdmin({
|
|
11456
|
+
req,
|
|
11457
|
+
auth,
|
|
11458
|
+
permissions,
|
|
11459
|
+
catalogClient,
|
|
11460
|
+
teamAdminGroup: teamAdminCfg.group,
|
|
11461
|
+
permission: litellmTeamManagePermission,
|
|
11462
|
+
logger
|
|
11463
|
+
});
|
|
11464
|
+
if (!check.ok) {
|
|
11465
|
+
res.status(check.status).json({ error: check.error });
|
|
11466
|
+
return;
|
|
11467
|
+
}
|
|
11468
|
+
try {
|
|
11469
|
+
const all = await client.listTeams();
|
|
11470
|
+
const owned = all.filter(
|
|
11471
|
+
(t) => typeof t.metadata?.owning_group === "string" && t.metadata.owning_group === teamAdminCfg.group
|
|
11472
|
+
);
|
|
11473
|
+
res.json(owned);
|
|
11474
|
+
} catch (err) {
|
|
11475
|
+
sendTeamError(err, res);
|
|
11476
|
+
}
|
|
11477
|
+
});
|
|
11478
|
+
async function authorizeTeamSubresource(req, res, permission) {
|
|
11479
|
+
if (!requireTeamMgmt(res)) return null;
|
|
11480
|
+
const check = await assertTeamAdmin({
|
|
11481
|
+
req,
|
|
11482
|
+
auth,
|
|
11483
|
+
permissions,
|
|
11484
|
+
catalogClient,
|
|
11485
|
+
teamAdminGroup: teamAdminCfg.group,
|
|
11486
|
+
permission,
|
|
11487
|
+
logger
|
|
11488
|
+
});
|
|
11489
|
+
if (!check.ok) {
|
|
11490
|
+
res.status(check.status).json({ error: check.error });
|
|
11491
|
+
return null;
|
|
11492
|
+
}
|
|
11493
|
+
const { teamId } = req.params;
|
|
11494
|
+
if (!teamId) {
|
|
11495
|
+
res.status(400).json({ error: "teamId is required" });
|
|
11496
|
+
return null;
|
|
11497
|
+
}
|
|
11498
|
+
let existing;
|
|
11499
|
+
try {
|
|
11500
|
+
existing = await client.getTeamInfo(teamId);
|
|
11501
|
+
} catch (err) {
|
|
11502
|
+
if (err instanceof LiteLLMUpstreamError && err.status === 404) {
|
|
11503
|
+
res.status(404).json({ error: "Team not found" });
|
|
11504
|
+
return null;
|
|
11505
|
+
}
|
|
11506
|
+
sendTeamError(err, res);
|
|
11507
|
+
return null;
|
|
11508
|
+
}
|
|
11509
|
+
const owningGroup = typeof existing.metadata?.owning_group === "string" ? existing.metadata.owning_group : void 0;
|
|
11510
|
+
if (!owningGroup) {
|
|
11511
|
+
res.status(403).json({
|
|
11512
|
+
error: "This team is not managed by Backstage team admins and cannot be edited here"
|
|
11513
|
+
});
|
|
11514
|
+
return null;
|
|
11515
|
+
}
|
|
11516
|
+
const owns = await isUserMemberOfGroup(
|
|
11517
|
+
check.userEntityRef,
|
|
11518
|
+
owningGroup,
|
|
11519
|
+
catalogClient,
|
|
11520
|
+
auth,
|
|
11521
|
+
logger
|
|
11522
|
+
);
|
|
11523
|
+
if (!owns) {
|
|
11524
|
+
res.status(403).json({ error: `Access denied: team is owned by ${owningGroup}` });
|
|
11525
|
+
return null;
|
|
11526
|
+
}
|
|
11527
|
+
return { teamId, owningGroup, actor: check.userEntityRef, team: existing };
|
|
11528
|
+
}
|
|
11529
|
+
router.post(
|
|
11530
|
+
"/teams/:teamId/members",
|
|
11531
|
+
async (req, res) => {
|
|
11532
|
+
const authz = await authorizeTeamSubresource(req, res, litellmTeamMembersManagePermission);
|
|
11533
|
+
if (!authz) return;
|
|
11534
|
+
const { teamId, owningGroup, actor } = authz;
|
|
11535
|
+
const body = req.body ?? {};
|
|
11536
|
+
const userEntityRef = (body.userEntityRef ?? "").trim();
|
|
11537
|
+
if (!userEntityRef) {
|
|
11538
|
+
res.status(400).json({ error: "userEntityRef is required" });
|
|
11539
|
+
return;
|
|
11540
|
+
}
|
|
11541
|
+
if (body.role !== void 0 && body.role !== "user") {
|
|
11542
|
+
res.status(400).json({
|
|
11543
|
+
error: "only the 'user' team role can be assigned from Backstage"
|
|
11544
|
+
});
|
|
11545
|
+
return;
|
|
11546
|
+
}
|
|
11547
|
+
let maxBudgetInTeam;
|
|
11548
|
+
if (body.maxBudgetInTeam !== void 0) {
|
|
11549
|
+
if (typeof body.maxBudgetInTeam !== "number" || !Number.isFinite(body.maxBudgetInTeam) || body.maxBudgetInTeam <= 0) {
|
|
11550
|
+
res.status(400).json({ error: "maxBudgetInTeam must be a positive number" });
|
|
11551
|
+
return;
|
|
11552
|
+
}
|
|
11553
|
+
maxBudgetInTeam = body.maxBudgetInTeam;
|
|
11554
|
+
}
|
|
11555
|
+
try {
|
|
11556
|
+
const { token } = await auth.getPluginRequestToken({
|
|
11557
|
+
onBehalfOf: await auth.getOwnServiceCredentials(),
|
|
11558
|
+
targetPluginId: "catalog"
|
|
11559
|
+
});
|
|
11560
|
+
const entity = await catalogClient.getEntityByRef(userEntityRef, {
|
|
11561
|
+
token
|
|
11562
|
+
});
|
|
11563
|
+
if (!entity || entity.kind !== "User") {
|
|
11564
|
+
res.status(400).json({
|
|
11565
|
+
error: `${userEntityRef} is not a User in the Backstage catalog`
|
|
11566
|
+
});
|
|
11567
|
+
return;
|
|
11568
|
+
}
|
|
11569
|
+
} catch (err) {
|
|
11570
|
+
logger.warn(
|
|
11571
|
+
`Catalog lookup failed for ${userEntityRef}: ${err.message}`
|
|
11572
|
+
);
|
|
11573
|
+
res.status(400).json({
|
|
11574
|
+
error: `Could not verify ${userEntityRef} in the Backstage catalog`
|
|
11575
|
+
});
|
|
11576
|
+
return;
|
|
11577
|
+
}
|
|
11578
|
+
const litellmUserId = toLiteLLMUserId(userEntityRef, userIdDomain);
|
|
11579
|
+
try {
|
|
11580
|
+
await getOrProvisionUser(
|
|
11581
|
+
client,
|
|
11582
|
+
userEntityRef,
|
|
11583
|
+
litellmUserId,
|
|
11584
|
+
provisioningEnabled,
|
|
11585
|
+
provisioningDefaults,
|
|
11586
|
+
roleConfigs,
|
|
11587
|
+
catalogClient,
|
|
11588
|
+
auth,
|
|
11589
|
+
logger
|
|
11590
|
+
);
|
|
11591
|
+
} catch (err) {
|
|
11592
|
+
if (err instanceof ProvisioningError) {
|
|
11593
|
+
res.status(err.status).json(err.body);
|
|
11594
|
+
return;
|
|
11595
|
+
}
|
|
11596
|
+
throw err;
|
|
11597
|
+
}
|
|
11598
|
+
try {
|
|
11599
|
+
await client.teamMemberAdd({
|
|
11600
|
+
team_id: teamId,
|
|
11601
|
+
user_id: litellmUserId,
|
|
11602
|
+
role: "user",
|
|
11603
|
+
...maxBudgetInTeam !== void 0 && {
|
|
11604
|
+
max_budget_in_team: maxBudgetInTeam
|
|
11605
|
+
}
|
|
11606
|
+
});
|
|
11607
|
+
logger.info({
|
|
11608
|
+
action: "team.member.add",
|
|
11609
|
+
actor,
|
|
11610
|
+
teamId,
|
|
11611
|
+
member: litellmUserId,
|
|
11612
|
+
owningGroup
|
|
11613
|
+
});
|
|
11614
|
+
const updated = await client.getTeamInfo(teamId);
|
|
11615
|
+
res.json(updated);
|
|
11616
|
+
} catch (err) {
|
|
11617
|
+
sendTeamError(err, res);
|
|
11618
|
+
}
|
|
11619
|
+
}
|
|
11620
|
+
);
|
|
11621
|
+
router.delete(
|
|
11622
|
+
"/teams/:teamId/members",
|
|
11623
|
+
async (req, res) => {
|
|
11624
|
+
const authz = await authorizeTeamSubresource(req, res, litellmTeamMembersManagePermission);
|
|
11625
|
+
if (!authz) return;
|
|
11626
|
+
const { teamId, owningGroup, actor } = authz;
|
|
11627
|
+
const userEntityRef = String(req.query.userEntityRef ?? "").trim();
|
|
11628
|
+
if (!userEntityRef) {
|
|
11629
|
+
res.status(400).json({ error: "userEntityRef query parameter is required" });
|
|
11630
|
+
return;
|
|
11631
|
+
}
|
|
11632
|
+
const litellmUserId = toLiteLLMUserId(userEntityRef, userIdDomain);
|
|
11633
|
+
try {
|
|
11634
|
+
await client.teamMemberDelete({
|
|
11635
|
+
team_id: teamId,
|
|
11636
|
+
user_id: litellmUserId
|
|
11637
|
+
});
|
|
11638
|
+
logger.info({
|
|
11639
|
+
action: "team.member.remove",
|
|
11640
|
+
actor,
|
|
11641
|
+
teamId,
|
|
11642
|
+
member: litellmUserId,
|
|
11643
|
+
owningGroup
|
|
11644
|
+
});
|
|
11645
|
+
const updated = await client.getTeamInfo(teamId);
|
|
11646
|
+
res.json(updated);
|
|
11647
|
+
} catch (err) {
|
|
11648
|
+
sendTeamError(err, res);
|
|
11649
|
+
}
|
|
11650
|
+
}
|
|
11651
|
+
);
|
|
11652
|
+
router.get("/vector-stores", async (req, res) => {
|
|
11653
|
+
if (!requireObjectPerms(res)) return;
|
|
11654
|
+
const check = await assertTeamAdmin({
|
|
11655
|
+
req,
|
|
11656
|
+
auth,
|
|
11657
|
+
permissions,
|
|
11658
|
+
catalogClient,
|
|
11659
|
+
teamAdminGroup: teamAdminCfg.group,
|
|
11660
|
+
permission: litellmTeamKnowledgebaseManagePermission,
|
|
11661
|
+
logger
|
|
11662
|
+
});
|
|
11663
|
+
if (!check.ok) {
|
|
11664
|
+
res.status(check.status).json({ error: check.error });
|
|
11665
|
+
return;
|
|
11666
|
+
}
|
|
11667
|
+
const allowed = new Set(teamAdminCfg.allowedVectorStores);
|
|
11668
|
+
try {
|
|
11669
|
+
const all = await client.listVectorStores();
|
|
11670
|
+
res.json(all.filter((s) => allowed.has(s.id) || s.name && allowed.has(s.name)));
|
|
11671
|
+
} catch (err) {
|
|
11672
|
+
sendTeamError(err, res);
|
|
11673
|
+
}
|
|
11674
|
+
});
|
|
11675
|
+
router.put(
|
|
11676
|
+
"/teams/:teamId/knowledge-bases",
|
|
11677
|
+
async (req, res) => {
|
|
11678
|
+
if (!requireObjectPerms(res)) return;
|
|
11679
|
+
const authz = await authorizeTeamSubresource(
|
|
11680
|
+
req,
|
|
11681
|
+
res,
|
|
11682
|
+
litellmTeamKnowledgebaseManagePermission
|
|
11683
|
+
);
|
|
11684
|
+
if (!authz) return;
|
|
11685
|
+
const { teamId, owningGroup, actor, team } = authz;
|
|
11686
|
+
const requested = req.body?.vector_stores ?? [];
|
|
11687
|
+
if (!Array.isArray(requested) || !requested.every((v) => typeof v === "string")) {
|
|
11688
|
+
res.status(400).json({ error: "vector_stores must be an array of strings" });
|
|
11689
|
+
return;
|
|
11690
|
+
}
|
|
11691
|
+
const allowed = new Set(teamAdminCfg.allowedVectorStores);
|
|
11692
|
+
const offenders = requested.filter((v) => !allowed.has(v));
|
|
11693
|
+
if (offenders.length) {
|
|
11694
|
+
res.status(400).json({
|
|
11695
|
+
error: `vector store(s) not in the allowed set for team admins: ${offenders.join(
|
|
11696
|
+
", "
|
|
11697
|
+
)}`
|
|
11698
|
+
});
|
|
11699
|
+
return;
|
|
11700
|
+
}
|
|
11701
|
+
const objectPermission = {
|
|
11702
|
+
...team.object_permission ?? {},
|
|
11703
|
+
vector_stores: requested
|
|
11704
|
+
};
|
|
11705
|
+
try {
|
|
11706
|
+
const updated = await client.updateTeam({
|
|
11707
|
+
team_id: teamId,
|
|
11708
|
+
object_permission: objectPermission,
|
|
11709
|
+
// /team/update replaces metadata wholesale — re-send the merged
|
|
11710
|
+
// object so owning_group / created_* survive and the change is
|
|
11711
|
+
// attributed.
|
|
11712
|
+
metadata: {
|
|
11713
|
+
...team.metadata ?? {},
|
|
11714
|
+
updated_by_backstage_user: actor,
|
|
11715
|
+
updated_at_iso: (/* @__PURE__ */ new Date()).toISOString()
|
|
11716
|
+
}
|
|
11717
|
+
});
|
|
11718
|
+
logger.info({
|
|
11719
|
+
action: "team.knowledgebase.set",
|
|
11720
|
+
actor,
|
|
11721
|
+
teamId,
|
|
11722
|
+
owningGroup,
|
|
11723
|
+
vector_stores: requested
|
|
11724
|
+
});
|
|
11725
|
+
res.json(updated);
|
|
11726
|
+
} catch (err) {
|
|
11727
|
+
sendTeamError(err, res);
|
|
11728
|
+
}
|
|
11729
|
+
}
|
|
11730
|
+
);
|
|
11731
|
+
router.get("/mcp-servers", async (req, res) => {
|
|
11732
|
+
if (!requireObjectPerms(res)) return;
|
|
11733
|
+
const check = await assertTeamAdmin({
|
|
11734
|
+
req,
|
|
11735
|
+
auth,
|
|
11736
|
+
permissions,
|
|
11737
|
+
catalogClient,
|
|
11738
|
+
teamAdminGroup: teamAdminCfg.group,
|
|
11739
|
+
permission: litellmTeamMcpManagePermission,
|
|
11740
|
+
logger
|
|
11741
|
+
});
|
|
11742
|
+
if (!check.ok) {
|
|
11743
|
+
res.status(check.status).json({ error: check.error });
|
|
11744
|
+
return;
|
|
11745
|
+
}
|
|
11746
|
+
const allowed = new Set(teamAdminCfg.allowedMcpServers);
|
|
11747
|
+
try {
|
|
11748
|
+
const all = await client.listMcpServers();
|
|
11749
|
+
res.json(all.filter((s) => allowed.has(s.id) || s.name && allowed.has(s.name)));
|
|
11750
|
+
} catch (err) {
|
|
11751
|
+
sendTeamError(err, res);
|
|
11752
|
+
}
|
|
11753
|
+
});
|
|
11754
|
+
router.put(
|
|
11755
|
+
"/teams/:teamId/mcp-servers",
|
|
11756
|
+
async (req, res) => {
|
|
11757
|
+
if (!requireObjectPerms(res)) return;
|
|
11758
|
+
const authz = await authorizeTeamSubresource(
|
|
11759
|
+
req,
|
|
11760
|
+
res,
|
|
11761
|
+
litellmTeamMcpManagePermission
|
|
11762
|
+
);
|
|
11763
|
+
if (!authz) return;
|
|
11764
|
+
const { teamId, owningGroup, actor, team } = authz;
|
|
11765
|
+
const requested = req.body?.mcp_servers ?? [];
|
|
11766
|
+
if (!Array.isArray(requested) || !requested.every((v) => typeof v === "string")) {
|
|
11767
|
+
res.status(400).json({ error: "mcp_servers must be an array of strings" });
|
|
11768
|
+
return;
|
|
11769
|
+
}
|
|
11770
|
+
const allowed = new Set(teamAdminCfg.allowedMcpServers);
|
|
11771
|
+
const offenders = requested.filter((v) => !allowed.has(v));
|
|
11772
|
+
if (offenders.length) {
|
|
11773
|
+
res.status(400).json({
|
|
11774
|
+
error: `MCP server(s) not in the allowed set for team admins: ${offenders.join(
|
|
11775
|
+
", "
|
|
11776
|
+
)}`
|
|
11777
|
+
});
|
|
11778
|
+
return;
|
|
11779
|
+
}
|
|
11780
|
+
const before = team.object_permission?.mcp_servers ?? [];
|
|
11781
|
+
const objectPermission = {
|
|
11782
|
+
...team.object_permission ?? {},
|
|
11783
|
+
mcp_servers: requested
|
|
11784
|
+
};
|
|
11785
|
+
try {
|
|
11786
|
+
const updated = await client.updateTeam({
|
|
11787
|
+
team_id: teamId,
|
|
11788
|
+
object_permission: objectPermission,
|
|
11789
|
+
// /team/update replaces metadata wholesale — re-send the merged
|
|
11790
|
+
// object so owning_group / created_* survive and the change is
|
|
11791
|
+
// attributed.
|
|
11792
|
+
metadata: {
|
|
11793
|
+
...team.metadata ?? {},
|
|
11794
|
+
updated_by_backstage_user: actor,
|
|
11795
|
+
updated_at_iso: (/* @__PURE__ */ new Date()).toISOString()
|
|
11796
|
+
}
|
|
11797
|
+
});
|
|
11798
|
+
logger.info({
|
|
11799
|
+
action: "team.mcp.set",
|
|
11800
|
+
actor,
|
|
11801
|
+
teamId,
|
|
11802
|
+
owningGroup,
|
|
11803
|
+
before,
|
|
11804
|
+
after: requested
|
|
11805
|
+
});
|
|
11806
|
+
res.json(updated);
|
|
11807
|
+
} catch (err) {
|
|
11808
|
+
sendTeamError(err, res);
|
|
11809
|
+
}
|
|
11810
|
+
}
|
|
11811
|
+
);
|
|
10903
11812
|
router.get("/teams/:teamId/usage", async (req, res) => {
|
|
10904
11813
|
try {
|
|
10905
11814
|
const { teamId } = req.params;
|
|
@@ -11090,26 +11999,37 @@ var litellmPlugin = (0, import_backend_plugin_api.createBackendPlugin)({
|
|
|
11090
11999
|
LiteLLMClient,
|
|
11091
12000
|
ProvisioningError,
|
|
11092
12001
|
applyRoleOverrides,
|
|
12002
|
+
assertTeamAdmin,
|
|
11093
12003
|
bridgeGenerateKey,
|
|
11094
12004
|
bridgeListKeys,
|
|
11095
12005
|
createRouter,
|
|
11096
12006
|
getOrProvisionUser,
|
|
11097
12007
|
getOrProvisionUserFromClaims,
|
|
12008
|
+
isTeamManagementEnabled,
|
|
11098
12009
|
litellmAuditReadPermission,
|
|
11099
12010
|
litellmKeyCreatePermission,
|
|
11100
12011
|
litellmKeyManagePermission,
|
|
11101
12012
|
litellmKeyRevokePermission,
|
|
11102
12013
|
litellmPermissions,
|
|
11103
12014
|
litellmPlugin,
|
|
12015
|
+
litellmTeamCreatePermission,
|
|
12016
|
+
litellmTeamDeletePermission,
|
|
12017
|
+
litellmTeamKnowledgebaseManagePermission,
|
|
12018
|
+
litellmTeamManagePermission,
|
|
12019
|
+
litellmTeamMcpManagePermission,
|
|
12020
|
+
litellmTeamMembersManagePermission,
|
|
11104
12021
|
newDefaultVerifier,
|
|
11105
12022
|
provisionUser,
|
|
11106
12023
|
readBridgeConfig,
|
|
11107
12024
|
readProvisioningDefaults,
|
|
11108
12025
|
readRoleConfigs,
|
|
12026
|
+
readTeamAdminConfig,
|
|
11109
12027
|
resolveBridgeUserId,
|
|
11110
12028
|
resolveUserId,
|
|
11111
12029
|
resolveUserProfile,
|
|
11112
12030
|
resolveUserRole,
|
|
11113
|
-
toLiteLLMUserId
|
|
12031
|
+
toLiteLLMUserId,
|
|
12032
|
+
validateTeamPatchInput,
|
|
12033
|
+
validateTeamWriteInput
|
|
11114
12034
|
});
|
|
11115
12035
|
//# sourceMappingURL=index.cjs.js.map
|