@acarmisc/backstage-plugin-litellm 0.1.0 → 0.1.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/index.esm.js CHANGED
@@ -1,62 +1,122 @@
1
- // src/plugin.ts
2
- import { createFrontendPlugin } from "@backstage/frontend-plugin-api";
3
- import { PageExtension } from "@backstage/frontend-plugin-api";
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
4
10
 
5
- // src/components/LiteLLMPage.tsx
6
- import { useState as useState3, useEffect, useCallback } from "react";
7
- import { Grid as Grid2, Box as Box4, Snackbar, Alert, CircularProgress as CircularProgress3 } from "@material-ui/core";
8
- import { useAsync } from "react-use";
11
+ // src/api.ts
12
+ import { createApiRef } from "@backstage/core-plugin-api";
13
+ var liteLlmApiRef, LiteLlmApi;
14
+ var init_api = __esm({
15
+ "src/api.ts"() {
16
+ "use strict";
17
+ liteLlmApiRef = createApiRef({
18
+ id: "plugin.litellm.api"
19
+ });
20
+ LiteLlmApi = class {
21
+ constructor(fetchApi, basePath = "/api/litellm") {
22
+ this.fetchApi = fetchApi;
23
+ this.basePath = basePath;
24
+ }
25
+ async get(path, params) {
26
+ const url = new URL(`${this.basePath}${path}`, window.location.origin);
27
+ if (params) {
28
+ Object.entries(params).forEach(([key, value]) => {
29
+ url.searchParams.append(key, value);
30
+ });
31
+ }
32
+ const response = await this.fetchApi.fetch(url.toString());
33
+ if (!response.ok) {
34
+ throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);
35
+ }
36
+ return response.json();
37
+ }
38
+ async post(path, body) {
39
+ const response = await this.fetchApi.fetch(`${this.basePath}${path}`, {
40
+ method: "POST",
41
+ headers: { "Content-Type": "application/json" },
42
+ body: JSON.stringify(body)
43
+ });
44
+ if (!response.ok) {
45
+ throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);
46
+ }
47
+ return response.json();
48
+ }
49
+ async del(path) {
50
+ const response = await this.fetchApi.fetch(`${this.basePath}${path}`, {
51
+ method: "DELETE",
52
+ headers: { "Content-Type": "application/json" }
53
+ });
54
+ if (!response.ok) {
55
+ throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);
56
+ }
57
+ return response.json();
58
+ }
59
+ async getUserInfo(userId) {
60
+ const params = userId ? { user_id: userId } : void 0;
61
+ return this.get("/user/info", params);
62
+ }
63
+ async listKeys(userId) {
64
+ const params = userId ? { user_id: userId } : void 0;
65
+ return this.get("/keys", params);
66
+ }
67
+ async generateKey(request) {
68
+ return this.post("/keys/generate", request);
69
+ }
70
+ async deleteKey(keyId) {
71
+ return this.del(`/keys/${encodeURIComponent(keyId)}`);
72
+ }
73
+ async listModels() {
74
+ return this.get("/models");
75
+ }
76
+ async getUsage(startDate, endDate, userId) {
77
+ const params = { start_date: startDate, end_date: endDate };
78
+ if (userId) params.user_id = userId;
79
+ return this.get("/usage", params);
80
+ }
81
+ };
82
+ }
83
+ });
9
84
 
10
85
  // src/components/DashboardHeader.tsx
11
- import { Box, Typography, LinearProgress, Paper, Chip } from "@material-ui/core";
12
- import { Warning, Group } from "@material-ui/icons";
13
- import { jsx, jsxs } from "react/jsx-runtime";
14
- var DashboardHeader = ({ userInfo, loading }) => {
15
- if (loading) {
16
- return /* @__PURE__ */ jsx(Paper, { style: { padding: 16, marginBottom: 16 }, children: /* @__PURE__ */ jsx(LinearProgress, {}) });
17
- }
18
- if (!userInfo) {
19
- return null;
20
- }
21
- const budget = userInfo.max_budget ?? 0;
22
- const spend = userInfo.current_spend ?? 0;
23
- const budgetPercentage = budget > 0 ? Math.min(spend / budget * 100, 100) : 0;
24
- const isOverBudget = budget > 0 && spend >= budget;
25
- const isNearLimit = budget > 0 && spend >= budget * 0.8 && !isOverBudget;
26
- return /* @__PURE__ */ jsx(Paper, { style: { padding: 16, marginBottom: 16 }, children: /* @__PURE__ */ jsxs(Box, { display: "flex", alignItems: "center", gap: 2, flexWrap: "wrap", children: [
27
- /* @__PURE__ */ jsxs(Box, { flexGrow: 1, children: [
28
- /* @__PURE__ */ jsx(Typography, { variant: "h6", children: userInfo.email }),
29
- userInfo.team_alias && /* @__PURE__ */ jsxs(Box, { display: "flex", alignItems: "center", gap: 0.5, mt: 0.5, children: [
30
- /* @__PURE__ */ jsx(Group, { fontSize: "small", color: "action" }),
31
- /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "textSecondary", children: userInfo.team_alias })
32
- ] })
33
- ] }),
34
- budget > 0 && /* @__PURE__ */ jsxs(Box, { minWidth: 200, children: [
35
- /* @__PURE__ */ jsxs(Box, { display: "flex", justifyContent: "space-between", mb: 0.5, children: [
36
- /* @__PURE__ */ jsxs(Typography, { variant: "body2", children: [
37
- "Spend: $",
38
- spend.toFixed(2),
39
- " / $",
40
- budget.toFixed(2)
41
- ] }),
42
- isOverBudget && /* @__PURE__ */ jsx(Chip, { icon: /* @__PURE__ */ jsx(Warning, {}), label: "Over Budget", size: "small", color: "secondary" }),
43
- isNearLimit && /* @__PURE__ */ jsx(Chip, { label: "Near Limit", size: "small", color: "warning" })
44
- ] }),
45
- /* @__PURE__ */ jsx(
86
+ import React from "react";
87
+ import { Box, Typography, LinearProgress, Paper, Chip } from "@mui/material";
88
+ import { Warning, Group } from "@mui/icons-material";
89
+ var DashboardHeader;
90
+ var init_DashboardHeader = __esm({
91
+ "src/components/DashboardHeader.tsx"() {
92
+ "use strict";
93
+ DashboardHeader = ({ userInfo, loading }) => {
94
+ if (loading) {
95
+ return /* @__PURE__ */ React.createElement(Paper, { sx: { p: 2, mb: 2 } }, /* @__PURE__ */ React.createElement(LinearProgress, null));
96
+ }
97
+ if (!userInfo) {
98
+ return null;
99
+ }
100
+ const budget = userInfo.max_budget ?? 0;
101
+ const spend = userInfo.current_spend ?? 0;
102
+ const budgetPercentage = budget > 0 ? Math.min(spend / budget * 100, 100) : 0;
103
+ const isOverBudget = budget > 0 && spend >= budget;
104
+ const isNearLimit = budget > 0 && spend >= budget * 0.8 && !isOverBudget;
105
+ return /* @__PURE__ */ React.createElement(Paper, { sx: { p: 2, mb: 2 } }, /* @__PURE__ */ React.createElement(Box, { display: "flex", alignItems: "center", gap: 2, flexWrap: "wrap" }, /* @__PURE__ */ React.createElement(Box, { flexGrow: 1 }, /* @__PURE__ */ React.createElement(Typography, { variant: "h6" }, userInfo.email), userInfo.team_alias && /* @__PURE__ */ React.createElement(Box, { display: "flex", alignItems: "center", gap: 0.5, mt: 0.5 }, /* @__PURE__ */ React.createElement(Group, { fontSize: "small", color: "action" }), /* @__PURE__ */ React.createElement(Typography, { variant: "body2", color: "text.secondary" }, userInfo.team_alias))), budget > 0 && /* @__PURE__ */ React.createElement(Box, { minWidth: 200 }, /* @__PURE__ */ React.createElement(Box, { display: "flex", justifyContent: "space-between", mb: 0.5 }, /* @__PURE__ */ React.createElement(Typography, { variant: "body2" }, "Spend: $", spend.toFixed(2), " / $", budget.toFixed(2)), isOverBudget && /* @__PURE__ */ React.createElement(Chip, { icon: /* @__PURE__ */ React.createElement(Warning, null), label: "Over Budget", size: "small", color: "error" }), isNearLimit && /* @__PURE__ */ React.createElement(Chip, { label: "Near Limit", size: "small", color: "warning" })), /* @__PURE__ */ React.createElement(
46
106
  LinearProgress,
47
107
  {
48
108
  variant: "determinate",
49
109
  value: budgetPercentage,
50
- color: isOverBudget ? "secondary" : isNearLimit ? "warning" : "primary",
51
- style: { height: 8, borderRadius: 4 }
110
+ color: isOverBudget ? "error" : isNearLimit ? "warning" : "primary",
111
+ sx: { height: 8, borderRadius: 1 }
52
112
  }
53
- )
54
- ] })
55
- ] }) });
56
- };
113
+ ))));
114
+ };
115
+ }
116
+ });
57
117
 
