@acarmisc/backstage-plugin-litellm 0.22.0 → 0.24.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 +28 -0
- package/dist/components/LiteLLMBudgetGauges.d.ts +27 -0
- package/dist/components/ui.d.ts +20 -0
- package/dist/index.cjs.js +379 -31
- package/dist/index.cjs.js.map +4 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.esm.js +379 -31
- package/dist/index.esm.js.map +4 -4
- package/dist/types.d.ts +13 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export { LiteLLMHomeWidget } from './components/LiteLLMHomeWidget';
|
|
|
8
8
|
export type { LiteLLMHomeWidgetProps } from './components/LiteLLMHomeWidget';
|
|
9
9
|
export { LiteLLMBudgetWidget } from './components/LiteLLMBudgetWidget';
|
|
10
10
|
export type { LiteLLMBudgetWidgetProps } from './components/LiteLLMBudgetWidget';
|
|
11
|
+
export { LiteLLMBudgetGauges } from './components/LiteLLMBudgetGauges';
|
|
12
|
+
export type { LiteLLMBudgetGaugesProps } from './components/LiteLLMBudgetGauges';
|
|
11
13
|
export { GenerateKeyDialog } from './components/GenerateKeyDialog';
|
|
12
14
|
export { ManageTeamDialog } from './components/ManageTeamDialog';
|
|
13
15
|
export { LiteLlmApi, liteLlmApiRef } from './api';
|
package/dist/index.esm.js
CHANGED
|
@@ -319,7 +319,7 @@ function SegmentedControl({
|
|
|
319
319
|
})
|
|
320
320
|
);
|
|
321
321
|
}
|
|
322
|
-
var MONTHS, fmtDateShort, fmtCompact, fmtUsdCompact, CHART_COLORS, SERIES, OTHER_COLOR, ChartTooltip, SeriesLegend, StatusPill, TagChip, SectionCard, ChartCard, MetricStrip, Stat, Meter, dataTableSx, quietIconButtonSx, EmptyState;
|
|
322
|
+
var MONTHS, fmtDateShort, fmtCompact, fmtUsdCompact, CHART_COLORS, SERIES, OTHER_COLOR, ChartTooltip, SeriesLegend, StatusPill, TagChip, SectionCard, ChartCard, MetricStrip, Stat, Meter, Gauge, dataTableSx, quietIconButtonSx, EmptyState;
|
|
323
323
|
var init_ui = __esm({
|
|
324
324
|
"src/components/ui.tsx"() {
|
|
325
325
|
"use strict";
|
|
@@ -683,6 +683,102 @@ var init_ui = __esm({
|
|
|
683
683
|
}
|
|
684
684
|
)
|
|
685
685
|
);
|
|
686
|
+
Gauge = ({ value, tone = "accent", size = 72, thickness = 6, label, caption }) => {
|
|
687
|
+
const clamped = Math.max(0, Math.min(100, value ?? 0));
|
|
688
|
+
const r = (size - thickness) / 2;
|
|
689
|
+
const c = 2 * Math.PI * r;
|
|
690
|
+
const dash = clamped / 100 * c;
|
|
691
|
+
return /* @__PURE__ */ React.createElement(
|
|
692
|
+
Box,
|
|
693
|
+
{
|
|
694
|
+
sx: (theme) => ({
|
|
695
|
+
position: "relative",
|
|
696
|
+
width: size,
|
|
697
|
+
height: size,
|
|
698
|
+
flexShrink: 0,
|
|
699
|
+
color: toneColor(theme, tone)
|
|
700
|
+
})
|
|
701
|
+
},
|
|
702
|
+
/* @__PURE__ */ React.createElement(
|
|
703
|
+
Box,
|
|
704
|
+
{
|
|
705
|
+
component: "svg",
|
|
706
|
+
viewBox: `0 0 ${size} ${size}`,
|
|
707
|
+
width: size,
|
|
708
|
+
height: size,
|
|
709
|
+
sx: { display: "block", transform: "rotate(-90deg)" }
|
|
710
|
+
},
|
|
711
|
+
/* @__PURE__ */ React.createElement(
|
|
712
|
+
Box,
|
|
713
|
+
{
|
|
714
|
+
component: "circle",
|
|
715
|
+
cx: size / 2,
|
|
716
|
+
cy: size / 2,
|
|
717
|
+
r,
|
|
718
|
+
fill: "none",
|
|
719
|
+
strokeWidth: thickness,
|
|
720
|
+
sx: (theme) => ({
|
|
721
|
+
stroke: alpha(
|
|
722
|
+
theme.palette.text.primary,
|
|
723
|
+
theme.palette.mode === "dark" ? 0.14 : 0.09
|
|
724
|
+
)
|
|
725
|
+
})
|
|
726
|
+
}
|
|
727
|
+
),
|
|
728
|
+
/* @__PURE__ */ React.createElement(
|
|
729
|
+
Box,
|
|
730
|
+
{
|
|
731
|
+
component: "circle",
|
|
732
|
+
cx: size / 2,
|
|
733
|
+
cy: size / 2,
|
|
734
|
+
r,
|
|
735
|
+
fill: "none",
|
|
736
|
+
strokeWidth: thickness,
|
|
737
|
+
strokeLinecap: "round",
|
|
738
|
+
strokeDasharray: `${dash} ${c - dash}`,
|
|
739
|
+
sx: (theme) => ({
|
|
740
|
+
stroke: "currentColor",
|
|
741
|
+
transition: theme.transitions.create("stroke-dasharray")
|
|
742
|
+
})
|
|
743
|
+
}
|
|
744
|
+
)
|
|
745
|
+
),
|
|
746
|
+
/* @__PURE__ */ React.createElement(
|
|
747
|
+
Box,
|
|
748
|
+
{
|
|
749
|
+
sx: {
|
|
750
|
+
position: "absolute",
|
|
751
|
+
inset: 0,
|
|
752
|
+
display: "flex",
|
|
753
|
+
flexDirection: "column",
|
|
754
|
+
alignItems: "center",
|
|
755
|
+
justifyContent: "center",
|
|
756
|
+
lineHeight: 1
|
|
757
|
+
}
|
|
758
|
+
},
|
|
759
|
+
/* @__PURE__ */ React.createElement(
|
|
760
|
+
Typography,
|
|
761
|
+
{
|
|
762
|
+
sx: {
|
|
763
|
+
fontSize: size <= 64 ? 15 : 18,
|
|
764
|
+
fontWeight: 700,
|
|
765
|
+
fontVariantNumeric: "tabular-nums",
|
|
766
|
+
color: "text.primary"
|
|
767
|
+
}
|
|
768
|
+
},
|
|
769
|
+
label ?? `${Math.round(value)}%`
|
|
770
|
+
),
|
|
771
|
+
caption && /* @__PURE__ */ React.createElement(
|
|
772
|
+
Typography,
|
|
773
|
+
{
|
|
774
|
+
variant: "caption",
|
|
775
|
+
sx: { fontSize: 9.5, color: "text.secondary", mt: 0.25 }
|
|
776
|
+
},
|
|
777
|
+
caption
|
|
778
|
+
)
|
|
779
|
+
)
|
|
780
|
+
);
|
|
781
|
+
};
|
|
686
782
|
dataTableSx = (theme) => ({
|
|
687
783
|
// Element selectors, not `.MuiTableCell-head`: host apps may set a MUI
|
|
688
784
|
// classname prefix (Backstage ships `v5-`), which breaks global class targeting.
|
|
@@ -2001,6 +2097,8 @@ var init_ManageTeamDialog = __esm({
|
|
|
2001
2097
|
const [mcpError, setMcpError] = useState3(null);
|
|
2002
2098
|
const allowUnlimitedBudget = config?.teamManagement?.allowUnlimitedBudget ?? false;
|
|
2003
2099
|
const maxBudgetCeiling = config?.teamManagement?.maxBudgetCeiling ?? FALLBACK_BUDGET_CEILING;
|
|
2100
|
+
const hideBudgetForManagers = config?.display?.hideTeamBudgetForManagers ?? false;
|
|
2101
|
+
const budgetWriteOnly = hideBudgetForManagers && mode === "edit";
|
|
2004
2102
|
const members = useMemo4(() => team?.members_with_roles ?? [], [team]);
|
|
2005
2103
|
const showMembers = mode === "edit" && !!canManageMembers;
|
|
2006
2104
|
const catalogApi = useApi(catalogApiRef);
|
|
@@ -2036,9 +2134,13 @@ var init_ManageTeamDialog = __esm({
|
|
|
2036
2134
|
if (mode === "edit" && team) {
|
|
2037
2135
|
setAlias(team.team_alias ?? "");
|
|
2038
2136
|
setModels(team.models ?? []);
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2137
|
+
if (hideBudgetForManagers) {
|
|
2138
|
+
setMaxBudget("");
|
|
2139
|
+
} else {
|
|
2140
|
+
setMaxBudget(
|
|
2141
|
+
team.max_budget ? String(team.max_budget) : String(maxBudgetCeiling)
|
|
2142
|
+
);
|
|
2143
|
+
}
|
|
2042
2144
|
setUnlimited(allowUnlimitedBudget && !team.max_budget);
|
|
2043
2145
|
setBudgetDuration(team.budget_duration ?? DEFAULT_BUDGET_DURATION);
|
|
2044
2146
|
} else {
|
|
@@ -2058,7 +2160,7 @@ var init_ManageTeamDialog = __esm({
|
|
|
2058
2160
|
setMcpIds(team?.object_permission?.mcp_servers ?? []);
|
|
2059
2161
|
setMcpError(null);
|
|
2060
2162
|
}
|
|
2061
|
-
}, [open, mode, team, allowUnlimitedBudget, maxBudgetCeiling]);
|
|
2163
|
+
}, [open, mode, team, allowUnlimitedBudget, maxBudgetCeiling, hideBudgetForManagers]);
|
|
2062
2164
|
const handleSubmit = async () => {
|
|
2063
2165
|
setError(null);
|
|
2064
2166
|
if (!alias.trim()) {
|
|
@@ -2070,18 +2172,21 @@ var init_ManageTeamDialog = __esm({
|
|
|
2070
2172
|
return;
|
|
2071
2173
|
}
|
|
2072
2174
|
if (!unlimited) {
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2175
|
+
const budgetLeftBlank = budgetWriteOnly && !maxBudget.trim();
|
|
2176
|
+
if (!budgetLeftBlank) {
|
|
2177
|
+
if (!maxBudget) {
|
|
2178
|
+
setError("Budget is required when unlimited budgets are disabled");
|
|
2179
|
+
return;
|
|
2180
|
+
}
|
|
2181
|
+
const budget = parseFloat(maxBudget);
|
|
2182
|
+
if (isNaN(budget) || budget <= 0) {
|
|
2183
|
+
setError("Budget must be a positive number");
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
if (budget > maxBudgetCeiling) {
|
|
2187
|
+
setError(`Budget cannot exceed $${maxBudgetCeiling}`);
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
2085
2190
|
}
|
|
2086
2191
|
}
|
|
2087
2192
|
try {
|
|
@@ -2095,10 +2200,16 @@ var init_ManageTeamDialog = __esm({
|
|
|
2095
2200
|
...budgetDuration && { budget_duration: budgetDuration }
|
|
2096
2201
|
};
|
|
2097
2202
|
} else {
|
|
2203
|
+
let budgetField = {};
|
|
2204
|
+
if (unlimited) {
|
|
2205
|
+
budgetField = { max_budget: null };
|
|
2206
|
+
} else if (maxBudget.trim()) {
|
|
2207
|
+
budgetField = { max_budget: parseFloat(maxBudget) };
|
|
2208
|
+
}
|
|
2098
2209
|
payload = {
|
|
2099
2210
|
team_alias: alias.trim(),
|
|
2100
2211
|
models,
|
|
2101
|
-
...
|
|
2212
|
+
...budgetField,
|
|
2102
2213
|
...budgetDuration && { budget_duration: budgetDuration }
|
|
2103
2214
|
};
|
|
2104
2215
|
}
|
|
@@ -2204,13 +2315,13 @@ var init_ManageTeamDialog = __esm({
|
|
|
2204
2315
|
), /* @__PURE__ */ React5.createElement(
|
|
2205
2316
|
TextField3,
|
|
2206
2317
|
{
|
|
2207
|
-
label: "Max Budget ($)",
|
|
2318
|
+
label: budgetWriteOnly ? "New Max Budget ($)" : "Max Budget ($)",
|
|
2208
2319
|
type: "number",
|
|
2209
2320
|
value: maxBudget,
|
|
2210
2321
|
onChange: (e) => setMaxBudget(e.target.value),
|
|
2211
2322
|
disabled: submitting || unlimited,
|
|
2212
2323
|
inputProps: { min: 0, max: maxBudgetCeiling },
|
|
2213
|
-
helperText: `Maximum: $${maxBudgetCeiling}`,
|
|
2324
|
+
helperText: budgetWriteOnly ? "Current budget is hidden \u2014 leave blank to keep it, or enter a new value" : `Maximum: $${maxBudgetCeiling}`,
|
|
2214
2325
|
fullWidth: true
|
|
2215
2326
|
}
|
|
2216
2327
|
), allowUnlimitedBudget && /* @__PURE__ */ React5.createElement(
|
|
@@ -2781,7 +2892,19 @@ function buildBudgetSummary(user, teams, keys, maxKeys = 3) {
|
|
|
2781
2892
|
pct: limitPct(spend, budget)
|
|
2782
2893
|
};
|
|
2783
2894
|
})() : void 0;
|
|
2784
|
-
const teamLimits = teams.filter((t) => (t.max_budget ?? 0) > 0).map((t) => {
|
|
2895
|
+
const teamLimits = teams.filter((t) => (t.max_budget ?? 0) > 0 || t.budget_hidden === true).map((t) => {
|
|
2896
|
+
if (t.budget_hidden === true) {
|
|
2897
|
+
return {
|
|
2898
|
+
kind: "team",
|
|
2899
|
+
label: t.team_alias || "Untitled team",
|
|
2900
|
+
sublabel: t.team_id,
|
|
2901
|
+
budget: 0,
|
|
2902
|
+
spend: 0,
|
|
2903
|
+
budgetDuration: t.budget_duration,
|
|
2904
|
+
pct: Math.min(100, Math.max(0, t.budget_pct ?? 0)),
|
|
2905
|
+
hidden: true
|
|
2906
|
+
};
|
|
2907
|
+
}
|
|
2785
2908
|
const spend = t.spend ?? 0;
|
|
2786
2909
|
return {
|
|
2787
2910
|
kind: "team",
|
|
@@ -2801,6 +2924,22 @@ function buildBudgetSummary(user, teams, keys, maxKeys = 3) {
|
|
|
2801
2924
|
teams: teamLimits
|
|
2802
2925
|
};
|
|
2803
2926
|
}
|
|
2927
|
+
function buildBudgetGauges(summary) {
|
|
2928
|
+
const byLevel = {
|
|
2929
|
+
key: summary.keys,
|
|
2930
|
+
user: summary.user ? [summary.user] : [],
|
|
2931
|
+
team: summary.teams
|
|
2932
|
+
};
|
|
2933
|
+
return GAUGE_KINDS.map((kind) => {
|
|
2934
|
+
const limits = byLevel[kind];
|
|
2935
|
+
const count = kind === "key" ? limits.length + summary.hiddenBudgetedKeys : limits.length;
|
|
2936
|
+
return {
|
|
2937
|
+
kind,
|
|
2938
|
+
count,
|
|
2939
|
+
limit: limits.length ? limits.reduce((a, b) => b.pct > a.pct ? b : a) : void 0
|
|
2940
|
+
};
|
|
2941
|
+
});
|
|
2942
|
+
}
|
|
2804
2943
|
function budgetHeadline(summary) {
|
|
2805
2944
|
const all = [
|
|
2806
2945
|
...summary.keys,
|
|
@@ -2827,9 +2966,11 @@ function budgetTone2(pct) {
|
|
|
2827
2966
|
if (pct >= 80) return "warning";
|
|
2828
2967
|
return "accent";
|
|
2829
2968
|
}
|
|
2969
|
+
var GAUGE_KINDS;
|
|
2830
2970
|
var init_budget = __esm({
|
|
2831
2971
|
"src/budget.ts"() {
|
|
2832
2972
|
"use strict";
|
|
2973
|
+
GAUGE_KINDS = ["key", "user", "team"];
|
|
2833
2974
|
}
|
|
2834
2975
|
});
|
|
2835
2976
|
|
|
@@ -2957,7 +3098,7 @@ var init_LiteLLMBudgetWidget = __esm({
|
|
|
2957
3098
|
}
|
|
2958
3099
|
},
|
|
2959
3100
|
limit.sublabel
|
|
2960
|
-
)), /* @__PURE__ */ React7.createElement(Typography7, { variant: "body2", sx: { fontWeight: 600, fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap" } }, fmtUsd(limit.spend), /* @__PURE__ */ React7.createElement(Box7, { component: "span", color: "text.secondary", sx: { fontWeight: 400 } }, " / ", fmtUsd(limit.budget)))), /* @__PURE__ */ React7.createElement(Box7, { sx: { mt: 1 } }, /* @__PURE__ */ React7.createElement(Meter, { value: limit.pct, tone, height: 6 })), /* @__PURE__ */ React7.createElement(
|
|
3101
|
+
)), limit.hidden ? /* @__PURE__ */ React7.createElement(Typography7, { variant: "body2", sx: { fontWeight: 600, fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap" } }, pct, "% used") : /* @__PURE__ */ React7.createElement(Typography7, { variant: "body2", sx: { fontWeight: 600, fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap" } }, fmtUsd(limit.spend), /* @__PURE__ */ React7.createElement(Box7, { component: "span", color: "text.secondary", sx: { fontWeight: 400 } }, " / ", fmtUsd(limit.budget)))), /* @__PURE__ */ React7.createElement(Box7, { sx: { mt: 1 } }, /* @__PURE__ */ React7.createElement(Meter, { value: limit.pct, tone, height: 6 })), /* @__PURE__ */ React7.createElement(
|
|
2961
3102
|
Box7,
|
|
2962
3103
|
{
|
|
2963
3104
|
sx: {
|
|
@@ -2968,7 +3109,7 @@ var init_LiteLLMBudgetWidget = __esm({
|
|
|
2968
3109
|
mt: 0.75
|
|
2969
3110
|
}
|
|
2970
3111
|
},
|
|
2971
|
-
/* @__PURE__ */ React7.createElement(Typography7, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, pct, "% of budget", closeTo && ` \xB7 soft-limit warning`),
|
|
3112
|
+
/* @__PURE__ */ React7.createElement(Typography7, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, limit.hidden ? /* @__PURE__ */ React7.createElement(React7.Fragment, null, "Hidden by admin") : /* @__PURE__ */ React7.createElement(React7.Fragment, null, pct, "% of budget", closeTo && ` \xB7 soft-limit warning`)),
|
|
2972
3113
|
/* @__PURE__ */ React7.createElement(Typography7, { variant: "caption", color: "text.secondary" }, fmtBudgetDuration(limit.budgetDuration) ? `resets ${fmtBudgetDuration(limit.budgetDuration)}` : "never resets")
|
|
2973
3114
|
));
|
|
2974
3115
|
};
|
|
@@ -3176,6 +3317,15 @@ function budgetTone3(isOver, isNear) {
|
|
|
3176
3317
|
if (isNear) return "warning";
|
|
3177
3318
|
return "accent";
|
|
3178
3319
|
}
|
|
3320
|
+
function visibleBudgetPct(spend, budget) {
|
|
3321
|
+
if (budget <= 0) return 0;
|
|
3322
|
+
return Math.min(spend / budget * 100, 100);
|
|
3323
|
+
}
|
|
3324
|
+
function budgetStatValue(hidden, budget) {
|
|
3325
|
+
if (hidden) return "Hidden";
|
|
3326
|
+
if (budget > 0) return fmtUsd2(budget);
|
|
3327
|
+
return "Unlimited";
|
|
3328
|
+
}
|
|
3179
3329
|
var fmtUsd2, TeamCard, TeamUsage;
|
|
3180
3330
|
var init_TeamUsage = __esm({
|
|
3181
3331
|
"src/components/TeamUsage.tsx"() {
|
|
@@ -3185,13 +3335,15 @@ var init_TeamUsage = __esm({
|
|
|
3185
3335
|
TeamCard = ({ team, usage, usageLoading, canManage, onEditTeam }) => {
|
|
3186
3336
|
const [expanded, setExpanded] = useState6(false);
|
|
3187
3337
|
const chart = useChartTheme();
|
|
3188
|
-
const
|
|
3189
|
-
const
|
|
3190
|
-
const
|
|
3191
|
-
const
|
|
3192
|
-
const
|
|
3338
|
+
const hidden = team.budget_hidden === true;
|
|
3339
|
+
const budget = hidden ? 0 : team.max_budget ?? 0;
|
|
3340
|
+
const spend = hidden ? 0 : team.spend ?? 0;
|
|
3341
|
+
const budgetPct = hidden ? Math.min(100, Math.max(0, team.budget_pct ?? 0)) : visibleBudgetPct(spend, budget);
|
|
3342
|
+
const isOver = hidden ? team.budget_status === "over" : budget > 0 && spend >= budget;
|
|
3343
|
+
const isNear = hidden ? team.budget_status === "near" : budget > 0 && spend >= budget * 0.8 && !isOver;
|
|
3193
3344
|
const dailyData = usage?.daily_usage?.map((d) => ({ date: d.date, spend: d.spend })) ?? [];
|
|
3194
3345
|
const renderDailySpendSection = () => {
|
|
3346
|
+
if (hidden) return null;
|
|
3195
3347
|
if (usageLoading) {
|
|
3196
3348
|
return /* @__PURE__ */ React8.createElement(Box8, { display: "flex", justifyContent: "center", py: 3 }, /* @__PURE__ */ React8.createElement(CircularProgress5, { size: 22 }));
|
|
3197
3349
|
}
|
|
@@ -3292,11 +3444,11 @@ var init_TeamUsage = __esm({
|
|
|
3292
3444
|
},
|
|
3293
3445
|
/* @__PURE__ */ React8.createElement(Stat, { label: "Members", value: team.members_with_roles?.length ?? "\u2014" }),
|
|
3294
3446
|
/* @__PURE__ */ React8.createElement(Stat, { label: "Models", value: team.models?.length ? team.models.length : "All" }),
|
|
3295
|
-
/* @__PURE__ */ React8.createElement(Stat, { label: "Budget", value:
|
|
3296
|
-
/* @__PURE__ */ React8.createElement(Stat, { label: "Spend", value: fmtUsd2(spend) }),
|
|
3447
|
+
/* @__PURE__ */ React8.createElement(Stat, { label: "Budget", value: budgetStatValue(hidden, budget) }),
|
|
3448
|
+
/* @__PURE__ */ React8.createElement(Stat, { label: "Spend", value: hidden ? "\u2014" : fmtUsd2(spend) }),
|
|
3297
3449
|
/* @__PURE__ */ React8.createElement(Stat, { label: "TPM", value: team.tpm_limit && team.tpm_limit > 0 ? team.tpm_limit.toLocaleString() : "\u2014" }),
|
|
3298
3450
|
/* @__PURE__ */ React8.createElement(Stat, { label: "RPM", value: team.rpm_limit && team.rpm_limit > 0 ? team.rpm_limit.toLocaleString() : "\u2014" })
|
|
3299
|
-
), budget > 0 && /* @__PURE__ */ React8.createElement(Box8, { mt: 2, maxWidth: 640 }, /* @__PURE__ */ React8.createElement(Box8, { display: "flex", justifyContent: "space-between", alignItems: "baseline", mb: 0.75 }, /* @__PURE__ */ React8.createElement(Typography8, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, fmtUsd2(spend), " of ", fmtUsd2(budget)), /* @__PURE__ */ React8.createElement(Typography8, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, budgetPct.toFixed(0), "%")), /* @__PURE__ */ React8.createElement(Meter, { value: budgetPct, tone: budgetTone3(isOver, isNear), height: 5 })), /* @__PURE__ */ React8.createElement(Collapse2, { in: expanded, unmountOnExit: true }, /* @__PURE__ */ React8.createElement(Divider3, { sx: { my: 2.5 } }), /* @__PURE__ */ React8.createElement(Box8, { mb: 2.5 }, sectionLabel("Models"), team.models?.length ? /* @__PURE__ */ React8.createElement(Box8, { display: "flex", gap: 0.5, flexWrap: "wrap" }, team.models.map((m) => /* @__PURE__ */ React8.createElement(TagChip, { key: m, label: m, title: m }))) : /* @__PURE__ */ React8.createElement(Typography8, { variant: "body2", color: "text.secondary" }, "All models allowed")), renderDailySpendSection(), /* @__PURE__ */ React8.createElement(Box8, null, sectionLabel("Members"), team.members_with_roles?.length ? /* @__PURE__ */ React8.createElement(
|
|
3451
|
+
), hidden && /* @__PURE__ */ React8.createElement(Box8, { mt: 2, maxWidth: 640 }, /* @__PURE__ */ React8.createElement(Box8, { display: "flex", justifyContent: "space-between", alignItems: "baseline", mb: 0.75 }, /* @__PURE__ */ React8.createElement(Typography8, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, budgetPct.toFixed(0), "% used"), /* @__PURE__ */ React8.createElement(Typography8, { variant: "caption", color: "text.secondary" }, "Hidden by admin")), /* @__PURE__ */ React8.createElement(Meter, { value: budgetPct, tone: budgetTone3(isOver, isNear), height: 5 }), /* @__PURE__ */ React8.createElement(Typography8, { variant: "caption", color: "text.secondary", sx: { display: "block", mt: 0.75 } }, "Team budget is hidden \u2014 showing usage level only", team.budget_duration ? ` \xB7 resets every ${team.budget_duration}` : "", ".")), !hidden && budget > 0 && /* @__PURE__ */ React8.createElement(Box8, { mt: 2, maxWidth: 640 }, /* @__PURE__ */ React8.createElement(Box8, { display: "flex", justifyContent: "space-between", alignItems: "baseline", mb: 0.75 }, /* @__PURE__ */ React8.createElement(Typography8, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, fmtUsd2(spend), " of ", fmtUsd2(budget)), /* @__PURE__ */ React8.createElement(Typography8, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, budgetPct.toFixed(0), "%")), /* @__PURE__ */ React8.createElement(Meter, { value: budgetPct, tone: budgetTone3(isOver, isNear), height: 5 })), /* @__PURE__ */ React8.createElement(Collapse2, { in: expanded, unmountOnExit: true }, /* @__PURE__ */ React8.createElement(Divider3, { sx: { my: 2.5 } }), /* @__PURE__ */ React8.createElement(Box8, { mb: 2.5 }, sectionLabel("Models"), team.models?.length ? /* @__PURE__ */ React8.createElement(Box8, { display: "flex", gap: 0.5, flexWrap: "wrap" }, team.models.map((m) => /* @__PURE__ */ React8.createElement(TagChip, { key: m, label: m, title: m }))) : /* @__PURE__ */ React8.createElement(Typography8, { variant: "body2", color: "text.secondary" }, "All models allowed")), renderDailySpendSection(), /* @__PURE__ */ React8.createElement(Box8, null, sectionLabel("Members"), team.members_with_roles?.length ? /* @__PURE__ */ React8.createElement(
|
|
3300
3452
|
TableContainer4,
|
|
3301
3453
|
{
|
|
3302
3454
|
component: Paper6,
|
|
@@ -4478,6 +4630,201 @@ var LiteLLMHomeWidget = ({
|
|
|
4478
4630
|
|
|
4479
4631
|
// src/index.ts
|
|
4480
4632
|
init_LiteLLMBudgetWidget();
|
|
4633
|
+
|
|
4634
|
+
// src/components/LiteLLMBudgetGauges.tsx
|
|
4635
|
+
init_api();
|
|
4636
|
+
init_format();
|
|
4637
|
+
init_ui();
|
|
4638
|
+
init_budget();
|
|
4639
|
+
import React14, { useEffect as useEffect5, useMemo as useMemo10, useState as useState11 } from "react";
|
|
4640
|
+
import Paper9 from "@mui/material/Paper";
|
|
4641
|
+
import Box13 from "@mui/material/Box";
|
|
4642
|
+
import Typography13 from "@mui/material/Typography";
|
|
4643
|
+
import CircularProgress8 from "@mui/material/CircularProgress";
|
|
4644
|
+
import Alert8 from "@mui/material/Alert";
|
|
4645
|
+
import { useApi as useApi5 } from "@backstage/core-plugin-api";
|
|
4646
|
+
import { Link as Link2 } from "@backstage/core-components";
|
|
4647
|
+
var DEFAULT_KEYS_HREF = "/litellm?tab=keys";
|
|
4648
|
+
var LEVEL = {
|
|
4649
|
+
key: { name: "Key", none: "No key budget" },
|
|
4650
|
+
user: { name: "User", none: "No personal budget" },
|
|
4651
|
+
team: { name: "Team", none: "No team budget" }
|
|
4652
|
+
};
|
|
4653
|
+
function fmtUsdShort(n) {
|
|
4654
|
+
const v = n ?? 0;
|
|
4655
|
+
if (v >= 100 && Number.isInteger(v)) return `$${v}`;
|
|
4656
|
+
return fmtUsd(v);
|
|
4657
|
+
}
|
|
4658
|
+
function spendCaption(limit) {
|
|
4659
|
+
if (limit.hidden) return "hidden by admin";
|
|
4660
|
+
return `${fmtUsdShort(limit.spend)} / ${fmtUsdShort(limit.budget)}`;
|
|
4661
|
+
}
|
|
4662
|
+
function resetLabel(limit) {
|
|
4663
|
+
const window2 = fmtBudgetDuration(limit.budgetDuration);
|
|
4664
|
+
return window2 ? `resets ${window2}` : "never resets";
|
|
4665
|
+
}
|
|
4666
|
+
var LevelGauge = ({
|
|
4667
|
+
gauge,
|
|
4668
|
+
size,
|
|
4669
|
+
keysHref
|
|
4670
|
+
}) => {
|
|
4671
|
+
const { name, none } = LEVEL[gauge.kind];
|
|
4672
|
+
const limit = gauge.limit;
|
|
4673
|
+
const tone = limit ? budgetTone2(limit.pct) : "neutral";
|
|
4674
|
+
const extra = gauge.kind === "key" ? Math.max(0, gauge.count - 1) : 0;
|
|
4675
|
+
return /* @__PURE__ */ React14.createElement(
|
|
4676
|
+
Box13,
|
|
4677
|
+
{
|
|
4678
|
+
sx: {
|
|
4679
|
+
display: "flex",
|
|
4680
|
+
flexDirection: "column",
|
|
4681
|
+
alignItems: "center",
|
|
4682
|
+
minWidth: 0,
|
|
4683
|
+
flex: 1
|
|
4684
|
+
}
|
|
4685
|
+
},
|
|
4686
|
+
/* @__PURE__ */ React14.createElement(
|
|
4687
|
+
Gauge,
|
|
4688
|
+
{
|
|
4689
|
+
value: limit?.pct ?? 0,
|
|
4690
|
+
tone,
|
|
4691
|
+
size,
|
|
4692
|
+
label: limit ? `${Math.round(limit.pct)}%` : "\u2014"
|
|
4693
|
+
}
|
|
4694
|
+
),
|
|
4695
|
+
/* @__PURE__ */ React14.createElement(
|
|
4696
|
+
Typography13,
|
|
4697
|
+
{
|
|
4698
|
+
sx: (theme) => ({
|
|
4699
|
+
mt: 0.75,
|
|
4700
|
+
fontSize: 10.5,
|
|
4701
|
+
fontWeight: 700,
|
|
4702
|
+
letterSpacing: "0.08em",
|
|
4703
|
+
textTransform: "uppercase",
|
|
4704
|
+
color: tone === "neutral" ? theme.palette.text.secondary : void 0
|
|
4705
|
+
})
|
|
4706
|
+
},
|
|
4707
|
+
name
|
|
4708
|
+
),
|
|
4709
|
+
limit ? /* @__PURE__ */ React14.createElement(React14.Fragment, null, /* @__PURE__ */ React14.createElement(
|
|
4710
|
+
Typography13,
|
|
4711
|
+
{
|
|
4712
|
+
variant: "caption",
|
|
4713
|
+
title: limit.sublabel ? `${limit.label} \xB7 ${limit.sublabel}` : limit.label,
|
|
4714
|
+
sx: {
|
|
4715
|
+
mt: 0.25,
|
|
4716
|
+
fontSize: 11,
|
|
4717
|
+
fontWeight: 600,
|
|
4718
|
+
maxWidth: "100%",
|
|
4719
|
+
overflow: "hidden",
|
|
4720
|
+
textOverflow: "ellipsis",
|
|
4721
|
+
whiteSpace: "nowrap"
|
|
4722
|
+
}
|
|
4723
|
+
},
|
|
4724
|
+
limit.label
|
|
4725
|
+
), /* @__PURE__ */ React14.createElement(
|
|
4726
|
+
Typography13,
|
|
4727
|
+
{
|
|
4728
|
+
variant: "caption",
|
|
4729
|
+
color: "text.secondary",
|
|
4730
|
+
sx: {
|
|
4731
|
+
fontSize: 10.5,
|
|
4732
|
+
fontVariantNumeric: "tabular-nums",
|
|
4733
|
+
textAlign: "center"
|
|
4734
|
+
}
|
|
4735
|
+
},
|
|
4736
|
+
spendCaption(limit)
|
|
4737
|
+
), extra > 0 ? /* @__PURE__ */ React14.createElement(
|
|
4738
|
+
Typography13,
|
|
4739
|
+
{
|
|
4740
|
+
component: Link2,
|
|
4741
|
+
to: keysHref,
|
|
4742
|
+
variant: "caption",
|
|
4743
|
+
sx: {
|
|
4744
|
+
fontSize: 10.5,
|
|
4745
|
+
color: "primary.main",
|
|
4746
|
+
textDecoration: "none",
|
|
4747
|
+
"&:hover": { textDecoration: "underline" }
|
|
4748
|
+
}
|
|
4749
|
+
},
|
|
4750
|
+
"+",
|
|
4751
|
+
extra,
|
|
4752
|
+
" more"
|
|
4753
|
+
) : /* @__PURE__ */ React14.createElement(Typography13, { variant: "caption", color: "text.secondary", sx: { fontSize: 10.5 } }, resetLabel(limit))) : /* @__PURE__ */ React14.createElement(
|
|
4754
|
+
Typography13,
|
|
4755
|
+
{
|
|
4756
|
+
variant: "caption",
|
|
4757
|
+
color: "text.secondary",
|
|
4758
|
+
sx: { mt: 0.25, fontSize: 10.5, textAlign: "center" }
|
|
4759
|
+
},
|
|
4760
|
+
none
|
|
4761
|
+
)
|
|
4762
|
+
);
|
|
4763
|
+
};
|
|
4764
|
+
var LiteLLMBudgetGauges = ({
|
|
4765
|
+
title = "Budget",
|
|
4766
|
+
size = 72,
|
|
4767
|
+
keysHref = DEFAULT_KEYS_HREF,
|
|
4768
|
+
action
|
|
4769
|
+
}) => {
|
|
4770
|
+
const api = useApi5(liteLlmApiRef);
|
|
4771
|
+
const [loading, setLoading] = useState11(true);
|
|
4772
|
+
const [error, setError] = useState11(null);
|
|
4773
|
+
const [user, setUser] = useState11(null);
|
|
4774
|
+
const [teams, setTeams] = useState11([]);
|
|
4775
|
+
const [keys, setKeys] = useState11([]);
|
|
4776
|
+
useEffect5(() => {
|
|
4777
|
+
let cancelled = false;
|
|
4778
|
+
setLoading(true);
|
|
4779
|
+
setError(null);
|
|
4780
|
+
Promise.allSettled([api.getUserInfo(), api.getTeams(), api.listKeys()]).then(
|
|
4781
|
+
([userResult, teamsResult, keysResult]) => {
|
|
4782
|
+
if (cancelled) return;
|
|
4783
|
+
if (userResult.status === "fulfilled") {
|
|
4784
|
+
setUser(userResult.value);
|
|
4785
|
+
} else {
|
|
4786
|
+
setUser(null);
|
|
4787
|
+
setError(
|
|
4788
|
+
userResult.reason?.message ?? "Failed to load your LiteLLM profile"
|
|
4789
|
+
);
|
|
4790
|
+
}
|
|
4791
|
+
setTeams(teamsResult.status === "fulfilled" ? teamsResult.value : []);
|
|
4792
|
+
setKeys(keysResult.status === "fulfilled" ? keysResult.value : []);
|
|
4793
|
+
setLoading(false);
|
|
4794
|
+
}
|
|
4795
|
+
);
|
|
4796
|
+
return () => {
|
|
4797
|
+
cancelled = true;
|
|
4798
|
+
};
|
|
4799
|
+
}, [api]);
|
|
4800
|
+
const summary = useMemo10(
|
|
4801
|
+
() => buildBudgetSummary(user, teams, keys, keys.length),
|
|
4802
|
+
[user, teams, keys]
|
|
4803
|
+
);
|
|
4804
|
+
const gauges = useMemo10(() => buildBudgetGauges(summary), [summary]);
|
|
4805
|
+
const headline = useMemo10(() => budgetHeadline(summary), [summary]);
|
|
4806
|
+
const summaryText = headline.count === 0 ? "no limits apply" : `${headline.count} limit${headline.count === 1 ? "" : "s"}`;
|
|
4807
|
+
return /* @__PURE__ */ React14.createElement(Paper9, { sx: { p: 2 } }, /* @__PURE__ */ React14.createElement(Box13, { sx: { display: "flex", alignItems: "center", gap: 1, mb: 1.5, minWidth: 0 } }, /* @__PURE__ */ React14.createElement(Typography13, { variant: "h6", lineHeight: 1.2, sx: { minWidth: 0, flex: 1 } }, title), !loading && !error && /* @__PURE__ */ React14.createElement(
|
|
4808
|
+
StatusPill,
|
|
4809
|
+
{
|
|
4810
|
+
label: summaryText,
|
|
4811
|
+
tone: budgetTone2(headline.closest?.pct ?? 0),
|
|
4812
|
+
dot: headline.count > 0
|
|
4813
|
+
}
|
|
4814
|
+
)), loading && /* @__PURE__ */ React14.createElement(Box13, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 110 }, /* @__PURE__ */ React14.createElement(CircularProgress8, { size: 28 })), !loading && error && /* @__PURE__ */ React14.createElement(Alert8, { severity: "error", sx: { mt: 0.5 } }, error), !loading && !error && /* @__PURE__ */ React14.createElement(Box13, { sx: { display: "flex", alignItems: "flex-start", gap: 1 } }, gauges.map((gauge) => /* @__PURE__ */ React14.createElement(LevelGauge, { key: gauge.kind, gauge, size, keysHref }))), action && /* @__PURE__ */ React14.createElement(
|
|
4815
|
+
Box13,
|
|
4816
|
+
{
|
|
4817
|
+
sx: (theme) => ({
|
|
4818
|
+
mt: 2,
|
|
4819
|
+
pt: 2,
|
|
4820
|
+
borderTop: `1px solid ${theme.palette.divider}`
|
|
4821
|
+
})
|
|
4822
|
+
},
|
|
4823
|
+
action
|
|
4824
|
+
));
|
|
4825
|
+
};
|
|
4826
|
+
|
|
4827
|
+
// src/index.ts
|
|
4481
4828
|
init_GenerateKeyDialog();
|
|
4482
4829
|
init_ManageTeamDialog();
|
|
4483
4830
|
init_api();
|
|
@@ -4486,6 +4833,7 @@ export {
|
|
|
4486
4833
|
DashboardHeader,
|
|
4487
4834
|
GenerateKeyDialog,
|
|
4488
4835
|
KeysTable,
|
|
4836
|
+
LiteLLMBudgetGauges,
|
|
4489
4837
|
LiteLLMBudgetWidget,
|
|
4490
4838
|
LiteLLMHomeWidget,
|
|
4491
4839
|
LiteLLMPage,
|