@acarmisc/backstage-plugin-litellm-backend 0.19.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/config.d.ts +41 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.js +21 -0
- package/dist/index.cjs.js +324 -49
- package/dist/index.cjs.js.map +4 -4
- package/dist/openapi.js +22 -0
- package/dist/provisioning.d.ts +14 -1
- package/dist/provisioning.js +83 -17
- package/dist/router.js +17 -2
- package/dist/routes/config.js +3 -0
- package/dist/routes/context.d.ts +7 -0
- package/dist/routes/keys.js +2 -2
- package/dist/routes/middleware/withUser.d.ts +1 -0
- package/dist/routes/middleware/withUser.js +5 -1
- package/dist/routes/opencode.js +3 -3
- package/dist/routes/teamUsage.js +109 -0
- package/dist/routes/teams.js +6 -4
- package/dist/services/keyService.js +3 -0
- 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 -1
- package/package.json +2 -2
package/dist/index.cjs.js
CHANGED
|
@@ -7982,20 +7982,20 @@ __export(index_exports, {
|
|
|
7982
7982
|
getOrProvisionUser: () => getOrProvisionUser,
|
|
7983
7983
|
getOrProvisionUserFromClaims: () => getOrProvisionUserFromClaims,
|
|
7984
7984
|
isTeamManagementEnabled: () => isTeamManagementEnabled,
|
|
7985
|
-
litellmAuditReadPermission: () =>
|
|
7986
|
-
litellmKeyCreatePermission: () =>
|
|
7987
|
-
litellmKeyManagePermission: () =>
|
|
7988
|
-
litellmKeyResetSpendPermission: () =>
|
|
7989
|
-
litellmKeyRevokePermission: () =>
|
|
7990
|
-
litellmKeyUnblockPermission: () =>
|
|
7991
|
-
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,
|
|
7992
7992
|
litellmPlugin: () => litellmPlugin,
|
|
7993
|
-
litellmTeamCreatePermission: () =>
|
|
7994
|
-
litellmTeamDeletePermission: () =>
|
|
7995
|
-
litellmTeamKnowledgebaseManagePermission: () =>
|
|
7996
|
-
litellmTeamManagePermission: () =>
|
|
7997
|
-
litellmTeamMcpManagePermission: () =>
|
|
7998
|
-
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,
|
|
7999
7999
|
newDefaultVerifier: () => newDefaultVerifier,
|
|
8000
8000
|
provisionUser: () => provisionUser,
|
|
8001
8001
|
readBridgeConfig: () => readBridgeConfig,
|
|
@@ -8197,6 +8197,28 @@ var LiteLLMClient = class {
|
|
|
8197
8197
|
throw err;
|
|
8198
8198
|
}
|
|
8199
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
|
+
}
|
|
8200
8222
|
toVirtualKey(k) {
|
|
8201
8223
|
return {
|
|
8202
8224
|
key: k.key_name ?? k.token,
|
|
@@ -8901,6 +8923,38 @@ var ProvisioningError = class extends Error {
|
|
|
8901
8923
|
this.body = { error: message2, hint, provisioning };
|
|
8902
8924
|
}
|
|
8903
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
|
+
}
|
|
8904
8958
|
async function getOrProvisionUser(client, tokenEntityRef, userId, provisioningEnabled, provisioningDefaults, roleConfigs, catalogClient, auth, logger) {
|
|
8905
8959
|
if (!userId) {
|
|
8906
8960
|
throw new ProvisioningError(
|
|
@@ -8909,17 +8963,10 @@ async function getOrProvisionUser(client, tokenEntityRef, userId, provisioningEn
|
|
|
8909
8963
|
provisioningEnabled
|
|
8910
8964
|
);
|
|
8911
8965
|
}
|
|
8912
|
-
const existing = await client
|
|
8966
|
+
const existing = await getUserByIdOrAdopted(client, userId);
|
|
8913
8967
|
if (existing) {
|
|
8914
8968
|
return existing;
|
|
8915
8969
|
}
|
|
8916
|
-
if (!provisioningEnabled) {
|
|
8917
|
-
throw new ProvisioningError(
|
|
8918
|
-
"User not found in LiteLLM",
|
|
8919
|
-
"Enable litellm.provisioning.enabled in app-config.yaml or create the user manually",
|
|
8920
|
-
false
|
|
8921
|
-
);
|
|
8922
|
-
}
|
|
8923
8970
|
const pending = provisioningInFlight.get(userId);
|
|
8924
8971
|
if (pending) {
|
|
8925
8972
|
logger.info(
|
|
@@ -8928,11 +8975,25 @@ async function getOrProvisionUser(client, tokenEntityRef, userId, provisioningEn
|
|
|
8928
8975
|
return pending;
|
|
8929
8976
|
}
|
|
8930
8977
|
const provisionPromise = (async () => {
|
|
8931
|
-
const
|
|
8932
|
-
const
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
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
|
+
);
|
|
8936
8997
|
const effectiveDefaults = matchedRole ? applyRoleOverrides(provisioningDefaults, matchedRole) : provisioningDefaults;
|
|
8937
8998
|
if (matchedRole) {
|
|
8938
8999
|
logger.info(
|
|
@@ -8961,7 +9022,7 @@ async function getOrProvisionUser(client, tokenEntityRef, userId, provisioningEn
|
|
|
8961
9022
|
logger.info(
|
|
8962
9023
|
`LiteLLM user ${userId} already exists during provisioning \u2014 re-fetching`
|
|
8963
9024
|
);
|
|
8964
|
-
const refetched = await client.getUserInfo(userId);
|
|
9025
|
+
const refetched = await client.getUserInfo(userId) ?? await adoptUserByEmail(client, userId, profile.email, logger);
|
|
8965
9026
|
if (refetched) {
|
|
8966
9027
|
return refetched;
|
|
8967
9028
|
}
|
|
@@ -9022,7 +9083,7 @@ async function resolveUserRole(userEntityRef, roleConfigs, catalogClient, auth,
|
|
|
9022
9083
|
}
|
|
9023
9084
|
|
|
9024
9085
|
// src/router.ts
|
|
9025
|
-
var
|
|
9086
|
+
var import_backstage_plugin_litellm_common10 = require("@acarmisc/backstage-plugin-litellm-common");
|
|
9026
9087
|
|
|
9027
9088
|
// src/teamAdmin.ts
|
|
9028
9089
|
var import_plugin_permission_common = __toESM(require_index_cjs2());
|
|
@@ -9297,6 +9358,93 @@ function readOpencodeConfig(config) {
|
|
|
9297
9358
|
};
|
|
9298
9359
|
}
|
|
9299
9360
|
|
|
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
|
+
};
|
|
9368
|
+
}
|
|
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());
|
|
9374
|
+
}
|
|
9375
|
+
for (const k of keys) {
|
|
9376
|
+
if (k.user_id?.includes("@")) emails.add(k.user_id.toLowerCase());
|
|
9377
|
+
}
|
|
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
|
|
9397
|
+
};
|
|
9398
|
+
rows.set(userId, row);
|
|
9399
|
+
}
|
|
9400
|
+
return row;
|
|
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 };
|
|
9438
|
+
}
|
|
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
|
+
};
|
|
9446
|
+
}
|
|
9447
|
+
|
|
9300
9448
|
// src/routes/middleware/withUser.ts
|
|
9301
9449
|
function createRequireUser(ctx) {
|
|
9302
9450
|
return async (req, res, next) => {
|
|
@@ -9317,7 +9465,7 @@ function createRequireUser(ctx) {
|
|
|
9317
9465
|
async function getProvisionedUser(ctx, res) {
|
|
9318
9466
|
const tokenEntityRef = res.locals.tokenEntityRef;
|
|
9319
9467
|
const userId = res.locals.userId;
|
|
9320
|
-
|
|
9468
|
+
const userInfo = await getOrProvisionUser(
|
|
9321
9469
|
ctx.client,
|
|
9322
9470
|
tokenEntityRef,
|
|
9323
9471
|
userId,
|
|
@@ -9328,6 +9476,8 @@ async function getProvisionedUser(ctx, res) {
|
|
|
9328
9476
|
ctx.auth,
|
|
9329
9477
|
ctx.logger
|
|
9330
9478
|
);
|
|
9479
|
+
res.locals.userId = effectiveUserId(userId);
|
|
9480
|
+
return userInfo;
|
|
9331
9481
|
}
|
|
9332
9482
|
|
|
9333
9483
|
// src/services/userInfoCache.ts
|
|
@@ -9536,6 +9686,7 @@ var openApiSpec = {
|
|
|
9536
9686
|
team_id: { type: "string" },
|
|
9537
9687
|
duration: { type: "string" },
|
|
9538
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' },
|
|
9539
9690
|
tpm_limit: { type: "number" },
|
|
9540
9691
|
rpm_limit: { type: "number" }
|
|
9541
9692
|
} } } }
|
|
@@ -9632,6 +9783,24 @@ var openApiSpec = {
|
|
|
9632
9783
|
responses: { "200": { description: "Team usage" }, "400": { description: "Missing date range" } }
|
|
9633
9784
|
}
|
|
9634
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
|
+
},
|
|
9635
9804
|
"/usage": {
|
|
9636
9805
|
get: {
|
|
9637
9806
|
tags: ["Usage"],
|
|
@@ -9737,6 +9906,9 @@ function registerConfigRoutes(router, ctx) {
|
|
|
9737
9906
|
readOnly: ctx.teamAdminCfg.readOnly,
|
|
9738
9907
|
memberManagerRoles: ctx.teamAdminCfg.memberManagerRoles
|
|
9739
9908
|
},
|
|
9909
|
+
teamUsage: {
|
|
9910
|
+
memberBreakdown: ctx.memberBreakdownCfg.enabled
|
|
9911
|
+
},
|
|
9740
9912
|
display: {
|
|
9741
9913
|
hideTeamBudgetForMembers: ctx.teamBudgetVisibility.hideTeamBudgetForMembers,
|
|
9742
9914
|
hideTeamBudgetForManagers: ctx.teamBudgetVisibility.hideTeamBudgetForManagers
|
|
@@ -9908,6 +10080,9 @@ async function createKeyForUser(user, input, ctx) {
|
|
|
9908
10080
|
if (input.max_budget !== void 0) {
|
|
9909
10081
|
upstreamRequest.max_budget = input.max_budget ?? void 0;
|
|
9910
10082
|
}
|
|
10083
|
+
if (input.budget_duration !== void 0) {
|
|
10084
|
+
upstreamRequest.budget_duration = input.budget_duration;
|
|
10085
|
+
}
|
|
9911
10086
|
if (input.tpm_limit !== void 0) {
|
|
9912
10087
|
upstreamRequest.tpm_limit = input.tpm_limit;
|
|
9913
10088
|
}
|
|
@@ -9966,8 +10141,8 @@ function registerKeysRoutes(router, ctx) {
|
|
|
9966
10141
|
});
|
|
9967
10142
|
router.get("/keys", async (_req, res) => {
|
|
9968
10143
|
try {
|
|
9969
|
-
const userId = res.locals.userId;
|
|
9970
10144
|
await getProvisionedUser(ctx, res);
|
|
10145
|
+
const userId = res.locals.userId;
|
|
9971
10146
|
const keys = await client.listKeys(userId);
|
|
9972
10147
|
res.json(keys);
|
|
9973
10148
|
} catch (error) {
|
|
@@ -9984,8 +10159,8 @@ function registerKeysRoutes(router, ctx) {
|
|
|
9984
10159
|
sendPermissionDenied(res, import_backstage_plugin_litellm_common3.litellmKeyRevokePermission);
|
|
9985
10160
|
return;
|
|
9986
10161
|
}
|
|
9987
|
-
const userId = res.locals.userId;
|
|
9988
10162
|
await getProvisionedUser(ctx, res);
|
|
10163
|
+
const userId = res.locals.userId;
|
|
9989
10164
|
const keys = await client.listKeys(userId);
|
|
9990
10165
|
const now = Date.now();
|
|
9991
10166
|
const expiredKeys = keys.filter((k) => {
|
|
@@ -10378,11 +10553,11 @@ function registerOpencodeRoutes(router, ctx) {
|
|
|
10378
10553
|
sendPermissionDenied(res, import_backstage_plugin_litellm_common4.litellmKeyCreatePermission);
|
|
10379
10554
|
return null;
|
|
10380
10555
|
}
|
|
10381
|
-
const
|
|
10556
|
+
const computedUserId = toLiteLLMUserId(tokenEntityRef, userIdDomain);
|
|
10382
10557
|
const userInfo = await getOrProvisionUser(
|
|
10383
10558
|
client,
|
|
10384
10559
|
tokenEntityRef,
|
|
10385
|
-
|
|
10560
|
+
computedUserId,
|
|
10386
10561
|
provisioningEnabled,
|
|
10387
10562
|
provisioningDefaults,
|
|
10388
10563
|
roleConfigs,
|
|
@@ -10404,7 +10579,7 @@ function registerOpencodeRoutes(router, ctx) {
|
|
|
10404
10579
|
res.status(400).json({ error: "A team is required for this connection" });
|
|
10405
10580
|
return null;
|
|
10406
10581
|
}
|
|
10407
|
-
return { tokenEntityRef, userId, userInfo, teamId, redirectUri };
|
|
10582
|
+
return { tokenEntityRef, userId: effectiveUserId(computedUserId), userInfo, teamId, redirectUri };
|
|
10408
10583
|
};
|
|
10409
10584
|
router.get("/opencode/connect", async (req, res) => {
|
|
10410
10585
|
try {
|
|
@@ -10866,12 +11041,12 @@ function registerTeamsRoutes(router, ctx) {
|
|
|
10866
11041
|
});
|
|
10867
11042
|
return;
|
|
10868
11043
|
}
|
|
10869
|
-
const
|
|
11044
|
+
const computedUserId = toLiteLLMUserId(userEntityRef, userIdDomain);
|
|
10870
11045
|
try {
|
|
10871
11046
|
await getOrProvisionUser(
|
|
10872
11047
|
client,
|
|
10873
11048
|
userEntityRef,
|
|
10874
|
-
|
|
11049
|
+
computedUserId,
|
|
10875
11050
|
provisioningEnabled,
|
|
10876
11051
|
provisioningDefaults,
|
|
10877
11052
|
roleConfigs,
|
|
@@ -10886,6 +11061,7 @@ function registerTeamsRoutes(router, ctx) {
|
|
|
10886
11061
|
}
|
|
10887
11062
|
throw err;
|
|
10888
11063
|
}
|
|
11064
|
+
const litellmUserId = effectiveUserId(computedUserId);
|
|
10889
11065
|
try {
|
|
10890
11066
|
await client.teamMemberAdd({
|
|
10891
11067
|
team_id: teamId,
|
|
@@ -10925,9 +11101,9 @@ function registerTeamsRoutes(router, ctx) {
|
|
|
10925
11101
|
res.status(400).json({ error: "userEntityRef query parameter is required" });
|
|
10926
11102
|
return;
|
|
10927
11103
|
}
|
|
10928
|
-
const litellmUserId =
|
|
11104
|
+
const litellmUserId = await ctx.resolveMemberUserId(userEntityRef);
|
|
10929
11105
|
if (via === "teamRole") {
|
|
10930
|
-
if (litellmUserId ===
|
|
11106
|
+
if (litellmUserId === await ctx.resolveMemberUserId(actor)) {
|
|
10931
11107
|
res.status(400).json({ error: "You cannot remove yourself from the team" });
|
|
10932
11108
|
return;
|
|
10933
11109
|
}
|
|
@@ -11145,6 +11321,7 @@ function registerObjectPermissionsRoutes(router, ctx) {
|
|
|
11145
11321
|
}
|
|
11146
11322
|
|
|
11147
11323
|
// src/routes/teamUsage.ts
|
|
11324
|
+
var import_backstage_plugin_litellm_common7 = require("@acarmisc/backstage-plugin-litellm-common");
|
|
11148
11325
|
function registerTeamUsageRoutes(router, ctx) {
|
|
11149
11326
|
const {
|
|
11150
11327
|
client,
|
|
@@ -11207,6 +11384,95 @@ function registerTeamUsageRoutes(router, ctx) {
|
|
|
11207
11384
|
sendError(res, error, logger, "fetch team usage");
|
|
11208
11385
|
}
|
|
11209
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
|
+
}
|
|
11210
11476
|
}
|
|
11211
11477
|
|
|
11212
11478
|
// node_modules/jose/dist/node/esm/runtime/base64url.js
|
|
@@ -12722,8 +12988,8 @@ async function bridgeListKeys(client, claims, provisioningEnabled, provisioningD
|
|
|
12722
12988
|
}
|
|
12723
12989
|
|
|
12724
12990
|
// src/routes/bridge.ts
|
|
12725
|
-
var import_backstage_plugin_litellm_common7 = require("@acarmisc/backstage-plugin-litellm-common");
|
|
12726
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");
|
|
12727
12993
|
function registerBridgeRoutes(router, ctx, bridgeOpts) {
|
|
12728
12994
|
const {
|
|
12729
12995
|
client,
|
|
@@ -12736,7 +13002,7 @@ function registerBridgeRoutes(router, ctx, bridgeOpts) {
|
|
|
12736
13002
|
keyValidationConfig
|
|
12737
13003
|
} = ctx;
|
|
12738
13004
|
const { config } = bridgeOpts;
|
|
12739
|
-
const generateKeyInputSchema = (0,
|
|
13005
|
+
const generateKeyInputSchema = (0, import_backstage_plugin_litellm_common9.createGenerateKeyInputSchema)(ctx.keyValidationConfig);
|
|
12740
13006
|
const bridgeCfg = readBridgeConfig(config);
|
|
12741
13007
|
const bridgeIdentity = {
|
|
12742
13008
|
userIdDomain,
|
|
@@ -12897,11 +13163,11 @@ function registerBridgeRoutes(router, ctx, bridgeOpts) {
|
|
|
12897
13163
|
}
|
|
12898
13164
|
const team = await client.getTeamInfo(teamId);
|
|
12899
13165
|
const allowed = team.models;
|
|
12900
|
-
if (!allowed || allowed.length === 0 || allowed.includes(
|
|
13166
|
+
if (!allowed || allowed.length === 0 || allowed.includes(import_backstage_plugin_litellm_common8.ALL_PROXY_MODELS)) {
|
|
12901
13167
|
res.json(models);
|
|
12902
13168
|
return;
|
|
12903
13169
|
}
|
|
12904
|
-
res.json(models.filter((m) => (0,
|
|
13170
|
+
res.json(models.filter((m) => (0, import_backstage_plugin_litellm_common8.isModelAllowed)(m, allowed)));
|
|
12905
13171
|
} catch (error) {
|
|
12906
13172
|
handleBridgeError(error, res);
|
|
12907
13173
|
}
|
|
@@ -12933,18 +13199,20 @@ async function createRouter(options) {
|
|
|
12933
13199
|
const keyMaxBudget = config.getOptionalNumber("litellm.keys.maxBudget") ?? 100;
|
|
12934
13200
|
const keyMaxTpm = config.getOptionalNumber("litellm.keys.maxTpm") ?? 1e5;
|
|
12935
13201
|
const keyMaxRpm = config.getOptionalNumber("litellm.keys.maxRpm") ?? 1e3;
|
|
12936
|
-
const keyAllowedDurations = config.getOptionalStringArray("litellm.keys.allowedDurations") ??
|
|
13202
|
+
const keyAllowedDurations = config.getOptionalStringArray("litellm.keys.allowedDurations") ?? import_backstage_plugin_litellm_common10.DEFAULT_KEY_DURATIONS;
|
|
12937
13203
|
const keyValidationConfig = {
|
|
12938
13204
|
maxBudget: keyMaxBudget,
|
|
12939
13205
|
maxTpm: keyMaxTpm,
|
|
12940
13206
|
maxRpm: keyMaxRpm,
|
|
12941
|
-
allowedDurations: keyAllowedDurations
|
|
13207
|
+
allowedDurations: keyAllowedDurations,
|
|
13208
|
+
allowedBudgetDurations: config.getOptionalStringArray("litellm.keys.allowedBudgetDurations") ?? import_backstage_plugin_litellm_common10.DEFAULT_KEY_BUDGET_DURATIONS
|
|
12942
13209
|
};
|
|
12943
13210
|
const teamMgmtEnabled = isTeamManagementEnabled(config);
|
|
12944
13211
|
const objectPermsEnabled = isObjectPermissionsEnabled(config);
|
|
12945
13212
|
const teamAdminCfg = readTeamAdminConfig(config);
|
|
12946
13213
|
const teamBudgetVisibility = readTeamBudgetVisibility(config);
|
|
12947
13214
|
const opencodeCfg = readOpencodeConfig(config);
|
|
13215
|
+
const memberBreakdownCfg = readMemberBreakdownConfig(config);
|
|
12948
13216
|
const catalogClient = options.catalogClient ?? new import_catalog_client.CatalogClient({ discoveryApi: discovery });
|
|
12949
13217
|
if (provisioningEnabled) {
|
|
12950
13218
|
logger.info(
|
|
@@ -12961,7 +13229,13 @@ async function createRouter(options) {
|
|
|
12961
13229
|
`LiteLLM team budget hiding enabled \u2014 members: ${teamBudgetVisibility.hideTeamBudgetForMembers}, managers: ${teamBudgetVisibility.hideTeamBudgetForManagers}`
|
|
12962
13230
|
);
|
|
12963
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
|
+
};
|
|
12964
13237
|
const ctx = {
|
|
13238
|
+
resolveMemberUserId,
|
|
12965
13239
|
client,
|
|
12966
13240
|
catalogClient,
|
|
12967
13241
|
auth,
|
|
@@ -12984,13 +13258,14 @@ async function createRouter(options) {
|
|
|
12984
13258
|
teamAdminCfg,
|
|
12985
13259
|
teamBudgetVisibility,
|
|
12986
13260
|
opencodeCfg,
|
|
13261
|
+
memberBreakdownCfg,
|
|
12987
13262
|
// Helper functions - defined inline below this object
|
|
12988
13263
|
authorizeKeyAction: async (req, keyId) => {
|
|
12989
13264
|
if (!req.res || !req.res.locals.tokenEntityRef || !req.res.locals.userId) {
|
|
12990
13265
|
throw new import_errors21.NotAllowedError("User identity not found in request");
|
|
12991
13266
|
}
|
|
12992
13267
|
const tokenEntityRef = req.res.locals.tokenEntityRef;
|
|
12993
|
-
const userId =
|
|
13268
|
+
const userId = await resolveMemberUserId(tokenEntityRef);
|
|
12994
13269
|
const ownKeys = await client.listKeys(userId);
|
|
12995
13270
|
const key = ownKeys.find((k) => (k.token ?? k.key) === keyId);
|
|
12996
13271
|
if (!key) {
|
|
@@ -13106,7 +13381,7 @@ async function createRouter(options) {
|
|
|
13106
13381
|
}
|
|
13107
13382
|
if (teamRoleEligible) {
|
|
13108
13383
|
const actor = await resolveUserId(req, auth);
|
|
13109
|
-
const litellmUserId = actor &&
|
|
13384
|
+
const litellmUserId = actor && await resolveMemberUserId(actor);
|
|
13110
13385
|
const role = existing.members_with_roles?.find(
|
|
13111
13386
|
(m) => m.user_id === litellmUserId
|
|
13112
13387
|
)?.role;
|
|
@@ -13140,7 +13415,7 @@ async function createRouter(options) {
|
|
|
13140
13415
|
}
|
|
13141
13416
|
|
|
13142
13417
|
// src/plugin.ts
|
|
13143
|
-
var
|
|
13418
|
+
var import_backstage_plugin_litellm_common11 = require("@acarmisc/backstage-plugin-litellm-common");
|
|
13144
13419
|
var BRIDGE_UNAUTHENTICATED_PATHS = [
|
|
13145
13420
|
"/bridge/health",
|
|
13146
13421
|
"/bridge/keys",
|
|
@@ -13169,7 +13444,7 @@ var litellmPlugin = (0, import_backend_plugin_api.createBackendPlugin)({
|
|
|
13169
13444
|
permissions,
|
|
13170
13445
|
permissionsRegistry
|
|
13171
13446
|
}) {
|
|
13172
|
-
permissionsRegistry.addPermissions(
|
|
13447
|
+
permissionsRegistry.addPermissions(import_backstage_plugin_litellm_common11.litellmPermissions);
|
|
13173
13448
|
const router = await createRouter({
|
|
13174
13449
|
config,
|
|
13175
13450
|
logger,
|
|
@@ -13189,7 +13464,7 @@ var litellmPlugin = (0, import_backend_plugin_api.createBackendPlugin)({
|
|
|
13189
13464
|
});
|
|
13190
13465
|
|
|
13191
13466
|
// src/index.ts
|
|
13192
|
-
var
|
|
13467
|
+
var import_backstage_plugin_litellm_common12 = require("@acarmisc/backstage-plugin-litellm-common");
|
|
13193
13468
|
// Annotate the CommonJS export names for ESM import in node:
|
|
13194
13469
|
0 && (module.exports = {
|
|
13195
13470
|
KeycloakJWTVerifier,
|