@acarmisc/backstage-plugin-litellm-backend 0.23.0 → 0.25.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
@@ -7982,20 +7982,20 @@ __export(index_exports, {
7982
7982
  getOrProvisionUser: () => getOrProvisionUser,
7983
7983
  getOrProvisionUserFromClaims: () => getOrProvisionUserFromClaims,
7984
7984
  isTeamManagementEnabled: () => isTeamManagementEnabled,
7985
- litellmAuditReadPermission: () => import_backstage_plugin_litellm_common12.litellmAuditReadPermission,
7986
- litellmKeyCreatePermission: () => import_backstage_plugin_litellm_common12.litellmKeyCreatePermission,
7987
- litellmKeyManagePermission: () => import_backstage_plugin_litellm_common12.litellmKeyManagePermission,
7988
- litellmKeyResetSpendPermission: () => import_backstage_plugin_litellm_common12.litellmKeyResetSpendPermission,
7989
- litellmKeyRevokePermission: () => import_backstage_plugin_litellm_common12.litellmKeyRevokePermission,
7990
- litellmKeyUnblockPermission: () => import_backstage_plugin_litellm_common12.litellmKeyUnblockPermission,
7991
- litellmPermissions: () => import_backstage_plugin_litellm_common12.litellmPermissions,
7985
+ litellmAuditReadPermission: () => import_backstage_plugin_litellm_common13.litellmAuditReadPermission,
7986
+ litellmKeyCreatePermission: () => import_backstage_plugin_litellm_common13.litellmKeyCreatePermission,
7987
+ litellmKeyManagePermission: () => import_backstage_plugin_litellm_common13.litellmKeyManagePermission,
7988
+ litellmKeyResetSpendPermission: () => import_backstage_plugin_litellm_common13.litellmKeyResetSpendPermission,
7989
+ litellmKeyRevokePermission: () => import_backstage_plugin_litellm_common13.litellmKeyRevokePermission,
7990
+ litellmKeyUnblockPermission: () => import_backstage_plugin_litellm_common13.litellmKeyUnblockPermission,
7991
+ litellmPermissions: () => import_backstage_plugin_litellm_common13.litellmPermissions,
7992
7992
  litellmPlugin: () => litellmPlugin,
7993
- litellmTeamCreatePermission: () => import_backstage_plugin_litellm_common12.litellmTeamCreatePermission,
7994
- litellmTeamDeletePermission: () => import_backstage_plugin_litellm_common12.litellmTeamDeletePermission,
7995
- litellmTeamKnowledgebaseManagePermission: () => import_backstage_plugin_litellm_common12.litellmTeamKnowledgebaseManagePermission,
7996
- litellmTeamManagePermission: () => import_backstage_plugin_litellm_common12.litellmTeamManagePermission,
7997
- litellmTeamMcpManagePermission: () => import_backstage_plugin_litellm_common12.litellmTeamMcpManagePermission,
7998
- litellmTeamMembersManagePermission: () => import_backstage_plugin_litellm_common12.litellmTeamMembersManagePermission,
7993
+ litellmTeamCreatePermission: () => import_backstage_plugin_litellm_common13.litellmTeamCreatePermission,
7994
+ litellmTeamDeletePermission: () => import_backstage_plugin_litellm_common13.litellmTeamDeletePermission,
7995
+ litellmTeamKnowledgebaseManagePermission: () => import_backstage_plugin_litellm_common13.litellmTeamKnowledgebaseManagePermission,
7996
+ litellmTeamManagePermission: () => import_backstage_plugin_litellm_common13.litellmTeamManagePermission,
7997
+ litellmTeamMcpManagePermission: () => import_backstage_plugin_litellm_common13.litellmTeamMcpManagePermission,
7998
+ litellmTeamMembersManagePermission: () => import_backstage_plugin_litellm_common13.litellmTeamMembersManagePermission,
7999
7999
  newDefaultVerifier: () => newDefaultVerifier,
8000
8000
  provisionUser: () => provisionUser,
8001
8001
  readBridgeConfig: () => readBridgeConfig,
@@ -8691,25 +8691,33 @@ var LiteLLMClient = class {
8691
8691
  return this.normalizeSpendLogs(raw, page, pageSize);
8692
8692
  }
8693
8693
  normalizeSpendLogs(raw, page, pageSize) {
8694
- const rows = Array.isArray(raw) ? raw : Array.isArray(raw?.data) ? raw.data : Array.isArray(raw?.results) ? raw.results : Array.isArray(raw?.logs) ? raw.logs : [];
8695
- const toNum = (v, fallback = 0) => {
8696
- const n = typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v) : NaN;
8697
- return Number.isFinite(n) ? n : fallback;
8694
+ const rows = [];
8695
+ if (Array.isArray(raw)) rows.push(...raw);
8696
+ else if (Array.isArray(raw?.data)) rows.push(...raw.data);
8697
+ else if (Array.isArray(raw?.results)) rows.push(...raw.results);
8698
+ else if (Array.isArray(raw?.logs)) rows.push(...raw.logs);
8699
+ const toNum2 = (v, fallback = 0) => {
8700
+ if (typeof v === "number") return Number.isFinite(v) ? v : fallback;
8701
+ if (typeof v === "string" && v.trim() !== "") {
8702
+ const n = Number(v);
8703
+ return Number.isFinite(n) ? n : fallback;
8704
+ }
8705
+ return fallback;
8698
8706
  };
8699
8707
  const data = rows.map((r) => {
8700
- const promptTokens = toNum(
8708
+ const promptTokens = toNum2(
8701
8709
  r?.prompt_tokens ?? r?.promptTokens ?? r?.input_tokens
8702
8710
  );
8703
- const completionTokens = toNum(
8711
+ const completionTokens = toNum2(
8704
8712
  r?.completion_tokens ?? r?.completionTokens ?? r?.output_tokens
8705
8713
  );
8706
8714
  return {
8707
8715
  requestId: String(r?.request_id ?? r?.requestId ?? r?.id ?? ""),
8708
8716
  model: String(r?.model ?? r?.model_name ?? ""),
8709
- spend: toNum(r?.spend ?? r?.cost),
8717
+ spend: toNum2(r?.spend ?? r?.cost),
8710
8718
  promptTokens,
8711
8719
  completionTokens,
8712
- totalTokens: toNum(
8720
+ totalTokens: toNum2(
8713
8721
  r?.total_tokens ?? r?.totalTokens,
8714
8722
  promptTokens + completionTokens
8715
8723
  ),
@@ -8725,7 +8733,7 @@ var LiteLLMClient = class {
8725
8733
  });
8726
8734
  return {
8727
8735
  data,
8728
- total: toNum(raw?.total ?? raw?.total_count, data.length),
8736
+ total: toNum2(raw?.total ?? raw?.total_count, data.length),
8729
8737
  page,
8730
8738
  pageSize,
8731
8739
  truncated: data.length >= pageSize
@@ -9159,7 +9167,7 @@ async function resolveUserRole(userEntityRef, roleConfigs, catalogClient, auth,
9159
9167
  }
9160
9168
 
9161
9169
  // src/router.ts
9162
- var import_backstage_plugin_litellm_common10 = require("@acarmisc/backstage-plugin-litellm-common");
9170
+ var import_backstage_plugin_litellm_common11 = require("@acarmisc/backstage-plugin-litellm-common");
9163
9171
 
9164
9172
  // src/teamAdmin.ts
9165
9173
  var import_plugin_permission_common = __toESM(require_index_cjs2());
@@ -9522,6 +9530,21 @@ function redactTeamMemberUsage(usage) {
9522
9530
  };
9523
9531
  }
9524
9532
 
9533
+ // src/langfuse.ts
9534
+ function readLangfuseConfig(config) {
9535
+ const enabled = config.getOptionalBoolean("litellm.langfuse.enabled") ?? false;
9536
+ return {
9537
+ enabled,
9538
+ baseUrl: config.getOptionalString("litellm.langfuse.baseUrl") ?? null,
9539
+ publicKey: config.getOptionalString("litellm.langfuse.publicKey") ?? null,
9540
+ secretKey: config.getOptionalString("litellm.langfuse.secretKey") ?? null,
9541
+ userIdField: config.getOptionalString("litellm.langfuse.userIdField") ?? "userId"
9542
+ };
9543
+ }
9544
+ function isLangfuseConfigured(cfg) {
9545
+ return cfg.enabled && !!cfg.baseUrl && !!cfg.publicKey && !!cfg.secretKey;
9546
+ }
9547
+
9525
9548
  // src/routes/middleware/withUser.ts
9526
9549
  function createRequireUser(ctx) {
9527
9550
  return async (req, res, next) => {
@@ -10045,7 +10068,7 @@ function registerConfigRoutes(router, ctx) {
10045
10068
  }
10046
10069
 
10047
10070
  // src/routes/keys.ts
10048
- var import_backstage_plugin_litellm_common3 = require("@acarmisc/backstage-plugin-litellm-common");
10071
+ var import_backstage_plugin_litellm_common4 = require("@acarmisc/backstage-plugin-litellm-common");
10049
10072
 
10050
10073
  // src/services/modelAccess.ts
10051
10074
  var import_backstage_plugin_litellm_common2 = require("@acarmisc/backstage-plugin-litellm-common");
@@ -10067,6 +10090,18 @@ async function findDisallowedModels(client, requested, allowed) {
10067
10090
  );
10068
10091
  }
10069
10092
 
10093
+ // src/services/mcpAccess.ts
10094
+ var import_backstage_plugin_litellm_common3 = require("@acarmisc/backstage-plugin-litellm-common");
10095
+ var NO_MCP_SERVERS = "no-mcp-servers";
10096
+ function teamMcpServers(team) {
10097
+ const listed = team?.object_permission?.mcp_servers ?? [];
10098
+ return [...new Set(listed)].filter((n) => import_backstage_plugin_litellm_common3.MCP_SERVER_NAME_RE.test(n));
10099
+ }
10100
+ function findDisallowedMcpServers(requested, allowed) {
10101
+ const ok = new Set(allowed);
10102
+ return [...new Set(requested)].filter((n) => !ok.has(n));
10103
+ }
10104
+
10070
10105
  // src/services/keyService.ts
10071
10106
  var KeyServiceError = class extends Error {
10072
10107
  constructor(status, body, message2) {
@@ -10108,10 +10143,12 @@ async function createKeyForUser(user, input, ctx) {
10108
10143
  );
10109
10144
  }
10110
10145
  }
10146
+ let teamPromise;
10147
+ const getTeam = (teamId) => teamPromise ??= client.getTeamInfo(teamId);
10111
10148
  if (input.models && input.models.length > 0) {
10112
10149
  let allowedModels = [];
10113
10150
  if (input.team_id) {
10114
- const teamInfo = await client.getTeamInfo(input.team_id);
10151
+ const teamInfo = await getTeam(input.team_id);
10115
10152
  allowedModels = teamInfo?.models ?? [];
10116
10153
  } else {
10117
10154
  allowedModels = userInfo?.models ?? [];
@@ -10144,6 +10181,19 @@ async function createKeyForUser(user, input, ctx) {
10144
10181
  ...clientMetadata,
10145
10182
  ...serverMetadata
10146
10183
  };
10184
+ const mcpServers = input.mcp_servers ? [...new Set(input.mcp_servers)] : void 0;
10185
+ if (mcpServers && mcpServers.length > 0) {
10186
+ if (!input.team_id) {
10187
+ throw new KeyServiceError(400, { error: "team_id is required when mcp_servers is set" });
10188
+ }
10189
+ const disallowed = findDisallowedMcpServers(mcpServers, teamMcpServers(await getTeam(input.team_id)));
10190
+ if (disallowed.length > 0) {
10191
+ throw new KeyServiceError(403, {
10192
+ error: "One or more requested MCP servers are not allowed",
10193
+ disallowed_mcp_servers: disallowed
10194
+ });
10195
+ }
10196
+ }
10147
10197
  const upstreamRequest = {
10148
10198
  alias: input.alias,
10149
10199
  user_id: userInfo.user_id
@@ -10172,6 +10222,11 @@ async function createKeyForUser(user, input, ctx) {
10172
10222
  if (input.key_type !== void 0) {
10173
10223
  upstreamRequest.key_type = input.key_type;
10174
10224
  }
10225
+ if (mcpServers) {
10226
+ upstreamRequest.object_permission = {
10227
+ mcp_servers: mcpServers.length > 0 ? mcpServers : [NO_MCP_SERVERS]
10228
+ };
10229
+ }
10175
10230
  upstreamRequest.metadata = enrichedMetadata;
10176
10231
  const result = await client.generateKey(upstreamRequest);
10177
10232
  logger.info("key.generate", { userId: userInfo.user_id, keyAlias: input.alias });
@@ -10194,8 +10249,8 @@ function registerKeysRoutes(router, ctx) {
10194
10249
  assertPermission,
10195
10250
  sendPermissionDenied
10196
10251
  } = ctx;
10197
- const generateKeyInputSchema = (0, import_backstage_plugin_litellm_common3.createGenerateKeyInputSchema)(keyValidationConfig);
10198
- const updateKeyInputSchema = (0, import_backstage_plugin_litellm_common3.createUpdateKeyInputSchema)(keyValidationConfig);
10252
+ const generateKeyInputSchema = (0, import_backstage_plugin_litellm_common4.createGenerateKeyInputSchema)(keyValidationConfig);
10253
+ const updateKeyInputSchema = (0, import_backstage_plugin_litellm_common4.createUpdateKeyInputSchema)(keyValidationConfig);
10199
10254
  router.get("/user/info", async (_req, res) => {
10200
10255
  try {
10201
10256
  const tokenEntityRef = res.locals.tokenEntityRef;
@@ -10232,8 +10287,8 @@ function registerKeysRoutes(router, ctx) {
10232
10287
  });
10233
10288
  router.post("/keys/prune-expired", async (_req, res) => {
10234
10289
  try {
10235
- if (!await assertPermission(_req, import_backstage_plugin_litellm_common3.litellmKeyRevokePermission)) {
10236
- sendPermissionDenied(res, import_backstage_plugin_litellm_common3.litellmKeyRevokePermission);
10290
+ if (!await assertPermission(_req, import_backstage_plugin_litellm_common4.litellmKeyRevokePermission)) {
10291
+ sendPermissionDenied(res, import_backstage_plugin_litellm_common4.litellmKeyRevokePermission);
10237
10292
  return;
10238
10293
  }
10239
10294
  await getProvisionedUser(ctx, res);
@@ -10297,8 +10352,8 @@ function registerKeysRoutes(router, ctx) {
10297
10352
  return;
10298
10353
  }
10299
10354
  const input = parseResult.data;
10300
- if (!await assertPermission(req, import_backstage_plugin_litellm_common3.litellmKeyCreatePermission)) {
10301
- sendPermissionDenied(res, import_backstage_plugin_litellm_common3.litellmKeyCreatePermission);
10355
+ if (!await assertPermission(req, import_backstage_plugin_litellm_common4.litellmKeyCreatePermission)) {
10356
+ sendPermissionDenied(res, import_backstage_plugin_litellm_common4.litellmKeyCreatePermission);
10302
10357
  return;
10303
10358
  }
10304
10359
  const tokenEntityRef = res.locals.tokenEntityRef;
@@ -10347,8 +10402,8 @@ function registerKeysRoutes(router, ctx) {
10347
10402
  res.status(400).json({ error: "keyId is required" });
10348
10403
  return;
10349
10404
  }
10350
- if (!await assertPermission(req, import_backstage_plugin_litellm_common3.litellmKeyManagePermission)) {
10351
- sendPermissionDenied(res, import_backstage_plugin_litellm_common3.litellmKeyManagePermission);
10405
+ if (!await assertPermission(req, import_backstage_plugin_litellm_common4.litellmKeyManagePermission)) {
10406
+ sendPermissionDenied(res, import_backstage_plugin_litellm_common4.litellmKeyManagePermission);
10352
10407
  return;
10353
10408
  }
10354
10409
  const { tokenEntityRef, userId, key } = await authorizeKeyAction(req, keyId);
@@ -10425,8 +10480,8 @@ function registerKeysRoutes(router, ctx) {
10425
10480
  res.status(400).json({ error: "keyId is required" });
10426
10481
  return;
10427
10482
  }
10428
- if (!await assertPermission(req, import_backstage_plugin_litellm_common3.litellmKeyRevokePermission)) {
10429
- sendPermissionDenied(res, import_backstage_plugin_litellm_common3.litellmKeyRevokePermission);
10483
+ if (!await assertPermission(req, import_backstage_plugin_litellm_common4.litellmKeyRevokePermission)) {
10484
+ sendPermissionDenied(res, import_backstage_plugin_litellm_common4.litellmKeyRevokePermission);
10430
10485
  return;
10431
10486
  }
10432
10487
  const { tokenEntityRef } = await authorizeKeyAction(req, keyId);
@@ -10440,8 +10495,8 @@ function registerKeysRoutes(router, ctx) {
10440
10495
  router.post("/keys/:keyId/block", async (req, res) => {
10441
10496
  try {
10442
10497
  const { keyId } = req.params;
10443
- if (!await assertPermission(req, import_backstage_plugin_litellm_common3.litellmKeyManagePermission)) {
10444
- sendPermissionDenied(res, import_backstage_plugin_litellm_common3.litellmKeyManagePermission);
10498
+ if (!await assertPermission(req, import_backstage_plugin_litellm_common4.litellmKeyManagePermission)) {
10499
+ sendPermissionDenied(res, import_backstage_plugin_litellm_common4.litellmKeyManagePermission);
10445
10500
  return;
10446
10501
  }
10447
10502
  const { tokenEntityRef, key } = await authorizeKeyAction(req, keyId);
@@ -10469,8 +10524,8 @@ function registerKeysRoutes(router, ctx) {
10469
10524
  try {
10470
10525
  const { keyId } = req.params;
10471
10526
  const { tokenEntityRef, key } = await authorizeKeyAction(req, keyId);
10472
- if (!await assertPermission(req, import_backstage_plugin_litellm_common3.litellmKeyUnblockPermission)) {
10473
- sendPermissionDenied(res, import_backstage_plugin_litellm_common3.litellmKeyUnblockPermission);
10527
+ if (!await assertPermission(req, import_backstage_plugin_litellm_common4.litellmKeyUnblockPermission)) {
10528
+ sendPermissionDenied(res, import_backstage_plugin_litellm_common4.litellmKeyUnblockPermission);
10474
10529
  return;
10475
10530
  }
10476
10531
  await client.unblockKey(keyId);
@@ -10498,8 +10553,8 @@ function registerKeysRoutes(router, ctx) {
10498
10553
  });
10499
10554
  return;
10500
10555
  }
10501
- if (!await assertPermission(req, import_backstage_plugin_litellm_common3.litellmKeyResetSpendPermission)) {
10502
- sendPermissionDenied(res, import_backstage_plugin_litellm_common3.litellmKeyResetSpendPermission);
10556
+ if (!await assertPermission(req, import_backstage_plugin_litellm_common4.litellmKeyResetSpendPermission)) {
10557
+ sendPermissionDenied(res, import_backstage_plugin_litellm_common4.litellmKeyResetSpendPermission);
10503
10558
  return;
10504
10559
  }
10505
10560
  const { tokenEntityRef } = await authorizeKeyAction(req, keyId);
@@ -10531,8 +10586,8 @@ function registerKeysRoutes(router, ctx) {
10531
10586
  res.status(403).json({ error: "Access denied: not a member of the audit group" });
10532
10587
  return;
10533
10588
  }
10534
- if (!await assertPermission(req, import_backstage_plugin_litellm_common3.litellmAuditReadPermission)) {
10535
- sendPermissionDenied(res, import_backstage_plugin_litellm_common3.litellmAuditReadPermission);
10589
+ if (!await assertPermission(req, import_backstage_plugin_litellm_common4.litellmAuditReadPermission)) {
10590
+ sendPermissionDenied(res, import_backstage_plugin_litellm_common4.litellmAuditReadPermission);
10536
10591
  return;
10537
10592
  }
10538
10593
  try {
@@ -10584,7 +10639,7 @@ function registerKeysRoutes(router, ctx) {
10584
10639
  }
10585
10640
 
10586
10641
  // src/routes/opencode.ts
10587
- var import_backstage_plugin_litellm_common4 = require("@acarmisc/backstage-plugin-litellm-common");
10642
+ var import_backstage_plugin_litellm_common5 = require("@acarmisc/backstage-plugin-litellm-common");
10588
10643
  function isValidRedirectUri(uri) {
10589
10644
  if (!uri) return false;
10590
10645
  try {
@@ -10626,8 +10681,8 @@ function registerOpencodeRoutes(router, ctx) {
10626
10681
  return null;
10627
10682
  }
10628
10683
  const redirectUri = redirectUriParam;
10629
- if (!await assertPermission(req, import_backstage_plugin_litellm_common4.litellmKeyCreatePermission)) {
10630
- sendPermissionDenied(res, import_backstage_plugin_litellm_common4.litellmKeyCreatePermission);
10684
+ if (!await assertPermission(req, import_backstage_plugin_litellm_common5.litellmKeyCreatePermission)) {
10685
+ sendPermissionDenied(res, import_backstage_plugin_litellm_common5.litellmKeyCreatePermission);
10631
10686
  return null;
10632
10687
  }
10633
10688
  const computedUserId = toLiteLLMUserId(tokenEntityRef, userIdDomain);
@@ -10795,7 +10850,7 @@ function registerOpencodeRoutes(router, ctx) {
10795
10850
  }
10796
10851
 
10797
10852
  // src/routes/teams.ts
10798
- var import_backstage_plugin_litellm_common5 = require("@acarmisc/backstage-plugin-litellm-common");
10853
+ var import_backstage_plugin_litellm_common6 = require("@acarmisc/backstage-plugin-litellm-common");
10799
10854
 
10800
10855
  // src/http/respondTeam.ts
10801
10856
  function respondTeam(res, team, ctx, caller) {
@@ -10890,7 +10945,7 @@ function registerTeamsRoutes(router, ctx) {
10890
10945
  permissions,
10891
10946
  catalogClient,
10892
10947
  teamAdminGroup: teamAdminCfg.group,
10893
- permission: import_backstage_plugin_litellm_common5.litellmTeamCreatePermission,
10948
+ permission: import_backstage_plugin_litellm_common6.litellmTeamCreatePermission,
10894
10949
  logger
10895
10950
  });
10896
10951
  if (!check.ok) {
@@ -10964,7 +11019,7 @@ function registerTeamsRoutes(router, ctx) {
10964
11019
  const authz = await authorizeTeamSubresource(
10965
11020
  req,
10966
11021
  res,
10967
- import_backstage_plugin_litellm_common5.litellmTeamManagePermission
11022
+ import_backstage_plugin_litellm_common6.litellmTeamManagePermission
10968
11023
  );
10969
11024
  if (!authz) return;
10970
11025
  const { teamId, owningGroup, actor, team: existing } = authz;
@@ -11005,7 +11060,7 @@ function registerTeamsRoutes(router, ctx) {
11005
11060
  const authz = await authorizeTeamSubresource(
11006
11061
  req,
11007
11062
  res,
11008
- import_backstage_plugin_litellm_common5.litellmTeamDeletePermission
11063
+ import_backstage_plugin_litellm_common6.litellmTeamDeletePermission
11009
11064
  );
11010
11065
  if (!authz) return;
11011
11066
  const { teamId, owningGroup, actor } = authz;
@@ -11046,7 +11101,7 @@ function registerTeamsRoutes(router, ctx) {
11046
11101
  permissions,
11047
11102
  catalogClient,
11048
11103
  teamAdminGroup: teamAdminCfg.group,
11049
- permission: import_backstage_plugin_litellm_common5.litellmTeamManagePermission,
11104
+ permission: import_backstage_plugin_litellm_common6.litellmTeamManagePermission,
11050
11105
  logger
11051
11106
  });
11052
11107
  if (!check.ok) {
@@ -11069,7 +11124,7 @@ function registerTeamsRoutes(router, ctx) {
11069
11124
  const authz = await authorizeTeamSubresource(
11070
11125
  req,
11071
11126
  res,
11072
- import_backstage_plugin_litellm_common5.litellmTeamMembersManagePermission,
11127
+ import_backstage_plugin_litellm_common6.litellmTeamMembersManagePermission,
11073
11128
  { allowTeamRole: true }
11074
11129
  );
11075
11130
  if (!authz) return;
@@ -11174,7 +11229,7 @@ function registerTeamsRoutes(router, ctx) {
11174
11229
  const authz = await authorizeTeamSubresource(
11175
11230
  req,
11176
11231
  res,
11177
- import_backstage_plugin_litellm_common5.litellmTeamMembersManagePermission,
11232
+ import_backstage_plugin_litellm_common6.litellmTeamMembersManagePermission,
11178
11233
  { allowTeamRole: true }
11179
11234
  );
11180
11235
  if (!authz) return;
@@ -11222,7 +11277,7 @@ function registerTeamsRoutes(router, ctx) {
11222
11277
  }
11223
11278
 
11224
11279
  // src/routes/objectPermissions.ts
11225
- var import_backstage_plugin_litellm_common6 = require("@acarmisc/backstage-plugin-litellm-common");
11280
+ var import_backstage_plugin_litellm_common7 = require("@acarmisc/backstage-plugin-litellm-common");
11226
11281
  function registerObjectPermissionsRoutes(router, ctx) {
11227
11282
  const {
11228
11283
  client,
@@ -11247,7 +11302,7 @@ function registerObjectPermissionsRoutes(router, ctx) {
11247
11302
  permissions,
11248
11303
  catalogClient,
11249
11304
  teamAdminGroup: teamAdminCfg.group,
11250
- permission: import_backstage_plugin_litellm_common6.litellmTeamKnowledgebaseManagePermission,
11305
+ permission: import_backstage_plugin_litellm_common7.litellmTeamKnowledgebaseManagePermission,
11251
11306
  logger
11252
11307
  });
11253
11308
  if (!check.ok) {
@@ -11269,7 +11324,7 @@ function registerObjectPermissionsRoutes(router, ctx) {
11269
11324
  const authz = await authorizeTeamSubresource(
11270
11325
  req,
11271
11326
  res,
11272
- import_backstage_plugin_litellm_common6.litellmTeamKnowledgebaseManagePermission
11327
+ import_backstage_plugin_litellm_common7.litellmTeamKnowledgebaseManagePermission
11273
11328
  );
11274
11329
  if (!authz) return;
11275
11330
  const { teamId, owningGroup, actor, team } = authz;
@@ -11329,7 +11384,7 @@ function registerObjectPermissionsRoutes(router, ctx) {
11329
11384
  permissions,
11330
11385
  catalogClient,
11331
11386
  teamAdminGroup: teamAdminCfg.group,
11332
- permission: import_backstage_plugin_litellm_common6.litellmTeamMcpManagePermission,
11387
+ permission: import_backstage_plugin_litellm_common7.litellmTeamMcpManagePermission,
11333
11388
  logger
11334
11389
  });
11335
11390
  if (!check.ok) {
@@ -11351,7 +11406,7 @@ function registerObjectPermissionsRoutes(router, ctx) {
11351
11406
  const authz = await authorizeTeamSubresource(
11352
11407
  req,
11353
11408
  res,
11354
- import_backstage_plugin_litellm_common6.litellmTeamMcpManagePermission
11409
+ import_backstage_plugin_litellm_common7.litellmTeamMcpManagePermission
11355
11410
  );
11356
11411
  if (!authz) return;
11357
11412
  const { teamId, owningGroup, actor, team } = authz;
@@ -11404,7 +11459,7 @@ function registerObjectPermissionsRoutes(router, ctx) {
11404
11459
  }
11405
11460
 
11406
11461
  // src/routes/teamUsage.ts
11407
- var import_backstage_plugin_litellm_common7 = require("@acarmisc/backstage-plugin-litellm-common");
11462
+ var import_backstage_plugin_litellm_common8 = require("@acarmisc/backstage-plugin-litellm-common");
11408
11463
  function registerTeamUsageRoutes(router, ctx) {
11409
11464
  const {
11410
11465
  client,
@@ -11502,7 +11557,7 @@ function registerTeamUsageRoutes(router, ctx) {
11502
11557
  }
11503
11558
  const role = team.members_with_roles?.find((m) => m.user_id === callerId)?.role;
11504
11559
  const viaRole = isMember && !!role && cfg.viewerRoles.includes(role);
11505
- const allowed = isManager || viaRole || isMember && cfg.permissionEnabled && await ctx.assertPermission(req, import_backstage_plugin_litellm_common7.litellmTeamUsageReadPermission);
11560
+ const allowed = isManager || viaRole || isMember && cfg.permissionEnabled && await ctx.assertPermission(req, import_backstage_plugin_litellm_common8.litellmTeamUsageReadPermission);
11506
11561
  if (!allowed) {
11507
11562
  res.status(403).json({
11508
11563
  error: "Access denied: viewing per-member usage needs a team role or permission"
@@ -11558,6 +11613,143 @@ function registerTeamUsageRoutes(router, ctx) {
11558
11613
  }
11559
11614
  }
11560
11615
 
11616
+ // src/langfuseClient.ts
11617
+ var DEFAULT_TIMEOUT2 = 15e3;
11618
+ var LangfuseUpstreamError = class extends Error {
11619
+ constructor(status, statusText) {
11620
+ super(`Langfuse API error: ${status} ${statusText}`);
11621
+ this.status = status;
11622
+ }
11623
+ };
11624
+ async function queryMetrics(baseUrl, publicKey, secretKey, queryObj, timeout = DEFAULT_TIMEOUT2) {
11625
+ const controller = new AbortController();
11626
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
11627
+ try {
11628
+ const url = `${baseUrl.replace(/\/$/, "")}/api/public/v2/metrics?query=${encodeURIComponent(
11629
+ JSON.stringify(queryObj)
11630
+ )}`;
11631
+ const response = await fetch(url, {
11632
+ signal: controller.signal,
11633
+ headers: {
11634
+ Authorization: `Basic ${Buffer.from(`${publicKey}:${secretKey}`).toString("base64")}`
11635
+ }
11636
+ });
11637
+ if (!response.ok) {
11638
+ throw new LangfuseUpstreamError(response.status, response.statusText);
11639
+ }
11640
+ return response.json();
11641
+ } finally {
11642
+ clearTimeout(timeoutId);
11643
+ }
11644
+ }
11645
+ function toNum(v) {
11646
+ if (typeof v === "number") return Number.isFinite(v) ? v : null;
11647
+ if (typeof v === "string" && v.trim() !== "") {
11648
+ const n = Number(v);
11649
+ return Number.isFinite(n) ? n : null;
11650
+ }
11651
+ return null;
11652
+ }
11653
+ function pick(row, names) {
11654
+ const scopes = [row, row?.metrics, row?.dimensions];
11655
+ for (const scope of scopes) {
11656
+ if (!scope || typeof scope !== "object") continue;
11657
+ for (const name of names) {
11658
+ const n = toNum(scope[name]);
11659
+ if (n !== null) return n;
11660
+ }
11661
+ }
11662
+ return null;
11663
+ }
11664
+ function pickText(row, names) {
11665
+ const scopes = [row, row?.metrics, row?.dimensions];
11666
+ for (const scope of scopes) {
11667
+ if (!scope || typeof scope !== "object") continue;
11668
+ for (const name of names) {
11669
+ const v = scope[name];
11670
+ if (typeof v === "string" && v) return v;
11671
+ }
11672
+ }
11673
+ return null;
11674
+ }
11675
+ function metricRows(body) {
11676
+ if (Array.isArray(body)) return body;
11677
+ if (Array.isArray(body?.data)) return body.data;
11678
+ if (Array.isArray(body?.results)) return body.results;
11679
+ if (Array.isArray(body?.rows)) return body.rows;
11680
+ return [];
11681
+ }
11682
+ function parseObservations(body) {
11683
+ const byModel = metricRows(body).map((row) => ({
11684
+ model: pickText(row, ["providedModelName", "model", "modelName"]) ?? "",
11685
+ cost: pick(row, ["sum_totalCost", "totalCost", "sum_total_cost", "cost", "sum_cost"]) ?? 0,
11686
+ tokens: pick(row, ["sum_totalTokens", "totalTokens", "sum_total_tokens", "tokens"]) ?? 0,
11687
+ avgLatencyMs: pick(row, ["avg_latency", "latency", "avg_latency_ms", "latencyMs"]),
11688
+ requests: pick(row, ["count_id", "count", "id_count", "requests", "observations"]) ?? 0
11689
+ })).filter((r) => r.model !== "");
11690
+ byModel.sort((a, b) => b.cost - a.cost);
11691
+ return byModel;
11692
+ }
11693
+ function parseScores(body) {
11694
+ const scores = metricRows(body).map((row) => ({
11695
+ name: pickText(row, ["name", "scoreName"]) ?? "",
11696
+ avg: pick(row, ["avg_value", "value", "avg"]) ?? NaN,
11697
+ count: pick(row, ["count_id", "count", "id_count", "scores"]) ?? 0
11698
+ })).filter((s) => s.name !== "" && Number.isFinite(s.avg));
11699
+ scores.sort((a, b) => a.name.localeCompare(b.name));
11700
+ return scores;
11701
+ }
11702
+ function toTimestamp(day2, endOfDay) {
11703
+ if (/^\d{4}-\d{2}-\d{2}$/.test(day2)) {
11704
+ return endOfDay ? `${day2}T23:59:59.999Z` : `${day2}T00:00:00.000Z`;
11705
+ }
11706
+ return day2;
11707
+ }
11708
+ async function getQualitySummary(cfg, opts) {
11709
+ if (!isLangfuseConfigured(cfg)) return null;
11710
+ const { baseUrl, publicKey, secretKey, userIdField } = cfg;
11711
+ const fromTimestamp = toTimestamp(opts.from, false);
11712
+ const toTimestampValue = toTimestamp(opts.to, true);
11713
+ try {
11714
+ const observations = await queryMetrics(baseUrl, publicKey, secretKey, {
11715
+ view: "observations",
11716
+ metrics: [
11717
+ { measure: "totalCost", aggregation: "SUM" },
11718
+ { measure: "totalTokens", aggregation: "SUM" },
11719
+ { measure: "latency", aggregation: "AVG" },
11720
+ { measure: "id", aggregation: "COUNT" }
11721
+ ],
11722
+ dimensions: [{ field: "providedModelName" }],
11723
+ filters: [
11724
+ { column: userIdField, operator: "=", value: opts.userId, type: "string" }
11725
+ ],
11726
+ fromTimestamp,
11727
+ toTimestamp: toTimestampValue,
11728
+ config: { row_limit: 50 }
11729
+ });
11730
+ let scores = [];
11731
+ try {
11732
+ const scoresBody = await queryMetrics(baseUrl, publicKey, secretKey, {
11733
+ view: "scores-numeric",
11734
+ metrics: [
11735
+ { measure: "value", aggregation: "AVG" },
11736
+ { measure: "id", aggregation: "COUNT" }
11737
+ ],
11738
+ dimensions: [{ field: "name" }],
11739
+ fromTimestamp,
11740
+ toTimestamp: toTimestampValue,
11741
+ config: { row_limit: 50 }
11742
+ });
11743
+ scores = parseScores(scoresBody);
11744
+ } catch {
11745
+ scores = [];
11746
+ }
11747
+ return { configured: true, byModel: parseObservations(observations), scores };
11748
+ } catch {
11749
+ return null;
11750
+ }
11751
+ }
11752
+
11561
11753
  // src/routes/insights.ts
11562
11754
  function registerInsightsRoutes(router, ctx) {
11563
11755
  const { client, catalogClient, auth, logger, auditGroup } = ctx;
@@ -11596,6 +11788,41 @@ function registerInsightsRoutes(router, ctx) {
11596
11788
  sendError(res, error, logger, "fetch spend logs");
11597
11789
  }
11598
11790
  });
11791
+ router.get("/insights/quality", requireUser, async (req, res) => {
11792
+ try {
11793
+ const { start_date, end_date, userId } = req.query;
11794
+ if (!start_date || !end_date) {
11795
+ res.status(400).json({ error: "start_date and end_date are required" });
11796
+ return;
11797
+ }
11798
+ const tokenEntityRef = res.locals.tokenEntityRef;
11799
+ await getProvisionedUser(ctx, res);
11800
+ const callerId = res.locals.userId;
11801
+ const isAudit = auditGroup ? await isUserMemberOfGroup(
11802
+ tokenEntityRef,
11803
+ auditGroup,
11804
+ catalogClient,
11805
+ auth,
11806
+ logger
11807
+ ) : false;
11808
+ if (!isLangfuseConfigured(ctx.langfuseCfg)) {
11809
+ res.json({ configured: false });
11810
+ return;
11811
+ }
11812
+ const summary = await getQualitySummary(ctx.langfuseCfg, {
11813
+ from: start_date,
11814
+ to: end_date,
11815
+ userId: isAudit ? userId || callerId : callerId
11816
+ });
11817
+ res.json(summary ?? { configured: false });
11818
+ } catch (error) {
11819
+ if (error instanceof ProvisioningError) {
11820
+ res.status(error.status).json(error.body);
11821
+ return;
11822
+ }
11823
+ sendError(res, error, logger, "fetch quality summary");
11824
+ }
11825
+ });
11599
11826
  }
11600
11827
 
11601
11828
  // node_modules/jose/dist/node/esm/runtime/base64url.js
@@ -13111,8 +13338,8 @@ async function bridgeListKeys(client, claims, provisioningEnabled, provisioningD
13111
13338
  }
13112
13339
 
13113
13340
  // src/routes/bridge.ts
13114
- var import_backstage_plugin_litellm_common8 = require("@acarmisc/backstage-plugin-litellm-common");
13115
13341
  var import_backstage_plugin_litellm_common9 = require("@acarmisc/backstage-plugin-litellm-common");
13342
+ var import_backstage_plugin_litellm_common10 = require("@acarmisc/backstage-plugin-litellm-common");
13116
13343
  function registerBridgeRoutes(router, ctx, bridgeOpts) {
13117
13344
  const {
13118
13345
  client,
@@ -13125,7 +13352,7 @@ function registerBridgeRoutes(router, ctx, bridgeOpts) {
13125
13352
  keyValidationConfig
13126
13353
  } = ctx;
13127
13354
  const { config } = bridgeOpts;
13128
- const generateKeyInputSchema = (0, import_backstage_plugin_litellm_common9.createGenerateKeyInputSchema)(ctx.keyValidationConfig);
13355
+ const generateKeyInputSchema = (0, import_backstage_plugin_litellm_common10.createGenerateKeyInputSchema)(ctx.keyValidationConfig);
13129
13356
  const bridgeCfg = readBridgeConfig(config);
13130
13357
  const bridgeIdentity = {
13131
13358
  userIdDomain,
@@ -13286,11 +13513,37 @@ function registerBridgeRoutes(router, ctx, bridgeOpts) {
13286
13513
  }
13287
13514
  const team = await client.getTeamInfo(teamId);
13288
13515
  const allowed = team.models;
13289
- if (!allowed || allowed.length === 0 || allowed.includes(import_backstage_plugin_litellm_common8.ALL_PROXY_MODELS)) {
13516
+ if (!allowed || allowed.length === 0 || allowed.includes(import_backstage_plugin_litellm_common9.ALL_PROXY_MODELS)) {
13290
13517
  res.json(models);
13291
13518
  return;
13292
13519
  }
13293
- res.json(models.filter((m) => (0, import_backstage_plugin_litellm_common8.isModelAllowed)(m, allowed)));
13520
+ res.json(models.filter((m) => (0, import_backstage_plugin_litellm_common9.isModelAllowed)(m, allowed)));
13521
+ } catch (error) {
13522
+ handleBridgeError(error, res);
13523
+ }
13524
+ });
13525
+ router.get("/bridge/mcp", async (req, res) => {
13526
+ try {
13527
+ const claims = await requireClaims(req);
13528
+ const teamId = typeof req.query.team_id === "string" ? req.query.team_id : "";
13529
+ if (!teamId) {
13530
+ res.status(400).json({ error: "team_id is required" });
13531
+ return;
13532
+ }
13533
+ const user = await getOrProvisionUserFromClaims(
13534
+ client,
13535
+ claims,
13536
+ provisioningEnabled,
13537
+ provisioningDefaults,
13538
+ logger,
13539
+ bridgeIdentity
13540
+ );
13541
+ if (!(user.teams ?? []).includes(teamId)) {
13542
+ res.status(403).json({ error: "Access denied: team is not one of your teams", team_id: teamId });
13543
+ return;
13544
+ }
13545
+ const names = teamMcpServers(await client.getTeamInfo(teamId));
13546
+ res.json(names.map((server_name) => ({ server_name })));
13294
13547
  } catch (error) {
13295
13548
  handleBridgeError(error, res);
13296
13549
  }
@@ -13322,13 +13575,13 @@ async function createRouter(options) {
13322
13575
  const keyMaxBudget = config.getOptionalNumber("litellm.keys.maxBudget") ?? 100;
13323
13576
  const keyMaxTpm = config.getOptionalNumber("litellm.keys.maxTpm") ?? 1e5;
13324
13577
  const keyMaxRpm = config.getOptionalNumber("litellm.keys.maxRpm") ?? 1e3;
13325
- const keyAllowedDurations = config.getOptionalStringArray("litellm.keys.allowedDurations") ?? import_backstage_plugin_litellm_common10.DEFAULT_KEY_DURATIONS;
13578
+ const keyAllowedDurations = config.getOptionalStringArray("litellm.keys.allowedDurations") ?? import_backstage_plugin_litellm_common11.DEFAULT_KEY_DURATIONS;
13326
13579
  const keyValidationConfig = {
13327
13580
  maxBudget: keyMaxBudget,
13328
13581
  maxTpm: keyMaxTpm,
13329
13582
  maxRpm: keyMaxRpm,
13330
13583
  allowedDurations: keyAllowedDurations,
13331
- allowedBudgetDurations: config.getOptionalStringArray("litellm.keys.allowedBudgetDurations") ?? import_backstage_plugin_litellm_common10.DEFAULT_KEY_BUDGET_DURATIONS
13584
+ allowedBudgetDurations: config.getOptionalStringArray("litellm.keys.allowedBudgetDurations") ?? import_backstage_plugin_litellm_common11.DEFAULT_KEY_BUDGET_DURATIONS
13332
13585
  };
13333
13586
  const teamMgmtEnabled = isTeamManagementEnabled(config);
13334
13587
  const objectPermsEnabled = isObjectPermissionsEnabled(config);
@@ -13336,6 +13589,7 @@ async function createRouter(options) {
13336
13589
  const teamBudgetVisibility = readTeamBudgetVisibility(config);
13337
13590
  const opencodeCfg = readOpencodeConfig(config);
13338
13591
  const memberBreakdownCfg = readMemberBreakdownConfig(config);
13592
+ const langfuseCfg = readLangfuseConfig(config);
13339
13593
  const catalogClient = options.catalogClient ?? new import_catalog_client.CatalogClient({ discoveryApi: discovery });
13340
13594
  if (provisioningEnabled) {
13341
13595
  logger.info(
@@ -13382,6 +13636,7 @@ async function createRouter(options) {
13382
13636
  teamBudgetVisibility,
13383
13637
  opencodeCfg,
13384
13638
  memberBreakdownCfg,
13639
+ langfuseCfg,
13385
13640
  // Helper functions - defined inline below this object
13386
13641
  authorizeKeyAction: async (req, keyId) => {
13387
13642
  if (!req.res || !req.res.locals.tokenEntityRef || !req.res.locals.userId) {
@@ -13539,10 +13794,11 @@ async function createRouter(options) {
13539
13794
  }
13540
13795
 
13541
13796
  // src/plugin.ts
13542
- var import_backstage_plugin_litellm_common11 = require("@acarmisc/backstage-plugin-litellm-common");
13797
+ var import_backstage_plugin_litellm_common12 = require("@acarmisc/backstage-plugin-litellm-common");
13543
13798
  var BRIDGE_UNAUTHENTICATED_PATHS = [
13544
13799
  "/bridge/health",
13545
13800
  "/bridge/keys",
13801
+ "/bridge/mcp",
13546
13802
  "/bridge/models",
13547
13803
  "/bridge/user/info"
13548
13804
  ];
@@ -13568,7 +13824,7 @@ var litellmPlugin = (0, import_backend_plugin_api.createBackendPlugin)({
13568
13824
  permissions,
13569
13825
  permissionsRegistry
13570
13826
  }) {
13571
- permissionsRegistry.addPermissions(import_backstage_plugin_litellm_common11.litellmPermissions);
13827
+ permissionsRegistry.addPermissions(import_backstage_plugin_litellm_common12.litellmPermissions);
13572
13828
  const router = await createRouter({
13573
13829
  config,
13574
13830
  logger,
@@ -13588,7 +13844,7 @@ var litellmPlugin = (0, import_backend_plugin_api.createBackendPlugin)({
13588
13844
  });
13589
13845
 
13590
13846
  // src/index.ts
13591
- var import_backstage_plugin_litellm_common12 = require("@acarmisc/backstage-plugin-litellm-common");
13847
+ var import_backstage_plugin_litellm_common13 = require("@acarmisc/backstage-plugin-litellm-common");
13592
13848
  // Annotate the CommonJS export names for ESM import in node:
13593
13849
  0 && (module.exports = {
13594
13850
  KeycloakJWTVerifier,