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