@acarmisc/backstage-plugin-litellm 0.34.0 → 0.35.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/api.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type FetchApi, type DiscoveryApi } from '@backstage/frontend-plugin-api';
2
- import { UserInfo, VirtualKey, ModelInfo, UsageMetrics, TeamInfo, GenerateKeyRequest, GenerateKeyResponse, UpdateKeyRequest, AuditLogsParams, PaginatedAuditLogs, LiteLlmConfig, VectorStoreInfo, McpServerInfo, CreateTeamRequest, CreateTeamResponse, UpdateTeamRequest } from './types';
2
+ import { UserInfo, VirtualKey, ModelInfo, UsageMetrics, TeamMemberUsage, TeamInfo, GenerateKeyRequest, GenerateKeyResponse, UpdateKeyRequest, AuditLogsParams, PaginatedAuditLogs, LiteLlmConfig, VectorStoreInfo, McpServerInfo, CreateTeamRequest, CreateTeamResponse, UpdateTeamRequest } from './types';
3
3
  export declare class ApiError extends Error {
4
4
  body: unknown;
5
5
  status: number;
@@ -38,6 +38,8 @@ export interface LiteLlmApiInterface {
38
38
  setTeamMcpServers(teamId: string, mcpServers: string[]): Promise<TeamInfo>;
39
39
  getUsage(startDate: string, endDate: string): Promise<UsageMetrics>;
40
40
  getTeamUsage(teamId: string, startDate: string, endDate: string): Promise<UsageMetrics>;
41
+ /** Per-member breakdown; rejects (403/404) when the caller may not see it. */
42
+ getTeamMemberUsage(teamId: string, startDate: string, endDate: string): Promise<TeamMemberUsage>;
41
43
  getAuditLogs(params: AuditLogsParams): Promise<PaginatedAuditLogs>;
42
44
  getConfig(): Promise<LiteLlmConfig>;
43
45
  createTeam(request: CreateTeamRequest): Promise<CreateTeamResponse>;
@@ -96,6 +98,7 @@ export declare class LiteLlmApi implements LiteLlmApiInterface {
96
98
  setTeamMcpServers(teamId: string, mcpServers: string[]): Promise<TeamInfo>;
97
99
  getUsage(startDate: string, endDate: string): Promise<UsageMetrics>;
98
100
  getTeamUsage(teamId: string, startDate: string, endDate: string): Promise<UsageMetrics>;
101
+ getTeamMemberUsage(teamId: string, startDate: string, endDate: string): Promise<TeamMemberUsage>;
99
102
  getConfig(): Promise<LiteLlmConfig>;
100
103
  createTeam(request: CreateTeamRequest): Promise<CreateTeamResponse>;
101
104
  updateTeam(teamId: string, request: UpdateTeamRequest): Promise<TeamInfo>;
@@ -1,10 +1,20 @@
1
1
  import { FC } from 'react';
2
- import { TeamInfo, UsageMetrics } from '../types';
2
+ import { TeamInfo, TeamMemberUsage, UsageMetrics } from '../types';
3
+ /**
4
+ * Per-member usage for the period, sorted by spend. Dollars are replaced by
5
+ * the share of team spend when the backend hides them.
6
+ */
7
+ export declare const MemberUsageTable: FC<{
8
+ usage: TeamMemberUsage;
9
+ }>;
3
10
  interface TeamUsageProps {
4
11
  teams: TeamInfo[];
5
12
  loading: boolean;
6
13
  getTeamUsage: (teamId: string) => UsageMetrics | null;
7
14
  getTeamUsageLoading: (teamId: string) => boolean;
15
+ /** Per-member breakdown for a team; null hides the section. */
16
+ getTeamMemberUsage?: (teamId: string) => TeamMemberUsage | null;
17
+ getTeamMemberUsageLoading?: (teamId: string) => boolean;
8
18
  canManage?: boolean;
9
19
  onEditTeam?: (team: TeamInfo) => void;
10
20
  /** When true (with `onCreateTeam`), a "Create Team" action renders in the section header. */
package/dist/index.cjs.js CHANGED
@@ -307,6 +307,12 @@ var init_api = __esm({
307
307
  end_date: endDate
308
308
  });
309
309
  }
310
+ async getTeamMemberUsage(teamId, startDate, endDate) {
311
+ return this.get(`/teams/${encodeURIComponent(teamId)}/usage/members`, {
312
+ start_date: startDate,
313
+ end_date: endDate
314
+ });
315
+ }
310
316
  async getConfig() {
311
317
  return this.get("/config");
312
318
  }
@@ -4986,7 +4992,7 @@ function budgetStatValue(hidden, budget) {
4986
4992
  if (budget > 0) return fmtUsd2(budget);
4987
4993
  return "Unlimited";
4988
4994
  }
4989
- var import_react10, import_Paper8, import_Box14, import_Typography12, import_Skeleton5, import_Divider3, import_Table4, import_TableBody4, import_TableCell4, import_TableHead4, import_TableRow4, import_TableContainer4, import_Collapse2, import_IconButton6, import_Button6, import_CircularProgress4, import_styles10, import_Tooltip5, import_icons_material7, import_recharts2, import_jsx_runtime15, fmtUsd2, TeamCard, TeamUsage;
4995
+ var import_react10, import_Paper8, import_Box14, import_Typography12, import_Skeleton5, import_Divider3, import_Table4, import_TableBody4, import_TableCell4, import_TableHead4, import_TableRow4, import_TableContainer4, import_Collapse2, import_IconButton6, import_Button6, import_CircularProgress4, import_styles10, import_Tooltip5, import_icons_material7, import_recharts2, import_jsx_runtime15, fmtUsd2, monoSx, MemberUsageTable, TeamCard, TeamUsage;
4990
4996
  var init_TeamUsage = __esm({
4991
4997
  "src/components/TeamUsage.tsx"() {
4992
4998
  "use strict";
@@ -5013,7 +5019,43 @@ var init_TeamUsage = __esm({
5013
5019
  init_ui();
5014
5020
  import_jsx_runtime15 = require("react/jsx-runtime");
5015
5021
  fmtUsd2 = (n) => `$${(n ?? 0).toFixed(2)}`;
5016
- TeamCard = ({ team, usage, usageLoading, canManage, onEditTeam, canManageMembers, onManageMembers, onExpand }) => {
5022
+ monoSx = { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 11 };
5023
+ MemberUsageTable = ({ usage }) => {
5024
+ const hidden = usage.budget_hidden === true;
5025
+ if (!usage.members.length) {
5026
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_Typography12.default, { variant: "body2", color: "text.secondary", children: "No members or activity in this period." });
5027
+ }
5028
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_TableContainer4.default, { component: import_Paper8.default, variant: "outlined", sx: { borderRadius: 1.5 }, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_Table4.default, { size: "small", sx: dataTableSx, "aria-label": "Usage by member", children: [
5029
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_TableHead4.default, { children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_TableRow4.default, { children: [
5030
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_TableCell4.default, { children: "Member" }),
5031
+ !hidden && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_TableCell4.default, { align: "right", children: "Spend" }),
5032
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_TableCell4.default, { align: "right", children: "Share" }),
5033
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_TableCell4.default, { align: "right", children: "Tokens" }),
5034
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_TableCell4.default, { align: "right", children: "Requests" }),
5035
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_TableCell4.default, { align: "right", children: "Success" }),
5036
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_TableCell4.default, { align: "right", children: "Keys" })
5037
+ ] }) }),
5038
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_TableBody4.default, { children: usage.members.map((m) => {
5039
+ const name = m.display_name ?? m.user_email ?? m.user_id;
5040
+ let sub = null;
5041
+ if (m.display_name) sub = m.user_email ?? m.user_id;
5042
+ else if (m.user_email && m.user_id !== m.user_email) sub = m.user_id;
5043
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_TableRow4.default, { children: [
5044
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_TableCell4.default, { children: m.user_id === null ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_Tooltip5.default, { title: "Activity of keys that were deleted or have no owner", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_Typography12.default, { variant: "body2", color: "text.secondary", sx: { fontStyle: "italic" }, children: "Unattributed" }) }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
5045
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_Typography12.default, { variant: "body2", children: name }),
5046
+ sub && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_Typography12.default, { variant: "caption", color: "text.secondary", sx: monoSx, children: sub })
5047
+ ] }) }),
5048
+ !hidden && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_TableCell4.default, { align: "right", children: fmtUsd2(m.spend) }),
5049
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_TableCell4.default, { align: "right", children: `${m.spend_share_pct.toFixed(m.spend_share_pct > 0 && m.spend_share_pct < 1 ? 1 : 0)}%` }),
5050
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_TableCell4.default, { align: "right", children: fmtCompact(m.total_tokens) }),
5051
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_TableCell4.default, { align: "right", children: m.api_requests.toLocaleString() }),
5052
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_TableCell4.default, { align: "right", children: m.success_rate === null ? "\u2014" : `${m.success_rate.toFixed(0)}%` }),
5053
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_TableCell4.default, { align: "right", children: m.user_id === null ? "\u2014" : m.key_count })
5054
+ ] }, m.user_id ?? "__unattributed");
5055
+ }) })
5056
+ ] }) });
5057
+ };
5058
+ TeamCard = ({ team, usage, usageLoading, memberUsage, memberUsageLoading, canManage, onEditTeam, canManageMembers, onManageMembers, onExpand }) => {
5017
5059
  const [expanded, setExpanded] = (0, import_react10.useState)(false);
5018
5060
  const chart = useChartTheme();
5019
5061
  const handleExpandChange = (newExpanded) => {
@@ -5206,6 +5248,10 @@ var init_TeamUsage = __esm({
5206
5248
  team.models?.length ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_Box14.default, { display: "flex", gap: 0.5, flexWrap: "wrap", children: team.models.map((m) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(TagChip, { label: m, title: m }, m)) }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_Typography12.default, { variant: "body2", color: "text.secondary", children: "All models allowed" })
5207
5249
  ] }),