58
118
  // src/components/KeysTable.tsx
59
- import { useState } from "react";
119
+ import React2, { useState } from "react";
60
120
  import {
61
121
  Paper as Paper2,
62
122
  Table,
@@ -77,202 +137,147 @@ import {
77
137
  MenuItem,
78
138
  Chip as Chip2,
79
139
  CircularProgress
80
- } from "@material-ui/core";
81
- import { ContentCopy, Delete, Add, Visibility, VisibilityOff } from "@material-ui/icons";
82
- import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
83
- var maskKey = (key) => {
84
- if (key.length <= 8) return "***";
85
- return `${key.slice(0, 4)}...${key.slice(-4)}`;
86
- };
87
- var formatDate = (dateStr) => {
88
- try {
89
- return new Date(dateStr).toLocaleDateString();
90
- } catch {
91
- return dateStr;
140
+ } from "@mui/material";
141
+ import { ContentCopy, Delete, Add, Visibility, VisibilityOff } from "@mui/icons-material";
142
+ var maskKey, formatDate, KeysTable;
143
+ var init_KeysTable = __esm({
144
+ "src/components/KeysTable.tsx"() {
145
+ "use strict";
146
+ maskKey = (key) => {
147
+ if (key.length <= 8) return "***";
148
+ return `${key.slice(0, 4)}...${key.slice(-4)}`;
149
+ };
150
+ formatDate = (dateStr) => {
151
+ try {
152
+ return new Date(dateStr).toLocaleDateString();
153
+ } catch {
154
+ return dateStr;
155
+ }
156
+ };
157
+ KeysTable = ({
158
+ keys,
159
+ models,
160
+ loading,
161
+ onGenerateKey,
162
+ onDeleteKey
163
+ }) => {
164
+ const [generateModalOpen, setGenerateModalOpen] = useState(false);
165
+ const [showKeyValue, setShowKeyValue] = useState(null);
166
+ const [newKeyValue, setNewKeyValue] = useState(null);
167
+ const [formData, setFormData] = useState({
168
+ alias: "",
169
+ models: [],
170
+ duration: "30d",
171
+ max_budget: void 0,
172
+ tpm_limit: void 0
173
+ });
174
+ const [submitting, setSubmitting] = useState(false);
175
+ const handleGenerate = async () => {
176
+ setSubmitting(true);
177
+ try {
178
+ const response = await onGenerateKey(formData);
179
+ setNewKeyValue(response?.key || "");
180
+ setFormData({ alias: "", models: [], duration: "30d" });
181
+ } catch (error) {
182
+ console.error("Failed to generate key:", error);
183
+ } finally {
184
+ setSubmitting(false);
185
+ }
186
+ };
187
+ const handleCloseModal = () => {
188
+ setGenerateModalOpen(false);
189
+ setNewKeyValue(null);
190
+ setFormData({ alias: "", models: [], duration: "30d" });
191
+ };
192
+ const copyToClipboard = (text) => {
193
+ navigator.clipboard.writeText(text);
194
+ };
195
+ return /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement(Paper2, { sx: { mb: 2 } }, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", justifyContent: "space-between", alignItems: "center", p: 2 }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "h6" }, "Virtual Keys"), /* @__PURE__ */ React2.createElement(
196
+ Button,
197
+ {
198
+ variant: "contained",
199
+ color: "primary",
200
+ startIcon: /* @__PURE__ */ React2.createElement(Add, null),
201
+ onClick: () => setGenerateModalOpen(true)
202
+ },
203
+ "Generate New Key"
204
+ )), /* @__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(Typography2, { variant: "body2", component: "code", sx: { fontFamily: "monospace" } }, showKeyValue === key.key ? key.key : maskKey(key.key)), /* @__PURE__ */ React2.createElement(
205
+ IconButton,
206
+ {
207
+ size: "small",
208
+ onClick: () => setShowKeyValue(showKeyValue === key.key ? null : key.key)
209
+ },
210
+ showKeyValue === key.key ? /* @__PURE__ */ React2.createElement(VisibilityOff, null) : /* @__PURE__ */ React2.createElement(Visibility, null)
211
+ ), /* @__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, { 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(
212
+ Box2,
213
+ {
214
+ display: "flex",
215
+ alignItems: "center",
216
+ gap: 1,
217
+ mt: 2,
218
+ p: 2,
219
+ sx: { backgroundColor: "action.hover", borderRadius: 1 }
220
+ },
221
+ /* @__PURE__ */ React2.createElement(Typography2, { component: "code", sx: { fontFamily: "monospace", wordBreak: "break-all" } }, newKeyValue),
222
+ /* @__PURE__ */ React2.createElement(IconButton, { onClick: () => copyToClipboard(newKeyValue) }, /* @__PURE__ */ React2.createElement(ContentCopy, null))
223
+ )) : /* @__PURE__ */ React2.createElement(Box2, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, /* @__PURE__ */ React2.createElement(
224
+ TextField,
225
+ {
226
+ label: "Alias",
227
+ value: formData.alias || "",
228
+ onChange: (e) => setFormData({ ...formData, alias: e.target.value }),
229
+ fullWidth: true
230
+ }
231
+ ), /* @__PURE__ */ React2.createElement(
232
+ TextField,
233
+ {
234
+ select: true,
235
+ label: "Duration",
236
+ value: formData.duration || "30d",
237
+ onChange: (e) => setFormData({ ...formData, duration: e.target.value }),
238
+ fullWidth: true
239
+ },
240
+ /* @__PURE__ */ React2.createElement(MenuItem, { value: "1d" }, "1 Day"),
241
+ /* @__PURE__ */ React2.createElement(MenuItem, { value: "7d" }, "7 Days"),
242
+ /* @__PURE__ */ React2.createElement(MenuItem, { value: "30d" }, "30 Days"),
243
+ /* @__PURE__ */ React2.createElement(MenuItem, { value: "90d" }, "90 Days"),
244
+ /* @__PURE__ */ React2.createElement(MenuItem, { value: "1y" }, "1 Year")
245
+ ), /* @__PURE__ */ React2.createElement(
246
+ TextField,
247
+ {
248
+ label: "Max Budget (USD)",
249
+ type: "number",
250
+ value: formData.max_budget ?? "",
251
+ onChange: (e) => setFormData({ ...formData, max_budget: e.target.value ? Number(e.target.value) : void 0 }),
252
+ fullWidth: true
253
+ }
254
+ ), /* @__PURE__ */ React2.createElement(
255
+ TextField,
256
+ {
257
+ label: "TPM Limit",
258
+ type: "number",
259
+ value: formData.tpm_limit ?? "",
260
+ onChange: (e) => setFormData({ ...formData, tpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
261
+ fullWidth: true
262
+ }
263
+ ), /* @__PURE__ */ React2.createElement(
264
+ TextField,
265
+ {
266
+ select: true,
267
+ label: "Models",
268
+ SelectProps: { multiple: true },
269
+ value: formData.models || [],
270
+ onChange: (e) => setFormData({ ...formData, models: e.target.value }),
271
+ fullWidth: true
272
+ },
273
+ models.map((model) => /* @__PURE__ */ React2.createElement(MenuItem, { key: model.model_name, value: model.model_name }, model.model_name))
274
+ ))), /* @__PURE__ */ React2.createElement(DialogActions, null, /* @__PURE__ */ React2.createElement(Button, { onClick: handleCloseModal }, newKeyValue ? "Done" : "Cancel"), !newKeyValue && /* @__PURE__ */ React2.createElement(Button, { onClick: handleGenerate, variant: "contained", color: "primary", disabled: submitting }, submitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 24 }) : "Generate"))));
275
+ };
92
276
  }
