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