@acarmisc/backstage-plugin-litellm 0.33.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/README.md CHANGED
@@ -1,97 +1,86 @@
1
1
  # @acarmisc/backstage-plugin-litellm
2
2
 
3
- Frontend for the **Backstage LiteLLM Governance Plugin** — lets developers manage LiteLLM virtual API keys and monitor AI model usage directly from Backstage.
3
+ Frontend for the **Backstage LiteLLM Governance Plugin** — lets developers manage
4
+ their LiteLLM virtual keys and follow their AI usage and budgets from Backstage.
4
5
 
5
- Built with the [New Frontend System](https://backstage.io/docs/frontend-system/) (`@backstage/frontend-plugin-api`) and Material-UI.
6
+ Built for the [New Frontend System](https://backstage.io/docs/frontend-system/)
7
+ (`@backstage/frontend-plugin-api`). It needs the backend,
8
+ [`@acarmisc/backstage-plugin-litellm-backend`](https://www.npmjs.com/package/@acarmisc/backstage-plugin-litellm-backend),
9
+ which talks to LiteLLM and enforces every rule.
6
10
 
7
- Pair this package with the backend, [`@acarmisc/backstage-plugin-litellm-backend`](https://www.npmjs.com/package/@acarmisc/backstage-plugin-litellm-backend), which proxies requests to your LiteLLM instance and resolves Backstage identity.
8
-
9
- Full setup docs, configuration reference, and architecture notes live in the [project README](https://github.com/acarmisc/backstage-plugin-litellm-govai#readme).
10
-
11
- ## Screenshots
12
-
13
- | View | Preview |
14
- |------|---------|
15
- | Home widget — `LiteLLMHomeWidget` | ![Home widget](../../docs/screenshots/home-widget.png) |
16
- | Overview / Usage Analytics | ![Usage analytics](../../docs/screenshots/usage-analytics.png) |
17
- | Keys tab — virtual key inventory | ![Keys tab](../../docs/screenshots/keys-tab.png) |
18
- | Generate New Key dialog | ![Generate New Key dialog](../../docs/screenshots/generate-key-dialog.png) |
19
- | Models tab | ![Models tab](../../docs/screenshots/models-tab.png) |
20
- | Compact "Create Key" card | ![Create Key card](../../docs/screenshots/key-card.png) |
11
+ Full documentation — adoption guide, configuration, permissions, security model
12
+ and architecture — starts at the
13
+ [project README](https://github.com/acarmisc/backstage-plugin-litellm-govai#readme).
21
14
 
22
15
  ## Installation
23
16
 
24
- This plugin is designed to be used **within a Backstage monorepo**.
25
-
26
17
  ```bash
27
- yarn add --cwd packages/app @acarmisc/backstage-plugin-litellm
18
+ yarn --cwd packages/app add @acarmisc/backstage-plugin-litellm
28
19
  ```
29
20
 
30
- You'll also need the backend package installed and configured — see its [README](https://www.npmjs.com/package/@acarmisc/backstage-plugin-litellm-backend) for `app-config.yaml` setup.
31
-
32
- ## Usage
33
-
34
- ### Full page
35
-
36
- The `LiteLLMPage` component renders the four-tab governance view: **Overview** (usage KPIs + charts), **Keys** (virtual key inventory), **Teams** (team-scoped usage), and **Models** (proxy model catalog).
21
+ If your app discovers features from its dependencies (`app.packages: all`),
22
+ the plugin is picked up automatically. Otherwise register it:
37
23
 
38
24
  ```tsx
39
- import { litellmPlugin, LiteLLMPage } from '@acarmisc/backstage-plugin-litellm';
40
-
41
25
  // packages/app/src/App.tsx
42
- <Route path="/litellm" element={<LiteLLMPage />} />
43
- ```
44
-
45
- ![Usage Analytics full page](../../docs/screenshots/usage-analytics-overview.png)
26
+ import { createApp } from '@backstage/frontend-defaults';
27
+ import litellmPlugin from '@acarmisc/backstage-plugin-litellm';
46
28
 
47
- ### Home page widget
48
-
49
- `LiteLLMHomeWidget` is a compact card for the Backstage homepage. It shows the signed-in user's own usage — identity is resolved server-side from the Backstage token, so no `userId` prop is required. The header includes a `Today` / `7d` / `30d` period selector and a daily-spend sparkline.
50
-
51
- ```tsx
52
- import { LiteLLMHomeWidget } from '@acarmisc/backstage-plugin-litellm';
53
-
54
- <LiteLLMHomeWidget defaultPeriod="7d" />
29
+ const app = createApp({
30
+ features: [litellmPlugin],
31
+ });
55
32
  ```
56
33
 
57
- | Prop | Type | Default | Description |
58
- |------|------|---------|-------------|
59
- | `defaultPeriod` | `'today' \| '7d' \| '30d'` | `'7d'` | Period shown on first render |
60
- | `title` | `string` | `'LiteLLM Usage'` | Card title override |
34
+ This adds the `/litellm` page and the `liteLlmApiRef` API used by every
35
+ component below.
61
36
 
62
- A slimmer **Create Key** card variant is available for surfaces that only need a one-click shortcut into the key-mint flow:
37
+ ## The `/litellm` page
63
38
 
64
- ![Create Key card](../../docs/screenshots/key-card.png)
39
+ Tabs: **Overview** (usage KPIs and charts), **Keys** (your virtual keys: edit,
40
+ block / unblock, reset spend, delete), **Teams** (team usage, and team
41
+ management for admins), **Models** (the proxy's model catalogue) and **Audit
42
+ Log** (members of `litellm.audit.group` only). `?tab=<name>` opens a tab and
43
+ `?generate=1` opens the Generate New Key dialog.
65
44
 
66
- ### Keys tab
45
+ ![The /litellm page](https://raw.githubusercontent.com/acarmisc/backstage-plugin-litellm-govai/main/docs/images/overview.png)
67
46
 
68
- The **Keys** tab lists every virtual key you own with its alias, key ID, creation and expiry dates, budget bar (`$spent / $limit`), TPM/RPM, and model scope. Each row exposes inline actions: edit, block/unblock, reset spend, and delete.
47
+ ## Homepage components
69
48
 
70
- ![Keys tab](../../docs/screenshots/keys-tab.png)
49
+ | Component | What it shows |
50
+ |---|---|
51
+ | `LiteLLMHomeWidget` | Spend and tokens for `Today` / `7d` / `30d` with a daily sparkline |
52
+ | `LiteLLMBudgetWidget` | The budget hierarchy (key → personal → team → global) with a meter per limit |
53
+ | `LiteLLMBudgetGauges` | A condensed budget card: one ring per level and a month-to-date chart |
54
+ | `GenerateKeyButton` | The shared "Generate New Key" button (`onClick`, or `to="/litellm?generate=1"`) |
71
55
 
72
- ### Generate New Key dialog
73
-
74
- The dialog walks the user through aliasing the key, picking a duration, binding a team, restricting model access, setting a max budget, and configuring per-key TPM/RPM caps. Failures (e.g. duplicate alias) surface inline with the upstream error code preserved.
75
-
76
- ![Generate New Key dialog](../../docs/screenshots/generate-key-dialog.png)
56
+ ```tsx
57
+ import {
58
+ LiteLLMHomeWidget,
59
+ LiteLLMBudgetGauges,
60
+ } from '@acarmisc/backstage-plugin-litellm';
77
61
 
78
- ### Models tab
62
+ <LiteLLMHomeWidget defaultPeriod="7d" />
63
+ <LiteLLMBudgetGauges />
64
+ ```
79
65
 
80
- The **Models** tab lists every model the LiteLLM proxy exposes, with per-model input/output cost, max input/output token limits, and capabilities. The team selector scopes the list to the models the bound team is allowed to call.
66
+ Props for each component are documented in
67
+ [docs/frontend.md](https://github.com/acarmisc/backstage-plugin-litellm-govai/blob/main/docs/frontend.md).
81
68
 
82
- ![Models tab](../../docs/screenshots/models-tab.png)
69
+ ![LiteLLMBudgetGauges](https://raw.githubusercontent.com/acarmisc/backstage-plugin-litellm-govai/main/docs/images/budget-gauges.png)
83
70
 
84
- ## Exports
71
+ To try it without a backend, run `npm start` in this package: it serves the
72
+ plugin with mock data at `http://localhost:3000/litellm` (see
73
+ [CONTRIBUTING.md](https://github.com/acarmisc/backstage-plugin-litellm-govai/blob/main/CONTRIBUTING.md#run-the-frontend-with-mock-data)).
85
74
 
86
- - `litellmPlugin` — the frontend plugin instance
87
- - `LiteLLMPage` — main plugin page (keys, usage, team context)
88
- - `LiteLLMHomeWidget` — homepage usage summary card
89
- - `KeyFormDialog` — shared key form for both generating and editing a key (surfaces failures inline, warns on duplicate aliases, team-scoped model picker, unlimited-budget toggle when gated on)
90
- - `GenerateKeyDialog` — deprecated wrapper around `KeyFormDialog` with `mode="create"`
91
- - `DashboardHeader`, `KeysTable`, `UsageStats`, `TeamUsage` — individual page sections, exported for composition
92
- - `LiteLlmApi`, `liteLlmApiRef` — API client and its Backstage API ref
75
+ ## Other exports
93
76
 
94
- Note: `DashboardHeader` and `KeysTable` are composition pieces driven by `LiteLLMPage` and their props differ from older versions. `LiteLLMPage` is the supported entry point; composing the sections directly means keeping them in sync with the page's data wiring (keys, models, teams, and the `onGenerateKeyClick`/`onEditKey` callbacks).
77
+ - `litellmPlugin` (also the default export)
78
+ - `LiteLLMPage`, and the building blocks it uses: `DashboardHeader`,
79
+ `KeysTable`, `UsageStats`, `TeamUsage`, `KeyFormDialog`, `ManageTeamDialog`.
80
+ Prefer `LiteLLMPage`; composing the pieces yourself means wiring their data
81
+ and callbacks the way the page does.
82
+ - `LiteLlmApi`, `liteLlmApiRef`, the shared API types, and the
83
+ `litellmTeam*Permission` definitions.
95
84
 
96
85
  ## License
97
86
 
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>;
@@ -26,5 +26,3 @@ export interface KeyFormDialogProps {
26
26
  onGetConfig: () => Promise<LiteLlmConfig>;
27
27
  }
28
28
  export declare const KeyFormDialog: FC<KeyFormDialogProps>;
29
- export declare function fmtCost(perToken?: number): string | null;
30
- export declare function formatContextWindow(maxInput?: number, maxOutput?: number): string | null;
@@ -82,5 +82,4 @@ export interface LiteLLMBudgetGaugesProps {
82
82
  teams?: TeamInfo[];
83
83
  keys?: VirtualKey[];
84
84
  }
85
- export { monthToDateRange } from '../dates';
86
85
  export declare const LiteLLMBudgetGauges: FC<LiteLLMBudgetGaugesProps>;
@@ -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. */
@@ -1,5 +1,4 @@
1
1
  import { VirtualKey, ModelInfo, GenerateKeyRequest, UpdateKeyRequest } from '../types';
2
- export declare const generateDefaultAlias: (username?: string) => string;
3
2
  export declare const createForm: (username?: string) => GenerateKeyRequest;
4
3
  export declare const editForm: (k: VirtualKey) => UpdateKeyRequest;
5
4
  export declare function isModelAllowedByTeam(model: ModelInfo, teamModels?: string[]): boolean;
@@ -2,11 +2,6 @@
2
2
  * Pure helper: determine if the Clipboard API is available.
3
3
  */
4
4
  export declare function isClipboardApiAvailable(): boolean;
5
- /**
6
- * Pure helper: fallback copy via textarea and document.execCommand.
7
- * Returns true if successful, false otherwise.
8
- */
9
- export declare function fallbackCopyToClipboard(text: string): boolean;
10
5
  /**
11
6
  * Pure helper: create a timer for the copied state.
12
7
  * Returns the timer ID so the caller can manage cleanup.
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
  }
@@ -1843,8 +1849,6 @@ var init_KeysTable = __esm({
1843
1849
  setPruneSubmitting(true);
1844
1850
  try {
1845
1851
  await onPruneExpiredKeys();
1846
- } catch (error) {
1847
- console.error("Failed to prune expired keys:", error);
1848
1852
  } finally {
1849
1853
  setPruneSubmitting(false);
1850
1854
  setPruneConfirmCount(null);
@@ -1913,7 +1917,8 @@ var init_KeysTable = __esm({
1913
1917
  bgcolor: (0, import_styles7.alpha)(theme.palette.text.primary, theme.palette.mode === "dark" ? 0.08 : 0.05),
1914
1918
  px: 0.75,
1915
1919
  py: 0.375,
1916
- borderRadius: "5px"
1920
+ borderRadius: "5px",
1921
+ whiteSpace: "nowrap"
1917
1922
  }),
1918
1923
  children: shortKeyId(keyId)
1919
1924
  }
@@ -4987,7 +4992,7 @@ function budgetStatValue(hidden, budget) {
4987
4992
  if (budget > 0) return fmtUsd2(budget);
4988
4993
  return "Unlimited";
4989
4994
  }
4990
- 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;
4991
4996
  var init_TeamUsage = __esm({
4992
4997
  "src/components/TeamUsage.tsx"() {
4993
4998
  "use strict";
@@ -5014,7 +5019,43 @@ var init_TeamUsage = __esm({
5014
5019
  init_ui();
5015
5020
  import_jsx_runtime15 = require("react/jsx-runtime");
5016
5021
  fmtUsd2 = (n) => `$${(n ?? 0).toFixed(2)}`;
5017
- 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 }) => {
5018
5059
  const [expanded, setExpanded] = (0, import_react10.useState)(false);
5019
5060
  const chart = useChartTheme();
5020
5061
  const handleExpandChange = (newExpanded) => {
@@ -5207,6 +5248,10 @@ var init_TeamUsage = __esm({
5207
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" })
5208
5249
  ] }),
5209
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
+ ] }),
5210
5255
  /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_Box14.default, { children: [
5211
5256
  sectionLabel("Members"),
5212
5257
  team.members_with_roles?.length ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
@@ -5261,6 +5306,8 @@ var init_TeamUsage = __esm({
5261
5306
  loading,
5262
5307
  getTeamUsage,
5263
5308
  getTeamUsageLoading,
5309
+ getTeamMemberUsage,
5310
+ getTeamMemberUsageLoading,
5264
5311
  canManage,
5265
5312
  onEditTeam,
5266
5313
  canCreate,
@@ -5320,6 +5367,8 @@ var init_TeamUsage = __esm({
5320
5367
  team,
5321
5368
  usage: getTeamUsage(team.team_id),
5322
5369
  usageLoading: getTeamUsageLoading(team.team_id),
5370
+ memberUsage: getTeamMemberUsage?.(team.team_id) ?? null,
5371
+ memberUsageLoading: getTeamMemberUsageLoading?.(team.team_id) ?? false,
5323
5372
  canManage,
5324
5373
  onEditTeam,
5325
5374
  canManageMembers: canManageMembers?.(team),
@@ -5897,15 +5946,6 @@ var init_dates = __esm({
5897
5946
  }
5898
5947
  });
5899
5948
 
5900
- // src/permissions.ts
5901
- var import_backstage_plugin_litellm_common5;
5902
- var init_permissions = __esm({
5903
- "src/permissions.ts"() {
5904
- "use strict";
5905
- import_backstage_plugin_litellm_common5 = require("@acarmisc/backstage-plugin-litellm-common");
5906
- }
5907
- });
5908
-
5909
5949
  // src/feedback.ts
5910
5950
  function toastFor(kind, errorMsg) {
5911
5951
  switch (kind) {
@@ -6014,7 +6054,7 @@ function initDateRange() {
6014
6054
  else start.setDate(start.getDate() - 7);
6015
6055
  return { start, end };
6016
6056
  }
6017
- var import_react13, import_Box17, import_Alert6, import_Button7, import_Typography15, import_Tabs2, import_Tab2, import_Grid2, import_Accordion2, import_AccordionSummary2, import_AccordionDetails2, import_ExpandMore, import_react_router_dom, import_react_use2, import_core_plugin_api5, import_core_components3, import_plugin_permission_react3, import_jsx_runtime18, PERIOD_LS_KEY2, PAGE_TABS, isPageTab, LiteLLMPage;
6057
+ var import_react13, import_Box17, import_Alert6, import_Button7, import_Typography15, import_Tabs2, import_Tab2, import_Grid2, import_Accordion2, import_AccordionSummary2, import_AccordionDetails2, import_ExpandMore, import_react_router_dom, import_react_use2, import_core_plugin_api5, import_core_components3, import_plugin_permission_react3, import_backstage_plugin_litellm_common5, import_jsx_runtime18, PERIOD_LS_KEY2, PAGE_TABS, isPageTab, LiteLLMPage;
6018
6058
  var init_LiteLLMPage = __esm({
6019
6059
  "src/components/LiteLLMPage.tsx"() {
6020
6060
  "use strict";
@@ -6046,7 +6086,7 @@ var init_LiteLLMPage = __esm({
6046
6086
  init_AuditLog();
6047
6087
  init_api();
6048
6088
  init_dates();
6049
- init_permissions();
6089
+ import_backstage_plugin_litellm_common5 = require("@acarmisc/backstage-plugin-litellm-common");
6050
6090
  init_feedback();
6051
6091
  init_teamMemberManager();
6052
6092
  init_unprovisionedCopy();
@@ -6135,6 +6175,8 @@ var init_LiteLLMPage = __esm({
6135
6175
  const keyFormOpen = generateDialogOpen || !!keyToEdit;
6136
6176
  const [teamUsageCache, setTeamUsageCache] = (0, import_react13.useState)({});
6137
6177
  const [teamUsageLoading, setTeamUsageLoading] = (0, import_react13.useState)({});
6178
+ const [memberUsageCache, setMemberUsageCache] = (0, import_react13.useState)({});
6179
+ const [memberUsageLoading, setMemberUsageLoading] = (0, import_react13.useState)({});
6138
6180
  const { value: userInfo, loading: userLoading, error: userError } = (0, import_react_use2.useAsync)(
6139
6181
  () => api.getUserInfo(),
6140
6182
  [api]
@@ -6195,6 +6237,8 @@ var init_LiteLLMPage = __esm({
6195
6237
  }
6196
6238
  setTeamUsageCache({});
6197
6239
  setTeamUsageLoading({});
6240
+ setMemberUsageCache({});
6241
+ setMemberUsageLoading({});
6198
6242
  }, [setDateRange, setCurrentPreset]);
6199
6243
  const loadTeamUsage = (0, import_react13.useCallback)(async (teamId) => {
6200
6244
  if (teamUsageCache[teamId] !== void 0 || teamUsageLoading[teamId]) return;
@@ -6210,6 +6254,20 @@ var init_LiteLLMPage = __esm({
6210
6254
  setTeamUsageLoading((prev) => ({ ...prev, [teamId]: false }));
6211
6255
  }
6212
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]);
6213
6271
  const allowedModels = (0, import_react13.useMemo)(() => {
6214
6272
  if (!allModels?.length) return [];
6215
6273
  const userModels = userInfo?.models;
@@ -6504,6 +6562,8 @@ var init_LiteLLMPage = __esm({
6504
6562
  loading: teamsLoading,
6505
6563
  getTeamUsage: (teamId) => teamUsageCache[teamId] ?? null,
6506
6564
  getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false,
6565
+ getTeamMemberUsage: (teamId) => memberUsageCache[teamId] ?? null,
6566
+ getTeamMemberUsageLoading: (teamId) => memberUsageLoading[teamId] ?? false,
6507
6567
  canManage: teamMgmtEnabled && canManageTeam && !readOnly,
6508
6568
  onEditTeam: (t) => setManageTeam({ mode: "edit", team: t }),
6509
6569
  canCreate: teamMgmtEnabled && canCreateTeam && !readOnly,
@@ -6511,7 +6571,10 @@ var init_LiteLLMPage = __esm({
6511
6571
  canManageMembers: (t) => teamMgmtEnabled && canManageMembers && isTeamMemberManager(t, userInfo?.user_id, memberManagerRoles, readOnly),
6512
6572
  onManageMembers: (t) => setManageTeam({ mode: "members", team: t }),
6513
6573
  readOnly,
6514
- onTeamExpand: (team) => loadTeamUsage(team.team_id)
6574
+ onTeamExpand: (team) => {
6575
+ loadTeamUsage(team.team_id);
6576
+ if (liteLlmConfig?.teamUsage?.memberBreakdown) loadMemberUsage(team.team_id);
6577
+ }
6515
6578
  }
6516
6579
  );
6517
6580
  })(),
@@ -6672,7 +6735,6 @@ var index_exports = {};
6672
6735
  __export(index_exports, {
6673
6736
  DashboardHeader: () => DashboardHeader,
6674
6737
  GenerateKeyButton: () => GenerateKeyButton,
6675
- GenerateKeyDialog: () => GenerateKeyDialog,
6676
6738
  KeyFormDialog: () => KeyFormDialog,
6677
6739
  KeysTable: () => KeysTable,
6678
6740
  LiteLLMBudgetGauges: () => LiteLLMBudgetGauges,
@@ -6683,13 +6745,14 @@ __export(index_exports, {
6683
6745
  ManageTeamDialog: () => ManageTeamDialog,
6684
6746
  TeamUsage: () => TeamUsage,
6685
6747
  UsageStats: () => UsageStats,
6748
+ default: () => litellmPlugin,
6686
6749
  liteLlmApiRef: () => liteLlmApiRef,
6687
6750
  litellmPlugin: () => litellmPlugin,
6688
- litellmTeamCreatePermission: () => import_backstage_plugin_litellm_common5.litellmTeamCreatePermission,
6689
- litellmTeamKnowledgebaseManagePermission: () => import_backstage_plugin_litellm_common5.litellmTeamKnowledgebaseManagePermission,
6690
- litellmTeamManagePermission: () => import_backstage_plugin_litellm_common5.litellmTeamManagePermission,
6691
- litellmTeamMcpManagePermission: () => import_backstage_plugin_litellm_common5.litellmTeamMcpManagePermission,
6692
- litellmTeamMembersManagePermission: () => import_backstage_plugin_litellm_common5.litellmTeamMembersManagePermission
6751
+ litellmTeamCreatePermission: () => import_backstage_plugin_litellm_common6.litellmTeamCreatePermission,
6752
+ litellmTeamKnowledgebaseManagePermission: () => import_backstage_plugin_litellm_common6.litellmTeamKnowledgebaseManagePermission,
6753
+ litellmTeamManagePermission: () => import_backstage_plugin_litellm_common6.litellmTeamManagePermission,
6754
+ litellmTeamMcpManagePermission: () => import_backstage_plugin_litellm_common6.litellmTeamMcpManagePermission,
6755
+ litellmTeamMembersManagePermission: () => import_backstage_plugin_litellm_common6.litellmTeamMembersManagePermission
6693
6756
  });
6694
6757
  module.exports = __toCommonJS(index_exports);
6695
6758
 
@@ -7620,30 +7683,7 @@ var LiteLLMBudgetGauges = ({
7620
7683
  // src/index.ts
7621
7684
  init_KeyFormDialog();
7622
7685
  init_GenerateKeyButton();
7623
-
7624
- // src/components/GenerateKeyDialog.tsx
7625
- init_KeyFormDialog();
7626
- var import_jsx_runtime22 = require("react/jsx-runtime");
7627
- var GenerateKeyDialog = (props) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
7628
- KeyFormDialog,
7629
- {
7630
- open: props.open,
7631
- onClose: props.onClose,
7632
- mode: "create",
7633
- keys: props.keys,
7634
- models: props.models,
7635
- teams: props.teams,
7636
- username: props.username,
7637
- keyGenerationSettings: props.keyGenerationSettings,
7638
- onCreateKey: props.onGenerateKey,
7639
- onUpdateKey: async () => {
7640
- },
7641
- onGetConfig: props.onGetConfig
7642
- }
7643
- );
7644
-
7645
- // src/index.ts
7646
7686
  init_ManageTeamDialog();
7647
7687
  init_api();
7648
- init_permissions();
7688
+ var import_backstage_plugin_litellm_common6 = require("@acarmisc/backstage-plugin-litellm-common");
7649
7689
  //# sourceMappingURL=index.cjs.js.map