@acarmisc/backstage-plugin-litellm 0.15.4 → 0.16.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 +27 -1
- package/dist/components/ManageTeamDialog.d.ts +31 -0
- package/dist/components/TeamUsage.d.ts +2 -0
- package/dist/index.cjs.js +805 -248
- package/dist/index.cjs.js.map +4 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.esm.js +774 -217
- package/dist/index.esm.js.map +4 -4
- package/dist/permissions.d.ts +5 -0
- package/dist/permissions.test.d.ts +1 -0
- package/dist/types.d.ts +48 -0
- package/package.json +5 -1
package/dist/index.esm.js
CHANGED
|
@@ -80,6 +80,24 @@ var init_api = __esm({
|
|
|
80
80
|
await this.throwIfNotOk(response);
|
|
81
81
|
return response.json();
|
|
82
82
|
}
|
|
83
|
+
async patch(path, body) {
|
|
84
|
+
const response = await this.fetchApi.fetch(`${this.basePath}${path}`, {
|
|
85
|
+
method: "PATCH",
|
|
86
|
+
headers: { "Content-Type": "application/json" },
|
|
87
|
+
body: JSON.stringify(body)
|
|
88
|
+
});
|
|
89
|
+
await this.throwIfNotOk(response);
|
|
90
|
+
return response.json();
|
|
91
|
+
}
|
|
92
|
+
async put(path, body) {
|
|
93
|
+
const response = await this.fetchApi.fetch(`${this.basePath}${path}`, {
|
|
94
|
+
method: "PUT",
|
|
95
|
+
headers: { "Content-Type": "application/json" },
|
|
96
|
+
body: JSON.stringify(body)
|
|
97
|
+
});
|
|
98
|
+
await this.throwIfNotOk(response);
|
|
99
|
+
return response.json();
|
|
100
|
+
}
|
|
83
101
|
// User identity is resolved server-side from the Backstage Bearer token.
|
|
84
102
|
// No user_id param needed on the frontend.
|
|
85
103
|
async getUserInfo() {
|
|
@@ -136,6 +154,44 @@ var init_api = __esm({
|
|
|
136
154
|
async getTeams() {
|
|
137
155
|
return this.get("/teams");
|
|
138
156
|
}
|
|
157
|
+
// Teams whose owning_group the caller administers (server-scoped). Distinct
|
|
158
|
+
// from getTeams(), which returns only the teams the caller is a member of.
|
|
159
|
+
async getManagedTeams() {
|
|
160
|
+
return this.get("/teams/managed");
|
|
161
|
+
}
|
|
162
|
+
async addTeamMember(teamId, body) {
|
|
163
|
+
return this.post(
|
|
164
|
+
`/teams/${encodeURIComponent(teamId)}/members`,
|
|
165
|
+
body
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
async removeTeamMember(teamId, userEntityRef) {
|
|
169
|
+
return this.del(
|
|
170
|
+
`/teams/${encodeURIComponent(teamId)}/members?userEntityRef=${encodeURIComponent(
|
|
171
|
+
userEntityRef
|
|
172
|
+
)}`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
// Vector stores (knowledge bases) the caller may attach — already filtered
|
|
176
|
+
// server-side to the operator's allowlist.
|
|
177
|
+
async getVectorStores() {
|
|
178
|
+
return this.get("/vector-stores");
|
|
179
|
+
}
|
|
180
|
+
async setTeamKnowledgeBases(teamId, vectorStores) {
|
|
181
|
+
return this.put(
|
|
182
|
+
`/teams/${encodeURIComponent(teamId)}/knowledge-bases`,
|
|
183
|
+
{ vector_stores: vectorStores }
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
async getMcpServers() {
|
|
187
|
+
return this.get("/mcp-servers");
|
|
188
|
+
}
|
|
189
|
+
async setTeamMcpServers(teamId, mcpServers) {
|
|
190
|
+
return this.put(
|
|
191
|
+
`/teams/${encodeURIComponent(teamId)}/mcp-servers`,
|
|
192
|
+
{ mcp_servers: mcpServers }
|
|
193
|
+
);
|
|
194
|
+
}
|
|
139
195
|
async getUsage(startDate, endDate) {
|
|
140
196
|
return this.get("/usage", { start_date: startDate, end_date: endDate });
|
|
141
197
|
}
|
|
@@ -148,6 +204,12 @@ var init_api = __esm({
|
|
|
148
204
|
async getConfig() {
|
|
149
205
|
return this.get("/config");
|
|
150
206
|
}
|
|
207
|
+
async createTeam(request) {
|
|
208
|
+
return this.post("/teams", request);
|
|
209
|
+
}
|
|
210
|
+
async updateTeam(teamId, request) {
|
|
211
|
+
return this.patch(`/teams/${encodeURIComponent(teamId)}`, request);
|
|
212
|
+
}
|
|
151
213
|
};
|
|
152
214
|
}
|
|
153
215
|
});
|
|
@@ -1857,12 +1919,351 @@ var init_GenerateKeyDialog = __esm({
|
|
|
1857
1919
|
}
|
|
1858
1920
|
});
|
|
1859
1921
|
|
|
1860
|
-
// src/components/
|
|
1861
|
-
import React5, {
|
|
1862
|
-
import
|
|
1922
|
+
// src/components/ManageTeamDialog.tsx
|
|
1923
|
+
import React5, { useState as useState3, useEffect as useEffect2 } from "react";
|
|
1924
|
+
import Dialog3 from "@mui/material/Dialog";
|
|
1925
|
+
import DialogTitle3 from "@mui/material/DialogTitle";
|
|
1926
|
+
import DialogContent3 from "@mui/material/DialogContent";
|
|
1927
|
+
import DialogActions3 from "@mui/material/DialogActions";
|
|
1928
|
+
import TextField3 from "@mui/material/TextField";
|
|
1929
|
+
import Button4 from "@mui/material/Button";
|
|
1930
|
+
import Alert2 from "@mui/material/Alert";
|
|
1931
|
+
import Checkbox2 from "@mui/material/Checkbox";
|
|
1932
|
+
import FormControlLabel2 from "@mui/material/FormControlLabel";
|
|
1933
|
+
import Autocomplete3 from "@mui/material/Autocomplete";
|
|
1863
1934
|
import Box5 from "@mui/material/Box";
|
|
1935
|
+
import Divider2 from "@mui/material/Divider";
|
|
1864
1936
|
import Typography5 from "@mui/material/Typography";
|
|
1865
|
-
import
|
|
1937
|
+
import IconButton3 from "@mui/material/IconButton";
|
|
1938
|
+
import List from "@mui/material/List";
|
|
1939
|
+
import ListItem from "@mui/material/ListItem";
|
|
1940
|
+
import ListItemText from "@mui/material/ListItemText";
|
|
1941
|
+
import { Delete as DeleteIcon } from "@mui/icons-material";
|
|
1942
|
+
var ManageTeamDialog;
|
|
1943
|
+
var init_ManageTeamDialog = __esm({
|
|
1944
|
+
"src/components/ManageTeamDialog.tsx"() {
|
|
1945
|
+
"use strict";
|
|
1946
|
+
ManageTeamDialog = ({
|
|
1947
|
+
open,
|
|
1948
|
+
onClose,
|
|
1949
|
+
mode,
|
|
1950
|
+
team,
|
|
1951
|
+
allModels,
|
|
1952
|
+
config,
|
|
1953
|
+
onSubmit,
|
|
1954
|
+
canManageMembers,
|
|
1955
|
+
onAddMember,
|
|
1956
|
+
onRemoveMember,
|
|
1957
|
+
canManageKnowledgeBases,
|
|
1958
|
+
vectorStores,
|
|
1959
|
+
onSaveKnowledgeBases,
|
|
1960
|
+
canManageMcpServers,
|
|
1961
|
+
mcpServers,
|
|
1962
|
+
onSaveMcpServers
|
|
1963
|
+
}) => {
|
|
1964
|
+
const [alias, setAlias] = useState3("");
|
|
1965
|
+
const [models, setModels] = useState3([]);
|
|
1966
|
+
const [maxBudget, setMaxBudget] = useState3("");
|
|
1967
|
+
const [unlimited, setUnlimited] = useState3(false);
|
|
1968
|
+
const [budgetDuration, setBudgetDuration] = useState3("");
|
|
1969
|
+
const [submitting, setSubmitting] = useState3(false);
|
|
1970
|
+
const [error, setError] = useState3(null);
|
|
1971
|
+
const [memberRef, setMemberRef] = useState3("");
|
|
1972
|
+
const [memberBudget, setMemberBudget] = useState3("");
|
|
1973
|
+
const [memberBusy, setMemberBusy] = useState3(false);
|
|
1974
|
+
const [memberError, setMemberError] = useState3(null);
|
|
1975
|
+
const [kbIds, setKbIds] = useState3([]);
|
|
1976
|
+
const [kbBusy, setKbBusy] = useState3(false);
|
|
1977
|
+
const [kbError, setKbError] = useState3(null);
|
|
1978
|
+
const [mcpIds, setMcpIds] = useState3([]);
|
|
1979
|
+
const [mcpBusy, setMcpBusy] = useState3(false);
|
|
1980
|
+
const [mcpError, setMcpError] = useState3(null);
|
|
1981
|
+
const allowUnlimitedBudget = config?.teamManagement?.allowUnlimitedBudget ?? false;
|
|
1982
|
+
const maxBudgetCeiling = config?.teamManagement?.maxBudgetCeiling;
|
|
1983
|
+
useEffect2(() => {
|
|
1984
|
+
if (open) {
|
|
1985
|
+
if (mode === "edit" && team) {
|
|
1986
|
+
setAlias(team.team_alias ?? "");
|
|
1987
|
+
setModels(team.models ?? []);
|
|
1988
|
+
setMaxBudget(team.max_budget ? String(team.max_budget) : "");
|
|
1989
|
+
setUnlimited(allowUnlimitedBudget && !team.max_budget);
|
|
1990
|
+
setBudgetDuration("");
|
|
1991
|
+
} else {
|
|
1992
|
+
setAlias("");
|
|
1993
|
+
setModels([]);
|
|
1994
|
+
setMaxBudget("");
|
|
1995
|
+
setUnlimited(allowUnlimitedBudget);
|
|
1996
|
+
setBudgetDuration("");
|
|
1997
|
+
}
|
|
1998
|
+
setError(null);
|
|
1999
|
+
setMemberRef("");
|
|
2000
|
+
setMemberBudget("");
|
|
2001
|
+
setMemberError(null);
|
|
2002
|
+
setKbIds(team?.object_permission?.vector_stores ?? []);
|
|
2003
|
+
setKbError(null);
|
|
2004
|
+
setMcpIds(team?.object_permission?.mcp_servers ?? []);
|
|
2005
|
+
setMcpError(null);
|
|
2006
|
+
}
|
|
2007
|
+
}, [open, mode, team, allowUnlimitedBudget]);
|
|
2008
|
+
const handleSubmit = async () => {
|
|
2009
|
+
setError(null);
|
|
2010
|
+
if (!alias.trim()) {
|
|
2011
|
+
setError("Team alias is required");
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
if (models.length === 0) {
|
|
2015
|
+
setError("At least one model is required");
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
if (!unlimited) {
|
|
2019
|
+
if (!maxBudget) {
|
|
2020
|
+
setError("Budget is required when unlimited budgets are disabled");
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
const budget = parseFloat(maxBudget);
|
|
2024
|
+
if (isNaN(budget) || budget <= 0) {
|
|
2025
|
+
setError("Budget must be a positive number");
|
|
2026
|
+
return;
|
|
2027
|
+
}
|
|
2028
|
+
if (maxBudgetCeiling !== null && maxBudgetCeiling !== void 0 && budget > maxBudgetCeiling) {
|
|
2029
|
+
setError(`Budget cannot exceed $${maxBudgetCeiling}`);
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
try {
|
|
2034
|
+
setSubmitting(true);
|
|
2035
|
+
let payload;
|
|
2036
|
+
if (mode === "create") {
|
|
2037
|
+
payload = {
|
|
2038
|
+
team_alias: alias.trim(),
|
|
2039
|
+
models,
|
|
2040
|
+
...unlimited ? { max_budget: null } : { max_budget: parseFloat(maxBudget) },
|
|
2041
|
+
...budgetDuration && { budget_duration: budgetDuration }
|
|
2042
|
+
};
|
|
2043
|
+
} else {
|
|
2044
|
+
payload = {
|
|
2045
|
+
team_alias: alias.trim(),
|
|
2046
|
+
models,
|
|
2047
|
+
...unlimited ? { max_budget: null } : { max_budget: parseFloat(maxBudget) },
|
|
2048
|
+
...budgetDuration && { budget_duration: budgetDuration }
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
await onSubmit(payload);
|
|
2052
|
+
onClose();
|
|
2053
|
+
} catch (err) {
|
|
2054
|
+
setError(err instanceof Error ? err.message : "An error occurred");
|
|
2055
|
+
} finally {
|
|
2056
|
+
setSubmitting(false);
|
|
2057
|
+
}
|
|
2058
|
+
};
|
|
2059
|
+
const handleAddMember = async () => {
|
|
2060
|
+
if (!onAddMember || !memberRef.trim()) return;
|
|
2061
|
+
setMemberError(null);
|
|
2062
|
+
const budget = memberBudget ? parseFloat(memberBudget) : void 0;
|
|
2063
|
+
if (budget !== void 0 && (isNaN(budget) || budget <= 0)) {
|
|
2064
|
+
setMemberError("Max budget in team must be a positive number");
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
2067
|
+
try {
|
|
2068
|
+
setMemberBusy(true);
|
|
2069
|
+
await onAddMember(memberRef.trim(), budget);
|
|
2070
|
+
setMemberRef("");
|
|
2071
|
+
setMemberBudget("");
|
|
2072
|
+
} catch (err) {
|
|
2073
|
+
setMemberError(err instanceof Error ? err.message : "Failed to add member");
|
|
2074
|
+
} finally {
|
|
2075
|
+
setMemberBusy(false);
|
|
2076
|
+
}
|
|
2077
|
+
};
|
|
2078
|
+
const handleRemoveMember = async (userId) => {
|
|
2079
|
+
if (!onRemoveMember) return;
|
|
2080
|
+
setMemberError(null);
|
|
2081
|
+
try {
|
|
2082
|
+
setMemberBusy(true);
|
|
2083
|
+
await onRemoveMember(userId);
|
|
2084
|
+
} catch (err) {
|
|
2085
|
+
setMemberError(err instanceof Error ? err.message : "Failed to remove member");
|
|
2086
|
+
} finally {
|
|
2087
|
+
setMemberBusy(false);
|
|
2088
|
+
}
|
|
2089
|
+
};
|
|
2090
|
+
const handleSaveKnowledgeBases = async () => {
|
|
2091
|
+
if (!onSaveKnowledgeBases) return;
|
|
2092
|
+
setKbError(null);
|
|
2093
|
+
try {
|
|
2094
|
+
setKbBusy(true);
|
|
2095
|
+
await onSaveKnowledgeBases(kbIds);
|
|
2096
|
+
} catch (err) {
|
|
2097
|
+
setKbError(err instanceof Error ? err.message : "Failed to save knowledge bases");
|
|
2098
|
+
} finally {
|
|
2099
|
+
setKbBusy(false);
|
|
2100
|
+
}
|
|
2101
|
+
};
|
|
2102
|
+
const handleSaveMcpServers = async () => {
|
|
2103
|
+
if (!onSaveMcpServers) return;
|
|
2104
|
+
setMcpError(null);
|
|
2105
|
+
try {
|
|
2106
|
+
setMcpBusy(true);
|
|
2107
|
+
await onSaveMcpServers(mcpIds);
|
|
2108
|
+
} catch (err) {
|
|
2109
|
+
setMcpError(err instanceof Error ? err.message : "Failed to save MCP servers");
|
|
2110
|
+
} finally {
|
|
2111
|
+
setMcpBusy(false);
|
|
2112
|
+
}
|
|
2113
|
+
};
|
|
2114
|
+
const modelNames = allModels.map((m) => m.model_name);
|
|
2115
|
+
const members = team?.members_with_roles ?? [];
|
|
2116
|
+
const showMembers = mode === "edit" && !!canManageMembers;
|
|
2117
|
+
const showKnowledgeBases = mode === "edit" && !!canManageKnowledgeBases;
|
|
2118
|
+
const kbOptions = (vectorStores ?? []).map((v) => v.id);
|
|
2119
|
+
const kbLabel = (id) => (vectorStores ?? []).find((v) => v.id === id)?.name ?? id;
|
|
2120
|
+
const showMcpServers = mode === "edit" && !!canManageMcpServers;
|
|
2121
|
+
const mcpOptions = (mcpServers ?? []).map((s) => s.id);
|
|
2122
|
+
const mcpLabel = (id) => (mcpServers ?? []).find((s) => s.id === id)?.name ?? id;
|
|
2123
|
+
return /* @__PURE__ */ React5.createElement(Dialog3, { open, onClose, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ React5.createElement(DialogTitle3, null, mode === "create" ? "Create Team" : "Edit Team"), /* @__PURE__ */ React5.createElement(DialogContent3, { sx: { pt: 2, display: "flex", flexDirection: "column", gap: 2 } }, error && /* @__PURE__ */ React5.createElement(Alert2, { severity: "error" }, error), /* @__PURE__ */ React5.createElement(
|
|
2124
|
+
TextField3,
|
|
2125
|
+
{
|
|
2126
|
+
label: "Team Alias",
|
|
2127
|
+
value: alias,
|
|
2128
|
+
onChange: (e) => setAlias(e.target.value),
|
|
2129
|
+
disabled: submitting,
|
|
2130
|
+
fullWidth: true
|
|
2131
|
+
}
|
|
2132
|
+
), /* @__PURE__ */ React5.createElement(
|
|
2133
|
+
Autocomplete3,
|
|
2134
|
+
{
|
|
2135
|
+
multiple: true,
|
|
2136
|
+
options: modelNames,
|
|
2137
|
+
value: models,
|
|
2138
|
+
onChange: (_, newValue) => setModels(newValue),
|
|
2139
|
+
disabled: submitting,
|
|
2140
|
+
renderInput: (params) => /* @__PURE__ */ React5.createElement(TextField3, { ...params, label: "Models" })
|
|
2141
|
+
}
|
|
2142
|
+
), /* @__PURE__ */ React5.createElement(
|
|
2143
|
+
TextField3,
|
|
2144
|
+
{
|
|
2145
|
+
label: "Max Budget ($)",
|
|
2146
|
+
type: "number",
|
|
2147
|
+
value: maxBudget,
|
|
2148
|
+
onChange: (e) => setMaxBudget(e.target.value),
|
|
2149
|
+
disabled: submitting || unlimited,
|
|
2150
|
+
helperText: maxBudgetCeiling !== null && maxBudgetCeiling !== void 0 ? `Maximum: $${maxBudgetCeiling}` : void 0,
|
|
2151
|
+
fullWidth: true
|
|
2152
|
+
}
|
|
2153
|
+
), allowUnlimitedBudget && /* @__PURE__ */ React5.createElement(
|
|
2154
|
+
FormControlLabel2,
|
|
2155
|
+
{
|
|
2156
|
+
control: /* @__PURE__ */ React5.createElement(Checkbox2, { checked: unlimited, onChange: (e) => setUnlimited(e.target.checked), disabled: submitting }),
|
|
2157
|
+
label: "Unlimited Budget"
|
|
2158
|
+
}
|
|
2159
|
+
), /* @__PURE__ */ React5.createElement(
|
|
2160
|
+
TextField3,
|
|
2161
|
+
{
|
|
2162
|
+
label: "Budget Duration",
|
|
2163
|
+
value: budgetDuration,
|
|
2164
|
+
onChange: (e) => setBudgetDuration(e.target.value),
|
|
2165
|
+
placeholder: "30d",
|
|
2166
|
+
disabled: submitting,
|
|
2167
|
+
fullWidth: true
|
|
2168
|
+
}
|
|
2169
|
+
), showMembers && /* @__PURE__ */ React5.createElement(Box5, null, /* @__PURE__ */ React5.createElement(Divider2, { sx: { my: 1 } }), /* @__PURE__ */ React5.createElement(Typography5, { variant: "subtitle2", sx: { mb: 1 } }, "Members"), memberError && /* @__PURE__ */ React5.createElement(Alert2, { severity: "error", sx: { mb: 1 }, onClose: () => setMemberError(null) }, memberError), members.length === 0 ? /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2", color: "text.secondary" }, "No members yet.") : /* @__PURE__ */ React5.createElement(List, { dense: true, disablePadding: true }, members.map((m) => /* @__PURE__ */ React5.createElement(
|
|
2170
|
+
ListItem,
|
|
2171
|
+
{
|
|
2172
|
+
key: m.user_id,
|
|
2173
|
+
disableGutters: true,
|
|
2174
|
+
secondaryAction: /* @__PURE__ */ React5.createElement(
|
|
2175
|
+
IconButton3,
|
|
2176
|
+
{
|
|
2177
|
+
edge: "end",
|
|
2178
|
+
size: "small",
|
|
2179
|
+
"aria-label": `remove ${m.user_id}`,
|
|
2180
|
+
disabled: memberBusy,
|
|
2181
|
+
onClick: () => handleRemoveMember(m.user_id)
|
|
2182
|
+
},
|
|
2183
|
+
/* @__PURE__ */ React5.createElement(DeleteIcon, { fontSize: "small" })
|
|
2184
|
+
)
|
|
2185
|
+
},
|
|
2186
|
+
/* @__PURE__ */ React5.createElement(ListItemText, { primary: m.user_id, secondary: m.role })
|
|
2187
|
+
))), /* @__PURE__ */ React5.createElement(Box5, { sx: { display: "flex", gap: 1, alignItems: "flex-start", mt: 1 } }, /* @__PURE__ */ React5.createElement(
|
|
2188
|
+
TextField3,
|
|
2189
|
+
{
|
|
2190
|
+
label: "Backstage user ref",
|
|
2191
|
+
placeholder: "user:default/alice",
|
|
2192
|
+
value: memberRef,
|
|
2193
|
+
onChange: (e) => setMemberRef(e.target.value),
|
|
2194
|
+
disabled: memberBusy,
|
|
2195
|
+
size: "small",
|
|
2196
|
+
sx: { flex: 1 }
|
|
2197
|
+
}
|
|
2198
|
+
), /* @__PURE__ */ React5.createElement(
|
|
2199
|
+
TextField3,
|
|
2200
|
+
{
|
|
2201
|
+
label: "Max budget in team ($)",
|
|
2202
|
+
type: "number",
|
|
2203
|
+
value: memberBudget,
|
|
2204
|
+
onChange: (e) => setMemberBudget(e.target.value),
|
|
2205
|
+
disabled: memberBusy,
|
|
2206
|
+
size: "small",
|
|
2207
|
+
sx: { width: 160 }
|
|
2208
|
+
}
|
|
2209
|
+
), /* @__PURE__ */ React5.createElement(
|
|
2210
|
+
Button4,
|
|
2211
|
+
{
|
|
2212
|
+
onClick: handleAddMember,
|
|
2213
|
+
disabled: memberBusy || !memberRef.trim(),
|
|
2214
|
+
variant: "outlined",
|
|
2215
|
+
sx: { mt: 0.5 }
|
|
2216
|
+
},
|
|
2217
|
+
"Add"
|
|
2218
|
+
))), showKnowledgeBases && /* @__PURE__ */ React5.createElement(Box5, null, /* @__PURE__ */ React5.createElement(Divider2, { sx: { my: 1 } }), /* @__PURE__ */ React5.createElement(Typography5, { variant: "subtitle2", sx: { mb: 1 } }, "Knowledge Bases"), /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2", color: "text.secondary", sx: { mb: 1 } }, "Attaching a knowledge base exposes its documents to every key in this team."), kbError && /* @__PURE__ */ React5.createElement(Alert2, { severity: "error", sx: { mb: 1 }, onClose: () => setKbError(null) }, kbError), /* @__PURE__ */ React5.createElement(
|
|
2219
|
+
Autocomplete3,
|
|
2220
|
+
{
|
|
2221
|
+
multiple: true,
|
|
2222
|
+
options: kbOptions,
|
|
2223
|
+
value: kbIds,
|
|
2224
|
+
getOptionLabel: kbLabel,
|
|
2225
|
+
onChange: (_, v) => setKbIds(v),
|
|
2226
|
+
disabled: kbBusy,
|
|
2227
|
+
renderInput: (params) => /* @__PURE__ */ React5.createElement(TextField3, { ...params, label: "Attached knowledge bases" })
|
|
2228
|
+
}
|
|
2229
|
+
), /* @__PURE__ */ React5.createElement(Box5, { sx: { display: "flex", justifyContent: "flex-end", mt: 1 } }, /* @__PURE__ */ React5.createElement(
|
|
2230
|
+
Button4,
|
|
2231
|
+
{
|
|
2232
|
+
onClick: handleSaveKnowledgeBases,
|
|
2233
|
+
disabled: kbBusy,
|
|
2234
|
+
variant: "outlined"
|
|
2235
|
+
},
|
|
2236
|
+
kbBusy ? "Saving\u2026" : "Save knowledge bases"
|
|
2237
|
+
))), showMcpServers && /* @__PURE__ */ React5.createElement(Box5, null, /* @__PURE__ */ React5.createElement(Divider2, { sx: { my: 1 } }), /* @__PURE__ */ React5.createElement(Typography5, { variant: "subtitle2", sx: { mb: 1 } }, "MCP Servers"), /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2", color: "text.secondary", sx: { mb: 1 } }, "Attaching an MCP server lets every key in this team invoke that server's tools through the model \u2014 including any side effects those tools have."), mcpError && /* @__PURE__ */ React5.createElement(Alert2, { severity: "error", sx: { mb: 1 }, onClose: () => setMcpError(null) }, mcpError), /* @__PURE__ */ React5.createElement(
|
|
2238
|
+
Autocomplete3,
|
|
2239
|
+
{
|
|
2240
|
+
multiple: true,
|
|
2241
|
+
options: mcpOptions,
|
|
2242
|
+
value: mcpIds,
|
|
2243
|
+
getOptionLabel: mcpLabel,
|
|
2244
|
+
onChange: (_, v) => setMcpIds(v),
|
|
2245
|
+
disabled: mcpBusy,
|
|
2246
|
+
renderInput: (params) => /* @__PURE__ */ React5.createElement(TextField3, { ...params, label: "Attached MCP servers" })
|
|
2247
|
+
}
|
|
2248
|
+
), /* @__PURE__ */ React5.createElement(Box5, { sx: { display: "flex", justifyContent: "flex-end", mt: 1 } }, /* @__PURE__ */ React5.createElement(
|
|
2249
|
+
Button4,
|
|
2250
|
+
{
|
|
2251
|
+
onClick: handleSaveMcpServers,
|
|
2252
|
+
disabled: mcpBusy,
|
|
2253
|
+
variant: "outlined"
|
|
2254
|
+
},
|
|
2255
|
+
mcpBusy ? "Saving\u2026" : "Save MCP servers"
|
|
2256
|
+
)))), /* @__PURE__ */ React5.createElement(DialogActions3, null, /* @__PURE__ */ React5.createElement(Button4, { onClick: onClose, disabled: submitting }, "Cancel"), /* @__PURE__ */ React5.createElement(Button4, { onClick: handleSubmit, disabled: submitting, variant: "contained" }, submitting ? "Saving..." : "Save")));
|
|
2257
|
+
};
|
|
2258
|
+
}
|
|
2259
|
+
});
|
|
2260
|
+
|
|
2261
|
+
// src/components/UsageStats.tsx
|
|
2262
|
+
import React6, { useMemo as useMemo4, useState as useState4 } from "react";
|
|
2263
|
+
import Paper3 from "@mui/material/Paper";
|
|
2264
|
+
import Box6 from "@mui/material/Box";
|
|
2265
|
+
import Typography6 from "@mui/material/Typography";
|
|
2266
|
+
import TextField4 from "@mui/material/TextField";
|
|
1866
2267
|
import MenuItem2 from "@mui/material/MenuItem";
|
|
1867
2268
|
import Grid from "@mui/material/Grid";
|
|
1868
2269
|
import Table2 from "@mui/material/Table";
|
|
@@ -1908,17 +2309,17 @@ var init_UsageStats = __esm({
|
|
|
1908
2309
|
"30d": "Last 30 days"
|
|
1909
2310
|
};
|
|
1910
2311
|
fmtPct = (n) => `${(n * 100).toFixed(1)}%`;
|
|
1911
|
-
ChartSkeleton = ({ height = 240 }) => /* @__PURE__ */
|
|
2312
|
+
ChartSkeleton = ({ height = 240 }) => /* @__PURE__ */ React6.createElement(Skeleton3, { variant: "rounded", height });
|
|
1912
2313
|
ChartOrFallback = ({ loading, empty, height = 240, children }) => {
|
|
1913
|
-
if (loading) return /* @__PURE__ */
|
|
1914
|
-
if (empty) return /* @__PURE__ */
|
|
1915
|
-
return /* @__PURE__ */
|
|
2314
|
+
if (loading) return /* @__PURE__ */ React6.createElement(ChartSkeleton, { height });
|
|
2315
|
+
if (empty) return /* @__PURE__ */ React6.createElement(EmptyState, { message: "No data for this period", height });
|
|
2316
|
+
return /* @__PURE__ */ React6.createElement(React6.Fragment, null, children);
|
|
1916
2317
|
};
|
|
1917
2318
|
SuccessRateCell = ({ rate, requests }) => {
|
|
1918
|
-
if (requests === 0) return /* @__PURE__ */
|
|
2319
|
+
if (requests === 0) return /* @__PURE__ */ React6.createElement(Typography6, { variant: "body2", color: "text.secondary" }, "\u2014");
|
|
1919
2320
|
const tone = rateTone(rate);
|
|
1920
|
-
return /* @__PURE__ */
|
|
1921
|
-
|
|
2321
|
+
return /* @__PURE__ */ React6.createElement(Box6, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React6.createElement(Box6, { width: 72, flexShrink: 0 }, /* @__PURE__ */ React6.createElement(Meter, { value: rate * 100, tone, height: 4 })), /* @__PURE__ */ React6.createElement(
|
|
2322
|
+
Typography6,
|
|
1922
2323
|
{
|
|
1923
2324
|
variant: "caption",
|
|
1924
2325
|
sx: { fontVariantNumeric: "tabular-nums", minWidth: 44 }
|
|
@@ -1936,8 +2337,8 @@ var init_UsageStats = __esm({
|
|
|
1936
2337
|
userInfo
|
|
1937
2338
|
}) => {
|
|
1938
2339
|
const chart = useChartTheme();
|
|
1939
|
-
const [selectedModel, setSelectedModel] =
|
|
1940
|
-
const [tab, setTab] =
|
|
2340
|
+
const [selectedModel, setSelectedModel] = useState4("all");
|
|
2341
|
+
const [tab, setTab] = useState4("costs");
|
|
1941
2342
|
const handlePresetChange = (preset) => {
|
|
1942
2343
|
const end = /* @__PURE__ */ new Date();
|
|
1943
2344
|
const start = /* @__PURE__ */ new Date();
|
|
@@ -2066,24 +2467,24 @@ var init_UsageStats = __esm({
|
|
|
2066
2467
|
];
|
|
2067
2468
|
const renderKeyRows = () => {
|
|
2068
2469
|
if (loading) {
|
|
2069
|
-
return /* @__PURE__ */
|
|
2470
|
+
return /* @__PURE__ */ React6.createElement(TableRow2, null, /* @__PURE__ */ React6.createElement(TableCell2, { colSpan: 7, sx: { py: 3 } }, /* @__PURE__ */ React6.createElement(Skeleton3, { variant: "rounded", height: 80 })));
|
|
2070
2471
|
}
|
|
2071
2472
|
if (keyRows.length === 0) {
|
|
2072
|
-
return /* @__PURE__ */
|
|
2473
|
+
return /* @__PURE__ */ React6.createElement(TableRow2, null, /* @__PURE__ */ React6.createElement(TableCell2, { colSpan: 7 }, /* @__PURE__ */ React6.createElement(EmptyState, { message: "No key activity in this period" })));
|
|
2073
2474
|
}
|
|
2074
|
-
return [...keyRows].sort((a, b) => b.spend - a.spend || b.apiRequests - a.apiRequests).map((r) => /* @__PURE__ */
|
|
2475
|
+
return [...keyRows].sort((a, b) => b.spend - a.spend || b.apiRequests - a.apiRequests).map((r) => /* @__PURE__ */ React6.createElement(TableRow2, { key: r.keyHash }, /* @__PURE__ */ React6.createElement(TableCell2, null, /* @__PURE__ */ React6.createElement(Typography6, { variant: "body2", sx: { fontWeight: 600 } }, r.keyAlias), r.teamId ? /* @__PURE__ */ React6.createElement(Typography6, { variant: "caption", color: "text.secondary" }, "team: ", r.teamId) : null), /* @__PURE__ */ React6.createElement(TableCell2, null, /* @__PURE__ */ React6.createElement(Box6, { display: "flex", gap: 0.5, flexWrap: "wrap" }, r.models.slice(0, 3).map((m) => /* @__PURE__ */ React6.createElement(TagChip, { key: m, label: m, title: m })), r.models.length > 3 && /* @__PURE__ */ React6.createElement(TagChip, { label: `+${r.models.length - 3}`, mono: false }))), /* @__PURE__ */ React6.createElement(TableCell2, { align: "right" }, fmtUsd(r.spend)), /* @__PURE__ */ React6.createElement(TableCell2, { align: "right" }, fmtInt(r.apiRequests)), /* @__PURE__ */ React6.createElement(TableCell2, { align: "right" }, fmtCompact(r.totalTokens)), /* @__PURE__ */ React6.createElement(TableCell2, { align: "right" }, fmtInt(r.failedRequests)), /* @__PURE__ */ React6.createElement(TableCell2, null, /* @__PURE__ */ React6.createElement(SuccessRateCell, { rate: r.successRate, requests: r.apiRequests }))));
|
|
2075
2476
|
};
|
|
2076
2477
|
const renderModelRows = () => {
|
|
2077
2478
|
if (loading) {
|
|
2078
|
-
return /* @__PURE__ */
|
|
2479
|
+
return /* @__PURE__ */ React6.createElement(TableRow2, null, /* @__PURE__ */ React6.createElement(TableCell2, { colSpan: 8, sx: { py: 3 } }, /* @__PURE__ */ React6.createElement(Skeleton3, { variant: "rounded", height: 80 })));
|
|
2079
2480
|
}
|
|
2080
2481
|
if (modelRows.length === 0) {
|
|
2081
|
-
return /* @__PURE__ */
|
|
2482
|
+
return /* @__PURE__ */ React6.createElement(TableRow2, null, /* @__PURE__ */ React6.createElement(TableCell2, { colSpan: 8 }, /* @__PURE__ */ React6.createElement(EmptyState, { message: "No model activity in this period" })));
|
|
2082
2483
|
}
|
|
2083
|
-
return [...modelRows].sort((a, b) => b.spend - a.spend || b.totalTokens - a.totalTokens).map((r) => /* @__PURE__ */
|
|
2484
|
+
return [...modelRows].sort((a, b) => b.spend - a.spend || b.totalTokens - a.totalTokens).map((r) => /* @__PURE__ */ React6.createElement(TableRow2, { key: r.model }, /* @__PURE__ */ React6.createElement(TableCell2, null, /* @__PURE__ */ React6.createElement(Box6, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React6.createElement(Box6, { sx: { width: 8, height: 8, borderRadius: "50%", bgcolor: chartColor(r.model), flexShrink: 0 } }), /* @__PURE__ */ React6.createElement(Typography6, { variant: "body2", sx: { fontWeight: 600 } }, r.model))), /* @__PURE__ */ React6.createElement(TableCell2, { align: "right" }, fmtUsd(r.spend)), /* @__PURE__ */ React6.createElement(TableCell2, { align: "right" }, fmtInt(r.apiRequests)), /* @__PURE__ */ React6.createElement(TableCell2, { align: "right" }, fmtInt(r.successfulRequests)), /* @__PURE__ */ React6.createElement(TableCell2, { align: "right" }, fmtInt(r.failedRequests)), /* @__PURE__ */ React6.createElement(TableCell2, { align: "right" }, fmtCompact(r.promptTokens)), /* @__PURE__ */ React6.createElement(TableCell2, { align: "right" }, fmtCompact(r.completionTokens)), /* @__PURE__ */ React6.createElement(TableCell2, null, /* @__PURE__ */ React6.createElement(SuccessRateCell, { rate: r.successRate, requests: r.apiRequests }))));
|
|
2084
2485
|
};
|
|
2085
|
-
const periodSelect = /* @__PURE__ */
|
|
2086
|
-
|
|
2486
|
+
const periodSelect = /* @__PURE__ */ React6.createElement(
|
|
2487
|
+
TextField4,
|
|
2087
2488
|
{
|
|
2088
2489
|
select: true,
|
|
2089
2490
|
size: "small",
|
|
@@ -2092,18 +2493,18 @@ var init_UsageStats = __esm({
|
|
|
2092
2493
|
onChange: (e) => handlePresetChange(e.target.value),
|
|
2093
2494
|
sx: { minWidth: 160 }
|
|
2094
2495
|
},
|
|
2095
|
-
Object.keys(PRESET_LABELS).map((p) => /* @__PURE__ */
|
|
2496
|
+
Object.keys(PRESET_LABELS).map((p) => /* @__PURE__ */ React6.createElement(MenuItem2, { key: p, value: p }, PRESET_LABELS[p]))
|
|
2096
2497
|
);
|
|
2097
|
-
return /* @__PURE__ */
|
|
2498
|
+
return /* @__PURE__ */ React6.createElement(
|
|
2098
2499
|
SectionCard,
|
|
2099
2500
|
{
|
|
2100
2501
|
title: "Usage Analytics",
|
|
2101
2502
|
subtitle: `${PRESET_LABELS[currentPreset]} \xB7 ${dateRange.start.toLocaleDateString()} \u2013 ${dateRange.end.toLocaleDateString()}`,
|
|
2102
2503
|
actions: periodSelect
|
|
2103
2504
|
},
|
|
2104
|
-
/* @__PURE__ */
|
|
2105
|
-
/* @__PURE__ */
|
|
2106
|
-
|
|
2505
|
+
/* @__PURE__ */ React6.createElement(MetricStrip, { metrics }),
|
|
2506
|
+
/* @__PURE__ */ React6.createElement(
|
|
2507
|
+
Box6,
|
|
2107
2508
|
{
|
|
2108
2509
|
sx: {
|
|
2109
2510
|
display: "flex",
|
|
@@ -2115,7 +2516,7 @@ var init_UsageStats = __esm({
|
|
|
2115
2516
|
mb: 2
|
|
2116
2517
|
}
|
|
2117
2518
|
},
|
|
2118
|
-
/* @__PURE__ */
|
|
2519
|
+
/* @__PURE__ */ React6.createElement(
|
|
2119
2520
|
SegmentedControl,
|
|
2120
2521
|
{
|
|
2121
2522
|
value: tab,
|
|
@@ -2127,8 +2528,8 @@ var init_UsageStats = __esm({
|
|
|
2127
2528
|
]
|
|
2128
2529
|
}
|
|
2129
2530
|
),
|
|
2130
|
-
tab === "models" && /* @__PURE__ */
|
|
2131
|
-
|
|
2531
|
+
tab === "models" && /* @__PURE__ */ React6.createElement(
|
|
2532
|
+
TextField4,
|
|
2132
2533
|
{
|
|
2133
2534
|
select: true,
|
|
2134
2535
|
size: "small",
|
|
@@ -2137,17 +2538,17 @@ var init_UsageStats = __esm({
|
|
|
2137
2538
|
onChange: (e) => setSelectedModel(e.target.value),
|
|
2138
2539
|
sx: { minWidth: 220 }
|
|
2139
2540
|
},
|
|
2140
|
-
/* @__PURE__ */
|
|
2141
|
-
models.map((m) => /* @__PURE__ */
|
|
2541
|
+
/* @__PURE__ */ React6.createElement(MenuItem2, { value: "all" }, "All models"),
|
|
2542
|
+
models.map((m) => /* @__PURE__ */ React6.createElement(MenuItem2, { key: m.model_name, value: m.model_name }, m.model_name))
|
|
2142
2543
|
)
|
|
2143
2544
|
),
|
|
2144
|
-
tab === "costs" && /* @__PURE__ */
|
|
2545
|
+
tab === "costs" && /* @__PURE__ */ React6.createElement(Grid, { container: true, spacing: 2 }, /* @__PURE__ */ React6.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React6.createElement(ChartCard, { title: "Daily spend by model", height: 260 }, /* @__PURE__ */ React6.createElement(ChartOrFallback, { loading, empty: modelSpendByDate.length === 0, height: 260 }, /* @__PURE__ */ React6.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React6.createElement(AreaChart, { data: modelSpendByDate, margin: { top: 4, right: 8, left: 0, bottom: 0 } }, /* @__PURE__ */ React6.createElement(CartesianGrid, { ...chart.grid }), /* @__PURE__ */ React6.createElement(XAxis, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }), /* @__PURE__ */ React6.createElement(YAxis, { tickFormatter: fmtUsdCompact, width: 56, ...chart.axis }), /* @__PURE__ */ React6.createElement(
|
|
2145
2546
|
Tooltip,
|
|
2146
2547
|
{
|
|
2147
2548
|
cursor: chart.cursor,
|
|
2148
|
-
content: /* @__PURE__ */
|
|
2549
|
+
content: /* @__PURE__ */ React6.createElement(ChartTooltip, { valueFormatter: fmtUsd, hideZero: true })
|
|
2149
2550
|
}
|
|
2150
|
-
), topSpendModels.map((m) => /* @__PURE__ */
|
|
2551
|
+
), topSpendModels.map((m) => /* @__PURE__ */ React6.createElement(
|
|
2151
2552
|
Area,
|
|
2152
2553
|
{
|
|
2153
2554
|
key: m,
|
|
@@ -2159,13 +2560,13 @@ var init_UsageStats = __esm({
|
|
|
2159
2560
|
fill: chartColor(m),
|
|
2160
2561
|
fillOpacity: 0.28
|
|
2161
2562
|
}
|
|
2162
|
-
))))), !loading && topSpendModels.length > 0 && /* @__PURE__ */
|
|
2563
|
+
))))), !loading && topSpendModels.length > 0 && /* @__PURE__ */ React6.createElement(SeriesLegend, { series: topSpendModels.map((m) => ({ name: m, color: chartColor(m) })) }))), /* @__PURE__ */ React6.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React6.createElement(ChartCard, { title: "Daily token usage" }, /* @__PURE__ */ React6.createElement(ChartOrFallback, { loading, empty: dailyData.length === 0 }, /* @__PURE__ */ React6.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React6.createElement(AreaChart, { data: dailyData, margin: { top: 4, right: 8, left: 0, bottom: 0 } }, /* @__PURE__ */ React6.createElement(CartesianGrid, { ...chart.grid }), /* @__PURE__ */ React6.createElement(XAxis, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }), /* @__PURE__ */ React6.createElement(YAxis, { tickFormatter: fmtCompact, width: 48, ...chart.axis }), /* @__PURE__ */ React6.createElement(Tooltip, { cursor: chart.cursor, content: /* @__PURE__ */ React6.createElement(ChartTooltip, { valueFormatter: fmtInt }) }), /* @__PURE__ */ React6.createElement(Legend, { ...chart.legend }), /* @__PURE__ */ React6.createElement(Area, { type: "monotone", dataKey: "promptTokens", name: "Input", stackId: "tok", stroke: SERIES.input, strokeWidth: 1.5, fill: SERIES.input, fillOpacity: 0.25 }), /* @__PURE__ */ React6.createElement(Area, { type: "monotone", dataKey: "completionTokens", name: "Output", stackId: "tok", stroke: SERIES.output, strokeWidth: 1.5, fill: SERIES.output, fillOpacity: 0.25 })))))), /* @__PURE__ */ React6.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React6.createElement(ChartCard, { title: "Daily requests" }, /* @__PURE__ */ React6.createElement(ChartOrFallback, { loading, empty: dailyData.length === 0 }, /* @__PURE__ */ React6.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React6.createElement(BarChart, { data: dailyData, margin: { top: 4, right: 8, left: 0, bottom: 0 }, barCategoryGap: "30%" }, /* @__PURE__ */ React6.createElement(CartesianGrid, { ...chart.grid }), /* @__PURE__ */ React6.createElement(XAxis, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }), /* @__PURE__ */ React6.createElement(YAxis, { tickFormatter: fmtCompact, width: 48, ...chart.axis }), /* @__PURE__ */ React6.createElement(Tooltip, { cursor: chart.cursor, content: /* @__PURE__ */ React6.createElement(ChartTooltip, { valueFormatter: fmtInt }) }), /* @__PURE__ */ React6.createElement(Legend, { ...chart.legend }), /* @__PURE__ */ React6.createElement(Bar, { dataKey: "successfulRequests", name: "Successful", fill: SERIES.success, stackId: "r" }), /* @__PURE__ */ React6.createElement(Bar, { dataKey: "failedRequests", name: "Failed", fill: SERIES.failure, stackId: "r", radius: [3, 3, 0, 0] })))))), /* @__PURE__ */ React6.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React6.createElement(ChartCard, { title: "Daily success rate" }, /* @__PURE__ */ React6.createElement(ChartOrFallback, { loading, empty: successRateData.length === 0 }, /* @__PURE__ */ React6.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React6.createElement(LineChart, { data: successRateData, margin: { top: 4, right: 8, left: 0, bottom: 0 } }, /* @__PURE__ */ React6.createElement(CartesianGrid, { ...chart.grid }), /* @__PURE__ */ React6.createElement(XAxis, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }), /* @__PURE__ */ React6.createElement(YAxis, { domain: [0, 100], tickFormatter: (v) => `${v}%`, width: 44, ...chart.axis }), /* @__PURE__ */ React6.createElement(
|
|
2163
2564
|
Tooltip,
|
|
2164
2565
|
{
|
|
2165
2566
|
cursor: chart.cursor,
|
|
2166
|
-
content: /* @__PURE__ */
|
|
2567
|
+
content: /* @__PURE__ */ React6.createElement(ChartTooltip, { valueFormatter: (v) => `${v}%` })
|
|
2167
2568
|
}
|
|
2168
|
-
), /* @__PURE__ */
|
|
2569
|
+
), /* @__PURE__ */ React6.createElement(ReferenceLine, { y: 100, stroke: SERIES.success, strokeDasharray: "4 4", strokeOpacity: 0.5 }), /* @__PURE__ */ React6.createElement(
|
|
2169
2570
|
Line,
|
|
2170
2571
|
{
|
|
2171
2572
|
type: "monotone",
|
|
@@ -2176,13 +2577,13 @@ var init_UsageStats = __esm({
|
|
|
2176
2577
|
dot: { r: 2.5, strokeWidth: 0, fill: SERIES.input },
|
|
2177
2578
|
activeDot: { r: 4 }
|
|
2178
2579
|
}
|
|
2179
|
-
)))))), /* @__PURE__ */
|
|
2580
|
+
)))))), /* @__PURE__ */ React6.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React6.createElement(
|
|
2180
2581
|
ChartCard,
|
|
2181
2582
|
{
|
|
2182
2583
|
title: maxBudget > 0 ? "Cumulative spend vs budget" : "Cumulative spend",
|
|
2183
2584
|
meta: maxBudget > 0 ? `${fmtUsd(totalCumSpend)} used \xB7 ${fmtUsd(Math.max(0, maxBudget - totalCumSpend))} left` : void 0
|
|
2184
2585
|
},
|
|
2185
|
-
/* @__PURE__ */
|
|
2586
|
+
/* @__PURE__ */ React6.createElement(ChartOrFallback, { loading, empty: cumulativeData.length === 0 }, /* @__PURE__ */ React6.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React6.createElement(AreaChart, { data: cumulativeData, margin: { top: 4, right: 8, left: 0, bottom: 0 } }, /* @__PURE__ */ React6.createElement(CartesianGrid, { ...chart.grid }), /* @__PURE__ */ React6.createElement(XAxis, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }), /* @__PURE__ */ React6.createElement(
|
|
2186
2587
|
YAxis,
|
|
2187
2588
|
{
|
|
2188
2589
|
tickFormatter: fmtUsdCompact,
|
|
@@ -2190,13 +2591,13 @@ var init_UsageStats = __esm({
|
|
|
2190
2591
|
domain: cumulativeDomain,
|
|
2191
2592
|
...chart.axis
|
|
2192
2593
|
}
|
|
2193
|
-
), /* @__PURE__ */
|
|
2594
|
+
), /* @__PURE__ */ React6.createElement(
|
|
2194
2595
|
Tooltip,
|
|
2195
2596
|
{
|
|
2196
2597
|
cursor: chart.cursor,
|
|
2197
|
-
content: /* @__PURE__ */
|
|
2598
|
+
content: /* @__PURE__ */ React6.createElement(ChartTooltip, { valueFormatter: fmtUsd })
|
|
2198
2599
|
}
|
|
2199
|
-
), maxBudget > 0 && /* @__PURE__ */
|
|
2600
|
+
), maxBudget > 0 && /* @__PURE__ */ React6.createElement(
|
|
2200
2601
|
ReferenceLine,
|
|
2201
2602
|
{
|
|
2202
2603
|
y: maxBudget,
|
|
@@ -2204,7 +2605,7 @@ var init_UsageStats = __esm({
|
|
|
2204
2605
|
strokeDasharray: "5 4",
|
|
2205
2606
|
label: { value: `Budget ${fmtUsd(maxBudget)}`, position: "insideTopRight", fontSize: 10, fill: SERIES.budget }
|
|
2206
2607
|
}
|
|
2207
|
-
), /* @__PURE__ */
|
|
2608
|
+
), /* @__PURE__ */ React6.createElement(
|
|
2208
2609
|
Area,
|
|
2209
2610
|
{
|
|
2210
2611
|
type: "monotone",
|
|
@@ -2217,44 +2618,44 @@ var init_UsageStats = __esm({
|
|
|
2217
2618
|
}
|
|
2218
2619
|
))))
|
|
2219
2620
|
))),
|
|
2220
|
-
tab === "models" && /* @__PURE__ */
|
|
2621
|
+
tab === "models" && /* @__PURE__ */ React6.createElement(Grid, { container: true, spacing: 2 }, /* @__PURE__ */ React6.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React6.createElement(ChartCard, { title: "Spend by model", height: Math.max(200, topModelSpendBars.length * 34) }, /* @__PURE__ */ React6.createElement(
|
|
2221
2622
|
ChartOrFallback,
|
|
2222
2623
|
{
|
|
2223
2624
|
loading,
|
|
2224
2625
|
empty: topModelSpendBars.length === 0,
|
|
2225
2626
|
height: Math.max(200, topModelSpendBars.length * 34)
|
|
2226
2627
|
},
|
|
2227
|
-
/* @__PURE__ */
|
|
2228
|
-
))), /* @__PURE__ */
|
|
2628
|
+
/* @__PURE__ */ React6.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React6.createElement(BarChart, { data: topModelSpendBars, layout: "vertical", margin: { top: 0, right: 12, left: 0, bottom: 0 } }, /* @__PURE__ */ React6.createElement(CartesianGrid, { ...chart.grid, vertical: true, horizontal: false }), /* @__PURE__ */ React6.createElement(XAxis, { type: "number", tickFormatter: fmtUsdCompact, ...chart.axis }), /* @__PURE__ */ React6.createElement(YAxis, { type: "category", dataKey: "model", width: 170, ...chart.axis, tick: { fontSize: 10.5, fill: chart.axis.tick.fill } }), /* @__PURE__ */ React6.createElement(Tooltip, { cursor: chart.cursor, content: /* @__PURE__ */ React6.createElement(ChartTooltip, { valueFormatter: fmtUsd, labelFormatter: (l) => l }) }), /* @__PURE__ */ React6.createElement(Bar, { dataKey: "spend", name: "Spend", radius: [0, 3, 3, 0], barSize: 14 }, topModelSpendBars.map((r) => /* @__PURE__ */ React6.createElement(Cell, { key: r.model, fill: chartColor(r.model) })))))
|
|
2629
|
+
))), /* @__PURE__ */ React6.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React6.createElement(ChartCard, { title: "Tokens by model", height: Math.max(200, topModelSpendBars.length * 34) }, /* @__PURE__ */ React6.createElement(
|
|
2229
2630
|
ChartOrFallback,
|
|
2230
2631
|
{
|
|
2231
2632
|
loading,
|
|
2232
2633
|
empty: modelRows.length === 0,
|
|
2233
2634
|
height: Math.max(200, topModelSpendBars.length * 34)
|
|
2234
2635
|
},
|
|
2235
|
-
/* @__PURE__ */
|
|
2236
|
-
))), /* @__PURE__ */
|
|
2237
|
-
tab === "keys" && /* @__PURE__ */
|
|
2636
|
+
/* @__PURE__ */ React6.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React6.createElement(BarChart, { data: topModelSpendBars, layout: "vertical", margin: { top: 0, right: 12, left: 0, bottom: 0 } }, /* @__PURE__ */ React6.createElement(CartesianGrid, { ...chart.grid, vertical: true, horizontal: false }), /* @__PURE__ */ React6.createElement(XAxis, { type: "number", tickFormatter: fmtCompact, ...chart.axis }), /* @__PURE__ */ React6.createElement(YAxis, { type: "category", dataKey: "model", width: 170, ...chart.axis, tick: { fontSize: 10.5, fill: chart.axis.tick.fill } }), /* @__PURE__ */ React6.createElement(Tooltip, { cursor: chart.cursor, content: /* @__PURE__ */ React6.createElement(ChartTooltip, { valueFormatter: fmtInt, labelFormatter: (l) => l }) }), /* @__PURE__ */ React6.createElement(Legend, { ...chart.legend }), /* @__PURE__ */ React6.createElement(Bar, { dataKey: "promptTokens", name: "Input", fill: SERIES.input, stackId: "t", barSize: 14 }), /* @__PURE__ */ React6.createElement(Bar, { dataKey: "completionTokens", name: "Output", fill: SERIES.output, stackId: "t", barSize: 14, radius: [0, 3, 3, 0] })))
|
|
2637
|
+
))), /* @__PURE__ */ React6.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React6.createElement(TableContainer2, { component: Paper3, variant: "outlined", sx: { borderRadius: 2 } }, /* @__PURE__ */ React6.createElement(Table2, { size: "small", sx: dataTableSx }, /* @__PURE__ */ React6.createElement(TableHead2, null, /* @__PURE__ */ React6.createElement(TableRow2, null, /* @__PURE__ */ React6.createElement(TableCell2, null, "Model"), /* @__PURE__ */ React6.createElement(TableCell2, { align: "right" }, "Spend"), /* @__PURE__ */ React6.createElement(TableCell2, { align: "right" }, "Requests"), /* @__PURE__ */ React6.createElement(TableCell2, { align: "right" }, "Success"), /* @__PURE__ */ React6.createElement(TableCell2, { align: "right" }, "Failed"), /* @__PURE__ */ React6.createElement(TableCell2, { align: "right" }, "Input"), /* @__PURE__ */ React6.createElement(TableCell2, { align: "right" }, "Output"), /* @__PURE__ */ React6.createElement(TableCell2, { sx: { width: 160 } }, "Success rate"))), /* @__PURE__ */ React6.createElement(TableBody2, null, renderModelRows()))))),
|
|
2638
|
+
tab === "keys" && /* @__PURE__ */ React6.createElement(Grid, { container: true, spacing: 2 }, (loading || topKeySpendBars.length > 0) && /* @__PURE__ */ React6.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React6.createElement(ChartCard, { title: "Spend by key", height: Math.max(160, topKeySpendBars.length * 32) }, /* @__PURE__ */ React6.createElement(
|
|
2238
2639
|
ChartOrFallback,
|
|
2239
2640
|
{
|
|
2240
2641
|
loading,
|
|
2241
2642
|
empty: topKeySpendBars.length === 0,
|
|
2242
2643
|
height: Math.max(160, topKeySpendBars.length * 32)
|
|
2243
2644
|
},
|
|
2244
|
-
/* @__PURE__ */
|
|
2245
|
-
))), /* @__PURE__ */
|
|
2645
|
+
/* @__PURE__ */ React6.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React6.createElement(BarChart, { data: topKeySpendBars, layout: "vertical", margin: { top: 0, right: 12, left: 0, bottom: 0 } }, /* @__PURE__ */ React6.createElement(CartesianGrid, { ...chart.grid, vertical: true, horizontal: false }), /* @__PURE__ */ React6.createElement(XAxis, { type: "number", tickFormatter: fmtUsdCompact, ...chart.axis }), /* @__PURE__ */ React6.createElement(YAxis, { type: "category", dataKey: "keyAlias", width: 160, ...chart.axis, tick: { fontSize: 10.5, fill: chart.axis.tick.fill } }), /* @__PURE__ */ React6.createElement(Tooltip, { cursor: chart.cursor, content: /* @__PURE__ */ React6.createElement(ChartTooltip, { valueFormatter: fmtUsd, labelFormatter: (l) => l }) }), /* @__PURE__ */ React6.createElement(Bar, { dataKey: "spend", name: "Spend", fill: SERIES.input, radius: [0, 3, 3, 0], barSize: 14 })))
|
|
2646
|
+
))), /* @__PURE__ */ React6.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React6.createElement(TableContainer2, { component: Paper3, variant: "outlined", sx: { borderRadius: 2 } }, /* @__PURE__ */ React6.createElement(Table2, { size: "small", sx: dataTableSx }, /* @__PURE__ */ React6.createElement(TableHead2, null, /* @__PURE__ */ React6.createElement(TableRow2, null, /* @__PURE__ */ React6.createElement(TableCell2, null, "Key"), /* @__PURE__ */ React6.createElement(TableCell2, null, "Models"), /* @__PURE__ */ React6.createElement(TableCell2, { align: "right" }, "Spend"), /* @__PURE__ */ React6.createElement(TableCell2, { align: "right" }, "Requests"), /* @__PURE__ */ React6.createElement(TableCell2, { align: "right" }, "Tokens"), /* @__PURE__ */ React6.createElement(TableCell2, { align: "right" }, "Failed"), /* @__PURE__ */ React6.createElement(TableCell2, { sx: { width: 160 } }, "Success rate"))), /* @__PURE__ */ React6.createElement(TableBody2, null, renderKeyRows())))))
|
|
2246
2647
|
);
|
|
2247
2648
|
};
|
|
2248
2649
|
}
|
|
2249
2650
|
});
|
|
2250
2651
|
|
|
2251
2652
|
// src/components/TeamUsage.tsx
|
|
2252
|
-
import
|
|
2653
|
+
import React7, { useState as useState5 } from "react";
|
|
2253
2654
|
import Paper4 from "@mui/material/Paper";
|
|
2254
|
-
import
|
|
2255
|
-
import
|
|
2655
|
+
import Box7 from "@mui/material/Box";
|
|
2656
|
+
import Typography7 from "@mui/material/Typography";
|
|
2256
2657
|
import Skeleton4 from "@mui/material/Skeleton";
|
|
2257
|
-
import
|
|
2658
|
+
import Divider3 from "@mui/material/Divider";
|
|
2258
2659
|
import Table3 from "@mui/material/Table";
|
|
2259
2660
|
import TableBody3 from "@mui/material/TableBody";
|
|
2260
2661
|
import TableCell3 from "@mui/material/TableCell";
|
|
@@ -2262,7 +2663,8 @@ import TableHead3 from "@mui/material/TableHead";
|
|
|
2262
2663
|
import TableRow3 from "@mui/material/TableRow";
|
|
2263
2664
|
import TableContainer3 from "@mui/material/TableContainer";
|
|
2264
2665
|
import Collapse from "@mui/material/Collapse";
|
|
2265
|
-
import
|
|
2666
|
+
import IconButton4 from "@mui/material/IconButton";
|
|
2667
|
+
import Button5 from "@mui/material/Button";
|
|
2266
2668
|
import CircularProgress3 from "@mui/material/CircularProgress";
|
|
2267
2669
|
import { alpha as alpha4 } from "@mui/material/styles";
|
|
2268
2670
|
import { ExpandMore as ExpandMore2, Group } from "@mui/icons-material";
|
|
@@ -2286,8 +2688,8 @@ var init_TeamUsage = __esm({
|
|
|
2286
2688
|
"use strict";
|
|
2287
2689
|
init_ui();
|
|
2288
2690
|
fmtUsd2 = (n) => `$${(n ?? 0).toFixed(2)}`;
|
|
2289
|
-
TeamCard = ({ team, usage, usageLoading }) => {
|
|
2290
|
-
const [expanded, setExpanded] =
|
|
2691
|
+
TeamCard = ({ team, usage, usageLoading, canManage, onEditTeam }) => {
|
|
2692
|
+
const [expanded, setExpanded] = useState5(false);
|
|
2291
2693
|
const chart = useChartTheme();
|
|
2292
2694
|
const budget = team.max_budget ?? 0;
|
|
2293
2695
|
const spend = team.spend ?? 0;
|
|
@@ -2297,24 +2699,24 @@ var init_TeamUsage = __esm({
|
|
|
2297
2699
|
const dailyData = usage?.daily_usage?.map((d) => ({ date: d.date, spend: d.spend })) ?? [];
|
|
2298
2700
|
const renderDailySpendSection = () => {
|
|
2299
2701
|
if (usageLoading) {
|
|
2300
|
-
return /* @__PURE__ */
|
|
2702
|
+
return /* @__PURE__ */ React7.createElement(Box7, { display: "flex", justifyContent: "center", py: 3 }, /* @__PURE__ */ React7.createElement(CircularProgress3, { size: 22 }));
|
|
2301
2703
|
}
|
|
2302
2704
|
if (dailyData.length === 0) return null;
|
|
2303
|
-
return /* @__PURE__ */
|
|
2304
|
-
|
|
2705
|
+
return /* @__PURE__ */ React7.createElement(Box7, { mb: 2.5 }, /* @__PURE__ */ React7.createElement(
|
|
2706
|
+
Typography7,
|
|
2305
2707
|
{
|
|
2306
2708
|
variant: "caption",
|
|
2307
2709
|
color: "text.secondary",
|
|
2308
2710
|
sx: { display: "block", fontSize: 11, fontWeight: 700, letterSpacing: "0.07em", textTransform: "uppercase", mb: 1 }
|
|
2309
2711
|
},
|
|
2310
2712
|
"Daily spend"
|
|
2311
|
-
), /* @__PURE__ */
|
|
2713
|
+
), /* @__PURE__ */ React7.createElement(Box7, { height: 150 }, /* @__PURE__ */ React7.createElement(ResponsiveContainer2, { width: "100%", height: "100%" }, /* @__PURE__ */ React7.createElement(AreaChart2, { data: dailyData, margin: { top: 4, right: 8, left: 0, bottom: 0 } }, /* @__PURE__ */ React7.createElement(CartesianGrid2, { ...chart.grid }), /* @__PURE__ */ React7.createElement(XAxis2, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }), /* @__PURE__ */ React7.createElement(YAxis2, { tickFormatter: fmtUsdCompact, width: 52, ...chart.axis }), /* @__PURE__ */ React7.createElement(
|
|
2312
2714
|
Tooltip2,
|
|
2313
2715
|
{
|
|
2314
2716
|
cursor: chart.cursor,
|
|
2315
|
-
content: /* @__PURE__ */
|
|
2717
|
+
content: /* @__PURE__ */ React7.createElement(ChartTooltip, { valueFormatter: (v) => `$${v.toFixed(4)}` })
|
|
2316
2718
|
}
|
|
2317
|
-
), /* @__PURE__ */
|
|
2719
|
+
), /* @__PURE__ */ React7.createElement(
|
|
2318
2720
|
Area2,
|
|
2319
2721
|
{
|
|
2320
2722
|
type: "monotone",
|
|
@@ -2327,8 +2729,8 @@ var init_TeamUsage = __esm({
|
|
|
2327
2729
|
}
|
|
2328
2730
|
)))));
|
|
2329
2731
|
};
|
|
2330
|
-
const sectionLabel = (label) => /* @__PURE__ */
|
|
2331
|
-
|
|
2732
|
+
const sectionLabel = (label) => /* @__PURE__ */ React7.createElement(
|
|
2733
|
+
Typography7,
|
|
2332
2734
|
{
|
|
2333
2735
|
variant: "caption",
|
|
2334
2736
|
color: "text.secondary",
|
|
@@ -2336,8 +2738,8 @@ var init_TeamUsage = __esm({
|
|
|
2336
2738
|
},
|
|
2337
2739
|
label
|
|
2338
2740
|
);
|
|
2339
|
-
return /* @__PURE__ */
|
|
2340
|
-
|
|
2741
|
+
return /* @__PURE__ */ React7.createElement(Paper4, { variant: "outlined", sx: { borderRadius: 2, p: 2.5 } }, /* @__PURE__ */ React7.createElement(Box7, { display: "flex", alignItems: "center", gap: 1.5 }, /* @__PURE__ */ React7.createElement(
|
|
2742
|
+
Box7,
|
|
2341
2743
|
{
|
|
2342
2744
|
sx: (theme) => ({
|
|
2343
2745
|
width: 36,
|
|
@@ -2351,17 +2753,26 @@ var init_TeamUsage = __esm({
|
|
|
2351
2753
|
color: theme.palette.primary.main
|
|
2352
2754
|
})
|
|
2353
2755
|
},
|
|
2354
|
-
/* @__PURE__ */
|
|
2355
|
-
), /* @__PURE__ */
|
|
2356
|
-
|
|
2756
|
+
/* @__PURE__ */ React7.createElement(Group, { fontSize: "small" })
|
|
2757
|
+
), /* @__PURE__ */ React7.createElement(Box7, { flexGrow: 1, minWidth: 0 }, /* @__PURE__ */ React7.createElement(Typography7, { variant: "subtitle1", sx: { fontWeight: 600, lineHeight: 1.3 } }, team.team_alias || "Untitled team"), /* @__PURE__ */ React7.createElement(
|
|
2758
|
+
Typography7,
|
|
2357
2759
|
{
|
|
2358
2760
|
variant: "caption",
|
|
2359
2761
|
color: "text.secondary",
|
|
2360
2762
|
sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 11 }
|
|
2361
2763
|
},
|
|
2362
2764
|
team.team_id
|
|
2363
|
-
)), isOver && /* @__PURE__ */
|
|
2364
|
-
|
|
2765
|
+
)), isOver && /* @__PURE__ */ React7.createElement(StatusPill, { label: "Over budget", tone: "danger" }), isNear && /* @__PURE__ */ React7.createElement(StatusPill, { label: "Near limit", tone: "warning" }), canManage && onEditTeam && /* @__PURE__ */ React7.createElement(
|
|
2766
|
+
Button5,
|
|
2767
|
+
{
|
|
2768
|
+
size: "small",
|
|
2769
|
+
variant: "text",
|
|
2770
|
+
onClick: () => onEditTeam(team),
|
|
2771
|
+
sx: { textTransform: "none" }
|
|
2772
|
+
},
|
|
2773
|
+
"Edit"
|
|
2774
|
+
), /* @__PURE__ */ React7.createElement(
|
|
2775
|
+
IconButton4,
|
|
2365
2776
|
{
|
|
2366
2777
|
size: "small",
|
|
2367
2778
|
onClick: () => setExpanded((e) => !e),
|
|
@@ -2373,9 +2784,9 @@ var init_TeamUsage = __esm({
|
|
|
2373
2784
|
transition: theme.transitions.create("transform")
|
|
2374
2785
|
})
|
|
2375
2786
|
},
|
|
2376
|
-
/* @__PURE__ */
|
|
2377
|
-
)), /* @__PURE__ */
|
|
2378
|
-
|
|
2787
|
+
/* @__PURE__ */ React7.createElement(ExpandMore2, null)
|
|
2788
|
+
)), /* @__PURE__ */ React7.createElement(
|
|
2789
|
+
Box7,
|
|
2379
2790
|
{
|
|
2380
2791
|
sx: {
|
|
2381
2792
|
display: "grid",
|
|
@@ -2385,35 +2796,35 @@ var init_TeamUsage = __esm({
|
|
|
2385
2796
|
maxWidth: 640
|
|
2386
2797
|
}
|
|
2387
2798
|
},
|
|
2388
|
-
/* @__PURE__ */
|
|
2389
|
-
/* @__PURE__ */
|
|
2390
|
-
/* @__PURE__ */
|
|
2391
|
-
/* @__PURE__ */
|
|
2392
|
-
/* @__PURE__ */
|
|
2393
|
-
/* @__PURE__ */
|
|
2394
|
-
), budget > 0 && /* @__PURE__ */
|
|
2799
|
+
/* @__PURE__ */ React7.createElement(Stat, { label: "Members", value: team.members_with_roles?.length ?? "\u2014" }),
|
|
2800
|
+
/* @__PURE__ */ React7.createElement(Stat, { label: "Models", value: team.models?.length ? team.models.length : "All" }),
|
|
2801
|
+
/* @__PURE__ */ React7.createElement(Stat, { label: "Budget", value: budget > 0 ? fmtUsd2(budget) : "Unlimited" }),
|
|
2802
|
+
/* @__PURE__ */ React7.createElement(Stat, { label: "Spend", value: fmtUsd2(spend) }),
|
|
2803
|
+
/* @__PURE__ */ React7.createElement(Stat, { label: "TPM", value: team.tpm_limit && team.tpm_limit > 0 ? team.tpm_limit.toLocaleString() : "\u2014" }),
|
|
2804
|
+
/* @__PURE__ */ React7.createElement(Stat, { label: "RPM", value: team.rpm_limit && team.rpm_limit > 0 ? team.rpm_limit.toLocaleString() : "\u2014" })
|
|
2805
|
+
), budget > 0 && /* @__PURE__ */ React7.createElement(Box7, { mt: 2, maxWidth: 640 }, /* @__PURE__ */ React7.createElement(Box7, { display: "flex", justifyContent: "space-between", alignItems: "baseline", mb: 0.75 }, /* @__PURE__ */ React7.createElement(Typography7, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, fmtUsd2(spend), " of ", fmtUsd2(budget)), /* @__PURE__ */ React7.createElement(Typography7, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, budgetPct.toFixed(0), "%")), /* @__PURE__ */ React7.createElement(Meter, { value: budgetPct, tone: budgetTone2(isOver, isNear), height: 5 })), /* @__PURE__ */ React7.createElement(Collapse, { in: expanded, unmountOnExit: true }, /* @__PURE__ */ React7.createElement(Divider3, { sx: { my: 2.5 } }), /* @__PURE__ */ React7.createElement(Box7, { mb: 2.5 }, sectionLabel("Models"), team.models?.length ? /* @__PURE__ */ React7.createElement(Box7, { display: "flex", gap: 0.5, flexWrap: "wrap" }, team.models.map((m) => /* @__PURE__ */ React7.createElement(TagChip, { key: m, label: m, title: m }))) : /* @__PURE__ */ React7.createElement(Typography7, { variant: "body2", color: "text.secondary" }, "All models allowed")), renderDailySpendSection(), /* @__PURE__ */ React7.createElement(Box7, null, sectionLabel("Members"), team.members_with_roles?.length ? /* @__PURE__ */ React7.createElement(
|
|
2395
2806
|
TableContainer3,
|
|
2396
2807
|
{
|
|
2397
2808
|
component: Paper4,
|
|
2398
2809
|
variant: "outlined",
|
|
2399
2810
|
sx: { borderRadius: 1.5 }
|
|
2400
2811
|
},
|
|
2401
|
-
/* @__PURE__ */
|
|
2402
|
-
|
|
2812
|
+
/* @__PURE__ */ React7.createElement(Table3, { size: "small", sx: dataTableSx }, /* @__PURE__ */ React7.createElement(TableHead3, null, /* @__PURE__ */ React7.createElement(TableRow3, null, /* @__PURE__ */ React7.createElement(TableCell3, null, "User"), /* @__PURE__ */ React7.createElement(TableCell3, { align: "right" }, "Role"))), /* @__PURE__ */ React7.createElement(TableBody3, null, team.members_with_roles.map((m) => /* @__PURE__ */ React7.createElement(TableRow3, { key: m.user_id }, /* @__PURE__ */ React7.createElement(TableCell3, null, m.user_email ? /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement(Typography7, { variant: "body2" }, m.user_email), /* @__PURE__ */ React7.createElement(
|
|
2813
|
+
Typography7,
|
|
2403
2814
|
{
|
|
2404
2815
|
variant: "caption",
|
|
2405
2816
|
color: "text.secondary",
|
|
2406
2817
|
sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 11 }
|
|
2407
2818
|
},
|
|
2408
2819
|
m.user_id
|
|
2409
|
-
)) : /* @__PURE__ */
|
|
2410
|
-
|
|
2820
|
+
)) : /* @__PURE__ */ React7.createElement(
|
|
2821
|
+
Typography7,
|
|
2411
2822
|
{
|
|
2412
2823
|
variant: "body2",
|
|
2413
2824
|
sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace" }
|
|
2414
2825
|
},
|
|
2415
2826
|
m.user_id
|
|
2416
|
-
)), /* @__PURE__ */
|
|
2827
|
+
)), /* @__PURE__ */ React7.createElement(TableCell3, { align: "right" }, /* @__PURE__ */ React7.createElement(
|
|
2417
2828
|
StatusPill,
|
|
2418
2829
|
{
|
|
2419
2830
|
label: m.role,
|
|
@@ -2421,19 +2832,21 @@ var init_TeamUsage = __esm({
|
|
|
2421
2832
|
dot: false
|
|
2422
2833
|
}
|
|
2423
2834
|
))))))
|
|
2424
|
-
) : /* @__PURE__ */
|
|
2835
|
+
) : /* @__PURE__ */ React7.createElement(Typography7, { variant: "body2", color: "text.secondary" }, "No members assigned."))));
|
|
2425
2836
|
};
|
|
2426
2837
|
TeamUsage = ({
|
|
2427
2838
|
teams,
|
|
2428
2839
|
loading,
|
|
2429
2840
|
getTeamUsage,
|
|
2430
|
-
getTeamUsageLoading
|
|
2841
|
+
getTeamUsageLoading,
|
|
2842
|
+
canManage,
|
|
2843
|
+
onEditTeam
|
|
2431
2844
|
}) => {
|
|
2432
2845
|
if (loading) {
|
|
2433
|
-
return /* @__PURE__ */
|
|
2846
|
+
return /* @__PURE__ */ React7.createElement(SectionCard, { title: "Teams" }, /* @__PURE__ */ React7.createElement(Skeleton4, { variant: "rounded", height: 120, sx: { mb: 2 } }), /* @__PURE__ */ React7.createElement(Skeleton4, { variant: "rounded", height: 120 }));
|
|
2434
2847
|
}
|
|
2435
2848
|
if (!teams.length) {
|
|
2436
|
-
return /* @__PURE__ */
|
|
2849
|
+
return /* @__PURE__ */ React7.createElement(SectionCard, { title: "Teams" }, /* @__PURE__ */ React7.createElement(
|
|
2437
2850
|
EmptyState,
|
|
2438
2851
|
{
|
|
2439
2852
|
message: "You're not a member of any LiteLLM team yet.",
|
|
@@ -2441,13 +2854,15 @@ var init_TeamUsage = __esm({
|
|
|
2441
2854
|
}
|
|
2442
2855
|
));
|
|
2443
2856
|
}
|
|
2444
|
-
return /* @__PURE__ */
|
|
2857
|
+
return /* @__PURE__ */ React7.createElement(SectionCard, { title: "Teams", subtitle: `${teams.length} team${teams.length === 1 ? "" : "s"}` }, /* @__PURE__ */ React7.createElement(Box7, { sx: { display: "flex", flexDirection: "column", gap: 2 } }, teams.map((team) => /* @__PURE__ */ React7.createElement(
|
|
2445
2858
|
TeamCard,
|
|
2446
2859
|
{
|
|
2447
2860
|
key: team.team_id,
|
|
2448
2861
|
team,
|
|
2449
2862
|
usage: getTeamUsage(team.team_id),
|
|
2450
|
-
usageLoading: getTeamUsageLoading(team.team_id)
|
|
2863
|
+
usageLoading: getTeamUsageLoading(team.team_id),
|
|
2864
|
+
canManage,
|
|
2865
|
+
onEditTeam
|
|
2451
2866
|
}
|
|
2452
2867
|
))));
|
|
2453
2868
|
};
|
|
@@ -2455,15 +2870,15 @@ var init_TeamUsage = __esm({
|
|
|
2455
2870
|
});
|
|
2456
2871
|
|
|
2457
2872
|
// src/components/ModelsTable.tsx
|
|
2458
|
-
import
|
|
2459
|
-
import
|
|
2873
|
+
import React8, { useState as useState6, useMemo as useMemo5, useCallback } from "react";
|
|
2874
|
+
import Box8 from "@mui/material/Box";
|
|
2460
2875
|
import Table4 from "@mui/material/Table";
|
|
2461
2876
|
import TableBody4 from "@mui/material/TableBody";
|
|
2462
2877
|
import TableCell4 from "@mui/material/TableCell";
|
|
2463
2878
|
import TableContainer4 from "@mui/material/TableContainer";
|
|
2464
2879
|
import TableHead4 from "@mui/material/TableHead";
|
|
2465
2880
|
import TableRow4 from "@mui/material/TableRow";
|
|
2466
|
-
import
|
|
2881
|
+
import Typography8 from "@mui/material/Typography";
|
|
2467
2882
|
import MenuItem3 from "@mui/material/MenuItem";
|
|
2468
2883
|
import Select from "@mui/material/Select";
|
|
2469
2884
|
import FormControl from "@mui/material/FormControl";
|
|
@@ -2471,7 +2886,7 @@ import InputLabel from "@mui/material/InputLabel";
|
|
|
2471
2886
|
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
|
|
2472
2887
|
import Tooltip3 from "@mui/material/Tooltip";
|
|
2473
2888
|
import Snackbar from "@mui/material/Snackbar";
|
|
2474
|
-
import
|
|
2889
|
+
import Alert3 from "@mui/material/Alert";
|
|
2475
2890
|
import { alpha as alpha5, useTheme as useTheme2 } from "@mui/material/styles";
|
|
2476
2891
|
function isModelAllowedByTeam2(model, teamModels) {
|
|
2477
2892
|
if (!teamModels || teamModels.length === 0 || teamModels.includes(ALL_PROXY_MODELS2)) return true;
|
|
@@ -2498,8 +2913,8 @@ var init_ModelsTable = __esm({
|
|
|
2498
2913
|
ALL_PROXY_MODELS2 = "all-proxy-models";
|
|
2499
2914
|
ModelsTable = ({ allModels, teams, loading }) => {
|
|
2500
2915
|
const theme = useTheme2();
|
|
2501
|
-
const [selectedTeamId, setSelectedTeamId] =
|
|
2502
|
-
const [copiedSnackbar, setCopiedSnackbar] =
|
|
2916
|
+
const [selectedTeamId, setSelectedTeamId] = useState6("");
|
|
2917
|
+
const [copiedSnackbar, setCopiedSnackbar] = useState6(false);
|
|
2503
2918
|
const selectedTeam = useMemo5(
|
|
2504
2919
|
() => teams.find((t) => t.team_id === selectedTeamId) ?? null,
|
|
2505
2920
|
[teams, selectedTeamId]
|
|
@@ -2525,13 +2940,13 @@ var init_ModelsTable = __esm({
|
|
|
2525
2940
|
return theme.palette.info.main;
|
|
2526
2941
|
}
|
|
2527
2942
|
};
|
|
2528
|
-
return /* @__PURE__ */
|
|
2943
|
+
return /* @__PURE__ */ React8.createElement(
|
|
2529
2944
|
SectionCard,
|
|
2530
2945
|
{
|
|
2531
2946
|
title: "Available Models",
|
|
2532
2947
|
subtitle: selectedTeam ? `${filteredModels.length} model${filteredModels.length !== 1 ? "s" : ""} in ${selectedTeam.team_alias ?? selectedTeam.team_id}` : "Select a team to view its models"
|
|
2533
2948
|
},
|
|
2534
|
-
/* @__PURE__ */
|
|
2949
|
+
/* @__PURE__ */ React8.createElement(Box8, { sx: { px: 2.5, pb: 2.5, pt: 1 } }, /* @__PURE__ */ React8.createElement(FormControl, { size: "small", sx: { minWidth: 240, mb: 2 } }, /* @__PURE__ */ React8.createElement(InputLabel, { id: "team-select-label" }, "Team"), /* @__PURE__ */ React8.createElement(
|
|
2535
2950
|
Select,
|
|
2536
2951
|
{
|
|
2537
2952
|
labelId: "team-select-label",
|
|
@@ -2539,25 +2954,25 @@ var init_ModelsTable = __esm({
|
|
|
2539
2954
|
label: "Team",
|
|
2540
2955
|
onChange: (e) => setSelectedTeamId(e.target.value)
|
|
2541
2956
|
},
|
|
2542
|
-
teams.map((t) => /* @__PURE__ */
|
|
2957
|
+
teams.map((t) => /* @__PURE__ */ React8.createElement(MenuItem3, { key: t.team_id, value: t.team_id }, t.team_alias ?? t.team_id))
|
|
2543
2958
|
))),
|
|
2544
|
-
loading && /* @__PURE__ */
|
|
2545
|
-
!loading && !selectedTeamId && /* @__PURE__ */
|
|
2959
|
+
loading && /* @__PURE__ */ React8.createElement(EmptyState, { message: "Loading models\u2026" }),
|
|
2960
|
+
!loading && !selectedTeamId && /* @__PURE__ */ React8.createElement(
|
|
2546
2961
|
EmptyState,
|
|
2547
2962
|
{
|
|
2548
2963
|
message: "Select a team to see available models",
|
|
2549
2964
|
hint: "Each team may have a different set of models assigned"
|
|
2550
2965
|
}
|
|
2551
2966
|
),
|
|
2552
|
-
!loading && selectedTeamId && filteredModels.length === 0 && /* @__PURE__ */
|
|
2967
|
+
!loading && selectedTeamId && filteredModels.length === 0 && /* @__PURE__ */ React8.createElement(
|
|
2553
2968
|
EmptyState,
|
|
2554
2969
|
{
|
|
2555
2970
|
message: "No models available for this team",
|
|
2556
2971
|
hint: allModels.length === 0 ? "No models are configured in the system" : "None of the team's assigned models were found in the catalog"
|
|
2557
2972
|
}
|
|
2558
2973
|
),
|
|
2559
|
-
!loading && filteredModels.length > 0 && /* @__PURE__ */
|
|
2560
|
-
|
|
2974
|
+
!loading && filteredModels.length > 0 && /* @__PURE__ */ React8.createElement(TableContainer4, null, /* @__PURE__ */ React8.createElement(Table4, { size: "small", sx: dataTableSx }, /* @__PURE__ */ React8.createElement(TableHead4, null, /* @__PURE__ */ React8.createElement(TableRow4, null, /* @__PURE__ */ React8.createElement(TableCell4, null, "Model ID"), /* @__PURE__ */ React8.createElement(TableCell4, null, "Mode"), /* @__PURE__ */ React8.createElement(TableCell4, { align: "right" }, "Input Cost"), /* @__PURE__ */ React8.createElement(TableCell4, { align: "right" }, "Output Cost"), /* @__PURE__ */ React8.createElement(TableCell4, { align: "right" }, "Max Input"), /* @__PURE__ */ React8.createElement(TableCell4, { align: "right" }, "Max Output"), /* @__PURE__ */ React8.createElement(TableCell4, null, "Capabilities"))), /* @__PURE__ */ React8.createElement(TableBody4, null, filteredModels.map((m) => /* @__PURE__ */ React8.createElement(TableRow4, { key: m.model_name, hover: true }, /* @__PURE__ */ React8.createElement(TableCell4, null, /* @__PURE__ */ React8.createElement(Box8, { sx: { display: "flex", alignItems: "center", gap: 0.5 } }, /* @__PURE__ */ React8.createElement(TagChip, { label: m.model_name, title: m.model_name }), /* @__PURE__ */ React8.createElement(Tooltip3, { title: "Copy model ID" }, /* @__PURE__ */ React8.createElement(
|
|
2975
|
+
Box8,
|
|
2561
2976
|
{
|
|
2562
2977
|
component: "span",
|
|
2563
2978
|
role: "button",
|
|
@@ -2567,9 +2982,9 @@ var init_ModelsTable = __esm({
|
|
|
2567
2982
|
sx: quietIconButtonSx("accent"),
|
|
2568
2983
|
style: { cursor: "pointer", display: "inline-flex" }
|
|
2569
2984
|
},
|
|
2570
|
-
/* @__PURE__ */
|
|
2571
|
-
)))), /* @__PURE__ */
|
|
2572
|
-
|
|
2985
|
+
/* @__PURE__ */ React8.createElement(ContentCopyIcon, { sx: { fontSize: 14 } })
|
|
2986
|
+
)))), /* @__PURE__ */ React8.createElement(TableCell4, null, /* @__PURE__ */ React8.createElement(
|
|
2987
|
+
Box8,
|
|
2573
2988
|
{
|
|
2574
2989
|
component: "span",
|
|
2575
2990
|
sx: {
|
|
@@ -2585,8 +3000,8 @@ var init_ModelsTable = __esm({
|
|
|
2585
3000
|
}
|
|
2586
3001
|
},
|
|
2587
3002
|
m.mode
|
|
2588
|
-
)), /* @__PURE__ */
|
|
2589
|
-
/* @__PURE__ */
|
|
3003
|
+
)), /* @__PURE__ */ React8.createElement(TableCell4, { align: "right" }, /* @__PURE__ */ React8.createElement(Typography8, { variant: "body2", sx: { fontVariantNumeric: "tabular-nums" } }, fmtCostPerToken(m.input_cost_per_token))), /* @__PURE__ */ React8.createElement(TableCell4, { align: "right" }, /* @__PURE__ */ React8.createElement(Typography8, { variant: "body2", sx: { fontVariantNumeric: "tabular-nums" } }, fmtCostPerToken(m.output_cost_per_token))), /* @__PURE__ */ React8.createElement(TableCell4, { align: "right" }, /* @__PURE__ */ React8.createElement(Typography8, { variant: "body2", sx: { fontVariantNumeric: "tabular-nums" } }, fmtTokens(m.max_input_tokens))), /* @__PURE__ */ React8.createElement(TableCell4, { align: "right" }, /* @__PURE__ */ React8.createElement(Typography8, { variant: "body2", sx: { fontVariantNumeric: "tabular-nums" } }, fmtTokens(m.max_output_tokens))), /* @__PURE__ */ React8.createElement(TableCell4, null, /* @__PURE__ */ React8.createElement(Box8, { sx: { display: "flex", gap: 0.5, flexWrap: "wrap" } }, m.supports_function_calling && /* @__PURE__ */ React8.createElement(TagChip, { label: "Fn", title: "Function calling" }), m.supports_vision && /* @__PURE__ */ React8.createElement(TagChip, { label: "\u{1F441}", title: "Vision" })))))))),
|
|
3004
|
+
/* @__PURE__ */ React8.createElement(
|
|
2590
3005
|
Snackbar,
|
|
2591
3006
|
{
|
|
2592
3007
|
open: copiedSnackbar,
|
|
@@ -2594,7 +3009,7 @@ var init_ModelsTable = __esm({
|
|
|
2594
3009
|
onClose: () => setCopiedSnackbar(false),
|
|
2595
3010
|
anchorOrigin: { vertical: "bottom", horizontal: "center" }
|
|
2596
3011
|
},
|
|
2597
|
-
/* @__PURE__ */
|
|
3012
|
+
/* @__PURE__ */ React8.createElement(Alert3, { severity: "success", variant: "filled", sx: { fontSize: 13 } }, "Model ID copied to clipboard")
|
|
2598
3013
|
)
|
|
2599
3014
|
);
|
|
2600
3015
|
};
|
|
@@ -2602,9 +3017,9 @@ var init_ModelsTable = __esm({
|
|
|
2602
3017
|
});
|
|
2603
3018
|
|
|
2604
3019
|
// src/components/AuditLog.tsx
|
|
2605
|
-
import
|
|
2606
|
-
import
|
|
2607
|
-
import
|
|
3020
|
+
import React9, { useState as useState7, useCallback as useCallback2 } from "react";
|
|
3021
|
+
import Box9 from "@mui/material/Box";
|
|
3022
|
+
import Typography9 from "@mui/material/Typography";
|
|
2608
3023
|
import Table5 from "@mui/material/Table";
|
|
2609
3024
|
import TableBody5 from "@mui/material/TableBody";
|
|
2610
3025
|
import TableCell5 from "@mui/material/TableCell";
|
|
@@ -2612,12 +3027,12 @@ import TableContainer5 from "@mui/material/TableContainer";
|
|
|
2612
3027
|
import TableHead5 from "@mui/material/TableHead";
|
|
2613
3028
|
import TableRow5 from "@mui/material/TableRow";
|
|
2614
3029
|
import TablePagination from "@mui/material/TablePagination";
|
|
2615
|
-
import
|
|
3030
|
+
import TextField5 from "@mui/material/TextField";
|
|
2616
3031
|
import MenuItem4 from "@mui/material/MenuItem";
|
|
2617
3032
|
import Skeleton5 from "@mui/material/Skeleton";
|
|
2618
3033
|
import Collapse2 from "@mui/material/Collapse";
|
|
2619
|
-
import
|
|
2620
|
-
import
|
|
3034
|
+
import IconButton5 from "@mui/material/IconButton";
|
|
3035
|
+
import Alert4 from "@mui/material/Alert";
|
|
2621
3036
|
import { alpha as alpha6 } from "@mui/material/styles";
|
|
2622
3037
|
import { KeyboardArrowDown } from "@mui/icons-material";
|
|
2623
3038
|
import { useAsync } from "react-use";
|
|
@@ -2641,10 +3056,10 @@ function formatDateTime(iso) {
|
|
|
2641
3056
|
}
|
|
2642
3057
|
function renderAuditLogBody(loading, entries) {
|
|
2643
3058
|
if (loading) {
|
|
2644
|
-
return /* @__PURE__ */
|
|
3059
|
+
return /* @__PURE__ */ React9.createElement(TableRow5, null, /* @__PURE__ */ React9.createElement(TableCell5, { colSpan: 6, sx: { py: 2 } }, /* @__PURE__ */ React9.createElement(Skeleton5, { variant: "rounded", height: 160 })));
|
|
2645
3060
|
}
|
|
2646
3061
|
if (entries.length === 0) {
|
|
2647
|
-
return /* @__PURE__ */
|
|
3062
|
+
return /* @__PURE__ */ React9.createElement(TableRow5, null, /* @__PURE__ */ React9.createElement(TableCell5, { colSpan: 6 }, /* @__PURE__ */ React9.createElement(
|
|
2648
3063
|
EmptyState,
|
|
2649
3064
|
{
|
|
2650
3065
|
message: "No audit events found",
|
|
@@ -2652,7 +3067,7 @@ function renderAuditLogBody(loading, entries) {
|
|
|
2652
3067
|
}
|
|
2653
3068
|
)));
|
|
2654
3069
|
}
|
|
2655
|
-
return entries.map((entry) => /* @__PURE__ */
|
|
3070
|
+
return entries.map((entry) => /* @__PURE__ */ React9.createElement(DetailRow, { key: entry.id, entry }));
|
|
2656
3071
|
}
|
|
2657
3072
|
var ACTION_TONES, TABLE_LABELS, DetailRow, AuditLog;
|
|
2658
3073
|
var init_AuditLog = __esm({
|
|
@@ -2669,13 +3084,15 @@ var init_AuditLog = __esm({
|
|
|
2669
3084
|
TABLE_LABELS = {
|
|
2670
3085
|
LiteLLM_VerificationToken: "Key",
|
|
2671
3086
|
LiteLLM_TeamTable: "Team",
|
|
3087
|
+
LiteLLM_TeamMembership: "Team member",
|
|
3088
|
+
LiteLLM_ObjectPermissionTable: "Team access (KB / MCP)",
|
|
2672
3089
|
LiteLLM_UserTable: "User"
|
|
2673
3090
|
};
|
|
2674
3091
|
DetailRow = ({ entry }) => {
|
|
2675
|
-
const [open, setOpen] =
|
|
3092
|
+
const [open, setOpen] = useState7(false);
|
|
2676
3093
|
const hasDetail = entry.before_value || entry.updated_values;
|
|
2677
|
-
return /* @__PURE__ */
|
|
2678
|
-
|
|
3094
|
+
return /* @__PURE__ */ React9.createElement(React9.Fragment, null, /* @__PURE__ */ React9.createElement(TableRow5, null, /* @__PURE__ */ React9.createElement(TableCell5, { sx: { width: 40, pr: 0 } }, hasDetail && /* @__PURE__ */ React9.createElement(
|
|
3095
|
+
IconButton5,
|
|
2679
3096
|
{
|
|
2680
3097
|
size: "small",
|
|
2681
3098
|
onClick: () => setOpen((o) => !o),
|
|
@@ -2687,9 +3104,9 @@ var init_AuditLog = __esm({
|
|
|
2687
3104
|
transition: theme.transitions.create("transform")
|
|
2688
3105
|
})
|
|
2689
3106
|
},
|
|
2690
|
-
/* @__PURE__ */
|
|
2691
|
-
)), /* @__PURE__ */
|
|
2692
|
-
|
|
3107
|
+
/* @__PURE__ */ React9.createElement(KeyboardArrowDown, { fontSize: "small" })
|
|
3108
|
+
)), /* @__PURE__ */ React9.createElement(TableCell5, { sx: { whiteSpace: "nowrap" } }, /* @__PURE__ */ React9.createElement(Typography9, { variant: "body2", color: "text.secondary" }, formatDateTime(entry.updated_at))), /* @__PURE__ */ React9.createElement(TableCell5, null, entry.action && /* @__PURE__ */ React9.createElement(StatusPill, { label: entry.action, tone: actionTone(entry.action) })), /* @__PURE__ */ React9.createElement(TableCell5, null, /* @__PURE__ */ React9.createElement(Typography9, { variant: "body2" }, prettyTableName(entry.table_name))), /* @__PURE__ */ React9.createElement(TableCell5, null, /* @__PURE__ */ React9.createElement(
|
|
3109
|
+
Typography9,
|
|
2693
3110
|
{
|
|
2694
3111
|
variant: "body2",
|
|
2695
3112
|
component: "code",
|
|
@@ -2698,8 +3115,8 @@ var init_AuditLog = __esm({
|
|
|
2698
3115
|
sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 12 }
|
|
2699
3116
|
},
|
|
2700
3117
|
entry.object_id ? entry.object_id.slice(0, 20) + (entry.object_id.length > 20 ? "\u2026" : "") : "\u2014"
|
|
2701
|
-
)), /* @__PURE__ */
|
|
2702
|
-
|
|
3118
|
+
)), /* @__PURE__ */ React9.createElement(TableCell5, null, entry.changed_by ?? "\u2014")), hasDetail && /* @__PURE__ */ React9.createElement(TableRow5, { sx: { "&&:hover": { bgcolor: "transparent" } } }, /* @__PURE__ */ React9.createElement(TableCell5, { colSpan: 6, sx: { "&&": { py: 0, border: 0 } } }, /* @__PURE__ */ React9.createElement(Collapse2, { in: open, unmountOnExit: true }, /* @__PURE__ */ React9.createElement(
|
|
3119
|
+
Box9,
|
|
2703
3120
|
{
|
|
2704
3121
|
display: "flex",
|
|
2705
3122
|
gap: 2,
|
|
@@ -2711,8 +3128,8 @@ var init_AuditLog = __esm({
|
|
|
2711
3128
|
bgcolor: alpha6(theme.palette.text.primary, theme.palette.mode === "dark" ? 0.05 : 0.03)
|
|
2712
3129
|
})
|
|
2713
3130
|
},
|
|
2714
|
-
entry.before_value && /* @__PURE__ */
|
|
2715
|
-
|
|
3131
|
+
entry.before_value && /* @__PURE__ */ React9.createElement(Box9, { flex: 1, minWidth: 200 }, /* @__PURE__ */ React9.createElement(
|
|
3132
|
+
Typography9,
|
|
2716
3133
|
{
|
|
2717
3134
|
variant: "caption",
|
|
2718
3135
|
color: "text.secondary",
|
|
@@ -2721,9 +3138,9 @@ var init_AuditLog = __esm({
|
|
|
2721
3138
|
sx: { fontSize: 10.5, fontWeight: 700, letterSpacing: "0.07em", textTransform: "uppercase" }
|
|
2722
3139
|
},
|
|
2723
3140
|
"Before"
|
|
2724
|
-
), /* @__PURE__ */
|
|
2725
|
-
entry.updated_values && /* @__PURE__ */
|
|
2726
|
-
|
|
3141
|
+
), /* @__PURE__ */ React9.createElement(Typography9, { component: "pre", variant: "caption", sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", whiteSpace: "pre-wrap", wordBreak: "break-all", m: 0 } }, JSON.stringify(entry.before_value, null, 2))),
|
|
3142
|
+
entry.updated_values && /* @__PURE__ */ React9.createElement(Box9, { flex: 1, minWidth: 200 }, /* @__PURE__ */ React9.createElement(
|
|
3143
|
+
Typography9,
|
|
2727
3144
|
{
|
|
2728
3145
|
variant: "caption",
|
|
2729
3146
|
color: "text.secondary",
|
|
@@ -2732,13 +3149,13 @@ var init_AuditLog = __esm({
|
|
|
2732
3149
|
sx: { fontSize: 10.5, fontWeight: 700, letterSpacing: "0.07em", textTransform: "uppercase" }
|
|
2733
3150
|
},
|
|
2734
3151
|
"After"
|
|
2735
|
-
), /* @__PURE__ */
|
|
3152
|
+
), /* @__PURE__ */ React9.createElement(Typography9, { component: "pre", variant: "caption", sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", whiteSpace: "pre-wrap", wordBreak: "break-all", m: 0 } }, JSON.stringify(entry.updated_values, null, 2)))
|
|
2736
3153
|
)))));
|
|
2737
3154
|
};
|
|
2738
3155
|
AuditLog = ({ api }) => {
|
|
2739
|
-
const [page, setPage] =
|
|
2740
|
-
const [pageSize, setPageSize] =
|
|
2741
|
-
const [filters, setFilters] =
|
|
3156
|
+
const [page, setPage] = useState7(0);
|
|
3157
|
+
const [pageSize, setPageSize] = useState7(25);
|
|
3158
|
+
const [filters, setFilters] = useState7({});
|
|
2742
3159
|
const fetchParams = useCallback2(
|
|
2743
3160
|
() => ({ page: page + 1, page_size: pageSize, ...filters }),
|
|
2744
3161
|
[page, pageSize, filters]
|
|
@@ -2749,14 +3166,14 @@ var init_AuditLog = __esm({
|
|
|
2749
3166
|
);
|
|
2750
3167
|
const entries = value?.audit_logs ?? [];
|
|
2751
3168
|
const total = value?.total ?? 0;
|
|
2752
|
-
return /* @__PURE__ */
|
|
3169
|
+
return /* @__PURE__ */ React9.createElement(
|
|
2753
3170
|
SectionCard,
|
|
2754
3171
|
{
|
|
2755
3172
|
title: "Audit Log",
|
|
2756
3173
|
subtitle: loading ? "Loading\u2026" : `${total.toLocaleString()} event${total === 1 ? "" : "s"}`,
|
|
2757
3174
|
flush: true,
|
|
2758
|
-
actions: /* @__PURE__ */
|
|
2759
|
-
|
|
3175
|
+
actions: /* @__PURE__ */ React9.createElement(React9.Fragment, null, /* @__PURE__ */ React9.createElement(
|
|
3176
|
+
TextField5,
|
|
2760
3177
|
{
|
|
2761
3178
|
size: "small",
|
|
2762
3179
|
label: "Action",
|
|
@@ -2768,13 +3185,13 @@ var init_AuditLog = __esm({
|
|
|
2768
3185
|
},
|
|
2769
3186
|
sx: { minWidth: 150 }
|
|
2770
3187
|
},
|
|
2771
|
-
/* @__PURE__ */
|
|
2772
|
-
/* @__PURE__ */
|
|
2773
|
-
/* @__PURE__ */
|
|
2774
|
-
/* @__PURE__ */
|
|
2775
|
-
/* @__PURE__ */
|
|
2776
|
-
), /* @__PURE__ */
|
|
2777
|
-
|
|
3188
|
+
/* @__PURE__ */ React9.createElement(MenuItem4, { value: "" }, "All actions"),
|
|
3189
|
+
/* @__PURE__ */ React9.createElement(MenuItem4, { value: "created" }, "Created"),
|
|
3190
|
+
/* @__PURE__ */ React9.createElement(MenuItem4, { value: "updated" }, "Updated"),
|
|
3191
|
+
/* @__PURE__ */ React9.createElement(MenuItem4, { value: "deleted" }, "Deleted"),
|
|
3192
|
+
/* @__PURE__ */ React9.createElement(MenuItem4, { value: "blocked" }, "Blocked")
|
|
3193
|
+
), /* @__PURE__ */ React9.createElement(
|
|
3194
|
+
TextField5,
|
|
2778
3195
|
{
|
|
2779
3196
|
size: "small",
|
|
2780
3197
|
label: "Table",
|
|
@@ -2786,12 +3203,14 @@ var init_AuditLog = __esm({
|
|
|
2786
3203
|
},
|
|
2787
3204
|
sx: { minWidth: 150 }
|
|
2788
3205
|
},
|
|
2789
|
-
/* @__PURE__ */
|
|
2790
|
-
/* @__PURE__ */
|
|
2791
|
-
/* @__PURE__ */
|
|
2792
|
-
/* @__PURE__ */
|
|
2793
|
-
|
|
2794
|
-
|
|
3206
|
+
/* @__PURE__ */ React9.createElement(MenuItem4, { value: "" }, "All tables"),
|
|
3207
|
+
/* @__PURE__ */ React9.createElement(MenuItem4, { value: "LiteLLM_VerificationToken" }, "Key"),
|
|
3208
|
+
/* @__PURE__ */ React9.createElement(MenuItem4, { value: "LiteLLM_TeamTable" }, "Team"),
|
|
3209
|
+
/* @__PURE__ */ React9.createElement(MenuItem4, { value: "LiteLLM_TeamMembership" }, "Team member"),
|
|
3210
|
+
/* @__PURE__ */ React9.createElement(MenuItem4, { value: "LiteLLM_ObjectPermissionTable" }, "Team access (KB / MCP)"),
|
|
3211
|
+
/* @__PURE__ */ React9.createElement(MenuItem4, { value: "LiteLLM_UserTable" }, "User")
|
|
3212
|
+
), /* @__PURE__ */ React9.createElement(
|
|
3213
|
+
TextField5,
|
|
2795
3214
|
{
|
|
2796
3215
|
size: "small",
|
|
2797
3216
|
label: "Changed by",
|
|
@@ -2804,9 +3223,9 @@ var init_AuditLog = __esm({
|
|
|
2804
3223
|
}
|
|
2805
3224
|
))
|
|
2806
3225
|
},
|
|
2807
|
-
error && /* @__PURE__ */
|
|
2808
|
-
/* @__PURE__ */
|
|
2809
|
-
/* @__PURE__ */
|
|
3226
|
+
error && /* @__PURE__ */ React9.createElement(Box9, { px: 2.5, pb: 2 }, /* @__PURE__ */ React9.createElement(Alert4, { severity: "error" }, error.message)),
|
|
3227
|
+
/* @__PURE__ */ React9.createElement(TableContainer5, null, /* @__PURE__ */ React9.createElement(Table5, { size: "small", sx: dataTableSx }, /* @__PURE__ */ React9.createElement(TableHead5, null, /* @__PURE__ */ React9.createElement(TableRow5, null, /* @__PURE__ */ React9.createElement(TableCell5, { sx: { width: 40 } }), /* @__PURE__ */ React9.createElement(TableCell5, null, "Time"), /* @__PURE__ */ React9.createElement(TableCell5, null, "Action"), /* @__PURE__ */ React9.createElement(TableCell5, null, "Table"), /* @__PURE__ */ React9.createElement(TableCell5, null, "Object ID"), /* @__PURE__ */ React9.createElement(TableCell5, null, "Changed By"))), /* @__PURE__ */ React9.createElement(TableBody5, null, renderAuditLogBody(loading, entries)))),
|
|
3228
|
+
/* @__PURE__ */ React9.createElement(
|
|
2810
3229
|
TablePagination,
|
|
2811
3230
|
{
|
|
2812
3231
|
component: "div",
|
|
@@ -2827,22 +3246,53 @@ var init_AuditLog = __esm({
|
|
|
2827
3246
|
}
|
|
2828
3247
|
});
|
|
2829
3248
|
|
|
3249
|
+
// src/permissions.ts
|
|
3250
|
+
import { createPermission } from "@backstage/plugin-permission-common";
|
|
3251
|
+
var litellmTeamCreatePermission, litellmTeamManagePermission, litellmTeamMembersManagePermission, litellmTeamKnowledgebaseManagePermission, litellmTeamMcpManagePermission;
|
|
3252
|
+
var init_permissions = __esm({
|
|
3253
|
+
"src/permissions.ts"() {
|
|
3254
|
+
"use strict";
|
|
3255
|
+
litellmTeamCreatePermission = createPermission({
|
|
3256
|
+
name: "litellm.team.create",
|
|
3257
|
+
attributes: { action: "create" }
|
|
3258
|
+
});
|
|
3259
|
+
litellmTeamManagePermission = createPermission({
|
|
3260
|
+
name: "litellm.team.manage",
|
|
3261
|
+
attributes: { action: "update" }
|
|
3262
|
+
});
|
|
3263
|
+
litellmTeamMembersManagePermission = createPermission({
|
|
3264
|
+
name: "litellm.team.members.manage",
|
|
3265
|
+
attributes: { action: "update" }
|
|
3266
|
+
});
|
|
3267
|
+
litellmTeamKnowledgebaseManagePermission = createPermission({
|
|
3268
|
+
name: "litellm.team.knowledgebase.manage",
|
|
3269
|
+
attributes: { action: "update" }
|
|
3270
|
+
});
|
|
3271
|
+
litellmTeamMcpManagePermission = createPermission({
|
|
3272
|
+
name: "litellm.team.mcp.manage",
|
|
3273
|
+
attributes: { action: "update" }
|
|
3274
|
+
});
|
|
3275
|
+
}
|
|
3276
|
+
});
|
|
3277
|
+
|
|
2830
3278
|
// src/components/LiteLLMPage.tsx
|
|
2831
3279
|
var LiteLLMPage_exports = {};
|
|
2832
3280
|
__export(LiteLLMPage_exports, {
|
|
2833
3281
|
LiteLLMPage: () => LiteLLMPage
|
|
2834
3282
|
});
|
|
2835
|
-
import
|
|
2836
|
-
import
|
|
3283
|
+
import React10, { useState as useState8, useCallback as useCallback3, useMemo as useMemo6 } from "react";
|
|
3284
|
+
import Box10 from "@mui/material/Box";
|
|
2837
3285
|
import Snackbar2 from "@mui/material/Snackbar";
|
|
2838
|
-
import
|
|
3286
|
+
import Alert5 from "@mui/material/Alert";
|
|
2839
3287
|
import CircularProgress4 from "@mui/material/CircularProgress";
|
|
2840
|
-
import
|
|
3288
|
+
import Typography10 from "@mui/material/Typography";
|
|
2841
3289
|
import Paper5 from "@mui/material/Paper";
|
|
2842
3290
|
import Tabs2 from "@mui/material/Tabs";
|
|
2843
3291
|
import Tab2 from "@mui/material/Tab";
|
|
3292
|
+
import Button6 from "@mui/material/Button";
|
|
2844
3293
|
import { useAsync as useAsync2, useAsyncRetry } from "react-use";
|
|
2845
3294
|
import { useApi } from "@backstage/core-plugin-api";
|
|
3295
|
+
import { usePermission } from "@backstage/plugin-permission-react";
|
|
2846
3296
|
function initDateRange() {
|
|
2847
3297
|
let preset = "7d";
|
|
2848
3298
|
try {
|
|
@@ -2864,27 +3314,30 @@ var init_LiteLLMPage = __esm({
|
|
|
2864
3314
|
init_DashboardHeader();
|
|
2865
3315
|
init_KeysTable();
|
|
2866
3316
|
init_GenerateKeyDialog();
|
|
3317
|
+
init_ManageTeamDialog();
|
|
2867
3318
|
init_UsageStats();
|
|
2868
3319
|
init_TeamUsage();
|
|
2869
3320
|
init_ModelsTable();
|
|
2870
3321
|
init_AuditLog();
|
|
2871
3322
|
init_api();
|
|
3323
|
+
init_permissions();
|
|
2872
3324
|
PERIOD_LS_KEY2 = "litellm_usage_period";
|
|
2873
3325
|
LiteLLMPage = () => {
|
|
2874
3326
|
const api = useApi(liteLlmApiRef);
|
|
2875
|
-
const [dateRange, setDateRange] =
|
|
2876
|
-
const [currentPreset, setCurrentPreset] =
|
|
3327
|
+
const [dateRange, setDateRange] = useState8(initDateRange);
|
|
3328
|
+
const [currentPreset, setCurrentPreset] = useState8(() => {
|
|
2877
3329
|
try {
|
|
2878
3330
|
return localStorage.getItem(PERIOD_LS_KEY2) ?? "7d";
|
|
2879
3331
|
} catch {
|
|
2880
3332
|
return "7d";
|
|
2881
3333
|
}
|
|
2882
3334
|
});
|
|
2883
|
-
const [activeTab, setActiveTab] =
|
|
2884
|
-
const [snackbar, setSnackbar] =
|
|
2885
|
-
const [generateDialogOpen, setGenerateDialogOpen] =
|
|
2886
|
-
const [
|
|
2887
|
-
const [
|
|
3335
|
+
const [activeTab, setActiveTab] = useState8("overview");
|
|
3336
|
+
const [snackbar, setSnackbar] = useState8(null);
|
|
3337
|
+
const [generateDialogOpen, setGenerateDialogOpen] = useState8(false);
|
|
3338
|
+
const [manageTeam, setManageTeam] = useState8(null);
|
|
3339
|
+
const [teamUsageCache, setTeamUsageCache] = useState8({});
|
|
3340
|
+
const [teamUsageLoading, setTeamUsageLoading] = useState8({});
|
|
2888
3341
|
const { value: userInfo, loading: userLoading, error: userError } = useAsync2(
|
|
2889
3342
|
() => api.getUserInfo(),
|
|
2890
3343
|
[api]
|
|
@@ -2904,11 +3357,30 @@ var init_LiteLLMPage = __esm({
|
|
|
2904
3357
|
() => api.listModels().catch(() => []),
|
|
2905
3358
|
[api]
|
|
2906
3359
|
);
|
|
2907
|
-
const { value: allTeams, loading: teamsLoading } =
|
|
3360
|
+
const { value: allTeams, loading: teamsLoading, retry: refreshTeams } = useAsyncRetry(
|
|
2908
3361
|
() => api.getTeams().catch(() => []),
|
|
2909
3362
|
[api]
|
|
2910
3363
|
);
|
|
2911
3364
|
const { value: liteLlmConfig } = useAsync2(() => api.getConfig(), [api]);
|
|
3365
|
+
const { value: managedTeams } = useAsync2(
|
|
3366
|
+
async () => liteLlmConfig?.teamManagement?.enabled ? api.getManagedTeams().catch(() => []) : [],
|
|
3367
|
+
[api, liteLlmConfig]
|
|
3368
|
+
);
|
|
3369
|
+
const { allowed: canCreateTeam } = usePermission({ permission: litellmTeamCreatePermission });
|
|
3370
|
+
const { allowed: canManageTeam } = usePermission({ permission: litellmTeamManagePermission });
|
|
3371
|
+
const { allowed: canManageMembers } = usePermission({ permission: litellmTeamMembersManagePermission });
|
|
3372
|
+
const { allowed: canManageKnowledgeBases } = usePermission({ permission: litellmTeamKnowledgebaseManagePermission });
|
|
3373
|
+
const { allowed: canManageMcpServers } = usePermission({ permission: litellmTeamMcpManagePermission });
|
|
3374
|
+
const teamMgmtEnabled = liteLlmConfig?.teamManagement?.enabled ?? false;
|
|
3375
|
+
const objectPermsEnabled = liteLlmConfig?.teamManagement?.objectPermissionsEnabled ?? false;
|
|
3376
|
+
const { value: vectorStores } = useAsync2(
|
|
3377
|
+
async () => objectPermsEnabled ? api.getVectorStores().catch(() => []) : [],
|
|
3378
|
+
[api, objectPermsEnabled]
|
|
3379
|
+
);
|
|
3380
|
+
const { value: mcpServers } = useAsync2(
|
|
3381
|
+
async () => objectPermsEnabled ? api.getMcpServers().catch(() => []) : [],
|
|
3382
|
+
[api, objectPermsEnabled]
|
|
3383
|
+
);
|
|
2912
3384
|
const teams = useMemo6(() => {
|
|
2913
3385
|
if (!allTeams?.length) return [];
|
|
2914
3386
|
if (!userInfo) return allTeams;
|
|
@@ -3056,14 +3528,14 @@ var init_LiteLLMPage = __esm({
|
|
|
3056
3528
|
);
|
|
3057
3529
|
const isInitialLoading = userLoading && !userInfo;
|
|
3058
3530
|
if (isInitialLoading) {
|
|
3059
|
-
return /* @__PURE__ */
|
|
3531
|
+
return /* @__PURE__ */ React10.createElement(Box10, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "50vh" }, /* @__PURE__ */ React10.createElement(CircularProgress4, null));
|
|
3060
3532
|
}
|
|
3061
3533
|
if (userError || !userInfo) {
|
|
3062
3534
|
const isProvisioningEnabled = userError?.body?.provisioning === true;
|
|
3063
3535
|
const hint = userError?.body?.hint;
|
|
3064
|
-
return /* @__PURE__ */
|
|
3536
|
+
return /* @__PURE__ */ React10.createElement(Box10, { p: 3 }, /* @__PURE__ */ React10.createElement(Paper5, { sx: { p: 3 } }, /* @__PURE__ */ React10.createElement(Typography10, { variant: "h6", gutterBottom: true }, "Account not provisioned"), /* @__PURE__ */ React10.createElement(Typography10, { color: "text.secondary", paragraph: true }, "Your Backstage account is not linked to a LiteLLM user."), hint ? /* @__PURE__ */ React10.createElement(Typography10, { variant: "body2", color: "text.secondary" }, hint) : /* @__PURE__ */ React10.createElement(Typography10, { variant: "body2", color: "text.secondary" }, isProvisioningEnabled ? "Auto-provisioning is enabled but failed. Check the backend logs." : "Set litellm.provisioning.enabled: true in app-config.yaml to enable auto-provisioning, or ask your administrator to create the account manually.")));
|
|
3065
3537
|
}
|
|
3066
|
-
const pageTabs = /* @__PURE__ */
|
|
3538
|
+
const pageTabs = /* @__PURE__ */ React10.createElement(
|
|
3067
3539
|
Tabs2,
|
|
3068
3540
|
{
|
|
3069
3541
|
value: activeTab,
|
|
@@ -3076,13 +3548,13 @@ var init_LiteLLMPage = __esm({
|
|
|
3076
3548
|
'& [class*="MuiTab-root"]': { minHeight: 44, textTransform: "none", fontSize: 14 }
|
|
3077
3549
|
}
|
|
3078
3550
|
},
|
|
3079
|
-
/* @__PURE__ */
|
|
3080
|
-
/* @__PURE__ */
|
|
3081
|
-
/* @__PURE__ */
|
|
3082
|
-
/* @__PURE__ */
|
|
3083
|
-
userInfo.can_view_audit && /* @__PURE__ */
|
|
3551
|
+
/* @__PURE__ */ React10.createElement(Tab2, { label: "Overview", value: "overview" }),
|
|
3552
|
+
/* @__PURE__ */ React10.createElement(Tab2, { label: "Keys", value: "keys" }),
|
|
3553
|
+
/* @__PURE__ */ React10.createElement(Tab2, { label: "Teams", value: "teams" }),
|
|
3554
|
+
/* @__PURE__ */ React10.createElement(Tab2, { label: "Models", value: "models" }),
|
|
3555
|
+
userInfo.can_view_audit && /* @__PURE__ */ React10.createElement(Tab2, { label: "Audit Log", value: "audit" })
|
|
3084
3556
|
);
|
|
3085
|
-
return /* @__PURE__ */
|
|
3557
|
+
return /* @__PURE__ */ React10.createElement(Box10, { sx: { p: 3, display: "flex", flexDirection: "column", gap: 2 } }, /* @__PURE__ */ React10.createElement(
|
|
3086
3558
|
DashboardHeader,
|
|
3087
3559
|
{
|
|
3088
3560
|
userInfo,
|
|
@@ -3092,7 +3564,7 @@ var init_LiteLLMPage = __esm({
|
|
|
3092
3564
|
onGenerateKeyClick: () => setGenerateDialogOpen(true),
|
|
3093
3565
|
tabs: pageTabs
|
|
3094
3566
|
}
|
|
3095
|
-
), activeTab === "overview" && /* @__PURE__ */
|
|
3567
|
+
), activeTab === "overview" && /* @__PURE__ */ React10.createElement(
|
|
3096
3568
|
UsageStats,
|
|
3097
3569
|
{
|
|
3098
3570
|
usage: usage ?? null,
|
|
@@ -3103,7 +3575,7 @@ var init_LiteLLMPage = __esm({
|
|
|
3103
3575
|
loading: usageLoading,
|
|
3104
3576
|
userInfo
|
|
3105
3577
|
}
|
|
3106
|
-
), activeTab === "keys" && /* @__PURE__ */
|
|
3578
|
+
), activeTab === "keys" && /* @__PURE__ */ React10.createElement(
|
|
3107
3579
|
KeysTable,
|
|
3108
3580
|
{
|
|
3109
3581
|
keys: keys ?? [],
|
|
@@ -3117,25 +3589,40 @@ var init_LiteLLMPage = __esm({
|
|
|
3117
3589
|
onDeleteKey: handleDeleteKey,
|
|
3118
3590
|
onPruneExpiredKeys: handlePruneExpiredKeys
|
|
3119
3591
|
}
|
|
3120
|
-
), activeTab === "teams" &&
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3592
|
+
), activeTab === "teams" && (() => {
|
|
3593
|
+
const teamsById = /* @__PURE__ */ new Map();
|
|
3594
|
+
for (const t of managedTeams ?? []) teamsById.set(t.team_id, t);
|
|
3595
|
+
for (const t of teams ?? []) teamsById.set(t.team_id, t);
|
|
3596
|
+
const visibleTeams = Array.from(teamsById.values());
|
|
3597
|
+
return /* @__PURE__ */ React10.createElement(Box10, { sx: { display: "flex", flexDirection: "column", gap: 2 } }, teamMgmtEnabled && canCreateTeam && /* @__PURE__ */ React10.createElement(Box10, { sx: { display: "flex", justifyContent: "flex-end" } }, /* @__PURE__ */ React10.createElement(
|
|
3598
|
+
Button6,
|
|
3599
|
+
{
|
|
3600
|
+
variant: "contained",
|
|
3601
|
+
onClick: () => setManageTeam({ mode: "create" })
|
|
3128
3602
|
},
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3603
|
+
"Create Team"
|
|
3604
|
+
)), /* @__PURE__ */ React10.createElement(
|
|
3605
|
+
TeamUsage,
|
|
3606
|
+
{
|
|
3607
|
+
teams: visibleTeams,
|
|
3608
|
+
loading: teamsLoading,
|
|
3609
|
+
getTeamUsage: (teamId) => {
|
|
3610
|
+
if (teamUsageCache[teamId] === void 0) loadTeamUsage(teamId);
|
|
3611
|
+
return teamUsageCache[teamId] ?? null;
|
|
3612
|
+
},
|
|
3613
|
+
getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false,
|
|
3614
|
+
canManage: teamMgmtEnabled && canManageTeam,
|
|
3615
|
+
onEditTeam: (t) => setManageTeam({ mode: "edit", team: t })
|
|
3616
|
+
}
|
|
3617
|
+
));
|
|
3618
|
+
})(), activeTab === "models" && /* @__PURE__ */ React10.createElement(
|
|
3132
3619
|
ModelsTable,
|
|
3133
3620
|
{
|
|
3134
3621
|
allModels: allModels ?? [],
|
|
3135
3622
|
teams: teams ?? [],
|
|
3136
3623
|
loading: modelsLoading
|
|
3137
3624
|
}
|
|
3138
|
-
), activeTab === "audit" && userInfo.can_view_audit && /* @__PURE__ */
|
|
3625
|
+
), activeTab === "audit" && userInfo.can_view_audit && /* @__PURE__ */ React10.createElement(AuditLog, { api }), /* @__PURE__ */ React10.createElement(
|
|
3139
3626
|
GenerateKeyDialog,
|
|
3140
3627
|
{
|
|
3141
3628
|
open: generateDialogOpen,
|
|
@@ -3148,7 +3635,69 @@ var init_LiteLLMPage = __esm({
|
|
|
3148
3635
|
onGenerateKey: handleGenerateKey,
|
|
3149
3636
|
onGetConfig: () => api.getConfig()
|
|
3150
3637
|
}
|
|
3151
|
-
), /* @__PURE__ */
|
|
3638
|
+
), /* @__PURE__ */ React10.createElement(
|
|
3639
|
+
ManageTeamDialog,
|
|
3640
|
+
{
|
|
3641
|
+
open: !!manageTeam,
|
|
3642
|
+
onClose: () => setManageTeam(null),
|
|
3643
|
+
mode: manageTeam?.mode ?? "create",
|
|
3644
|
+
team: manageTeam?.team,
|
|
3645
|
+
allModels: allModels ?? [],
|
|
3646
|
+
config: liteLlmConfig,
|
|
3647
|
+
onSubmit: async (payload) => {
|
|
3648
|
+
if (manageTeam?.mode === "edit" && manageTeam.team) {
|
|
3649
|
+
await api.updateTeam(manageTeam.team.team_id, payload);
|
|
3650
|
+
} else {
|
|
3651
|
+
await api.createTeam(payload);
|
|
3652
|
+
}
|
|
3653
|
+
setSnackbar({ message: "Team saved", severity: "success" });
|
|
3654
|
+
refreshTeams();
|
|
3655
|
+
},
|
|
3656
|
+
canManageMembers: teamMgmtEnabled && canManageMembers,
|
|
3657
|
+
onAddMember: async (userEntityRef, maxBudgetInTeam) => {
|
|
3658
|
+
if (!manageTeam?.team) return;
|
|
3659
|
+
const updated = await api.addTeamMember(manageTeam.team.team_id, {
|
|
3660
|
+
userEntityRef,
|
|
3661
|
+
...maxBudgetInTeam ? { maxBudgetInTeam } : {}
|
|
3662
|
+
});
|
|
3663
|
+
setManageTeam((s) => s && s.team ? { ...s, team: updated } : s);
|
|
3664
|
+
refreshTeams();
|
|
3665
|
+
},
|
|
3666
|
+
onRemoveMember: async (userEntityRef) => {
|
|
3667
|
+
if (!manageTeam?.team) return;
|
|
3668
|
+
const updated = await api.removeTeamMember(
|
|
3669
|
+
manageTeam.team.team_id,
|
|
3670
|
+
userEntityRef
|
|
3671
|
+
);
|
|
3672
|
+
setManageTeam((s) => s && s.team ? { ...s, team: updated } : s);
|
|
3673
|
+
refreshTeams();
|
|
3674
|
+
},
|
|
3675
|
+
canManageKnowledgeBases: teamMgmtEnabled && objectPermsEnabled && canManageKnowledgeBases,
|
|
3676
|
+
vectorStores: vectorStores ?? [],
|
|
3677
|
+
onSaveKnowledgeBases: async (vectorStoreIds) => {
|
|
3678
|
+
if (!manageTeam?.team) return;
|
|
3679
|
+
const updated = await api.setTeamKnowledgeBases(
|
|
3680
|
+
manageTeam.team.team_id,
|
|
3681
|
+
vectorStoreIds
|
|
3682
|
+
);
|
|
3683
|
+
setManageTeam((s) => s && s.team ? { ...s, team: updated } : s);
|
|
3684
|
+
setSnackbar({ message: "Knowledge bases updated", severity: "success" });
|
|
3685
|
+
refreshTeams();
|
|
3686
|
+
},
|
|
3687
|
+
canManageMcpServers: teamMgmtEnabled && objectPermsEnabled && canManageMcpServers,
|
|
3688
|
+
mcpServers: mcpServers ?? [],
|
|
3689
|
+
onSaveMcpServers: async (mcpServerIds) => {
|
|
3690
|
+
if (!manageTeam?.team) return;
|
|
3691
|
+
const updated = await api.setTeamMcpServers(
|
|
3692
|
+
manageTeam.team.team_id,
|
|
3693
|
+
mcpServerIds
|
|
3694
|
+
);
|
|
3695
|
+
setManageTeam((s) => s && s.team ? { ...s, team: updated } : s);
|
|
3696
|
+
setSnackbar({ message: "MCP servers updated", severity: "success" });
|
|
3697
|
+
refreshTeams();
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
), /* @__PURE__ */ React10.createElement(
|
|
3152
3701
|
Snackbar2,
|
|
3153
3702
|
{
|
|
3154
3703
|
open: !!snackbar,
|
|
@@ -3156,7 +3705,7 @@ var init_LiteLLMPage = __esm({
|
|
|
3156
3705
|
onClose: () => setSnackbar(null),
|
|
3157
3706
|
anchorOrigin: { vertical: "bottom", horizontal: "right" }
|
|
3158
3707
|
},
|
|
3159
|
-
snackbar ? /* @__PURE__ */
|
|
3708
|
+
snackbar ? /* @__PURE__ */ React10.createElement(Alert5, { severity: snackbar.severity, onClose: () => setSnackbar(null) }, snackbar.message) : void 0
|
|
3160
3709
|
));
|
|
3161
3710
|
};
|
|
3162
3711
|
}
|
|
@@ -3164,7 +3713,7 @@ var init_LiteLLMPage = __esm({
|
|
|
3164
3713
|
|
|
3165
3714
|
// src/plugin.tsx
|
|
3166
3715
|
init_api();
|
|
3167
|
-
import
|
|
3716
|
+
import React11 from "react";
|
|
3168
3717
|
import { TrendingUp as TrendingUpIcon } from "@mui/icons-material";
|
|
3169
3718
|
import {
|
|
3170
3719
|
createFrontendPlugin,
|
|
@@ -3183,10 +3732,10 @@ var liteLlmPage = PageBlueprint.make({
|
|
|
3183
3732
|
params: {
|
|
3184
3733
|
path: "/litellm",
|
|
3185
3734
|
title: "LiteLLM",
|
|
3186
|
-
icon: /* @__PURE__ */
|
|
3735
|
+
icon: /* @__PURE__ */ React11.createElement(TrendingUpIcon, null),
|
|
3187
3736
|
loader: async () => {
|
|
3188
3737
|
const { LiteLLMPage: LiteLLMPage2 } = await Promise.resolve().then(() => (init_LiteLLMPage(), LiteLLMPage_exports));
|
|
3189
|
-
return /* @__PURE__ */
|
|
3738
|
+
return /* @__PURE__ */ React11.createElement(LiteLLMPage2, null);
|
|
3190
3739
|
}
|
|
3191
3740
|
}
|
|
3192
3741
|
});
|
|
@@ -3205,16 +3754,16 @@ init_TeamUsage();
|
|
|
3205
3754
|
// src/components/LiteLLMHomeWidget.tsx
|
|
3206
3755
|
init_api();
|
|
3207
3756
|
init_format();
|
|
3208
|
-
import
|
|
3757
|
+
import React12, { useState as useState9, useEffect as useEffect3 } from "react";
|
|
3209
3758
|
import Paper6 from "@mui/material/Paper";
|
|
3210
|
-
import
|
|
3211
|
-
import
|
|
3759
|
+
import Box11 from "@mui/material/Box";
|
|
3760
|
+
import Typography11 from "@mui/material/Typography";
|
|
3212
3761
|
import FormControl2 from "@mui/material/FormControl";
|
|
3213
3762
|
import Select2 from "@mui/material/Select";
|
|
3214
3763
|
import MenuItem5 from "@mui/material/MenuItem";
|
|
3215
3764
|
import Grid2 from "@mui/material/Grid";
|
|
3216
3765
|
import CircularProgress5 from "@mui/material/CircularProgress";
|
|
3217
|
-
import
|
|
3766
|
+
import Alert6 from "@mui/material/Alert";
|
|
3218
3767
|
import { AreaChart as AreaChart3, Area as Area3, ResponsiveContainer as ResponsiveContainer3 } from "recharts";
|
|
3219
3768
|
import { useApi as useApi2 } from "@backstage/core-plugin-api";
|
|
3220
3769
|
function presetToDateRange(preset) {
|
|
@@ -3229,19 +3778,19 @@ function presetToDateRange(preset) {
|
|
|
3229
3778
|
}
|
|
3230
3779
|
return { start, end };
|
|
3231
3780
|
}
|
|
3232
|
-
var Kpi = ({ label, value }) => /* @__PURE__ */
|
|
3781
|
+
var Kpi = ({ label, value }) => /* @__PURE__ */ React12.createElement(Box11, null, /* @__PURE__ */ React12.createElement(Typography11, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ React12.createElement(Typography11, { variant: "subtitle1", fontWeight: 600 }, value));
|
|
3233
3782
|
var LiteLLMHomeWidget = ({
|
|
3234
3783
|
defaultPeriod = "7d",
|
|
3235
3784
|
title = "LiteLLM Usage"
|
|
3236
3785
|
}) => {
|
|
3237
3786
|
const api = useApi2(liteLlmApiRef);
|
|
3238
|
-
const [period, setPeriod] =
|
|
3239
|
-
const [loading, setLoading] =
|
|
3240
|
-
const [usageError, setUsageError] =
|
|
3241
|
-
const [keysError, setKeysError] =
|
|
3242
|
-
const [usage, setUsage] =
|
|
3243
|
-
const [keys, setKeys] =
|
|
3244
|
-
|
|
3787
|
+
const [period, setPeriod] = useState9(defaultPeriod);
|
|
3788
|
+
const [loading, setLoading] = useState9(true);
|
|
3789
|
+
const [usageError, setUsageError] = useState9(null);
|
|
3790
|
+
const [keysError, setKeysError] = useState9(null);
|
|
3791
|
+
const [usage, setUsage] = useState9(null);
|
|
3792
|
+
const [keys, setKeys] = useState9([]);
|
|
3793
|
+
useEffect3(() => {
|
|
3245
3794
|
let cancelled = false;
|
|
3246
3795
|
setLoading(true);
|
|
3247
3796
|
setUsageError(null);
|
|
@@ -3278,17 +3827,17 @@ var LiteLLMHomeWidget = ({
|
|
|
3278
3827
|
spend: d.spend
|
|
3279
3828
|
}));
|
|
3280
3829
|
const hasSparkline = dailyData.length > 0;
|
|
3281
|
-
return /* @__PURE__ */
|
|
3830
|
+
return /* @__PURE__ */ React12.createElement(Paper6, { sx: { p: 2 } }, /* @__PURE__ */ React12.createElement(Box11, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1.5 }, /* @__PURE__ */ React12.createElement(Typography11, { variant: "h6" }, title), /* @__PURE__ */ React12.createElement(FormControl2, { size: "small", sx: { minWidth: 90 } }, /* @__PURE__ */ React12.createElement(
|
|
3282
3831
|
Select2,
|
|
3283
3832
|
{
|
|
3284
3833
|
value: period,
|
|
3285
3834
|
onChange: (e) => setPeriod(e.target.value),
|
|
3286
3835
|
displayEmpty: true
|
|
3287
3836
|
},
|
|
3288
|
-
/* @__PURE__ */
|
|
3289
|
-
/* @__PURE__ */
|
|
3290
|
-
/* @__PURE__ */
|
|
3291
|
-
))), loading && /* @__PURE__ */
|
|
3837
|
+
/* @__PURE__ */ React12.createElement(MenuItem5, { value: "today" }, "Today"),
|
|
3838
|
+
/* @__PURE__ */ React12.createElement(MenuItem5, { value: "7d" }, "7d"),
|
|
3839
|
+
/* @__PURE__ */ React12.createElement(MenuItem5, { value: "30d" }, "30d")
|
|
3840
|
+
))), loading && /* @__PURE__ */ React12.createElement(Box11, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ React12.createElement(CircularProgress5, { size: 32 })), !loading && totalFailure && /* @__PURE__ */ React12.createElement(Alert6, { severity: "error", sx: { mt: 1 } }, usageError ?? "Failed to load usage data"), !loading && !totalFailure && /* @__PURE__ */ React12.createElement(React12.Fragment, null, partialFailure && /* @__PURE__ */ React12.createElement(Alert6, { severity: "warning", sx: { mt: 1, mb: 1 } }, usageError ? `Usage data unavailable (${usageError}).` : "", keysError ? ` Key list unavailable (${keysError}).` : "", " Showing what loaded."), /* @__PURE__ */ React12.createElement(Grid2, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ React12.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React12.createElement(Kpi, { label: "USD Spent", value: fmtUsd(usage?.total_spend ?? 0) })), /* @__PURE__ */ React12.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React12.createElement(Kpi, { label: "Tokens In", value: fmtInt(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ React12.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React12.createElement(Kpi, { label: "Tokens Out", value: fmtInt(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ React12.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React12.createElement(Kpi, { label: "Keys", value: fmtInt(keys.length) }))), hasSparkline && /* @__PURE__ */ React12.createElement(Box11, { height: 120 }, /* @__PURE__ */ React12.createElement(ResponsiveContainer3, { width: "100%", height: "100%" }, /* @__PURE__ */ React12.createElement(AreaChart3, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 } }, /* @__PURE__ */ React12.createElement(
|
|
3292
3841
|
Area3,
|
|
3293
3842
|
{
|
|
3294
3843
|
type: "monotone",
|
|
@@ -3304,7 +3853,9 @@ var LiteLLMHomeWidget = ({
|
|
|
3304
3853
|
|
|
3305
3854
|
// src/index.ts
|
|
3306
3855
|
init_GenerateKeyDialog();
|
|
3856
|
+
init_ManageTeamDialog();
|
|
3307
3857
|
init_api();
|
|
3858
|
+
init_permissions();
|
|
3308
3859
|
export {
|
|
3309
3860
|
DashboardHeader,
|
|
3310
3861
|
GenerateKeyDialog,
|
|
@@ -3312,9 +3863,15 @@ export {
|
|
|
3312
3863
|
LiteLLMHomeWidget,
|
|
3313
3864
|
LiteLLMPage,
|
|
3314
3865
|
LiteLlmApi,
|
|
3866
|
+
ManageTeamDialog,
|
|
3315
3867
|
TeamUsage,
|
|
3316
3868
|
UsageStats,
|
|
3317
3869
|
liteLlmApiRef,
|
|
3318
|
-
litellmPlugin
|
|
3870
|
+
litellmPlugin,
|
|
3871
|
+
litellmTeamCreatePermission,
|
|
3872
|
+
litellmTeamKnowledgebaseManagePermission,
|
|
3873
|
+
litellmTeamManagePermission,
|
|
3874
|
+
litellmTeamMcpManagePermission,
|
|
3875
|
+
litellmTeamMembersManagePermission
|
|
3319
3876
|
};
|
|
3320
3877
|
//# sourceMappingURL=index.esm.js.map
|