@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/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 {
|
|
@@ -39,6 +45,28 @@ export interface BudgetSummary {
|
|
|
39
45
|
* team falls back to the owner's personal budget.
|
|
40
46
|
*/
|
|
41
47
|
export declare function buildBudgetSummary(user: UserInfo | null, teams: TeamInfo[], keys: VirtualKey[], maxKeys?: number): BudgetSummary;
|
|
48
|
+
/** One gauge in the condensed homepage card — a single enforcement level. */
|
|
49
|
+
export interface BudgetGauge {
|
|
50
|
+
kind: 'key' | 'user' | 'team';
|
|
51
|
+
/**
|
|
52
|
+
* The limit of this level nearest its cap, or undefined when the user has
|
|
53
|
+
* no cap at this level (render an empty gauge).
|
|
54
|
+
*/
|
|
55
|
+
limit?: BudgetLimit;
|
|
56
|
+
/** How many limits apply at this level (1 for a personal budget). */
|
|
57
|
+
count: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Collapses a `BudgetSummary` to exactly one gauge per enforcement level —
|
|
61
|
+
* key, user, team — carrying the limit nearest its cap and how many limits
|
|
62
|
+
* that level holds. Every level is always returned, with `limit` undefined
|
|
63
|
+
* when the user has no cap there, so the caller can render a stable row.
|
|
64
|
+
*
|
|
65
|
+
* Pass a summary built with a large `maxKeys` (e.g. `keys.length`) so the
|
|
66
|
+
* nearest key is present even when it falls outside the display cap; a key
|
|
67
|
+
* count hidden by that cap is still reflected in `count`.
|
|
68
|
+
*/
|
|
69
|
+
export declare function buildBudgetGauges(summary: BudgetSummary): BudgetGauge[];
|
|
42
70
|
/**
|
|
43
71
|
* One-line summary of a `BudgetSummary`, for a collapsed/compact header:
|
|
44
72
|
* how many concrete limits the user is subject to, and the one closest to
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Condensed budget card for a homepage column: one ring gauge per
|
|
3
|
+
* enforcement level (key → personal → team), each showing the limit nearest
|
|
4
|
+
* its cap. Where the full `LiteLLMBudgetWidget` lists every limit as a
|
|
5
|
+
* spend-vs-cap meter, this trades detail for density — a single row of rings
|
|
6
|
+
* with a label and spend figure under each.
|
|
7
|
+
*
|
|
8
|
+
* Gauges are always rendered in the same order, with an empty ring when the
|
|
9
|
+
* user has no cap at that level, so the card keeps a stable shape as data
|
|
10
|
+
* loads and across users. When a level holds several limits, the ring shows
|
|
11
|
+
* the one closest to its cap and a "+N more" link counts the rest.
|
|
12
|
+
*/
|
|
13
|
+
import React from 'react';
|
|
14
|
+
export interface LiteLLMBudgetGaugesProps {
|
|
15
|
+
/** Optional title override. Defaults to 'Budget'. */
|
|
16
|
+
title?: string;
|
|
17
|
+
/** Ring diameter in px. Defaults to 72. */
|
|
18
|
+
size?: number;
|
|
19
|
+
/** Where the "+N more" key link points. Defaults to the Keys tab. */
|
|
20
|
+
keysHref?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Node rendered at the card bottom, below a divider — e.g. a "create key"
|
|
23
|
+
* button.
|
|
24
|
+
*/
|
|
25
|
+
action?: React.ReactNode;
|
|
26
|
+
}
|
|
27
|
+
export declare const LiteLLMBudgetGauges: React.FC<LiteLLMBudgetGaugesProps>;
|
package/dist/components/ui.d.ts
CHANGED
|
@@ -156,6 +156,26 @@ export declare const Meter: React.FC<{
|
|
|
156
156
|
tone?: Tone;
|
|
157
157
|
height?: number;
|
|
158
158
|
}>;
|
|
159
|
+
/**
|
|
160
|
+
* Ring gauge for at-a-glance ratios. Hand-drawn SVG rather than MUI's
|
|
161
|
+
* `CircularProgress`: the track has to stay a neutral hairline (MUI tints it
|
|
162
|
+
* from the bar colour, so an empty ring reads as full), and the value may
|
|
163
|
+
* legitimately exceed 100% — the arc clamps at a full turn while the centre
|
|
164
|
+
* label keeps the real number.
|
|
165
|
+
*/
|
|
166
|
+
export declare const Gauge: React.FC<{
|
|
167
|
+
/** Share consumed, 0..100+ (unclamped for the label). */
|
|
168
|
+
value: number;
|
|
169
|
+
tone?: Tone;
|
|
170
|
+
/** Outer diameter in px. */
|
|
171
|
+
size?: number;
|
|
172
|
+
/** Ring thickness in px. */
|
|
173
|
+
thickness?: number;
|
|
174
|
+
/** Centre content; defaults to the rounded percentage. */
|
|
175
|
+
label?: React.ReactNode;
|
|
176
|
+
/** Small caption under the value, inside the ring. */
|
|
177
|
+
caption?: React.ReactNode;
|
|
178
|
+
}>;
|
|
159
179
|
/**
|
|
160
180
|
* Secondary navigation inside a card. Hand-rolled rather than a second set of
|
|
161
181
|
* `Tabs` so it reads as subordinate to the page tabs instead of competing
|
package/dist/index.cjs.js
CHANGED
|
@@ -335,7 +335,7 @@ function SegmentedControl({
|
|
|
335
335
|
})
|
|
336
336
|
);
|
|
337
337
|
}
|
|
338
|
-
var import_react, import_Box, import_ButtonBase, import_Paper, import_Typography, import_styles, MONTHS, fmtDateShort, fmtCompact, fmtUsdCompact, CHART_COLORS, SERIES, OTHER_COLOR, ChartTooltip, SeriesLegend, StatusPill, TagChip, SectionCard, ChartCard, MetricStrip, Stat, Meter, dataTableSx, quietIconButtonSx, EmptyState;
|
|
338
|
+
var import_react, import_Box, import_ButtonBase, import_Paper, import_Typography, import_styles, MONTHS, fmtDateShort, fmtCompact, fmtUsdCompact, CHART_COLORS, SERIES, OTHER_COLOR, ChartTooltip, SeriesLegend, StatusPill, TagChip, SectionCard, ChartCard, MetricStrip, Stat, Meter, Gauge, dataTableSx, quietIconButtonSx, EmptyState;
|
|
339
339
|
var init_ui = __esm({
|
|
340
340
|
"src/components/ui.tsx"() {
|
|
341
341
|
"use strict";
|
|
@@ -705,6 +705,102 @@ var init_ui = __esm({
|
|
|
705
705
|
}
|
|
706
706
|
)
|
|
707
707
|
);
|
|
708
|
+
Gauge = ({ value, tone = "accent", size = 72, thickness = 6, label, caption }) => {
|
|
709
|
+
const clamped = Math.max(0, Math.min(100, value ?? 0));
|
|
710
|
+
const r = (size - thickness) / 2;
|
|
711
|
+
const c = 2 * Math.PI * r;
|
|
712
|
+
const dash = clamped / 100 * c;
|
|
713
|
+
return /* @__PURE__ */ import_react.default.createElement(
|
|
714
|
+
import_Box.default,
|
|
715
|
+
{
|
|
716
|
+
sx: (theme) => ({
|
|
717
|
+
position: "relative",
|
|
718
|
+
width: size,
|
|
719
|
+
height: size,
|
|
720
|
+
flexShrink: 0,
|
|
721
|
+
color: toneColor(theme, tone)
|
|
722
|
+
})
|
|
723
|
+
},
|
|
724
|
+
/* @__PURE__ */ import_react.default.createElement(
|
|
725
|
+
import_Box.default,
|
|
726
|
+
{
|
|
727
|
+
component: "svg",
|
|
728
|
+
viewBox: `0 0 ${size} ${size}`,
|
|
729
|
+
width: size,
|
|
730
|
+
height: size,
|
|
731
|
+
sx: { display: "block", transform: "rotate(-90deg)" }
|
|
732
|
+
},
|
|
733
|
+
/* @__PURE__ */ import_react.default.createElement(
|
|
734
|
+
import_Box.default,
|
|
735
|
+
{
|
|
736
|
+
component: "circle",
|
|
737
|
+
cx: size / 2,
|
|
738
|
+
cy: size / 2,
|
|
739
|
+
r,
|
|
740
|
+
fill: "none",
|
|
741
|
+
strokeWidth: thickness,
|
|
742
|
+
sx: (theme) => ({
|
|
743
|
+
stroke: (0, import_styles.alpha)(
|
|
744
|
+
theme.palette.text.primary,
|
|
745
|
+
theme.palette.mode === "dark" ? 0.14 : 0.09
|
|
746
|
+
)
|
|
747
|
+
})
|
|
748
|
+
}
|
|
749
|
+
),
|
|
750
|
+
/* @__PURE__ */ import_react.default.createElement(
|
|
751
|
+
import_Box.default,
|
|
752
|
+
{
|
|
753
|
+
component: "circle",
|
|
754
|
+
cx: size / 2,
|
|
755
|
+
cy: size / 2,
|
|
756
|
+
r,
|
|
757
|
+
fill: "none",
|
|
758
|
+
strokeWidth: thickness,
|
|
759
|
+
strokeLinecap: "round",
|
|
760
|
+
strokeDasharray: `${dash} ${c - dash}`,
|
|
761
|
+
sx: (theme) => ({
|
|
762
|
+
stroke: "currentColor",
|
|
763
|
+
transition: theme.transitions.create("stroke-dasharray")
|
|
764
|
+
})
|
|
765
|
+
}
|
|
766
|
+
)
|
|
767
|
+
),
|
|
768
|
+
/* @__PURE__ */ import_react.default.createElement(
|
|
769
|
+
import_Box.default,
|
|
770
|
+
{
|
|
771
|
+
sx: {
|
|
772
|
+
position: "absolute",
|
|
773
|
+
inset: 0,
|
|
774
|
+
display: "flex",
|
|
775
|
+
flexDirection: "column",
|
|
776
|
+
alignItems: "center",
|
|
777
|
+
justifyContent: "center",
|
|
778
|
+
lineHeight: 1
|
|
779
|
+
}
|
|
780
|
+
},
|
|
781
|
+
/* @__PURE__ */ import_react.default.createElement(
|
|
782
|
+
import_Typography.default,
|
|
783
|
+
{
|
|
784
|
+
sx: {
|
|
785
|
+
fontSize: size <= 64 ? 15 : 18,
|
|
786
|
+
fontWeight: 700,
|
|
787
|
+
fontVariantNumeric: "tabular-nums",
|
|
788
|
+
color: "text.primary"
|
|
789
|
+
}
|
|
790
|
+
},
|
|
791
|
+
label ?? `${Math.round(value)}%`
|
|
792
|
+
),
|
|
793
|
+
caption && /* @__PURE__ */ import_react.default.createElement(
|
|
794
|
+
import_Typography.default,
|
|
795
|
+
{
|
|
796
|
+
variant: "caption",
|
|
797
|
+
sx: { fontSize: 9.5, color: "text.secondary", mt: 0.25 }
|
|
798
|
+
},
|
|
799
|
+
caption
|
|
800
|
+
)
|
|
801
|
+
)
|
|
802
|
+
);
|
|
803
|
+
};
|
|
708
804
|
dataTableSx = (theme) => ({
|
|
709
805
|
// Element selectors, not `.MuiTableCell-head`: host apps may set a MUI
|
|
710
806
|
// classname prefix (Backstage ships `v5-`), which breaks global class targeting.
|
|
@@ -2023,6 +2119,8 @@ var init_ManageTeamDialog = __esm({
|
|
|
2023
2119
|
const [mcpError, setMcpError] = (0, import_react5.useState)(null);
|
|
2024
2120
|
const allowUnlimitedBudget = config?.teamManagement?.allowUnlimitedBudget ?? false;
|
|
2025
2121
|
const maxBudgetCeiling = config?.teamManagement?.maxBudgetCeiling ?? FALLBACK_BUDGET_CEILING;
|
|
2122
|
+
const hideBudgetForManagers = config?.display?.hideTeamBudgetForManagers ?? false;
|
|
2123
|
+
const budgetWriteOnly = hideBudgetForManagers && mode === "edit";
|
|
2026
2124
|
const members = (0, import_react5.useMemo)(() => team?.members_with_roles ?? [], [team]);
|
|
2027
2125
|
const showMembers = mode === "edit" && !!canManageMembers;
|
|
2028
2126
|
const catalogApi = (0, import_core_plugin_api2.useApi)(import_plugin_catalog_react.catalogApiRef);
|
|
@@ -2058,9 +2156,13 @@ var init_ManageTeamDialog = __esm({
|
|
|
2058
2156
|
if (mode === "edit" && team) {
|
|
2059
2157
|
setAlias(team.team_alias ?? "");
|
|
2060
2158
|
setModels(team.models ?? []);
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2159
|
+
if (hideBudgetForManagers) {
|
|
2160
|
+
setMaxBudget("");
|
|
2161
|
+
} else {
|
|
2162
|
+
setMaxBudget(
|
|
2163
|
+
team.max_budget ? String(team.max_budget) : String(maxBudgetCeiling)
|
|
2164
|
+
);
|
|
2165
|
+
}
|
|
2064
2166
|
setUnlimited(allowUnlimitedBudget && !team.max_budget);
|
|
2065
2167
|
setBudgetDuration(team.budget_duration ?? DEFAULT_BUDGET_DURATION);
|
|
2066
2168
|
} else {
|
|
@@ -2080,7 +2182,7 @@ var init_ManageTeamDialog = __esm({
|
|
|
2080
2182
|
setMcpIds(team?.object_permission?.mcp_servers ?? []);
|
|
2081
2183
|
setMcpError(null);
|
|
2082
2184
|
}
|
|
2083
|
-
}, [open, mode, team, allowUnlimitedBudget, maxBudgetCeiling]);
|
|
2185
|
+
}, [open, mode, team, allowUnlimitedBudget, maxBudgetCeiling, hideBudgetForManagers]);
|
|
2084
2186
|
const handleSubmit = async () => {
|
|
2085
2187
|
setError(null);
|
|
2086
2188
|
if (!alias.trim()) {
|
|
@@ -2092,18 +2194,21 @@ var init_ManageTeamDialog = __esm({
|
|
|
2092
2194
|
return;
|
|
2093
2195
|
}
|
|
2094
2196
|
if (!unlimited) {
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2197
|
+
const budgetLeftBlank = budgetWriteOnly && !maxBudget.trim();
|
|
2198
|
+
if (!budgetLeftBlank) {
|
|
2199
|
+
if (!maxBudget) {
|
|
2200
|
+
setError("Budget is required when unlimited budgets are disabled");
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2203
|
+
const budget = parseFloat(maxBudget);
|
|
2204
|
+
if (isNaN(budget) || budget <= 0) {
|
|
2205
|
+
setError("Budget must be a positive number");
|
|
2206
|
+
return;
|
|
2207
|
+
}
|
|
2208
|
+
if (budget > maxBudgetCeiling) {
|
|
2209
|
+
setError(`Budget cannot exceed $${maxBudgetCeiling}`);
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2107
2212
|
}
|
|
2108
2213
|
}
|
|
2109
2214
|
try {
|
|
@@ -2117,10 +2222,16 @@ var init_ManageTeamDialog = __esm({
|
|
|
2117
2222
|
...budgetDuration && { budget_duration: budgetDuration }
|
|
2118
2223
|
};
|
|
2119
2224
|
} else {
|
|
2225
|
+
let budgetField = {};
|
|
2226
|
+
if (unlimited) {
|
|
2227
|
+
budgetField = { max_budget: null };
|
|
2228
|
+
} else if (maxBudget.trim()) {
|
|
2229
|
+
budgetField = { max_budget: parseFloat(maxBudget) };
|
|
2230
|
+
}
|
|
2120
2231
|
payload = {
|
|
2121
2232
|
team_alias: alias.trim(),
|
|
2122
2233
|
models,
|
|
2123
|
-
...
|
|
2234
|
+
...budgetField,
|
|
2124
2235
|
...budgetDuration && { budget_duration: budgetDuration }
|
|
2125
2236
|
};
|
|
2126
2237
|
}
|
|
@@ -2226,13 +2337,13 @@ var init_ManageTeamDialog = __esm({
|
|
|
2226
2337
|
), /* @__PURE__ */ import_react5.default.createElement(
|
|
2227
2338
|
import_TextField3.default,
|
|
2228
2339
|
{
|
|
2229
|
-
label: "Max Budget ($)",
|
|
2340
|
+
label: budgetWriteOnly ? "New Max Budget ($)" : "Max Budget ($)",
|
|
2230
2341
|
type: "number",
|
|
2231
2342
|
value: maxBudget,
|
|
2232
2343
|
onChange: (e) => setMaxBudget(e.target.value),
|
|
2233
2344
|
disabled: submitting || unlimited,
|
|
2234
2345
|
inputProps: { min: 0, max: maxBudgetCeiling },
|
|
2235
|
-
helperText: `Maximum: $${maxBudgetCeiling}`,
|
|
2346
|
+
helperText: budgetWriteOnly ? "Current budget is hidden \u2014 leave blank to keep it, or enter a new value" : `Maximum: $${maxBudgetCeiling}`,
|
|
2236
2347
|
fullWidth: true
|
|
2237
2348
|
}
|
|
2238
2349
|
), allowUnlimitedBudget && /* @__PURE__ */ import_react5.default.createElement(
|
|
@@ -2788,7 +2899,19 @@ function buildBudgetSummary(user, teams, keys, maxKeys = 3) {
|
|
|
2788
2899
|
pct: limitPct(spend, budget)
|
|
2789
2900
|
};
|
|
2790
2901
|
})() : void 0;
|
|
2791
|
-
const teamLimits = teams.filter((t) => (t.max_budget ?? 0) > 0).map((t) => {
|
|
2902
|
+
const teamLimits = teams.filter((t) => (t.max_budget ?? 0) > 0 || t.budget_hidden === true).map((t) => {
|
|
2903
|
+
if (t.budget_hidden === true) {
|
|
2904
|
+
return {
|
|
2905
|
+
kind: "team",
|
|
2906
|
+
label: t.team_alias || "Untitled team",
|
|
2907
|
+
sublabel: t.team_id,
|
|
2908
|
+
budget: 0,
|
|
2909
|
+
spend: 0,
|
|
2910
|
+
budgetDuration: t.budget_duration,
|
|
2911
|
+
pct: Math.min(100, Math.max(0, t.budget_pct ?? 0)),
|
|
2912
|
+
hidden: true
|
|
2913
|
+
};
|
|
2914
|
+
}
|
|
2792
2915
|
const spend = t.spend ?? 0;
|
|
2793
2916
|
return {
|
|
2794
2917
|
kind: "team",
|
|
@@ -2808,6 +2931,22 @@ function buildBudgetSummary(user, teams, keys, maxKeys = 3) {
|
|
|
2808
2931
|
teams: teamLimits
|
|
2809
2932
|
};
|
|
2810
2933
|
}
|
|
2934
|
+
function buildBudgetGauges(summary) {
|
|
2935
|
+
const byLevel = {
|
|
2936
|
+
key: summary.keys,
|
|
2937
|
+
user: summary.user ? [summary.user] : [],
|
|
2938
|
+
team: summary.teams
|
|
2939
|
+
};
|
|
2940
|
+
return GAUGE_KINDS.map((kind) => {
|
|
2941
|
+
const limits = byLevel[kind];
|
|
2942
|
+
const count = kind === "key" ? limits.length + summary.hiddenBudgetedKeys : limits.length;
|
|
2943
|
+
return {
|
|
2944
|
+
kind,
|
|
2945
|
+
count,
|
|
2946
|
+
limit: limits.length ? limits.reduce((a, b) => b.pct > a.pct ? b : a) : void 0
|
|
2947
|
+
};
|
|
2948
|
+
});
|
|
2949
|
+
}
|
|
2811
2950
|
function budgetHeadline(summary) {
|
|
2812
2951
|
const all = [
|
|
2813
2952
|
...summary.keys,
|
|
@@ -2834,9 +2973,11 @@ function budgetTone2(pct) {
|
|
|
2834
2973
|
if (pct >= 80) return "warning";
|
|
2835
2974
|
return "accent";
|
|
2836
2975
|
}
|
|
2976
|
+
var GAUGE_KINDS;
|
|
2837
2977
|
var init_budget = __esm({
|
|
2838
2978
|
"src/budget.ts"() {
|
|
2839
2979
|
"use strict";
|
|
2980
|
+
GAUGE_KINDS = ["key", "user", "team"];
|
|
2840
2981
|
}
|
|
2841
2982
|
});
|
|
2842
2983
|
|
|
@@ -2964,7 +3105,7 @@ var init_LiteLLMBudgetWidget = __esm({
|
|
|
2964
3105
|
}
|
|
2965
3106
|
},
|
|
2966
3107
|
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(
|
|
3108
|
+
)), 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
3109
|
import_Box7.default,
|
|
2969
3110
|
{
|
|
2970
3111
|
sx: {
|
|
@@ -2975,7 +3116,7 @@ var init_LiteLLMBudgetWidget = __esm({
|
|
|
2975
3116
|
mt: 0.75
|
|
2976
3117
|
}
|
|
2977
3118
|
},
|
|
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`),
|
|
3119
|
+
/* @__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
3120
|
/* @__PURE__ */ import_react7.default.createElement(import_Typography7.default, { variant: "caption", color: "text.secondary" }, fmtBudgetDuration(limit.budgetDuration) ? `resets ${fmtBudgetDuration(limit.budgetDuration)}` : "never resets")
|
|
2980
3121
|
));
|
|
2981
3122
|
};
|
|
@@ -3156,6 +3297,15 @@ function budgetTone3(isOver, isNear) {
|
|
|
3156
3297
|
if (isNear) return "warning";
|
|
3157
3298
|
return "accent";
|
|
3158
3299
|
}
|
|
3300
|
+
function visibleBudgetPct(spend, budget) {
|
|
3301
|
+
if (budget <= 0) return 0;
|
|
3302
|
+
return Math.min(spend / budget * 100, 100);
|
|
3303
|
+
}
|
|
3304
|
+
function budgetStatValue(hidden, budget) {
|
|
3305
|
+
if (hidden) return "Hidden";
|
|
3306
|
+
if (budget > 0) return fmtUsd2(budget);
|
|
3307
|
+
return "Unlimited";
|
|
3308
|
+
}
|
|
3159
3309
|
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
3310
|
var init_TeamUsage = __esm({
|
|
3161
3311
|
"src/components/TeamUsage.tsx"() {
|
|
@@ -3184,13 +3334,15 @@ var init_TeamUsage = __esm({
|
|
|
3184
3334
|
TeamCard = ({ team, usage, usageLoading, canManage, onEditTeam }) => {
|
|
3185
3335
|
const [expanded, setExpanded] = (0, import_react8.useState)(false);
|
|
3186
3336
|
const chart = useChartTheme();
|
|
3187
|
-
const
|
|
3188
|
-
const
|
|
3189
|
-
const
|
|
3190
|
-
const
|
|
3191
|
-
const
|
|
3337
|
+
const hidden = team.budget_hidden === true;
|
|
3338
|
+
const budget = hidden ? 0 : team.max_budget ?? 0;
|
|
3339
|
+
const spend = hidden ? 0 : team.spend ?? 0;
|
|
3340
|
+
const budgetPct = hidden ? Math.min(100, Math.max(0, team.budget_pct ?? 0)) : visibleBudgetPct(spend, budget);
|
|
3341
|
+
const isOver = hidden ? team.budget_status === "over" : budget > 0 && spend >= budget;
|
|
3342
|
+
const isNear = hidden ? team.budget_status === "near" : budget > 0 && spend >= budget * 0.8 && !isOver;
|
|
3192
3343
|
const dailyData = usage?.daily_usage?.map((d) => ({ date: d.date, spend: d.spend })) ?? [];
|
|
3193
3344
|
const renderDailySpendSection = () => {
|
|
3345
|
+
if (hidden) return null;
|
|
3194
3346
|
if (usageLoading) {
|
|
3195
3347
|
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
3348
|
}
|
|
@@ -3291,11 +3443,11 @@ var init_TeamUsage = __esm({
|
|
|
3291
3443
|
},
|
|
3292
3444
|
/* @__PURE__ */ import_react8.default.createElement(Stat, { label: "Members", value: team.members_with_roles?.length ?? "\u2014" }),
|
|
3293
3445
|
/* @__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) }),
|
|
3446
|
+
/* @__PURE__ */ import_react8.default.createElement(Stat, { label: "Budget", value: budgetStatValue(hidden, budget) }),
|
|
3447
|
+
/* @__PURE__ */ import_react8.default.createElement(Stat, { label: "Spend", value: hidden ? "\u2014" : fmtUsd2(spend) }),
|
|
3296
3448
|
/* @__PURE__ */ import_react8.default.createElement(Stat, { label: "TPM", value: team.tpm_limit && team.tpm_limit > 0 ? team.tpm_limit.toLocaleString() : "\u2014" }),
|
|
3297
3449
|
/* @__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(
|
|
3450
|
+
), 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
3451
|
import_TableContainer4.default,
|
|
3300
3452
|
{
|
|
3301
3453
|
component: import_Paper6.default,
|
|
@@ -4341,6 +4493,7 @@ __export(index_exports, {
|
|
|
4341
4493
|
DashboardHeader: () => DashboardHeader,
|
|
4342
4494
|
GenerateKeyDialog: () => GenerateKeyDialog,
|
|
4343
4495
|
KeysTable: () => KeysTable,
|
|
4496
|
+
LiteLLMBudgetGauges: () => LiteLLMBudgetGauges,
|
|
4344
4497
|
LiteLLMBudgetWidget: () => LiteLLMBudgetWidget,
|
|
4345
4498
|
LiteLLMHomeWidget: () => LiteLLMHomeWidget,
|
|
4346
4499
|
LiteLLMPage: () => LiteLLMPage,
|
|
@@ -4495,6 +4648,201 @@ var LiteLLMHomeWidget = ({
|
|
|
4495
4648
|
|
|
4496
4649
|
// src/index.ts
|
|
4497
4650
|
init_LiteLLMBudgetWidget();
|
|
4651
|
+
|
|
4652
|
+
// src/components/LiteLLMBudgetGauges.tsx
|
|
4653
|
+
var import_react14 = __toESM(require("react"));
|
|
4654
|
+
var import_Paper9 = __toESM(require("@mui/material/Paper"));
|
|
4655
|
+
var import_Box13 = __toESM(require("@mui/material/Box"));
|
|
4656
|
+
var import_Typography13 = __toESM(require("@mui/material/Typography"));
|
|
4657
|
+
var import_CircularProgress8 = __toESM(require("@mui/material/CircularProgress"));
|
|
4658
|
+
var import_Alert8 = __toESM(require("@mui/material/Alert"));
|
|
4659
|
+
var import_core_plugin_api6 = require("@backstage/core-plugin-api");
|
|
4660
|
+
var import_core_components2 = require("@backstage/core-components");
|
|
4661
|
+
init_api();
|
|
4662
|
+
init_format();
|
|
4663
|
+
init_ui();
|
|
4664
|
+
init_budget();
|
|
4665
|
+
var DEFAULT_KEYS_HREF = "/litellm?tab=keys";
|
|
4666
|
+
var LEVEL = {
|
|
4667
|
+
key: { name: "Key", none: "No key budget" },
|
|
4668
|
+
user: { name: "User", none: "No personal budget" },
|
|
4669
|
+
team: { name: "Team", none: "No team budget" }
|
|
4670
|
+
};
|
|
4671
|
+
function fmtUsdShort(n) {
|
|
4672
|
+
const v = n ?? 0;
|
|
4673
|
+
if (v >= 100 && Number.isInteger(v)) return `$${v}`;
|
|
4674
|
+
return fmtUsd(v);
|
|
4675
|
+
}
|
|
4676
|
+
function spendCaption(limit) {
|
|
4677
|
+
if (limit.hidden) return "hidden by admin";
|
|
4678
|
+
return `${fmtUsdShort(limit.spend)} / ${fmtUsdShort(limit.budget)}`;
|
|
4679
|
+
}
|
|
4680
|
+
function resetLabel(limit) {
|
|
4681
|
+
const window2 = fmtBudgetDuration(limit.budgetDuration);
|
|
4682
|
+
return window2 ? `resets ${window2}` : "never resets";
|
|
4683
|
+
}
|
|
4684
|
+
var LevelGauge = ({
|
|
4685
|
+
gauge,
|
|
4686
|
+
size,
|
|
4687
|
+
keysHref
|
|
4688
|
+
}) => {
|
|
4689
|
+
const { name, none } = LEVEL[gauge.kind];
|
|
4690
|
+
const limit = gauge.limit;
|
|
4691
|
+
const tone = limit ? budgetTone2(limit.pct) : "neutral";
|
|
4692
|
+
const extra = gauge.kind === "key" ? Math.max(0, gauge.count - 1) : 0;
|
|
4693
|
+
return /* @__PURE__ */ import_react14.default.createElement(
|
|
4694
|
+
import_Box13.default,
|
|
4695
|
+
{
|
|
4696
|
+
sx: {
|
|
4697
|
+
display: "flex",
|
|
4698
|
+
flexDirection: "column",
|
|
4699
|
+
alignItems: "center",
|
|
4700
|
+
minWidth: 0,
|
|
4701
|
+
flex: 1
|
|
4702
|
+
}
|
|
4703
|
+
},
|
|
4704
|
+
/* @__PURE__ */ import_react14.default.createElement(
|
|
4705
|
+
Gauge,
|
|
4706
|
+
{
|
|
4707
|
+
value: limit?.pct ?? 0,
|
|
4708
|
+
tone,
|
|
4709
|
+
size,
|
|
4710
|
+
label: limit ? `${Math.round(limit.pct)}%` : "\u2014"
|
|
4711
|
+
}
|
|
4712
|
+
),
|
|
4713
|
+
/* @__PURE__ */ import_react14.default.createElement(
|
|
4714
|
+
import_Typography13.default,
|
|
4715
|
+
{
|
|
4716
|
+
sx: (theme) => ({
|
|
4717
|
+
mt: 0.75,
|
|
4718
|
+
fontSize: 10.5,
|
|
4719
|
+
fontWeight: 700,
|
|
4720
|
+
letterSpacing: "0.08em",
|
|
4721
|
+
textTransform: "uppercase",
|
|
4722
|
+
color: tone === "neutral" ? theme.palette.text.secondary : void 0
|
|
4723
|
+
})
|
|
4724
|
+
},
|
|
4725
|
+
name
|
|
4726
|
+
),
|
|
4727
|
+
limit ? /* @__PURE__ */ import_react14.default.createElement(import_react14.default.Fragment, null, /* @__PURE__ */ import_react14.default.createElement(
|
|
4728
|
+
import_Typography13.default,
|
|
4729
|
+
{
|
|
4730
|
+
variant: "caption",
|
|
4731
|
+
title: limit.sublabel ? `${limit.label} \xB7 ${limit.sublabel}` : limit.label,
|
|
4732
|
+
sx: {
|
|
4733
|
+
mt: 0.25,
|
|
4734
|
+
fontSize: 11,
|
|
4735
|
+
fontWeight: 600,
|
|
4736
|
+
maxWidth: "100%",
|
|
4737
|
+
overflow: "hidden",
|
|
4738
|
+
textOverflow: "ellipsis",
|
|
4739
|
+
whiteSpace: "nowrap"
|
|
4740
|
+
}
|
|
4741
|
+
},
|
|
4742
|
+
limit.label
|
|
4743
|
+
), /* @__PURE__ */ import_react14.default.createElement(
|
|
4744
|
+
import_Typography13.default,
|
|
4745
|
+
{
|
|
4746
|
+
variant: "caption",
|
|
4747
|
+
color: "text.secondary",
|
|
4748
|
+
sx: {
|
|
4749
|
+
fontSize: 10.5,
|
|
4750
|
+
fontVariantNumeric: "tabular-nums",
|
|
4751
|
+
textAlign: "center"
|
|
4752
|
+
}
|
|
4753
|
+
},
|
|
4754
|
+
spendCaption(limit)
|
|
4755
|
+
), extra > 0 ? /* @__PURE__ */ import_react14.default.createElement(
|
|
4756
|
+
import_Typography13.default,
|
|
4757
|
+
{
|
|
4758
|
+
component: import_core_components2.Link,
|
|
4759
|
+
to: keysHref,
|
|
4760
|
+
variant: "caption",
|
|
4761
|
+
sx: {
|
|
4762
|
+
fontSize: 10.5,
|
|
4763
|
+
color: "primary.main",
|
|
4764
|
+
textDecoration: "none",
|
|
4765
|
+
"&:hover": { textDecoration: "underline" }
|
|
4766
|
+
}
|
|
4767
|
+
},
|
|
4768
|
+
"+",
|
|
4769
|
+
extra,
|
|
4770
|
+
" more"
|
|
4771
|
+
) : /* @__PURE__ */ import_react14.default.createElement(import_Typography13.default, { variant: "caption", color: "text.secondary", sx: { fontSize: 10.5 } }, resetLabel(limit))) : /* @__PURE__ */ import_react14.default.createElement(
|
|
4772
|
+
import_Typography13.default,
|
|
4773
|
+
{
|
|
4774
|
+
variant: "caption",
|
|
4775
|
+
color: "text.secondary",
|
|
4776
|
+
sx: { mt: 0.25, fontSize: 10.5, textAlign: "center" }
|
|
4777
|
+
},
|
|
4778
|
+
none
|
|
4779
|
+
)
|
|
4780
|
+
);
|
|
4781
|
+
};
|
|
4782
|
+
var LiteLLMBudgetGauges = ({
|
|
4783
|
+
title = "Budget",
|
|
4784
|
+
size = 72,
|
|
4785
|
+
keysHref = DEFAULT_KEYS_HREF,
|
|
4786
|
+
action
|
|
4787
|
+
}) => {
|
|
4788
|
+
const api = (0, import_core_plugin_api6.useApi)(liteLlmApiRef);
|
|
4789
|
+
const [loading, setLoading] = (0, import_react14.useState)(true);
|
|
4790
|
+
const [error, setError] = (0, import_react14.useState)(null);
|
|
4791
|
+
const [user, setUser] = (0, import_react14.useState)(null);
|
|
4792
|
+
const [teams, setTeams] = (0, import_react14.useState)([]);
|
|
4793
|
+
const [keys, setKeys] = (0, import_react14.useState)([]);
|
|
4794
|
+
(0, import_react14.useEffect)(() => {
|
|
4795
|
+
let cancelled = false;
|
|
4796
|
+
setLoading(true);
|
|
4797
|
+
setError(null);
|
|
4798
|
+
Promise.allSettled([api.getUserInfo(), api.getTeams(), api.listKeys()]).then(
|
|
4799
|
+
([userResult, teamsResult, keysResult]) => {
|
|
4800
|
+
if (cancelled) return;
|
|
4801
|
+
if (userResult.status === "fulfilled") {
|
|
4802
|
+
setUser(userResult.value);
|
|
4803
|
+
} else {
|
|
4804
|
+
setUser(null);
|
|
4805
|
+
setError(
|
|
4806
|
+
userResult.reason?.message ?? "Failed to load your LiteLLM profile"
|
|
4807
|
+
);
|
|
4808
|
+
}
|
|
4809
|
+
setTeams(teamsResult.status === "fulfilled" ? teamsResult.value : []);
|
|
4810
|
+
setKeys(keysResult.status === "fulfilled" ? keysResult.value : []);
|
|
4811
|
+
setLoading(false);
|
|
4812
|
+
}
|
|
4813
|
+
);
|
|
4814
|
+
return () => {
|
|
4815
|
+
cancelled = true;
|
|
4816
|
+
};
|
|
4817
|
+
}, [api]);
|
|
4818
|
+
const summary = (0, import_react14.useMemo)(
|
|
4819
|
+
() => buildBudgetSummary(user, teams, keys, keys.length),
|
|
4820
|
+
[user, teams, keys]
|
|
4821
|
+
);
|
|
4822
|
+
const gauges = (0, import_react14.useMemo)(() => buildBudgetGauges(summary), [summary]);
|
|
4823
|
+
const headline = (0, import_react14.useMemo)(() => budgetHeadline(summary), [summary]);
|
|
4824
|
+
const summaryText = headline.count === 0 ? "no limits apply" : `${headline.count} limit${headline.count === 1 ? "" : "s"}`;
|
|
4825
|
+
return /* @__PURE__ */ import_react14.default.createElement(import_Paper9.default, { sx: { p: 2 } }, /* @__PURE__ */ import_react14.default.createElement(import_Box13.default, { sx: { display: "flex", alignItems: "center", gap: 1, mb: 1.5, minWidth: 0 } }, /* @__PURE__ */ import_react14.default.createElement(import_Typography13.default, { variant: "h6", lineHeight: 1.2, sx: { minWidth: 0, flex: 1 } }, title), !loading && !error && /* @__PURE__ */ import_react14.default.createElement(
|
|
4826
|
+
StatusPill,
|
|
4827
|
+
{
|
|
4828
|
+
label: summaryText,
|
|
4829
|
+
tone: budgetTone2(headline.closest?.pct ?? 0),
|
|
4830
|
+
dot: headline.count > 0
|
|
4831
|
+
}
|
|
4832
|
+
)), loading && /* @__PURE__ */ import_react14.default.createElement(import_Box13.default, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 110 }, /* @__PURE__ */ import_react14.default.createElement(import_CircularProgress8.default, { size: 28 })), !loading && error && /* @__PURE__ */ import_react14.default.createElement(import_Alert8.default, { severity: "error", sx: { mt: 0.5 } }, error), !loading && !error && /* @__PURE__ */ import_react14.default.createElement(import_Box13.default, { sx: { display: "flex", alignItems: "flex-start", gap: 1 } }, gauges.map((gauge) => /* @__PURE__ */ import_react14.default.createElement(LevelGauge, { key: gauge.kind, gauge, size, keysHref }))), action && /* @__PURE__ */ import_react14.default.createElement(
|
|
4833
|
+
import_Box13.default,
|
|
4834
|
+
{
|
|
4835
|
+
sx: (theme) => ({
|
|
4836
|
+
mt: 2,
|
|
4837
|
+
pt: 2,
|
|
4838
|
+
borderTop: `1px solid ${theme.palette.divider}`
|
|
4839
|
+
})
|
|
4840
|
+
},
|
|
4841
|
+
action
|
|
4842
|
+
));
|
|
4843
|
+
};
|
|
4844
|
+
|
|
4845
|
+
// src/index.ts
|
|
4498
4846
|
init_GenerateKeyDialog();
|
|
4499
4847
|
init_ManageTeamDialog();
|
|
4500
4848
|
init_api();
|