@acarmisc/backstage-plugin-litellm 0.2.0 → 0.2.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,915 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from))
18
+ if (!__hasOwnProp.call(to, key) && key !== except)
19
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
24
+ // If the importer is in node compatibility mode or this is not an ESM
25
+ // file that has been converted to a CommonJS file using a Babel-
26
+ // compatible transform (i.e. "__esModule" has not been set), then set
27
+ // "default" to the CommonJS "module.exports" for node compatibility.
28
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
29
+ mod
30
+ ));
31
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
+
33
+ // src/api.ts
34
+ var import_core_plugin_api, ApiError, liteLlmApiRef, LiteLlmApi;
35
+ var init_api = __esm({
36
+ "src/api.ts"() {
37
+ "use strict";
38
+ import_core_plugin_api = require("@backstage/core-plugin-api");
39
+ ApiError = class extends Error {
40
+ constructor(message, status, body) {
41
+ super(message);
42
+ this.status = status;
43
+ this.body = body;
44
+ }
45
+ };
46
+ liteLlmApiRef = (0, import_core_plugin_api.createApiRef)({
47
+ id: "plugin.litellm.api"
48
+ });
49
+ LiteLlmApi = class {
50
+ constructor(fetchApi, basePath = "/api/litellm") {
51
+ this.fetchApi = fetchApi;
52
+ this.basePath = basePath;
53
+ }
54
+ async throwIfNotOk(response) {
55
+ if (!response.ok) {
56
+ let body;
57
+ try {
58
+ body = await response.json();
59
+ } catch {
60
+ body = await response.text().catch(() => "");
61
+ }
62
+ throw new ApiError(`${response.status} ${response.statusText}`, response.status, body);
63
+ }
64
+ }
65
+ async get(path, params) {
66
+ const url = new URL(`${this.basePath}${path}`, window.location.origin);
67
+ if (params) {
68
+ Object.entries(params).forEach(([key, value]) => url.searchParams.append(key, value));
69
+ }
70
+ const response = await this.fetchApi.fetch(url.toString());
71
+ await this.throwIfNotOk(response);
72
+ return response.json();
73
+ }
74
+ async post(path, body) {
75
+ const response = await this.fetchApi.fetch(`${this.basePath}${path}`, {
76
+ method: "POST",
77
+ headers: { "Content-Type": "application/json" },
78
+ body: JSON.stringify(body)
79
+ });
80
+ await this.throwIfNotOk(response);
81
+ return response.json();
82
+ }
83
+ async del(path) {
84
+ const response = await this.fetchApi.fetch(`${this.basePath}${path}`, {
85
+ method: "DELETE",
86
+ headers: { "Content-Type": "application/json" }
87
+ });
88
+ await this.throwIfNotOk(response);
89
+ return response.json();
90
+ }
91
+ // User identity is resolved server-side from the Backstage Bearer token.
92
+ // No user_id param needed on the frontend.
93
+ async getUserInfo() {
94
+ return this.get("/user/info");
95
+ }
96
+ async listKeys() {
97
+ return this.get("/keys");
98
+ }
99
+ async generateKey(request) {
100
+ return this.post("/keys/generate", request);
101
+ }
102
+ async updateKey(keyId, request) {
103
+ return this.post(`/keys/${encodeURIComponent(keyId)}/update`, request);
104
+ }
105
+ async deleteKey(keyId) {
106
+ return this.del(`/keys/${encodeURIComponent(keyId)}`);
107
+ }
108
+ async listModels() {
109
+ return this.get("/models");
110
+ }
111
+ async getTeams() {
112
+ return this.get("/teams");
113
+ }
114
+ async getUsage(startDate, endDate) {
115
+ return this.get("/usage", { start_date: startDate, end_date: endDate });
116
+ }
117
+ async getTeamUsage(teamId, startDate, endDate) {
118
+ return this.get(`/teams/${encodeURIComponent(teamId)}/usage`, {
119
+ start_date: startDate,
120
+ end_date: endDate
121
+ });
122
+ }
123
+ };
124
+ }
125
+ });
126
+
127
+ // src/components/DashboardHeader.tsx
128
+ var import_react, import_material, import_icons_material, DashboardHeader;
129
+ var init_DashboardHeader = __esm({
130
+ "src/components/DashboardHeader.tsx"() {
131
+ "use strict";
132
+ import_react = __toESM(require("react"));
133
+ import_material = require("@mui/material");
134
+ import_icons_material = require("@mui/icons-material");
135
+ DashboardHeader = ({ userInfo, teams, loading }) => {
136
+ if (loading) {
137
+ return /* @__PURE__ */ import_react.default.createElement(import_material.Paper, { sx: { p: 2, mb: 2 } }, /* @__PURE__ */ import_react.default.createElement(import_material.LinearProgress, null));
138
+ }
139
+ const displayName = userInfo.user_email ?? userInfo.email ?? userInfo.user_id;
140
+ const budget = userInfo.max_budget ?? 0;
141
+ const spend = userInfo.spend ?? userInfo.current_spend ?? 0;
142
+ const budgetPct = budget > 0 ? Math.min(spend / budget * 100, 100) : 0;
143
+ const isOver = budget > 0 && spend >= budget;
144
+ const isNear = budget > 0 && spend >= budget * 0.8 && !isOver;
145
+ return /* @__PURE__ */ import_react.default.createElement(import_material.Paper, { sx: { p: 2, mb: 2 } }, /* @__PURE__ */ import_react.default.createElement(import_material.Box, { display: "flex", alignItems: "flex-start", gap: 2, flexWrap: "wrap" }, /* @__PURE__ */ import_react.default.createElement(import_material.Box, { flexGrow: 1 }, /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "h6" }, displayName), /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "caption", color: "text.secondary" }, userInfo.user_id), teams.length > 0 && /* @__PURE__ */ import_react.default.createElement(import_material.Box, { display: "flex", gap: 0.75, flexWrap: "wrap", mt: 1 }, teams.map((team) => /* @__PURE__ */ import_react.default.createElement(
146
+ import_material.Chip,
147
+ {
148
+ key: team.team_id,
149
+ label: team.team_alias || team.team_id,
150
+ size: "small",
151
+ variant: "outlined",
152
+ color: "primary"
153
+ }
154
+ )))), budget > 0 && /* @__PURE__ */ import_react.default.createElement(import_material.Box, { minWidth: 220 }, /* @__PURE__ */ import_react.default.createElement(import_material.Box, { display: "flex", justifyContent: "space-between", mb: 0.5 }, /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "body2" }, "$", spend.toFixed(2), " / $", budget.toFixed(2)), isOver && /* @__PURE__ */ import_react.default.createElement(import_material.Chip, { icon: /* @__PURE__ */ import_react.default.createElement(import_icons_material.Warning, null), label: "Over Budget", size: "small", color: "error" }), isNear && /* @__PURE__ */ import_react.default.createElement(import_material.Chip, { label: "Near Limit", size: "small", color: "warning" })), /* @__PURE__ */ import_react.default.createElement(
155
+ import_material.LinearProgress,
156
+ {
157
+ variant: "determinate",
158
+ value: budgetPct,
159
+ color: isOver ? "error" : isNear ? "warning" : "primary",
160
+ sx: { height: 8, borderRadius: 1 }
161
+ }
162
+ ))));
163
+ };
164
+ }
165
+ });
166
+
167
+ // src/components/KeysTable.tsx
168
+ var import_react2, import_material2, import_icons_material2, maskKey, formatDate, emptyForm, keyToEditForm, KeysTable;
169
+ var init_KeysTable = __esm({
170
+ "src/components/KeysTable.tsx"() {
171
+ "use strict";
172
+ import_react2 = __toESM(require("react"));
173
+ import_material2 = require("@mui/material");
174
+ import_icons_material2 = require("@mui/icons-material");
175
+ maskKey = (key) => {
176
+ if (key.length <= 8) return "***";
177
+ return `${key.slice(0, 4)}...${key.slice(-4)}`;
178
+ };
179
+ formatDate = (dateStr) => {
180
+ try {
181
+ return new Date(dateStr).toLocaleDateString();
182
+ } catch {
183
+ return dateStr;
184
+ }
185
+ };
186
+ emptyForm = () => ({
187
+ alias: "",
188
+ models: [],
189
+ duration: "30d",
190
+ max_budget: void 0,
191
+ tpm_limit: void 0,
192
+ team_id: void 0,
193
+ key_type: "llm_api"
194
+ });
195
+ keyToEditForm = (k) => ({
196
+ key_alias: k.key_alias ?? "",
197
+ models: k.models ?? [],
198
+ max_budget: k.max_budget,
199
+ tpm_limit: k.tpm_limit,
200
+ rpm_limit: k.rpm_limit
201
+ });
202
+ KeysTable = ({
203
+ keys,
204
+ models,
205
+ teams,
206
+ loading,
207
+ onGenerateKey,
208
+ onUpdateKey,
209
+ onDeleteKey
210
+ }) => {
211
+ const [generateModalOpen, setGenerateModalOpen] = (0, import_react2.useState)(false);
212
+ const [showKeyValue, setShowKeyValue] = (0, import_react2.useState)(null);
213
+ const [newKeyValue, setNewKeyValue] = (0, import_react2.useState)(null);
214
+ const [formData, setFormData] = (0, import_react2.useState)(emptyForm());
215
+ const [submitting, setSubmitting] = (0, import_react2.useState)(false);
216
+ (0, import_react2.useEffect)(() => {
217
+ if (!generateModalOpen) return;
218
+ if (!formData.team_id && teams.length > 0) {
219
+ setFormData((f) => ({ ...f, team_id: teams[0].team_id }));
220
+ }
221
+ }, [generateModalOpen, teams, formData.team_id]);
222
+ const canGenerate = true;
223
+ const [editingKey, setEditingKey] = (0, import_react2.useState)(null);
224
+ const [editForm, setEditForm] = (0, import_react2.useState)({});
225
+ const [editSubmitting, setEditSubmitting] = (0, import_react2.useState)(false);
226
+ const selectedModels = models.filter((m) => (formData.models || []).includes(m.model_name));
227
+ const selectedTeam = teams.find((t) => t.team_id === formData.team_id) ?? null;
228
+ const editSelectedModels = models.filter((m) => (editForm.models || []).includes(m.model_name));
229
+ const handleGenerate = async () => {
230
+ setSubmitting(true);
231
+ try {
232
+ const response = await onGenerateKey(formData);
233
+ setNewKeyValue(response.key);
234
+ setFormData(emptyForm());
235
+ setTimeout(() => {
236
+ setGenerateModalOpen(false);
237
+ setNewKeyValue(null);
238
+ }, 1500);
239
+ } catch (error) {
240
+ console.error("Failed to generate key:", error);
241
+ } finally {
242
+ setSubmitting(false);
243
+ }
244
+ };
245
+ const handleCloseModal = () => {
246
+ setGenerateModalOpen(false);
247
+ setNewKeyValue(null);
248
+ setFormData(emptyForm());
249
+ };
250
+ const handleOpenEdit = (k) => {
251
+ setEditingKey(k);
252
+ setEditForm(keyToEditForm(k));
253
+ };
254
+ const handleCloseEdit = () => {
255
+ setEditingKey(null);
256
+ setEditForm({});
257
+ };
258
+ const handleUpdate = async () => {
259
+ if (!editingKey) return;
260
+ setEditSubmitting(true);
261
+ try {
262
+ await onUpdateKey(editingKey.key, editForm);
263
+ handleCloseEdit();
264
+ } catch (error) {
265
+ console.error("Failed to update key:", error);
266
+ } finally {
267
+ setEditSubmitting(false);
268
+ }
269
+ };
270
+ const copyToClipboard = (text) => {
271
+ navigator.clipboard.writeText(text);
272
+ };
273
+ return /* @__PURE__ */ import_react2.default.createElement(import_react2.default.Fragment, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Paper, { sx: { mb: 2 } }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", justifyContent: "space-between", alignItems: "center", p: 2 }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "h6" }, "Virtual Keys"), /* @__PURE__ */ import_react2.default.createElement(
274
+ import_material2.Button,
275
+ {
276
+ variant: "contained",
277
+ color: "primary",
278
+ startIcon: /* @__PURE__ */ import_react2.default.createElement(import_icons_material2.Add, null),
279
+ onClick: () => setGenerateModalOpen(true)
280
+ },
281
+ "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(
308
+ import_material2.Box,
309
+ {
310
+ display: "flex",
311
+ alignItems: "center",
312
+ gap: 1,
313
+ mt: 2,
314
+ p: 2,
315
+ sx: {
316
+ backgroundColor: "grey.100",
317
+ borderRadius: 1
318
+ }
319
+ },
320
+ /* @__PURE__ */ import_react2.default.createElement(
321
+ import_material2.Typography,
322
+ {
323
+ component: "code",
324
+ color: "text.primary",
325
+ sx: { fontFamily: "monospace", wordBreak: "break-all", flex: 1 }
326
+ },
327
+ newKeyValue
328
+ ),
329
+ /* @__PURE__ */ import_react2.default.createElement(import_material2.IconButton, { onClick: () => copyToClipboard(newKeyValue) }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material2.ContentCopy, null))
330
+ )) : /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, /* @__PURE__ */ import_react2.default.createElement(
331
+ import_material2.TextField,
332
+ {
333
+ label: "Alias",
334
+ value: formData.alias || "",
335
+ onChange: (e) => setFormData({ ...formData, alias: e.target.value }),
336
+ fullWidth: true
337
+ }
338
+ ), /* @__PURE__ */ import_react2.default.createElement(
339
+ import_material2.TextField,
340
+ {
341
+ select: true,
342
+ label: "Duration",
343
+ value: formData.duration || "30d",
344
+ onChange: (e) => setFormData({ ...formData, duration: e.target.value }),
345
+ fullWidth: true
346
+ },
347
+ /* @__PURE__ */ import_react2.default.createElement(import_material2.MenuItem, { value: "1d" }, "1 Day"),
348
+ /* @__PURE__ */ import_react2.default.createElement(import_material2.MenuItem, { value: "7d" }, "7 Days"),
349
+ /* @__PURE__ */ import_react2.default.createElement(import_material2.MenuItem, { value: "30d" }, "30 Days"),
350
+ /* @__PURE__ */ import_react2.default.createElement(import_material2.MenuItem, { value: "90d" }, "90 Days"),
351
+ /* @__PURE__ */ import_react2.default.createElement(import_material2.MenuItem, { value: "1y" }, "1 Year")
352
+ ), teams.length > 0 && /* @__PURE__ */ import_react2.default.createElement(
353
+ import_material2.Autocomplete,
354
+ {
355
+ options: teams,
356
+ getOptionLabel: (t) => t.team_alias || t.team_id,
357
+ value: selectedTeam,
358
+ onChange: (_e, team) => setFormData({ ...formData, team_id: team?.team_id }),
359
+ renderInput: (params) => /* @__PURE__ */ import_react2.default.createElement(
360
+ import_material2.TextField,
361
+ {
362
+ ...params,
363
+ label: "Team",
364
+ helperText: "Optional: bind this key to a specific team for scoped access",
365
+ fullWidth: true
366
+ }
367
+ )
368
+ }
369
+ ), models.length > 0 && /* @__PURE__ */ import_react2.default.createElement(
370
+ import_material2.Autocomplete,
371
+ {
372
+ multiple: true,
373
+ options: models,
374
+ groupBy: (m) => m.mode || "other",
375
+ getOptionLabel: (m) => m.model_name,
376
+ value: selectedModels,
377
+ onChange: (_e, selected) => setFormData({ ...formData, models: selected.map((m) => m.model_name) }),
378
+ renderOption: (props, m) => /* @__PURE__ */ import_react2.default.createElement("li", { ...props }, m.model_name, m.supports_function_calling && " \u{1F527}", m.supports_vision && " \u{1F441}\uFE0F"),
379
+ renderInput: (params) => /* @__PURE__ */ import_react2.default.createElement(import_material2.TextField, { ...params, label: "Models", fullWidth: true })
380
+ }
381
+ ), /* @__PURE__ */ import_react2.default.createElement(
382
+ import_material2.TextField,
383
+ {
384
+ label: "Max Budget (USD)",
385
+ type: "number",
386
+ value: formData.max_budget ?? "",
387
+ onChange: (e) => setFormData({ ...formData, max_budget: e.target.value ? Number(e.target.value) : void 0 }),
388
+ fullWidth: true
389
+ }
390
+ ), /* @__PURE__ */ import_react2.default.createElement(
391
+ import_material2.TextField,
392
+ {
393
+ label: "TPM Limit",
394
+ type: "number",
395
+ value: formData.tpm_limit ?? "",
396
+ onChange: (e) => setFormData({ ...formData, tpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
397
+ fullWidth: true
398
+ }
399
+ ))), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogActions, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: handleCloseModal }, newKeyValue ? "Done" : "Cancel"), !newKeyValue && /* @__PURE__ */ import_react2.default.createElement(
400
+ import_material2.Button,
401
+ {
402
+ onClick: handleGenerate,
403
+ variant: "contained",
404
+ color: "primary",
405
+ disabled: submitting || !canGenerate
406
+ },
407
+ submitting ? /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 24 }) : "Generate"
408
+ ), newKeyValue && /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: handleCloseModal, variant: "contained", color: "success" }, "Done"))), /* @__PURE__ */ import_react2.default.createElement(import_material2.Dialog, { open: !!editingKey, onClose: handleCloseEdit, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogTitle, null, "Edit Key"), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogContent, null, editingKey && /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2", color: "text.secondary" }, /* @__PURE__ */ import_react2.default.createElement("code", { style: { fontFamily: "monospace", color: "inherit" } }, maskKey(editingKey.key))), /* @__PURE__ */ import_react2.default.createElement(
409
+ import_material2.TextField,
410
+ {
411
+ label: "Alias",
412
+ value: editForm.key_alias || "",
413
+ onChange: (e) => setEditForm({ ...editForm, key_alias: e.target.value }),
414
+ fullWidth: true
415
+ }
416
+ ), models.length > 0 && /* @__PURE__ */ import_react2.default.createElement(
417
+ import_material2.Autocomplete,
418
+ {
419
+ multiple: true,
420
+ options: models,
421
+ groupBy: (m) => m.mode || "other",
422
+ getOptionLabel: (m) => m.model_name,
423
+ value: editSelectedModels,
424
+ onChange: (_e, selected) => setEditForm({ ...editForm, models: selected.map((m) => m.model_name) }),
425
+ renderInput: (params) => /* @__PURE__ */ import_react2.default.createElement(import_material2.TextField, { ...params, label: "Models", fullWidth: true })
426
+ }
427
+ ), /* @__PURE__ */ import_react2.default.createElement(
428
+ import_material2.TextField,
429
+ {
430
+ label: "Max Budget (USD)",
431
+ type: "number",
432
+ value: editForm.max_budget ?? "",
433
+ onChange: (e) => setEditForm({ ...editForm, max_budget: e.target.value ? Number(e.target.value) : void 0 }),
434
+ fullWidth: true
435
+ }
436
+ ), /* @__PURE__ */ import_react2.default.createElement(
437
+ import_material2.TextField,
438
+ {
439
+ label: "TPM Limit",
440
+ type: "number",
441
+ value: editForm.tpm_limit ?? "",
442
+ onChange: (e) => setEditForm({ ...editForm, tpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
443
+ fullWidth: true
444
+ }
445
+ ), /* @__PURE__ */ import_react2.default.createElement(
446
+ import_material2.TextField,
447
+ {
448
+ label: "RPM Limit",
449
+ type: "number",
450
+ value: editForm.rpm_limit ?? "",
451
+ onChange: (e) => setEditForm({ ...editForm, rpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
452
+ fullWidth: true
453
+ }
454
+ ))), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogActions, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: handleCloseEdit }, "Cancel"), /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: handleUpdate, variant: "contained", color: "primary", disabled: editSubmitting }, editSubmitting ? /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 24 }) : "Save"))));
455
+ };
456
+ }
457
+ });
458
+
459
+ // src/components/UsageStats.tsx
460
+ var import_react3, import_material3, import_recharts, fmtUsd, fmtInt, fmtPct, KpiCard, UsageStats;
461
+ var init_UsageStats = __esm({
462
+ "src/components/UsageStats.tsx"() {
463
+ "use strict";
464
+ import_react3 = __toESM(require("react"));
465
+ import_material3 = require("@mui/material");
466
+ import_recharts = require("recharts");
467
+ fmtUsd = (n) => `$${(n ?? 0).toFixed(n < 1 ? 4 : 2)}`;
468
+ fmtInt = (n) => (n ?? 0).toLocaleString();
469
+ fmtPct = (n) => `${(n * 100).toFixed(1)}%`;
470
+ KpiCard = ({ label, value, hint }) => /* @__PURE__ */ import_react3.default.createElement(import_material3.Paper, { variant: "outlined", sx: { p: 2, height: "100%" } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "caption", color: "text.secondary" }, label), /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "h5", sx: { mt: 0.5 } }, value), hint ? /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "caption", color: "text.secondary" }, hint) : null);
471
+ UsageStats = ({
472
+ usage,
473
+ models,
474
+ dateRange,
475
+ onDateRangeChange,
476
+ loading
477
+ }) => {
478
+ const [selectedModel, setSelectedModel] = (0, import_react3.useState)("all");
479
+ const [tab, setTab] = (0, import_react3.useState)("costs");
480
+ const selectedPreset = (0, import_react3.useMemo)(() => {
481
+ const start = dateRange.start;
482
+ const end = dateRange.end;
483
+ if (start.toDateString() === end.toDateString()) return "today";
484
+ const diffMs = end.getTime() - start.getTime();
485
+ const diffDays = Math.ceil(diffMs / (1e3 * 60 * 60 * 24));
486
+ if (diffDays <= 7) return "7d";
487
+ return "30d";
488
+ }, [dateRange]);
489
+ const handlePresetChange = (preset) => {
490
+ const end = /* @__PURE__ */ new Date();
491
+ const start = /* @__PURE__ */ new Date();
492
+ if (preset === "today") start.setHours(0, 0, 0, 0);
493
+ else if (preset === "7d") start.setDate(start.getDate() - 7);
494
+ else if (preset === "30d") start.setDate(start.getDate() - 30);
495
+ onDateRangeChange({ start, end });
496
+ };
497
+ const todayRows = (0, import_react3.useMemo)(() => {
498
+ const rows = usage?.daily_by_model ?? [];
499
+ if (rows.length === 0) return [];
500
+ const latestDate = rows.map((r) => r.date).sort().slice(-1)[0];
501
+ return rows.filter((r) => r.date === latestDate).map((r) => ({
502
+ model: r.model,
503
+ promptTokens: r.prompt_tokens,
504
+ completionTokens: r.completion_tokens
505
+ })).filter((r) => r.promptTokens + r.completionTokens > 0).sort((a, b) => b.promptTokens + b.completionTokens - (a.promptTokens + a.completionTokens));
506
+ }, [usage]);
507
+ const todayLabel = (0, import_react3.useMemo)(() => {
508
+ const rows = usage?.daily_by_model ?? [];
509
+ if (rows.length === 0) return null;
510
+ return rows.map((r) => r.date).sort().slice(-1)[0];
511
+ }, [usage]);
512
+ const dailyData = (0, import_react3.useMemo)(
513
+ () => (usage?.daily_usage ?? []).map((d) => ({
514
+ date: d.date,
515
+ spend: d.spend,
516
+ promptTokens: d.prompt_tokens,
517
+ completionTokens: d.completion_tokens,
518
+ totalTokens: d.total_tokens,
519
+ apiRequests: d.api_requests,
520
+ successfulRequests: d.successful_requests,
521
+ failedRequests: d.failed_requests
522
+ })),
523
+ [usage]
524
+ );
525
+ const modelRows = (0, import_react3.useMemo)(() => {
526
+ const entries = Object.entries(usage?.usage_by_model ?? {}).map(([model, d]) => ({
527
+ model,
528
+ spend: d.total_spend,
529
+ promptTokens: d.prompt_tokens,
530
+ completionTokens: d.completion_tokens,
531
+ totalTokens: d.total_tokens,
532
+ apiRequests: d.api_requests,
533
+ successfulRequests: d.successful_requests,
534
+ failedRequests: d.failed_requests,
535
+ successRate: d.api_requests > 0 ? d.successful_requests / d.api_requests : 0
536
+ }));
537
+ return selectedModel === "all" ? entries : entries.filter((e) => e.model === selectedModel);
538
+ }, [usage, selectedModel]);
539
+ const keyRows = (0, import_react3.useMemo)(
540
+ () => Object.entries(usage?.usage_by_key ?? {}).map(([keyHash, d]) => ({
541
+ keyHash,
542
+ keyAlias: d.key_alias ?? keyHash.slice(0, 8),
543
+ teamId: d.team_id ?? null,
544
+ models: d.models,
545
+ spend: d.total_spend,
546
+ totalTokens: d.total_tokens,
547
+ promptTokens: d.prompt_tokens,
548
+ completionTokens: d.completion_tokens,
549
+ apiRequests: d.api_requests,
550
+ successfulRequests: d.successful_requests,
551
+ failedRequests: d.failed_requests,
552
+ successRate: d.api_requests > 0 ? d.successful_requests / d.api_requests : 0
553
+ })),
554
+ [usage]
555
+ );
556
+ const totalRequests = usage?.api_requests ?? 0;
557
+ const overallSuccessRate = totalRequests > 0 ? (usage?.successful_requests ?? 0) / totalRequests : 0;
558
+ if (loading) {
559
+ return /* @__PURE__ */ import_react3.default.createElement(import_material3.Paper, { sx: { p: 2 } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { display: "flex", justifyContent: "center", p: 4 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.CircularProgress, null)));
560
+ }
561
+ return /* @__PURE__ */ import_react3.default.createElement(import_material3.Paper, { sx: { p: 2 } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2, flexWrap: "wrap", gap: 2 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "h6" }, "Usage Analytics"), /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { display: "flex", gap: 2, alignItems: "center", flexWrap: "wrap" }, /* @__PURE__ */ import_react3.default.createElement(import_material3.FormControl, { size: "small", sx: { minWidth: 140 } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.InputLabel, null, "Period"), /* @__PURE__ */ import_react3.default.createElement(
562
+ import_material3.Select,
563
+ {
564
+ value: selectedPreset,
565
+ label: "Period",
566
+ onChange: (e) => handlePresetChange(e.target.value)
567
+ },
568
+ /* @__PURE__ */ import_react3.default.createElement(import_material3.MenuItem, { value: "today" }, "Today"),
569
+ /* @__PURE__ */ import_react3.default.createElement(import_material3.MenuItem, { value: "7d" }, "Last 7 days"),
570
+ /* @__PURE__ */ import_react3.default.createElement(import_material3.MenuItem, { value: "30d" }, "Last 30 days")
571
+ )), tab === "models" && /* @__PURE__ */ import_react3.default.createElement(import_material3.FormControl, { size: "small", sx: { minWidth: 180 } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.InputLabel, null, "Model"), /* @__PURE__ */ import_react3.default.createElement(
572
+ import_material3.Select,
573
+ {
574
+ value: selectedModel,
575
+ label: "Model",
576
+ onChange: (e) => setSelectedModel(e.target.value)
577
+ },
578
+ /* @__PURE__ */ import_react3.default.createElement(import_material3.MenuItem, { value: "all" }, "All Models"),
579
+ models.map((m) => /* @__PURE__ */ import_react3.default.createElement(import_material3.MenuItem, { key: m.model_name, value: m.model_name }, m.model_name))
580
+ )))), /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { container: true, spacing: 2, sx: { mb: 2 } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { item: true, xs: 6, sm: 3 }, /* @__PURE__ */ import_react3.default.createElement(KpiCard, { label: "Total Spend", value: fmtUsd(usage?.total_spend ?? 0) })), /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { item: true, xs: 6, sm: 3 }, /* @__PURE__ */ import_react3.default.createElement(KpiCard, { label: "Total Requests", value: fmtInt(totalRequests), hint: `${fmtInt(usage?.failed_requests ?? 0)} failed` })), /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { item: true, xs: 6, sm: 3 }, /* @__PURE__ */ import_react3.default.createElement(KpiCard, { label: "Success Rate", value: totalRequests > 0 ? fmtPct(overallSuccessRate) : "\u2014" })), /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { item: true, xs: 6, sm: 3 }, /* @__PURE__ */ import_react3.default.createElement(
581
+ KpiCard,
582
+ {
583
+ label: "Total Tokens",
584
+ value: fmtInt(usage?.total_tokens ?? 0),
585
+ hint: `${fmtInt(usage?.prompt_tokens ?? 0)} in \xB7 ${fmtInt(usage?.completion_tokens ?? 0)} out`
586
+ }
587
+ ))), /* @__PURE__ */ import_react3.default.createElement(import_material3.Tabs, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 2 } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Tab, { value: "costs", label: "Costs" }), /* @__PURE__ */ import_react3.default.createElement(import_material3.Tab, { value: "models", label: "Model Activity" }), /* @__PURE__ */ import_react3.default.createElement(import_material3.Tab, { value: "keys", label: "Key Activity" })), tab === "costs" && /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { container: true, spacing: 3 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend"), /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { height: 260 }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.AreaChart, { data: dailyData }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.XAxis, { dataKey: "date", tick: { fontSize: 12 } }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.YAxis, { tick: { fontSize: 12 }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Tooltip, { formatter: (value) => [`$${value.toFixed(4)}`, "Spend"] }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Area, { type: "monotone", dataKey: "spend", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.3 }))))), /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Requests"), /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { height: 260 }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.BarChart, { data: dailyData }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.XAxis, { dataKey: "date", tick: { fontSize: 12 } }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.YAxis, { tick: { fontSize: 12 } }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Tooltip, null), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Legend, null), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Bar, { dataKey: "successfulRequests", name: "Successful", fill: "#82ca9d", stackId: "r" }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Bar, { dataKey: "failedRequests", name: "Failed", fill: "#e57373", stackId: "r" }))))), /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, todayLabel ? `Tokens In / Out by Model \u2014 ${todayLabel}` : "Tokens In / Out by Model \u2014 Today"), /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { height: 300 }, todayRows.length === 0 ? /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { display: "flex", alignItems: "center", justifyContent: "center", height: "100%" }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { color: "text.secondary" }, "No token activity for the latest day")) : /* @__PURE__ */ import_react3.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.BarChart, { data: todayRows, layout: "vertical", margin: { left: 60 } }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.XAxis, { type: "number", tick: { fontSize: 12 }, tickFormatter: fmtInt }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.YAxis, { type: "category", dataKey: "model", tick: { fontSize: 11 }, width: 180 }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Tooltip, { formatter: (value) => fmtInt(value) }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Legend, null), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Bar, { dataKey: "promptTokens", name: "Input (prompt)", fill: "#8884d8", stackId: "io" }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Bar, { dataKey: "completionTokens", name: "Output (completion)", fill: "#82ca9d", stackId: "io" })))))), tab === "models" && /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { container: true, spacing: 3 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Tokens by Model"), /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { height: 260 }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.BarChart, { data: modelRows }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.XAxis, { dataKey: "model", tick: { fontSize: 10 }, interval: 0, angle: -15, textAnchor: "end", height: 60 }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.YAxis, { tick: { fontSize: 12 } }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Tooltip, null), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Legend, null), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Bar, { dataKey: "promptTokens", name: "Prompt", fill: "#8884d8", stackId: "t" }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Bar, { dataKey: "completionTokens", name: "Completion", fill: "#82ca9d", stackId: "t" }))))), /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.TableContainer, { component: import_material3.Paper, variant: "outlined" }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Table, { size: "small" }, /* @__PURE__ */ import_react3.default.createElement(import_material3.TableHead, null, /* @__PURE__ */ import_react3.default.createElement(import_material3.TableRow, null, /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, null, "Model"), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, "Spend"), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, "Requests"), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, "Success"), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, "Failed"), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, "Prompt"), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, "Completion"), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { sx: { minWidth: 120 } }, "Success rate"))), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableBody, null, modelRows.length === 0 ? /* @__PURE__ */ import_react3.default.createElement(import_material3.TableRow, null, /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { colSpan: 8, align: "center" }, "No model activity")) : modelRows.sort((a, b) => b.spend - a.spend || b.totalTokens - a.totalTokens).map((r) => /* @__PURE__ */ import_react3.default.createElement(import_material3.TableRow, { key: r.model }, /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, null, r.model), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, fmtUsd(r.spend)), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, fmtInt(r.apiRequests)), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, fmtInt(r.successfulRequests)), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, fmtInt(r.failedRequests)), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, fmtInt(r.promptTokens)), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, fmtInt(r.completionTokens)), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, null, r.apiRequests > 0 ? /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ import_react3.default.createElement(
588
+ import_material3.LinearProgress,
589
+ {
590
+ variant: "determinate",
591
+ value: r.successRate * 100,
592
+ sx: { flex: 1, height: 6, borderRadius: 3 }
593
+ }
594
+ ), /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "caption" }, fmtPct(r.successRate))) : "\u2014")))))))), tab === "keys" && /* @__PURE__ */ import_react3.default.createElement(import_material3.TableContainer, { component: import_material3.Paper, variant: "outlined" }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Table, { size: "small" }, /* @__PURE__ */ import_react3.default.createElement(import_material3.TableHead, null, /* @__PURE__ */ import_react3.default.createElement(import_material3.TableRow, null, /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, null, "Key"), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, null, "Models"), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, "Spend"), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, "Requests"), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, "Tokens"), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, "Failed"), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { sx: { minWidth: 120 } }, "Success rate"))), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableBody, null, keyRows.length === 0 ? /* @__PURE__ */ import_react3.default.createElement(import_material3.TableRow, null, /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { colSpan: 7, align: "center" }, "No key activity")) : keyRows.sort((a, b) => b.spend - a.spend || b.apiRequests - a.apiRequests).map((r) => /* @__PURE__ */ import_react3.default.createElement(import_material3.TableRow, { key: r.keyHash }, /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, null, /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "body2" }, r.keyAlias), r.teamId ? /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "caption", color: "text.secondary" }, "team: ", r.teamId) : null), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, null, /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { display: "flex", gap: 0.5, flexWrap: "wrap" }, r.models.map((m) => /* @__PURE__ */ import_react3.default.createElement(import_material3.Chip, { key: m, label: m, size: "small", variant: "outlined" })))), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, fmtUsd(r.spend)), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, fmtInt(r.apiRequests)), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, fmtInt(r.totalTokens)), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, fmtInt(r.failedRequests)), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, null, r.apiRequests > 0 ? /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ import_react3.default.createElement(
595
+ import_material3.LinearProgress,
596
+ {
597
+ variant: "determinate",
598
+ value: r.successRate * 100,
599
+ sx: { flex: 1, height: 6, borderRadius: 3 }
600
+ }
601
+ ), /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "caption" }, fmtPct(r.successRate))) : "\u2014")))))));
602
+ };
603
+ }
604
+ });
605
+
606
+ // src/components/TeamUsage.tsx
607
+ var import_react4, import_material4, import_icons_material3, import_recharts2, TeamCard, TeamUsage;
608
+ var init_TeamUsage = __esm({
609
+ "src/components/TeamUsage.tsx"() {
610
+ "use strict";
611
+ import_react4 = __toESM(require("react"));
612
+ import_material4 = require("@mui/material");
613
+ import_icons_material3 = require("@mui/icons-material");
614
+ import_recharts2 = require("recharts");
615
+ TeamCard = ({ team, usage, usageLoading }) => {
616
+ const [expanded, setExpanded] = (0, import_react4.useState)(false);
617
+ const budget = team.max_budget ?? 0;
618
+ const spend = team.spend ?? 0;
619
+ const budgetPct = budget > 0 ? Math.min(spend / budget * 100, 100) : 0;
620
+ const isOver = budget > 0 && spend >= budget;
621
+ const isNear = budget > 0 && spend >= budget * 0.8 && !isOver;
622
+ const dailyData = usage?.daily_usage?.map((d) => ({ date: d.date, spend: d.spend })) ?? [];
623
+ return /* @__PURE__ */ import_react4.default.createElement(import_material4.Paper, { variant: "outlined", sx: { p: 2, mb: 2 } }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_icons_material3.Group, { color: "action" }), /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { flexGrow: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle1", fontWeight: 600 }, team.team_alias ?? team.team_id), team.models?.length ? /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", gap: 0.5, flexWrap: "wrap", mt: 0.5 }, team.models.map((m) => /* @__PURE__ */ import_react4.default.createElement(import_material4.Chip, { key: m, label: m, size: "small", variant: "outlined" }))) : /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "caption", color: "text.secondary" }, "All models")), /* @__PURE__ */ import_react4.default.createElement(import_material4.IconButton, { size: "small", onClick: () => setExpanded((e) => !e) }, expanded ? /* @__PURE__ */ import_react4.default.createElement(import_icons_material3.ExpandLess, null) : /* @__PURE__ */ import_react4.default.createElement(import_icons_material3.ExpandMore, null))), budget > 0 && /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { mt: 1.5 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", justifyContent: "space-between", mb: 0.5 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "body2" }, "$", spend.toFixed(2), " / $", budget.toFixed(2)), isOver && /* @__PURE__ */ import_react4.default.createElement(import_material4.Chip, { label: "Over Budget", size: "small", color: "error" }), isNear && /* @__PURE__ */ import_react4.default.createElement(import_material4.Chip, { label: "Near Limit", size: "small", color: "warning" })), /* @__PURE__ */ import_react4.default.createElement(
624
+ import_material4.LinearProgress,
625
+ {
626
+ variant: "determinate",
627
+ value: budgetPct,
628
+ color: isOver ? "error" : isNear ? "warning" : "primary",
629
+ sx: { height: 6, borderRadius: 1 }
630
+ }
631
+ )), /* @__PURE__ */ import_react4.default.createElement(import_material4.Collapse, { in: expanded }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { mt: 2 }, usageLoading ? /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", justifyContent: "center", p: 2 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.CircularProgress, { size: 24 })) : dailyData.length > 0 ? /* @__PURE__ */ import_react4.default.createElement(import_react4.default.Fragment, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend"), /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { height: 160 }, /* @__PURE__ */ import_react4.default.createElement(import_recharts2.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react4.default.createElement(import_recharts2.AreaChart, { data: dailyData }, /* @__PURE__ */ import_react4.default.createElement(import_recharts2.CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.XAxis, { dataKey: "date", tick: { fontSize: 11 } }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.YAxis, { tick: { fontSize: 11 }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.Tooltip, { formatter: (v) => [`$${v.toFixed(4)}`, "Spend"] }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.Area, { type: "monotone", dataKey: "spend", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.3 }))))) : null, team.members_with_roles?.length ? /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { mt: 2 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Members"), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableContainer, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Table, { size: "small" }, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableHead, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableRow, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, "User"), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, "Role"))), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableBody, null, team.members_with_roles.map((m) => /* @__PURE__ */ import_react4.default.createElement(import_material4.TableRow, { key: m.user_id }, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { sx: { fontFamily: "monospace", fontSize: 12 } }, m.user_id), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, /* @__PURE__ */ import_react4.default.createElement(
632
+ import_material4.Chip,
633
+ {
634
+ label: m.role,
635
+ size: "small",
636
+ color: m.role === "admin" ? "primary" : "default"
637
+ }
638
+ )))))))) : null)));
639
+ };
640
+ TeamUsage = ({
641
+ teams,
642
+ loading,
643
+ getTeamUsage,
644
+ getTeamUsageLoading
645
+ }) => {
646
+ if (loading) {
647
+ return /* @__PURE__ */ import_react4.default.createElement(import_material4.Paper, { sx: { p: 2 } }, /* @__PURE__ */ import_react4.default.createElement(import_material4.LinearProgress, null));
648
+ }
649
+ if (!teams.length) {
650
+ return /* @__PURE__ */ import_react4.default.createElement(import_material4.Paper, { sx: { p: 2 } }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_icons_material3.Group, { color: "disabled" }), /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { color: "text.secondary", variant: "body2" }, "No team membership found in LiteLLM for this account.")));
651
+ }
652
+ return /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "h6", mb: 1 }, "Teams"), teams.map((team) => /* @__PURE__ */ import_react4.default.createElement(
653
+ TeamCard,
654
+ {
655
+ key: team.team_id,
656
+ team,
657
+ usage: getTeamUsage(team.team_id),
658
+ usageLoading: getTeamUsageLoading(team.team_id)
659
+ }
660
+ )));
661
+ };
662
+ }
663
+ });
664
+
665
+ // src/components/LiteLLMPage.tsx
666
+ var LiteLLMPage_exports = {};
667
+ __export(LiteLLMPage_exports, {
668
+ LiteLLMPage: () => LiteLLMPage
669
+ });
670
+ var import_react5, import_material5, import_react_use, import_core_plugin_api2, LiteLLMPage;
671
+ var init_LiteLLMPage = __esm({
672
+ "src/components/LiteLLMPage.tsx"() {
673
+ "use strict";
674
+ import_react5 = __toESM(require("react"));
675
+ import_material5 = require("@mui/material");
676
+ import_react_use = require("react-use");
677
+ import_core_plugin_api2 = require("@backstage/core-plugin-api");
678
+ init_DashboardHeader();
679
+ init_KeysTable();
680
+ init_UsageStats();
681
+ init_TeamUsage();
682
+ init_api();
683
+ LiteLLMPage = () => {
684
+ const api = (0, import_core_plugin_api2.useApi)(liteLlmApiRef);
685
+ const [dateRange, setDateRange] = (0, import_react5.useState)(() => {
686
+ const end = /* @__PURE__ */ new Date();
687
+ const start = /* @__PURE__ */ new Date();
688
+ start.setDate(start.getDate() - 7);
689
+ return { start, end };
690
+ });
691
+ const [snackbar, setSnackbar] = (0, import_react5.useState)(null);
692
+ const [teamUsageCache, setTeamUsageCache] = (0, import_react5.useState)({});
693
+ const [teamUsageLoading, setTeamUsageLoading] = (0, import_react5.useState)({});
694
+ const { value: userInfo, loading: userLoading, error: userError } = (0, import_react_use.useAsync)(
695
+ () => api.getUserInfo(),
696
+ [api]
697
+ );
698
+ const { value: keys, loading: keysLoading, retry: refreshKeys } = (0, import_react_use.useAsyncRetry)(
699
+ async () => {
700
+ try {
701
+ return await api.listKeys();
702
+ } catch (e) {
703
+ setSnackbar({ message: `Failed to load keys: ${e.message}`, severity: "error" });
704
+ return [];
705
+ }
706
+ },
707
+ [api]
708
+ );
709
+ const { value: allModels, loading: modelsLoading } = (0, import_react_use.useAsync)(
710
+ () => api.listModels().catch(() => []),
711
+ [api]
712
+ );
713
+ const { value: allTeams, loading: teamsLoading } = (0, import_react_use.useAsync)(
714
+ () => api.getTeams().catch(() => []),
715
+ [api]
716
+ );
717
+ const teams = (0, import_react5.useMemo)(() => {
718
+ if (!allTeams?.length) return [];
719
+ if (!userInfo) return allTeams;
720
+ const userId = userInfo.user_id;
721
+ if (userInfo.teams?.length) {
722
+ return allTeams.filter((t) => userInfo.teams.includes(t.team_id));
723
+ }
724
+ const byMembership = allTeams.filter(
725
+ (t) => t.members_with_roles?.some((m) => m.user_id === userId)
726
+ );
727
+ return byMembership.length > 0 ? byMembership : allTeams;
728
+ }, [allTeams, userInfo]);
729
+ const { value: usage, loading: usageLoading } = (0, import_react_use.useAsync)(async () => {
730
+ const startDate = dateRange.start.toISOString().split("T")[0];
731
+ const endDate = dateRange.end.toISOString().split("T")[0];
732
+ return api.getUsage(startDate, endDate);
733
+ }, [api, dateRange]);
734
+ const loadTeamUsage = (0, import_react5.useCallback)(async (teamId) => {
735
+ if (teamUsageCache[teamId] !== void 0 || teamUsageLoading[teamId]) return;
736
+ setTeamUsageLoading((prev) => ({ ...prev, [teamId]: true }));
737
+ try {
738
+ const startDate = dateRange.start.toISOString().split("T")[0];
739
+ const endDate = dateRange.end.toISOString().split("T")[0];
740
+ const data = await api.getTeamUsage(teamId, startDate, endDate);
741
+ setTeamUsageCache((prev) => ({ ...prev, [teamId]: data }));
742
+ } catch {
743
+ setTeamUsageCache((prev) => ({ ...prev, [teamId]: null }));
744
+ } finally {
745
+ setTeamUsageLoading((prev) => ({ ...prev, [teamId]: false }));
746
+ }
747
+ }, [api, dateRange, teamUsageCache, teamUsageLoading]);
748
+ const allowedModels = (0, import_react5.useMemo)(() => {
749
+ if (!allModels?.length) return [];
750
+ const userModels = userInfo?.models;
751
+ const teamModels = teams?.flatMap((t) => t.models ?? []);
752
+ const hasUserRestriction = userModels && userModels.length > 0;
753
+ const hasTeamRestriction = teamModels && teamModels.length > 0;
754
+ if (!hasUserRestriction && !hasTeamRestriction) return allModels;
755
+ const allowed = /* @__PURE__ */ new Set([
756
+ ...hasUserRestriction ? userModels : allModels.map((m) => m.model_name),
757
+ ...hasTeamRestriction ? teamModels : []
758
+ ]);
759
+ return allModels.filter((m) => allowed.has(m.model_name));
760
+ }, [allModels, userInfo, teams]);
761
+ const handleGenerateKey = (0, import_react5.useCallback)(
762
+ async (request) => {
763
+ const response = await api.generateKey(request);
764
+ setSnackbar({ message: "Key generated successfully", severity: "success" });
765
+ refreshKeys();
766
+ return response;
767
+ },
768
+ [api, refreshKeys]
769
+ );
770
+ const handleUpdateKey = (0, import_react5.useCallback)(
771
+ async (keyId, request) => {
772
+ try {
773
+ await api.updateKey(keyId, request);
774
+ setSnackbar({ message: "Key updated successfully", severity: "success" });
775
+ refreshKeys();
776
+ } catch (e) {
777
+ setSnackbar({ message: `Failed to update key: ${e.message}`, severity: "error" });
778
+ throw e;
779
+ }
780
+ },
781
+ [api, refreshKeys]
782
+ );
783
+ const handleDeleteKey = (0, import_react5.useCallback)(
784
+ async (keyId) => {
785
+ try {
786
+ await api.deleteKey(keyId);
787
+ setSnackbar({
788
+ message: "Key revoked successfully",
789
+ severity: "success"
790
+ });
791
+ refreshKeys();
792
+ } catch (e) {
793
+ if (e.body?.success && (e.body?.message?.includes("already deleted") || e.body?.message?.includes("never existed"))) {
794
+ setSnackbar({
795
+ message: "Key was already deleted",
796
+ severity: "warning"
797
+ });
798
+ refreshKeys();
799
+ return;
800
+ }
801
+ setSnackbar({
802
+ message: `Failed to revoke key: ${e.message}`,
803
+ severity: "error"
804
+ });
805
+ } finally {
806
+ refreshKeys();
807
+ }
808
+ },
809
+ [api, refreshKeys]
810
+ );
811
+ const isInitialLoading = userLoading && !userInfo;
812
+ if (isInitialLoading) {
813
+ return /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "50vh" }, /* @__PURE__ */ import_react5.default.createElement(import_material5.CircularProgress, null));
814
+ }
815
+ if (userError || !userInfo) {
816
+ const isProvisioningEnabled = userError?.body?.provisioning === true;
817
+ const hint = userError?.body?.hint;
818
+ return /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { p: 3 }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Paper, { sx: { p: 3 } }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "h6", gutterBottom: true }, "Account not provisioned"), /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { color: "text.secondary", paragraph: true }, "Your Backstage account is not linked to a LiteLLM user."), hint ? /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "body2", color: "text.secondary" }, hint) : /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "body2", color: "text.secondary" }, isProvisioningEnabled ? "Auto-provisioning is enabled but failed. Check the backend logs." : "Set litellm.provisioning.enabled: true in app-config.yaml to enable auto-provisioning, or ask your administrator to create the account manually.")));
819
+ }
820
+ return /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { p: 3 }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Grid, { container: true, spacing: 2 }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react5.default.createElement(DashboardHeader, { userInfo, teams: teams ?? [], loading: userLoading || teamsLoading })), /* @__PURE__ */ import_react5.default.createElement(import_material5.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react5.default.createElement(
821
+ TeamUsage,
822
+ {
823
+ teams: teams ?? [],
824
+ loading: teamsLoading,
825
+ dateRange,
826
+ getTeamUsage: (teamId) => {
827
+ if (teamUsageCache[teamId] === void 0) loadTeamUsage(teamId);
828
+ return teamUsageCache[teamId] ?? null;
829
+ },
830
+ getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false
831
+ }
832
+ )), /* @__PURE__ */ import_react5.default.createElement(import_material5.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react5.default.createElement(
833
+ KeysTable,
834
+ {
835
+ keys: keys ?? [],
836
+ models: allowedModels,
837
+ teams: teams ?? [],
838
+ loading: keysLoading || modelsLoading,
839
+ onGenerateKey: handleGenerateKey,
840
+ onUpdateKey: handleUpdateKey,
841
+ onDeleteKey: handleDeleteKey
842
+ }
843
+ )), /* @__PURE__ */ import_react5.default.createElement(import_material5.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react5.default.createElement(
844
+ UsageStats,
845
+ {
846
+ usage: usage ?? null,
847
+ models: allModels ?? [],
848
+ dateRange,
849
+ onDateRangeChange: setDateRange,
850
+ loading: usageLoading
851
+ }
852
+ ))), /* @__PURE__ */ import_react5.default.createElement(
853
+ import_material5.Snackbar,
854
+ {
855
+ open: !!snackbar,
856
+ autoHideDuration: 5e3,
857
+ onClose: () => setSnackbar(null),
858
+ anchorOrigin: { vertical: "bottom", horizontal: "right" }
859
+ },
860
+ snackbar ? /* @__PURE__ */ import_react5.default.createElement(import_material5.Alert, { severity: snackbar.severity, onClose: () => setSnackbar(null) }, snackbar.message) : void 0
861
+ ));
862
+ };
863
+ }
864
+ });
865
+
866
+ // src/index.ts
867
+ var index_exports = {};
868
+ __export(index_exports, {
869
+ DashboardHeader: () => DashboardHeader,
870
+ KeysTable: () => KeysTable,
871
+ LiteLLMPage: () => LiteLLMPage,
872
+ LiteLlmApi: () => LiteLlmApi,
873
+ TeamUsage: () => TeamUsage,
874
+ UsageStats: () => UsageStats,
875
+ liteLlmApiRef: () => liteLlmApiRef,
876
+ litellmPlugin: () => litellmPlugin
877
+ });
878
+ module.exports = __toCommonJS(index_exports);
879
+
880
+ // src/plugin.tsx
881
+ var import_react6 = __toESM(require("react"));
882
+ var import_icons_material4 = require("@mui/icons-material");
883
+ var import_frontend_plugin_api = require("@backstage/frontend-plugin-api");
884
+ init_api();
885
+ var liteLlmApi = import_frontend_plugin_api.ApiBlueprint.make({
886
+ params: (defineParams) => defineParams({
887
+ api: liteLlmApiRef,
888
+ deps: { fetchApi: import_frontend_plugin_api.fetchApiRef },
889
+ factory: ({ fetchApi }) => new LiteLlmApi(fetchApi)
890
+ })
891
+ });
892
+ var liteLlmPage = import_frontend_plugin_api.PageBlueprint.make({
893
+ params: {
894
+ path: "/litellm",
895
+ title: "LiteLLM",
896
+ icon: /* @__PURE__ */ import_react6.default.createElement(import_icons_material4.TrendingUp, null),
897
+ loader: async () => {
898
+ const { LiteLLMPage: LiteLLMPage2 } = await Promise.resolve().then(() => (init_LiteLLMPage(), LiteLLMPage_exports));
899
+ return /* @__PURE__ */ import_react6.default.createElement(LiteLLMPage2, null);
900
+ }
901
+ }
902
+ });
903
+ var litellmPlugin = (0, import_frontend_plugin_api.createFrontendPlugin)({
904
+ pluginId: "litellm",
905
+ extensions: [liteLlmApi, liteLlmPage]
906
+ });
907
+
908
+ // src/index.ts
909
+ init_LiteLLMPage();
910
+ init_DashboardHeader();
911
+ init_KeysTable();
912
+ init_UsageStats();
913
+ init_TeamUsage();
914
+ init_api();
915
+ //# sourceMappingURL=index.cjs.js.map