@acarmisc/backstage-plugin-litellm 0.2.1 → 0.3.1

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.
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ export interface LiteLLMHomeWidgetProps {
3
+ /** Default period when the widget mounts. Defaults to '7d'. */
4
+ defaultPeriod?: 'today' | '7d' | '30d';
5
+ /** Optional title override. Defaults to 'LiteLLM Usage'. */
6
+ title?: string;
7
+ }
8
+ export declare const LiteLLMHomeWidget: React.FC<LiteLLMHomeWidgetProps>;
package/dist/index.cjs.js CHANGED
@@ -165,7 +165,7 @@ var init_DashboardHeader = __esm({
165
165
  });
166
166
 
167
167
  // src/components/KeysTable.tsx
168
- var import_react2, import_material2, import_icons_material2, maskKey, formatDate, emptyForm, keyToEditForm, KeysTable;
168
+ var import_react2, import_material2, import_icons_material2, maskKey, shortKeyId, formatDate, emptyForm, keyToEditForm, KeysTable;
169
169
  var init_KeysTable = __esm({
170
170
  "src/components/KeysTable.tsx"() {
171
171
  "use strict";
@@ -176,6 +176,11 @@ var init_KeysTable = __esm({
176
176
  if (key.length <= 8) return "***";
177
177
  return `${key.slice(0, 4)}...${key.slice(-4)}`;
178
178
  };
179
+ shortKeyId = (token) => {
180
+ if (!token) return "-";
181
+ if (token.length <= 16) return token;
182
+ return `${token.slice(0, 12)}\u2026`;
183
+ };
179
184
  formatDate = (dateStr) => {
180
185
  try {
181
186
  return new Date(dateStr).toLocaleDateString();
@@ -209,7 +214,6 @@ var init_KeysTable = __esm({
209
214
  onDeleteKey
210
215
  }) => {
211
216
  const [generateModalOpen, setGenerateModalOpen] = (0, import_react2.useState)(false);
212
- const [showKeyValue, setShowKeyValue] = (0, import_react2.useState)(null);
213
217
  const [newKeyValue, setNewKeyValue] = (0, import_react2.useState)(null);
214
218
  const [formData, setFormData] = (0, import_react2.useState)(emptyForm());
215
219
  const [submitting, setSubmitting] = (0, import_react2.useState)(false);
@@ -279,32 +283,26 @@ var init_KeysTable = __esm({
279
283
  onClick: () => setGenerateModalOpen(true)
280
284
  },
281
285
  "Generate New Key"
282
- )), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableContainer, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Table, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableHead, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Alias"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Key"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Created"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Spend"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Budget"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "TPM Limit"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Models"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { align: "right" }, "Actions"))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableBody, null, loading ? /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 24 }))) : keys.length === 0 ? /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { color: "text.secondary" }, "No keys found"))) : keys.map((key) => /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, { key: key.key }, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, key.key_alias || "-"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", alignItems: "center", gap: 0.5 }, /* @__PURE__ */ import_react2.default.createElement(
283
- import_material2.Typography,
284
- {
285
- variant: "body2",
286
- component: "code",
287
- color: "primary",
288
- sx: {
289
- fontFamily: "monospace",
290
- backgroundColor: "background.default",
291
- px: 1,
292
- py: 0.5,
293
- borderRadius: 1,
294
- maxWidth: "250px",
295
- overflow: "hidden",
296
- textOverflow: "ellipsis"
297
- }
298
- },
299
- showKeyValue === key.key ? key.key : maskKey(key.key)
300
- ), /* @__PURE__ */ import_react2.default.createElement(
301
- import_material2.IconButton,
302
- {
303
- size: "small",
304
- onClick: () => setShowKeyValue(showKeyValue === key.key ? null : key.key)
305
- },
306
- showKeyValue === key.key ? /* @__PURE__ */ import_react2.default.createElement(import_icons_material2.VisibilityOff, null) : /* @__PURE__ */ import_react2.default.createElement(import_icons_material2.Visibility, null)
307
- ), /* @__PURE__ */ import_react2.default.createElement(import_material2.IconButton, { size: "small", onClick: () => copyToClipboard(key.key) }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material2.ContentCopy, { fontSize: "small" })))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, formatDate(key.created_at)), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "$", key.spend?.toFixed(4) || "0.00"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, key.max_budget ? `$${key.max_budget}` : "-"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, key.tpm_limit || "-"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", gap: 0.5, flexWrap: "wrap" }, key.models?.slice(0, 2).map((model) => /* @__PURE__ */ import_react2.default.createElement(import_material2.Chip, { key: model, label: model, size: "small" })), (key.models?.length || 0) > 2 && /* @__PURE__ */ import_react2.default.createElement(import_material2.Chip, { label: `+${(key.models?.length || 0) - 2}`, size: "small", variant: "outlined" }))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { align: "right" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.IconButton, { onClick: () => handleOpenEdit(key) }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material2.Edit, { fontSize: "small" })), /* @__PURE__ */ import_react2.default.createElement(import_material2.IconButton, { color: "error", onClick: () => onDeleteKey(key.key) }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material2.Delete, null))))))))), /* @__PURE__ */ import_react2.default.createElement(import_material2.Dialog, { open: generateModalOpen, onClose: handleCloseModal, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogTitle, null, newKeyValue ? "Key Generated" : "Generate New Key"), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogContent, null, newKeyValue ? /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2", color: "text.secondary", gutterBottom: true }, "Copy this key now. You won't be able to see it again."), /* @__PURE__ */ import_react2.default.createElement(
286
+ )), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableContainer, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Table, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableHead, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Alias"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Key ID"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Created"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Spend"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Budget"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "TPM Limit"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Models"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { align: "right" }, "Actions"))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableBody, null, loading ? /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 24 }))) : keys.length === 0 ? /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { color: "text.secondary" }, "No keys found"))) : keys.map((key) => {
287
+ const keyId = key.token ?? key.key;
288
+ return /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, { key: keyId }, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, key.key_alias || "-"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", alignItems: "center", gap: 0.5 }, /* @__PURE__ */ import_react2.default.createElement(
289
+ import_material2.Typography,
290
+ {
291
+ variant: "body2",
292
+ component: "code",
293
+ color: "text.secondary",
294
+ title: keyId,
295
+ sx: {
296
+ fontFamily: "monospace",
297
+ backgroundColor: "background.default",
298
+ px: 1,
299
+ py: 0.5,
300
+ borderRadius: 1
301
+ }
302
+ },
303
+ shortKeyId(keyId)
304
+ ), /* @__PURE__ */ import_react2.default.createElement(import_material2.IconButton, { size: "small", onClick: () => copyToClipboard(keyId), title: "Copy Key ID" }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material2.ContentCopy, { fontSize: "small" })))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, formatDate(key.created_at)), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "$", key.spend?.toFixed(4) || "0.00"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, key.max_budget ? `$${key.max_budget}` : "-"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, key.tpm_limit || "-"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", gap: 0.5, flexWrap: "wrap" }, key.models?.slice(0, 2).map((model) => /* @__PURE__ */ import_react2.default.createElement(import_material2.Chip, { key: model, label: model, size: "small" })), (key.models?.length || 0) > 2 && /* @__PURE__ */ import_react2.default.createElement(import_material2.Chip, { label: `+${(key.models?.length || 0) - 2}`, size: "small", variant: "outlined" }))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { align: "right" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.IconButton, { onClick: () => handleOpenEdit(key) }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material2.Edit, { fontSize: "small" })), /* @__PURE__ */ import_react2.default.createElement(import_material2.IconButton, { color: "error", onClick: () => onDeleteKey(key.key) }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material2.Delete, null))));
305
+ }))))), /* @__PURE__ */ import_react2.default.createElement(import_material2.Dialog, { open: generateModalOpen, onClose: handleCloseModal, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogTitle, null, newKeyValue ? "Key Generated" : "Generate New Key"), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogContent, null, newKeyValue ? /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2", color: "text.secondary", gutterBottom: true }, "Copy this key now. You won't be able to see it again."), /* @__PURE__ */ import_react2.default.createElement(
308
306
  import_material2.Box,
309
307
  {
310
308
  display: "flex",
@@ -868,6 +866,7 @@ var index_exports = {};
868
866
  __export(index_exports, {
869
867
  DashboardHeader: () => DashboardHeader,
870
868
  KeysTable: () => KeysTable,
869
+ LiteLLMHomeWidget: () => LiteLLMHomeWidget,
871
870
  LiteLLMPage: () => LiteLLMPage,
872
871
  LiteLlmApi: () => LiteLlmApi,
873
872
  TeamUsage: () => TeamUsage,
@@ -911,5 +910,90 @@ init_DashboardHeader();
911
910
  init_KeysTable();
912
911
  init_UsageStats();
913
912
  init_TeamUsage();
913
+
914
+ // src/components/LiteLLMHomeWidget.tsx
915
+ var import_react7 = __toESM(require("react"));
916
+ var import_material6 = require("@mui/material");
917
+ var import_recharts3 = require("recharts");
918
+ var import_core_plugin_api3 = require("@backstage/core-plugin-api");
919
+ init_api();
920
+ var fmtUsd2 = (n) => `$${(n ?? 0).toFixed(n < 1 ? 4 : 2)}`;
921
+ var fmtInt2 = (n) => (n ?? 0).toLocaleString();
922
+ function presetToDateRange(preset) {
923
+ const end = /* @__PURE__ */ new Date();
924
+ const start = /* @__PURE__ */ new Date();
925
+ if (preset === "today") {
926
+ start.setHours(0, 0, 0, 0);
927
+ } else if (preset === "7d") {
928
+ start.setDate(start.getDate() - 7);
929
+ } else {
930
+ start.setDate(start.getDate() - 30);
931
+ }
932
+ return { start, end };
933
+ }
934
+ var Kpi = ({ label, value }) => /* @__PURE__ */ import_react7.default.createElement(import_material6.Box, null, /* @__PURE__ */ import_react7.default.createElement(import_material6.Typography, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ import_react7.default.createElement(import_material6.Typography, { variant: "subtitle1", fontWeight: 600 }, value));
935
+ var LiteLLMHomeWidget = ({
936
+ defaultPeriod = "7d",
937
+ title = "LiteLLM Usage"
938
+ }) => {
939
+ const api = (0, import_core_plugin_api3.useApi)(liteLlmApiRef);
940
+ const [period, setPeriod] = (0, import_react7.useState)(defaultPeriod);
941
+ const [loading, setLoading] = (0, import_react7.useState)(true);
942
+ const [error, setError] = (0, import_react7.useState)(null);
943
+ const [usage, setUsage] = (0, import_react7.useState)(null);
944
+ const [keys, setKeys] = (0, import_react7.useState)([]);
945
+ (0, import_react7.useEffect)(() => {
946
+ let cancelled = false;
947
+ setLoading(true);
948
+ setError(null);
949
+ const { start, end } = presetToDateRange(period);
950
+ const startDate = start.toISOString().split("T")[0];
951
+ const endDate = end.toISOString().split("T")[0];
952
+ Promise.all([api.getUsage(startDate, endDate), api.listKeys()]).then(([usageData, keysData]) => {
953
+ if (!cancelled) {
954
+ setUsage(usageData);
955
+ setKeys(keysData);
956
+ setLoading(false);
957
+ }
958
+ }).catch((err) => {
959
+ if (!cancelled) {
960
+ setError(err.message ?? "Failed to load usage data");
961
+ setLoading(false);
962
+ }
963
+ });
964
+ return () => {
965
+ cancelled = true;
966
+ };
967
+ }, [api, period]);
968
+ const dailyData = (usage?.daily_usage ?? []).map((d) => ({
969
+ date: d.date,
970
+ spend: d.spend
971
+ }));
972
+ const hasSparkline = dailyData.length > 0;
973
+ return /* @__PURE__ */ import_react7.default.createElement(import_material6.Paper, { sx: { p: 2 } }, /* @__PURE__ */ import_react7.default.createElement(import_material6.Box, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1.5 }, /* @__PURE__ */ import_react7.default.createElement(import_material6.Typography, { variant: "h6" }, title), /* @__PURE__ */ import_react7.default.createElement(import_material6.FormControl, { size: "small", sx: { minWidth: 90 } }, /* @__PURE__ */ import_react7.default.createElement(
974
+ import_material6.Select,
975
+ {
976
+ value: period,
977
+ onChange: (e) => setPeriod(e.target.value),
978
+ displayEmpty: true
979
+ },
980
+ /* @__PURE__ */ import_react7.default.createElement(import_material6.MenuItem, { value: "today" }, "Today"),
981
+ /* @__PURE__ */ import_react7.default.createElement(import_material6.MenuItem, { value: "7d" }, "7d"),
982
+ /* @__PURE__ */ import_react7.default.createElement(import_material6.MenuItem, { value: "30d" }, "30d")
983
+ ))), loading && /* @__PURE__ */ import_react7.default.createElement(import_material6.Box, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ import_react7.default.createElement(import_material6.CircularProgress, { size: 32 })), !loading && error && /* @__PURE__ */ import_react7.default.createElement(import_material6.Alert, { severity: "error", sx: { mt: 1 } }, error), !loading && !error && /* @__PURE__ */ import_react7.default.createElement(import_react7.default.Fragment, null, /* @__PURE__ */ import_react7.default.createElement(import_material6.Grid, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ import_react7.default.createElement(import_material6.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react7.default.createElement(Kpi, { label: "USD Spent", value: fmtUsd2(usage?.total_spend ?? 0) })), /* @__PURE__ */ import_react7.default.createElement(import_material6.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react7.default.createElement(Kpi, { label: "Tokens In", value: fmtInt2(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ import_react7.default.createElement(import_material6.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react7.default.createElement(Kpi, { label: "Tokens Out", value: fmtInt2(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ import_react7.default.createElement(import_material6.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react7.default.createElement(Kpi, { label: "Keys", value: fmtInt2(keys.length) }))), hasSparkline && /* @__PURE__ */ import_react7.default.createElement(import_material6.Box, { height: 120 }, /* @__PURE__ */ import_react7.default.createElement(import_recharts3.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react7.default.createElement(import_recharts3.AreaChart, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 } }, /* @__PURE__ */ import_react7.default.createElement(
984
+ import_recharts3.Area,
985
+ {
986
+ type: "monotone",
987
+ dataKey: "spend",
988
+ stroke: "#8884d8",
989
+ fill: "#8884d8",
990
+ fillOpacity: 0.3,
991
+ dot: false,
992
+ isAnimationActive: false
993
+ }
994
+ ))))));
995
+ };
996
+
997
+ // src/index.ts
914
998
  init_api();
915
999
  //# sourceMappingURL=index.cjs.js.map