@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/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 filteredModels = useMemo6(() => {
3047
+ const teamModels = useMemo6(() => {
3048
3048
  if (!selectedTeam) return [];
3049
- let result = allModels.filter((model) => isModelAllowedByTeam2(model, selectedTeam.models));
3050
- if (filterText.trim()) {
3051
- const q = filterText.toLowerCase();
3052
- result = result.filter(
3053
- (model) => model.model_name.toLowerCase().includes(q) || (model.access_groups || []).some((group) => group.toLowerCase().includes(q))
3054
- );
3055
- }
3056
- return result;
3057
- }, [allModels, selectedTeam, filterText]);
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 !== 1 ? "s" : ""} in ${selectedTeam.team_alias ?? selectedTeam.team_id}` : "Select a team to view its models"
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 && filteredModels.length === 0 && /* @__PURE__ */ React8.createElement(
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 && 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(TagChip, { label: m.model_name, title: m.model_name }), /* @__PURE__ */ React8.createElement(Tooltip3, { title: "Copy model ID" }, /* @__PURE__ */ React8.createElement(
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,