93
- };
94
- var KeysTable = ({
95
- keys,
96
- models,
97
- loading,
98
- onGenerateKey,
99
- onDeleteKey
100
- }) => {
101
- const [generateModalOpen, setGenerateModalOpen] = useState(false);
102
- const [showKeyValue, setShowKeyValue] = useState(null);
103
- const [newKeyValue, setNewKeyValue] = useState(null);
104
- const [formData, setFormData] = useState({
105
- alias: "",
106
- models: [],
107
- duration: "30d",
108
- max_budget: void 0,
109
- tpm_limit: void 0
110
- });
111
- const [submitting, setSubmitting] = useState(false);
112
- const handleGenerate = async () => {
113
- setSubmitting(true);
114
- try {
115
- const response = await onGenerateKey(formData);
116
- setNewKeyValue(response?.key || "");
117
- setFormData({ alias: "", models: [], duration: "30d" });
118
- } catch (error) {
119
- console.error("Failed to generate key:", error);
120
- } finally {
121
- setSubmitting(false);
122
- }
123
- };
124
- const handleCloseModal = () => {
125
- setGenerateModalOpen(false);
126
- setNewKeyValue(null);
127
- setFormData({ alias: "", models: [], duration: "30d" });
128
- };
129
- const copyToClipboard = (text) => {
130
- navigator.clipboard.writeText(text);
131
- };
132
- return /* @__PURE__ */ jsxs2(Fragment, { children: [
133
- /* @__PURE__ */ jsxs2(Paper2, { style: { marginBottom: 16 }, children: [
134
- /* @__PURE__ */ jsxs2(Box2, { display: "flex", justifyContent: "space-between", alignItems: "center", p: 2, children: [
135
- /* @__PURE__ */ jsx2(Typography2, { variant: "h6", children: "Virtual Keys" }),
136
- /* @__PURE__ */ jsx2(
137
- Button,
138
- {
139
- variant: "contained",
140
- color: "primary",
141
- startIcon: /* @__PURE__ */ jsx2(Add, {}),
142
- onClick: () => setGenerateModalOpen(true),
143
- children: "Generate New Key"
144
- }
145
- )
146
- ] }),
147
- /* @__PURE__ */ jsx2(TableContainer, { children: /* @__PURE__ */ jsxs2(Table, { children: [
148
- /* @__PURE__ */ jsx2(TableHead, { children: /* @__PURE__ */ jsxs2(TableRow, { children: [
149
- /* @__PURE__ */ jsx2(TableCell, { children: "Alias" }),
150
- /* @__PURE__ */ jsx2(TableCell, { children: "Key" }),
151
- /* @__PURE__ */ jsx2(TableCell, { children: "Created" }),
152
- /* @__PURE__ */ jsx2(TableCell, { children: "Spend" }),
153
- /* @__PURE__ */ jsx2(TableCell, { children: "Budget" }),
154
- /* @__PURE__ */ jsx2(TableCell, { children: "TPM Limit" }),
155
- /* @__PURE__ */ jsx2(TableCell, { children: "Models" }),
156
- /* @__PURE__ */ jsx2(TableCell, { align: "right", children: "Actions" })
157
- ] }) }),
158
- /* @__PURE__ */ jsx2(TableBody, { children: loading ? /* @__PURE__ */ jsx2(TableRow, { children: /* @__PURE__ */ jsx2(TableCell, { colSpan: 8, align: "center", children: /* @__PURE__ */ jsx2(CircularProgress, { size: 24 }) }) }) : keys.length === 0 ? /* @__PURE__ */ jsx2(TableRow, { children: /* @__PURE__ */ jsx2(TableCell, { colSpan: 8, align: "center", children: /* @__PURE__ */ jsx2(Typography2, { color: "textSecondary", children: "No keys found" }) }) }) : keys.map((key) => /* @__PURE__ */ jsxs2(TableRow, { children: [
159
- /* @__PURE__ */ jsx2(TableCell, { children: key.key_alias || "-" }),
160
- /* @__PURE__ */ jsx2(TableCell, { children: /* @__PURE__ */ jsxs2(Box2, { display: "flex", alignItems: "center", gap: 0.5, children: [
161
- /* @__PURE__ */ jsx2(Typography2, { variant: "body2", component: "code", style: { fontFamily: "monospace" }, children: showKeyValue === key.key ? key.key : maskKey(key.key) }),
162
- /* @__PURE__ */ jsx2(
163
- IconButton,
164
- {
165
- size: "small",
166
- onClick: () => setShowKeyValue(showKeyValue === key.key ? null : key.key),
167
- children: showKeyValue === key.key ? /* @__PURE__ */ jsx2(VisibilityOff, {}) : /* @__PURE__ */ jsx2(Visibility, {})
168
- }
169
- ),
170
- /* @__PURE__ */ jsx2(IconButton, { size: "small", onClick: () => copyToClipboard(key.key), children: /* @__PURE__ */ jsx2(ContentCopy, { fontSize: "small" }) })
171
- ] }) }),
172
- /* @__PURE__ */ jsx2(TableCell, { children: formatDate(key.created_at) }),
173
- /* @__PURE__ */ jsxs2(TableCell, { children: [
174
- "$",
175
- key.spend?.toFixed(4) || "0.00"
176
- ] }),
177
- /* @__PURE__ */ jsx2(TableCell, { children: key.max_budget ? `$${key.max_budget}` : "-" }),
178
- /* @__PURE__ */ jsx2(TableCell, { children: key.tpm_limit || "-" }),
179
- /* @__PURE__ */ jsx2(TableCell, { children: /* @__PURE__ */ jsxs2(Box2, { display: "flex", gap: 0.5, flexWrap: "wrap", children: [
180
- key.models?.slice(0, 2).map((model) => /* @__PURE__ */ jsx2(Chip2, { label: model, size: "small" }, model)),
181
- (key.models?.length || 0) > 2 && /* @__PURE__ */ jsx2(Chip2, { label: `+${(key.models?.length || 0) - 2}`, size: "small", variant: "outlined" })
182
- ] }) }),
183
- /* @__PURE__ */ jsx2(TableCell, { align: "right", children: /* @__PURE__ */ jsx2(IconButton, { color: "secondary", onClick: () => onDeleteKey(key.key), children: /* @__PURE__ */ jsx2(Delete, {}) }) })
184
- ] }, key.key)) })
185
- ] }) })
186
- ] }),
187
- /* @__PURE__ */ jsxs2(Dialog, { open: generateModalOpen, onClose: handleCloseModal, maxWidth: "sm", fullWidth: true, children: [
188
- /* @__PURE__ */ jsx2(DialogTitle, { children: newKeyValue ? "Key Generated" : "Generate New Key" }),
189
- /* @__PURE__ */ jsx2(DialogContent, { children: newKeyValue ? /* @__PURE__ */ jsxs2(Box2, { children: [
190
- /* @__PURE__ */ jsx2(Typography2, { variant: "body2", color: "textSecondary", gutterBottom: true, children: "Copy this key now. You won't be able to see it again." }),
191
- /* @__PURE__ */ jsxs2(
192
- Box2,
193
- {
194
- display: "flex",
195
- alignItems: "center",
196
- gap: 1,
197
- mt: 2,
198
- p: 2,
199
- style: { backgroundColor: "#f5f5f5", borderRadius: 4 },
200
- children: [
201
- /* @__PURE__ */ jsx2(Typography2, { component: "code", style: { fontFamily: "monospace", wordBreak: "break-all" }, children: newKeyValue }),
202
- /* @__PURE__ */ jsx2(IconButton, { onClick: () => copyToClipboard(newKeyValue), children: /* @__PURE__ */ jsx2(ContentCopy, {}) })
203
- ]
204
- }
205
- )
206
- ] }) : /* @__PURE__ */ jsxs2(Box2, { display: "flex", flexDirection: "column", gap: 2, mt: 1, children: [
207
- /* @__PURE__ */ jsx2(
208
- TextField,
209
- {
210
- label: "Alias",
211
- value: formData.alias || "",
212
- onChange: (e) => setFormData({ ...formData, alias: e.target.value }),
213
- fullWidth: true
214
- }
215
- ),
216
- /* @__PURE__ */ jsxs2(
217
- TextField,
218
- {
219
- select: true,
220
- label: "Duration",
221
- value: formData.duration || "30d",
222
- onChange: (e) => setFormData({ ...formData, duration: e.target.value }),
223
- fullWidth: true,
224
- children: [
225
- /* @__PURE__ */ jsx2(MenuItem, { value: "1d", children: "1 Day" }),
226
- /* @__PURE__ */ jsx2(MenuItem, { value: "7d", children: "7 Days" }),
227
- /* @__PURE__ */ jsx2(MenuItem, { value: "30d", children: "30 Days" }),
228
- /* @__PURE__ */ jsx2(MenuItem, { value: "90d", children: "90 Days" }),
229
- /* @__PURE__ */ jsx2(MenuItem, { value: "1y", children: "1 Year" })
230
- ]
231
- }
232
- ),
233
- /* @__PURE__ */ jsx2(
234
- TextField,
235
- {
236
- label: "Max Budget (USD)",
237
- type: "number",
238
- value: formData.max_budget || "",
239
- onChange: (e) => setFormData({ ...formData, max_budget: e.target.value ? Number(e.target.value) : void 0 }),
240
- fullWidth: true
241
- }
242
- ),
243
- /* @__PURE__ */ jsx2(
244
- TextField,
245
- {
246
- label: "TPM Limit",
247
- type: "number",
248
- value: formData.tpm_limit || "",
249
- onChange: (e) => setFormData({ ...formData, tpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
250
- fullWidth: true
251
- }
252
- ),
253
- /* @__PURE__ */ jsx2(
254
- TextField,
255
- {
256
- select: true,
257
- label: "Models",
258
- SelectProps: { multiple: true },
259
- value: formData.models || [],
260
- onChange: (e) => setFormData({ ...formData, models: e.target.value }),
261
- fullWidth: true,
262
- children: models.map((model) => /* @__PURE__ */ jsx2(MenuItem, { value: model.model_name, children: model.model_name }, model.model_name))
263
- }
264
- )
265
- ] }) }),
266
- /* @__PURE__ */ jsxs2(DialogActions, { children: [
267
- /* @__PURE__ */ jsx2(Button, { onClick: handleCloseModal, children: newKeyValue ? "Done" : "Cancel" }),
268
- !newKeyValue && /* @__PURE__ */ jsx2(Button, { onClick: handleGenerate, variant: "contained", color: "primary", disabled: submitting, children: submitting ? /* @__PURE__ */ jsx2(CircularProgress, { size: 24 }) : "Generate" })
269
- ] })
270
- ] })
271
- ] });
272
- };
277
+ });
273
278
 
