@acarmisc/backstage-plugin-litellm-backend 0.18.0 → 0.19.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
@@ -7976,7 +7976,6 @@ __export(index_exports, {
7976
7976
  ProvisioningError: () => ProvisioningError,
7977
7977
  applyRoleOverrides: () => applyRoleOverrides,
7978
7978
  assertTeamAdmin: () => assertTeamAdmin,
7979
- bridgeGenerateKey: () => bridgeGenerateKey,
7980
7979
  bridgeListKeys: () => bridgeListKeys,
7981
7980
  createRouter: () => createRouter,
7982
7981
  default: () => litellmPlugin,
@@ -7998,7 +7997,6 @@ __export(index_exports, {
7998
7997
  litellmTeamMcpManagePermission: () => import_backstage_plugin_litellm_common11.litellmTeamMcpManagePermission,
7999
7998
  litellmTeamMembersManagePermission: () => import_backstage_plugin_litellm_common11.litellmTeamMembersManagePermission,
8000
7999
  newDefaultVerifier: () => newDefaultVerifier,
8001
- normalizeRequestTags: () => normalizeRequestTags,
8002
8000
  provisionUser: () => provisionUser,
8003
8001
  readBridgeConfig: () => readBridgeConfig,
8004
8002
  readProvisioningDefaults: () => readProvisioningDefaults,
@@ -8024,16 +8022,11 @@ var import_backend_plugin_api = require("@backstage/backend-plugin-api");
8024
8022
  // src/router.ts
8025
8023
  var import_express = __toESM(require("express"));
8026
8024
  var import_plugin_permission_common2 = __toESM(require_index_cjs2());
8027
- var import_errors20 = __toESM(require_index_cjs());
8025
+ var import_errors21 = __toESM(require_index_cjs());
8028
8026
  var import_catalog_client = require("@backstage/catalog-client");
8029
8027
 
8030
8028
  // src/client.ts
8031
8029
  var DEFAULT_TIMEOUT = 3e4;
8032
- function normalizeRequestTags(tags) {
8033
- if (!tags) return [];
8034
- if (Array.isArray(tags)) return tags.filter((t) => typeof t === "string");
8035
- return Object.entries(tags).map(([k, v]) => `${k}:${v}`);
8036
- }
8037
8030
  var LiteLLMUpstreamError = class extends Error {
8038
8031
  constructor(status, statusText, body) {
8039
8032
  let message2 = `LiteLLM API error: ${status} ${statusText}`;
@@ -8089,6 +8082,29 @@ var LiteLLMClient = class {
8089
8082
  clearTimeout(timeoutId);
8090
8083
  }
8091
8084
  }
8085
+ /**
8086
+ * Normalizes a raw LiteLLM user response to the UserInfo contract.
8087
+ * Handles both `/user/info` (with `user_info` wrapper) and `/user/list`
8088
+ * response shapes, and converts team arrays to team_id strings.
8089
+ */
8090
+ normalizeUserInfo(raw, fallbackUserId) {
8091
+ const inner = raw?.user_info ?? {};
8092
+ const teamIds = Array.isArray(raw?.teams) ? raw.teams.map((t) => typeof t === "string" ? t : t?.team_id).filter((t) => typeof t === "string") : [];
8093
+ return {
8094
+ user_id: raw?.user_id ?? inner.user_id ?? fallbackUserId ?? "",
8095
+ user_email: inner.user_email ?? raw?.user_email,
8096
+ email: inner.email ?? raw?.email,
8097
+ teams: teamIds,
8098
+ models: inner.models ?? raw?.models,
8099
+ max_budget: inner.max_budget ?? raw?.max_budget,
8100
+ budget_duration: inner.budget_duration ?? raw?.budget_duration,
8101
+ budget_reset_at: inner.budget_reset_at ?? raw?.budget_reset_at,
8102
+ spend: inner.spend ?? raw?.spend,
8103
+ current_spend: inner.current_spend ?? raw?.current_spend,
8104
+ soft_limit: inner.soft_limit ?? raw?.soft_limit,
8105
+ hard_limit: inner.hard_limit ?? raw?.hard_limit
8106
+ };
8107
+ }
8092
8108
  /**
8093
8109
  * Returns null when the user is not found in LiteLLM (404).
8094
8110
  * Throws on all other errors so callers know something went wrong.
@@ -8102,22 +8118,7 @@ var LiteLLMClient = class {
8102
8118
  const query = userId ? `?user_id=${encodeURIComponent(userId)}` : "";
8103
8119
  try {
8104
8120
  const raw = await this.request(`/user/info${query}`);
8105
- const inner = raw?.user_info ?? {};
8106
- const teamIds = Array.isArray(raw?.teams) ? raw.teams.map((t) => typeof t === "string" ? t : t?.team_id).filter((t) => typeof t === "string") : [];
8107
- return {
8108
- user_id: raw?.user_id ?? inner.user_id ?? userId ?? "",
8109
- user_email: inner.user_email ?? raw?.user_email,
8110
- email: inner.email ?? raw?.email,
8111
- teams: teamIds,
8112
- models: inner.models ?? raw?.models,
8113
- max_budget: inner.max_budget ?? raw?.max_budget,
8114
- budget_duration: inner.budget_duration ?? raw?.budget_duration,
8115
- budget_reset_at: inner.budget_reset_at ?? raw?.budget_reset_at,
8116
- spend: inner.spend ?? raw?.spend,
8117
- current_spend: inner.current_spend ?? raw?.current_spend,
8118
- soft_limit: inner.soft_limit ?? raw?.soft_limit,
8119
- hard_limit: inner.hard_limit ?? raw?.hard_limit
8120
- };
8121
+ return this.normalizeUserInfo(raw, userId);
8121
8122
  } catch (err) {
8122
8123
  if (err instanceof LiteLLMUpstreamError && err.status === 404) return null;
8123
8124
  throw err;
@@ -8144,22 +8145,7 @@ var LiteLLMClient = class {
8144
8145
  (u) => (u.user_email ?? u.email ?? "").toLowerCase() === email.toLowerCase()
8145
8146
  );
8146
8147
  if (!found) return null;
8147
- const inner = found.user_info ?? {};
8148
- const teamIds = Array.isArray(found.teams) ? found.teams.map((t) => typeof t === "string" ? t : t?.team_id).filter((t) => typeof t === "string") : [];
8149
- return {
8150
- user_id: found.user_id ?? inner.user_id ?? "",
8151
- user_email: inner.user_email ?? found.user_email,
8152
- email: inner.email ?? found.email,
8153
- teams: teamIds,
8154
- models: inner.models ?? found.models,
8155
- max_budget: inner.max_budget ?? found.max_budget,
8156
- budget_duration: inner.budget_duration ?? found.budget_duration,
8157
- budget_reset_at: inner.budget_reset_at ?? found.budget_reset_at,
8158
- spend: inner.spend ?? found.spend,
8159
- current_spend: inner.current_spend ?? found.current_spend,
8160
- soft_limit: inner.soft_limit ?? found.soft_limit,
8161
- hard_limit: inner.hard_limit ?? found.hard_limit
8162
- };
8148
+ return this.normalizeUserInfo(found);
8163
8149
  } catch (err) {
8164
8150
  if (err instanceof LiteLLMUpstreamError && err.status === 404) return null;
8165
8151
  throw err;
@@ -8423,18 +8409,6 @@ var LiteLLMClient = class {
8423
8409
  body: JSON.stringify({ team_ids: [teamId] })
8424
8410
  });
8425
8411
  }
8426
- async blockTeam(teamId) {
8427
- return this.request("/team/block", {
8428
- method: "POST",
8429
- body: JSON.stringify({ team_id: teamId })
8430
- });
8431
- }
8432
- async unblockTeam(teamId) {
8433
- return this.request("/team/unblock", {
8434
- method: "POST",
8435
- body: JSON.stringify({ team_id: teamId })
8436
- });
8437
- }
8438
8412
  /**
8439
8413
  * Adds a member to a team. LiteLLM's `/team/member_add` nests the member
8440
8414
  * under a `member` object; `role` is 'user' or 'admin' (we only ever send
@@ -8498,43 +8472,6 @@ var LiteLLMClient = class {
8498
8472
  url: r?.url ?? void 0
8499
8473
  })).filter((r) => r.id);
8500
8474
  }
8501
- /**
8502
- * Reads per-request spend logs from LiteLLM (`GET /spend/logs`), filtered
8503
- * by date range and optionally by key/user/team. Rows are normalised so
8504
- * `request_tags` is always a `string[]` (LiteLLM returns either an array
8505
- * or an object depending on version) — callers group by tag to attribute
8506
- * spend to a conversation or caller.
8507
- *
8508
- * This is the admin/DB-backed endpoint: it can return a large number of
8509
- * rows, so callers should keep the date window tight and cap `page_size`.
8510
- */
8511
- async getSpendLogs(params) {
8512
- const query = new URLSearchParams({
8513
- start_date: params.start_date,
8514
- end_date: params.end_date
8515
- });
8516
- if (params.api_key) query.append("api_key", params.api_key);
8517
- if (params.user_id) query.append("user_id", params.user_id);
8518
- if (params.team_id) query.append("team_id", params.team_id);
8519
- if (params.page_size) query.append("page_size", String(params.page_size));
8520
- const raw = await this.request(`/spend/logs?${query.toString()}`);
8521
- const rows = Array.isArray(raw) ? raw : raw?.data ?? [];
8522
- return rows.map((row) => ({
8523
- request_id: row?.request_id,
8524
- startTime: row?.startTime ?? row?.start_time,
8525
- endTime: row?.endTime ?? row?.end_time,
8526
- spend: row?.spend ?? 0,
8527
- total_tokens: row?.total_tokens ?? 0,
8528
- prompt_tokens: row?.prompt_tokens ?? 0,
8529
- completion_tokens: row?.completion_tokens ?? 0,
8530
- model: row?.model,
8531
- api_key: row?.api_key,
8532
- user: row?.user,
8533
- team_id: row?.team_id,
8534
- request_tags: normalizeRequestTags(row?.request_tags),
8535
- metadata: row?.metadata
8536
- }));
8537
- }
8538
8475
  emptyUsage() {
8539
8476
  return {
8540
8477
  total_spend: 0,
@@ -8698,6 +8635,114 @@ var LiteLLMClient = class {
8698
8635
  }
8699
8636
  };
8700
8637
 
8638
+ // src/errors.ts
8639
+ var HTML_TAG_PATTERN = /<[^>]*>|&[a-z]+;/gi;
8640
+ function isHttpishError(error) {
8641
+ return error !== null && typeof error === "object" && "status" in error && "body" in error && typeof error.status === "number" && // any: property access after guard check
8642
+ typeof error.body === "object";
8643
+ }
8644
+ function sanitizeUpstreamMessage(msg) {
8645
+ if (!msg || typeof msg !== "string") return "";
8646
+ return msg.replace(/Bearer\s+[A-Za-z0-9._\-+/=]+/g, "Bearer [redacted]").replace(/sk-[A-Za-z0-9_\-]{8,}/g, "sk-[redacted]").replace(HTML_TAG_PATTERN, "").replace(/\s+/g, " ").trim().slice(0, 500);
8647
+ }
8648
+ function toHttpError(error) {
8649
+ if (isHttpishError(error)) {
8650
+ return {
8651
+ status: error.status,
8652
+ body: error.body,
8653
+ logLevel: "warn"
8654
+ };
8655
+ }
8656
+ if (error instanceof LiteLLMUpstreamError) {
8657
+ if (error.status === 401 || error.status === 403) {
8658
+ return {
8659
+ status: 502,
8660
+ body: { error: "LiteLLM rejected the request" },
8661
+ logLevel: "warn"
8662
+ };
8663
+ }
8664
+ if (error.status >= 500 || error.status < 100) {
8665
+ return {
8666
+ status: 502,
8667
+ body: { error: "LiteLLM is unavailable" },
8668
+ logLevel: "error"
8669
+ };
8670
+ }
8671
+ return {
8672
+ status: error.status === 404 || error.status === 409 ? error.status : 400,
8673
+ body: {
8674
+ error: sanitizeUpstreamMessage(error.message),
8675
+ ...error.param ? { param: error.param } : {}
8676
+ },
8677
+ logLevel: "warn"
8678
+ };
8679
+ }
8680
+ if (error && typeof error === "object" && "name" in error && "message" in error) {
8681
+ const name = error.name;
8682
+ const message3 = String(error.message);
8683
+ if (name === "InputError") {
8684
+ return {
8685
+ status: 400,
8686
+ body: { error: message3 },
8687
+ logLevel: "warn"
8688
+ };
8689
+ }
8690
+ if (name === "NotAllowedError") {
8691
+ return {
8692
+ status: 403,
8693
+ body: { error: message3 },
8694
+ logLevel: "warn"
8695
+ };
8696
+ }
8697
+ if (name === "NotFoundError") {
8698
+ return {
8699
+ status: 404,
8700
+ body: { error: message3 },
8701
+ logLevel: "warn"
8702
+ };
8703
+ }
8704
+ if (name === "ConflictError") {
8705
+ return {
8706
+ status: 409,
8707
+ body: { error: message3 },
8708
+ logLevel: "warn"
8709
+ };
8710
+ }
8711
+ }
8712
+ if (error instanceof Error) {
8713
+ const code = error.code;
8714
+ const cause = error.cause;
8715
+ const networkCodes = ["ECONNREFUSED", "ECONNRESET", "ENOTFOUND", "ETIMEDOUT", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT"];
8716
+ if (error.name === "AbortError" || error.name === "TimeoutError" || error.name === "TypeError" && error.message === "fetch failed" || typeof code === "string" && networkCodes.includes(code) || typeof cause?.code === "string" && networkCodes.includes(cause.code)) {
8717
+ return {
8718
+ status: 502,
8719
+ body: { error: "LiteLLM is unavailable" },
8720
+ logLevel: "error",
8721
+ _logMessage: error.message
8722
+ };
8723
+ }
8724
+ }
8725
+ const message2 = error instanceof Error ? error.message : String(error);
8726
+ return {
8727
+ status: 500,
8728
+ body: { error: "Internal error" },
8729
+ logLevel: "error",
8730
+ _logMessage: message2
8731
+ };
8732
+ }
8733
+ function sendError(res, error, logger, context) {
8734
+ const { status, body, logLevel, _logMessage } = toHttpError(error);
8735
+ const realMessage = _logMessage || (error instanceof Error ? error.message : String(error));
8736
+ if (logLevel === "error") {
8737
+ logger.error(`Failed to ${context}`, error instanceof Error ? error : new Error(String(error)));
8738
+ } else if (logLevel === "warn") {
8739
+ logger.warn(`${context}: ${realMessage}`);
8740
+ } else {
8741
+ logger.info(`${context}: ${realMessage}`);
8742
+ }
8743
+ res.status(status).json(body);
8744
+ }
8745
+
8701
8746
  // src/provisioning.ts
8702
8747
  function toLiteLLMUserId(userEntityRef, userIdDomain) {
8703
8748
  const name = userEntityRef.split("/").pop() ?? userEntityRef;
@@ -8754,8 +8799,9 @@ function readProvisioningDefaults(config) {
8754
8799
  return { enabled, defaults };
8755
8800
  }
8756
8801
  async function resolveUserId(req, auth) {
8757
- const rawToken = req.headers.authorization?.slice(7);
8758
- if (!rawToken) return void 0;
8802
+ const authHeader = req.headers.authorization ?? "";
8803
+ if (!authHeader.startsWith("Bearer ")) return void 0;
8804
+ const rawToken = authHeader.slice(7);
8759
8805
  try {
8760
8806
  const credentials = await auth.authenticate(rawToken);
8761
8807
  const principal = credentials.principal;
@@ -8767,8 +8813,9 @@ async function resolveUserId(req, auth) {
8767
8813
  return void 0;
8768
8814
  }
8769
8815
  async function resolveCredentials(req, auth) {
8770
- const rawToken = req.headers.authorization?.slice(7);
8771
- if (!rawToken) return void 0;
8816
+ const authHeader = req.headers.authorization ?? "";
8817
+ if (!authHeader.startsWith("Bearer ")) return void 0;
8818
+ const rawToken = authHeader.slice(7);
8772
8819
  try {
8773
8820
  return await auth.authenticate(rawToken);
8774
8821
  } catch {
@@ -8847,10 +8894,6 @@ async function provisionUser(client, userId, defaults, profile, backstageEntity,
8847
8894
  }
8848
8895
  }
8849
8896
  var provisioningInFlight = /* @__PURE__ */ new Map();
8850
- function sanitizeUpstreamMessage(message2) {
8851
- if (!message2) return "unknown error";
8852
- return message2.replace(/Bearer\s+[A-Za-z0-9._\-+/=]+/g, "Bearer [redacted]").replace(/sk-[A-Za-z0-9_\-]{8,}/g, "sk-[redacted]").slice(0, 500);
8853
- }
8854
8897
  var ProvisioningError = class extends Error {
8855
8898
  constructor(message2, hint, provisioning, status = 404) {
8856
8899
  super(message2);
@@ -9254,114 +9297,6 @@ function readOpencodeConfig(config) {
9254
9297
  };
9255
9298
  }
9256
9299
 
9257
- // src/errors.ts
9258
- var HTML_TAG_PATTERN = /<[^>]*>|&[a-z]+;/gi;
9259
- function isHttpishError(error) {
9260
- return error !== null && typeof error === "object" && "status" in error && "body" in error && typeof error.status === "number" && // any: property access after guard check
9261
- typeof error.body === "object";
9262
- }
9263
- function sanitizeUpstreamMessage2(msg) {
9264
- if (!msg || typeof msg !== "string") return "";
9265
- return msg.replace(HTML_TAG_PATTERN, "").replace(/\s+/g, " ").trim().slice(0, 500);
9266
- }
9267
- function toHttpError(error) {
9268
- if (isHttpishError(error)) {
9269
- return {
9270
- status: error.status,
9271
- body: error.body,
9272
- logLevel: "warn"
9273
- };
9274
- }
9275
- if (error instanceof LiteLLMUpstreamError) {
9276
- if (error.status === 401 || error.status === 403) {
9277
- return {
9278
- status: 502,
9279
- body: { error: "LiteLLM rejected the request" },
9280
- logLevel: "warn"
9281
- };
9282
- }
9283
- if (error.status >= 500 || error.status < 100) {
9284
- return {
9285
- status: 502,
9286
- body: { error: "LiteLLM is unavailable" },
9287
- logLevel: "error"
9288
- };
9289
- }
9290
- return {
9291
- status: error.status === 404 || error.status === 409 ? error.status : 400,
9292
- body: {
9293
- error: sanitizeUpstreamMessage2(error.message),
9294
- ...error.param ? { param: error.param } : {}
9295
- },
9296
- logLevel: "warn"
9297
- };
9298
- }
9299
- if (error && typeof error === "object" && "name" in error && "message" in error) {
9300
- const name = error.name;
9301
- const message3 = String(error.message);
9302
- if (name === "InputError") {
9303
- return {
9304
- status: 400,
9305
- body: { error: message3 },
9306
- logLevel: "warn"
9307
- };
9308
- }
9309
- if (name === "NotAllowedError") {
9310
- return {
9311
- status: 403,
9312
- body: { error: message3 },
9313
- logLevel: "warn"
9314
- };
9315
- }
9316
- if (name === "NotFoundError") {
9317
- return {
9318
- status: 404,
9319
- body: { error: message3 },
9320
- logLevel: "warn"
9321
- };
9322
- }
9323
- if (name === "ConflictError") {
9324
- return {
9325
- status: 409,
9326
- body: { error: message3 },
9327
- logLevel: "warn"
9328
- };
9329
- }
9330
- }
9331
- if (error instanceof Error) {
9332
- const code = error.code;
9333
- const cause = error.cause;
9334
- const networkCodes = ["ECONNREFUSED", "ECONNRESET", "ENOTFOUND", "ETIMEDOUT", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT"];
9335
- if (error.name === "AbortError" || error.name === "TimeoutError" || error.name === "TypeError" && error.message === "fetch failed" || typeof code === "string" && networkCodes.includes(code) || typeof cause?.code === "string" && networkCodes.includes(cause.code)) {
9336
- return {
9337
- status: 502,
9338
- body: { error: "LiteLLM is unavailable" },
9339
- logLevel: "error",
9340
- _logMessage: error.message
9341
- };
9342
- }
9343
- }
9344
- const message2 = error instanceof Error ? error.message : String(error);
9345
- return {
9346
- status: 500,
9347
- body: { error: "Internal error" },
9348
- logLevel: "error",
9349
- _logMessage: message2
9350
- };
9351
- }
9352
- function sendError(res, error, logger, context) {
9353
- const { status, body, logLevel, _logMessage } = toHttpError(error);
9354
- const realMessage = _logMessage || (error instanceof Error ? error.message : String(error));
9355
- if (logLevel === "error") {
9356
- logger.error(`Failed to ${context}`, error instanceof Error ? error : new Error(String(error)));
9357
- } else if (logLevel === "warn") {
9358
- logger.warn(`${context}: ${realMessage}`);
9359
- } else {
9360
- logger.info(`${context}: ${realMessage}`);
9361
- }
9362
- res.status(status).json(body);
9363
- }
9364
-
9365
9300
  // src/routes/middleware/withUser.ts
9366
9301
  function createRequireUser(ctx) {
9367
9302
  return async (req, res, next) => {
@@ -9897,56 +9832,9 @@ async function createKeyForUser(user, input, ctx) {
9897
9832
  config,
9898
9833
  catalogClient,
9899
9834
  auth,
9900
- logger,
9901
- provisioningEnabled,
9902
- provisioningDefaults,
9903
- roleConfigs
9835
+ logger
9904
9836
  } = ctx;
9905
- let userInfo;
9906
- if (catalogClient !== void 0 && catalogClient !== null && auth !== void 0 && auth !== null) {
9907
- userInfo = await getOrProvisionUser(
9908
- client,
9909
- user.tokenEntityRef || "",
9910
- user.userId,
9911
- provisioningEnabled,
9912
- provisioningDefaults,
9913
- roleConfigs,
9914
- catalogClient,
9915
- auth,
9916
- logger
9917
- );
9918
- } else {
9919
- userInfo = await client.getUserInfo(user.userId);
9920
- if (!userInfo) {
9921
- if (!provisioningEnabled) {
9922
- throw new KeyServiceError(
9923
- 404,
9924
- {
9925
- error: "User not found in LiteLLM",
9926
- detail: "No LiteLLM user for this identity. Log in to Backstage once to be provisioned, or enable litellm.provisioning.enabled."
9927
- }
9928
- );
9929
- }
9930
- const created = await client.createUser({
9931
- user_id: user.userId,
9932
- user_email: user.userId,
9933
- max_budget: provisioningDefaults.maxBudget,
9934
- budget_duration: provisioningDefaults.budgetDuration,
9935
- models: provisioningDefaults.models,
9936
- teams: provisioningDefaults.teams?.length ? [provisioningDefaults.teams[0]] : []
9937
- });
9938
- if (!created) {
9939
- throw new KeyServiceError(
9940
- 500,
9941
- {
9942
- error: "User provisioning failed",
9943
- detail: "Failed to create user in LiteLLM"
9944
- }
9945
- );
9946
- }
9947
- userInfo = await client.getUserInfo(user.userId);
9948
- }
9949
- }
9837
+ const userInfo = user.userInfo;
9950
9838
  if (!config.allowUnlimitedBudget && (input.max_budget === null || input.max_budget === void 0)) {
9951
9839
  throw new KeyServiceError(
9952
9840
  400,
@@ -9992,13 +9880,15 @@ async function createKeyForUser(user, input, ctx) {
9992
9880
  }
9993
9881
  const profile = user.tokenEntityRef && catalogClient !== void 0 && catalogClient !== null && auth !== void 0 && auth !== null ? await resolveUserProfile(user.tokenEntityRef, catalogClient, auth, logger) : {};
9994
9882
  const clientMetadata = input.metadata ?? {};
9883
+ const createdVia = user.createdVia ?? "backstage";
9995
9884
  const serverMetadata = {
9996
- created_by_backstage_user: user.tokenEntityRef ?? "unknown",
9885
+ ...user.tokenEntityRef && { created_by_backstage_user: user.tokenEntityRef },
9886
+ ...!user.tokenEntityRef && { created_by: userInfo.user_id },
9997
9887
  ...profile.email && { created_by_email: profile.email },
9998
9888
  ...profile.displayName && {
9999
9889
  created_by_display_name: profile.displayName
10000
9890
  },
10001
- created_via: "backstage",
9891
+ created_via: createdVia,
10002
9892
  created_at_iso: (/* @__PURE__ */ new Date()).toISOString()
10003
9893
  };
10004
9894
  const enrichedMetadata = {
@@ -10007,7 +9897,7 @@ async function createKeyForUser(user, input, ctx) {
10007
9897
  };
10008
9898
  const upstreamRequest = {
10009
9899
  alias: input.alias,
10010
- user_id: user.userId
9900
+ user_id: userInfo.user_id
10011
9901
  };
10012
9902
  if (input.models !== void 0) {
10013
9903
  upstreamRequest.models = input.models;
@@ -10032,7 +9922,7 @@ async function createKeyForUser(user, input, ctx) {
10032
9922
  }
10033
9923
  upstreamRequest.metadata = enrichedMetadata;
10034
9924
  const result = await client.generateKey(upstreamRequest);
10035
- logger.info("key.generate", { userId: user.userId, keyAlias: input.alias });
9925
+ logger.info("key.generate", { userId: userInfo.user_id, keyAlias: input.alias });
10036
9926
  return result;
10037
9927
  }
10038
9928
 
@@ -10043,17 +9933,12 @@ function registerKeysRoutes(router, ctx) {
10043
9933
  catalogClient,
10044
9934
  auth,
10045
9935
  logger,
10046
- userIdDomain,
10047
- provisioningEnabled,
10048
- provisioningDefaults,
10049
- roleConfigs,
10050
9936
  auditGroup,
10051
9937
  allowUnlimitedBudget,
10052
9938
  teamRequired,
10053
9939
  allowOwnerResetSpend,
10054
9940
  keyValidationConfig,
10055
9941
  authorizeKeyAction,
10056
- sendOwnershipError,
10057
9942
  assertPermission,
10058
9943
  sendPermissionDenied
10059
9944
  } = ctx;
@@ -10125,7 +10010,7 @@ function registerKeysRoutes(router, ctx) {
10125
10010
  const keyId = key.token ?? key.key;
10126
10011
  let errorMsg = "Unknown error";
10127
10012
  if (err instanceof Error) {
10128
- errorMsg = sanitizeUpstreamMessage2(err.message) || "Unknown error";
10013
+ errorMsg = sanitizeUpstreamMessage(err.message) || "Unknown error";
10129
10014
  }
10130
10015
  failures.push({ keyId, error: errorMsg });
10131
10016
  logger.warn("key.prune-expired failed", {
@@ -10165,7 +10050,7 @@ function registerKeysRoutes(router, ctx) {
10165
10050
  return;
10166
10051
  }
10167
10052
  const tokenEntityRef = res.locals.tokenEntityRef;
10168
- const resolvedUserId = res.locals.userId;
10053
+ const userInfo = await getProvisionedUser(ctx, res);
10169
10054
  const keyCreateCtx = {
10170
10055
  client,
10171
10056
  config: {
@@ -10175,14 +10060,10 @@ function registerKeysRoutes(router, ctx) {
10175
10060
  catalogClient,
10176
10061
  auth,
10177
10062
  logger,
10178
- keyValidationConfig,
10179
- provisioningEnabled,
10180
- provisioningDefaults,
10181
- roleConfigs,
10182
- userIdDomain
10063
+ keyValidationConfig
10183
10064
  };
10184
10065
  const result = await createKeyForUser(
10185
- { tokenEntityRef, userId: resolvedUserId },
10066
+ { tokenEntityRef, userInfo },
10186
10067
  input,
10187
10068
  keyCreateCtx
10188
10069
  );
@@ -10282,7 +10163,6 @@ function registerKeysRoutes(router, ctx) {
10282
10163
  logger.info("key.update", { userId: tokenEntityRef ?? "unknown", keyId });
10283
10164
  res.json(result);
10284
10165
  } catch (error) {
10285
- if (sendOwnershipError(error, res)) return;
10286
10166
  sendError(res, error, logger, "update key");
10287
10167
  }
10288
10168
  });
@@ -10302,7 +10182,6 @@ function registerKeysRoutes(router, ctx) {
10302
10182
  logger.info("key.delete", { userId: tokenEntityRef ?? "unknown", keyId });
10303
10183
  res.json({ success: true });
10304
10184
  } catch (error) {
10305
- if (sendOwnershipError(error, res)) return;
10306
10185
  sendError(res, error, logger, "delete key");
10307
10186
  }
10308
10187
  });
@@ -10331,7 +10210,6 @@ function registerKeysRoutes(router, ctx) {
10331
10210
  logger.info("key.block", { userId: tokenEntityRef ?? "unknown", keyId });
10332
10211
  res.json({ success: true });
10333
10212
  } catch (error) {
10334
- if (sendOwnershipError(error, res)) return;
10335
10213
  sendError(res, error, logger, "block key");
10336
10214
  }
10337
10215
  });
@@ -10356,7 +10234,6 @@ function registerKeysRoutes(router, ctx) {
10356
10234
  logger.info("key.unblock", { userId: tokenEntityRef ?? "unknown", keyId });
10357
10235
  res.json({ success: true });
10358
10236
  } catch (error) {
10359
- if (sendOwnershipError(error, res)) return;
10360
10237
  sendError(res, error, logger, "unblock key");
10361
10238
  }
10362
10239
  });
@@ -10378,7 +10255,6 @@ function registerKeysRoutes(router, ctx) {
10378
10255
  logger.info("key.reset_spend", { userId: tokenEntityRef ?? "unknown", keyId });
10379
10256
  res.json({ success: true });
10380
10257
  } catch (error) {
10381
- if (sendOwnershipError(error, res)) return;
10382
10258
  sendError(res, error, logger, "reset key spend");
10383
10259
  }
10384
10260
  });
@@ -10438,23 +10314,11 @@ function registerKeysRoutes(router, ctx) {
10438
10314
  res.status(400).json({ error: "start_date and end_date are required" });
10439
10315
  return;
10440
10316
  }
10441
- const tokenEntityRef = res.locals.tokenEntityRef;
10442
- const userId = res.locals.userId;
10443
- await getOrProvisionUser(
10444
- client,
10445
- tokenEntityRef,
10446
- userId,
10447
- provisioningEnabled,
10448
- provisioningDefaults,
10449
- roleConfigs,
10450
- catalogClient,
10451
- auth,
10452
- logger
10453
- );
10317
+ await getProvisionedUser(ctx, res);
10454
10318
  const usage = await client.getUsage(
10455
10319
  start_date,
10456
10320
  end_date,
10457
- userId
10321
+ res.locals.userId
10458
10322
  );
10459
10323
  res.json(usage);
10460
10324
  } catch (error) {
@@ -10676,6 +10540,10 @@ function registerOpencodeRoutes(router, ctx) {
10676
10540
  var import_backstage_plugin_litellm_common5 = require("@acarmisc/backstage-plugin-litellm-common");
10677
10541
 
10678
10542
  // src/http/respondTeam.ts
10543
+ function respondTeam(res, team, ctx, caller) {
10544
+ const shouldHide = caller === "member" ? ctx.teamBudgetVisibility.hideTeamBudgetForMembers : ctx.teamBudgetVisibility.hideTeamBudgetForManagers;
10545
+ res.json(shouldHide ? redactTeamBudget(team) : team);
10546
+ }
10679
10547
  function respondTeamList(res, teams, ctx, caller) {
10680
10548
  const shouldHide = caller === "member" ? ctx.teamBudgetVisibility.hideTeamBudgetForMembers : ctx.teamBudgetVisibility.hideTeamBudgetForManagers;
10681
10549
  res.json(shouldHide ? teams.map(redactTeamBudget) : teams);
@@ -10716,7 +10584,6 @@ function registerTeamsRoutes(router, ctx) {
10716
10584
  provisioningDefaults,
10717
10585
  roleConfigs,
10718
10586
  teamAdminCfg,
10719
- teamBudgetVisibility,
10720
10587
  requireTeamMgmt,
10721
10588
  sendTeamError,
10722
10589
  authorizeTeamSubresource
@@ -10796,7 +10663,7 @@ function registerTeamsRoutes(router, ctx) {
10796
10663
  logger.info(`Team creation already in flight for ${key} \u2014 joining`);
10797
10664
  try {
10798
10665
  const result = await pending;
10799
- res.json(result);
10666
+ respondTeam(res, result, ctx, "manager");
10800
10667
  } catch (err) {
10801
10668
  sendTeamError(err, res);
10802
10669
  }
@@ -10828,9 +10695,7 @@ function registerTeamsRoutes(router, ctx) {
10828
10695
  teamCreateInFlight.set(key, createPromise);
10829
10696
  try {
10830
10697
  const result = await createPromise;
10831
- res.json(
10832
- teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(result) : result
10833
- );
10698
+ respondTeam(res, result, ctx, "manager");
10834
10699
  } catch (err) {
10835
10700
  sendTeamError(err, res);
10836
10701
  } finally {
@@ -10873,9 +10738,7 @@ function registerTeamsRoutes(router, ctx) {
10873
10738
  teamId,
10874
10739
  owningGroup
10875
10740
  });
10876
- res.json(
10877
- teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(r) : r
10878
- );
10741
+ respondTeam(res, r, ctx, "manager");
10879
10742
  } catch (err) {
10880
10743
  sendTeamError(err, res);
10881
10744
  }
@@ -10937,9 +10800,7 @@ function registerTeamsRoutes(router, ctx) {
10937
10800
  const owned = all.filter(
10938
10801
  (t) => typeof t.metadata?.owning_group === "string" && t.metadata.owning_group === teamAdminCfg.group
10939
10802
  );
10940
- res.json(
10941
- teamBudgetVisibility.hideTeamBudgetForManagers ? owned.map(redactTeamBudget) : owned
10942
- );
10803
+ respondTeamList(res, owned, ctx, "manager");
10943
10804
  } catch (err) {
10944
10805
  sendTeamError(err, res);
10945
10806
  }
@@ -11042,9 +10903,7 @@ function registerTeamsRoutes(router, ctx) {
11042
10903
  via
11043
10904
  });
11044
10905
  const updated = await withTeamFetchRetry(() => client.getTeamInfo(teamId));
11045
- res.json(
11046
- teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(updated) : updated
11047
- );
10906
+ respondTeam(res, updated, ctx, "manager");
11048
10907
  } catch (err) {
11049
10908
  sendTeamError(err, res);
11050
10909
  }
@@ -11095,9 +10954,7 @@ function registerTeamsRoutes(router, ctx) {
11095
10954
  via
11096
10955
  });
11097
10956
  const updated = await withTeamFetchRetry(() => client.getTeamInfo(teamId));
11098
- res.json(
11099
- teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(updated) : updated
11100
- );
10957
+ respondTeam(res, updated, ctx, "manager");
11101
10958
  } catch (err) {
11102
10959
  sendTeamError(err, res);
11103
10960
  }
@@ -11115,7 +10972,6 @@ function registerObjectPermissionsRoutes(router, ctx) {
11115
10972
  permissions,
11116
10973
  logger,
11117
10974
  teamAdminCfg,
11118
- teamBudgetVisibility,
11119
10975
  requireObjectPerms,
11120
10976
  sendTeamError,
11121
10977
  authorizeTeamSubresource
@@ -11196,9 +11052,7 @@ function registerObjectPermissionsRoutes(router, ctx) {
11196
11052
  owningGroup,
11197
11053
  vector_stores: requested
11198
11054
  });
11199
- res.json(
11200
- teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(updated) : updated
11201
- );
11055
+ respondTeam(res, updated, ctx, "manager");
11202
11056
  } catch (err) {
11203
11057
  sendTeamError(err, res);
11204
11058
  }
@@ -11282,9 +11136,7 @@ function registerObjectPermissionsRoutes(router, ctx) {
11282
11136
  before,
11283
11137
  after: requested
11284
11138
  });
11285
- res.json(
11286
- teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(updated) : updated
11287
- );
11139
+ respondTeam(res, updated, ctx, "manager");
11288
11140
  } catch (err) {
11289
11141
  sendTeamError(err, res);
11290
11142
  }
@@ -11299,9 +11151,6 @@ function registerTeamUsageRoutes(router, ctx) {
11299
11151
  catalogClient,
11300
11152
  auth,
11301
11153
  logger,
11302
- provisioningEnabled,
11303
- provisioningDefaults,
11304
- roleConfigs,
11305
11154
  teamBudgetVisibility
11306
11155
  } = ctx;
11307
11156
  const requireUser = createRequireUser(ctx);
@@ -11314,18 +11163,7 @@ function registerTeamUsageRoutes(router, ctx) {
11314
11163
  return;
11315
11164
  }
11316
11165
  const tokenEntityRef = res.locals.tokenEntityRef;
11317
- const userId = res.locals.userId;
11318
- const userInfo = await getOrProvisionUser(
11319
- client,
11320
- tokenEntityRef,
11321
- userId,
11322
- provisioningEnabled,
11323
- provisioningDefaults,
11324
- roleConfigs,
11325
- catalogClient,
11326
- auth,
11327
- logger
11328
- );
11166
+ const userInfo = await getProvisionedUser(ctx, res);
11329
11167
  const isMember = userInfo?.teams?.includes(teamId) ?? false;
11330
11168
  let isManager = false;
11331
11169
  const managerCheckNeeded = ctx.teamMgmtEnabled && (!isMember || teamBudgetVisibility.hideTeamBudgetForMembers !== teamBudgetVisibility.hideTeamBudgetForManagers);
@@ -12784,18 +12622,17 @@ function newDefaultVerifier(cfg) {
12784
12622
  return new KeycloakJWTVerifier({ issuer: cfg.issuer, clientId: cfg.clientId });
12785
12623
  }
12786
12624
  function normalizeIdentityOptions(opts) {
12787
- const o = typeof opts === "string" ? { userIdDomain: opts } : opts ?? {};
12788
12625
  let domains = [];
12789
- if (o.trustedEmailDomains?.length) {
12790
- domains = o.trustedEmailDomains;
12791
- } else if (o.userIdDomain) {
12792
- domains = [o.userIdDomain];
12626
+ if (opts.trustedEmailDomains?.length) {
12627
+ domains = opts.trustedEmailDomains;
12628
+ } else if (opts.userIdDomain) {
12629
+ domains = [opts.userIdDomain];
12793
12630
  }
12794
12631
  const trusted = domains.map((d) => d.toLowerCase());
12795
- return { userIdDomain: o.userIdDomain, trusted };
12632
+ return { userIdDomain: opts.userIdDomain, trusted };
12796
12633
  }
12797
12634
  function resolveBridgeUserId(claims, identity) {
12798
- const { userIdDomain, trusted } = normalizeIdentityOptions(identity);
12635
+ const { userIdDomain, trusted } = normalizeIdentityOptions(identity ?? {});
12799
12636
  if (trusted.length === 0) {
12800
12637
  throw new BridgeIdentityError(
12801
12638
  "set litellm.bridge.allowedEmailDomains (or litellm.userIdDomain) to use the CLI bridge"
@@ -12830,8 +12667,8 @@ function resolveBridgeUserId(claims, identity) {
12830
12667
  name = name || emailLocalPart;
12831
12668
  return toLiteLLMUserId(name, userIdDomain);
12832
12669
  }
12833
- async function getOrProvisionUserFromClaims(client, claims, provisioningEnabled, provisioningDefaults, logger, userIdDomain) {
12834
- const userId = resolveBridgeUserId(claims, userIdDomain);
12670
+ async function getOrProvisionUserFromClaims(client, claims, provisioningEnabled, provisioningDefaults, logger, identity) {
12671
+ const userId = resolveBridgeUserId(claims, identity);
12835
12672
  const existing = await client.getUserInfo(userId);
12836
12673
  if (existing) return existing;
12837
12674
  if (claims.email) {
@@ -12872,38 +12709,17 @@ async function getOrProvisionUserFromClaims(client, claims, provisioningEnabled,
12872
12709
  }
12873
12710
  return created;
12874
12711
  }
12875
- async function bridgeListKeys(client, claims, provisioningEnabled, provisioningDefaults, logger, userIdDomain) {
12712
+ async function bridgeListKeys(client, claims, provisioningEnabled, provisioningDefaults, logger, identity) {
12876
12713
  const user = await getOrProvisionUserFromClaims(
12877
12714
  client,
12878
12715
  claims,
12879
12716
  provisioningEnabled,
12880
12717
  provisioningDefaults,
12881
12718
  logger,
12882
- userIdDomain
12719
+ identity
12883
12720
  );
12884
12721
  return client.listKeys(user.user_id);
12885
12722
  }
12886
- async function bridgeGenerateKey(client, claims, provisioningEnabled, provisioningDefaults, logger, request, userIdDomain) {
12887
- const user = await getOrProvisionUserFromClaims(
12888
- client,
12889
- claims,
12890
- provisioningEnabled,
12891
- provisioningDefaults,
12892
- logger,
12893
- userIdDomain
12894
- );
12895
- const enriched = {
12896
- ...request,
12897
- user_id: user.user_id,
12898
- metadata: {
12899
- ...request.metadata ?? {},
12900
- created_via: "abby-cli",
12901
- created_by: user.user_id,
12902
- created_at_iso: (/* @__PURE__ */ new Date()).toISOString()
12903
- }
12904
- };
12905
- return client.generateKey(enriched);
12906
- }
12907
12723
 
12908
12724
  // src/routes/bridge.ts
12909
12725
  var import_backstage_plugin_litellm_common7 = require("@acarmisc/backstage-plugin-litellm-common");
@@ -12915,7 +12731,6 @@ function registerBridgeRoutes(router, ctx, bridgeOpts) {
12915
12731
  userIdDomain,
12916
12732
  provisioningEnabled,
12917
12733
  provisioningDefaults,
12918
- roleConfigs,
12919
12734
  allowUnlimitedBudget,
12920
12735
  teamRequired,
12921
12736
  keyValidationConfig
@@ -13000,26 +12815,25 @@ function registerBridgeRoutes(router, ctx, bridgeOpts) {
13000
12815
  if (!allowUnlimitedBudget && input.max_budget === void 0) {
13001
12816
  input.max_budget = provisioningDefaults.maxBudget;
13002
12817
  }
13003
- const userId = resolveBridgeUserId(claims, bridgeIdentity);
12818
+ const userInfo = await getOrProvisionUserFromClaims(
12819
+ client,
12820
+ claims,
12821
+ provisioningEnabled,
12822
+ provisioningDefaults,
12823
+ logger,
12824
+ bridgeIdentity
12825
+ );
13004
12826
  const keyCreateCtx = {
13005
12827
  client,
13006
12828
  config: {
13007
12829
  allowUnlimitedBudget,
13008
12830
  teamRequired
13009
12831
  },
13010
- // Bridge does not have Backstage catalogClient/auth
13011
- // so provisioning from JWT claims only (no Backstage profile enrichment)
13012
- catalogClient: void 0,
13013
- auth: void 0,
13014
12832
  logger,
13015
- keyValidationConfig,
13016
- provisioningEnabled,
13017
- provisioningDefaults,
13018
- roleConfigs,
13019
- userIdDomain
12833
+ keyValidationConfig
13020
12834
  };
13021
12835
  const result = await createKeyForUser(
13022
- { userId },
12836
+ { userInfo, createdVia: "abby-cli" },
13023
12837
  input,
13024
12838
  keyCreateCtx
13025
12839
  );
@@ -13040,20 +12854,21 @@ function registerBridgeRoutes(router, ctx, bridgeOpts) {
13040
12854
  bridgeIdentity
13041
12855
  );
13042
12856
  const teamIds = user.teams ?? [];
13043
- const teamMetadata = [];
13044
- for (const teamId of teamIds) {
13045
- try {
13046
- const teamInfo = await client.getTeamInfo(teamId);
13047
- teamMetadata.push({
13048
- id: teamId,
13049
- name: teamInfo.team_alias || teamId
13050
- // Use alias as display name, fallback to ID
13051
- });
13052
- } catch (e) {
13053
- logger.debug(`Could not fetch team info for ${teamId}: ${e.message}`);
13054
- teamMetadata.push({ id: teamId });
13055
- }
13056
- }
12857
+ const teamMetadata = await Promise.all(
12858
+ teamIds.map(async (teamId) => {
12859
+ try {
12860
+ const teamInfo = await client.getTeamInfo(teamId);
12861
+ return {
12862
+ id: teamId,
12863
+ name: teamInfo.team_alias || teamId
12864
+ // Use alias as display name, fallback to ID
12865
+ };
12866
+ } catch (e) {
12867
+ logger.debug(`Could not fetch team info for ${teamId}: ${e.message}`);
12868
+ return { id: teamId };
12869
+ }
12870
+ })
12871
+ );
13057
12872
  res.json({ user_id: user.user_id, teams: teamIds, team_metadata: teamMetadata });
13058
12873
  } catch (error) {
13059
12874
  handleBridgeError(error, res);
@@ -13172,31 +12987,17 @@ async function createRouter(options) {
13172
12987
  // Helper functions - defined inline below this object
13173
12988
  authorizeKeyAction: async (req, keyId) => {
13174
12989
  if (!req.res || !req.res.locals.tokenEntityRef || !req.res.locals.userId) {
13175
- throw new import_errors20.NotAllowedError("User identity not found in request");
12990
+ throw new import_errors21.NotAllowedError("User identity not found in request");
13176
12991
  }
13177
12992
  const tokenEntityRef = req.res.locals.tokenEntityRef;
13178
12993
  const userId = req.res.locals.userId;
13179
12994
  const ownKeys = await client.listKeys(userId);
13180
12995
  const key = ownKeys.find((k) => (k.token ?? k.key) === keyId);
13181
12996
  if (!key) {
13182
- throw new import_errors20.NotAllowedError("Access denied: key does not belong to the caller");
12997
+ throw new import_errors21.NotAllowedError("Access denied: key does not belong to the caller");
13183
12998
  }
13184
12999
  return { tokenEntityRef, userId, key };
13185
13000
  },
13186
- sendOwnershipError: (err, res) => {
13187
- if (err instanceof import_errors20.NotAllowedError) {
13188
- res.status(403).json({ error: err.message });
13189
- return true;
13190
- }
13191
- if (err && typeof err === "object" && "status" in err && "body" in err) {
13192
- const errObj = err;
13193
- if (typeof errObj.status === "number" && errObj.body) {
13194
- res.status(errObj.status).json(errObj.body);
13195
- return true;
13196
- }
13197
- }
13198
- return false;
13199
- },
13200
13001
  assertPermission: async (req, permission) => {
13201
13002
  const credentials = await resolveCredentials(req, auth);
13202
13003
  if (!credentials) return false;
@@ -13396,7 +13197,6 @@ var import_backstage_plugin_litellm_common11 = require("@acarmisc/backstage-plug
13396
13197
  ProvisioningError,
13397
13198
  applyRoleOverrides,
13398
13199
  assertTeamAdmin,
13399
- bridgeGenerateKey,
13400
13200
  bridgeListKeys,
13401
13201
  createRouter,
13402
13202
  getOrProvisionUser,
@@ -13417,7 +13217,6 @@ var import_backstage_plugin_litellm_common11 = require("@acarmisc/backstage-plug
13417
13217
  litellmTeamMcpManagePermission,
13418
13218
  litellmTeamMembersManagePermission,
13419
13219
  newDefaultVerifier,
13420
- normalizeRequestTags,
13421
13220
  provisionUser,
13422
13221
  readBridgeConfig,
13423
13222
  readProvisioningDefaults,