@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/README.md +58 -0
- package/dist/bridge.d.ts +4 -6
- package/dist/bridge.js +10 -28
- package/dist/client.d.ts +7 -20
- package/dist/client.js +29 -107
- package/dist/errors.d.ts +2 -1
- package/dist/errors.js +4 -1
- package/dist/index.cjs.js +204 -405
- package/dist/index.cjs.js.map +4 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -3
- package/dist/provisioning.d.ts +2 -0
- package/dist/provisioning.js +11 -20
- package/dist/router.js +0 -16
- package/dist/routes/bridge.js +10 -19
- package/dist/routes/context.d.ts +0 -1
- package/dist/routes/keys.js +8 -21
- package/dist/routes/middleware/withUser.d.ts +3 -2
- package/dist/routes/middleware/withUser.js +1 -1
- package/dist/routes/objectPermissions.js +4 -8
- package/dist/routes/teamUsage.js +2 -3
- package/dist/routes/teams.js +7 -20
- package/dist/services/keyService.d.ts +10 -13
- package/dist/services/keyService.js +12 -47
- package/dist/types.cjs.js.map +1 -1
- package/dist/types.d.ts +0 -36
- package/package.json +3 -7
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
8758
|
-
if (!
|
|
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
|
|
8771
|
-
if (!
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
12790
|
-
domains =
|
|
12791
|
-
} else if (
|
|
12792
|
-
domains = [
|
|
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:
|
|
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,
|
|
12834
|
-
const userId = resolveBridgeUserId(claims,
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
{
|
|
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
|
-
|
|
13045
|
-
|
|
13046
|
-
|
|
13047
|
-
|
|
13048
|
-
|
|
13049
|
-
|
|
13050
|
-
|
|
13051
|
-
|
|
13052
|
-
|
|
13053
|
-
|
|
13054
|
-
|
|
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
|
|
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
|
|
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,
|