274
279
  // src/components/UsageStats.tsx
275
- import { useState as useState2 } from "react";
280
+ import React3, { useState as useState2 } from "react";
276
281
  import {
277
282
  Paper as Paper3,
278
283
  Box as Box3,
@@ -283,7 +288,7 @@ import {
283
288
  MenuItem as MenuItem2,
284
289
  Grid,
285
290
  CircularProgress as CircularProgress2
286
- } from "@material-ui/core";
291
+ } from "@mui/material";
287
292
  import {
288
293
  AreaChart,
289
294
  Area,
@@ -296,331 +301,226 @@ import {
296
301
  ResponsiveContainer,
297
302
  Legend
298
303
  } from "recharts";
299
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
300
- var DateRangeSelector = ({ preset, onChange, dateRange }) => {
301
- return /* @__PURE__ */ jsx3(Box3, { display: "flex", gap: 1, children: ["today", "7d", "30d"].map((p) => /* @__PURE__ */ jsxs3(
302
- Select,
303
- {
304
- value: preset,
305
- onChange: (e) => onChange(e.target.value),
306
- size: "small",
307
- children: [
308
- /* @__PURE__ */ jsx3(MenuItem2, { value: "today", children: "Today" }),
309
- /* @__PURE__ */ jsx3(MenuItem2, { value: "7d", children: "Last 7 days" }),
310
- /* @__PURE__ */ jsx3(MenuItem2, { value: "30d", children: "Last 30 days" })
311
- ]
312
- },
313
- p
314
- )) });
315
- };
316
- var UsageStats = ({
317
- usage,
318
- models,
319
- dateRange,
320
- onDateRangeChange,
321
- loading
322
- }) => {
323
- const [selectedModel, setSelectedModel] = useState2("all");
324
- if (loading) {
325
- return /* @__PURE__ */ jsx3(Paper3, { style: { padding: 16 }, children: /* @__PURE__ */ jsx3(Box3, { display: "flex", justifyContent: "center", p: 4, children: /* @__PURE__ */ jsx3(CircularProgress2, {}) }) });
326
- }
327
- const formatDate2 = (date) => {
328
- return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
329
- };
330
- const dailyData = usage?.daily_usage?.map((d) => ({
331
- date: d.date,
332
- spend: d.spend,
333
- promptTokens: d.prompt_tokens,
334
- completionTokens: d.completion_tokens,
335
- totalTokens: d.total_tokens
336
- })) || [];
337
- const usageByModelData = Object.entries(usage?.usage_by_model || {}).map(([model, data]) => ({
338
- model,
339
- spend: data.total_spend,
340
- promptTokens: data.prompt_tokens,
341
- completionTokens: data.completion_tokens
342
- }));
343
- const filteredModelData = selectedModel === "all" ? usageByModelData : usageByModelData.filter((d) => d.model === selectedModel);
344
- return /* @__PURE__ */ jsxs3(Paper3, { style: { padding: 16 }, children: [
345
- /* @__PURE__ */ jsxs3(Box3, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 3, children: [
346
- /* @__PURE__ */ jsx3(Typography3, { variant: "h6", children: "Usage Analytics" }),
347
- /* @__PURE__ */ jsxs3(Box3, { display: "flex", gap: 2, alignItems: "center", children: [
348
- /* @__PURE__ */ jsx3(
349
- DateRangeSelector,
350
- {
351
- preset: "7d",
352
- onChange: (preset) => {
353
- const end = /* @__PURE__ */ new Date();
354
- const start = /* @__PURE__ */ new Date();
355
- if (preset === "today") start.setHours(0, 0, 0, 0);
356
- else if (preset === "7d") start.setDate(start.getDate() - 7);
357
- else if (preset === "30d") start.setDate(start.getDate() - 30);
358
- onDateRangeChange({ start, end });
359
- },
360
- dateRange
361
- }
362
- ),
363
- /* @__PURE__ */ jsxs3(FormControl, { size: "small", style: { minWidth: 150 }, children: [
364
- /* @__PURE__ */ jsx3(InputLabel, { children: "Model" }),
365
- /* @__PURE__ */ jsxs3(Select, { value: selectedModel, onChange: (e) => setSelectedModel(e.target.value), label: "Model", children: [
366
- /* @__PURE__ */ jsx3(MenuItem2, { value: "all", children: "All Models" }),
367
- models.map((m) => /* @__PURE__ */ jsx3(MenuItem2, { value: m.model_name, children: m.model_name }, m.model_name))
368
- ] })
369
- ] })
370
- ] })
371
- ] }),
372
- /* @__PURE__ */ jsxs3(Grid, { container: true, spacing: 3, children: [
373
- /* @__PURE__ */ jsxs3(Grid, { item: true, xs: 12, md: 6, children: [
374
- /* @__PURE__ */ jsx3(Typography3, { variant: "subtitle2", color: "textSecondary", gutterBottom: true, children: "Daily Spend" }),
375
- /* @__PURE__ */ jsx3(Box3, { height: 250, children: /* @__PURE__ */ jsx3(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs3(AreaChart, { data: dailyData, children: [
376
- /* @__PURE__ */ jsx3(CartesianGrid, { strokeDasharray: "3 3" }),
377
- /* @__PURE__ */ jsx3(XAxis, { dataKey: "date", tick: { fontSize: 12 } }),
378
- /* @__PURE__ */ jsx3(YAxis, { tick: { fontSize: 12 }, tickFormatter: (v) => `$${v.toFixed(2)}` }),
379
- /* @__PURE__ */ jsx3(Tooltip, { formatter: (value) => [`$${value.toFixed(4)}`, "Spend"] }),
380
- /* @__PURE__ */ jsx3(Area, { type: "monotone", dataKey: "spend", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.3 })
381
- ] }) }) })
382
- ] }),
383
- /* @__PURE__ */ jsxs3(Grid, { item: true, xs: 12, md: 6, children: [
384
- /* @__PURE__ */ jsx3(Typography3, { variant: "subtitle2", color: "textSecondary", gutterBottom: true, children: "Token Usage by Model" }),
385
- /* @__PURE__ */ jsx3(Box3, { height: 250, children: /* @__PURE__ */ jsx3(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs3(BarChart, { data: filteredModelData, children: [
386
- /* @__PURE__ */ jsx3(CartesianGrid, { strokeDasharray: "3 3" }),
387
- /* @__PURE__ */ jsx3(XAxis, { dataKey: "model", tick: { fontSize: 10 } }),
388
- /* @__PURE__ */ jsx3(YAxis, { tick: { fontSize: 12 } }),
389
- /* @__PURE__ */ jsx3(Tooltip, {}),
390
- /* @__PURE__ */ jsx3(Legend, {}),
391
- /* @__PURE__ */ jsx3(Bar, { dataKey: "promptTokens", name: "Prompt Tokens", fill: "#8884d8", stackId: "a" }),
392
- /* @__PURE__ */ jsx3(Bar, { dataKey: "completionTokens", name: "Completion Tokens", fill: "#82ca9d", stackId: "a" })
393
- ] }) }) })
394
- ] }),
395
- /* @__PURE__ */ jsx3(Grid, { item: true, xs: 12, children: /* @__PURE__ */ jsxs3(Box3, { display: "flex", gap: 4, flexWrap: "wrap", children: [
396
- /* @__PURE__ */ jsxs3(Box3, { children: [
397
- /* @__PURE__ */ jsx3(Typography3, { variant: "body2", color: "textSecondary", children: "Total Spend" }),
398
- /* @__PURE__ */ jsxs3(Typography3, { variant: "h5", children: [
399
- "$",
400
- usage?.total_spend?.toFixed(4) || "0.00"
401
- ] })
402
- ] }),
403
- /* @__PURE__ */ jsxs3(Box3, { children: [
404
- /* @__PURE__ */ jsx3(Typography3, { variant: "body2", color: "textSecondary", children: "Total Tokens" }),
405
- /* @__PURE__ */ jsx3(Typography3, { variant: "h5", children: usage?.total_tokens?.toLocaleString() || "0" })
406
- ] }),
407
- /* @__PURE__ */ jsxs3(Box3, { children: [
408
- /* @__PURE__ */ jsx3(Typography3, { variant: "body2", color: "textSecondary", children: "Prompt Tokens" }),
409
- /* @__PURE__ */ jsx3(Typography3, { variant: "h5", children: usage?.prompt_tokens?.toLocaleString() || "0" })
410
- ] }),
411
- /* @__PURE__ */ jsxs3(Box3, { children: [
412
- /* @__PURE__ */ jsx3(Typography3, { variant: "body2", color: "textSecondary", children: "Completion Tokens" }),
413
- /* @__PURE__ */ jsx3(Typography3, { variant: "h5", children: usage?.completion_tokens?.toLocaleString() || "0" })
414
- ] })
415
- ] }) })
416
- ] })
417
- ] });
418
- };
419
-
420
- // src/api.ts
421
- var LiteLlmApi = class {
422
- fetchApi;
423
- basePath;
424
- constructor(fetchApi, basePath = "/api/litellm") {
425
- this.fetchApi = fetchApi;
426
- this.basePath = basePath;
427
- }
428
- async get(path, params) {
429
- const url = new URL(`${this.basePath}${path}`, window.location.origin);
430
- if (params) {
431
- Object.entries(params).forEach(([key, value]) => {
432
- url.searchParams.append(key, value);
433
- });
434
- }
435
- const response = await this.fetchApi.fetch(url.toString());
436
- if (!response.ok) {
437
- throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);
438
- }
439
- return response.json();
440
- }
441
- async post(path, body) {
442
- const response = await this.fetchApi.fetch(`${this.basePath}${path}`, {
443
- method: "POST",
444
- headers: { "Content-Type": "application/json" },
445
- body: JSON.stringify(body)
446
- });
447
- if (!response.ok) {
448
- throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);
449
- }
450
- return response.json();
451
- }
452
- async del(path) {
453
- const response = await fetch(`${this.basePath}${path}`, {
454
- method: "DELETE",
455
- headers: { "Content-Type": "application/json" }
456
- });
457
- if (!response.ok) {
458
- throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);
459
- }
460
- return response.json();
461
- }
462
- async getUserInfo(userId) {
463
- const params = userId ? { user_id: userId } : void 0;
464
- return this.get("/user/info", params);
465
- }
466
- async listKeys(userId) {
467
- const params = userId ? { user_id: userId } : void 0;
468
- return this.get("/keys", params);
469
- }
470
- async generateKey(request) {
471
- return this.post("/keys/generate", request);
472
- }
473
- async deleteKey(keyId) {
474
- return this.del(`/keys/${encodeURIComponent(keyId)}`);
475
- }
476
- async listModels() {
477
- return this.get("/models");
478
- }
479
- async getUsage(startDate, endDate, userId) {
480
- const params = { start_date: startDate, end_date: endDate };
481
- if (userId) params.user_id = userId;
482
- return this.get("/usage", params);
304
+ var UsageStats;
305
+ var init_UsageStats = __esm({
306
+ "src/components/UsageStats.tsx"() {
307
+ "use strict";
308
+ UsageStats = ({
309
+ usage,
310
+ models,
311
+ dateRange,
312
+ onDateRangeChange,
313
+ loading
314
+ }) => {
315
+ const [selectedPreset, setSelectedPreset] = useState2("7d");
316
+ const [selectedModel, setSelectedModel] = useState2("all");
317
+ const handlePresetChange = (preset) => {
318
+ setSelectedPreset(preset);
319
+ const end = /* @__PURE__ */ new Date();
320
+ const start = /* @__PURE__ */ new Date();
321
+ if (preset === "today") start.setHours(0, 0, 0, 0);
322
+ else if (preset === "7d") start.setDate(start.getDate() - 7);
323
+ else if (preset === "30d") start.setDate(start.getDate() - 30);
324
+ onDateRangeChange({ start, end });
325
+ };
326
+ if (loading) {
327
+ return /* @__PURE__ */ React3.createElement(Paper3, { sx: { p: 2 } }, /* @__PURE__ */ React3.createElement(Box3, { display: "flex", justifyContent: "center", p: 4 }, /* @__PURE__ */ React3.createElement(CircularProgress2, null)));
328
+ }
329
+ const dailyData = usage?.daily_usage?.map((d) => ({
330
+ date: d.date,
331
+ spend: d.spend,
332
+ promptTokens: d.prompt_tokens,
333
+ completionTokens: d.completion_tokens,
334
+ totalTokens: d.total_tokens
335
+ })) || [];
336
+ const usageByModelData = Object.entries(usage?.usage_by_model || {}).map(([model, data]) => ({
337
+ model,
338
+ spend: data.total_spend,
339
+ promptTokens: data.prompt_tokens,
340
+ completionTokens: data.completion_tokens
341
+ }));
342
+ const filteredModelData = selectedModel === "all" ? usageByModelData : usageByModelData.filter((d) => d.model === selectedModel);
343
+ return /* @__PURE__ */ React3.createElement(Paper3, { sx: { p: 2 } }, /* @__PURE__ */ React3.createElement(Box3, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 3 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "h6" }, "Usage Analytics"), /* @__PURE__ */ React3.createElement(Box3, { display: "flex", gap: 2, alignItems: "center" }, /* @__PURE__ */ React3.createElement(FormControl, { size: "small", sx: { minWidth: 140 } }, /* @__PURE__ */ React3.createElement(InputLabel, null, "Period"), /* @__PURE__ */ React3.createElement(
344
+ Select,
345
+ {
346
+ value: selectedPreset,
347
+ label: "Period",
348
+ onChange: (e) => handlePresetChange(e.target.value)
349
+ },
350
+ /* @__PURE__ */ React3.createElement(MenuItem2, { value: "today" }, "Today"),
351
+ /* @__PURE__ */ React3.createElement(MenuItem2, { value: "7d" }, "Last 7 days"),
352
+ /* @__PURE__ */ React3.createElement(MenuItem2, { value: "30d" }, "Last 30 days")
353
+ )), /* @__PURE__ */ React3.createElement(FormControl, { size: "small", sx: { minWidth: 150 } }, /* @__PURE__ */ React3.createElement(InputLabel, null, "Model"), /* @__PURE__ */ React3.createElement(
354
+ Select,
355
+ {
356
+ value: selectedModel,
357
+ label: "Model",
358
+ onChange: (e) => setSelectedModel(e.target.value)
359
+ },
360
+ /* @__PURE__ */ React3.createElement(MenuItem2, { value: "all" }, "All Models"),
361
+ models.map((m) => /* @__PURE__ */ React3.createElement(MenuItem2, { key: m.model_name, value: m.model_name }, m.model_name))
362
+ )))), /* @__PURE__ */ React3.createElement(Grid, { container: true, spacing: 3 }, /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend"), /* @__PURE__ */ React3.createElement(Box3, { height: 250 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(AreaChart, { data: dailyData }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "date", tick: { fontSize: 12 } }), /* @__PURE__ */ React3.createElement(YAxis, { tick: { fontSize: 12 }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (value) => [`$${value.toFixed(4)}`, "Spend"] }), /* @__PURE__ */ React3.createElement(Area, { type: "monotone", dataKey: "spend", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.3 }))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Token Usage by Model"), /* @__PURE__ */ React3.createElement(Box3, { height: 250 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(BarChart, { data: filteredModelData }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "model", tick: { fontSize: 10 } }), /* @__PURE__ */ React3.createElement(YAxis, { tick: { fontSize: 12 } }), /* @__PURE__ */ React3.createElement(Tooltip, null), /* @__PURE__ */ React3.createElement(Legend, null), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "promptTokens", name: "Prompt Tokens", fill: "#8884d8", stackId: "a" }), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "completionTokens", name: "Completion Tokens", fill: "#82ca9d", stackId: "a" }))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(Box3, { display: "flex", gap: 4, flexWrap: "wrap" }, /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Typography3, { variant: "body2", color: "text.secondary" }, "Total Spend"), /* @__PURE__ */ React3.createElement(Typography3, { variant: "h5" }, "$", usage?.total_spend?.toFixed(4) || "0.00")), /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Typography3, { variant: "body2", color: "text.secondary" }, "Total Tokens"), /* @__PURE__ */ React3.createElement(Typography3, { variant: "h5" }, usage?.total_tokens?.toLocaleString() || "0")), /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Typography3, { variant: "body2", color: "text.secondary" }, "Prompt Tokens"), /* @__PURE__ */ React3.createElement(Typography3, { variant: "h5" }, usage?.prompt_tokens?.toLocaleString() || "0")), /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Typography3, { variant: "body2", color: "text.secondary" }, "Completion Tokens"), /* @__PURE__ */ React3.createElement(Typography3, { variant: "h5" }, usage?.completion_tokens?.toLocaleString() || "0"))))));
363
+ };
483
364
  }
