@acarmisc/backstage-plugin-litellm-backend 0.26.1 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs.js +125 -42
- package/dist/index.cjs.js.map +2 -2
- package/dist/routes/teamUsage.js +104 -54
- package/dist/teamMemberUsage.d.ts +9 -1
- package/dist/teamMemberUsage.js +50 -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
|
@@ -9530,6 +9530,47 @@ function redactTeamMemberUsage(usage) {
|
|
|
9530
9530
|
members: usage.members.map((m) => ({ ...m, spend: 0 }))
|
|
9531
9531
|
};
|
|
9532
9532
|
}
|
|
9533
|
+
function buildTeamKeyUsage(teamId, usage, keys, team, profiles = /* @__PURE__ */ new Map()) {
|
|
9534
|
+
const emailById = /* @__PURE__ */ new Map();
|
|
9535
|
+
for (const m of team.members_with_roles ?? []) {
|
|
9536
|
+
if (m.user_id && m.user_email) emailById.set(m.user_id, m.user_email);
|
|
9537
|
+
}
|
|
9538
|
+
const rows = keys.map((k) => {
|
|
9539
|
+
const u = k.token && usage.usage_by_key?.[k.token] || void 0;
|
|
9540
|
+
const userId = k.user_id ?? null;
|
|
9541
|
+
const email = userId && emailById.get(userId) || (userId?.includes("@") ? userId : void 0);
|
|
9542
|
+
const displayName = email && profiles.get(email.toLowerCase())?.displayName;
|
|
9543
|
+
return {
|
|
9544
|
+
key: k.key,
|
|
9545
|
+
...k.key_alias && { key_alias: k.key_alias },
|
|
9546
|
+
user_id: userId,
|
|
9547
|
+
...email && { user_email: email },
|
|
9548
|
+
...displayName && { display_name: displayName },
|
|
9549
|
+
created_at: k.created_at,
|
|
9550
|
+
...k.expires_at && { expires_at: k.expires_at },
|
|
9551
|
+
blocked: k.blocked === true,
|
|
9552
|
+
...k.max_budget !== void 0 && { max_budget: k.max_budget },
|
|
9553
|
+
spend: u?.total_spend ?? 0,
|
|
9554
|
+
prompt_tokens: u?.prompt_tokens ?? 0,
|
|
9555
|
+
completion_tokens: u?.completion_tokens ?? 0,
|
|
9556
|
+
total_tokens: u?.total_tokens ?? 0,
|
|
9557
|
+
api_requests: u?.api_requests ?? 0,
|
|
9558
|
+
successful_requests: u?.successful_requests ?? 0,
|
|
9559
|
+
failed_requests: u?.failed_requests ?? 0
|
|
9560
|
+
};
|
|
9561
|
+
});
|
|
9562
|
+
rows.sort(
|
|
9563
|
+
(a, b) => b.spend - a.spend || b.total_tokens - a.total_tokens || a.key.localeCompare(b.key)
|
|
9564
|
+
);
|
|
9565
|
+
return { team_id: teamId, keys: rows };
|
|
9566
|
+
}
|
|
9567
|
+
function redactTeamKeyUsage(usage) {
|
|
9568
|
+
return {
|
|
9569
|
+
...usage,
|
|
9570
|
+
budget_hidden: true,
|
|
9571
|
+
keys: usage.keys.map(({ max_budget: _hidden, ...k }) => ({ ...k, spend: 0 }))
|
|
9572
|
+
};
|
|
9573
|
+
}
|
|
9533
9574
|
|
|
9534
9575
|
// src/langfuse.ts
|
|
9535
9576
|
function readLangfuseConfig(config) {
|
|
@@ -11533,60 +11574,77 @@ function registerTeamUsageRoutes(router, ctx) {
|
|
|
11533
11574
|
sendError(res, error, logger, "fetch team usage");
|
|
11534
11575
|
}
|
|
11535
11576
|
});
|
|
11536
|
-
|
|
11577
|
+
async function authorizeMemberView(req, res) {
|
|
11537
11578
|
const cfg = ctx.memberBreakdownCfg;
|
|
11538
11579
|
if (!cfg.enabled) {
|
|
11539
11580
|
res.status(404).json({ error: "Not found" });
|
|
11540
|
-
return;
|
|
11581
|
+
return null;
|
|
11582
|
+
}
|
|
11583
|
+
const { teamId } = req.params;
|
|
11584
|
+
const { start_date, end_date } = req.query;
|
|
11585
|
+
if (!start_date || !end_date) {
|
|
11586
|
+
res.status(400).json({ error: "start_date and end_date are required" });
|
|
11587
|
+
return null;
|
|
11541
11588
|
}
|
|
11589
|
+
const tokenEntityRef = res.locals.tokenEntityRef;
|
|
11590
|
+
const userInfo = await getProvisionedUser(ctx, res);
|
|
11591
|
+
const callerId = res.locals.userId;
|
|
11592
|
+
const isMember = userInfo?.teams?.includes(teamId) ?? false;
|
|
11593
|
+
let team;
|
|
11542
11594
|
try {
|
|
11543
|
-
|
|
11544
|
-
|
|
11545
|
-
if (
|
|
11546
|
-
res.status(400).json({ error: "start_date and end_date are required" });
|
|
11547
|
-
return;
|
|
11548
|
-
}
|
|
11549
|
-
const tokenEntityRef = res.locals.tokenEntityRef;
|
|
11550
|
-
const userInfo = await getProvisionedUser(ctx, res);
|
|
11551
|
-
const callerId = res.locals.userId;
|
|
11552
|
-
const isMember = userInfo?.teams?.includes(teamId) ?? false;
|
|
11553
|
-
let team;
|
|
11554
|
-
try {
|
|
11555
|
-
team = await withTeamFetchRetry(() => client.getTeamInfo(teamId));
|
|
11556
|
-
} catch (err) {
|
|
11557
|
-
if (err instanceof LiteLLMUpstreamError && err.status === 404) {
|
|
11558
|
-
res.status(404).json({ error: "Team not found" });
|
|
11559
|
-
return;
|
|
11560
|
-
}
|
|
11561
|
-
throw err;
|
|
11562
|
-
}
|
|
11563
|
-
const owningGroup = typeof team.metadata?.owning_group === "string" ? team.metadata.owning_group : void 0;
|
|
11564
|
-
const isManager = ctx.teamMgmtEnabled && !!owningGroup && await isUserMemberOfGroup(tokenEntityRef, owningGroup, catalogClient, auth, logger);
|
|
11565
|
-
if (!isMember && !isManager) {
|
|
11595
|
+
team = await withTeamFetchRetry(() => client.getTeamInfo(teamId));
|
|
11596
|
+
} catch (err) {
|
|
11597
|
+
if (err instanceof LiteLLMUpstreamError && err.status === 404) {
|
|
11566
11598
|
res.status(404).json({ error: "Team not found" });
|
|
11567
|
-
return;
|
|
11568
|
-
}
|
|
11569
|
-
const role = team.members_with_roles?.find((m) => m.user_id === callerId)?.role;
|
|
11570
|
-
const viaRole = isMember && !!role && cfg.viewerRoles.includes(role);
|
|
11571
|
-
const allowed = isManager || viaRole || isMember && cfg.permissionEnabled && await ctx.assertPermission(req, import_backstage_plugin_litellm_common8.litellmTeamUsageReadPermission);
|
|
11572
|
-
if (!allowed) {
|
|
11573
|
-
res.status(403).json({
|
|
11574
|
-
error: "Access denied: viewing per-member usage needs a team role or permission"
|
|
11575
|
-
});
|
|
11576
|
-
return;
|
|
11599
|
+
return null;
|
|
11577
11600
|
}
|
|
11601
|
+
throw err;
|
|
11602
|
+
}
|
|
11603
|
+
const owningGroup = typeof team.metadata?.owning_group === "string" ? team.metadata.owning_group : void 0;
|
|
11604
|
+
const isManager = ctx.teamMgmtEnabled && !!owningGroup && await isUserMemberOfGroup(tokenEntityRef, owningGroup, catalogClient, auth, logger);
|
|
11605
|
+
if (!isMember && !isManager) {
|
|
11606
|
+
res.status(404).json({ error: "Team not found" });
|
|
11607
|
+
return null;
|
|
11608
|
+
}
|
|
11609
|
+
const role = team.members_with_roles?.find((m) => m.user_id === callerId)?.role;
|
|
11610
|
+
const viaRole = isMember && !!role && cfg.viewerRoles.includes(role);
|
|
11611
|
+
const allowed = isManager || viaRole || isMember && cfg.permissionEnabled && await ctx.assertPermission(req, import_backstage_plugin_litellm_common8.litellmTeamUsageReadPermission);
|
|
11612
|
+
if (!allowed) {
|
|
11613
|
+
res.status(403).json({
|
|
11614
|
+
error: "Access denied: viewing per-member usage needs a team role or permission"
|
|
11615
|
+
});
|
|
11616
|
+
return null;
|
|
11617
|
+
}
|
|
11618
|
+
let via = "permission";
|
|
11619
|
+
if (isManager) via = "manager";
|
|
11620
|
+
else if (viaRole) via = "teamRole";
|
|
11621
|
+
return {
|
|
11622
|
+
teamId,
|
|
11623
|
+
team,
|
|
11624
|
+
startDate: start_date,
|
|
11625
|
+
endDate: end_date,
|
|
11626
|
+
isManager,
|
|
11627
|
+
via
|
|
11628
|
+
};
|
|
11629
|
+
}
|
|
11630
|
+
const hideFor = (isManager) => isManager ? teamBudgetVisibility.hideTeamBudgetForManagers : teamBudgetVisibility.hideTeamBudgetForMembers;
|
|
11631
|
+
router.get("/teams/:teamId/usage/members", requireUser, async (req, res) => {
|
|
11632
|
+
try {
|
|
11633
|
+
const gate = await authorizeMemberView(req, res);
|
|
11634
|
+
if (!gate) return;
|
|
11635
|
+
const { teamId, team, startDate, endDate, isManager, via } = gate;
|
|
11578
11636
|
const [usage, keys] = await Promise.all([
|
|
11579
|
-
client.getTeamUsage(teamId,
|
|
11637
|
+
client.getTeamUsage(teamId, startDate, endDate),
|
|
11580
11638
|
client.listTeamKeys(teamId)
|
|
11581
11639
|
]);
|
|
11582
11640
|
const profiles = await lookupProfiles(memberEmails(team, keys));
|
|
11583
11641
|
const breakdown = buildTeamMemberUsage(teamId, usage, keys, team, profiles);
|
|
11584
|
-
|
|
11585
|
-
|
|
11586
|
-
|
|
11587
|
-
|
|
11588
|
-
|
|
11589
|
-
res.json(
|
|
11642
|
+
logger.info("team.usage.members.read", {
|
|
11643
|
+
actor: res.locals.tokenEntityRef,
|
|
11644
|
+
teamId,
|
|
11645
|
+
via
|
|
11646
|
+
});
|
|
11647
|
+
res.json(hideFor(isManager) ? redactTeamMemberUsage(breakdown) : breakdown);
|
|
11590
11648
|
} catch (error) {
|
|
11591
11649
|
if (error instanceof ProvisioningError) {
|
|
11592
11650
|
res.status(error.status).json(error.body);
|
|
@@ -11595,6 +11653,31 @@ function registerTeamUsageRoutes(router, ctx) {
|
|
|
11595
11653
|
sendError(res, error, logger, "fetch team member usage");
|
|
11596
11654
|
}
|
|
11597
11655
|
});
|
|
11656
|
+
router.get("/teams/:teamId/usage/keys", requireUser, async (req, res) => {
|
|
11657
|
+
try {
|
|
11658
|
+
const gate = await authorizeMemberView(req, res);
|
|
11659
|
+
if (!gate) return;
|
|
11660
|
+
const { teamId, team, startDate, endDate, isManager, via } = gate;
|
|
11661
|
+
const [usage, keys] = await Promise.all([
|
|
11662
|
+
client.getTeamUsage(teamId, startDate, endDate),
|
|
11663
|
+
client.listTeamKeys(teamId)
|
|
11664
|
+
]);
|
|
11665
|
+
const profiles = await lookupProfiles(memberEmails(team, keys));
|
|
11666
|
+
const keyUsage = buildTeamKeyUsage(teamId, usage, keys, team, profiles);
|
|
11667
|
+
logger.info("team.usage.keys.read", {
|
|
11668
|
+
actor: res.locals.tokenEntityRef,
|
|
11669
|
+
teamId,
|
|
11670
|
+
via
|
|
11671
|
+
});
|
|
11672
|
+
res.json(hideFor(isManager) ? redactTeamKeyUsage(keyUsage) : keyUsage);
|
|
11673
|
+
} catch (error) {
|
|
11674
|
+
if (error instanceof ProvisioningError) {
|
|
11675
|
+
res.status(error.status).json(error.body);
|
|
11676
|
+
return;
|
|
11677
|
+
}
|
|
11678
|
+
sendError(res, error, logger, "fetch team key usage");
|
|
11679
|
+
}
|
|
11680
|
+
});
|
|
11598
11681
|
async function lookupProfiles(emails) {
|
|
11599
11682
|
const profiles = /* @__PURE__ */ new Map();
|
|
11600
11683
|
if (!emails.length) return profiles;
|