@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/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 AuthorizeResult2 = {
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 = AuthorizeResult2;
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 import_plugin_permission_common2 = __toESM(require_index_cjs2());
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 ?? teamId,
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 catalogClient = new import_catalog_client.CatalogClient({ discoveryApi: discovery });
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 === import_plugin_permission_common2.AuthorizeResult.ALLOW;
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