5208
5250
  renderDailySpendSection(),
5251
+ (memberUsageLoading || memberUsage) && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_Box14.default, { mb: 2.5, children: [
5252
+ sectionLabel("Usage by member"),
5253
+ memberUsage ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(MemberUsageTable, { usage: memberUsage }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_Box14.default, { display: "flex", justifyContent: "center", py: 2, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_CircularProgress4.default, { size: 22 }) })
5254
+ ] }),
5209
5255
  /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_Box14.default, { children: [
5210
5256
  sectionLabel("Members"),
5211
5257
  team.members_with_roles?.length ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
@@ -5260,6 +5306,8 @@ var init_TeamUsage = __esm({
5260
5306
  loading,
5261
5307
  getTeamUsage,
5262
5308
  getTeamUsageLoading,
5309
+ getTeamMemberUsage,
5310
+ getTeamMemberUsageLoading,
5263
5311
  canManage,
5264
5312
  onEditTeam,
5265
5313
  canCreate,
@@ -5319,6 +5367,8 @@ var init_TeamUsage = __esm({
5319
5367
  team,
5320
5368
  usage: getTeamUsage(team.team_id),
5321
5369
  usageLoading: getTeamUsageLoading(team.team_id),
5370
+ memberUsage: getTeamMemberUsage?.(team.team_id) ?? null,
5371
+ memberUsageLoading: getTeamMemberUsageLoading?.(team.team_id) ?? false,
5322
5372
  canManage,
5323
5373
  onEditTeam,
5324
5374
  canManageMembers: canManageMembers?.(team),
@@ -6125,6 +6175,8 @@ var init_LiteLLMPage = __esm({
6125
6175
  const keyFormOpen = generateDialogOpen || !!keyToEdit;
6126
6176
  const [teamUsageCache, setTeamUsageCache] = (0, import_react13.useState)({});
6127
6177
  const [teamUsageLoading, setTeamUsageLoading] = (0, import_react13.useState)({});
6178
+ const [memberUsageCache, setMemberUsageCache] = (0, import_react13.useState)({});
6179
+ const [memberUsageLoading, setMemberUsageLoading] = (0, import_react13.useState)({});
6128
6180
  const { value: userInfo, loading: userLoading, error: userError } = (0, import_react_use2.useAsync)(
6129
6181
  () => api.getUserInfo(),
6130
6182
  [api]
@@ -6185,6 +6237,8 @@ var init_LiteLLMPage = __esm({
6185
6237
  }
6186
6238
  setTeamUsageCache({});
6187
6239
  setTeamUsageLoading({});
6240
+ setMemberUsageCache({});
6241
+ setMemberUsageLoading({});
6188
6242
  }, [setDateRange, setCurrentPreset]);
6189
6243
  const loadTeamUsage = (0, import_react13.useCallback)(async (teamId) => {
6190
6244
  if (teamUsageCache[teamId] !== void 0 || teamUsageLoading[teamId]) return;
@@ -6200,6 +6254,20 @@ var init_LiteLLMPage = __esm({
6200
6254
  setTeamUsageLoading((prev) => ({ ...prev, [teamId]: false }));
6201
6255
  }
6202
6256
  }, [api, dateRange, teamUsageCache, teamUsageLoading]);
6257
+ const loadMemberUsage = (0, import_react13.useCallback)(async (teamId) => {
6258
+ if (memberUsageCache[teamId] !== void 0 || memberUsageLoading[teamId]) return;
6259
+ setMemberUsageLoading((prev) => ({ ...prev, [teamId]: true }));
6260
+ try {
6261
+ const startDate = toLocalDay(dateRange.start);
6262
+ const endDate = toLocalDay(dateRange.end);
6263
+ const data = await api.getTeamMemberUsage(teamId, startDate, endDate);
6264
+ setMemberUsageCache((prev) => ({ ...prev, [teamId]: data }));
6265
+ } catch {
6266
+ setMemberUsageCache((prev) => ({ ...prev, [teamId]: null }));
6267
+ } finally {
6268
+ setMemberUsageLoading((prev) => ({ ...prev, [teamId]: false }));
6269
+ }
6270
+ }, [api, dateRange, memberUsageCache, memberUsageLoading]);
6203
6271
  const allowedModels = (0, import_react13.useMemo)(() => {
6204
6272
  if (!allModels?.length) return [];
6205
6273
  const userModels = userInfo?.models;
@@ -6494,6 +6562,8 @@ var init_LiteLLMPage = __esm({
6494
6562
  loading: teamsLoading,
6495
6563
  getTeamUsage: (teamId) => teamUsageCache[teamId] ?? null,
6496
6564
  getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false,
6565
+ getTeamMemberUsage: (teamId) => memberUsageCache[teamId] ?? null,
6566
+ getTeamMemberUsageLoading: (teamId) => memberUsageLoading[teamId] ?? false,
6497
6567
  canManage: teamMgmtEnabled && canManageTeam && !readOnly,
6498
6568
  onEditTeam: (t) => setManageTeam({ mode: "edit", team: t }),
6499
6569
  canCreate: teamMgmtEnabled && canCreateTeam && !readOnly,
@@ -6501,7 +6571,10 @@ var init_LiteLLMPage = __esm({
6501
6571
  canManageMembers: (t) => teamMgmtEnabled && canManageMembers && isTeamMemberManager(t, userInfo?.user_id, memberManagerRoles, readOnly),
6502
6572
  onManageMembers: (t) => setManageTeam({ mode: "members", team: t }),
6503
6573
  readOnly,
6504
- onTeamExpand: (team) => loadTeamUsage(team.team_id)
6574
+ onTeamExpand: (team) => {
6575
+ loadTeamUsage(team.team_id);
6576
+ if (liteLlmConfig?.teamUsage?.memberBreakdown) loadMemberUsage(team.team_id);
6577
+ }
6505
6578
  }
6506
6579
  );
6507
6580
  })(),