484
- };
365
+ });
485
366
 
486
367
  // src/components/LiteLLMPage.tsx
487
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
488
- var LiteLLMPage = ({ fetchApi }) => {
489
- const api = new LiteLlmApi(fetchApi);
490
- const [dateRange, setDateRange] = useState3(() => {
491
- const end = /* @__PURE__ */ new Date();
492
- const start = /* @__PURE__ */ new Date();
493
- start.setDate(start.getDate() - 7);
494
- return { start, end };
495
- });
496
- const [snackbar, setSnackbar] = useState3(null);
497
- const {
498
- value: userInfo,
499
- loading: userLoading,
500
- error: userError
501
- } = useAsync(async () => {
502
- try {
503
- return await api.getUserInfo();
504
- } catch (e) {
505
- setSnackbar({ message: `Failed to load user info: ${e.message}`, severity: "error" });
506
- return null;
507
- }
508
- }, []);
509
- const {
510
- value: keys,
511
- loading: keysLoading,
512
- error: keysError,
513
- refresh: refreshKeys
514
- } = useAsync(async () => {
515
- try {
516
- return await api.listKeys();
517
- } catch (e) {
518
- setSnackbar({ message: `Failed to load keys: ${e.message}`, severity: "error" });
519
- return [];
520
- }
521
- }, []);
522
- const {
523
- value: models,
524
- loading: modelsLoading
525
- } = useAsync(async () => {
526
- try {
527
- return await api.listModels();
528
- } catch {
529
- return [];
530
- }
531
- }, []);
532
- const {
533
- value: usage,
534
- loading: usageLoading
535
- } = useAsync(async () => {
536
- const startDate = dateRange.start.toISOString().split("T")[0];
537
- const endDate = dateRange.end.toISOString().split("T")[0];
538
- return api.getUsage(startDate, endDate);
539
- }, [dateRange]);
540
- useEffect(() => {
541
- if (userError) {
542
- setSnackbar({ message: `Error: ${userError.message}`, severity: "error" });
543
- }
544
- if (keysError) {
545
- setSnackbar({ message: `Error: ${keysError.message}`, severity: "error" });
546
- }
547
- }, [userError, keysError]);
548
- const handleGenerateKey = useCallback(async (request) => {
549
- const response = await api.generateKey(request);
550
- setSnackbar({ message: "Key generated successfully", severity: "success" });
551
- refreshKeys();
552
- return response;
553
- }, [api, refreshKeys]);
554
- const handleDeleteKey = useCallback(async (keyId) => {
555
- try {
556
- await api.deleteKey(keyId);
557
- setSnackbar({ message: "Key revoked successfully", severity: "success" });
558
- refreshKeys();
559
- } catch (e) {
560
- setSnackbar({ message: `Failed to revoke key: ${e.message}`, severity: "error" });
561
- }
562
- }, [api, refreshKeys]);
563
- const isLoading = userLoading || keysLoading || modelsLoading;
564
- if (isLoading && !userInfo && !keys) {
565
- return /* @__PURE__ */ jsx4(Box4, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "50vh", children: /* @__PURE__ */ jsx4(CircularProgress3, {}) });
566
- }
567
- return /* @__PURE__ */ jsxs4(Box4, { p: 3, children: [
568
- /* @__PURE__ */ jsxs4(Grid2, { container: true, spacing: 2, children: [
569
- /* @__PURE__ */ jsx4(Grid2, { item: true, xs: 12, children: /* @__PURE__ */ jsx4(DashboardHeader, { userInfo, loading: userLoading }) }),
570
- /* @__PURE__ */ jsx4(Grid2, { item: true, xs: 12, children: /* @__PURE__ */ jsx4(
368
+ var LiteLLMPage_exports = {};
369
+ __export(LiteLLMPage_exports, {
370
+ LiteLLMPage: () => LiteLLMPage
371
+ });
372
+ import React4, { useState as useState3, useCallback } from "react";
373
+ import { Grid as Grid2, Box as Box4, Snackbar, Alert, CircularProgress as CircularProgress3 } from "@mui/material";
374
+ import { useAsync, useAsyncRetry } from "react-use";
375
+ import { useApi } from "@backstage/core-plugin-api";
376
+ var LiteLLMPage;
377
+ var init_LiteLLMPage = __esm({
378
+ "src/components/LiteLLMPage.tsx"() {
379
+ "use strict";
380
+ init_DashboardHeader();
381
+ init_KeysTable();
382
+ init_UsageStats();
383
+ init_api();
384
+ LiteLLMPage = () => {
385
+ const api = useApi(liteLlmApiRef);
386
+ const [dateRange, setDateRange] = useState3(() => {
387
+ const end = /* @__PURE__ */ new Date();
388
+ const start = /* @__PURE__ */ new Date();
389
+ start.setDate(start.getDate() - 7);
390
+ return { start, end };
391
+ });
392
+ const [snackbar, setSnackbar] = useState3(null);
393
+ const { value: userInfo, loading: userLoading } = useAsync(async () => {
394
+ try {
395
+ return await api.getUserInfo();
396
+ } catch (e) {
397
+ setSnackbar({ message: `Failed to load user info: ${e.message}`, severity: "error" });
398
+ return null;
399
+ }
400
+ }, [api]);
401
+ const {
402
+ value: keys,
403
+ loading: keysLoading,
404
+ retry: refreshKeys
405
+ } = useAsyncRetry(async () => {
406
+ try {
407
+ return await api.listKeys();
408
+ } catch (e) {
409
+ setSnackbar({ message: `Failed to load keys: ${e.message}`, severity: "error" });
410
+ return [];
411
+ }
412
+ }, [api]);
413
+ const { value: models, loading: modelsLoading } = useAsync(async () => {
414
+ try {
415
+ return await api.listModels();
416
+ } catch {
417
+ return [];
418
+ }
419
+ }, [api]);
420
+ const { value: usage, loading: usageLoading } = useAsync(async () => {
421
+ const startDate = dateRange.start.toISOString().split("T")[0];
422
+ const endDate = dateRange.end.toISOString().split("T")[0];
423
+ return api.getUsage(startDate, endDate);
424
+ }, [api, dateRange]);
425
+ const handleGenerateKey = useCallback(
426
+ async (request) => {
427
+ const response = await api.generateKey(request);
428
+ setSnackbar({ message: "Key generated successfully", severity: "success" });
429
+ refreshKeys();
430
+ return response;
431
+ },
432
+ [api, refreshKeys]
433
+ );
434
+ const handleDeleteKey = useCallback(
435
+ async (keyId) => {
436
+ try {
437
+ await api.deleteKey(keyId);
438
+ setSnackbar({ message: "Key revoked successfully", severity: "success" });
439
+ refreshKeys();
440
+ } catch (e) {
441
+ setSnackbar({ message: `Failed to revoke key: ${e.message}`, severity: "error" });
442
+ }
443
+ },
444
+ [api, refreshKeys]
445
+ );
446
+ if ((userLoading || keysLoading || modelsLoading) && !userInfo && !keys) {
447
+ return /* @__PURE__ */ React4.createElement(Box4, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "50vh" }, /* @__PURE__ */ React4.createElement(CircularProgress3, null));
448
+ }
449
+ return /* @__PURE__ */ React4.createElement(Box4, { p: 3 }, /* @__PURE__ */ React4.createElement(Grid2, { container: true, spacing: 2 }, /* @__PURE__ */ React4.createElement(Grid2, { item: true, xs: 12 }, /* @__PURE__ */ React4.createElement(DashboardHeader, { userInfo: userInfo ?? null, loading: userLoading })), /* @__PURE__ */ React4.createElement(Grid2, { item: true, xs: 12 }, /* @__PURE__ */ React4.createElement(
571
450
  KeysTable,
572
451
  {
573
- keys: keys || [],
574
- models: models || [],
452
+ keys: keys ?? [],
453
+ models: models ?? [],
575
454
  loading: keysLoading,
576
455
  onGenerateKey: handleGenerateKey,
577
456
  onDeleteKey: handleDeleteKey
578
457
  }
579
- ) }),
580
- /* @__PURE__ */ jsx4(Grid2, { item: true, xs: 12, children: /* @__PURE__ */ jsx4(
458
+ )), /* @__PURE__ */ React4.createElement(Grid2, { item: true, xs: 12 }, /* @__PURE__ */ React4.createElement(
581
459
  UsageStats,
582
460
  {
583
- usage: usage || null,
584
- models: models || [],
461
+ usage: usage ?? null,
462
+ models: models ?? [],
585
463
  dateRange,
586
464
  onDateRangeChange: setDateRange,
587
465
  loading: usageLoading
588
466
  }
589
- ) })
590
- ] }),
591
- /* @__PURE__ */ jsx4(
592
- Snackbar,
593
- {
594
- open: !!snackbar,
595
- autoHideDuration: 5e3,
596
- onClose: () => setSnackbar(null),
597
- anchorOrigin: { vertical: "bottom", horizontal: "right" },
598
- children: snackbar && /* @__PURE__ */ jsx4(Alert, { severity: snackbar.severity, onClose: () => setSnackbar(null), children: snackbar.message })
599
- }
600
- )
601
- ] });
602
- };
467
+ ))), /* @__PURE__ */ React4.createElement(
468
+ Snackbar,
469
+ {
470
+ open: !!snackbar,
471
+ autoHideDuration: 5e3,
472
+ onClose: () => setSnackbar(null),
473
+ anchorOrigin: { vertical: "bottom", horizontal: "right" }
474
+ },
475
+ snackbar ? /* @__PURE__ */ React4.createElement(Alert, { severity: snackbar.severity, onClose: () => setSnackbar(null) }, snackbar.message) : void 0
476
+ ));
477
+ };
478
+ }
479
+ });
603
480
 
