@acarmisc/backstage-plugin-litellm 0.21.3 → 0.23.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 +6 -0
- package/dist/index.cjs.js +70 -32
- package/dist/index.cjs.js.map +2 -2
- package/dist/index.esm.js +84 -46
- package/dist/index.esm.js.map +3 -3
- package/dist/types.d.ts +13 -0
- package/package.json +1 -1
package/dist/budget.d.ts
CHANGED
|
@@ -17,6 +17,12 @@ export interface BudgetLimit {
|
|
|
17
17
|
softLimit?: number;
|
|
18
18
|
/** Share of the cap spent, 0..100+ (unclamped). */
|
|
19
19
|
pct: number;
|
|
20
|
+
/**
|
|
21
|
+
* True when the backend redacted the dollar amounts (team budget hiding).
|
|
22
|
+
* `budget`/`spend` are then 0 and only `pct` carries the signal — render
|
|
23
|
+
* the meter without `$` figures.
|
|
24
|
+
*/
|
|
25
|
+
hidden?: boolean;
|
|
20
26
|
}
|
|
21
27
|
/** The budgets that apply to the current user, grouped by enforcement level. */
|
|
22
28
|
export interface BudgetSummary {
|
package/dist/index.cjs.js
CHANGED
|
@@ -2023,6 +2023,8 @@ var init_ManageTeamDialog = __esm({
|
|
|
2023
2023
|
const [mcpError, setMcpError] = (0, import_react5.useState)(null);
|
|
2024
2024
|
const allowUnlimitedBudget = config?.teamManagement?.allowUnlimitedBudget ?? false;
|
|
2025
2025
|
const maxBudgetCeiling = config?.teamManagement?.maxBudgetCeiling ?? FALLBACK_BUDGET_CEILING;
|
|
2026
|
+
const hideBudgetForManagers = config?.display?.hideTeamBudgetForManagers ?? false;
|
|
2027
|
+
const budgetWriteOnly = hideBudgetForManagers && mode === "edit";
|
|
2026
2028
|
const members = (0, import_react5.useMemo)(() => team?.members_with_roles ?? [], [team]);
|
|
2027
2029
|
const showMembers = mode === "edit" && !!canManageMembers;
|
|
2028
2030
|
const catalogApi = (0, import_core_plugin_api2.useApi)(import_plugin_catalog_react.catalogApiRef);
|
|
@@ -2058,9 +2060,13 @@ var init_ManageTeamDialog = __esm({
|
|
|
2058
2060
|
if (mode === "edit" && team) {
|
|
2059
2061
|
setAlias(team.team_alias ?? "");
|
|
2060
2062
|
setModels(team.models ?? []);
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2063
|
+
if (hideBudgetForManagers) {
|
|
2064
|
+
setMaxBudget("");
|
|
2065
|
+
} else {
|
|
2066
|
+
setMaxBudget(
|
|
2067
|
+
team.max_budget ? String(team.max_budget) : String(maxBudgetCeiling)
|
|
2068
|
+
);
|
|
2069
|
+
}
|
|
2064
2070
|
setUnlimited(allowUnlimitedBudget && !team.max_budget);
|
|
2065
2071
|
setBudgetDuration(team.budget_duration ?? DEFAULT_BUDGET_DURATION);
|
|
2066
2072
|
} else {
|
|
@@ -2080,7 +2086,7 @@ var init_ManageTeamDialog = __esm({
|
|
|
2080
2086
|
setMcpIds(team?.object_permission?.mcp_servers ?? []);
|
|
2081
2087
|
setMcpError(null);
|
|
2082
2088
|
}
|
|
2083
|
-
}, [open, mode, team, allowUnlimitedBudget, maxBudgetCeiling]);
|
|
2089
|
+
}, [open, mode, team, allowUnlimitedBudget, maxBudgetCeiling, hideBudgetForManagers]);
|
|
2084
2090
|
const handleSubmit = async () => {
|
|
2085
2091
|
setError(null);
|
|
2086
2092
|
if (!alias.trim()) {
|
|
@@ -2092,18 +2098,21 @@ var init_ManageTeamDialog = __esm({
|
|
|
2092
2098
|
return;
|
|
2093
2099
|
}
|
|
2094
2100
|
if (!unlimited) {
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2101
|
+
const budgetLeftBlank = budgetWriteOnly && !maxBudget.trim();
|
|
2102
|
+
if (!budgetLeftBlank) {
|
|
2103
|
+
if (!maxBudget) {
|
|
2104
|
+
setError("Budget is required when unlimited budgets are disabled");
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
const budget = parseFloat(maxBudget);
|
|
2108
|
+
if (isNaN(budget) || budget <= 0) {
|
|
2109
|
+
setError("Budget must be a positive number");
|
|
2110
|
+
return;
|
|
2111
|
+
}
|
|
2112
|
+
if (budget > maxBudgetCeiling) {
|
|
2113
|
+
setError(`Budget cannot exceed $${maxBudgetCeiling}`);
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2107
2116
|
}
|
|
2108
2117
|
}
|
|
2109
2118
|
try {
|
|
@@ -2117,10 +2126,16 @@ var init_ManageTeamDialog = __esm({
|
|
|
2117
2126
|
...budgetDuration && { budget_duration: budgetDuration }
|
|
2118
2127
|
};
|
|
2119
2128
|
} else {
|
|
2129
|
+
let budgetField = {};
|
|
2130
|
+
if (unlimited) {
|
|
2131
|
+
budgetField = { max_budget: null };
|
|
2132
|
+
} else if (maxBudget.trim()) {
|
|
2133
|
+
budgetField = { max_budget: parseFloat(maxBudget) };
|
|
2134
|
+
}
|
|
2120
2135
|
payload = {
|
|
2121
2136
|
team_alias: alias.trim(),
|
|
2122
2137
|
models,
|
|
2123
|
-
...
|
|
2138
|
+
...budgetField,
|
|
2124
2139
|
...budgetDuration && { budget_duration: budgetDuration }
|
|
2125
2140
|
};
|
|
2126
2141
|
}
|
|
@@ -2226,13 +2241,13 @@ var init_ManageTeamDialog = __esm({
|
|
|
2226
2241
|
), /* @__PURE__ */ import_react5.default.createElement(
|
|
2227
2242
|
import_TextField3.default,
|
|
2228
2243
|
{
|
|
2229
|
-
label: "Max Budget ($)",
|
|
2244
|
+
label: budgetWriteOnly ? "New Max Budget ($)" : "Max Budget ($)",
|
|
2230
2245
|
type: "number",
|
|
2231
2246
|
value: maxBudget,
|
|
2232
2247
|
onChange: (e) => setMaxBudget(e.target.value),
|
|
2233
2248
|
disabled: submitting || unlimited,
|
|
2234
2249
|
inputProps: { min: 0, max: maxBudgetCeiling },
|
|
2235
|
-
helperText: `Maximum: $${maxBudgetCeiling}`,
|
|
2250
|
+
helperText: budgetWriteOnly ? "Current budget is hidden \u2014 leave blank to keep it, or enter a new value" : `Maximum: $${maxBudgetCeiling}`,
|
|
2236
2251
|
fullWidth: true
|
|
2237
2252
|
}
|
|
2238
2253
|
), allowUnlimitedBudget && /* @__PURE__ */ import_react5.default.createElement(
|
|
@@ -2788,7 +2803,19 @@ function buildBudgetSummary(user, teams, keys, maxKeys = 3) {
|
|
|
2788
2803
|
pct: limitPct(spend, budget)
|
|
2789
2804
|
};
|
|
2790
2805
|
})() : void 0;
|
|
2791
|
-
const teamLimits = teams.filter((t) => (t.max_budget ?? 0) > 0).map((t) => {
|
|
2806
|
+
const teamLimits = teams.filter((t) => (t.max_budget ?? 0) > 0 || t.budget_hidden === true).map((t) => {
|
|
2807
|
+
if (t.budget_hidden === true) {
|
|
2808
|
+
return {
|
|
2809
|
+
kind: "team",
|
|
2810
|
+
label: t.team_alias || "Untitled team",
|
|
2811
|
+
sublabel: t.team_id,
|
|
2812
|
+
budget: 0,
|
|
2813
|
+
spend: 0,
|
|
2814
|
+
budgetDuration: t.budget_duration,
|
|
2815
|
+
pct: Math.min(100, Math.max(0, t.budget_pct ?? 0)),
|
|
2816
|
+
hidden: true
|
|
2817
|
+
};
|
|
2818
|
+
}
|
|
2792
2819
|
const spend = t.spend ?? 0;
|
|
2793
2820
|
return {
|
|
2794
2821
|
kind: "team",
|
|
@@ -2964,7 +2991,7 @@ var init_LiteLLMBudgetWidget = __esm({
|
|
|
2964
2991
|
}
|
|
2965
2992
|
},
|
|
2966
2993
|
limit.sublabel
|
|
2967
|
-
)), /* @__PURE__ */ import_react7.default.createElement(import_Typography7.default, { variant: "body2", sx: { fontWeight: 600, fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap" } }, fmtUsd(limit.spend), /* @__PURE__ */ import_react7.default.createElement(import_Box7.default, { component: "span", color: "text.secondary", sx: { fontWeight: 400 } }, " / ", fmtUsd(limit.budget)))), /* @__PURE__ */ import_react7.default.createElement(import_Box7.default, { sx: { mt: 1 } }, /* @__PURE__ */ import_react7.default.createElement(Meter, { value: limit.pct, tone, height: 6 })), /* @__PURE__ */ import_react7.default.createElement(
|
|
2994
|
+
)), limit.hidden ? /* @__PURE__ */ import_react7.default.createElement(import_Typography7.default, { variant: "body2", sx: { fontWeight: 600, fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap" } }, pct, "% used") : /* @__PURE__ */ import_react7.default.createElement(import_Typography7.default, { variant: "body2", sx: { fontWeight: 600, fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap" } }, fmtUsd(limit.spend), /* @__PURE__ */ import_react7.default.createElement(import_Box7.default, { component: "span", color: "text.secondary", sx: { fontWeight: 400 } }, " / ", fmtUsd(limit.budget)))), /* @__PURE__ */ import_react7.default.createElement(import_Box7.default, { sx: { mt: 1 } }, /* @__PURE__ */ import_react7.default.createElement(Meter, { value: limit.pct, tone, height: 6 })), /* @__PURE__ */ import_react7.default.createElement(
|
|
2968
2995
|
import_Box7.default,
|
|
2969
2996
|
{
|
|
2970
2997
|
sx: {
|
|
@@ -2975,7 +3002,7 @@ var init_LiteLLMBudgetWidget = __esm({
|
|
|
2975
3002
|
mt: 0.75
|
|
2976
3003
|
}
|
|
2977
3004
|
},
|
|
2978
|
-
/* @__PURE__ */ import_react7.default.createElement(import_Typography7.default, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, pct, "% of budget", closeTo && ` \xB7 soft-limit warning`),
|
|
3005
|
+
/* @__PURE__ */ import_react7.default.createElement(import_Typography7.default, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, limit.hidden ? /* @__PURE__ */ import_react7.default.createElement(import_react7.default.Fragment, null, "Hidden by admin") : /* @__PURE__ */ import_react7.default.createElement(import_react7.default.Fragment, null, pct, "% of budget", closeTo && ` \xB7 soft-limit warning`)),
|
|
2979
3006
|
/* @__PURE__ */ import_react7.default.createElement(import_Typography7.default, { variant: "caption", color: "text.secondary" }, fmtBudgetDuration(limit.budgetDuration) ? `resets ${fmtBudgetDuration(limit.budgetDuration)}` : "never resets")
|
|
2980
3007
|
));
|
|
2981
3008
|
};
|
|
@@ -3156,6 +3183,15 @@ function budgetTone3(isOver, isNear) {
|
|
|
3156
3183
|
if (isNear) return "warning";
|
|
3157
3184
|
return "accent";
|
|
3158
3185
|
}
|
|
3186
|
+
function visibleBudgetPct(spend, budget) {
|
|
3187
|
+
if (budget <= 0) return 0;
|
|
3188
|
+
return Math.min(spend / budget * 100, 100);
|
|
3189
|
+
}
|
|
3190
|
+
function budgetStatValue(hidden, budget) {
|
|
3191
|
+
if (hidden) return "Hidden";
|
|
3192
|
+
if (budget > 0) return fmtUsd2(budget);
|
|
3193
|
+
return "Unlimited";
|
|
3194
|
+
}
|
|
3159
3195
|
var import_react8, import_Paper6, import_Box8, import_Typography8, import_Skeleton4, import_Divider3, import_Table4, import_TableBody4, import_TableCell4, import_TableHead4, import_TableRow4, import_TableContainer4, import_Collapse2, import_IconButton5, import_Button5, import_CircularProgress5, import_styles5, import_icons_material6, import_recharts2, fmtUsd2, TeamCard, TeamUsage;
|
|
3160
3196
|
var init_TeamUsage = __esm({
|
|
3161
3197
|
"src/components/TeamUsage.tsx"() {
|
|
@@ -3184,13 +3220,15 @@ var init_TeamUsage = __esm({
|
|
|
3184
3220
|
TeamCard = ({ team, usage, usageLoading, canManage, onEditTeam }) => {
|
|
3185
3221
|
const [expanded, setExpanded] = (0, import_react8.useState)(false);
|
|
3186
3222
|
const chart = useChartTheme();
|
|
3187
|
-
const
|
|
3188
|
-
const
|
|
3189
|
-
const
|
|
3190
|
-
const
|
|
3191
|
-
const
|
|
3223
|
+
const hidden = team.budget_hidden === true;
|
|
3224
|
+
const budget = hidden ? 0 : team.max_budget ?? 0;
|
|
3225
|
+
const spend = hidden ? 0 : team.spend ?? 0;
|
|
3226
|
+
const budgetPct = hidden ? Math.min(100, Math.max(0, team.budget_pct ?? 0)) : visibleBudgetPct(spend, budget);
|
|
3227
|
+
const isOver = hidden ? team.budget_status === "over" : budget > 0 && spend >= budget;
|
|
3228
|
+
const isNear = hidden ? team.budget_status === "near" : budget > 0 && spend >= budget * 0.8 && !isOver;
|
|
3192
3229
|
const dailyData = usage?.daily_usage?.map((d) => ({ date: d.date, spend: d.spend })) ?? [];
|
|
3193
3230
|
const renderDailySpendSection = () => {
|
|
3231
|
+
if (hidden) return null;
|
|
3194
3232
|
if (usageLoading) {
|
|
3195
3233
|
return /* @__PURE__ */ import_react8.default.createElement(import_Box8.default, { display: "flex", justifyContent: "center", py: 3 }, /* @__PURE__ */ import_react8.default.createElement(import_CircularProgress5.default, { size: 22 }));
|
|
3196
3234
|
}
|
|
@@ -3291,11 +3329,11 @@ var init_TeamUsage = __esm({
|
|
|
3291
3329
|
},
|
|
3292
3330
|
/* @__PURE__ */ import_react8.default.createElement(Stat, { label: "Members", value: team.members_with_roles?.length ?? "\u2014" }),
|
|
3293
3331
|
/* @__PURE__ */ import_react8.default.createElement(Stat, { label: "Models", value: team.models?.length ? team.models.length : "All" }),
|
|
3294
|
-
/* @__PURE__ */ import_react8.default.createElement(Stat, { label: "Budget", value:
|
|
3295
|
-
/* @__PURE__ */ import_react8.default.createElement(Stat, { label: "Spend", value: fmtUsd2(spend) }),
|
|
3332
|
+
/* @__PURE__ */ import_react8.default.createElement(Stat, { label: "Budget", value: budgetStatValue(hidden, budget) }),
|
|
3333
|
+
/* @__PURE__ */ import_react8.default.createElement(Stat, { label: "Spend", value: hidden ? "\u2014" : fmtUsd2(spend) }),
|
|
3296
3334
|
/* @__PURE__ */ import_react8.default.createElement(Stat, { label: "TPM", value: team.tpm_limit && team.tpm_limit > 0 ? team.tpm_limit.toLocaleString() : "\u2014" }),
|
|
3297
3335
|
/* @__PURE__ */ import_react8.default.createElement(Stat, { label: "RPM", value: team.rpm_limit && team.rpm_limit > 0 ? team.rpm_limit.toLocaleString() : "\u2014" })
|
|
3298
|
-
), budget > 0 && /* @__PURE__ */ import_react8.default.createElement(import_Box8.default, { mt: 2, maxWidth: 640 }, /* @__PURE__ */ import_react8.default.createElement(import_Box8.default, { display: "flex", justifyContent: "space-between", alignItems: "baseline", mb: 0.75 }, /* @__PURE__ */ import_react8.default.createElement(import_Typography8.default, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, fmtUsd2(spend), " of ", fmtUsd2(budget)), /* @__PURE__ */ import_react8.default.createElement(import_Typography8.default, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, budgetPct.toFixed(0), "%")), /* @__PURE__ */ import_react8.default.createElement(Meter, { value: budgetPct, tone: budgetTone3(isOver, isNear), height: 5 })), /* @__PURE__ */ import_react8.default.createElement(import_Collapse2.default, { in: expanded, unmountOnExit: true }, /* @__PURE__ */ import_react8.default.createElement(import_Divider3.default, { sx: { my: 2.5 } }), /* @__PURE__ */ import_react8.default.createElement(import_Box8.default, { mb: 2.5 }, sectionLabel("Models"), team.models?.length ? /* @__PURE__ */ import_react8.default.createElement(import_Box8.default, { display: "flex", gap: 0.5, flexWrap: "wrap" }, team.models.map((m) => /* @__PURE__ */ import_react8.default.createElement(TagChip, { key: m, label: m, title: m }))) : /* @__PURE__ */ import_react8.default.createElement(import_Typography8.default, { variant: "body2", color: "text.secondary" }, "All models allowed")), renderDailySpendSection(), /* @__PURE__ */ import_react8.default.createElement(import_Box8.default, null, sectionLabel("Members"), team.members_with_roles?.length ? /* @__PURE__ */ import_react8.default.createElement(
|
|
3336
|
+
), hidden && /* @__PURE__ */ import_react8.default.createElement(import_Box8.default, { mt: 2, maxWidth: 640 }, /* @__PURE__ */ import_react8.default.createElement(import_Box8.default, { display: "flex", justifyContent: "space-between", alignItems: "baseline", mb: 0.75 }, /* @__PURE__ */ import_react8.default.createElement(import_Typography8.default, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, budgetPct.toFixed(0), "% used"), /* @__PURE__ */ import_react8.default.createElement(import_Typography8.default, { variant: "caption", color: "text.secondary" }, "Hidden by admin")), /* @__PURE__ */ import_react8.default.createElement(Meter, { value: budgetPct, tone: budgetTone3(isOver, isNear), height: 5 }), /* @__PURE__ */ import_react8.default.createElement(import_Typography8.default, { 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__ */ import_react8.default.createElement(import_Box8.default, { mt: 2, maxWidth: 640 }, /* @__PURE__ */ import_react8.default.createElement(import_Box8.default, { display: "flex", justifyContent: "space-between", alignItems: "baseline", mb: 0.75 }, /* @__PURE__ */ import_react8.default.createElement(import_Typography8.default, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, fmtUsd2(spend), " of ", fmtUsd2(budget)), /* @__PURE__ */ import_react8.default.createElement(import_Typography8.default, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, budgetPct.toFixed(0), "%")), /* @__PURE__ */ import_react8.default.createElement(Meter, { value: budgetPct, tone: budgetTone3(isOver, isNear), height: 5 })), /* @__PURE__ */ import_react8.default.createElement(import_Collapse2.default, { in: expanded, unmountOnExit: true }, /* @__PURE__ */ import_react8.default.createElement(import_Divider3.default, { sx: { my: 2.5 } }), /* @__PURE__ */ import_react8.default.createElement(import_Box8.default, { mb: 2.5 }, sectionLabel("Models"), team.models?.length ? /* @__PURE__ */ import_react8.default.createElement(import_Box8.default, { display: "flex", gap: 0.5, flexWrap: "wrap" }, team.models.map((m) => /* @__PURE__ */ import_react8.default.createElement(TagChip, { key: m, label: m, title: m }))) : /* @__PURE__ */ import_react8.default.createElement(import_Typography8.default, { variant: "body2", color: "text.secondary" }, "All models allowed")), renderDailySpendSection(), /* @__PURE__ */ import_react8.default.createElement(import_Box8.default, null, sectionLabel("Members"), team.members_with_roles?.length ? /* @__PURE__ */ import_react8.default.createElement(
|
|
3299
3337
|
import_TableContainer4.default,
|
|
3300
3338
|
{
|
|
3301
3339
|
component: import_Paper6.default,
|
|
@@ -3763,10 +3801,10 @@ var init_AuditLog = __esm({
|
|
|
3763
3801
|
const [page, setPage] = (0, import_react10.useState)(0);
|
|
3764
3802
|
const [pageSize, setPageSize] = (0, import_react10.useState)(25);
|
|
3765
3803
|
const [filters, setFilters] = (0, import_react10.useState)({});
|
|
3766
|
-
const fetchParams = (0, import_react10.
|
|
3804
|
+
const fetchParams = (0, import_react10.useMemo)(
|
|
3767
3805
|
() => ({ page: page + 1, page_size: pageSize, ...filters }),
|
|
3768
3806
|
[page, pageSize, filters]
|
|
3769
|
-
)
|
|
3807
|
+
);
|
|
3770
3808
|
const { value, loading, error } = (0, import_react_use2.useAsync)(
|
|
3771
3809
|
() => api.getAuditLogs(fetchParams),
|
|
3772
3810
|
[api, fetchParams]
|