@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/budget.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { UserInfo, TeamInfo, VirtualKey } from './types';
|
|
2
|
+
import { Tone } from './components/ui';
|
|
3
|
+
/** One concrete budget limit the signed-in user is subject to. */
|
|
4
|
+
export interface BudgetLimit {
|
|
5
|
+
kind: 'key' | 'user' | 'team';
|
|
6
|
+
/** Display name: key_alias / "Personal budget" / team_alias. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** Secondary identifier: masked key, team_id, etc. */
|
|
9
|
+
sublabel?: string;
|
|
10
|
+
/** Spend cap in USD (> 0 when a cap exists). */
|
|
11
|
+
budget: number;
|
|
12
|
+
/** Spend so far in the current window. */
|
|
13
|
+
spend: number;
|
|
14
|
+
/** Reset window, e.g. "30d". Missing means the budget never resets. */
|
|
15
|
+
budgetDuration?: string;
|
|
16
|
+
/** User-facing warning threshold (soft limit). */
|
|
17
|
+
softLimit?: number;
|
|
18
|
+
/** Share of the cap spent, 0..100+ (unclamped). */
|
|
19
|
+
pct: number;
|
|
20
|
+
}
|
|
21
|
+
/** The budgets that apply to the current user, grouped by enforcement level. */
|
|
22
|
+
export interface BudgetSummary {
|
|
23
|
+
/** Budgeted keys, capped at `maxKeys`, closest to the cap first. */
|
|
24
|
+
keys: BudgetLimit[];
|
|
25
|
+
/** Budgeted keys hidden by the cap. */
|
|
26
|
+
hiddenBudgetedKeys: number;
|
|
27
|
+
/** Every key owned by the user (budgeted or not). */
|
|
28
|
+
totalKeyCount: number;
|
|
29
|
+
/** Personal budget, when one is set (undefined otherwise). */
|
|
30
|
+
user?: BudgetLimit;
|
|
31
|
+
/** Team budgets shared by the user's teams. */
|
|
32
|
+
teams: BudgetLimit[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Collapses the current user's data into the budget limits that LiteLLM
|
|
36
|
+
* actually enforces for them: key budgets (top `maxKeys`, closest to the cap
|
|
37
|
+
* first), then the personal budget (when set), then each team budget. Team
|
|
38
|
+
* budgets are enforced for every key bound to that team; a key *without* a
|
|
39
|
+
* team falls back to the owner's personal budget.
|
|
40
|
+
*/
|
|
41
|
+
export declare function buildBudgetSummary(user: UserInfo | null, teams: TeamInfo[], keys: VirtualKey[], maxKeys?: number): BudgetSummary;
|
|
42
|
+
/** "30d" → "every 30 days"; "1d" → "daily". Returns null when duration is unset. */
|
|
43
|
+
export declare function fmtBudgetDuration(duration?: string): string | null;
|
|
44
|
+
/** status pill tone + meter tone for a budget at `pct`. */
|
|
45
|
+
export declare function budgetTone(pct: number): Tone;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LiteLLM budget-policy widget. Renders the enforcement hierarchy as a
|
|
3
|
+
* numbered flow — key → personal → team → global — and, against each level
|
|
4
|
+
* the signed-in user actually has a limit on, a live meter showing how close
|
|
5
|
+
* they are to the cap. Intended for at-a-glance use on a homepage card next
|
|
6
|
+
* to `LiteLLMHomeWidget`, but works anywhere.
|
|
7
|
+
*
|
|
8
|
+
* The policy text mirrors the LiteLLM proxy behaviour:
|
|
9
|
+
* - every request is checked against stacked limits, in order;
|
|
10
|
+
* - a key bound to a team is capped by the team budgets, not the owner's
|
|
11
|
+
* personal budget;
|
|
12
|
+
* - a limit with a `budget_duration` resets its spend when the window
|
|
13
|
+
* closes; without one it never resets.
|
|
14
|
+
*/
|
|
15
|
+
import React from 'react';
|
|
16
|
+
export interface LiteLLMBudgetWidgetProps {
|
|
17
|
+
/** Optional title override. Defaults to 'Budget Policy'. */
|
|
18
|
+
title?: string;
|
|
19
|
+
/** Max key budgets to show, closest to the cap first. Defaults to 3. */
|
|
20
|
+
maxKeys?: number;
|
|
21
|
+
}
|
|
22
|
+
export declare const LiteLLMBudgetWidget: React.FC<LiteLLMBudgetWidgetProps>;
|
package/dist/index.cjs.js
CHANGED
|
@@ -3007,7 +3007,7 @@ function fmtTokens(n) {
|
|
|
3007
3007
|
if (n >= 1e3) return `${(n / 1e3).toFixed(0)}K`;
|
|
3008
3008
|
return String(n);
|
|
3009
3009
|
}
|
|
3010
|
-
var import_react8, import_Box8, import_Table5, import_TableBody5, import_TableCell5, import_TableContainer5, import_TableHead5, import_TableRow5, import_Typography8, import_MenuItem4, import_Select, import_FormControl, import_InputLabel,
|
|
3010
|
+
var import_react8, import_Box8, import_Table5, import_TableBody5, import_TableCell5, import_TableContainer5, import_TableHead5, import_TableRow5, import_Typography8, import_MenuItem4, import_Select, import_FormControl, import_InputLabel, import_TextField5, import_InputAdornment2, import_icons_material6, import_ContentCopy, import_Tooltip, import_Snackbar, import_Alert3, import_styles5, ALL_PROXY_MODELS2, ModelsTable;
|
|
3011
3011
|
var init_ModelsTable = __esm({
|
|
3012
3012
|
"src/components/ModelsTable.tsx"() {
|
|
3013
3013
|
"use strict";
|
|
@@ -3024,15 +3024,15 @@ var init_ModelsTable = __esm({
|
|
|
3024
3024
|
import_Select = __toESM(require("@mui/material/Select"));
|
|
3025
3025
|
import_FormControl = __toESM(require("@mui/material/FormControl"));
|
|
3026
3026
|
import_InputLabel = __toESM(require("@mui/material/InputLabel"));
|
|
3027
|
+
import_TextField5 = __toESM(require("@mui/material/TextField"));
|
|
3028
|
+
import_InputAdornment2 = __toESM(require("@mui/material/InputAdornment"));
|
|
3029
|
+
import_icons_material6 = require("@mui/icons-material");
|
|
3027
3030
|
import_ContentCopy = __toESM(require("@mui/icons-material/ContentCopy"));
|
|
3028
3031
|
import_Tooltip = __toESM(require("@mui/material/Tooltip"));
|
|
3029
3032
|
import_Snackbar = __toESM(require("@mui/material/Snackbar"));
|
|
3030
3033
|
import_Alert3 = __toESM(require("@mui/material/Alert"));
|
|
3031
3034
|
init_ui();
|
|
3032
3035
|
import_styles5 = require("@mui/material/styles");
|
|
3033
|
-
import_TextField5 = __toESM(require("@mui/material/TextField"));
|
|
3034
|
-
import_InputAdornment2 = __toESM(require("@mui/material/InputAdornment"));
|
|
3035
|
-
import_icons_material6 = require("@mui/icons-material");
|
|
3036
3036
|
ALL_PROXY_MODELS2 = "all-proxy-models";
|
|
3037
3037
|
ModelsTable = ({ allModels, teams, loading }) => {
|
|
3038
3038
|
const theme = (0, import_styles5.useTheme)();
|
|
@@ -3043,17 +3043,17 @@ var init_ModelsTable = __esm({
|
|
|
3043
3043
|
() => teams.find((t) => t.team_id === selectedTeamId) ?? null,
|
|
3044
3044
|
[teams, selectedTeamId]
|
|
3045
3045
|
);
|
|
3046
|
-
const
|
|
3046
|
+
const teamModels = (0, import_react8.useMemo)(() => {
|
|
3047
3047
|
if (!selectedTeam) return [];
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
}, [
|
|
3048
|
+
return allModels.filter((model) => isModelAllowedByTeam2(model, selectedTeam.models));
|
|
3049
|
+
}, [allModels, selectedTeam]);
|
|
3050
|
+
const filteredModels = (0, import_react8.useMemo)(() => {
|
|
3051
|
+
const q = filterText.trim().toLowerCase();
|
|
3052
|
+
if (!q) return teamModels;
|
|
3053
|
+
return teamModels.filter(
|
|
3054
|
+
(model) => model.model_name.toLowerCase().includes(q) || (model.access_groups || []).some((group) => group.toLowerCase().includes(q))
|
|
3055
|
+
);
|
|
3056
|
+
}, [teamModels, filterText]);
|
|
3057
3057
|
const handleCopyModelId = (0, import_react8.useCallback)((modelId) => {
|
|
3058
3058
|
navigator.clipboard.writeText(modelId).then(() => setCopiedSnackbar(true));
|
|
3059
3059
|
}, []);
|
|
@@ -3075,9 +3075,9 @@ var init_ModelsTable = __esm({
|
|
|
3075
3075
|
SectionCard,
|
|
3076
3076
|
{
|
|
3077
3077
|
title: "Available Models",
|
|
3078
|
-
subtitle: selectedTeam ? `${filteredModels.length} model${filteredModels.length
|
|
3078
|
+
subtitle: selectedTeam ? `${filteredModels.length}${filterText.trim() ? ` of ${teamModels.length}` : ""} model${filteredModels.length === 1 ? "" : "s"}` : "Select a team to view its models"
|
|
3079
3079
|
},
|
|
3080
|
-
/* @__PURE__ */ import_react8.default.createElement(import_Box8.default, { sx: { px: 2.5, pb: 2.5, pt: 1 } }, /* @__PURE__ */ import_react8.default.createElement(import_FormControl.default, { size: "small", sx: { minWidth: 240, mb: 2 } }, /* @__PURE__ */ import_react8.default.createElement(import_InputLabel.default, { id: "team-select-label" }, "Team"), /* @__PURE__ */ import_react8.default.createElement(
|
|
3080
|
+
/* @__PURE__ */ import_react8.default.createElement(import_Box8.default, { sx: { px: 2.5, pb: 2.5, pt: 1, display: "flex", gap: 2, flexWrap: "wrap" } }, /* @__PURE__ */ import_react8.default.createElement(import_FormControl.default, { size: "small", sx: { minWidth: 240, mb: 2 } }, /* @__PURE__ */ import_react8.default.createElement(import_InputLabel.default, { id: "team-select-label" }, "Team"), /* @__PURE__ */ import_react8.default.createElement(
|
|
3081
3081
|
import_Select.default,
|
|
3082
3082
|
{
|
|
3083
3083
|
labelId: "team-select-label",
|
|
@@ -3093,6 +3093,7 @@ var init_ModelsTable = __esm({
|
|
|
3093
3093
|
size: "small",
|
|
3094
3094
|
value: filterText,
|
|
3095
3095
|
onChange: (e) => setFilterText(e.target.value),
|
|
3096
|
+
disabled: !selectedTeamId,
|
|
3096
3097
|
sx: { minWidth: 240, mb: 2 },
|
|
3097
3098
|
InputProps: {
|
|
3098
3099
|
startAdornment: /* @__PURE__ */ import_react8.default.createElement(import_InputAdornment2.default, { position: "start" }, /* @__PURE__ */ import_react8.default.createElement(import_icons_material6.Search, { fontSize: "small" }))
|
|
@@ -3107,14 +3108,32 @@ var init_ModelsTable = __esm({
|
|
|
3107
3108
|
hint: "Each team may have a different set of models assigned"
|
|
3108
3109
|
}
|
|
3109
3110
|
),
|
|
3110
|
-
!loading && selectedTeamId &&
|
|
3111
|
+
!loading && selectedTeamId && teamModels.length === 0 && /* @__PURE__ */ import_react8.default.createElement(
|
|
3111
3112
|
EmptyState,
|
|
3112
3113
|
{
|
|
3113
3114
|
message: "No models available for this team",
|
|
3114
3115
|
hint: allModels.length === 0 ? "No models are configured in the system" : "None of the team's assigned models were found in the catalog"
|
|
3115
3116
|
}
|
|
3116
3117
|
),
|
|
3117
|
-
!loading &&
|
|
3118
|
+
!loading && selectedTeamId && teamModels.length > 0 && filteredModels.length === 0 && /* @__PURE__ */ import_react8.default.createElement(
|
|
3119
|
+
EmptyState,
|
|
3120
|
+
{
|
|
3121
|
+
message: "No models match your search",
|
|
3122
|
+
hint: "Try a different search term"
|
|
3123
|
+
}
|
|
3124
|
+
),
|
|
3125
|
+
!loading && filteredModels.length > 0 && /* @__PURE__ */ import_react8.default.createElement(import_TableContainer5.default, null, /* @__PURE__ */ import_react8.default.createElement(import_Table5.default, { size: "small", sx: dataTableSx }, /* @__PURE__ */ import_react8.default.createElement(import_TableHead5.default, null, /* @__PURE__ */ import_react8.default.createElement(import_TableRow5.default, null, /* @__PURE__ */ import_react8.default.createElement(import_TableCell5.default, null, "Model ID"), /* @__PURE__ */ import_react8.default.createElement(import_TableCell5.default, null, "Mode"), /* @__PURE__ */ import_react8.default.createElement(import_TableCell5.default, { align: "right" }, "Input Cost"), /* @__PURE__ */ import_react8.default.createElement(import_TableCell5.default, { align: "right" }, "Output Cost"), /* @__PURE__ */ import_react8.default.createElement(import_TableCell5.default, { align: "right" }, "Max Input"), /* @__PURE__ */ import_react8.default.createElement(import_TableCell5.default, { align: "right" }, "Max Output"), /* @__PURE__ */ import_react8.default.createElement(import_TableCell5.default, null, "Capabilities"))), /* @__PURE__ */ import_react8.default.createElement(import_TableBody5.default, null, filteredModels.map((m) => /* @__PURE__ */ import_react8.default.createElement(import_TableRow5.default, { key: m.model_name, hover: true }, /* @__PURE__ */ import_react8.default.createElement(import_TableCell5.default, null, /* @__PURE__ */ import_react8.default.createElement(import_Box8.default, { sx: { display: "flex", alignItems: "center", gap: 0.5 } }, /* @__PURE__ */ import_react8.default.createElement(import_Tooltip.default, { title: m.model_name, placement: "top" }, /* @__PURE__ */ import_react8.default.createElement(
|
|
3126
|
+
"span",
|
|
3127
|
+
{
|
|
3128
|
+
role: "button",
|
|
3129
|
+
tabIndex: 0,
|
|
3130
|
+
"aria-label": "Copy model ID",
|
|
3131
|
+
onClick: () => handleCopyModelId(m.model_name),
|
|
3132
|
+
onKeyDown: (e) => e.key === "Enter" && handleCopyModelId(m.model_name),
|
|
3133
|
+
style: { cursor: "pointer", display: "inline-flex" }
|
|
3134
|
+
},
|
|
3135
|
+
/* @__PURE__ */ import_react8.default.createElement(TagChip, { label: m.model_name, title: "Copy model ID" })
|
|
3136
|
+
)), /* @__PURE__ */ import_react8.default.createElement(import_Tooltip.default, { title: "Copy model ID" }, /* @__PURE__ */ import_react8.default.createElement(
|
|
3118
3137
|
import_Box8.default,
|
|
3119
3138
|
{
|
|
3120
3139
|
component: "span",
|
|
@@ -3854,6 +3873,7 @@ __export(index_exports, {
|
|
|
3854
3873
|
DashboardHeader: () => DashboardHeader,
|
|
3855
3874
|
GenerateKeyDialog: () => GenerateKeyDialog,
|
|
3856
3875
|
KeysTable: () => KeysTable,
|
|
3876
|
+
LiteLLMBudgetWidget: () => LiteLLMBudgetWidget,
|
|
3857
3877
|
LiteLLMHomeWidget: () => LiteLLMHomeWidget,
|
|
3858
3878
|
LiteLLMPage: () => LiteLLMPage,
|
|
3859
3879
|
LiteLlmApi: () => LiteLlmApi,
|
|
@@ -4005,6 +4025,280 @@ var LiteLLMHomeWidget = ({
|
|
|
4005
4025
|
))))));
|
|
4006
4026
|
};
|
|
4007
4027
|
|
|
4028
|
+
// src/components/LiteLLMBudgetWidget.tsx
|
|
4029
|
+
var import_react13 = __toESM(require("react"));
|
|
4030
|
+
var import_Paper8 = __toESM(require("@mui/material/Paper"));
|
|
4031
|
+
var import_Box12 = __toESM(require("@mui/material/Box"));
|
|
4032
|
+
var import_Typography12 = __toESM(require("@mui/material/Typography"));
|
|
4033
|
+
var import_CircularProgress7 = __toESM(require("@mui/material/CircularProgress"));
|
|
4034
|
+
var import_Alert7 = __toESM(require("@mui/material/Alert"));
|
|
4035
|
+
var import_styles7 = require("@mui/material/styles");
|
|
4036
|
+
var import_core_plugin_api5 = require("@backstage/core-plugin-api");
|
|
4037
|
+
init_api();
|
|
4038
|
+
init_format();
|
|
4039
|
+
init_ui();
|
|
4040
|
+
|
|
4041
|
+
// src/budget.ts
|
|
4042
|
+
function limitPct(spend, budget) {
|
|
4043
|
+
if (budget <= 0) return 0;
|
|
4044
|
+
return spend / budget * 100;
|
|
4045
|
+
}
|
|
4046
|
+
function buildBudgetSummary(user, teams, keys, maxKeys = 3) {
|
|
4047
|
+
const budgeted = keys.filter((k) => (k.max_budget ?? 0) > 0).map((k) => {
|
|
4048
|
+
const spend = k.spend ?? 0;
|
|
4049
|
+
return {
|
|
4050
|
+
kind: "key",
|
|
4051
|
+
label: k.key_alias || k.key || "Untitled key",
|
|
4052
|
+
sublabel: k.key,
|
|
4053
|
+
budget: k.max_budget,
|
|
4054
|
+
spend,
|
|
4055
|
+
budgetDuration: k.budget_duration,
|
|
4056
|
+
pct: limitPct(spend, k.max_budget)
|
|
4057
|
+
};
|
|
4058
|
+
}).sort((a, b) => b.pct - a.pct);
|
|
4059
|
+
const userBudget = user && (user.max_budget ?? user.hard_limit ?? 0) > 0 ? (() => {
|
|
4060
|
+
const budget = user.max_budget ?? user.hard_limit ?? 0;
|
|
4061
|
+
const spend = user.current_spend ?? user.spend ?? 0;
|
|
4062
|
+
return {
|
|
4063
|
+
kind: "user",
|
|
4064
|
+
label: "Personal budget",
|
|
4065
|
+
sublabel: user.user_email || user.user_id,
|
|
4066
|
+
budget,
|
|
4067
|
+
spend,
|
|
4068
|
+
budgetDuration: user.budget_duration,
|
|
4069
|
+
softLimit: user.soft_limit,
|
|
4070
|
+
pct: limitPct(spend, budget)
|
|
4071
|
+
};
|
|
4072
|
+
})() : void 0;
|
|
4073
|
+
const teamLimits = teams.filter((t) => (t.max_budget ?? 0) > 0).map((t) => {
|
|
4074
|
+
const spend = t.spend ?? 0;
|
|
4075
|
+
return {
|
|
4076
|
+
kind: "team",
|
|
4077
|
+
label: t.team_alias || "Untitled team",
|
|
4078
|
+
sublabel: t.team_id,
|
|
4079
|
+
budget: t.max_budget,
|
|
4080
|
+
spend,
|
|
4081
|
+
budgetDuration: t.budget_duration,
|
|
4082
|
+
pct: limitPct(spend, t.max_budget)
|
|
4083
|
+
};
|
|
4084
|
+
}).sort((a, b) => b.pct - a.pct);
|
|
4085
|
+
return {
|
|
4086
|
+
keys: budgeted.slice(0, maxKeys),
|
|
4087
|
+
hiddenBudgetedKeys: Math.max(0, budgeted.length - maxKeys),
|
|
4088
|
+
totalKeyCount: keys.length,
|
|
4089
|
+
user: userBudget,
|
|
4090
|
+
teams: teamLimits
|
|
4091
|
+
};
|
|
4092
|
+
}
|
|
4093
|
+
function fmtBudgetDuration(duration) {
|
|
4094
|
+
if (!duration) return null;
|
|
4095
|
+
const m = /^(\d+)(s|m|h|d)$/.exec(duration);
|
|
4096
|
+
if (!m) return duration;
|
|
4097
|
+
const n = Number(m[1]);
|
|
4098
|
+
const unit = m[2];
|
|
4099
|
+
if (unit === "d") return n === 1 ? "daily" : `every ${n} days`;
|
|
4100
|
+
if (unit === "h") return n === 1 ? "hourly" : `every ${n} hours`;
|
|
4101
|
+
if (unit === "m") return n === 1 ? "every minute" : `every ${n} minutes`;
|
|
4102
|
+
return n === 1 ? "every second" : `every ${n} seconds`;
|
|
4103
|
+
}
|
|
4104
|
+
function budgetTone3(pct) {
|
|
4105
|
+
if (pct >= 100) return "danger";
|
|
4106
|
+
if (pct >= 80) return "warning";
|
|
4107
|
+
return "accent";
|
|
4108
|
+
}
|
|
4109
|
+
|
|
4110
|
+
// src/components/LiteLLMBudgetWidget.tsx
|
|
4111
|
+
function levelToneColor(tone, theme) {
|
|
4112
|
+
switch (tone) {
|
|
4113
|
+
case "success":
|
|
4114
|
+
return theme.palette.success.main;
|
|
4115
|
+
case "warning":
|
|
4116
|
+
return theme.palette.warning.main;
|
|
4117
|
+
case "danger":
|
|
4118
|
+
return theme.palette.error.main;
|
|
4119
|
+
case "info":
|
|
4120
|
+
return theme.palette.info.main;
|
|
4121
|
+
default:
|
|
4122
|
+
return theme.palette.primary.main;
|
|
4123
|
+
}
|
|
4124
|
+
}
|
|
4125
|
+
var LevelFrame = ({
|
|
4126
|
+
rank,
|
|
4127
|
+
name,
|
|
4128
|
+
tagline,
|
|
4129
|
+
note,
|
|
4130
|
+
tone,
|
|
4131
|
+
isLast,
|
|
4132
|
+
children
|
|
4133
|
+
}) => /* @__PURE__ */ import_react13.default.createElement(import_Box12.default, { sx: { display: "flex", gap: 1.5 } }, /* @__PURE__ */ import_react13.default.createElement(import_Box12.default, { sx: { display: "flex", flexDirection: "column", alignItems: "center", width: 26, flexShrink: 0 } }, /* @__PURE__ */ import_react13.default.createElement(
|
|
4134
|
+
import_Box12.default,
|
|
4135
|
+
{
|
|
4136
|
+
sx: (theme) => ({
|
|
4137
|
+
width: 26,
|
|
4138
|
+
height: 26,
|
|
4139
|
+
borderRadius: "50%",
|
|
4140
|
+
display: "flex",
|
|
4141
|
+
alignItems: "center",
|
|
4142
|
+
justifyContent: "center",
|
|
4143
|
+
bgcolor: (0, import_styles7.alpha)(levelToneColor(tone, theme), 0.14),
|
|
4144
|
+
color: levelToneColor(tone, theme),
|
|
4145
|
+
fontSize: 12,
|
|
4146
|
+
fontWeight: 700,
|
|
4147
|
+
flexShrink: 0
|
|
4148
|
+
})
|
|
4149
|
+
},
|
|
4150
|
+
rank
|
|
4151
|
+
), /* @__PURE__ */ import_react13.default.createElement(
|
|
4152
|
+
import_Box12.default,
|
|
4153
|
+
{
|
|
4154
|
+
sx: (theme) => ({
|
|
4155
|
+
flex: 1,
|
|
4156
|
+
width: 2,
|
|
4157
|
+
minHeight: 14,
|
|
4158
|
+
bgcolor: isLast ? "transparent" : (0, import_styles7.alpha)(theme.palette.text.primary, 0.12)
|
|
4159
|
+
})
|
|
4160
|
+
}
|
|
4161
|
+
)), /* @__PURE__ */ import_react13.default.createElement(import_Box12.default, { sx: { flex: 1, minWidth: 0, pb: isLast ? 0 : 2 } }, /* @__PURE__ */ import_react13.default.createElement(import_Box12.default, { sx: { display: "flex", alignItems: "baseline", gap: 1, flexWrap: "wrap", mb: 0.75 } }, /* @__PURE__ */ import_react13.default.createElement(
|
|
4162
|
+
import_Typography12.default,
|
|
4163
|
+
{
|
|
4164
|
+
sx: (theme) => ({
|
|
4165
|
+
fontSize: 12.5,
|
|
4166
|
+
fontWeight: 700,
|
|
4167
|
+
letterSpacing: "0.08em",
|
|
4168
|
+
textTransform: "uppercase",
|
|
4169
|
+
color: levelToneColor(tone, theme)
|
|
4170
|
+
})
|
|
4171
|
+
},
|
|
4172
|
+
name
|
|
4173
|
+
), /* @__PURE__ */ import_react13.default.createElement(import_Typography12.default, { variant: "caption", color: "text.secondary", sx: { fontSize: 11.5 } }, tagline), note && /* @__PURE__ */ import_react13.default.createElement(StatusPill, { label: note, tone: "neutral", dot: false, sx: { ml: "auto" } })), /* @__PURE__ */ import_react13.default.createElement(import_Box12.default, { sx: { display: "flex", flexDirection: "column", gap: 1 } }, children)));
|
|
4174
|
+
var EmptyLimitCard = ({ children }) => /* @__PURE__ */ import_react13.default.createElement(
|
|
4175
|
+
import_Paper8.default,
|
|
4176
|
+
{
|
|
4177
|
+
variant: "outlined",
|
|
4178
|
+
sx: (theme) => ({
|
|
4179
|
+
px: 1.5,
|
|
4180
|
+
py: 1.25,
|
|
4181
|
+
borderRadius: 1.5,
|
|
4182
|
+
bgcolor: (0, import_styles7.alpha)(theme.palette.text.primary, theme.palette.mode === "dark" ? 0.03 : 0.015)
|
|
4183
|
+
})
|
|
4184
|
+
},
|
|
4185
|
+
/* @__PURE__ */ import_react13.default.createElement(import_Typography12.default, { variant: "body2", color: "text.secondary", sx: { fontSize: 13 } }, children)
|
|
4186
|
+
);
|
|
4187
|
+
var LimitCard = ({ limit }) => {
|
|
4188
|
+
const tone = budgetTone3(limit.pct);
|
|
4189
|
+
const pct = Math.round(limit.pct);
|
|
4190
|
+
const closeTo = limit.softLimit !== void 0 && limit.spend >= limit.softLimit;
|
|
4191
|
+
return /* @__PURE__ */ import_react13.default.createElement(import_Paper8.default, { variant: "outlined", sx: { px: 1.5, py: 1.25, borderRadius: 1.5 } }, /* @__PURE__ */ import_react13.default.createElement(import_Box12.default, { sx: { display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 1.5 } }, /* @__PURE__ */ import_react13.default.createElement(import_Box12.default, { sx: { minWidth: 0, flex: 1 } }, /* @__PURE__ */ import_react13.default.createElement(
|
|
4192
|
+
import_Typography12.default,
|
|
4193
|
+
{
|
|
4194
|
+
variant: "body2",
|
|
4195
|
+
sx: { fontWeight: 600, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
4196
|
+
title: limit.sublabel ? `${limit.label} \xB7 ${limit.sublabel}` : limit.label
|
|
4197
|
+
},
|
|
4198
|
+
limit.label
|
|
4199
|
+
), limit.sublabel && /* @__PURE__ */ import_react13.default.createElement(
|
|
4200
|
+
import_Typography12.default,
|
|
4201
|
+
{
|
|
4202
|
+
variant: "caption",
|
|
4203
|
+
color: "text.secondary",
|
|
4204
|
+
sx: {
|
|
4205
|
+
fontSize: 10.5,
|
|
4206
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
4207
|
+
display: "block",
|
|
4208
|
+
overflow: "hidden",
|
|
4209
|
+
textOverflow: "ellipsis",
|
|
4210
|
+
whiteSpace: "nowrap"
|
|
4211
|
+
}
|
|
4212
|
+
},
|
|
4213
|
+
limit.sublabel
|
|
4214
|
+
)), /* @__PURE__ */ import_react13.default.createElement(import_Typography12.default, { variant: "body2", sx: { fontWeight: 600, fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap" } }, fmtUsd(limit.spend), /* @__PURE__ */ import_react13.default.createElement(import_Box12.default, { component: "span", color: "text.secondary", sx: { fontWeight: 400 } }, " / ", fmtUsd(limit.budget)))), /* @__PURE__ */ import_react13.default.createElement(import_Box12.default, { sx: { mt: 1 } }, /* @__PURE__ */ import_react13.default.createElement(Meter, { value: limit.pct, tone, height: 6 })), /* @__PURE__ */ import_react13.default.createElement(
|
|
4215
|
+
import_Box12.default,
|
|
4216
|
+
{
|
|
4217
|
+
sx: {
|
|
4218
|
+
display: "flex",
|
|
4219
|
+
justifyContent: "space-between",
|
|
4220
|
+
alignItems: "center",
|
|
4221
|
+
gap: 1,
|
|
4222
|
+
mt: 0.75
|
|
4223
|
+
}
|
|
4224
|
+
},
|
|
4225
|
+
/* @__PURE__ */ import_react13.default.createElement(import_Typography12.default, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, pct, "% of budget", closeTo && ` \xB7 soft-limit warning`),
|
|
4226
|
+
/* @__PURE__ */ import_react13.default.createElement(import_Typography12.default, { variant: "caption", color: "text.secondary" }, fmtBudgetDuration(limit.budgetDuration) ? `resets ${fmtBudgetDuration(limit.budgetDuration)}` : "never resets")
|
|
4227
|
+
));
|
|
4228
|
+
};
|
|
4229
|
+
var LiteLLMBudgetWidget = ({
|
|
4230
|
+
title = "Budget Policy",
|
|
4231
|
+
maxKeys = 3
|
|
4232
|
+
}) => {
|
|
4233
|
+
const api = (0, import_core_plugin_api5.useApi)(liteLlmApiRef);
|
|
4234
|
+
const [loading, setLoading] = (0, import_react13.useState)(true);
|
|
4235
|
+
const [error, setError] = (0, import_react13.useState)(null);
|
|
4236
|
+
const [user, setUser] = (0, import_react13.useState)(null);
|
|
4237
|
+
const [teams, setTeams] = (0, import_react13.useState)([]);
|
|
4238
|
+
const [keys, setKeys] = (0, import_react13.useState)([]);
|
|
4239
|
+
(0, import_react13.useEffect)(() => {
|
|
4240
|
+
let cancelled = false;
|
|
4241
|
+
setLoading(true);
|
|
4242
|
+
setError(null);
|
|
4243
|
+
Promise.allSettled([api.getUserInfo(), api.getTeams(), api.listKeys()]).then(
|
|
4244
|
+
([userResult, teamsResult, keysResult]) => {
|
|
4245
|
+
if (cancelled) return;
|
|
4246
|
+
if (userResult.status === "fulfilled") {
|
|
4247
|
+
setUser(userResult.value);
|
|
4248
|
+
} else {
|
|
4249
|
+
setUser(null);
|
|
4250
|
+
setError(
|
|
4251
|
+
userResult.reason?.message ?? "Failed to load your LiteLLM profile"
|
|
4252
|
+
);
|
|
4253
|
+
}
|
|
4254
|
+
setTeams(teamsResult.status === "fulfilled" ? teamsResult.value : []);
|
|
4255
|
+
setKeys(keysResult.status === "fulfilled" ? keysResult.value : []);
|
|
4256
|
+
setLoading(false);
|
|
4257
|
+
}
|
|
4258
|
+
);
|
|
4259
|
+
return () => {
|
|
4260
|
+
cancelled = true;
|
|
4261
|
+
};
|
|
4262
|
+
}, [api]);
|
|
4263
|
+
const hasAnyLimit = !!user && (user.max_budget ?? user.hard_limit ?? 0) > 0;
|
|
4264
|
+
const summary = (0, import_react13.useMemo)(() => buildBudgetSummary(user, teams, keys, maxKeys), [user, teams, keys, maxKeys]);
|
|
4265
|
+
const hasBudgetedKeys = keys.some((k) => (k.max_budget ?? 0) > 0);
|
|
4266
|
+
return /* @__PURE__ */ import_react13.default.createElement(import_Paper8.default, { sx: { p: 2 } }, /* @__PURE__ */ import_react13.default.createElement(import_Box12.default, { sx: { mb: 1.5 } }, /* @__PURE__ */ import_react13.default.createElement(import_Typography12.default, { variant: "h6", lineHeight: 1.2 }, title), /* @__PURE__ */ import_react13.default.createElement(import_Typography12.default, { variant: "caption", color: "text.secondary" }, "How LiteLLM caps spend \u2014 and where you stand right now")), loading && /* @__PURE__ */ import_react13.default.createElement(import_Box12.default, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 140 }, /* @__PURE__ */ import_react13.default.createElement(import_CircularProgress7.default, { size: 32 })), !loading && error && /* @__PURE__ */ import_react13.default.createElement(import_Alert7.default, { severity: "error", sx: { mt: 0.5 } }, error), !loading && !error && /* @__PURE__ */ import_react13.default.createElement(import_react13.default.Fragment, null, /* @__PURE__ */ import_react13.default.createElement(
|
|
4267
|
+
LevelFrame,
|
|
4268
|
+
{
|
|
4269
|
+
rank: 1,
|
|
4270
|
+
name: "Key",
|
|
4271
|
+
tagline: "per-key cap \xB7 highest priority",
|
|
4272
|
+
note: hasBudgetedKeys ? void 0 : "no key budgets set"
|
|
4273
|
+
},
|
|
4274
|
+
!hasBudgetedKeys && /* @__PURE__ */ import_react13.default.createElement(EmptyLimitCard, null, "No key has a budget, so no key-level cap applies."),
|
|
4275
|
+
summary.keys.map((k) => /* @__PURE__ */ import_react13.default.createElement(LimitCard, { key: k.sublabel ?? k.label, limit: k })),
|
|
4276
|
+
summary.hiddenBudgetedKeys > 0 && /* @__PURE__ */ import_react13.default.createElement(import_Typography12.default, { variant: "caption", color: "text.secondary" }, "+", summary.hiddenBudgetedKeys, " more budgeted key", summary.hiddenBudgetedKeys > 1 ? "s" : "", " further from the cap")
|
|
4277
|
+
), /* @__PURE__ */ import_react13.default.createElement(
|
|
4278
|
+
LevelFrame,
|
|
4279
|
+
{
|
|
4280
|
+
rank: 2,
|
|
4281
|
+
name: "User",
|
|
4282
|
+
tagline: "cap on everything you spend with your own keys",
|
|
4283
|
+
note: "skipped for team-bound keys"
|
|
4284
|
+
},
|
|
4285
|
+
hasAnyLimit && summary.user ? /* @__PURE__ */ import_react13.default.createElement(LimitCard, { limit: summary.user }) : /* @__PURE__ */ import_react13.default.createElement(EmptyLimitCard, null, "No personal budget is set on your account.")
|
|
4286
|
+
), /* @__PURE__ */ import_react13.default.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__ */ import_react13.default.createElement(LimitCard, { key: t.sublabel ?? t.label, limit: t })) : /* @__PURE__ */ import_react13.default.createElement(EmptyLimitCard, null, "No team you belong to has a budget.")), /* @__PURE__ */ import_react13.default.createElement(LevelFrame, { rank: 4, name: "Global", tagline: "proxy-wide cap set by your admin", isLast: true }, /* @__PURE__ */ import_react13.default.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__ */ import_react13.default.createElement(
|
|
4287
|
+
import_Box12.default,
|
|
4288
|
+
{
|
|
4289
|
+
sx: (theme) => ({
|
|
4290
|
+
mt: 2,
|
|
4291
|
+
px: 1.5,
|
|
4292
|
+
py: 1.25,
|
|
4293
|
+
borderRadius: 1.5,
|
|
4294
|
+
border: "1px dashed",
|
|
4295
|
+
borderColor: (0, import_styles7.alpha)(theme.palette.text.primary, 0.18)
|
|
4296
|
+
})
|
|
4297
|
+
},
|
|
4298
|
+
/* @__PURE__ */ import_react13.default.createElement(import_Typography12.default, { 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.")
|
|
4299
|
+
)));
|
|
4300
|
+
};
|
|
4301
|
+
|
|
4008
4302
|
// src/index.ts
|
|
4009
4303
|
init_GenerateKeyDialog();
|
|
4010
4304
|
init_ManageTeamDialog();
|