604
481
  // src/plugin.ts
482
+ init_api();
483
+ import React5 from "react";
484
+ import {
485
+ createFrontendPlugin,
486
+ createApiExtension,
487
+ createPageExtension,
488
+ createApiFactory,
489
+ fetchApiRef
490
+ } from "@backstage/frontend-plugin-api";
605
491
  var litellmPlugin = createFrontendPlugin({
606
492
  id: "litellm",
607
493
  extensions: [
608
- PageExtension.create({
609
- id: "litellm.page",
494
+ createApiExtension({
495
+ factory: createApiFactory({
496
+ api: liteLlmApiRef,
497
+ deps: { fetchApi: fetchApiRef },
498
+ factory: ({ fetchApi }) => new LiteLlmApi(fetchApi)
499
+ })
500
+ }),
501
+ createPageExtension({
610
502
  defaultPath: "/litellm",
611
- title: "LiteLLM",
612
- component: {
613
- loader: async () => LiteLLMPage
503
+ loader: async () => {
504
+ const { LiteLLMPage: LiteLLMPage2 } = await Promise.resolve().then(() => (init_LiteLLMPage(), LiteLLMPage_exports));
505
+ return React5.createElement(LiteLLMPage2);
614
506
  }
615
507
  })
616
508
  ]
617
509
  });
510
+
511
+ // src/index.ts
512
+ init_LiteLLMPage();
513
+ init_DashboardHeader();
514
+ init_KeysTable();
515
+ init_UsageStats();
516
+ init_api();
618
517
  export {
619
518
  DashboardHeader,
620
519
  KeysTable,
621
520
  LiteLLMPage,
622
521
  LiteLlmApi,
623
522
  UsageStats,
523
+ liteLlmApiRef,
624
524
  litellmPlugin
625
525
  };
626
526
  //# sourceMappingURL=index.esm.js.map