@acarmisc/backstage-plugin-litellm 0.19.0 → 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/budget.test.d.ts +1 -0
- package/dist/components/LiteLLMBudgetWidget.d.ts +22 -0
- package/dist/index.cjs.js +275 -0
- package/dist/index.cjs.js.map +4 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.esm.js +275 -0
- package/dist/index.esm.js.map +4 -4
- package/dist/types.d.ts +3 -0
- package/package.json +1 -1
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
|
@@ -4008,6 +4008,280 @@ var LiteLLMHomeWidget = ({
|
|
|
4008
4008
|
))))));
|
|
4009
4009
|
};
|
|
4010
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
|
+
|
|
4011
4285
|
// src/index.ts
|
|
4012
4286
|
init_GenerateKeyDialog();
|
|
4013
4287
|
init_ManageTeamDialog();
|
|
@@ -4017,6 +4291,7 @@ export {
|
|
|
4017
4291
|
DashboardHeader,
|
|
4018
4292
|
GenerateKeyDialog,
|
|
4019
4293
|
KeysTable,
|
|
4294
|
+
LiteLLMBudgetWidget,
|
|
4020
4295
|
LiteLLMHomeWidget,
|
|
4021
4296
|
LiteLLMPage,
|
|
4022
4297
|
LiteLlmApi,
|