@acarmisc/backstage-plugin-litellm 0.21.0 → 0.21.2

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 CHANGED
@@ -41,12 +41,13 @@ export interface BudgetSummary {
41
41
  export declare function buildBudgetSummary(user: UserInfo | null, teams: TeamInfo[], keys: VirtualKey[], maxKeys?: number): BudgetSummary;
42
42
  /**
43
43
  * One-line summary of a `BudgetSummary`, for a collapsed/compact header:
44
- * how many concrete limits the user is subject to, and how close the
45
- * closest one is to its cap. `closestPct` is null when there are no limits.
44
+ * how many concrete limits the user is subject to, and the one closest to
45
+ * its cap — kept as the whole `BudgetLimit` so the caller can name its
46
+ * level (key / personal / team). `closest` is null when there are no limits.
46
47
  */
47
48
  export declare function budgetHeadline(summary: BudgetSummary): {
48
49
  count: number;
49
- closestPct: number | null;
50
+ closest: BudgetLimit | null;
50
51
  };
51
52
  /** "30d" → "every 30 days"; "1d" → "daily". Returns null when duration is unset. */
52
53
  export declare function fmtBudgetDuration(duration?: string): string | null;
package/dist/index.cjs.js CHANGED
@@ -2809,16 +2809,14 @@ function buildBudgetSummary(user, teams, keys, maxKeys = 3) {
2809
2809
  };
2810
2810
  }
