@acarmisc/backstage-plugin-litellm 0.18.1 → 0.20.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/budget.d.ts +45 -0
- package/dist/components/LiteLLMBudgetWidget.d.ts +22 -0
- package/dist/index.cjs.js +312 -18
- package/dist/index.cjs.js.map +4 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.esm.js +311 -17
- package/dist/index.esm.js.map +4 -4
- package/dist/types.d.ts +3 -0
- package/package.json +1 -1
- package/dist/components/SearchInput.d.ts +0 -9
- /package/dist/{SearchInput.d.ts → budget.test.d.ts} +0 -0
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export { UsageStats } from './components/UsageStats';
|
|
|
6
6
|
export { TeamUsage } from './components/TeamUsage';
|
|
7
7
|
export { LiteLLMHomeWidget } from './components/LiteLLMHomeWidget';
|
|
8
8
|
export type { LiteLLMHomeWidgetProps } from './components/LiteLLMHomeWidget';
|
|
9
|
+
export { LiteLLMBudgetWidget } from './components/LiteLLMBudgetWidget';
|
|
10
|
+
export type { LiteLLMBudgetWidgetProps } from './components/LiteLLMBudgetWidget';
|
|
9
11
|
export { GenerateKeyDialog } from './components/GenerateKeyDialog';
|
|
10
12
|
export { ManageTeamDialog } from './components/ManageTeamDialog';
|
|
11
13
|
export { LiteLlmApi, liteLlmApiRef } from './api';
|
package/dist/index.esm.js
CHANGED
|
@@ -3004,14 +3004,14 @@ import MenuItem4 from "@mui/material/MenuItem";
|
|
|
3004
3004
|
import Select from "@mui/material/Select";
|
|
3005
3005
|
import FormControl from "@mui/material/FormControl";
|
|
3006
3006
|
import InputLabel from "@mui/material/InputLabel";
|
|
3007
|
+
import TextField5 from "@mui/material/TextField";
|
|
3008
|
+
import InputAdornment2 from "@mui/material/InputAdornment";
|
|
3009
|
+
import { Search as Search2 } from "@mui/icons-material";
|
|
3007
3010
|
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
|
|
3008
3011
|
import Tooltip3 from "@mui/material/Tooltip";
|
|
3009
3012
|
import Snackbar from "@mui/material/Snackbar";
|
|
3010
3013
|
import Alert3 from "@mui/material/Alert";
|
|
3011
3014
|
import { alpha as alpha5, useTheme as useTheme2 } from "@mui/material/styles";
|
|
3012
|
-
import TextField5 from "@mui/material/TextField";
|
|
3013
|
-
import InputAdornment2 from "@mui/material/InputAdornment";
|
|
3014
|
-
import { Search as Search2 } from "@mui/icons-material";
|
|
3015
3015
|
function isModelAllowedByTeam2(model, teamModels) {
|
|
3016
3016
|
if (!teamModels || teamModels.length === 0 || teamModels.includes(ALL_PROXY_MODELS2)) return true;
|
|
3017
3017
|
return teamModels.includes(model.model_name) || !!model.access_groups?.some((group) => teamModels.includes(group));
|
|
@@ -3044,17 +3044,17 @@ var init_ModelsTable = __esm({
|
|
|
3044
3044
|
() => teams.find((t) => t.team_id === selectedTeamId) ?? null,
|
|
3045
3045
|
[teams, selectedTeamId]
|
|
3046
3046
|
);
|
|
3047
|
-
const
|
|
3047
|
+
const teamModels = useMemo6(() => {
|
|
3048
3048
|
if (!selectedTeam) return [];
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
}, [
|
|
3049
|
+
return allModels.filter((model) => isModelAllowedByTeam2(model, selectedTeam.models));
|
|
3050
|
+
}, [allModels, selectedTeam]);
|
|
3051
|
+
const filteredModels = useMemo6(() => {
|
|
3052
|
+
const q = filterText.trim().toLowerCase();
|
|
3053
|
+
if (!q) return teamModels;
|
|
3054
|
+
return teamModels.filter(
|
|
3055
|
+
(model) => model.model_name.toLowerCase().includes(q) || (model.access_groups || []).some((group) => group.toLowerCase().includes(q))
|
|
3056
|
+
);
|
|
3057
|
+
}, [teamModels, filterText]);
|
|
3058
3058
|
const handleCopyModelId = useCallback((modelId) => {
|
|
3059
3059
|
navigator.clipboard.writeText(modelId).then(() => setCopiedSnackbar(true));
|
|
3060
3060
|
}, []);
|
|
@@ -3076,9 +3076,9 @@ var init_ModelsTable = __esm({
|
|
|
3076
3076
|
SectionCard,
|
|
3077
3077
|
{
|
|
3078
3078
|
title: "Available Models",
|
|
3079
|
-
subtitle: selectedTeam ? `${filteredModels.length} model${filteredModels.length
|
|
3079
|
+
subtitle: selectedTeam ? `${filteredModels.length}${filterText.trim() ? ` of ${teamModels.length}` : ""} model${filteredModels.length === 1 ? "" : "s"}` : "Select a team to view its models"
|
|
3080
3080
|
},
|
|
3081
|
-
/* @__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(
|
|
3081
|
+
/* @__PURE__ */ React8.createElement(Box8, { sx: { px: 2.5, pb: 2.5, pt: 1, display: "flex", gap: 2, flexWrap: "wrap" } }, /* @__PURE__ */ React8.createElement(FormControl, { size: "small", sx: { minWidth: 240, mb: 2 } }, /* @__PURE__ */ React8.createElement(InputLabel, { id: "team-select-label" }, "Team"), /* @__PURE__ */ React8.createElement(
|
|
3082
3082
|
Select,
|
|
3083
3083
|
{
|
|
3084
3084
|
labelId: "team-select-label",
|
|
@@ -3094,6 +3094,7 @@ var init_ModelsTable = __esm({
|
|
|
3094
3094
|
size: "small",
|
|
3095
3095
|
value: filterText,
|
|
3096
3096
|
onChange: (e) => setFilterText(e.target.value),
|
|
3097
|
+
disabled: !selectedTeamId,
|
|
3097
3098
|
sx: { minWidth: 240, mb: 2 },
|
|
3098
3099
|
InputProps: {
|
|
3099
3100
|
startAdornment: /* @__PURE__ */ React8.createElement(InputAdornment2, { position: "start" }, /* @__PURE__ */ React8.createElement(Search2, { fontSize: "small" }))
|
|
@@ -3108,14 +3109,32 @@ var init_ModelsTable = __esm({
|
|
|
3108
3109
|
hint: "Each team may have a different set of models assigned"
|
|
3109
3110
|
}
|
|
3110
3111
|
),
|
|
3111
|
-
!loading && selectedTeamId &&
|
|
3112
|
+
!loading && selectedTeamId && teamModels.length === 0 && /* @__PURE__ */ React8.createElement(
|
|
3112
3113
|
EmptyState,
|
|
3113
3114
|
{
|
|
3114
3115
|
message: "No models available for this team",
|
|
3115
3116
|
hint: allModels.length === 0 ? "No models are configured in the system" : "None of the team's assigned models were found in the catalog"
|
|
3116
3117
|
}
|
|
3117
3118
|
),
|
|
3118
|
-
!loading &&
|
|
3119
|
+
!loading && selectedTeamId && teamModels.length > 0 && filteredModels.length === 0 && /* @__PURE__ */ React8.createElement(
|
|
3120
|
+
EmptyState,
|
|
3121
|
+
{
|
|
3122
|
+
message: "No models match your search",
|
|
3123
|
+
hint: "Try a different search term"
|
|
3124
|
+
}
|
|
3125
|
+
),
|
|
3126
|
+
!loading && filteredModels.length > 0 && /* @__PURE__ */ React8.createElement(TableContainer5, null, /* @__PURE__ */ React8.createElement(Table5, { size: "small", sx: dataTableSx }, /* @__PURE__ */ React8.createElement(TableHead5, null, /* @__PURE__ */ React8.createElement(TableRow5, null, /* @__PURE__ */ React8.createElement(TableCell5, null, "Model ID"), /* @__PURE__ */ React8.createElement(TableCell5, null, "Mode"), /* @__PURE__ */ React8.createElement(TableCell5, { align: "right" }, "Input Cost"), /* @__PURE__ */ React8.createElement(TableCell5, { align: "right" }, "Output Cost"), /* @__PURE__ */ React8.createElement(TableCell5, { align: "right" }, "Max Input"), /* @__PURE__ */ React8.createElement(TableCell5, { align: "right" }, "Max Output"), /* @__PURE__ */ React8.createElement(TableCell5, null, "Capabilities"))), /* @__PURE__ */ React8.createElement(TableBody5, null, filteredModels.map((m) => /* @__PURE__ */ React8.createElement(TableRow5, { key: m.model_name, hover: true }, /* @__PURE__ */ React8.createElement(TableCell5, null, /* @__PURE__ */ React8.createElement(Box8, { sx: { display: "flex", alignItems: "center", gap: 0.5 } }, /* @__PURE__ */ React8.createElement(Tooltip3, { title: m.model_name, placement: "top" }, /* @__PURE__ */ React8.createElement(
|
|
3127
|
+
"span",
|
|
3128
|
+
{
|
|
3129
|
+
role: "button",
|
|
3130
|
+
tabIndex: 0,
|
|
3131
|
+
"aria-label": "Copy model ID",
|
|
3132
|
+
onClick: () => handleCopyModelId(m.model_name),
|
|
3133
|
+
onKeyDown: (e) => e.key === "Enter" && handleCopyModelId(m.model_name),
|
|
3134
|
+
style: { cursor: "pointer", display: "inline-flex" }
|
|
3135
|
+
},
|
|
3136
|
+
/* @__PURE__ */ React8.createElement(TagChip, { label: m.model_name, title: "Copy model ID" })
|
|
3137
|
+
)), /* @__PURE__ */ React8.createElement(Tooltip3, { title: "Copy model ID" }, /* @__PURE__ */ React8.createElement(
|
|
3119
3138
|
Box8,
|
|
3120
3139
|
{
|
|
3121
3140
|
component: "span",
|
|
@@ -3989,6 +4008,280 @@ var LiteLLMHomeWidget = ({
|
|
|
3989
4008
|
))))));
|
|
3990
4009
|
};
|
|
3991
4010
|
|
|
4011
|
+
// src/components/LiteLLMBudgetWidget.tsx
|
|
4012
|
+
init_api();
|
|
4013
|
+
init_format();
|
|
4014
|
+
init_ui();
|
|
4015
|
+
import React13, { useEffect as useEffect4, useMemo as useMemo8, useState as useState10 } from "react";
|
|
4016
|
+
import Paper8 from "@mui/material/Paper";
|
|
4017
|
+
import Box12 from "@mui/material/Box";
|
|
4018
|
+
import Typography12 from "@mui/material/Typography";
|
|
4019
|
+
import CircularProgress7 from "@mui/material/CircularProgress";
|
|
4020
|
+
import Alert7 from "@mui/material/Alert";
|
|
4021
|
+
import { alpha as alpha7 } from "@mui/material/styles";
|
|
4022
|
+
import { useApi as useApi4 } from "@backstage/core-plugin-api";
|
|
4023
|
+
|
|
4024
|
+
// src/budget.ts
|
|
4025
|
+
function limitPct(spend, budget) {
|
|
4026
|
+
if (budget <= 0) return 0;
|
|
4027
|
+
return spend / budget * 100;
|
|
4028
|
+
}
|
|
4029
|
+
function buildBudgetSummary(user, teams, keys, maxKeys = 3) {
|
|
4030
|
+
const budgeted = keys.filter((k) => (k.max_budget ?? 0) > 0).map((k) => {
|
|
4031
|
+
const spend = k.spend ?? 0;
|
|
4032
|
+
return {
|
|
4033
|
+
kind: "key",
|
|
4034
|
+
label: k.key_alias || k.key || "Untitled key",
|
|
4035
|
+
sublabel: k.key,
|
|
4036
|
+
budget: k.max_budget,
|
|
4037
|
+
spend,
|
|
4038
|
+
budgetDuration: k.budget_duration,
|
|
4039
|
+
pct: limitPct(spend, k.max_budget)
|
|
4040
|
+
};
|
|
4041
|
+
}).sort((a, b) => b.pct - a.pct);
|
|
4042
|
+
const userBudget = user && (user.max_budget ?? user.hard_limit ?? 0) > 0 ? (() => {
|
|
4043
|
+
const budget = user.max_budget ?? user.hard_limit ?? 0;
|
|
4044
|
+
const spend = user.current_spend ?? user.spend ?? 0;
|
|
4045
|
+
return {
|
|
4046
|
+
kind: "user",
|
|
4047
|
+
label: "Personal budget",
|
|
4048
|
+
sublabel: user.user_email || user.user_id,
|
|
4049
|
+
budget,
|
|
4050
|
+
spend,
|
|
4051
|
+
budgetDuration: user.budget_duration,
|
|
4052
|
+
softLimit: user.soft_limit,
|
|
4053
|
+
pct: limitPct(spend, budget)
|
|
4054
|
+
};
|
|
4055
|
+
})() : void 0;
|
|
4056
|
+
const teamLimits = teams.filter((t) => (t.max_budget ?? 0) > 0).map((t) => {
|
|
4057
|
+
const spend = t.spend ?? 0;
|
|
4058
|
+
return {
|
|
4059
|
+
kind: "team",
|
|
4060
|
+
label: t.team_alias || "Untitled team",
|
|
4061
|
+
sublabel: t.team_id,
|
|
4062
|
+
budget: t.max_budget,
|
|
4063
|
+
spend,
|
|
4064
|
+
budgetDuration: t.budget_duration,
|
|
4065
|
+
pct: limitPct(spend, t.max_budget)
|
|
4066
|
+
};
|
|
4067
|
+
}).sort((a, b) => b.pct - a.pct);
|
|
4068
|
+
return {
|
|
4069
|
+
keys: budgeted.slice(0, maxKeys),
|
|
4070
|
+
hiddenBudgetedKeys: Math.max(0, budgeted.length - maxKeys),
|
|
4071
|
+
totalKeyCount: keys.length,
|
|
4072
|
+
user: userBudget,
|
|
4073
|
+
teams: teamLimits
|
|
4074
|
+
};
|
|
4075
|
+
}
|
|
4076
|
+
function fmtBudgetDuration(duration) {
|
|
4077
|
+
if (!duration) return null;
|
|
4078
|
+
const m = /^(\d+)(s|m|h|d)$/.exec(duration);
|
|
4079
|
+
if (!m) return duration;
|
|
4080
|
+
const n = Number(m[1]);
|
|
4081
|
+
const unit = m[2];
|
|
4082
|
+
if (unit === "d") return n === 1 ? "daily" : `every ${n} days`;
|
|
4083
|
+
if (unit === "h") return n === 1 ? "hourly" : `every ${n} hours`;
|
|
4084
|
+
if (unit === "m") return n === 1 ? "every minute" : `every ${n} minutes`;
|
|
4085
|
+
return n === 1 ? "every second" : `every ${n} seconds`;
|
|
4086
|
+
}
|
|
4087
|
+
function budgetTone3(pct) {
|
|
4088
|
+
if (pct >= 100) return "danger";
|
|
4089
|
+
if (pct >= 80) return "warning";
|
|
4090
|
+
return "accent";
|
|
4091
|
+
}
|
|
4092
|
+
|
|
4093
|
+
// src/components/LiteLLMBudgetWidget.tsx
|
|
4094
|
+
function levelToneColor(tone, theme) {
|
|
4095
|
+
switch (tone) {
|
|
4096
|
+
case "success":
|
|
4097
|
+
return theme.palette.success.main;
|
|
4098
|
+
case "warning":
|
|
4099
|
+
return theme.palette.warning.main;
|
|
4100
|
+
case "danger":
|
|
4101
|
+
return theme.palette.error.main;
|
|
4102
|
+
case "info":
|
|
4103
|
+
return theme.palette.info.main;
|
|
4104
|
+
default:
|
|
4105
|
+
return theme.palette.primary.main;
|
|
4106
|
+
}
|
|
4107
|
+
}
|
|
4108
|
+
var LevelFrame = ({
|
|
4109
|
+
rank,
|
|
4110
|
+
name,
|
|
4111
|
+
tagline,
|
|
4112
|
+
note,
|
|
4113
|
+
tone,
|
|
4114
|
+
isLast,
|
|
4115
|
+
children
|
|
4116
|
+
}) => /* @__PURE__ */ React13.createElement(Box12, { sx: { display: "flex", gap: 1.5 } }, /* @__PURE__ */ React13.createElement(Box12, { sx: { display: "flex", flexDirection: "column", alignItems: "center", width: 26, flexShrink: 0 } }, /* @__PURE__ */ React13.createElement(
|
|
4117
|
+
Box12,
|
|
4118
|
+
{
|
|
4119
|
+
sx: (theme) => ({
|
|
4120
|
+
width: 26,
|
|
4121
|
+
height: 26,
|
|
4122
|
+
borderRadius: "50%",
|
|
4123
|
+
display: "flex",
|
|
4124
|
+
alignItems: "center",
|
|
4125
|
+
justifyContent: "center",
|
|
4126
|
+
bgcolor: alpha7(levelToneColor(tone, theme), 0.14),
|
|
4127
|
+
color: levelToneColor(tone, theme),
|
|
4128
|
+
fontSize: 12,
|
|
4129
|
+
fontWeight: 700,
|
|
4130
|
+
flexShrink: 0
|
|
4131
|
+
})
|
|
4132
|
+
},
|
|
4133
|
+
rank
|
|
4134
|
+
), /* @__PURE__ */ React13.createElement(
|
|
4135
|
+
Box12,
|
|
4136
|
+
{
|
|
4137
|
+
sx: (theme) => ({
|
|
4138
|
+
flex: 1,
|
|
4139
|
+
width: 2,
|
|
4140
|
+
minHeight: 14,
|
|
4141
|
+
bgcolor: isLast ? "transparent" : alpha7(theme.palette.text.primary, 0.12)
|
|
4142
|
+
})
|
|
4143
|
+
}
|
|
4144
|
+
)), /* @__PURE__ */ React13.createElement(Box12, { sx: { flex: 1, minWidth: 0, pb: isLast ? 0 : 2 } }, /* @__PURE__ */ React13.createElement(Box12, { sx: { display: "flex", alignItems: "baseline", gap: 1, flexWrap: "wrap", mb: 0.75 } }, /* @__PURE__ */ React13.createElement(
|
|
4145
|
+
Typography12,
|
|
4146
|
+
{
|
|
4147
|
+
sx: (theme) => ({
|
|
4148
|
+
fontSize: 12.5,
|
|
4149
|
+
fontWeight: 700,
|
|
4150
|
+
letterSpacing: "0.08em",
|
|
4151
|
+
textTransform: "uppercase",
|
|
4152
|
+
color: levelToneColor(tone, theme)
|
|
4153
|
+
})
|
|
4154
|
+
},
|
|
4155
|
+
name
|
|
4156
|
+
), /* @__PURE__ */ React13.createElement(Typography12, { variant: "caption", color: "text.secondary", sx: { fontSize: 11.5 } }, tagline), note && /* @__PURE__ */ React13.createElement(StatusPill, { label: note, tone: "neutral", dot: false, sx: { ml: "auto" } })), /* @__PURE__ */ React13.createElement(Box12, { sx: { display: "flex", flexDirection: "column", gap: 1 } }, children)));
|
|
4157
|
+
var EmptyLimitCard = ({ children }) => /* @__PURE__ */ React13.createElement(
|
|
4158
|
+
Paper8,
|
|
4159
|
+
{
|
|
4160
|
+
variant: "outlined",
|
|
4161
|
+
sx: (theme) => ({
|
|
4162
|
+
px: 1.5,
|
|
4163
|
+
py: 1.25,
|
|
4164
|
+
borderRadius: 1.5,
|
|
4165
|
+
bgcolor: alpha7(theme.palette.text.primary, theme.palette.mode === "dark" ? 0.03 : 0.015)
|
|
4166
|
+
})
|
|
4167
|
+
},
|
|
4168
|
+
/* @__PURE__ */ React13.createElement(Typography12, { variant: "body2", color: "text.secondary", sx: { fontSize: 13 } }, children)
|
|
4169
|
+
);
|
|
4170
|
+
var LimitCard = ({ limit }) => {
|
|
4171
|
+
const tone = budgetTone3(limit.pct);
|
|
4172
|
+
const pct = Math.round(limit.pct);
|
|
4173
|
+
const closeTo = limit.softLimit !== void 0 && limit.spend >= limit.softLimit;
|
|
4174
|
+
return /* @__PURE__ */ React13.createElement(Paper8, { variant: "outlined", sx: { px: 1.5, py: 1.25, borderRadius: 1.5 } }, /* @__PURE__ */ React13.createElement(Box12, { sx: { display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 1.5 } }, /* @__PURE__ */ React13.createElement(Box12, { sx: { minWidth: 0, flex: 1 } }, /* @__PURE__ */ React13.createElement(
|
|
4175
|
+
Typography12,
|
|
4176
|
+
{
|
|
4177
|
+
variant: "body2",
|
|
4178
|
+
sx: { fontWeight: 600, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
4179
|
+
title: limit.sublabel ? `${limit.label} \xB7 ${limit.sublabel}` : limit.label
|
|
4180
|
+
},
|
|
4181
|
+
limit.label
|
|
4182
|
+
), limit.sublabel && /* @__PURE__ */ React13.createElement(
|
|
4183
|
+
Typography12,
|
|
4184
|
+
{
|
|
4185
|
+
variant: "caption",
|
|
4186
|
+
color: "text.secondary",
|
|
4187
|
+
sx: {
|
|
4188
|
+
fontSize: 10.5,
|
|
4189
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
4190
|
+
display: "block",
|
|
4191
|
+
overflow: "hidden",
|
|
4192
|
+
textOverflow: "ellipsis",
|
|
4193
|
+
whiteSpace: "nowrap"
|
|
4194
|
+
}
|
|
4195
|
+
},
|
|
4196
|
+
limit.sublabel
|
|
4197
|
+
)), /* @__PURE__ */ React13.createElement(Typography12, { variant: "body2", sx: { fontWeight: 600, fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap" } }, fmtUsd(limit.spend), /* @__PURE__ */ React13.createElement(Box12, { component: "span", color: "text.secondary", sx: { fontWeight: 400 } }, " / ", fmtUsd(limit.budget)))), /* @__PURE__ */ React13.createElement(Box12, { sx: { mt: 1 } }, /* @__PURE__ */ React13.createElement(Meter, { value: limit.pct, tone, height: 6 })), /* @__PURE__ */ React13.createElement(
|
|
4198
|
+
Box12,
|
|
4199
|
+
{
|
|
4200
|
+
sx: {
|
|
4201
|
+
display: "flex",
|
|
4202
|
+
justifyContent: "space-between",
|
|
4203
|
+
alignItems: "center",
|
|
4204
|
+
gap: 1,
|
|
4205
|
+
mt: 0.75
|
|
4206
|
+
}
|
|
4207
|
+
},
|
|
4208
|
+
/* @__PURE__ */ React13.createElement(Typography12, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, pct, "% of budget", closeTo && ` \xB7 soft-limit warning`),
|
|
4209
|
+
/* @__PURE__ */ React13.createElement(Typography12, { variant: "caption", color: "text.secondary" }, fmtBudgetDuration(limit.budgetDuration) ? `resets ${fmtBudgetDuration(limit.budgetDuration)}` : "never resets")
|
|
4210
|
+
));
|
|
4211
|
+
};
|
|
4212
|
+
var LiteLLMBudgetWidget = ({
|
|
4213
|
+
title = "Budget Policy",
|
|
4214
|
+
maxKeys = 3
|
|
4215
|
+
}) => {
|
|
4216
|
+
const api = useApi4(liteLlmApiRef);
|
|
4217
|
+
const [loading, setLoading] = useState10(true);
|
|
4218
|
+
const [error, setError] = useState10(null);
|
|
4219
|
+
const [user, setUser] = useState10(null);
|
|
4220
|
+
const [teams, setTeams] = useState10([]);
|
|
4221
|
+
const [keys, setKeys] = useState10([]);
|
|
4222
|
+
useEffect4(() => {
|
|
4223
|
+
let cancelled = false;
|
|
4224
|
+
setLoading(true);
|
|
4225
|
+
setError(null);
|
|
4226
|
+
Promise.allSettled([api.getUserInfo(), api.getTeams(), api.listKeys()]).then(
|
|
4227
|
+
([userResult, teamsResult, keysResult]) => {
|
|
4228
|
+
if (cancelled) return;
|
|
4229
|
+
if (userResult.status === "fulfilled") {
|
|
4230
|
+
setUser(userResult.value);
|
|
4231
|
+
} else {
|
|
4232
|
+
setUser(null);
|
|
4233
|
+
setError(
|
|
4234
|
+
userResult.reason?.message ?? "Failed to load your LiteLLM profile"
|
|
4235
|
+
);
|
|
4236
|
+
}
|
|
4237
|
+
setTeams(teamsResult.status === "fulfilled" ? teamsResult.value : []);
|
|
4238
|
+
setKeys(keysResult.status === "fulfilled" ? keysResult.value : []);
|
|
4239
|
+
setLoading(false);
|
|
4240
|
+
}
|
|
4241
|
+
);
|
|
4242
|
+
return () => {
|
|
4243
|
+
cancelled = true;
|
|
4244
|
+
};
|
|
4245
|
+
}, [api]);
|
|
4246
|
+
const hasAnyLimit = !!user && (user.max_budget ?? user.hard_limit ?? 0) > 0;
|
|
4247
|
+
const summary = useMemo8(() => buildBudgetSummary(user, teams, keys, maxKeys), [user, teams, keys, maxKeys]);
|
|
4248
|
+
const hasBudgetedKeys = keys.some((k) => (k.max_budget ?? 0) > 0);
|
|
4249
|
+
return /* @__PURE__ */ React13.createElement(Paper8, { sx: { p: 2 } }, /* @__PURE__ */ React13.createElement(Box12, { sx: { mb: 1.5 } }, /* @__PURE__ */ React13.createElement(Typography12, { variant: "h6", lineHeight: 1.2 }, title), /* @__PURE__ */ React13.createElement(Typography12, { variant: "caption", color: "text.secondary" }, "How LiteLLM caps spend \u2014 and where you stand right now")), loading && /* @__PURE__ */ React13.createElement(Box12, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 140 }, /* @__PURE__ */ React13.createElement(CircularProgress7, { size: 32 })), !loading && error && /* @__PURE__ */ React13.createElement(Alert7, { severity: "error", sx: { mt: 0.5 } }, error), !loading && !error && /* @__PURE__ */ React13.createElement(React13.Fragment, null, /* @__PURE__ */ React13.createElement(
|
|
4250
|
+
LevelFrame,
|
|
4251
|
+
{
|
|
4252
|
+
rank: 1,
|
|
4253
|
+
name: "Key",
|
|
4254
|
+
tagline: "per-key cap \xB7 highest priority",
|
|
4255
|
+
note: hasBudgetedKeys ? void 0 : "no key budgets set"
|
|
4256
|
+
},
|
|
4257
|
+
!hasBudgetedKeys && /* @__PURE__ */ React13.createElement(EmptyLimitCard, null, "No key has a budget, so no key-level cap applies."),
|
|
4258
|
+
summary.keys.map((k) => /* @__PURE__ */ React13.createElement(LimitCard, { key: k.sublabel ?? k.label, limit: k })),
|
|
4259
|
+
summary.hiddenBudgetedKeys > 0 && /* @__PURE__ */ React13.createElement(Typography12, { variant: "caption", color: "text.secondary" }, "+", summary.hiddenBudgetedKeys, " more budgeted key", summary.hiddenBudgetedKeys > 1 ? "s" : "", " further from the cap")
|
|
4260
|
+
), /* @__PURE__ */ React13.createElement(
|
|
4261
|
+
LevelFrame,
|
|
4262
|
+
{
|
|
4263
|
+
rank: 2,
|
|
4264
|
+
name: "User",
|
|
4265
|
+
tagline: "cap on everything you spend with your own keys",
|
|
4266
|
+
note: "skipped for team-bound keys"
|
|
4267
|
+
},
|
|
4268
|
+
hasAnyLimit && summary.user ? /* @__PURE__ */ React13.createElement(LimitCard, { limit: summary.user }) : /* @__PURE__ */ React13.createElement(EmptyLimitCard, null, "No personal budget is set on your account.")
|
|
4269
|
+
), /* @__PURE__ */ React13.createElement(LevelFrame, { rank: 3, name: "Team", tagline: "shared cap for all keys in a team (if any)" }, summary.teams.length > 0 ? summary.teams.map((t) => /* @__PURE__ */ React13.createElement(LimitCard, { key: t.sublabel ?? t.label, limit: t })) : /* @__PURE__ */ React13.createElement(EmptyLimitCard, null, "No team you belong to has a budget.")), /* @__PURE__ */ React13.createElement(LevelFrame, { rank: 4, name: "Global", tagline: "proxy-wide cap set by your admin", isLast: true }, /* @__PURE__ */ React13.createElement(EmptyLimitCard, null, "Configured by your admin in the proxy config \u2014 it isn't visible from this page. When set, it limits spend across every request on the proxy.")), /* @__PURE__ */ React13.createElement(
|
|
4270
|
+
Box12,
|
|
4271
|
+
{
|
|
4272
|
+
sx: (theme) => ({
|
|
4273
|
+
mt: 2,
|
|
4274
|
+
px: 1.5,
|
|
4275
|
+
py: 1.25,
|
|
4276
|
+
borderRadius: 1.5,
|
|
4277
|
+
border: "1px dashed",
|
|
4278
|
+
borderColor: alpha7(theme.palette.text.primary, 0.18)
|
|
4279
|
+
})
|
|
4280
|
+
},
|
|
4281
|
+
/* @__PURE__ */ React13.createElement(Typography12, { variant: "caption", color: "text.secondary", display: "block" }, "The first cap a request runs into is the one that stops it. A limit with a reset window (e.g. daily or every 30 days) clears its spend to $0 when the window closes; without a window it never resets.")
|
|
4282
|
+
)));
|
|
4283
|
+
};
|
|
4284
|
+
|
|
3992
4285
|
// src/index.ts
|
|
3993
4286
|
init_GenerateKeyDialog();
|
|
3994
4287
|
init_ManageTeamDialog();
|
|
@@ -3998,6 +4291,7 @@ export {
|
|
|
3998
4291
|
DashboardHeader,
|
|
3999
4292
|
GenerateKeyDialog,
|
|
4000
4293
|
KeysTable,
|
|
4294
|
+
LiteLLMBudgetWidget,
|
|
4001
4295
|
LiteLLMHomeWidget,
|
|
4002
4296
|
LiteLLMPage,
|
|
4003
4297
|
LiteLlmApi,
|