@acarmisc/backstage-plugin-litellm-backend 0.19.0 → 0.21.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 +47 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.js +21 -0
- package/dist/index.cjs.js +332 -54
- package/dist/index.cjs.js.map +4 -4
- package/dist/openapi.js +22 -0
- package/dist/opencode.d.ts +5 -0
- package/dist/opencode.js +1 -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 +12 -8
- 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());
|
|
@@ -9290,6 +9351,7 @@ function readOpencodeConfig(config) {
|
|
|
9290
9351
|
enabled,
|
|
9291
9352
|
keyDuration: config.getOptionalString("litellm.opencode.keyDuration") ?? "30d",
|
|
9292
9353
|
maxBudget: config.getOptionalNumber("litellm.opencode.maxBudget") ?? 50,
|
|
9354
|
+
budgetDuration: config.getOptionalString("litellm.opencode.budgetDuration"),
|
|
9293
9355
|
requireTeam: config.getOptionalBoolean("litellm.opencode.requireTeam") ?? false,
|
|
9294
9356
|
metadata: config.getOptional(
|
|
9295
9357
|
"litellm.opencode.metadata"
|
|
@@ -9297,6 +9359,93 @@ function readOpencodeConfig(config) {
|
|
|
9297
9359
|
};
|
|
9298
9360
|
}
|
|
9299
9361
|
|
|
9362
|
+
// src/teamMemberUsage.ts
|
|
9363
|
+
function readMemberBreakdownConfig(config) {
|
|
9364
|
+
return {
|
|
9365
|
+
enabled: config.getOptionalBoolean("litellm.teamUsage.memberBreakdown.enabled") ?? false,
|
|
9366
|
+
viewerRoles: config.getOptionalStringArray("litellm.teamUsage.memberBreakdown.viewerRoles") ?? [],
|
|
9367
|
+
permissionEnabled: config.getOptionalBoolean("permission.enabled") ?? false
|
|
9368
|
+
};
|
|
9369
|
+
}
|
|
9370
|
+
function memberEmails(team, keys) {
|
|
9371
|
+
const emails = /* @__PURE__ */ new Set();
|
|
9372
|
+
for (const m of team.members_with_roles ?? []) {
|
|
9373
|
+
if (m.user_email) emails.add(m.user_email.toLowerCase());
|
|
9374
|
+
else if (m.user_id?.includes("@")) emails.add(m.user_id.toLowerCase());
|
|
9375
|
+
}
|
|
9376
|
+
for (const k of keys) {
|
|
9377
|
+
if (k.user_id?.includes("@")) emails.add(k.user_id.toLowerCase());
|
|
9378
|
+
}
|
|
9379
|
+
return [...emails];
|
|
9380
|
+
}
|
|
9381
|
+
function buildTeamMemberUsage(teamId, usage, keys, team, profiles = /* @__PURE__ */ new Map()) {
|
|
9382
|
+
const rows = /* @__PURE__ */ new Map();
|
|
9383
|
+
const rowFor = (userId) => {
|
|
9384
|
+
let row = rows.get(userId);
|
|
9385
|
+
if (!row) {
|
|
9386
|
+
row = {
|
|
9387
|
+
user_id: userId,
|
|
9388
|
+
spend: 0,
|
|
9389
|
+
spend_share_pct: 0,
|
|
9390
|
+
prompt_tokens: 0,
|
|
9391
|
+
completion_tokens: 0,
|
|
9392
|
+
total_tokens: 0,
|
|
9393
|
+
api_requests: 0,
|
|
9394
|
+
successful_requests: 0,
|
|
9395
|
+
failed_requests: 0,
|
|
9396
|
+
success_rate: null,
|
|
9397
|
+
key_count: 0
|
|
9398
|
+
};
|
|
9399
|
+
rows.set(userId, row);
|
|
9400
|
+
}
|
|
9401
|
+
return row;
|
|
9402
|
+
};
|
|
9403
|
+
for (const m of team.members_with_roles ?? []) {
|
|
9404
|
+
if (!m.user_id) continue;
|
|
9405
|
+
const row = rowFor(m.user_id);
|
|
9406
|
+
row.role = m.role;
|
|
9407
|
+
if (m.user_email) row.user_email = m.user_email;
|
|
9408
|
+
}
|
|
9409
|
+
const ownerByKey = /* @__PURE__ */ new Map();
|
|
9410
|
+
for (const k of keys) {
|
|
9411
|
+
if (!k.user_id || !k.token) continue;
|
|
9412
|
+
ownerByKey.set(k.token, k.user_id);
|
|
9413
|
+
rowFor(k.user_id).key_count += 1;
|
|
9414
|
+
}
|
|
9415
|
+
for (const [keyHash, k] of Object.entries(usage.usage_by_key ?? {})) {
|
|
9416
|
+
const row = rowFor(ownerByKey.get(keyHash) ?? null);
|
|
9417
|
+
row.spend += k.total_spend ?? 0;
|
|
9418
|
+
row.prompt_tokens += k.prompt_tokens ?? 0;
|
|
9419
|
+
row.completion_tokens += k.completion_tokens ?? 0;
|
|
9420
|
+
row.total_tokens += k.total_tokens ?? 0;
|
|
9421
|
+
row.api_requests += k.api_requests ?? 0;
|
|
9422
|
+
row.successful_requests += k.successful_requests ?? 0;
|
|
9423
|
+
row.failed_requests += k.failed_requests ?? 0;
|
|
9424
|
+
}
|
|
9425
|
+
const members = [...rows.values()];
|
|
9426
|
+
const totalSpend = members.reduce((sum, r) => sum + r.spend, 0);
|
|
9427
|
+
for (const row of members) {
|
|
9428
|
+
row.spend_share_pct = totalSpend > 0 ? row.spend / totalSpend * 100 : 0;
|
|
9429
|
+
const finished = row.successful_requests + row.failed_requests;
|
|
9430
|
+
row.success_rate = finished > 0 ? row.successful_requests / finished * 100 : null;
|
|
9431
|
+
if (!row.user_email && row.user_id?.includes("@")) row.user_email = row.user_id;
|
|
9432
|
+
const profile = row.user_email && profiles.get(row.user_email.toLowerCase());
|
|
9433
|
+
if (profile && profile.displayName) row.display_name = profile.displayName;
|
|
9434
|
+
}
|
|
9435
|
+
members.sort(
|
|
9436
|
+
(a, b) => b.spend - a.spend || b.total_tokens - a.total_tokens || String(a.user_id ?? "\uFFFF").localeCompare(String(b.user_id ?? "\uFFFF"))
|
|
9437
|
+
);
|
|
9438
|
+
return { team_id: teamId, total_spend: totalSpend, members };
|
|
9439
|
+
}
|
|
9440
|
+
function redactTeamMemberUsage(usage) {
|
|
9441
|
+
return {
|
|
9442
|
+
...usage,
|
|
9443
|
+
total_spend: 0,
|
|
9444
|
+
budget_hidden: true,
|
|
9445
|
+
members: usage.members.map((m) => ({ ...m, spend: 0 }))
|
|
9446
|
+
};
|
|
9447
|
+
}
|
|
9448
|
+
|
|
9300
9449
|
// src/routes/middleware/withUser.ts
|
|
9301
9450
|
function createRequireUser(ctx) {
|
|
9302
9451
|
return async (req, res, next) => {
|
|
@@ -9317,7 +9466,7 @@ function createRequireUser(ctx) {
|
|
|
9317
9466
|
async function getProvisionedUser(ctx, res) {
|
|
9318
9467
|
const tokenEntityRef = res.locals.tokenEntityRef;
|
|
9319
9468
|
const userId = res.locals.userId;
|
|
9320
|
-
|
|
9469
|
+
const userInfo = await getOrProvisionUser(
|
|
9321
9470
|
ctx.client,
|
|
9322
9471
|
tokenEntityRef,
|
|
9323
9472
|
userId,
|
|
@@ -9328,6 +9477,8 @@ async function getProvisionedUser(ctx, res) {
|
|
|
9328
9477
|
ctx.auth,
|
|
9329
9478
|
ctx.logger
|
|
9330
9479
|
);
|
|
9480
|
+
res.locals.userId = effectiveUserId(userId);
|
|
9481
|
+
return userInfo;
|
|
9331
9482
|
}
|
|
9332
9483
|
|
|
9333
9484
|
// src/services/userInfoCache.ts
|
|
@@ -9536,6 +9687,7 @@ var openApiSpec = {
|
|
|
9536
9687
|
team_id: { type: "string" },
|
|
9537
9688
|
duration: { type: "string" },
|
|
9538
9689
|
max_budget: { type: "number", nullable: true, description: "Positive number caps spend; null = unlimited" },
|
|
9690
|
+
budget_duration: { type: "string", description: 'How often max_budget resets (e.g. "30d", "1mo"); one of litellm.keys.allowedBudgetDurations' },
|
|
9539
9691
|
tpm_limit: { type: "number" },
|
|
9540
9692
|
rpm_limit: { type: "number" }
|
|
9541
9693
|
} } } }
|
|
@@ -9632,6 +9784,24 @@ var openApiSpec = {
|
|
|
9632
9784
|
responses: { "200": { description: "Team usage" }, "400": { description: "Missing date range" } }
|
|
9633
9785
|
}
|
|
9634
9786
|
},
|
|
9787
|
+
"/teams/{teamId}/usage/members": {
|
|
9788
|
+
get: {
|
|
9789
|
+
tags: ["Teams"],
|
|
9790
|
+
summary: "Per-member usage breakdown for a team",
|
|
9791
|
+
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.",
|
|
9792
|
+
parameters: [
|
|
9793
|
+
{ name: "teamId", in: "path", required: true, schema: { type: "string" } },
|
|
9794
|
+
{ name: "start_date", in: "query", required: true, schema: { type: "string" } },
|
|
9795
|
+
{ name: "end_date", in: "query", required: true, schema: { type: "string" } }
|
|
9796
|
+
],
|
|
9797
|
+
responses: {
|
|
9798
|
+
"200": { description: "One row per member, sorted by spend" },
|
|
9799
|
+
"400": { description: "Missing date range" },
|
|
9800
|
+
"403": { description: "Caller may see the team but not its member breakdown" },
|
|
9801
|
+
"404": { description: "Feature disabled, or team not found / not visible to the caller" }
|
|
9802
|
+
}
|
|
9803
|
+
}
|
|
9804
|
+
},
|
|
9635
9805
|
"/usage": {
|
|
9636
9806
|
get: {
|
|
9637
9807
|
tags: ["Usage"],
|
|
@@ -9737,6 +9907,9 @@ function registerConfigRoutes(router, ctx) {
|
|
|
9737
9907
|
readOnly: ctx.teamAdminCfg.readOnly,
|
|
9738
9908
|
memberManagerRoles: ctx.teamAdminCfg.memberManagerRoles
|
|
9739
9909
|
},
|
|
9910
|
+
teamUsage: {
|
|
9911
|
+
memberBreakdown: ctx.memberBreakdownCfg.enabled
|
|
9912
|
+
},
|
|
9740
9913
|
display: {
|
|
9741
9914
|
hideTeamBudgetForMembers: ctx.teamBudgetVisibility.hideTeamBudgetForMembers,
|
|
9742
9915
|
hideTeamBudgetForManagers: ctx.teamBudgetVisibility.hideTeamBudgetForManagers
|
|
@@ -9908,6 +10081,9 @@ async function createKeyForUser(user, input, ctx) {
|
|
|
9908
10081
|
if (input.max_budget !== void 0) {
|
|
9909
10082
|
upstreamRequest.max_budget = input.max_budget ?? void 0;
|
|
9910
10083
|
}
|
|
10084
|
+
if (input.budget_duration !== void 0) {
|
|
10085
|
+
upstreamRequest.budget_duration = input.budget_duration;
|
|
10086
|
+
}
|
|
9911
10087
|
if (input.tpm_limit !== void 0) {
|
|
9912
10088
|
upstreamRequest.tpm_limit = input.tpm_limit;
|
|
9913
10089
|
}
|
|
@@ -9966,8 +10142,8 @@ function registerKeysRoutes(router, ctx) {
|
|
|
9966
10142
|
});
|
|
9967
10143
|
router.get("/keys", async (_req, res) => {
|
|
9968
10144
|
try {
|
|
9969
|
-
const userId = res.locals.userId;
|
|
9970
10145
|
await getProvisionedUser(ctx, res);
|
|
10146
|
+
const userId = res.locals.userId;
|
|
9971
10147
|
const keys = await client.listKeys(userId);
|
|
9972
10148
|
res.json(keys);
|
|
9973
10149
|
} catch (error) {
|
|
@@ -9984,8 +10160,8 @@ function registerKeysRoutes(router, ctx) {
|
|
|
9984
10160
|
sendPermissionDenied(res, import_backstage_plugin_litellm_common3.litellmKeyRevokePermission);
|
|
9985
10161
|
return;
|
|
9986
10162
|
}
|
|
9987
|
-
const userId = res.locals.userId;
|
|
9988
10163
|
await getProvisionedUser(ctx, res);
|
|
10164
|
+
const userId = res.locals.userId;
|
|
9989
10165
|
const keys = await client.listKeys(userId);
|
|
9990
10166
|
const now = Date.now();
|
|
9991
10167
|
const expiredKeys = keys.filter((k) => {
|
|
@@ -10378,11 +10554,11 @@ function registerOpencodeRoutes(router, ctx) {
|
|
|
10378
10554
|
sendPermissionDenied(res, import_backstage_plugin_litellm_common4.litellmKeyCreatePermission);
|
|
10379
10555
|
return null;
|
|
10380
10556
|
}
|
|
10381
|
-
const
|
|
10557
|
+
const computedUserId = toLiteLLMUserId(tokenEntityRef, userIdDomain);
|
|
10382
10558
|
const userInfo = await getOrProvisionUser(
|
|
10383
10559
|
client,
|
|
10384
10560
|
tokenEntityRef,
|
|
10385
|
-
|
|
10561
|
+
computedUserId,
|
|
10386
10562
|
provisioningEnabled,
|
|
10387
10563
|
provisioningDefaults,
|
|
10388
10564
|
roleConfigs,
|
|
@@ -10404,7 +10580,7 @@ function registerOpencodeRoutes(router, ctx) {
|
|
|
10404
10580
|
res.status(400).json({ error: "A team is required for this connection" });
|
|
10405
10581
|
return null;
|
|
10406
10582
|
}
|
|
10407
|
-
return { tokenEntityRef, userId, userInfo, teamId, redirectUri };
|
|
10583
|
+
return { tokenEntityRef, userId: effectiveUserId(computedUserId), userInfo, teamId, redirectUri };
|
|
10408
10584
|
};
|
|
10409
10585
|
router.get("/opencode/connect", async (req, res) => {
|
|
10410
10586
|
try {
|
|
@@ -10472,6 +10648,7 @@ function registerOpencodeRoutes(router, ctx) {
|
|
|
10472
10648
|
alias,
|
|
10473
10649
|
duration: opencodeCfg.keyDuration,
|
|
10474
10650
|
max_budget: opencodeCfg.maxBudget,
|
|
10651
|
+
...opencodeCfg.budgetDuration && { budget_duration: opencodeCfg.budgetDuration },
|
|
10475
10652
|
team_id: teamId,
|
|
10476
10653
|
metadata: {
|
|
10477
10654
|
...opencodeCfg.metadata,
|
|
@@ -10506,14 +10683,15 @@ function registerOpencodeRoutes(router, ctx) {
|
|
|
10506
10683
|
res.status(502).json({ error: "LiteLLM returned no key material" });
|
|
10507
10684
|
return;
|
|
10508
10685
|
}
|
|
10686
|
+
logger.info("opencode.connect", { userId, team: teamId ?? null, rotated });
|
|
10687
|
+
if (req.is("application/json")) {
|
|
10688
|
+
res.set("Cache-Control", "no-store");
|
|
10689
|
+
res.json({ key, team: teamId ?? null });
|
|
10690
|
+
return;
|
|
10691
|
+
}
|
|
10509
10692
|
const url = new URL(redirectUri);
|
|
10510
10693
|
url.searchParams.set("key", key);
|
|
10511
10694
|
if (teamId) url.searchParams.set("team", teamId);
|
|
10512
|
-
logger.info("opencode.connect", {
|
|
10513
|
-
userId,
|
|
10514
|
-
team: teamId ?? null,
|
|
10515
|
-
rotated
|
|
10516
|
-
});
|
|
10517
10695
|
res.redirect(302, url.href);
|
|
10518
10696
|
} catch (error) {
|
|
10519
10697
|
if (error instanceof ProvisioningError) {
|
|
@@ -10866,12 +11044,12 @@ function registerTeamsRoutes(router, ctx) {
|
|
|
10866
11044
|
});
|
|
10867
11045
|
return;
|
|
10868
11046
|
}
|
|
10869
|
-
const
|
|
11047
|
+
const computedUserId = toLiteLLMUserId(userEntityRef, userIdDomain);
|
|
10870
11048
|
try {
|
|
10871
11049
|
await getOrProvisionUser(
|
|
10872
11050
|
client,
|
|
10873
11051
|
userEntityRef,
|
|
10874
|
-
|
|
11052
|
+
computedUserId,
|
|
10875
11053
|
provisioningEnabled,
|
|
10876
11054
|
provisioningDefaults,
|
|
10877
11055
|
roleConfigs,
|
|
@@ -10886,6 +11064,7 @@ function registerTeamsRoutes(router, ctx) {
|
|
|
10886
11064
|
}
|
|
10887
11065
|
throw err;
|
|
10888
11066
|
}
|
|
11067
|
+
const litellmUserId = effectiveUserId(computedUserId);
|
|
10889
11068
|
try {
|
|
10890
11069
|
await client.teamMemberAdd({
|
|
10891
11070
|
team_id: teamId,
|
|
@@ -10925,9 +11104,9 @@ function registerTeamsRoutes(router, ctx) {
|
|
|
10925
11104
|
res.status(400).json({ error: "userEntityRef query parameter is required" });
|
|
10926
11105
|
return;
|
|
10927
11106
|
}
|
|
10928
|
-
const litellmUserId =
|
|
11107
|
+
const litellmUserId = await ctx.resolveMemberUserId(userEntityRef);
|
|
10929
11108
|
if (via === "teamRole") {
|
|
10930
|
-
if (litellmUserId ===
|
|
11109
|
+
if (litellmUserId === await ctx.resolveMemberUserId(actor)) {
|
|
10931
11110
|
res.status(400).json({ error: "You cannot remove yourself from the team" });
|
|
10932
11111
|
return;
|
|
10933
11112
|
}
|
|
@@ -11145,6 +11324,7 @@ function registerObjectPermissionsRoutes(router, ctx) {
|
|
|
11145
11324
|
}
|
|
11146
11325
|
|
|
11147
11326
|
// src/routes/teamUsage.ts
|
|
11327
|
+
var import_backstage_plugin_litellm_common7 = require("@acarmisc/backstage-plugin-litellm-common");
|
|
11148
11328
|
function registerTeamUsageRoutes(router, ctx) {
|
|
11149
11329
|
const {
|
|
11150
11330
|
client,
|
|
@@ -11207,6 +11387,95 @@ function registerTeamUsageRoutes(router, ctx) {
|
|
|
11207
11387
|
sendError(res, error, logger, "fetch team usage");
|
|
11208
11388
|
}
|
|
11209
11389
|
});
|
|
11390
|
+
router.get("/teams/:teamId/usage/members", requireUser, async (req, res) => {
|
|
11391
|
+
const cfg = ctx.memberBreakdownCfg;
|
|
11392
|
+
if (!cfg.enabled) {
|
|
11393
|
+
res.status(404).json({ error: "Not found" });
|
|
11394
|
+
return;
|
|
11395
|
+
}
|
|
11396
|
+
try {
|
|
11397
|
+
const { teamId } = req.params;
|
|
11398
|
+
const { start_date, end_date } = req.query;
|
|
11399
|
+
if (!start_date || !end_date) {
|
|
11400
|
+
res.status(400).json({ error: "start_date and end_date are required" });
|
|
11401
|
+
return;
|
|
11402
|
+
}
|
|
11403
|
+
const tokenEntityRef = res.locals.tokenEntityRef;
|
|
11404
|
+
const userInfo = await getProvisionedUser(ctx, res);
|
|
11405
|
+
const callerId = res.locals.userId;
|
|
11406
|
+
const isMember = userInfo?.teams?.includes(teamId) ?? false;
|
|
11407
|
+
let team;
|
|
11408
|
+
try {
|
|
11409
|
+
team = await withTeamFetchRetry(() => client.getTeamInfo(teamId));
|
|
11410
|
+
} catch (err) {
|
|
11411
|
+
if (err instanceof LiteLLMUpstreamError && err.status === 404) {
|
|
11412
|
+
res.status(404).json({ error: "Team not found" });
|
|
11413
|
+
return;
|
|
11414
|
+
}
|
|
11415
|
+
throw err;
|
|
11416
|
+
}
|
|
11417
|
+
const owningGroup = typeof team.metadata?.owning_group === "string" ? team.metadata.owning_group : void 0;
|
|
11418
|
+
const isManager = ctx.teamMgmtEnabled && !!owningGroup && await isUserMemberOfGroup(tokenEntityRef, owningGroup, catalogClient, auth, logger);
|
|
11419
|
+
if (!isMember && !isManager) {
|
|
11420
|
+
res.status(404).json({ error: "Team not found" });
|
|
11421
|
+
return;
|
|
11422
|
+
}
|
|
11423
|
+
const role = team.members_with_roles?.find((m) => m.user_id === callerId)?.role;
|
|
11424
|
+
const viaRole = isMember && !!role && cfg.viewerRoles.includes(role);
|
|
11425
|
+
const allowed = isManager || viaRole || isMember && cfg.permissionEnabled && await ctx.assertPermission(req, import_backstage_plugin_litellm_common7.litellmTeamUsageReadPermission);
|
|
11426
|
+
if (!allowed) {
|
|
11427
|
+
res.status(403).json({
|
|
11428
|
+
error: "Access denied: viewing per-member usage needs a team role or permission"
|
|
11429
|
+
});
|
|
11430
|
+
return;
|
|
11431
|
+
}
|
|
11432
|
+
const [usage, keys] = await Promise.all([
|
|
11433
|
+
client.getTeamUsage(teamId, start_date, end_date),
|
|
11434
|
+
client.listTeamKeys(teamId)
|
|
11435
|
+
]);
|
|
11436
|
+
const profiles = await lookupProfiles(memberEmails(team, keys));
|
|
11437
|
+
const breakdown = buildTeamMemberUsage(teamId, usage, keys, team, profiles);
|
|
11438
|
+
let via = "permission";
|
|
11439
|
+
if (isManager) via = "manager";
|
|
11440
|
+
else if (viaRole) via = "teamRole";
|
|
11441
|
+
logger.info("team.usage.members.read", { actor: tokenEntityRef, teamId, via });
|
|
11442
|
+
const hide = isManager ? teamBudgetVisibility.hideTeamBudgetForManagers : teamBudgetVisibility.hideTeamBudgetForMembers;
|
|
11443
|
+
res.json(hide ? redactTeamMemberUsage(breakdown) : breakdown);
|
|
11444
|
+
} catch (error) {
|
|
11445
|
+
if (error instanceof ProvisioningError) {
|
|
11446
|
+
res.status(error.status).json(error.body);
|
|
11447
|
+
return;
|
|
11448
|
+
}
|
|
11449
|
+
sendError(res, error, logger, "fetch team member usage");
|
|
11450
|
+
}
|
|
11451
|
+
});
|
|
11452
|
+
async function lookupProfiles(emails) {
|
|
11453
|
+
const profiles = /* @__PURE__ */ new Map();
|
|
11454
|
+
if (!emails.length) return profiles;
|
|
11455
|
+
try {
|
|
11456
|
+
const { token } = await auth.getPluginRequestToken({
|
|
11457
|
+
onBehalfOf: await auth.getOwnServiceCredentials(),
|
|
11458
|
+
targetPluginId: "catalog"
|
|
11459
|
+
});
|
|
11460
|
+
const { items } = await catalogClient.getEntities(
|
|
11461
|
+
{
|
|
11462
|
+
filter: { kind: "User", "spec.profile.email": emails },
|
|
11463
|
+
fields: ["spec.profile"]
|
|
11464
|
+
},
|
|
11465
|
+
{ token }
|
|
11466
|
+
);
|
|
11467
|
+
for (const entity of items) {
|
|
11468
|
+
const profile = entity.spec?.profile ?? {};
|
|
11469
|
+
if (typeof profile.email === "string") {
|
|
11470
|
+
profiles.set(profile.email.toLowerCase(), { displayName: profile.displayName });
|
|
11471
|
+
}
|
|
11472
|
+
}
|
|
11473
|
+
} catch (err) {
|
|
11474
|
+
const message2 = err instanceof Error ? err.message : String(err);
|
|
11475
|
+
logger.warn(`Catalog lookup for team member names failed: ${message2}`);
|
|
11476
|
+
}
|
|
11477
|
+
return profiles;
|
|
11478
|
+
}
|
|
11210
11479
|
}
|
|
11211
11480
|
|
|
11212
11481
|
// node_modules/jose/dist/node/esm/runtime/base64url.js
|
|
@@ -12722,8 +12991,8 @@ async function bridgeListKeys(client, claims, provisioningEnabled, provisioningD
|
|
|
12722
12991
|
}
|
|
12723
12992
|
|
|
12724
12993
|
// src/routes/bridge.ts
|
|
12725
|
-
var import_backstage_plugin_litellm_common7 = require("@acarmisc/backstage-plugin-litellm-common");
|
|
12726
12994
|
var import_backstage_plugin_litellm_common8 = require("@acarmisc/backstage-plugin-litellm-common");
|
|
12995
|
+
var import_backstage_plugin_litellm_common9 = require("@acarmisc/backstage-plugin-litellm-common");
|
|
12727
12996
|
function registerBridgeRoutes(router, ctx, bridgeOpts) {
|
|
12728
12997
|
const {
|
|
12729
12998
|
client,
|
|
@@ -12736,7 +13005,7 @@ function registerBridgeRoutes(router, ctx, bridgeOpts) {
|
|
|
12736
13005
|
keyValidationConfig
|
|
12737
13006
|
} = ctx;
|
|
12738
13007
|
const { config } = bridgeOpts;
|
|
12739
|
-
const generateKeyInputSchema = (0,
|
|
13008
|
+
const generateKeyInputSchema = (0, import_backstage_plugin_litellm_common9.createGenerateKeyInputSchema)(ctx.keyValidationConfig);
|
|
12740
13009
|
const bridgeCfg = readBridgeConfig(config);
|
|
12741
13010
|
const bridgeIdentity = {
|
|
12742
13011
|
userIdDomain,
|
|
@@ -12897,11 +13166,11 @@ function registerBridgeRoutes(router, ctx, bridgeOpts) {
|
|
|
12897
13166
|
}
|
|
12898
13167
|
const team = await client.getTeamInfo(teamId);
|
|
12899
13168
|
const allowed = team.models;
|
|
12900
|
-
if (!allowed || allowed.length === 0 || allowed.includes(
|
|
13169
|
+
if (!allowed || allowed.length === 0 || allowed.includes(import_backstage_plugin_litellm_common8.ALL_PROXY_MODELS)) {
|
|
12901
13170
|
res.json(models);
|
|
12902
13171
|
return;
|
|
12903
13172
|
}
|
|
12904
|
-
res.json(models.filter((m) => (0,
|
|
13173
|
+
res.json(models.filter((m) => (0, import_backstage_plugin_litellm_common8.isModelAllowed)(m, allowed)));
|
|
12905
13174
|
} catch (error) {
|
|
12906
13175
|
handleBridgeError(error, res);
|
|
12907
13176
|
}
|
|
@@ -12933,18 +13202,20 @@ async function createRouter(options) {
|
|
|
12933
13202
|
const keyMaxBudget = config.getOptionalNumber("litellm.keys.maxBudget") ?? 100;
|
|
12934
13203
|
const keyMaxTpm = config.getOptionalNumber("litellm.keys.maxTpm") ?? 1e5;
|
|
12935
13204
|
const keyMaxRpm = config.getOptionalNumber("litellm.keys.maxRpm") ?? 1e3;
|
|
12936
|
-
const keyAllowedDurations = config.getOptionalStringArray("litellm.keys.allowedDurations") ??
|
|
13205
|
+
const keyAllowedDurations = config.getOptionalStringArray("litellm.keys.allowedDurations") ?? import_backstage_plugin_litellm_common10.DEFAULT_KEY_DURATIONS;
|
|
12937
13206
|
const keyValidationConfig = {
|
|
12938
13207
|
maxBudget: keyMaxBudget,
|
|
12939
13208
|
maxTpm: keyMaxTpm,
|
|
12940
13209
|
maxRpm: keyMaxRpm,
|
|
12941
|
-
allowedDurations: keyAllowedDurations
|
|
13210
|
+
allowedDurations: keyAllowedDurations,
|
|
13211
|
+
allowedBudgetDurations: config.getOptionalStringArray("litellm.keys.allowedBudgetDurations") ?? import_backstage_plugin_litellm_common10.DEFAULT_KEY_BUDGET_DURATIONS
|
|
12942
13212
|
};
|
|
12943
13213
|
const teamMgmtEnabled = isTeamManagementEnabled(config);
|
|
12944
13214
|
const objectPermsEnabled = isObjectPermissionsEnabled(config);
|
|
12945
13215
|
const teamAdminCfg = readTeamAdminConfig(config);
|
|
12946
13216
|
const teamBudgetVisibility = readTeamBudgetVisibility(config);
|
|
12947
13217
|
const opencodeCfg = readOpencodeConfig(config);
|
|
13218
|
+
const memberBreakdownCfg = readMemberBreakdownConfig(config);
|
|
12948
13219
|
const catalogClient = options.catalogClient ?? new import_catalog_client.CatalogClient({ discoveryApi: discovery });
|
|
12949
13220
|
if (provisioningEnabled) {
|
|
12950
13221
|
logger.info(
|
|
@@ -12961,7 +13232,13 @@ async function createRouter(options) {
|
|
|
12961
13232
|
`LiteLLM team budget hiding enabled \u2014 members: ${teamBudgetVisibility.hideTeamBudgetForMembers}, managers: ${teamBudgetVisibility.hideTeamBudgetForManagers}`
|
|
12962
13233
|
);
|
|
12963
13234
|
}
|
|
13235
|
+
const resolveMemberUserId = async (userEntityRef) => {
|
|
13236
|
+
const computed = toLiteLLMUserId(userEntityRef, userIdDomain);
|
|
13237
|
+
await findLiteLLMUser(client, userEntityRef, computed, catalogClient, auth, logger);
|
|
13238
|
+
return effectiveUserId(computed);
|
|
13239
|
+
};
|
|
12964
13240
|
const ctx = {
|
|
13241
|
+
resolveMemberUserId,
|
|
12965
13242
|
client,
|
|
12966
13243
|
catalogClient,
|
|
12967
13244
|
auth,
|
|
@@ -12984,13 +13261,14 @@ async function createRouter(options) {
|
|
|
12984
13261
|
teamAdminCfg,
|
|
12985
13262
|
teamBudgetVisibility,
|
|
12986
13263
|
opencodeCfg,
|
|
13264
|
+
memberBreakdownCfg,
|
|
12987
13265
|
// Helper functions - defined inline below this object
|
|
12988
13266
|
authorizeKeyAction: async (req, keyId) => {
|
|
12989
13267
|
if (!req.res || !req.res.locals.tokenEntityRef || !req.res.locals.userId) {
|
|
12990
13268
|
throw new import_errors21.NotAllowedError("User identity not found in request");
|
|
12991
13269
|
}
|
|
12992
13270
|
const tokenEntityRef = req.res.locals.tokenEntityRef;
|
|
12993
|
-
const userId =
|
|
13271
|
+
const userId = await resolveMemberUserId(tokenEntityRef);
|
|
12994
13272
|
const ownKeys = await client.listKeys(userId);
|
|
12995
13273
|
const key = ownKeys.find((k) => (k.token ?? k.key) === keyId);
|
|
12996
13274
|
if (!key) {
|
|
@@ -13106,7 +13384,7 @@ async function createRouter(options) {
|
|
|
13106
13384
|
}
|
|
13107
13385
|
if (teamRoleEligible) {
|
|
13108
13386
|
const actor = await resolveUserId(req, auth);
|
|
13109
|
-
const litellmUserId = actor &&
|
|
13387
|
+
const litellmUserId = actor && await resolveMemberUserId(actor);
|
|
13110
13388
|
const role = existing.members_with_roles?.find(
|
|
13111
13389
|
(m) => m.user_id === litellmUserId
|
|
13112
13390
|
)?.role;
|
|
@@ -13140,7 +13418,7 @@ async function createRouter(options) {
|
|
|
13140
13418
|
}
|
|
13141
13419
|
|
|
13142
13420
|
// src/plugin.ts
|
|
13143
|
-
var
|
|
13421
|
+
var import_backstage_plugin_litellm_common11 = require("@acarmisc/backstage-plugin-litellm-common");
|
|
13144
13422
|
var BRIDGE_UNAUTHENTICATED_PATHS = [
|
|
13145
13423
|
"/bridge/health",
|
|
13146
13424
|
"/bridge/keys",
|
|
@@ -13169,7 +13447,7 @@ var litellmPlugin = (0, import_backend_plugin_api.createBackendPlugin)({
|
|
|
13169
13447
|
permissions,
|
|
13170
13448
|
permissionsRegistry
|
|
13171
13449
|
}) {
|
|
13172
|
-
permissionsRegistry.addPermissions(
|
|
13450
|
+
permissionsRegistry.addPermissions(import_backstage_plugin_litellm_common11.litellmPermissions);
|
|
13173
13451
|
const router = await createRouter({
|
|
13174
13452
|
config,
|
|
13175
13453
|
logger,
|
|
@@ -13189,7 +13467,7 @@ var litellmPlugin = (0, import_backend_plugin_api.createBackendPlugin)({
|
|
|
13189
13467
|
});
|
|
13190
13468
|
|
|
13191
13469
|
// src/index.ts
|
|
13192
|
-
var
|
|
13470
|
+
var import_backstage_plugin_litellm_common12 = require("@acarmisc/backstage-plugin-litellm-common");
|
|
13193
13471
|
// Annotate the CommonJS export names for ESM import in node:
|
|
13194
13472
|
0 && (module.exports = {
|
|
13195
13473
|
KeycloakJWTVerifier,
|