2811
2811
  function budgetHeadline(summary) {
2812
- const pcts = [
2813
- ...summary.keys.map((k) => k.pct),
2814
- ...summary.user ? [summary.user.pct] : [],
2815
- ...summary.teams.map((t) => t.pct)
2812
+ const all = [
2813
+ ...summary.keys,
2814
+ ...summary.user ? [summary.user] : [],
2815
+ ...summary.teams
2816
2816
  ];
2817
2817
  const count = summary.keys.length + summary.hiddenBudgetedKeys + (summary.user ? 1 : 0) + summary.teams.length;
2818
- return {
2819
- count,
2820
- closestPct: pcts.length ? Math.max(...pcts) : null
2821
- };
2818
+ const closest = all.length ? all.reduce((a, b) => b.pct > a.pct ? b : a) : null;
2819
+ return { count, closest };
2822
2820
  }
2823
2821
  function fmtBudgetDuration(duration) {
2824
2822
  if (!duration) return null;
@@ -2857,7 +2855,7 @@ function levelToneColor(tone, theme) {
2857
2855
  return theme.palette.primary.main;
2858
2856
  }
2859
2857
  }
2860
- var import_react7, import_Paper5, import_Box7, import_Typography7, import_CircularProgress4, import_Alert3, import_Collapse, import_IconButton4, import_icons_material5, import_styles4, import_core_plugin_api3, LevelFrame, EmptyLimitCard, LimitCard, TaggedLimit, FullBody, CompactBody, LiteLLMBudgetWidget;
2858
+ var import_react7, import_Paper5, import_Box7, import_Typography7, import_CircularProgress4, import_Alert3, import_Collapse, import_IconButton4, import_icons_material5, import_styles4, import_core_plugin_api3, import_core_components, LevelFrame, EmptyLimitCard, LimitCard, CLOSEST_LEVEL_LABEL, KEYS_PATH, MoreKeysNote, HierarchyNote, TaggedLimit, FullBody, CompactBody, LiteLLMBudgetWidget;
2861
2859
  var init_LiteLLMBudgetWidget = __esm({
2862
2860
  "src/components/LiteLLMBudgetWidget.tsx"() {
2863
2861
  "use strict";
@@ -2872,6 +2870,7 @@ var init_LiteLLMBudgetWidget = __esm({
2872
2870
  import_icons_material5 = require("@mui/icons-material");
2873
2871
  import_styles4 = require("@mui/material/styles");
2874
2872
  import_core_plugin_api3 = require("@backstage/core-plugin-api");
2873
+ import_core_components = require("@backstage/core-components");
2875
2874
  init_api();
2876
2875
  init_format();
2877
2876
  init_ui();
@@ -2980,6 +2979,26 @@ var init_LiteLLMBudgetWidget = __esm({
2980
2979
  /* @__PURE__ */ import_react7.default.createElement(import_Typography7.default, { variant: "caption", color: "text.secondary" }, fmtBudgetDuration(limit.budgetDuration) ? `resets ${fmtBudgetDuration(limit.budgetDuration)}` : "never resets")
2981
2980
  ));
2982
2981
  };
2982
+ CLOSEST_LEVEL_LABEL = {
2983
+ key: "key",
2984
+ user: "personal",
2985
+ team: "team"
2986
+ };
2987
+ KEYS_PATH = "/litellm?tab=keys";
2988
+ MoreKeysNote = ({ count }) => /* @__PURE__ */ import_react7.default.createElement(import_core_components.Link, { to: KEYS_PATH, variant: "caption" }, "+", count, " more budgeted key", count > 1 ? "s" : "", " further from the cap \u2192");
2989
+ HierarchyNote = ({ withReset }) => /* @__PURE__ */ import_react7.default.createElement(
2990
+ import_Box7.default,
2991
+ {
2992
+ sx: (theme) => ({
2993
+ px: 1.5,
2994
+ py: 1.25,
2995
+ borderRadius: 1.5,
2996
+ border: "1px dashed",
2997
+ borderColor: (0, import_styles4.alpha)(theme.palette.text.primary, 0.18)
2998
+ })
2999
+ },
3000
+ /* @__PURE__ */ import_react7.default.createElement(import_Typography7.default, { variant: "caption", color: "text.secondary", display: "block" }, /* @__PURE__ */ import_react7.default.createElement("b", null, "Order:"), " key \u2192 personal \u2192 team \u2192 global. The first cap you reach blocks the request; a key that belongs to a team uses the team's cap, not your personal one.", withReset && " A cap with a reset window drops to $0 when the window closes; without one it never resets.")
3001
+ );
2983
3002
  TaggedLimit = ({ limit }) => {
2984
3003
  const tone = budgetTone2(limit.pct);
2985
3004
  return /* @__PURE__ */ import_react7.default.createElement(import_Box7.default, null, /* @__PURE__ */ import_react7.default.createElement(
@@ -3009,7 +3028,7 @@ var init_LiteLLMBudgetWidget = __esm({
3009
3028
  },
3010
3029
  !hasBudgetedKeys && /* @__PURE__ */ import_react7.default.createElement(EmptyLimitCard, null, "No key has a budget, so no key-level cap applies."),
3011
3030
  summary.keys.map((k) => /* @__PURE__ */ import_react7.default.createElement(LimitCard, { key: k.sublabel ?? k.label, limit: k })),
3012
- summary.hiddenBudgetedKeys > 0 && /* @__PURE__ */ import_react7.default.createElement(import_Typography7.default, { variant: "caption", color: "text.secondary" }, "+", summary.hiddenBudgetedKeys, " more budgeted key", summary.hiddenBudgetedKeys > 1 ? "s" : "", " further from the cap")
3031
+ summary.hiddenBudgetedKeys > 0 && /* @__PURE__ */ import_react7.default.createElement(MoreKeysNote, { count: summary.hiddenBudgetedKeys })
3013
3032
  ), /* @__PURE__ */ import_react7.default.createElement(
3014
3033
  LevelFrame,
3015
3034
  {
@@ -3019,27 +3038,14 @@ var init_LiteLLMBudgetWidget = __esm({
3019
3038
  note: "skipped for team-bound keys"
3020
3039
  },
3021
3040
  summary.user ? /* @__PURE__ */ import_react7.default.createElement(LimitCard, { limit: summary.user }) : /* @__PURE__ */ import_react7.default.createElement(EmptyLimitCard, null, "No personal budget is set on your account.")
3022
- ), /* @__PURE__ */ import_react7.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_react7.default.createElement(LimitCard, { key: t.sublabel ?? t.label, limit: t })) : /* @__PURE__ */ import_react7.default.createElement(EmptyLimitCard, null, "No team you belong to has a budget.")), /* @__PURE__ */ import_react7.default.createElement(LevelFrame, { rank: 4, name: "Global", tagline: "proxy-wide cap set by your admin", isLast: true }, /* @__PURE__ */ import_react7.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_react7.default.createElement(
3023
- import_Box7.default,
3024
- {
3025
- sx: (theme) => ({
3026
- mt: 2,
3027
- px: 1.5,
3028
- py: 1.25,
3029
- borderRadius: 1.5,
3030
- border: "1px dashed",
3031
- borderColor: (0, import_styles4.alpha)(theme.palette.text.primary, 0.18)
3032
- })
3033
- },
3034
- /* @__PURE__ */ import_react7.default.createElement(import_Typography7.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.")
3035
- ));
3041
+ ), /* @__PURE__ */ import_react7.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_react7.default.createElement(LimitCard, { key: t.sublabel ?? t.label, limit: t })) : /* @__PURE__ */ import_react7.default.createElement(EmptyLimitCard, null, "No team you belong to has a budget.")), /* @__PURE__ */ import_react7.default.createElement(LevelFrame, { rank: 4, name: "Global", tagline: "proxy-wide cap set by your admin", isLast: true }, /* @__PURE__ */ import_react7.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_react7.default.createElement(import_Box7.default, { sx: { mt: 2 } }, /* @__PURE__ */ import_react7.default.createElement(HierarchyNote, { withReset: true })));
3036
3042
  };
3037
3043
  CompactBody = ({ summary }) => {
3038
3044
  const { count } = budgetHeadline(summary);
3039
3045
  if (count === 0) {
3040
3046
  return /* @__PURE__ */ import_react7.default.createElement(import_Typography7.default, { variant: "body2", color: "text.secondary", sx: { fontSize: 13 } }, "No budget limits apply to you right now.");
3041
3047
  }
3042
- return /* @__PURE__ */ import_react7.default.createElement(import_Box7.default, { sx: { display: "flex", flexDirection: "column", gap: 1.25 } }, summary.keys.map((k) => /* @__PURE__ */ import_react7.default.createElement(TaggedLimit, { key: k.sublabel ?? k.label, limit: k })), summary.hiddenBudgetedKeys > 0 && /* @__PURE__ */ import_react7.default.createElement(import_Typography7.default, { variant: "caption", color: "text.secondary" }, "+", summary.hiddenBudgetedKeys, " more budgeted key", summary.hiddenBudgetedKeys > 1 ? "s" : "", " further from the cap"), summary.user && /* @__PURE__ */ import_react7.default.createElement(TaggedLimit, { limit: summary.user }), summary.teams.map((t) => /* @__PURE__ */ import_react7.default.createElement(TaggedLimit, { key: t.sublabel ?? t.label, limit: t })));
3048
+ return /* @__PURE__ */ import_react7.default.createElement(import_Box7.default, { sx: { display: "flex", flexDirection: "column", gap: 1.25 } }, /* @__PURE__ */ import_react7.default.createElement(HierarchyNote, null), summary.keys.map((k) => /* @__PURE__ */ import_react7.default.createElement(TaggedLimit, { key: k.sublabel ?? k.label, limit: k })), summary.hiddenBudgetedKeys > 0 && /* @__PURE__ */ import_react7.default.createElement(MoreKeysNote, { count: summary.hiddenBudgetedKeys }), summary.user && /* @__PURE__ */ import_react7.default.createElement(TaggedLimit, { limit: summary.user }), summary.teams.map((t) => /* @__PURE__ */ import_react7.default.createElement(TaggedLimit, { key: t.sublabel ?? t.label, limit: t })));
3043
3049
  };
3044
3050
  LiteLLMBudgetWidget = ({
3045
3051
  title = "Budget Policy",
@@ -3086,7 +3092,9 @@ var init_LiteLLMBudgetWidget = __esm({
3086
3092
  );
3087
3093
  const headline = (0, import_react7.useMemo)(() => budgetHeadline(summary), [summary]);
3088
3094
  const limitsWord = `${headline.count} limit${headline.count === 1 ? "" : "s"}`;
3089
- const closestWord = headline.closestPct === null ? "" : ` \xB7 closest ${Math.round(headline.closestPct)}%`;
3095
+ const closestWord = headline.closest === null ? "" : ` \xB7 closest: ${CLOSEST_LEVEL_LABEL[headline.closest.kind]} ${Math.round(
3096
+ headline.closest.pct
3097
+ )}%`;
3090
3098
  const summaryText = headline.count === 0 ? "no limits apply" : `${limitsWord}${closestWord}`;
3091
3099
  const body = /* @__PURE__ */ import_react7.default.createElement(import_react7.default.Fragment, null, loading && /* @__PURE__ */ import_react7.default.createElement(import_Box7.default, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 140 }, /* @__PURE__ */ import_react7.default.createElement(import_CircularProgress4.default, { size: 32 })), !loading && error && /* @__PURE__ */ import_react7.default.createElement(import_Alert3.default, { severity: "error", sx: { mt: 0.5 } }, error), !loading && !error && (compact ? /* @__PURE__ */ import_react7.default.createElement(CompactBody, { summary }) : /* @__PURE__ */ import_react7.default.createElement(FullBody, { summary })));
3092
3100
  return /* @__PURE__ */ import_react7.default.createElement(import_Paper5.default, { sx: { p: 2 } }, collapsible ? /* @__PURE__ */ import_react7.default.createElement(
@@ -3102,7 +3110,13 @@ var init_LiteLLMBudgetWidget = __esm({
3102
3110
  mb: expanded ? 1.5 : 0
3103
3111
  }
3104
3112
  },
3105
- /* @__PURE__ */ import_react7.default.createElement(import_Box7.default, { sx: { minWidth: 0, flex: 1, display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" } }, /* @__PURE__ */ import_react7.default.createElement(import_Typography7.default, { variant: "h6", lineHeight: 1.2 }, title), !loading && !error && /* @__PURE__ */ import_react7.default.createElement(StatusPill, { label: summaryText, tone: budgetTone2(headline.closestPct ?? 0) })),
3113
+ /* @__PURE__ */ import_react7.default.createElement(import_Box7.default, { sx: { minWidth: 0, flex: 1 } }, /* @__PURE__ */ import_react7.default.createElement(import_Typography7.default, { variant: "h6", lineHeight: 1.2 }, title), !loading && !error && /* @__PURE__ */ import_react7.default.createElement(import_Box7.default, { sx: { mt: 0.5 } }, /* @__PURE__ */ import_react7.default.createElement(
3114
+ StatusPill,
3115
+ {
3116
+ label: summaryText,
3117
+ tone: budgetTone2(headline.closest?.pct ?? 0)
3118
+ }
3119
+ ))),
3106
3120
  /* @__PURE__ */ import_react7.default.createElement(
3107
3121
  import_IconButton4.default,
3108
3122
  {
@@ -3836,7 +3850,7 @@ function initDateRange() {
3836
3850
  else start.setDate(start.getDate() - 7);
3837
3851
  return { start, end };
3838
3852
  }
3839
- var import_react11, import_Box11, import_Snackbar2, import_Alert6, import_CircularProgress6, import_Typography11, import_Paper7, import_Tabs2, import_Tab2, import_Grid2, import_react_use3, import_core_plugin_api4, import_plugin_permission_react, PERIOD_LS_KEY2, LiteLLMPage;
3853
+ var import_react11, import_Box11, import_Snackbar2, import_Alert6, import_CircularProgress6, import_Typography11, import_Paper7, import_Tabs2, import_Tab2, import_Grid2, import_react_router_dom, import_react_use3, import_core_plugin_api4, import_plugin_permission_react, PERIOD_LS_KEY2, PAGE_TABS, isPageTab, LiteLLMPage;
3840
3854
  var init_LiteLLMPage = __esm({
3841
3855
  "src/components/LiteLLMPage.tsx"() {
3842
3856
  "use strict";
@@ -3850,6 +3864,7 @@ var init_LiteLLMPage = __esm({
3850
3864
  import_Tabs2 = __toESM(require("@mui/material/Tabs"));
3851
3865
  import_Tab2 = __toESM(require("@mui/material/Tab"));
3852
3866
  import_Grid2 = __toESM(require("@mui/material/Grid"));
3867
+ import_react_router_dom = require("react-router-dom");
3853
3868
  import_react_use3 = require("react-use");
3854
3869
  import_core_plugin_api4 = require("@backstage/core-plugin-api");
3855
3870
  import_plugin_permission_react = require("@backstage/plugin-permission-react");
@@ -3865,6 +3880,8 @@ var init_LiteLLMPage = __esm({
3865
3880
  init_api();
3866
3881
  init_permissions();
3867
3882
  PERIOD_LS_KEY2 = "litellm_usage_period";
3883
+ PAGE_TABS = ["overview", "keys", "teams", "models", "audit"];
3884
+ isPageTab = (v) => !!v && PAGE_TABS.includes(v);
3868
3885
  LiteLLMPage = () => {
3869
3886
  const api = (0, import_core_plugin_api4.useApi)(liteLlmApiRef);
3870
3887
  const [dateRange, setDateRange] = (0, import_react11.useState)(initDateRange);
@@ -3875,7 +3892,25 @@ var init_LiteLLMPage = __esm({
3875
3892
  return "7d";
3876
3893
  }
3877
3894
  });
3878
- const [activeTab, setActiveTab] = (0, import_react11.useState)("overview");
3895
+ const [searchParams, setSearchParams] = (0, import_react_router_dom.useSearchParams)();
3896
+ const [activeTab, setActiveTab] = (0, import_react11.useState)(() => {
3897
+ const t = searchParams.get("tab");
3898
+ return isPageTab(t) ? t : "overview";
3899
+ });
3900
+ const selectTab = (0, import_react11.useCallback)(
3901
+ (tab) => {
3902
+ setActiveTab(tab);
3903
+ setSearchParams(
3904
+ (prev) => {
3905
+ const next = new URLSearchParams(prev);
3906
+ next.set("tab", tab);
3907
+ return next;
3908
+ },
3909
+ { replace: true }
3910
+ );
3911
+ },
3912
+ [setSearchParams]
3913
+ );
3879
3914
  const [snackbar, setSnackbar] = (0, import_react11.useState)(null);
3880
3915
  const [generateDialogOpen, setGenerateDialogOpen] = (0, import_react11.useState)(false);
3881
3916
  const [manageTeam, setManageTeam] = (0, import_react11.useState)(null);
@@ -4082,7 +4117,7 @@ var init_LiteLLMPage = __esm({
4082
4117
  import_Tabs2.default,
4083
4118
  {
4084
4119
  value: activeTab,
4085
- onChange: (_, v) => setActiveTab(v),
4120
+ onChange: (_, v) => selectTab(v),
4086
4121
  variant: "scrollable",
4087
4122
  scrollButtons: "auto",
4088
4123
  sx: {