@acarmisc/backstage-plugin-litellm-backend 0.18.0 → 0.20.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/config.d.ts +41 -0
- package/dist/bridge.d.ts +4 -6
- package/dist/bridge.js +10 -28
- package/dist/client.d.ts +12 -20
- package/dist/client.js +50 -107
- package/dist/errors.d.ts +2 -1
- package/dist/errors.js +4 -1
- package/dist/index.cjs.js +519 -445
- package/dist/index.cjs.js.map +4 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -3
- package/dist/openapi.js +22 -0
- package/dist/provisioning.d.ts +16 -1
- package/dist/provisioning.js +94 -37
- package/dist/router.js +17 -18
- package/dist/routes/bridge.js +10 -19
- package/dist/routes/config.js +3 -0
- package/dist/routes/context.d.ts +7 -1
- package/dist/routes/keys.js +10 -23
- package/dist/routes/middleware/withUser.d.ts +4 -2
- package/dist/routes/middleware/withUser.js +6 -2
- package/dist/routes/objectPermissions.js +4 -8
- package/dist/routes/opencode.js +3 -3
- package/dist/routes/teamUsage.js +111 -3
- package/dist/routes/teams.js +13 -24
- package/dist/services/keyService.d.ts +10 -13
- package/dist/services/keyService.js +15 -47
- package/dist/teamMemberUsage.d.ts +34 -0
- package/dist/teamMemberUsage.js +114 -0
- package/dist/types.cjs.js.map +1 -1
- package/dist/types.d.ts +1 -37
- package/package.json +4 -8
package/dist/index.cjs.js
CHANGED
|
@@ -7976,29 +7976,27 @@ __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,
|
|
7983
7982
|
getOrProvisionUser: () => getOrProvisionUser,
|
|
7984
7983
|
getOrProvisionUserFromClaims: () => getOrProvisionUserFromClaims,
|
|
7985
7984
|
isTeamManagementEnabled: () => isTeamManagementEnabled,
|
|
7986
|
-
litellmAuditReadPermission: () =>
|
|
7987
|
-
litellmKeyCreatePermission: () =>
|
|
7988
|
-
litellmKeyManagePermission: () =>
|
|
7989
|
-
litellmKeyResetSpendPermission: () =>
|
|
7990
|
-
litellmKeyRevokePermission: () =>
|
|
7991
|
-
litellmKeyUnblockPermission: () =>
|
|
7992
|
-
litellmPermissions: () =>
|
|
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,
|
|
7993
7992
|
litellmPlugin: () => litellmPlugin,
|
|
7994
|
-
litellmTeamCreatePermission: () =>
|
|
7995
|
-
litellmTeamDeletePermission: () =>
|
|
7996
|
-
litellmTeamKnowledgebaseManagePermission: () =>
|
|
7997
|
-
litellmTeamManagePermission: () =>
|
|
7998
|
-
litellmTeamMcpManagePermission: () =>
|
|
7999
|
-
litellmTeamMembersManagePermission: () =>
|
|
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,
|
|
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;
|
|
@@ -8211,6 +8197,28 @@ var LiteLLMClient = class {
|
|
|
8211
8197
|
throw err;
|
|
8212
8198
|
}
|
|
8213
8199
|
}
|
|
8200
|
+
/**
|
|
8201
|
+
* Lists every key bound to a team (any owner), following `/key/list`
|
|
8202
|
+
* pagination. Stops after 50 pages of 100 keys.
|
|
8203
|
+
*/
|
|
8204
|
+
async listTeamKeys(teamId) {
|
|
8205
|
+
const keys = [];
|
|
8206
|
+
for (let page = 1; page <= 50; page++) {
|
|
8207
|
+
const params = new URLSearchParams({
|
|
8208
|
+
team_id: teamId,
|
|
8209
|
+
return_full_object: "true",
|
|
8210
|
+
page: String(page),
|
|
8211
|
+
size: "100"
|
|
8212
|
+
});
|
|
8213
|
+
const response = await this.request(`/key/list?${params.toString()}`);
|
|
8214
|
+
const rawKeys = (response.keys ?? []).filter(
|
|
8215
|
+
(k) => typeof k === "object" && k !== null
|
|
8216
|
+
);
|
|
8217
|
+
keys.push(...rawKeys.map(this.toVirtualKey));
|
|
8218
|
+
if (rawKeys.length === 0 || page >= (response.total_pages ?? 1)) break;
|
|
8219
|
+
}
|
|
8220
|
+
return keys;
|
|
8221
|
+
}
|
|
8214
8222
|
toVirtualKey(k) {
|
|
8215
8223
|
return {
|
|
8216
8224
|
key: k.key_name ?? k.token,
|
|
@@ -8423,18 +8431,6 @@ var LiteLLMClient = class {
|
|
|
8423
8431
|
body: JSON.stringify({ team_ids: [teamId] })
|
|
8424
8432
|
});
|
|
8425
8433
|
}
|
|
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
8434
|
/**
|
|
8439
8435
|
* Adds a member to a team. LiteLLM's `/team/member_add` nests the member
|
|
8440
8436
|
* under a `member` object; `role` is 'user' or 'admin' (we only ever send
|
|
@@ -8498,43 +8494,6 @@ var LiteLLMClient = class {
|
|
|
8498
8494
|
url: r?.url ?? void 0
|
|
8499
8495
|
})).filter((r) => r.id);
|
|
8500
8496
|
}
|
|
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
8497
|
emptyUsage() {
|
|
8539
8498
|
return {
|
|
8540
8499
|
total_spend: 0,
|
|
@@ -8698,6 +8657,114 @@ var LiteLLMClient = class {
|
|
|
8698
8657
|
}
|
|
8699
8658
|
};
|
|
8700
8659
|
|
|
8660
|
+
// src/errors.ts
|
|
8661
|
+
var HTML_TAG_PATTERN = /<[^>]*>|&[a-z]+;/gi;
|
|
8662
|
+
function isHttpishError(error) {
|
|
8663
|
+
return error !== null && typeof error === "object" && "status" in error && "body" in error && typeof error.status === "number" && // any: property access after guard check
|
|
8664
|
+
typeof error.body === "object";
|
|
8665
|
+
}
|
|
8666
|
+
function sanitizeUpstreamMessage(msg) {
|
|
8667
|
+
if (!msg || typeof msg !== "string") return "";
|
|
8668
|
+
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);
|
|
8669
|
+
}
|
|
8670
|
+
function toHttpError(error) {
|
|
8671
|
+
if (isHttpishError(error)) {
|
|
8672
|
+
return {
|
|
8673
|
+
status: error.status,
|
|
8674
|
+
body: error.body,
|
|
8675
|
+
logLevel: "warn"
|
|
8676
|
+
};
|
|
8677
|
+
}
|
|
8678
|
+
if (error instanceof LiteLLMUpstreamError) {
|
|
8679
|
+
if (error.status === 401 || error.status === 403) {
|
|
8680
|
+
return {
|
|
8681
|
+
status: 502,
|
|
8682
|
+
body: { error: "LiteLLM rejected the request" },
|
|
8683
|
+
logLevel: "warn"
|
|
8684
|
+
};
|
|
8685
|
+
}
|
|
8686
|
+
if (error.status >= 500 || error.status < 100) {
|
|
8687
|
+
return {
|
|
8688
|
+
status: 502,
|
|
8689
|
+
body: { error: "LiteLLM is unavailable" },
|
|
8690
|
+
logLevel: "error"
|
|
8691
|
+
};
|
|
8692
|
+
}
|
|
8693
|
+
return {
|
|
8694
|
+
status: error.status === 404 || error.status === 409 ? error.status : 400,
|
|
8695
|
+
body: {
|
|
8696
|
+
error: sanitizeUpstreamMessage(error.message),
|
|
8697
|
+
...error.param ? { param: error.param } : {}
|
|
8698
|
+
},
|
|
8699
|
+
logLevel: "warn"
|
|
8700
|
+
};
|
|
8701
|
+
}
|
|
8702
|
+
if (error && typeof error === "object" && "name" in error && "message" in error) {
|
|
8703
|
+
const name = error.name;
|
|
8704
|
+
const message3 = String(error.message);
|
|
8705
|
+
if (name === "InputError") {
|
|
8706
|
+
return {
|
|
8707
|
+
status: 400,
|
|
8708
|
+
body: { error: message3 },
|
|
8709
|
+
logLevel: "warn"
|
|
8710
|
+
};
|
|
8711
|
+
}
|
|
8712
|
+
if (name === "NotAllowedError") {
|
|
8713
|
+
return {
|
|
8714
|
+
status: 403,
|
|
8715
|
+
body: { error: message3 },
|
|
8716
|
+
logLevel: "warn"
|
|
8717
|
+
};
|
|
8718
|
+
}
|
|
8719
|
+
if (name === "NotFoundError") {
|
|
8720
|
+
return {
|
|
8721
|
+
status: 404,
|
|
8722
|
+
body: { error: message3 },
|
|
8723
|
+
logLevel: "warn"
|
|
8724
|
+
};
|
|
8725
|
+
}
|
|
8726
|
+
if (name === "ConflictError") {
|
|
8727
|
+
return {
|
|
8728
|
+
status: 409,
|
|
8729
|
+
body: { error: message3 },
|
|
8730
|
+
logLevel: "warn"
|
|
8731
|
+
};
|
|
8732
|
+
}
|
|
8733
|
+
}
|
|
8734
|
+
if (error instanceof Error) {
|
|
8735
|
+
const code = error.code;
|
|
8736
|
+
const cause = error.cause;
|
|
8737
|
+
const networkCodes = ["ECONNREFUSED", "ECONNRESET", "ENOTFOUND", "ETIMEDOUT", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT"];
|
|
8738
|
+
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)) {
|
|
8739
|
+
return {
|
|
8740
|
+
status: 502,
|
|
8741
|
+
body: { error: "LiteLLM is unavailable" },
|
|
8742
|
+
logLevel: "error",
|
|
8743
|
+
_logMessage: error.message
|
|
8744
|
+
};
|
|
8745
|
+
}
|
|
8746
|
+
}
|
|
8747
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
8748
|
+
return {
|
|
8749
|
+
status: 500,
|
|
8750
|
+
body: { error: "Internal error" },
|
|
8751
|
+
logLevel: "error",
|
|
8752
|
+
_logMessage: message2
|
|
8753
|
+
};
|
|
8754
|
+
}
|
|
8755
|
+
function sendError(res, error, logger, context) {
|
|
8756
|
+
const { status, body, logLevel, _logMessage } = toHttpError(error);
|
|
8757
|
+
const realMessage = _logMessage || (error instanceof Error ? error.message : String(error));
|
|
8758
|
+
if (logLevel === "error") {
|
|
8759
|
+
logger.error(`Failed to ${context}`, error instanceof Error ? error : new Error(String(error)));
|
|
8760
|
+
} else if (logLevel === "warn") {
|
|
8761
|
+
logger.warn(`${context}: ${realMessage}`);
|
|
8762
|
+
} else {
|
|
8763
|
+
logger.info(`${context}: ${realMessage}`);
|
|
8764
|
+
}
|
|
8765
|
+
res.status(status).json(body);
|
|
8766
|
+
}
|
|
8767
|
+
|
|
8701
8768
|
// src/provisioning.ts
|
|
8702
8769
|
function toLiteLLMUserId(userEntityRef, userIdDomain) {
|
|
8703
8770
|
const name = userEntityRef.split("/").pop() ?? userEntityRef;
|
|
@@ -8754,8 +8821,9 @@ function readProvisioningDefaults(config) {
|
|
|
8754
8821
|
return { enabled, defaults };
|
|
8755
8822
|
}
|
|
8756
8823
|
async function resolveUserId(req, auth) {
|
|
8757
|
-
const
|
|
8758
|
-
if (!
|
|
8824
|
+
const authHeader = req.headers.authorization ?? "";
|
|
8825
|
+
if (!authHeader.startsWith("Bearer ")) return void 0;
|
|
8826
|
+
const rawToken = authHeader.slice(7);
|
|
8759
8827
|
try {
|
|
8760
8828
|
const credentials = await auth.authenticate(rawToken);
|
|
8761
8829
|
const principal = credentials.principal;
|
|
@@ -8767,8 +8835,9 @@ async function resolveUserId(req, auth) {
|
|
|
8767
8835
|
return void 0;
|
|
8768
8836
|
}
|
|
8769
8837
|
async function resolveCredentials(req, auth) {
|
|
8770
|
-
const
|
|
8771
|
-
if (!
|
|
8838
|
+
const authHeader = req.headers.authorization ?? "";
|
|
8839
|
+
if (!authHeader.startsWith("Bearer ")) return void 0;
|
|
8840
|
+
const rawToken = authHeader.slice(7);
|
|
8772
8841
|
try {
|
|
8773
8842
|
return await auth.authenticate(rawToken);
|
|
8774
8843
|
} catch {
|
|
@@ -8847,10 +8916,6 @@ async function provisionUser(client, userId, defaults, profile, backstageEntity,
|
|
|
8847
8916
|
}
|
|
8848
8917
|
}
|
|
8849
8918
|
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
8919
|
var ProvisioningError = class extends Error {
|
|
8855
8920
|
constructor(message2, hint, provisioning, status = 404) {
|
|
8856
8921
|
super(message2);
|
|
@@ -8858,6 +8923,38 @@ var ProvisioningError = class extends Error {
|
|
|
8858
8923
|
this.body = { error: message2, hint, provisioning };
|
|
8859
8924
|
}
|
|
8860
8925
|
};
|
|
8926
|
+
var adoptedUserIds = /* @__PURE__ */ new Map();
|
|
8927
|
+
function effectiveUserId(userId) {
|
|
8928
|
+
return adoptedUserIds.get(userId) ?? userId;
|
|
8929
|
+
}
|
|
8930
|
+
async function getUserByIdOrAdopted(client, userId) {
|
|
8931
|
+
const adopted = adoptedUserIds.get(userId);
|
|
8932
|
+
if (adopted) {
|
|
8933
|
+
const info = await client.getUserInfo(adopted);
|
|
8934
|
+
if (info) return info;
|
|
8935
|
+
adoptedUserIds.delete(userId);
|
|
8936
|
+
}
|
|
8937
|
+
return client.getUserInfo(userId);
|
|
8938
|
+
}
|
|
8939
|
+
async function adoptUserByEmail(client, userId, email, logger) {
|
|
8940
|
+
if (!email) return null;
|
|
8941
|
+
const byEmail = await client.getUserByEmail(email);
|
|
8942
|
+
if (!byEmail?.user_id) return null;
|
|
8943
|
+
if (byEmail.user_id !== userId) {
|
|
8944
|
+
logger.info(
|
|
8945
|
+
`Found existing LiteLLM user ${byEmail.user_id} by email ${email}, reusing for ${userId}`
|
|
8946
|
+
);
|
|
8947
|
+
adoptedUserIds.set(userId, byEmail.user_id);
|
|
8948
|
+
}
|
|
8949
|
+
return byEmail;
|
|
8950
|
+
}
|
|
8951
|
+
async function findLiteLLMUser(client, tokenEntityRef, userId, catalogClient, auth, logger) {
|
|
8952
|
+
const existing = await getUserByIdOrAdopted(client, userId);
|
|
8953
|
+
if (existing) return existing;
|
|
8954
|
+
if (!tokenEntityRef) return null;
|
|
8955
|
+
const profile = await resolveUserProfile(tokenEntityRef, catalogClient, auth, logger);
|
|
8956
|
+
return adoptUserByEmail(client, userId, profile.email, logger);
|
|
8957
|
+
}
|
|
8861
8958
|
async function getOrProvisionUser(client, tokenEntityRef, userId, provisioningEnabled, provisioningDefaults, roleConfigs, catalogClient, auth, logger) {
|
|
8862
8959
|
if (!userId) {
|
|
8863
8960
|
throw new ProvisioningError(
|
|
@@ -8866,17 +8963,10 @@ async function getOrProvisionUser(client, tokenEntityRef, userId, provisioningEn
|
|
|
8866
8963
|
provisioningEnabled
|
|
8867
8964
|
);
|
|
8868
8965
|
}
|
|
8869
|
-
const existing = await client
|
|
8966
|
+
const existing = await getUserByIdOrAdopted(client, userId);
|
|
8870
8967
|
if (existing) {
|
|
8871
8968
|
return existing;
|
|
8872
8969
|
}
|
|
8873
|
-
if (!provisioningEnabled) {
|
|
8874
|
-
throw new ProvisioningError(
|
|
8875
|
-
"User not found in LiteLLM",
|
|
8876
|
-
"Enable litellm.provisioning.enabled in app-config.yaml or create the user manually",
|
|
8877
|
-
false
|
|
8878
|
-
);
|
|
8879
|
-
}
|
|
8880
8970
|
const pending = provisioningInFlight.get(userId);
|
|
8881
8971
|
if (pending) {
|
|
8882
8972
|
logger.info(
|
|
@@ -8885,11 +8975,25 @@ async function getOrProvisionUser(client, tokenEntityRef, userId, provisioningEn
|
|
|
8885
8975
|
return pending;
|
|
8886
8976
|
}
|
|
8887
8977
|
const provisionPromise = (async () => {
|
|
8888
|
-
const
|
|
8889
|
-
const
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
|
|
8978
|
+
const profile = tokenEntityRef ? await resolveUserProfile(tokenEntityRef, catalogClient, auth, logger) : {};
|
|
8979
|
+
const byEmail = await adoptUserByEmail(client, userId, profile.email, logger);
|
|
8980
|
+
if (byEmail) {
|
|
8981
|
+
return byEmail;
|
|
8982
|
+
}
|
|
8983
|
+
if (!provisioningEnabled) {
|
|
8984
|
+
throw new ProvisioningError(
|
|
8985
|
+
"User not found in LiteLLM",
|
|
8986
|
+
"Enable litellm.provisioning.enabled in app-config.yaml or create the user manually",
|
|
8987
|
+
false
|
|
8988
|
+
);
|
|
8989
|
+
}
|
|
8990
|
+
const matchedRole = await resolveUserRole(
|
|
8991
|
+
tokenEntityRef ?? userId,
|
|
8992
|
+
roleConfigs,
|
|
8993
|
+
catalogClient,
|
|
8994
|
+
auth,
|
|
8995
|
+
logger
|
|
8996
|
+
);
|
|
8893
8997
|
const effectiveDefaults = matchedRole ? applyRoleOverrides(provisioningDefaults, matchedRole) : provisioningDefaults;
|
|
8894
8998
|
if (matchedRole) {
|
|
8895
8999
|
logger.info(
|
|
@@ -8918,7 +9022,7 @@ async function getOrProvisionUser(client, tokenEntityRef, userId, provisioningEn
|
|
|
8918
9022
|
logger.info(
|
|
8919
9023
|
`LiteLLM user ${userId} already exists during provisioning \u2014 re-fetching`
|
|
8920
9024
|
);
|
|
8921
|
-
const refetched = await client.getUserInfo(userId);
|
|
9025
|
+
const refetched = await client.getUserInfo(userId) ?? await adoptUserByEmail(client, userId, profile.email, logger);
|
|
8922
9026
|
if (refetched) {
|
|
8923
9027
|
return refetched;
|
|
8924
9028
|
}
|
|
@@ -8979,7 +9083,7 @@ async function resolveUserRole(userEntityRef, roleConfigs, catalogClient, auth,
|
|
|
8979
9083
|
}
|
|
8980
9084
|
|
|
8981
9085
|
// src/router.ts
|
|
8982
|
-
var
|
|
9086
|
+
var import_backstage_plugin_litellm_common10 = require("@acarmisc/backstage-plugin-litellm-common");
|
|
8983
9087
|
|
|
8984
9088
|
// src/teamAdmin.ts
|
|
8985
9089
|
var import_plugin_permission_common = __toESM(require_index_cjs2());
|
|
@@ -9254,112 +9358,91 @@ function readOpencodeConfig(config) {
|
|
|
9254
9358
|
};
|
|
9255
9359
|
}
|
|
9256
9360
|
|
|
9257
|
-
// src/
|
|
9258
|
-
|
|
9259
|
-
|
|
9260
|
-
|
|
9261
|
-
|
|
9262
|
-
|
|
9263
|
-
|
|
9264
|
-
if (!msg || typeof msg !== "string") return "";
|
|
9265
|
-
return msg.replace(HTML_TAG_PATTERN, "").replace(/\s+/g, " ").trim().slice(0, 500);
|
|
9361
|
+
// src/teamMemberUsage.ts
|
|
9362
|
+
function readMemberBreakdownConfig(config) {
|
|
9363
|
+
return {
|
|
9364
|
+
enabled: config.getOptionalBoolean("litellm.teamUsage.memberBreakdown.enabled") ?? false,
|
|
9365
|
+
viewerRoles: config.getOptionalStringArray("litellm.teamUsage.memberBreakdown.viewerRoles") ?? [],
|
|
9366
|
+
permissionEnabled: config.getOptionalBoolean("permission.enabled") ?? false
|
|
9367
|
+
};
|
|
9266
9368
|
}
|
|
9267
|
-
function
|
|
9268
|
-
|
|
9269
|
-
|
|
9270
|
-
|
|
9271
|
-
|
|
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
|
-
};
|
|
9369
|
+
function memberEmails(team, keys) {
|
|
9370
|
+
const emails = /* @__PURE__ */ new Set();
|
|
9371
|
+
for (const m of team.members_with_roles ?? []) {
|
|
9372
|
+
if (m.user_email) emails.add(m.user_email.toLowerCase());
|
|
9373
|
+
else if (m.user_id?.includes("@")) emails.add(m.user_id.toLowerCase());
|
|
9298
9374
|
}
|
|
9299
|
-
|
|
9300
|
-
|
|
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
|
-
}
|
|
9375
|
+
for (const k of keys) {
|
|
9376
|
+
if (k.user_id?.includes("@")) emails.add(k.user_id.toLowerCase());
|
|
9330
9377
|
}
|
|
9331
|
-
|
|
9332
|
-
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
|
|
9378
|
+
return [...emails];
|
|
9379
|
+
}
|
|
9380
|
+
function buildTeamMemberUsage(teamId, usage, keys, team, profiles = /* @__PURE__ */ new Map()) {
|
|
9381
|
+
const rows = /* @__PURE__ */ new Map();
|
|
9382
|
+
const rowFor = (userId) => {
|
|
9383
|
+
let row = rows.get(userId);
|
|
9384
|
+
if (!row) {
|
|
9385
|
+
row = {
|
|
9386
|
+
user_id: userId,
|
|
9387
|
+
spend: 0,
|
|
9388
|
+
spend_share_pct: 0,
|
|
9389
|
+
prompt_tokens: 0,
|
|
9390
|
+
completion_tokens: 0,
|
|
9391
|
+
total_tokens: 0,
|
|
9392
|
+
api_requests: 0,
|
|
9393
|
+
successful_requests: 0,
|
|
9394
|
+
failed_requests: 0,
|
|
9395
|
+
success_rate: null,
|
|
9396
|
+
key_count: 0
|
|
9341
9397
|
};
|
|
9398
|
+
rows.set(userId, row);
|
|
9342
9399
|
}
|
|
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
|
|
9400
|
+
return row;
|
|
9350
9401
|
};
|
|
9402
|
+
for (const m of team.members_with_roles ?? []) {
|
|
9403
|
+
if (!m.user_id) continue;
|
|
9404
|
+
const row = rowFor(m.user_id);
|
|
9405
|
+
row.role = m.role;
|
|
9406
|
+
if (m.user_email) row.user_email = m.user_email;
|
|
9407
|
+
}
|
|
9408
|
+
const ownerByKey = /* @__PURE__ */ new Map();
|
|
9409
|
+
for (const k of keys) {
|
|
9410
|
+
if (!k.user_id || !k.token) continue;
|
|
9411
|
+
ownerByKey.set(k.token, k.user_id);
|
|
9412
|
+
rowFor(k.user_id).key_count += 1;
|
|
9413
|
+
}
|
|
9414
|
+
for (const [keyHash, k] of Object.entries(usage.usage_by_key ?? {})) {
|
|
9415
|
+
const row = rowFor(ownerByKey.get(keyHash) ?? null);
|
|
9416
|
+
row.spend += k.total_spend ?? 0;
|
|
9417
|
+
row.prompt_tokens += k.prompt_tokens ?? 0;
|
|
9418
|
+
row.completion_tokens += k.completion_tokens ?? 0;
|
|
9419
|
+
row.total_tokens += k.total_tokens ?? 0;
|
|
9420
|
+
row.api_requests += k.api_requests ?? 0;
|
|
9421
|
+
row.successful_requests += k.successful_requests ?? 0;
|
|
9422
|
+
row.failed_requests += k.failed_requests ?? 0;
|
|
9423
|
+
}
|
|
9424
|
+
const members = [...rows.values()];
|
|
9425
|
+
const totalSpend = members.reduce((sum, r) => sum + r.spend, 0);
|
|
9426
|
+
for (const row of members) {
|
|
9427
|
+
row.spend_share_pct = totalSpend > 0 ? row.spend / totalSpend * 100 : 0;
|
|
9428
|
+
const finished = row.successful_requests + row.failed_requests;
|
|
9429
|
+
row.success_rate = finished > 0 ? row.successful_requests / finished * 100 : null;
|
|
9430
|
+
if (!row.user_email && row.user_id?.includes("@")) row.user_email = row.user_id;
|
|
9431
|
+
const profile = row.user_email && profiles.get(row.user_email.toLowerCase());
|
|
9432
|
+
if (profile && profile.displayName) row.display_name = profile.displayName;
|
|
9433
|
+
}
|
|
9434
|
+
members.sort(
|
|
9435
|
+
(a, b) => b.spend - a.spend || b.total_tokens - a.total_tokens || String(a.user_id ?? "\uFFFF").localeCompare(String(b.user_id ?? "\uFFFF"))
|
|
9436
|
+
);
|
|
9437
|
+
return { team_id: teamId, total_spend: totalSpend, members };
|
|
9351
9438
|
}
|
|
9352
|
-
function
|
|
9353
|
-
|
|
9354
|
-
|
|
9355
|
-
|
|
9356
|
-
|
|
9357
|
-
|
|
9358
|
-
|
|
9359
|
-
} else {
|
|
9360
|
-
logger.info(`${context}: ${realMessage}`);
|
|
9361
|
-
}
|
|
9362
|
-
res.status(status).json(body);
|
|
9439
|
+
function redactTeamMemberUsage(usage) {
|
|
9440
|
+
return {
|
|
9441
|
+
...usage,
|
|
9442
|
+
total_spend: 0,
|
|
9443
|
+
budget_hidden: true,
|
|
9444
|
+
members: usage.members.map((m) => ({ ...m, spend: 0 }))
|
|
9445
|
+
};
|
|
9363
9446
|
}
|
|
9364
9447
|
|
|
9365
9448
|
// src/routes/middleware/withUser.ts
|
|
@@ -9382,7 +9465,7 @@ function createRequireUser(ctx) {
|
|
|
9382
9465
|
async function getProvisionedUser(ctx, res) {
|
|
9383
9466
|
const tokenEntityRef = res.locals.tokenEntityRef;
|
|
9384
9467
|
const userId = res.locals.userId;
|
|
9385
|
-
|
|
9468
|
+
const userInfo = await getOrProvisionUser(
|
|
9386
9469
|
ctx.client,
|
|
9387
9470
|
tokenEntityRef,
|
|
9388
9471
|
userId,
|
|
@@ -9393,6 +9476,8 @@ async function getProvisionedUser(ctx, res) {
|
|
|
9393
9476
|
ctx.auth,
|
|
9394
9477
|
ctx.logger
|
|
9395
9478
|
);
|
|
9479
|
+
res.locals.userId = effectiveUserId(userId);
|
|
9480
|
+
return userInfo;
|
|
9396
9481
|
}
|
|
9397
9482
|
|
|
9398
9483
|
// src/services/userInfoCache.ts
|
|
@@ -9601,6 +9686,7 @@ var openApiSpec = {
|
|
|
9601
9686
|
team_id: { type: "string" },
|
|
9602
9687
|
duration: { type: "string" },
|
|
9603
9688
|
max_budget: { type: "number", nullable: true, description: "Positive number caps spend; null = unlimited" },
|
|
9689
|
+
budget_duration: { type: "string", description: 'How often max_budget resets (e.g. "30d", "1mo"); one of litellm.keys.allowedBudgetDurations' },
|
|
9604
9690
|
tpm_limit: { type: "number" },
|
|
9605
9691
|
rpm_limit: { type: "number" }
|
|
9606
9692
|
} } } }
|
|
@@ -9697,6 +9783,24 @@ var openApiSpec = {
|
|
|
9697
9783
|
responses: { "200": { description: "Team usage" }, "400": { description: "Missing date range" } }
|
|
9698
9784
|
}
|
|
9699
9785
|
},
|
|
9786
|
+
"/teams/{teamId}/usage/members": {
|
|
9787
|
+
get: {
|
|
9788
|
+
tags: ["Teams"],
|
|
9789
|
+
summary: "Per-member usage breakdown for a team",
|
|
9790
|
+
description: "Needs litellm.teamUsage.memberBreakdown.enabled. Allowed for team managers, members holding one of memberBreakdown.viewerRoles, and members granted litellm.team.usage.read. Spend is zeroed (with budget_hidden: true) under team budget hiding; spend_share_pct remains.",
|
|
9791
|
+
parameters: [
|
|
9792
|
+
{ name: "teamId", in: "path", required: true, schema: { type: "string" } },
|
|
9793
|
+
{ name: "start_date", in: "query", required: true, schema: { type: "string" } },
|
|
9794
|
+
{ name: "end_date", in: "query", required: true, schema: { type: "string" } }
|
|
9795
|
+
],
|
|
9796
|
+
responses: {
|
|
9797
|
+
"200": { description: "One row per member, sorted by spend" },
|
|
9798
|
+
"400": { description: "Missing date range" },
|
|
9799
|
+
"403": { description: "Caller may see the team but not its member breakdown" },
|
|
9800
|
+
"404": { description: "Feature disabled, or team not found / not visible to the caller" }
|
|
9801
|
+
}
|
|
9802
|
+
}
|
|
9803
|
+
},
|
|
9700
9804
|
"/usage": {
|
|
9701
9805
|
get: {
|
|
9702
9806
|
tags: ["Usage"],
|
|
@@ -9802,6 +9906,9 @@ function registerConfigRoutes(router, ctx) {
|
|
|
9802
9906
|
readOnly: ctx.teamAdminCfg.readOnly,
|
|
9803
9907
|
memberManagerRoles: ctx.teamAdminCfg.memberManagerRoles
|
|
9804
9908
|
},
|
|
9909
|
+
teamUsage: {
|
|
9910
|
+
memberBreakdown: ctx.memberBreakdownCfg.enabled
|
|
9911
|
+
},
|
|
9805
9912
|
display: {
|
|
9806
9913
|
hideTeamBudgetForMembers: ctx.teamBudgetVisibility.hideTeamBudgetForMembers,
|
|
9807
9914
|
hideTeamBudgetForManagers: ctx.teamBudgetVisibility.hideTeamBudgetForManagers
|
|
@@ -9897,56 +10004,9 @@ async function createKeyForUser(user, input, ctx) {
|
|
|
9897
10004
|
config,
|
|
9898
10005
|
catalogClient,
|
|
9899
10006
|
auth,
|
|
9900
|
-
logger
|
|
9901
|
-
provisioningEnabled,
|
|
9902
|
-
provisioningDefaults,
|
|
9903
|
-
roleConfigs
|
|
10007
|
+
logger
|
|
9904
10008
|
} = 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
|
-
}
|
|
10009
|
+
const userInfo = user.userInfo;
|
|
9950
10010
|
if (!config.allowUnlimitedBudget && (input.max_budget === null || input.max_budget === void 0)) {
|
|
9951
10011
|
throw new KeyServiceError(
|
|
9952
10012
|
400,
|
|
@@ -9992,13 +10052,15 @@ async function createKeyForUser(user, input, ctx) {
|
|
|
9992
10052
|
}
|
|
9993
10053
|
const profile = user.tokenEntityRef && catalogClient !== void 0 && catalogClient !== null && auth !== void 0 && auth !== null ? await resolveUserProfile(user.tokenEntityRef, catalogClient, auth, logger) : {};
|
|
9994
10054
|
const clientMetadata = input.metadata ?? {};
|
|
10055
|
+
const createdVia = user.createdVia ?? "backstage";
|
|
9995
10056
|
const serverMetadata = {
|
|
9996
|
-
created_by_backstage_user: user.tokenEntityRef
|
|
10057
|
+
...user.tokenEntityRef && { created_by_backstage_user: user.tokenEntityRef },
|
|
10058
|
+
...!user.tokenEntityRef && { created_by: userInfo.user_id },
|
|
9997
10059
|
...profile.email && { created_by_email: profile.email },
|
|
9998
10060
|
...profile.displayName && {
|
|
9999
10061
|
created_by_display_name: profile.displayName
|
|
10000
10062
|
},
|
|
10001
|
-
created_via:
|
|
10063
|
+
created_via: createdVia,
|
|
10002
10064
|
created_at_iso: (/* @__PURE__ */ new Date()).toISOString()
|
|
10003
10065
|
};
|
|
10004
10066
|
const enrichedMetadata = {
|
|
@@ -10007,7 +10069,7 @@ async function createKeyForUser(user, input, ctx) {
|
|
|
10007
10069
|
};
|
|
10008
10070
|
const upstreamRequest = {
|
|
10009
10071
|
alias: input.alias,
|
|
10010
|
-
user_id:
|
|
10072
|
+
user_id: userInfo.user_id
|
|
10011
10073
|
};
|
|
10012
10074
|
if (input.models !== void 0) {
|
|
10013
10075
|
upstreamRequest.models = input.models;
|
|
@@ -10018,6 +10080,9 @@ async function createKeyForUser(user, input, ctx) {
|
|
|
10018
10080
|
if (input.max_budget !== void 0) {
|
|
10019
10081
|
upstreamRequest.max_budget = input.max_budget ?? void 0;
|
|
10020
10082
|
}
|
|
10083
|
+
if (input.budget_duration !== void 0) {
|
|
10084
|
+
upstreamRequest.budget_duration = input.budget_duration;
|
|
10085
|
+
}
|
|
10021
10086
|
if (input.tpm_limit !== void 0) {
|
|
10022
10087
|
upstreamRequest.tpm_limit = input.tpm_limit;
|
|
10023
10088
|
}
|
|
@@ -10032,7 +10097,7 @@ async function createKeyForUser(user, input, ctx) {
|
|
|
10032
10097
|
}
|
|
10033
10098
|
upstreamRequest.metadata = enrichedMetadata;
|
|
10034
10099
|
const result = await client.generateKey(upstreamRequest);
|
|
10035
|
-
logger.info("key.generate", { userId:
|
|
10100
|
+
logger.info("key.generate", { userId: userInfo.user_id, keyAlias: input.alias });
|
|
10036
10101
|
return result;
|
|
10037
10102
|
}
|
|
10038
10103
|
|
|
@@ -10043,17 +10108,12 @@ function registerKeysRoutes(router, ctx) {
|
|
|
10043
10108
|
catalogClient,
|
|
10044
10109
|
auth,
|
|
10045
10110
|
logger,
|
|
10046
|
-
userIdDomain,
|
|
10047
|
-
provisioningEnabled,
|
|
10048
|
-
provisioningDefaults,
|
|
10049
|
-
roleConfigs,
|
|
10050
10111
|
auditGroup,
|
|
10051
10112
|
allowUnlimitedBudget,
|
|
10052
10113
|
teamRequired,
|
|
10053
10114
|
allowOwnerResetSpend,
|
|
10054
10115
|
keyValidationConfig,
|
|
10055
10116
|
authorizeKeyAction,
|
|
10056
|
-
sendOwnershipError,
|
|
10057
10117
|
assertPermission,
|
|
10058
10118
|
sendPermissionDenied
|
|
10059
10119
|
} = ctx;
|
|
@@ -10081,8 +10141,8 @@ function registerKeysRoutes(router, ctx) {
|
|
|
10081
10141
|
});
|
|
10082
10142
|
router.get("/keys", async (_req, res) => {
|
|
10083
10143
|
try {
|
|
10084
|
-
const userId = res.locals.userId;
|
|
10085
10144
|
await getProvisionedUser(ctx, res);
|
|
10145
|
+
const userId = res.locals.userId;
|
|
10086
10146
|
const keys = await client.listKeys(userId);
|
|
10087
10147
|
res.json(keys);
|
|
10088
10148
|
} catch (error) {
|
|
@@ -10099,8 +10159,8 @@ function registerKeysRoutes(router, ctx) {
|
|
|
10099
10159
|
sendPermissionDenied(res, import_backstage_plugin_litellm_common3.litellmKeyRevokePermission);
|
|
10100
10160
|
return;
|
|
10101
10161
|
}
|
|
10102
|
-
const userId = res.locals.userId;
|
|
10103
10162
|
await getProvisionedUser(ctx, res);
|
|
10163
|
+
const userId = res.locals.userId;
|
|
10104
10164
|
const keys = await client.listKeys(userId);
|
|
10105
10165
|
const now = Date.now();
|
|
10106
10166
|
const expiredKeys = keys.filter((k) => {
|
|
@@ -10125,7 +10185,7 @@ function registerKeysRoutes(router, ctx) {
|
|
|
10125
10185
|
const keyId = key.token ?? key.key;
|
|
10126
10186
|
let errorMsg = "Unknown error";
|
|
10127
10187
|
if (err instanceof Error) {
|
|
10128
|
-
errorMsg =
|
|
10188
|
+
errorMsg = sanitizeUpstreamMessage(err.message) || "Unknown error";
|
|
10129
10189
|
}
|
|
10130
10190
|
failures.push({ keyId, error: errorMsg });
|
|
10131
10191
|
logger.warn("key.prune-expired failed", {
|
|
@@ -10165,7 +10225,7 @@ function registerKeysRoutes(router, ctx) {
|
|
|
10165
10225
|
return;
|
|
10166
10226
|
}
|
|
10167
10227
|
const tokenEntityRef = res.locals.tokenEntityRef;
|
|
10168
|
-
const
|
|
10228
|
+
const userInfo = await getProvisionedUser(ctx, res);
|
|
10169
10229
|
const keyCreateCtx = {
|
|
10170
10230
|
client,
|
|
10171
10231
|
config: {
|
|
@@ -10175,14 +10235,10 @@ function registerKeysRoutes(router, ctx) {
|
|
|
10175
10235
|
catalogClient,
|
|
10176
10236
|
auth,
|
|
10177
10237
|
logger,
|
|
10178
|
-
keyValidationConfig
|
|
10179
|
-
provisioningEnabled,
|
|
10180
|
-
provisioningDefaults,
|
|
10181
|
-
roleConfigs,
|
|
10182
|
-
userIdDomain
|
|
10238
|
+
keyValidationConfig
|
|
10183
10239
|
};
|
|
10184
10240
|
const result = await createKeyForUser(
|
|
10185
|
-
{ tokenEntityRef,
|
|
10241
|
+
{ tokenEntityRef, userInfo },
|
|
10186
10242
|
input,
|
|
10187
10243
|
keyCreateCtx
|
|
10188
10244
|
);
|
|
@@ -10282,7 +10338,6 @@ function registerKeysRoutes(router, ctx) {
|
|
|
10282
10338
|
logger.info("key.update", { userId: tokenEntityRef ?? "unknown", keyId });
|
|
10283
10339
|
res.json(result);
|
|
10284
10340
|
} catch (error) {
|
|
10285
|
-
if (sendOwnershipError(error, res)) return;
|
|
10286
10341
|
sendError(res, error, logger, "update key");
|
|
10287
10342
|
}
|
|
10288
10343
|
});
|
|
@@ -10302,7 +10357,6 @@ function registerKeysRoutes(router, ctx) {
|
|
|
10302
10357
|
logger.info("key.delete", { userId: tokenEntityRef ?? "unknown", keyId });
|
|
10303
10358
|
res.json({ success: true });
|
|
10304
10359
|
} catch (error) {
|
|
10305
|
-
if (sendOwnershipError(error, res)) return;
|
|
10306
10360
|
sendError(res, error, logger, "delete key");
|
|
10307
10361
|
}
|
|
10308
10362
|
});
|
|
@@ -10331,7 +10385,6 @@ function registerKeysRoutes(router, ctx) {
|
|
|
10331
10385
|
logger.info("key.block", { userId: tokenEntityRef ?? "unknown", keyId });
|
|
10332
10386
|
res.json({ success: true });
|
|
10333
10387
|
} catch (error) {
|
|
10334
|
-
if (sendOwnershipError(error, res)) return;
|
|
10335
10388
|
sendError(res, error, logger, "block key");
|
|
10336
10389
|
}
|
|
10337
10390
|
});
|
|
@@ -10356,7 +10409,6 @@ function registerKeysRoutes(router, ctx) {
|
|
|
10356
10409
|
logger.info("key.unblock", { userId: tokenEntityRef ?? "unknown", keyId });
|
|
10357
10410
|
res.json({ success: true });
|
|
10358
10411
|
} catch (error) {
|
|
10359
|
-
if (sendOwnershipError(error, res)) return;
|
|
10360
10412
|
sendError(res, error, logger, "unblock key");
|
|
10361
10413
|
}
|
|
10362
10414
|
});
|
|
@@ -10378,7 +10430,6 @@ function registerKeysRoutes(router, ctx) {
|
|
|
10378
10430
|
logger.info("key.reset_spend", { userId: tokenEntityRef ?? "unknown", keyId });
|
|
10379
10431
|
res.json({ success: true });
|
|
10380
10432
|
} catch (error) {
|
|
10381
|
-
if (sendOwnershipError(error, res)) return;
|
|
10382
10433
|
sendError(res, error, logger, "reset key spend");
|
|
10383
10434
|
}
|
|
10384
10435
|
});
|
|
@@ -10438,23 +10489,11 @@ function registerKeysRoutes(router, ctx) {
|
|
|
10438
10489
|
res.status(400).json({ error: "start_date and end_date are required" });
|
|
10439
10490
|
return;
|
|
10440
10491
|
}
|
|
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
|
-
);
|
|
10492
|
+
await getProvisionedUser(ctx, res);
|
|
10454
10493
|
const usage = await client.getUsage(
|
|
10455
10494
|
start_date,
|
|
10456
10495
|
end_date,
|
|
10457
|
-
userId
|
|
10496
|
+
res.locals.userId
|
|
10458
10497
|
);
|
|
10459
10498
|
res.json(usage);
|
|
10460
10499
|
} catch (error) {
|
|
@@ -10514,11 +10553,11 @@ function registerOpencodeRoutes(router, ctx) {
|
|
|
10514
10553
|
sendPermissionDenied(res, import_backstage_plugin_litellm_common4.litellmKeyCreatePermission);
|
|
10515
10554
|
return null;
|
|
10516
10555
|
}
|
|
10517
|
-
const
|
|
10556
|
+
const computedUserId = toLiteLLMUserId(tokenEntityRef, userIdDomain);
|
|
10518
10557
|
const userInfo = await getOrProvisionUser(
|
|
10519
10558
|
client,
|
|
10520
10559
|
tokenEntityRef,
|
|
10521
|
-
|
|
10560
|
+
computedUserId,
|
|
10522
10561
|
provisioningEnabled,
|
|
10523
10562
|
provisioningDefaults,
|
|
10524
10563
|
roleConfigs,
|
|
@@ -10540,7 +10579,7 @@ function registerOpencodeRoutes(router, ctx) {
|
|
|
10540
10579
|
res.status(400).json({ error: "A team is required for this connection" });
|
|
10541
10580
|
return null;
|
|
10542
10581
|
}
|
|
10543
|
-
return { tokenEntityRef, userId, userInfo, teamId, redirectUri };
|
|
10582
|
+
return { tokenEntityRef, userId: effectiveUserId(computedUserId), userInfo, teamId, redirectUri };
|
|
10544
10583
|
};
|
|
10545
10584
|
router.get("/opencode/connect", async (req, res) => {
|
|
10546
10585
|
try {
|
|
@@ -10676,6 +10715,10 @@ function registerOpencodeRoutes(router, ctx) {
|
|
|
10676
10715
|
var import_backstage_plugin_litellm_common5 = require("@acarmisc/backstage-plugin-litellm-common");
|
|
10677
10716
|
|
|
10678
10717
|
// src/http/respondTeam.ts
|
|
10718
|
+
function respondTeam(res, team, ctx, caller) {
|
|
10719
|
+
const shouldHide = caller === "member" ? ctx.teamBudgetVisibility.hideTeamBudgetForMembers : ctx.teamBudgetVisibility.hideTeamBudgetForManagers;
|
|
10720
|
+
res.json(shouldHide ? redactTeamBudget(team) : team);
|
|
10721
|
+
}
|
|
10679
10722
|
function respondTeamList(res, teams, ctx, caller) {
|
|
10680
10723
|
const shouldHide = caller === "member" ? ctx.teamBudgetVisibility.hideTeamBudgetForMembers : ctx.teamBudgetVisibility.hideTeamBudgetForManagers;
|
|
10681
10724
|
res.json(shouldHide ? teams.map(redactTeamBudget) : teams);
|
|
@@ -10716,7 +10759,6 @@ function registerTeamsRoutes(router, ctx) {
|
|
|
10716
10759
|
provisioningDefaults,
|
|
10717
10760
|
roleConfigs,
|
|
10718
10761
|
teamAdminCfg,
|
|
10719
|
-
teamBudgetVisibility,
|
|
10720
10762
|
requireTeamMgmt,
|
|
10721
10763
|
sendTeamError,
|
|
10722
10764
|
authorizeTeamSubresource
|
|
@@ -10796,7 +10838,7 @@ function registerTeamsRoutes(router, ctx) {
|
|
|
10796
10838
|
logger.info(`Team creation already in flight for ${key} \u2014 joining`);
|
|
10797
10839
|
try {
|
|
10798
10840
|
const result = await pending;
|
|
10799
|
-
res
|
|
10841
|
+
respondTeam(res, result, ctx, "manager");
|
|
10800
10842
|
} catch (err) {
|
|
10801
10843
|
sendTeamError(err, res);
|
|
10802
10844
|
}
|
|
@@ -10828,9 +10870,7 @@ function registerTeamsRoutes(router, ctx) {
|
|
|
10828
10870
|
teamCreateInFlight.set(key, createPromise);
|
|
10829
10871
|
try {
|
|
10830
10872
|
const result = await createPromise;
|
|
10831
|
-
res
|
|
10832
|
-
teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(result) : result
|
|
10833
|
-
);
|
|
10873
|
+
respondTeam(res, result, ctx, "manager");
|
|
10834
10874
|
} catch (err) {
|
|
10835
10875
|
sendTeamError(err, res);
|
|
10836
10876
|
} finally {
|
|
@@ -10873,9 +10913,7 @@ function registerTeamsRoutes(router, ctx) {
|
|
|
10873
10913
|
teamId,
|
|
10874
10914
|
owningGroup
|
|
10875
10915
|
});
|
|
10876
|
-
res
|
|
10877
|
-
teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(r) : r
|
|
10878
|
-
);
|
|
10916
|
+
respondTeam(res, r, ctx, "manager");
|
|
10879
10917
|
} catch (err) {
|
|
10880
10918
|
sendTeamError(err, res);
|
|
10881
10919
|
}
|
|
@@ -10937,9 +10975,7 @@ function registerTeamsRoutes(router, ctx) {
|
|
|
10937
10975
|
const owned = all.filter(
|
|
10938
10976
|
(t) => typeof t.metadata?.owning_group === "string" && t.metadata.owning_group === teamAdminCfg.group
|
|
10939
10977
|
);
|
|
10940
|
-
res
|
|
10941
|
-
teamBudgetVisibility.hideTeamBudgetForManagers ? owned.map(redactTeamBudget) : owned
|
|
10942
|
-
);
|
|
10978
|
+
respondTeamList(res, owned, ctx, "manager");
|
|
10943
10979
|
} catch (err) {
|
|
10944
10980
|
sendTeamError(err, res);
|
|
10945
10981
|
}
|
|
@@ -11005,12 +11041,12 @@ function registerTeamsRoutes(router, ctx) {
|
|
|
11005
11041
|
});
|
|
11006
11042
|
return;
|
|
11007
11043
|
}
|
|
11008
|
-
const
|
|
11044
|
+
const computedUserId = toLiteLLMUserId(userEntityRef, userIdDomain);
|
|
11009
11045
|
try {
|
|
11010
11046
|
await getOrProvisionUser(
|
|
11011
11047
|
client,
|
|
11012
11048
|
userEntityRef,
|
|
11013
|
-
|
|
11049
|
+
computedUserId,
|
|
11014
11050
|
provisioningEnabled,
|
|
11015
11051
|
provisioningDefaults,
|
|
11016
11052
|
roleConfigs,
|
|
@@ -11025,6 +11061,7 @@ function registerTeamsRoutes(router, ctx) {
|
|
|
11025
11061
|
}
|
|
11026
11062
|
throw err;
|
|
11027
11063
|
}
|
|
11064
|
+
const litellmUserId = effectiveUserId(computedUserId);
|
|
11028
11065
|
try {
|
|
11029
11066
|
await client.teamMemberAdd({
|
|
11030
11067
|
team_id: teamId,
|
|
@@ -11042,9 +11079,7 @@ function registerTeamsRoutes(router, ctx) {
|
|
|
11042
11079
|
via
|
|
11043
11080
|
});
|
|
11044
11081
|
const updated = await withTeamFetchRetry(() => client.getTeamInfo(teamId));
|
|
11045
|
-
res
|
|
11046
|
-
teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(updated) : updated
|
|
11047
|
-
);
|
|
11082
|
+
respondTeam(res, updated, ctx, "manager");
|
|
11048
11083
|
} catch (err) {
|
|
11049
11084
|
sendTeamError(err, res);
|
|
11050
11085
|
}
|
|
@@ -11066,9 +11101,9 @@ function registerTeamsRoutes(router, ctx) {
|
|
|
11066
11101
|
res.status(400).json({ error: "userEntityRef query parameter is required" });
|
|
11067
11102
|
return;
|
|
11068
11103
|
}
|
|
11069
|
-
const litellmUserId =
|
|
11104
|
+
const litellmUserId = await ctx.resolveMemberUserId(userEntityRef);
|
|
11070
11105
|
if (via === "teamRole") {
|
|
11071
|
-
if (litellmUserId ===
|
|
11106
|
+
if (litellmUserId === await ctx.resolveMemberUserId(actor)) {
|
|
11072
11107
|
res.status(400).json({ error: "You cannot remove yourself from the team" });
|
|
11073
11108
|
return;
|
|
11074
11109
|
}
|
|
@@ -11095,9 +11130,7 @@ function registerTeamsRoutes(router, ctx) {
|
|
|
11095
11130
|
via
|
|
11096
11131
|
});
|
|
11097
11132
|
const updated = await withTeamFetchRetry(() => client.getTeamInfo(teamId));
|
|
11098
|
-
res
|
|
11099
|
-
teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(updated) : updated
|
|
11100
|
-
);
|
|
11133
|
+
respondTeam(res, updated, ctx, "manager");
|
|
11101
11134
|
} catch (err) {
|
|
11102
11135
|
sendTeamError(err, res);
|
|
11103
11136
|
}
|
|
@@ -11115,7 +11148,6 @@ function registerObjectPermissionsRoutes(router, ctx) {
|
|
|
11115
11148
|
permissions,
|
|
11116
11149
|
logger,
|
|
11117
11150
|
teamAdminCfg,
|
|
11118
|
-
teamBudgetVisibility,
|
|
11119
11151
|
requireObjectPerms,
|
|
11120
11152
|
sendTeamError,
|
|
11121
11153
|
authorizeTeamSubresource
|
|
@@ -11196,9 +11228,7 @@ function registerObjectPermissionsRoutes(router, ctx) {
|
|
|
11196
11228
|
owningGroup,
|
|
11197
11229
|
vector_stores: requested
|
|
11198
11230
|
});
|
|
11199
|
-
res
|
|
11200
|
-
teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(updated) : updated
|
|
11201
|
-
);
|
|
11231
|
+
respondTeam(res, updated, ctx, "manager");
|
|
11202
11232
|
} catch (err) {
|
|
11203
11233
|
sendTeamError(err, res);
|
|
11204
11234
|
}
|
|
@@ -11282,9 +11312,7 @@ function registerObjectPermissionsRoutes(router, ctx) {
|
|
|
11282
11312
|
before,
|
|
11283
11313
|
after: requested
|
|
11284
11314
|
});
|
|
11285
|
-
res
|
|
11286
|
-
teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(updated) : updated
|
|
11287
|
-
);
|
|
11315
|
+
respondTeam(res, updated, ctx, "manager");
|
|
11288
11316
|
} catch (err) {
|
|
11289
11317
|
sendTeamError(err, res);
|
|
11290
11318
|
}
|
|
@@ -11293,15 +11321,13 @@ function registerObjectPermissionsRoutes(router, ctx) {
|
|
|
11293
11321
|
}
|
|
11294
11322
|
|
|
11295
11323
|
// src/routes/teamUsage.ts
|
|
11324
|
+
var import_backstage_plugin_litellm_common7 = require("@acarmisc/backstage-plugin-litellm-common");
|
|
11296
11325
|
function registerTeamUsageRoutes(router, ctx) {
|
|
11297
11326
|
const {
|
|
11298
11327
|
client,
|
|
11299
11328
|
catalogClient,
|
|
11300
11329
|
auth,
|
|
11301
11330
|
logger,
|
|
11302
|
-
provisioningEnabled,
|
|
11303
|
-
provisioningDefaults,
|
|
11304
|
-
roleConfigs,
|
|
11305
11331
|
teamBudgetVisibility
|
|
11306
11332
|
} = ctx;
|
|
11307
11333
|
const requireUser = createRequireUser(ctx);
|
|
@@ -11314,18 +11340,7 @@ function registerTeamUsageRoutes(router, ctx) {
|
|
|
11314
11340
|
return;
|
|
11315
11341
|
}
|
|
11316
11342
|
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
|
-
);
|
|
11343
|
+
const userInfo = await getProvisionedUser(ctx, res);
|
|
11329
11344
|
const isMember = userInfo?.teams?.includes(teamId) ?? false;
|
|
11330
11345
|
let isManager = false;
|
|
11331
11346
|
const managerCheckNeeded = ctx.teamMgmtEnabled && (!isMember || teamBudgetVisibility.hideTeamBudgetForMembers !== teamBudgetVisibility.hideTeamBudgetForManagers);
|
|
@@ -11369,6 +11384,95 @@ function registerTeamUsageRoutes(router, ctx) {
|
|
|
11369
11384
|
sendError(res, error, logger, "fetch team usage");
|
|
11370
11385
|
}
|
|
11371
11386
|
});
|
|
11387
|
+
router.get("/teams/:teamId/usage/members", requireUser, async (req, res) => {
|
|
11388
|
+
const cfg = ctx.memberBreakdownCfg;
|
|
11389
|
+
if (!cfg.enabled) {
|
|
11390
|
+
res.status(404).json({ error: "Not found" });
|
|
11391
|
+
return;
|
|
11392
|
+
}
|
|
11393
|
+
try {
|
|
11394
|
+
const { teamId } = req.params;
|
|
11395
|
+
const { start_date, end_date } = req.query;
|
|
11396
|
+
if (!start_date || !end_date) {
|
|
11397
|
+
res.status(400).json({ error: "start_date and end_date are required" });
|
|
11398
|
+
return;
|
|
11399
|
+
}
|
|
11400
|
+
const tokenEntityRef = res.locals.tokenEntityRef;
|
|
11401
|
+
const userInfo = await getProvisionedUser(ctx, res);
|
|
11402
|
+
const callerId = res.locals.userId;
|
|
11403
|
+
const isMember = userInfo?.teams?.includes(teamId) ?? false;
|
|
11404
|
+
let team;
|
|
11405
|
+
try {
|
|
11406
|
+
team = await withTeamFetchRetry(() => client.getTeamInfo(teamId));
|
|
11407
|
+
} catch (err) {
|
|
11408
|
+
if (err instanceof LiteLLMUpstreamError && err.status === 404) {
|
|
11409
|
+
res.status(404).json({ error: "Team not found" });
|
|
11410
|
+
return;
|
|
11411
|
+
}
|
|
11412
|
+
throw err;
|
|
11413
|
+
}
|
|
11414
|
+
const owningGroup = typeof team.metadata?.owning_group === "string" ? team.metadata.owning_group : void 0;
|
|
11415
|
+
const isManager = ctx.teamMgmtEnabled && !!owningGroup && await isUserMemberOfGroup(tokenEntityRef, owningGroup, catalogClient, auth, logger);
|
|
11416
|
+
if (!isMember && !isManager) {
|
|
11417
|
+
res.status(404).json({ error: "Team not found" });
|
|
11418
|
+
return;
|
|
11419
|
+
}
|
|
11420
|
+
const role = team.members_with_roles?.find((m) => m.user_id === callerId)?.role;
|
|
11421
|
+
const viaRole = isMember && !!role && cfg.viewerRoles.includes(role);
|
|
11422
|
+
const allowed = isManager || viaRole || isMember && cfg.permissionEnabled && await ctx.assertPermission(req, import_backstage_plugin_litellm_common7.litellmTeamUsageReadPermission);
|
|
11423
|
+
if (!allowed) {
|
|
11424
|
+
res.status(403).json({
|
|
11425
|
+
error: "Access denied: viewing per-member usage needs a team role or permission"
|
|
11426
|
+
});
|
|
11427
|
+
return;
|
|
11428
|
+
}
|
|
11429
|
+
const [usage, keys] = await Promise.all([
|
|
11430
|
+
client.getTeamUsage(teamId, start_date, end_date),
|
|
11431
|
+
client.listTeamKeys(teamId)
|
|
11432
|
+
]);
|
|
11433
|
+
const profiles = await lookupProfiles(memberEmails(team, keys));
|
|
11434
|
+
const breakdown = buildTeamMemberUsage(teamId, usage, keys, team, profiles);
|
|
11435
|
+
let via = "permission";
|
|
11436
|
+
if (isManager) via = "manager";
|
|
11437
|
+
else if (viaRole) via = "teamRole";
|
|
11438
|
+
logger.info("team.usage.members.read", { actor: tokenEntityRef, teamId, via });
|
|
11439
|
+
const hide = isManager ? teamBudgetVisibility.hideTeamBudgetForManagers : teamBudgetVisibility.hideTeamBudgetForMembers;
|
|
11440
|
+
res.json(hide ? redactTeamMemberUsage(breakdown) : breakdown);
|
|
11441
|
+
} catch (error) {
|
|
11442
|
+
if (error instanceof ProvisioningError) {
|
|
11443
|
+
res.status(error.status).json(error.body);
|
|
11444
|
+
return;
|
|
11445
|
+
}
|
|
11446
|
+
sendError(res, error, logger, "fetch team member usage");
|
|
11447
|
+
}
|
|
11448
|
+
});
|
|
11449
|
+
async function lookupProfiles(emails) {
|
|
11450
|
+
const profiles = /* @__PURE__ */ new Map();
|
|
11451
|
+
if (!emails.length) return profiles;
|
|
11452
|
+
try {
|
|
11453
|
+
const { token } = await auth.getPluginRequestToken({
|
|
11454
|
+
onBehalfOf: await auth.getOwnServiceCredentials(),
|
|
11455
|
+
targetPluginId: "catalog"
|
|
11456
|
+
});
|
|
11457
|
+
const { items } = await catalogClient.getEntities(
|
|
11458
|
+
{
|
|
11459
|
+
filter: { kind: "User", "spec.profile.email": emails },
|
|
11460
|
+
fields: ["spec.profile"]
|
|
11461
|
+
},
|
|
11462
|
+
{ token }
|
|
11463
|
+
);
|
|
11464
|
+
for (const entity of items) {
|
|
11465
|
+
const profile = entity.spec?.profile ?? {};
|
|
11466
|
+
if (typeof profile.email === "string") {
|
|
11467
|
+
profiles.set(profile.email.toLowerCase(), { displayName: profile.displayName });
|
|
11468
|
+
}
|
|
11469
|
+
}
|
|
11470
|
+
} catch (err) {
|
|
11471
|
+
const message2 = err instanceof Error ? err.message : String(err);
|
|
11472
|
+
logger.warn(`Catalog lookup for team member names failed: ${message2}`);
|
|
11473
|
+
}
|
|
11474
|
+
return profiles;
|
|
11475
|
+
}
|
|
11372
11476
|
}
|
|
11373
11477
|
|
|
11374
11478
|
// node_modules/jose/dist/node/esm/runtime/base64url.js
|
|
@@ -12784,18 +12888,17 @@ function newDefaultVerifier(cfg) {
|
|
|
12784
12888
|
return new KeycloakJWTVerifier({ issuer: cfg.issuer, clientId: cfg.clientId });
|
|
12785
12889
|
}
|
|
12786
12890
|
function normalizeIdentityOptions(opts) {
|
|
12787
|
-
const o = typeof opts === "string" ? { userIdDomain: opts } : opts ?? {};
|
|
12788
12891
|
let domains = [];
|
|
12789
|
-
if (
|
|
12790
|
-
domains =
|
|
12791
|
-
} else if (
|
|
12792
|
-
domains = [
|
|
12892
|
+
if (opts.trustedEmailDomains?.length) {
|
|
12893
|
+
domains = opts.trustedEmailDomains;
|
|
12894
|
+
} else if (opts.userIdDomain) {
|
|
12895
|
+
domains = [opts.userIdDomain];
|
|
12793
12896
|
}
|
|
12794
12897
|
const trusted = domains.map((d) => d.toLowerCase());
|
|
12795
|
-
return { userIdDomain:
|
|
12898
|
+
return { userIdDomain: opts.userIdDomain, trusted };
|
|
12796
12899
|
}
|
|
12797
12900
|
function resolveBridgeUserId(claims, identity) {
|
|
12798
|
-
const { userIdDomain, trusted } = normalizeIdentityOptions(identity);
|
|
12901
|
+
const { userIdDomain, trusted } = normalizeIdentityOptions(identity ?? {});
|
|
12799
12902
|
if (trusted.length === 0) {
|
|
12800
12903
|
throw new BridgeIdentityError(
|
|
12801
12904
|
"set litellm.bridge.allowedEmailDomains (or litellm.userIdDomain) to use the CLI bridge"
|
|
@@ -12830,8 +12933,8 @@ function resolveBridgeUserId(claims, identity) {
|
|
|
12830
12933
|
name = name || emailLocalPart;
|
|
12831
12934
|
return toLiteLLMUserId(name, userIdDomain);
|
|
12832
12935
|
}
|
|
12833
|
-
async function getOrProvisionUserFromClaims(client, claims, provisioningEnabled, provisioningDefaults, logger,
|
|
12834
|
-
const userId = resolveBridgeUserId(claims,
|
|
12936
|
+
async function getOrProvisionUserFromClaims(client, claims, provisioningEnabled, provisioningDefaults, logger, identity) {
|
|
12937
|
+
const userId = resolveBridgeUserId(claims, identity);
|
|
12835
12938
|
const existing = await client.getUserInfo(userId);
|
|
12836
12939
|
if (existing) return existing;
|
|
12837
12940
|
if (claims.email) {
|
|
@@ -12872,42 +12975,21 @@ async function getOrProvisionUserFromClaims(client, claims, provisioningEnabled,
|
|
|
12872
12975
|
}
|
|
12873
12976
|
return created;
|
|
12874
12977
|
}
|
|
12875
|
-
async function bridgeListKeys(client, claims, provisioningEnabled, provisioningDefaults, logger,
|
|
12978
|
+
async function bridgeListKeys(client, claims, provisioningEnabled, provisioningDefaults, logger, identity) {
|
|
12876
12979
|
const user = await getOrProvisionUserFromClaims(
|
|
12877
12980
|
client,
|
|
12878
12981
|
claims,
|
|
12879
12982
|
provisioningEnabled,
|
|
12880
12983
|
provisioningDefaults,
|
|
12881
12984
|
logger,
|
|
12882
|
-
|
|
12985
|
+
identity
|
|
12883
12986
|
);
|
|
12884
12987
|
return client.listKeys(user.user_id);
|
|
12885
12988
|
}
|
|
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
12989
|
|
|
12908
12990
|
// src/routes/bridge.ts
|
|
12909
|
-
var import_backstage_plugin_litellm_common7 = require("@acarmisc/backstage-plugin-litellm-common");
|
|
12910
12991
|
var import_backstage_plugin_litellm_common8 = require("@acarmisc/backstage-plugin-litellm-common");
|
|
12992
|
+
var import_backstage_plugin_litellm_common9 = require("@acarmisc/backstage-plugin-litellm-common");
|
|
12911
12993
|
function registerBridgeRoutes(router, ctx, bridgeOpts) {
|
|
12912
12994
|
const {
|
|
12913
12995
|
client,
|
|
@@ -12915,13 +12997,12 @@ function registerBridgeRoutes(router, ctx, bridgeOpts) {
|
|
|
12915
12997
|
userIdDomain,
|
|
12916
12998
|
provisioningEnabled,
|
|
12917
12999
|
provisioningDefaults,
|
|
12918
|
-
roleConfigs,
|
|
12919
13000
|
allowUnlimitedBudget,
|
|
12920
13001
|
teamRequired,
|
|
12921
13002
|
keyValidationConfig
|
|
12922
13003
|
} = ctx;
|
|
12923
13004
|
const { config } = bridgeOpts;
|
|
12924
|
-
const generateKeyInputSchema = (0,
|
|
13005
|
+
const generateKeyInputSchema = (0, import_backstage_plugin_litellm_common9.createGenerateKeyInputSchema)(ctx.keyValidationConfig);
|
|
12925
13006
|
const bridgeCfg = readBridgeConfig(config);
|
|
12926
13007
|
const bridgeIdentity = {
|
|
12927
13008
|
userIdDomain,
|
|
@@ -13000,26 +13081,25 @@ function registerBridgeRoutes(router, ctx, bridgeOpts) {
|
|
|
13000
13081
|
if (!allowUnlimitedBudget && input.max_budget === void 0) {
|
|
13001
13082
|
input.max_budget = provisioningDefaults.maxBudget;
|
|
13002
13083
|
}
|
|
13003
|
-
const
|
|
13084
|
+
const userInfo = await getOrProvisionUserFromClaims(
|
|
13085
|
+
client,
|
|
13086
|
+
claims,
|
|
13087
|
+
provisioningEnabled,
|
|
13088
|
+
provisioningDefaults,
|
|
13089
|
+
logger,
|
|
13090
|
+
bridgeIdentity
|
|
13091
|
+
);
|
|
13004
13092
|
const keyCreateCtx = {
|
|
13005
13093
|
client,
|
|
13006
13094
|
config: {
|
|
13007
13095
|
allowUnlimitedBudget,
|
|
13008
13096
|
teamRequired
|
|
13009
13097
|
},
|
|
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
13098
|
logger,
|
|
13015
|
-
keyValidationConfig
|
|
13016
|
-
provisioningEnabled,
|
|
13017
|
-
provisioningDefaults,
|
|
13018
|
-
roleConfigs,
|
|
13019
|
-
userIdDomain
|
|
13099
|
+
keyValidationConfig
|
|
13020
13100
|
};
|
|
13021
13101
|
const result = await createKeyForUser(
|
|
13022
|
-
{
|
|
13102
|
+
{ userInfo, createdVia: "abby-cli" },
|
|
13023
13103
|
input,
|
|
13024
13104
|
keyCreateCtx
|
|
13025
13105
|
);
|
|
@@ -13040,20 +13120,21 @@ function registerBridgeRoutes(router, ctx, bridgeOpts) {
|
|
|
13040
13120
|
bridgeIdentity
|
|
13041
13121
|
);
|
|
13042
13122
|
const teamIds = user.teams ?? [];
|
|
13043
|
-
const teamMetadata =
|
|
13044
|
-
|
|
13045
|
-
|
|
13046
|
-
|
|
13047
|
-
|
|
13048
|
-
|
|
13049
|
-
|
|
13050
|
-
|
|
13051
|
-
|
|
13052
|
-
|
|
13053
|
-
|
|
13054
|
-
|
|
13055
|
-
|
|
13056
|
-
|
|
13123
|
+
const teamMetadata = await Promise.all(
|
|
13124
|
+
teamIds.map(async (teamId) => {
|
|
13125
|
+
try {
|
|
13126
|
+
const teamInfo = await client.getTeamInfo(teamId);
|
|
13127
|
+
return {
|
|
13128
|
+
id: teamId,
|
|
13129
|
+
name: teamInfo.team_alias || teamId
|
|
13130
|
+
// Use alias as display name, fallback to ID
|
|
13131
|
+
};
|
|
13132
|
+
} catch (e) {
|
|
13133
|
+
logger.debug(`Could not fetch team info for ${teamId}: ${e.message}`);
|
|
13134
|
+
return { id: teamId };
|
|
13135
|
+
}
|
|
13136
|
+
})
|
|
13137
|
+
);
|
|
13057
13138
|
res.json({ user_id: user.user_id, teams: teamIds, team_metadata: teamMetadata });
|
|
13058
13139
|
} catch (error) {
|
|
13059
13140
|
handleBridgeError(error, res);
|
|
@@ -13082,11 +13163,11 @@ function registerBridgeRoutes(router, ctx, bridgeOpts) {
|
|
|
13082
13163
|
}
|
|
13083
13164
|
const team = await client.getTeamInfo(teamId);
|
|
13084
13165
|
const allowed = team.models;
|
|
13085
|
-
if (!allowed || allowed.length === 0 || allowed.includes(
|
|
13166
|
+
if (!allowed || allowed.length === 0 || allowed.includes(import_backstage_plugin_litellm_common8.ALL_PROXY_MODELS)) {
|
|
13086
13167
|
res.json(models);
|
|
13087
13168
|
return;
|
|
13088
13169
|
}
|
|
13089
|
-
res.json(models.filter((m) => (0,
|
|
13170
|
+
res.json(models.filter((m) => (0, import_backstage_plugin_litellm_common8.isModelAllowed)(m, allowed)));
|
|
13090
13171
|
} catch (error) {
|
|
13091
13172
|
handleBridgeError(error, res);
|
|
13092
13173
|
}
|
|
@@ -13118,18 +13199,20 @@ async function createRouter(options) {
|
|
|
13118
13199
|
const keyMaxBudget = config.getOptionalNumber("litellm.keys.maxBudget") ?? 100;
|
|
13119
13200
|
const keyMaxTpm = config.getOptionalNumber("litellm.keys.maxTpm") ?? 1e5;
|
|
13120
13201
|
const keyMaxRpm = config.getOptionalNumber("litellm.keys.maxRpm") ?? 1e3;
|
|
13121
|
-
const keyAllowedDurations = config.getOptionalStringArray("litellm.keys.allowedDurations") ??
|
|
13202
|
+
const keyAllowedDurations = config.getOptionalStringArray("litellm.keys.allowedDurations") ?? import_backstage_plugin_litellm_common10.DEFAULT_KEY_DURATIONS;
|
|
13122
13203
|
const keyValidationConfig = {
|
|
13123
13204
|
maxBudget: keyMaxBudget,
|
|
13124
13205
|
maxTpm: keyMaxTpm,
|
|
13125
13206
|
maxRpm: keyMaxRpm,
|
|
13126
|
-
allowedDurations: keyAllowedDurations
|
|
13207
|
+
allowedDurations: keyAllowedDurations,
|
|
13208
|
+
allowedBudgetDurations: config.getOptionalStringArray("litellm.keys.allowedBudgetDurations") ?? import_backstage_plugin_litellm_common10.DEFAULT_KEY_BUDGET_DURATIONS
|
|
13127
13209
|
};
|
|
13128
13210
|
const teamMgmtEnabled = isTeamManagementEnabled(config);
|
|
13129
13211
|
const objectPermsEnabled = isObjectPermissionsEnabled(config);
|
|
13130
13212
|
const teamAdminCfg = readTeamAdminConfig(config);
|
|
13131
13213
|
const teamBudgetVisibility = readTeamBudgetVisibility(config);
|
|
13132
13214
|
const opencodeCfg = readOpencodeConfig(config);
|
|
13215
|
+
const memberBreakdownCfg = readMemberBreakdownConfig(config);
|
|
13133
13216
|
const catalogClient = options.catalogClient ?? new import_catalog_client.CatalogClient({ discoveryApi: discovery });
|
|
13134
13217
|
if (provisioningEnabled) {
|
|
13135
13218
|
logger.info(
|
|
@@ -13146,7 +13229,13 @@ async function createRouter(options) {
|
|
|
13146
13229
|
`LiteLLM team budget hiding enabled \u2014 members: ${teamBudgetVisibility.hideTeamBudgetForMembers}, managers: ${teamBudgetVisibility.hideTeamBudgetForManagers}`
|
|
13147
13230
|
);
|
|
13148
13231
|
}
|
|
13232
|
+
const resolveMemberUserId = async (userEntityRef) => {
|
|
13233
|
+
const computed = toLiteLLMUserId(userEntityRef, userIdDomain);
|
|
13234
|
+
await findLiteLLMUser(client, userEntityRef, computed, catalogClient, auth, logger);
|
|
13235
|
+
return effectiveUserId(computed);
|
|
13236
|
+
};
|
|
13149
13237
|
const ctx = {
|
|
13238
|
+
resolveMemberUserId,
|
|
13150
13239
|
client,
|
|
13151
13240
|
catalogClient,
|
|
13152
13241
|
auth,
|
|
@@ -13169,34 +13258,21 @@ async function createRouter(options) {
|
|
|
13169
13258
|
teamAdminCfg,
|
|
13170
13259
|
teamBudgetVisibility,
|
|
13171
13260
|
opencodeCfg,
|
|
13261
|
+
memberBreakdownCfg,
|
|
13172
13262
|
// Helper functions - defined inline below this object
|
|
13173
13263
|
authorizeKeyAction: async (req, keyId) => {
|
|
13174
13264
|
if (!req.res || !req.res.locals.tokenEntityRef || !req.res.locals.userId) {
|
|
13175
|
-
throw new
|
|
13265
|
+
throw new import_errors21.NotAllowedError("User identity not found in request");
|
|
13176
13266
|
}
|
|
13177
13267
|
const tokenEntityRef = req.res.locals.tokenEntityRef;
|
|
13178
|
-
const userId =
|
|
13268
|
+
const userId = await resolveMemberUserId(tokenEntityRef);
|
|
13179
13269
|
const ownKeys = await client.listKeys(userId);
|
|
13180
13270
|
const key = ownKeys.find((k) => (k.token ?? k.key) === keyId);
|
|
13181
13271
|
if (!key) {
|
|
13182
|
-
throw new
|
|
13272
|
+
throw new import_errors21.NotAllowedError("Access denied: key does not belong to the caller");
|
|
13183
13273
|
}
|
|
13184
13274
|
return { tokenEntityRef, userId, key };
|
|
13185
13275
|
},
|
|
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
13276
|
assertPermission: async (req, permission) => {
|
|
13201
13277
|
const credentials = await resolveCredentials(req, auth);
|
|
13202
13278
|
if (!credentials) return false;
|
|
@@ -13305,7 +13381,7 @@ async function createRouter(options) {
|
|
|
13305
13381
|
}
|
|
13306
13382
|
if (teamRoleEligible) {
|
|
13307
13383
|
const actor = await resolveUserId(req, auth);
|
|
13308
|
-
const litellmUserId = actor &&
|
|
13384
|
+
const litellmUserId = actor && await resolveMemberUserId(actor);
|
|
13309
13385
|
const role = existing.members_with_roles?.find(
|
|
13310
13386
|
(m) => m.user_id === litellmUserId
|
|
13311
13387
|
)?.role;
|
|
@@ -13339,7 +13415,7 @@ async function createRouter(options) {
|
|
|
13339
13415
|
}
|
|
13340
13416
|
|
|
13341
13417
|
// src/plugin.ts
|
|
13342
|
-
var
|
|
13418
|
+
var import_backstage_plugin_litellm_common11 = require("@acarmisc/backstage-plugin-litellm-common");
|
|
13343
13419
|
var BRIDGE_UNAUTHENTICATED_PATHS = [
|
|
13344
13420
|
"/bridge/health",
|
|
13345
13421
|
"/bridge/keys",
|
|
@@ -13368,7 +13444,7 @@ var litellmPlugin = (0, import_backend_plugin_api.createBackendPlugin)({
|
|
|
13368
13444
|
permissions,
|
|
13369
13445
|
permissionsRegistry
|
|
13370
13446
|
}) {
|
|
13371
|
-
permissionsRegistry.addPermissions(
|
|
13447
|
+
permissionsRegistry.addPermissions(import_backstage_plugin_litellm_common11.litellmPermissions);
|
|
13372
13448
|
const router = await createRouter({
|
|
13373
13449
|
config,
|
|
13374
13450
|
logger,
|
|
@@ -13388,7 +13464,7 @@ var litellmPlugin = (0, import_backend_plugin_api.createBackendPlugin)({
|
|
|
13388
13464
|
});
|
|
13389
13465
|
|
|
13390
13466
|
// src/index.ts
|
|
13391
|
-
var
|
|
13467
|
+
var import_backstage_plugin_litellm_common12 = require("@acarmisc/backstage-plugin-litellm-common");
|
|
13392
13468
|
// Annotate the CommonJS export names for ESM import in node:
|
|
13393
13469
|
0 && (module.exports = {
|
|
13394
13470
|
KeycloakJWTVerifier,
|
|
@@ -13396,7 +13472,6 @@ var import_backstage_plugin_litellm_common11 = require("@acarmisc/backstage-plug
|
|
|
13396
13472
|
ProvisioningError,
|
|
13397
13473
|
applyRoleOverrides,
|
|
13398
13474
|
assertTeamAdmin,
|
|
13399
|
-
bridgeGenerateKey,
|
|
13400
13475
|
bridgeListKeys,
|
|
13401
13476
|
createRouter,
|
|
13402
13477
|
getOrProvisionUser,
|
|
@@ -13417,7 +13492,6 @@ var import_backstage_plugin_litellm_common11 = require("@acarmisc/backstage-plug
|
|
|
13417
13492
|
litellmTeamMcpManagePermission,
|
|
13418
13493
|
litellmTeamMembersManagePermission,
|
|
13419
13494
|
newDefaultVerifier,
|
|
13420
|
-
normalizeRequestTags,
|
|
13421
13495
|
provisionUser,
|
|
13422
13496
|
readBridgeConfig,
|
|
13423
13497
|
readProvisioningDefaults,
|