@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.
package/dist/index.esm.js CHANGED
@@ -166,8 +166,8 @@ import {
166
166
  CircularProgress,
167
167
  Autocomplete
168
168
  } from "@mui/material";
169
- import { ContentCopy, Delete, Add, Edit, Visibility, VisibilityOff } from "@mui/icons-material";
170
- var maskKey, formatDate, emptyForm, keyToEditForm, KeysTable;
169
+ import { ContentCopy, Delete, Add, Edit } from "@mui/icons-material";
170
+ var maskKey, shortKeyId, formatDate, emptyForm, keyToEditForm, KeysTable;
171
171
  var init_KeysTable = __esm({
172
172
  "src/components/KeysTable.tsx"() {
173
173
  "use strict";
@@ -175,6 +175,11 @@ var init_KeysTable = __esm({
175
175
  if (key.length <= 8) return "***";
176
176
  return `${key.slice(0, 4)}...${key.slice(-4)}`;
177
177
  };
178
+ shortKeyId = (token) => {
179
+ if (!token) return "-";
180
+ if (token.length <= 16) return token;
181
+ return `${token.slice(0, 12)}\u2026`;
182
+ };
178
183
  formatDate = (dateStr) => {
179
184
  try {
180
185
  return new Date(dateStr).toLocaleDateString();
@@ -208,7 +213,6 @@ var init_KeysTable = __esm({
208
213
  onDeleteKey
209
214
  }) => {
210
215
  const [generateModalOpen, setGenerateModalOpen] = useState(false);
211
- const [showKeyValue, setShowKeyValue] = useState(null);
212
216
  const [newKeyValue, setNewKeyValue] = useState(null);
213
217
  const [formData, setFormData] = useState(emptyForm());
214
218
  const [submitting, setSubmitting] = useState(false);
@@ -278,32 +282,26 @@ var init_KeysTable = __esm({
278
282
  onClick: () => setGenerateModalOpen(true)
279
283
  },
280
284
  "Generate New Key"
281
- )), /* @__PURE__ */ React2.createElement(TableContainer, null, /* @__PURE__ */ React2.createElement(Table, null, /* @__PURE__ */ React2.createElement(TableHead, null, /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, null, "Alias"), /* @__PURE__ */ React2.createElement(TableCell, null, "Key"), /* @__PURE__ */ React2.createElement(TableCell, null, "Created"), /* @__PURE__ */ React2.createElement(TableCell, null, "Spend"), /* @__PURE__ */ React2.createElement(TableCell, null, "Budget"), /* @__PURE__ */ React2.createElement(TableCell, null, "TPM Limit"), /* @__PURE__ */ React2.createElement(TableCell, null, "Models"), /* @__PURE__ */ React2.createElement(TableCell, { align: "right" }, "Actions"))), /* @__PURE__ */ React2.createElement(TableBody, null, loading ? /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ React2.createElement(CircularProgress, { size: 24 }))) : keys.length === 0 ? /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ React2.createElement(Typography2, { color: "text.secondary" }, "No keys found"))) : keys.map((key) => /* @__PURE__ */ React2.createElement(TableRow, { key: key.key }, /* @__PURE__ */ React2.createElement(TableCell, null, key.key_alias || "-"), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", alignItems: "center", gap: 0.5 }, /* @__PURE__ */ React2.createElement(
282
- Typography2,
283
- {
284
- variant: "body2",
285
- component: "code",
286
- color: "primary",
287
- sx: {
288
- fontFamily: "monospace",
289
- backgroundColor: "background.default",
290
- px: 1,
291
- py: 0.5,
292
- borderRadius: 1,
293
- maxWidth: "250px",
294
- overflow: "hidden",
295
- textOverflow: "ellipsis"
296
- }
297
- },
298
- showKeyValue === key.key ? key.key : maskKey(key.key)
299
- ), /* @__PURE__ */ React2.createElement(
300
- IconButton,
301
- {
302
- size: "small",
303
- onClick: () => setShowKeyValue(showKeyValue === key.key ? null : key.key)
304
- },
305
- showKeyValue === key.key ? /* @__PURE__ */ React2.createElement(VisibilityOff, null) : /* @__PURE__ */ React2.createElement(Visibility, null)
306
- ), /* @__PURE__ */ React2.createElement(IconButton, { size: "small", onClick: () => copyToClipboard(key.key) }, /* @__PURE__ */ React2.createElement(ContentCopy, { fontSize: "small" })))), /* @__PURE__ */ React2.createElement(TableCell, null, formatDate(key.created_at)), /* @__PURE__ */ React2.createElement(TableCell, null, "$", key.spend?.toFixed(4) || "0.00"), /* @__PURE__ */ React2.createElement(TableCell, null, key.max_budget ? `$${key.max_budget}` : "-"), /* @__PURE__ */ React2.createElement(TableCell, null, key.tpm_limit || "-"), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", gap: 0.5, flexWrap: "wrap" }, key.models?.slice(0, 2).map((model) => /* @__PURE__ */ React2.createElement(Chip2, { key: model, label: model, size: "small" })), (key.models?.length || 0) > 2 && /* @__PURE__ */ React2.createElement(Chip2, { label: `+${(key.models?.length || 0) - 2}`, size: "small", variant: "outlined" }))), /* @__PURE__ */ React2.createElement(TableCell, { align: "right" }, /* @__PURE__ */ React2.createElement(IconButton, { onClick: () => handleOpenEdit(key) }, /* @__PURE__ */ React2.createElement(Edit, { fontSize: "small" })), /* @__PURE__ */ React2.createElement(IconButton, { color: "error", onClick: () => onDeleteKey(key.key) }, /* @__PURE__ */ React2.createElement(Delete, null))))))))), /* @__PURE__ */ React2.createElement(Dialog, { open: generateModalOpen, onClose: handleCloseModal, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ React2.createElement(DialogTitle, null, newKeyValue ? "Key Generated" : "Generate New Key"), /* @__PURE__ */ React2.createElement(DialogContent, null, newKeyValue ? /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "text.secondary", gutterBottom: true }, "Copy this key now. You won't be able to see it again."), /* @__PURE__ */ React2.createElement(
285
+ )), /* @__PURE__ */ React2.createElement(TableContainer, null, /* @__PURE__ */ React2.createElement(Table, null, /* @__PURE__ */ React2.createElement(TableHead, null, /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, null, "Alias"), /* @__PURE__ */ React2.createElement(TableCell, null, "Key ID"), /* @__PURE__ */ React2.createElement(TableCell, null, "Created"), /* @__PURE__ */ React2.createElement(TableCell, null, "Spend"), /* @__PURE__ */ React2.createElement(TableCell, null, "Budget"), /* @__PURE__ */ React2.createElement(TableCell, null, "TPM Limit"), /* @__PURE__ */ React2.createElement(TableCell, null, "Models"), /* @__PURE__ */ React2.createElement(TableCell, { align: "right" }, "Actions"))), /* @__PURE__ */ React2.createElement(TableBody, null, loading ? /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ React2.createElement(CircularProgress, { size: 24 }))) : keys.length === 0 ? /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ React2.createElement(Typography2, { color: "text.secondary" }, "No keys found"))) : keys.map((key) => {
286
+ const keyId = key.token ?? key.key;
287
+ return /* @__PURE__ */ React2.createElement(TableRow, { key: keyId }, /* @__PURE__ */ React2.createElement(TableCell, null, key.key_alias || "-"), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", alignItems: "center", gap: 0.5 }, /* @__PURE__ */ React2.createElement(
288
+ Typography2,
289
+ {
290
+ variant: "body2",
291
+ component: "code",
292
+ color: "text.secondary",
293
+ title: keyId,
294
+ sx: {
295
+ fontFamily: "monospace",
296
+ backgroundColor: "background.default",
297
+ px: 1,
298
+ py: 0.5,
299
+ borderRadius: 1
300
+ }
301
+ },
302
+ shortKeyId(keyId)
303
+ ), /* @__PURE__ */ React2.createElement(IconButton, { size: "small", onClick: () => copyToClipboard(keyId), title: "Copy Key ID" }, /* @__PURE__ */ React2.createElement(ContentCopy, { fontSize: "small" })))), /* @__PURE__ */ React2.createElement(TableCell, null, formatDate(key.created_at)), /* @__PURE__ */ React2.createElement(TableCell, null, "$", key.spend?.toFixed(4) || "0.00"), /* @__PURE__ */ React2.createElement(TableCell, null, key.max_budget ? `$${key.max_budget}` : "-"), /* @__PURE__ */ React2.createElement(TableCell, null, key.tpm_limit || "-"), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", gap: 0.5, flexWrap: "wrap" }, key.models?.slice(0, 2).map((model) => /* @__PURE__ */ React2.createElement(Chip2, { key: model, label: model, size: "small" })), (key.models?.length || 0) > 2 && /* @__PURE__ */ React2.createElement(Chip2, { label: `+${(key.models?.length || 0) - 2}`, size: "small", variant: "outlined" }))), /* @__PURE__ */ React2.createElement(TableCell, { align: "right" }, /* @__PURE__ */ React2.createElement(IconButton, { onClick: () => handleOpenEdit(key) }, /* @__PURE__ */ React2.createElement(Edit, { fontSize: "small" })), /* @__PURE__ */ React2.createElement(IconButton, { color: "error", onClick: () => onDeleteKey(key.key) }, /* @__PURE__ */ React2.createElement(Delete, null))));
304
+ }))))), /* @__PURE__ */ React2.createElement(Dialog, { open: generateModalOpen, onClose: handleCloseModal, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ React2.createElement(DialogTitle, null, newKeyValue ? "Key Generated" : "Generate New Key"), /* @__PURE__ */ React2.createElement(DialogContent, null, newKeyValue ? /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "text.secondary", gutterBottom: true }, "Copy this key now. You won't be able to see it again."), /* @__PURE__ */ React2.createElement(
307
305
  Box2,
308
306
  {
309
307
  display: "flex",
@@ -955,10 +953,106 @@ init_DashboardHeader();
955
953
  init_KeysTable();
956
954
  init_UsageStats();
957
955
  init_TeamUsage();
956
+
957
+ // src/components/LiteLLMHomeWidget.tsx
958
+ init_api();
959
+ import React7, { useState as useState5, useEffect as useEffect2 } from "react";
960
+ import {
961
+ Paper as Paper6,
962
+ Box as Box6,
963
+ Typography as Typography6,
964
+ FormControl as FormControl2,
965
+ Select as Select2,
966
+ MenuItem as MenuItem3,
967
+ Grid as Grid3,
968
+ CircularProgress as CircularProgress5,
969
+ Alert as Alert2
970
+ } from "@mui/material";
971
+ import { AreaChart as AreaChart3, Area as Area3, ResponsiveContainer as ResponsiveContainer3 } from "recharts";
972
+ import { useApi as useApi2 } from "@backstage/core-plugin-api";
973
+ var fmtUsd2 = (n) => `$${(n ?? 0).toFixed(n < 1 ? 4 : 2)}`;
974
+ var fmtInt2 = (n) => (n ?? 0).toLocaleString();
975
+ function presetToDateRange(preset) {
976
+ const end = /* @__PURE__ */ new Date();
977
+ const start = /* @__PURE__ */ new Date();
978
+ if (preset === "today") {
979
+ start.setHours(0, 0, 0, 0);
980
+ } else if (preset === "7d") {
981
+ start.setDate(start.getDate() - 7);
982
+ } else {
983
+ start.setDate(start.getDate() - 30);
984
+ }
985
+ return { start, end };
986
+ }
987
+ var Kpi = ({ label, value }) => /* @__PURE__ */ React7.createElement(Box6, null, /* @__PURE__ */ React7.createElement(Typography6, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ React7.createElement(Typography6, { variant: "subtitle1", fontWeight: 600 }, value));
988
+ var LiteLLMHomeWidget = ({
989
+ defaultPeriod = "7d",
990
+ title = "LiteLLM Usage"
991
+ }) => {
992
+ const api = useApi2(liteLlmApiRef);
993
+ const [period, setPeriod] = useState5(defaultPeriod);
994
+ const [loading, setLoading] = useState5(true);
995
+ const [error, setError] = useState5(null);
996
+ const [usage, setUsage] = useState5(null);
997
+ const [keys, setKeys] = useState5([]);
998
+ useEffect2(() => {
999
+ let cancelled = false;
1000
+ setLoading(true);
1001
+ setError(null);
1002
+ const { start, end } = presetToDateRange(period);
1003
+ const startDate = start.toISOString().split("T")[0];
1004
+ const endDate = end.toISOString().split("T")[0];
1005
+ Promise.all([api.getUsage(startDate, endDate), api.listKeys()]).then(([usageData, keysData]) => {
1006
+ if (!cancelled) {
1007
+ setUsage(usageData);
1008
+ setKeys(keysData);
1009
+ setLoading(false);
1010
+ }
1011
+ }).catch((err) => {
1012
+ if (!cancelled) {
1013
+ setError(err.message ?? "Failed to load usage data");
1014
+ setLoading(false);
1015
+ }
1016
+ });
1017
+ return () => {
1018
+ cancelled = true;
1019
+ };
1020
+ }, [api, period]);
1021
+ const dailyData = (usage?.daily_usage ?? []).map((d) => ({
1022
+ date: d.date,
1023
+ spend: d.spend
1024
+ }));
1025
+ const hasSparkline = dailyData.length > 0;
1026
+ return /* @__PURE__ */ React7.createElement(Paper6, { sx: { p: 2 } }, /* @__PURE__ */ React7.createElement(Box6, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1.5 }, /* @__PURE__ */ React7.createElement(Typography6, { variant: "h6" }, title), /* @__PURE__ */ React7.createElement(FormControl2, { size: "small", sx: { minWidth: 90 } }, /* @__PURE__ */ React7.createElement(
1027
+ Select2,
1028
+ {
1029
+ value: period,
1030
+ onChange: (e) => setPeriod(e.target.value),
1031
+ displayEmpty: true
1032
+ },
1033
+ /* @__PURE__ */ React7.createElement(MenuItem3, { value: "today" }, "Today"),
1034
+ /* @__PURE__ */ React7.createElement(MenuItem3, { value: "7d" }, "7d"),
1035
+ /* @__PURE__ */ React7.createElement(MenuItem3, { value: "30d" }, "30d")
1036
+ ))), loading && /* @__PURE__ */ React7.createElement(Box6, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ React7.createElement(CircularProgress5, { size: 32 })), !loading && error && /* @__PURE__ */ React7.createElement(Alert2, { severity: "error", sx: { mt: 1 } }, error), !loading && !error && /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement(Grid3, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ React7.createElement(Grid3, { item: true, xs: 6 }, /* @__PURE__ */ React7.createElement(Kpi, { label: "USD Spent", value: fmtUsd2(usage?.total_spend ?? 0) })), /* @__PURE__ */ React7.createElement(Grid3, { item: true, xs: 6 }, /* @__PURE__ */ React7.createElement(Kpi, { label: "Tokens In", value: fmtInt2(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ React7.createElement(Grid3, { item: true, xs: 6 }, /* @__PURE__ */ React7.createElement(Kpi, { label: "Tokens Out", value: fmtInt2(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ React7.createElement(Grid3, { item: true, xs: 6 }, /* @__PURE__ */ React7.createElement(Kpi, { label: "Keys", value: fmtInt2(keys.length) }))), hasSparkline && /* @__PURE__ */ React7.createElement(Box6, { height: 120 }, /* @__PURE__ */ React7.createElement(ResponsiveContainer3, { width: "100%", height: "100%" }, /* @__PURE__ */ React7.createElement(AreaChart3, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 } }, /* @__PURE__ */ React7.createElement(
1037
+ Area3,
1038
+ {
1039
+ type: "monotone",
1040
+ dataKey: "spend",
1041
+ stroke: "#8884d8",
1042
+ fill: "#8884d8",
1043
+ fillOpacity: 0.3,
1044
+ dot: false,
1045
+ isAnimationActive: false
1046
+ }
1047
+ ))))));
1048
+ };
1049
+
1050
+ // src/index.ts
958
1051
  init_api();
959
1052
  export {
960
1053
  DashboardHeader,
961
1054
  KeysTable,
1055
+ LiteLLMHomeWidget,
962
1056
  LiteLLMPage,
963
1057
  LiteLlmApi,
964
1058
  TeamUsage,