@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 +55 -66
- package/dist/api.d.ts +4 -1
- package/dist/components/KeyFormDialog.d.ts +0 -2
- package/dist/components/LiteLLMBudgetGauges.d.ts +0 -1
- package/dist/components/TeamUsage.d.ts +11 -1
- package/dist/components/keyFormHelpers.d.ts +0 -1
- package/dist/hooks/useCopyToClipboard.d.ts +0 -5
- package/dist/index.cjs.js +87 -47
- package/dist/index.cjs.js.map +4 -4
- package/dist/index.d.ts +2 -3
- package/dist/index.esm.js +98 -59
- package/dist/index.esm.js.map +4 -4
- package/dist/types.d.ts +6 -1
- package/package.json +5 -11
- package/dist/components/GenerateKeyDialog.d.ts +0 -21
- package/dist/permissions.d.ts +0 -1
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
|
|
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
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
## Screenshots
|
|
12
|
-
|
|
13
|
-
| View | Preview |
|
|
14
|
-
|------|---------|
|
|
15
|
-
| Home widget — `LiteLLMHomeWidget` |  |
|
|
16
|
-
| Overview / Usage Analytics |  |
|
|
17
|
-
| Keys tab — virtual key inventory |  |
|
|
18
|
-
| Generate New Key dialog |  |
|
|
19
|
-
| Models tab |  |
|
|
20
|
-
| Compact "Create Key" card |  |
|
|
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
|
|
18
|
+
yarn --cwd packages/app add @acarmisc/backstage-plugin-litellm
|
|
28
19
|
```
|
|
29
20
|
|
|
30
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-

|
|
26
|
+
import { createApp } from '@backstage/frontend-defaults';
|
|
27
|
+
import litellmPlugin from '@acarmisc/backstage-plugin-litellm';
|
|
46
28
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
+
## The `/litellm` page
|
|
63
38
|
|
|
64
|
-
|
|
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
|
-
|
|
45
|
+

|
|
67
46
|
|
|
68
|
-
|
|
47
|
+
## Homepage components
|
|
69
48
|
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
56
|
+
```tsx
|
|
57
|
+
import {
|
|
58
|
+
LiteLLMHomeWidget,
|
|
59
|
+
LiteLLMBudgetGauges,
|
|
60
|
+
} from '@acarmisc/backstage-plugin-litellm';
|
|
77
61
|
|
|
78
|
-
|
|
62
|
+
<LiteLLMHomeWidget defaultPeriod="7d" />
|
|
63
|
+
<LiteLLMBudgetGauges />
|
|
64
|
+
```
|
|
79
65
|
|
|
80
|
-
|
|
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
|
-

|
|
83
70
|
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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: () =>
|
|
6689
|
-
litellmTeamKnowledgebaseManagePermission: () =>
|
|
6690
|
-
litellmTeamManagePermission: () =>
|
|
6691
|
-
litellmTeamMcpManagePermission: () =>
|
|
6692
|
-
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
|
-
|
|
7688
|
+
var import_backstage_plugin_litellm_common6 = require("@acarmisc/backstage-plugin-litellm-common");
|
|
7649
7689
|
//# sourceMappingURL=index.cjs.js.map
|