@acarmisc/backstage-plugin-litellm 0.4.0 → 0.5.0
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/api.d.ts +11 -1
- package/dist/components/AuditLog.d.ts +7 -0
- package/dist/components/KeysTable.d.ts +4 -0
- package/dist/index.cjs.js +469 -79
- package/dist/index.cjs.js.map +4 -4
- package/dist/index.esm.js +509 -99
- package/dist/index.esm.js.map +4 -4
- package/dist/types.d.ts +32 -0
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
2
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
-
var __esm = (fn, res) => function __init() {
|
|
4
|
-
|
|
3
|
+
var __esm = (fn, res, err) => function __init() {
|
|
4
|
+
if (err) throw err[0];
|
|
5
|
+
try {
|
|
6
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
7
|
+
} catch (e) {
|
|
8
|
+
throw err = [e], e;
|
|
9
|
+
}
|
|
5
10
|
};
|
|
6
11
|
var __export = (target, all) => {
|
|
7
12
|
for (var name in all)
|
|
@@ -83,6 +88,29 @@ var init_api = __esm({
|
|
|
83
88
|
async deleteKey(keyId) {
|
|
84
89
|
return this.del(`/keys/${encodeURIComponent(keyId)}`);
|
|
85
90
|
}
|
|
91
|
+
async rotateKey(keyId) {
|
|
92
|
+
return this.post(`/keys/${encodeURIComponent(keyId)}/regenerate`, {});
|
|
93
|
+
}
|
|
94
|
+
async blockKey(keyId) {
|
|
95
|
+
await this.post(`/keys/${encodeURIComponent(keyId)}/block`, {});
|
|
96
|
+
}
|
|
97
|
+
async unblockKey(keyId) {
|
|
98
|
+
await this.post(`/keys/${encodeURIComponent(keyId)}/unblock`, {});
|
|
99
|
+
}
|
|
100
|
+
async resetKeySpend(keyId) {
|
|
101
|
+
await this.post(`/keys/${encodeURIComponent(keyId)}/reset_spend`, {});
|
|
102
|
+
}
|
|
103
|
+
async getAuditLogs(params) {
|
|
104
|
+
const strParams = {};
|
|
105
|
+
if (params.page !== void 0) strParams.page = String(params.page);
|
|
106
|
+
if (params.page_size !== void 0) strParams.page_size = String(params.page_size);
|
|
107
|
+
if (params.start_date) strParams.start_date = params.start_date;
|
|
108
|
+
if (params.end_date) strParams.end_date = params.end_date;
|
|
109
|
+
if (params.action) strParams.action = params.action;
|
|
110
|
+
if (params.table_name) strParams.table_name = params.table_name;
|
|
111
|
+
if (params.changed_by) strParams.changed_by = params.changed_by;
|
|
112
|
+
return this.get("/audit", strParams);
|
|
113
|
+
}
|
|
86
114
|
async listModels() {
|
|
87
115
|
return this.get("/models");
|
|
88
116
|
}
|
|
@@ -129,7 +157,7 @@ var init_DashboardHeader = __esm({
|
|
|
129
157
|
});
|
|
130
158
|
|
|
131
159
|
// src/components/KeysTable.tsx
|
|
132
|
-
import React2, { useState } from "react";
|
|
160
|
+
import React2, { useState, useMemo } from "react";
|
|
133
161
|
import {
|
|
134
162
|
Paper as Paper2,
|
|
135
163
|
Table,
|
|
@@ -150,9 +178,38 @@ import {
|
|
|
150
178
|
MenuItem,
|
|
151
179
|
Chip as Chip2,
|
|
152
180
|
CircularProgress,
|
|
153
|
-
Autocomplete
|
|
181
|
+
Autocomplete,
|
|
182
|
+
LinearProgress as LinearProgress2,
|
|
183
|
+
InputAdornment
|
|
154
184
|
} from "@mui/material";
|
|
155
|
-
import { ContentCopy, Delete, Add, Edit } from "@mui/icons-material";
|
|
185
|
+
import { ContentCopy, Delete, Add, Edit, Autorenew, Search, Warning, Lock, LockOpen } from "@mui/icons-material";
|
|
186
|
+
function expiryStatus(expiresAt) {
|
|
187
|
+
if (!expiresAt) return null;
|
|
188
|
+
const diff = new Date(expiresAt).getTime() - Date.now();
|
|
189
|
+
const days = diff / (1e3 * 60 * 60 * 24);
|
|
190
|
+
if (days < 0) return "expired";
|
|
191
|
+
if (days < 7) return "soon";
|
|
192
|
+
return "ok";
|
|
193
|
+
}
|
|
194
|
+
function ExpiryChip({ expiresAt }) {
|
|
195
|
+
const status = expiryStatus(expiresAt);
|
|
196
|
+
if (!status) return /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "text.secondary" }, "-");
|
|
197
|
+
const label = status === "expired" ? "Expired" : status === "soon" ? `${Math.ceil((new Date(expiresAt).getTime() - Date.now()) / 864e5)}d left` : formatDate(expiresAt);
|
|
198
|
+
const color = status === "expired" ? "error" : status === "soon" ? "warning" : "default";
|
|
199
|
+
const icon = status === "expired" || status === "soon" ? /* @__PURE__ */ React2.createElement(Warning, { fontSize: "small" }) : void 0;
|
|
200
|
+
return /* @__PURE__ */ React2.createElement(Chip2, { label, color, size: "small", icon });
|
|
201
|
+
}
|
|
202
|
+
function BudgetCell({ spend, maxBudget }) {
|
|
203
|
+
if (!maxBudget) return /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "text.secondary" }, "-");
|
|
204
|
+
const pct = Math.min(100, spend / maxBudget * 100);
|
|
205
|
+
const color = pct >= 100 ? "error" : pct >= 80 ? "warning" : "primary";
|
|
206
|
+
return /* @__PURE__ */ React2.createElement(Box2, { minWidth: 100 }, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", justifyContent: "space-between" }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "caption" }, "$", spend.toFixed(2)), /* @__PURE__ */ React2.createElement(Typography2, { variant: "caption", color: "text.secondary" }, "$", maxBudget)), /* @__PURE__ */ React2.createElement(LinearProgress2, { variant: "determinate", value: pct, color, sx: { height: 5, borderRadius: 1, mt: 0.25 } }));
|
|
207
|
+
}
|
|
208
|
+
function fmtCost(perToken) {
|
|
209
|
+
if (!perToken) return null;
|
|
210
|
+
const per1k = perToken * 1e3;
|
|
211
|
+
return per1k < 0.01 ? `$${(perToken * 1e6).toFixed(2)}/M` : `$${per1k.toFixed(3)}/1K`;
|
|
212
|
+
}
|
|
156
213
|
var maskKey, shortKeyId, formatDate, emptyForm, keyToEditForm, KeysTable;
|
|
157
214
|
var init_KeysTable = __esm({
|
|
158
215
|
"src/components/KeysTable.tsx"() {
|
|
@@ -179,6 +236,7 @@ var init_KeysTable = __esm({
|
|
|
179
236
|
duration: "30d",
|
|
180
237
|
max_budget: 100,
|
|
181
238
|
tpm_limit: void 0,
|
|
239
|
+
rpm_limit: void 0,
|
|
182
240
|
team_id: void 0,
|
|
183
241
|
key_type: "llm_api"
|
|
184
242
|
});
|
|
@@ -196,16 +254,34 @@ var init_KeysTable = __esm({
|
|
|
196
254
|
loading,
|
|
197
255
|
onGenerateKey,
|
|
198
256
|
onUpdateKey,
|
|
257
|
+
onRotateKey,
|
|
258
|
+
onBlockKey,
|
|
259
|
+
onUnblockKey,
|
|
260
|
+
onResetKeySpend,
|
|
199
261
|
onDeleteKey
|
|
200
262
|
}) => {
|
|
201
263
|
const [generateModalOpen, setGenerateModalOpen] = useState(false);
|
|
202
264
|
const [newKeyValue, setNewKeyValue] = useState(null);
|
|
203
265
|
const [formData, setFormData] = useState(emptyForm());
|
|
204
266
|
const [submitting, setSubmitting] = useState(false);
|
|
205
|
-
const canGenerate = true;
|
|
206
267
|
const [editingKey, setEditingKey] = useState(null);
|
|
207
268
|
const [editForm, setEditForm] = useState({});
|
|
208
269
|
const [editSubmitting, setEditSubmitting] = useState(false);
|
|
270
|
+
const [rotatingKeyId, setRotatingKeyId] = useState(null);
|
|
271
|
+
const [rotatedKeyValue, setRotatedKeyValue] = useState(null);
|
|
272
|
+
const [blockingKeyId, setBlockingKeyId] = useState(null);
|
|
273
|
+
const [deleteConfirmId, setDeleteConfirmId] = useState(null);
|
|
274
|
+
const [deleteSubmitting, setDeleteSubmitting] = useState(false);
|
|
275
|
+
const [resetSpendConfirm, setResetSpendConfirm] = useState(false);
|
|
276
|
+
const [resetSpendSubmitting, setResetSpendSubmitting] = useState(false);
|
|
277
|
+
const [filterText, setFilterText] = useState("");
|
|
278
|
+
const filteredKeys = useMemo(() => {
|
|
279
|
+
if (!filterText.trim()) return keys;
|
|
280
|
+
const q = filterText.toLowerCase();
|
|
281
|
+
return keys.filter(
|
|
282
|
+
(k) => (k.key_alias ?? "").toLowerCase().includes(q) || k.models?.some((m) => m.toLowerCase().includes(q))
|
|
283
|
+
);
|
|
284
|
+
}, [keys, filterText]);
|
|
209
285
|
const selectedModels = models.filter((m) => (formData.models || []).includes(m.model_name));
|
|
210
286
|
const selectedTeam = teams.find((t) => t.team_id === formData.team_id) ?? null;
|
|
211
287
|
const editSelectedModels = models.filter((m) => (editForm.models || []).includes(m.model_name));
|
|
@@ -233,6 +309,7 @@ var init_KeysTable = __esm({
|
|
|
233
309
|
const handleCloseEdit = () => {
|
|
234
310
|
setEditingKey(null);
|
|
235
311
|
setEditForm({});
|
|
312
|
+
setResetSpendConfirm(false);
|
|
236
313
|
};
|
|
237
314
|
const handleUpdate = async () => {
|
|
238
315
|
if (!editingKey) return;
|
|
@@ -246,10 +323,72 @@ var init_KeysTable = __esm({
|
|
|
246
323
|
setEditSubmitting(false);
|
|
247
324
|
}
|
|
248
325
|
};
|
|
326
|
+
const handleRotate = async (keyId) => {
|
|
327
|
+
setRotatingKeyId(keyId);
|
|
328
|
+
try {
|
|
329
|
+
const response = await onRotateKey(keyId);
|
|
330
|
+
setRotatedKeyValue(response.key);
|
|
331
|
+
} catch (error) {
|
|
332
|
+
console.error("Failed to rotate key:", error);
|
|
333
|
+
} finally {
|
|
334
|
+
setRotatingKeyId(null);
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
const handleToggleBlock = async (key) => {
|
|
338
|
+
const keyId = key.token ?? key.key;
|
|
339
|
+
setBlockingKeyId(keyId);
|
|
340
|
+
try {
|
|
341
|
+
if (key.blocked) {
|
|
342
|
+
await onUnblockKey(keyId);
|
|
343
|
+
} else {
|
|
344
|
+
await onBlockKey(keyId);
|
|
345
|
+
}
|
|
346
|
+
} finally {
|
|
347
|
+
setBlockingKeyId(null);
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
const handleResetSpend = async () => {
|
|
351
|
+
if (!editingKey) return;
|
|
352
|
+
setResetSpendSubmitting(true);
|
|
353
|
+
try {
|
|
354
|
+
await onResetKeySpend(editingKey.token ?? editingKey.key);
|
|
355
|
+
setResetSpendConfirm(false);
|
|
356
|
+
handleCloseEdit();
|
|
357
|
+
} finally {
|
|
358
|
+
setResetSpendSubmitting(false);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
const handleConfirmDelete = async () => {
|
|
362
|
+
if (!deleteConfirmId) return;
|
|
363
|
+
setDeleteSubmitting(true);
|
|
364
|
+
try {
|
|
365
|
+
await onDeleteKey(deleteConfirmId);
|
|
366
|
+
} finally {
|
|
367
|
+
setDeleteSubmitting(false);
|
|
368
|
+
setDeleteConfirmId(null);
|
|
369
|
+
}
|
|
370
|
+
};
|
|
249
371
|
const copyToClipboard = (text) => {
|
|
250
372
|
navigator.clipboard.writeText(text);
|
|
251
373
|
};
|
|
252
|
-
|
|
374
|
+
const modelOption = (m) => {
|
|
375
|
+
const inCost = fmtCost(m.input_cost_per_token);
|
|
376
|
+
const outCost = fmtCost(m.output_cost_per_token);
|
|
377
|
+
return /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement("span", null, m.model_name), m.supports_function_calling && " \u{1F527}", m.supports_vision && " \u{1F441}\uFE0F", (inCost || outCost) && /* @__PURE__ */ React2.createElement(Typography2, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, inCost, " in \xB7 ", outCost, " out"));
|
|
378
|
+
};
|
|
379
|
+
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, gap: 2 }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "h6" }, "Virtual Keys"), /* @__PURE__ */ React2.createElement(Box2, { display: "flex", gap: 1, alignItems: "center", flex: 1, justifyContent: "flex-end" }, /* @__PURE__ */ React2.createElement(
|
|
380
|
+
TextField,
|
|
381
|
+
{
|
|
382
|
+
size: "small",
|
|
383
|
+
placeholder: "Filter by alias or model\u2026",
|
|
384
|
+
value: filterText,
|
|
385
|
+
onChange: (e) => setFilterText(e.target.value),
|
|
386
|
+
sx: { minWidth: 240 },
|
|
387
|
+
InputProps: {
|
|
388
|
+
startAdornment: /* @__PURE__ */ React2.createElement(InputAdornment, { position: "start" }, /* @__PURE__ */ React2.createElement(Search, { fontSize: "small" }))
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
), /* @__PURE__ */ React2.createElement(
|
|
253
392
|
Button,
|
|
254
393
|
{
|
|
255
394
|
variant: "contained",
|
|
@@ -258,9 +397,11 @@ var init_KeysTable = __esm({
|
|
|
258
397
|
onClick: () => setGenerateModalOpen(true)
|
|
259
398
|
},
|
|
260
399
|
"Generate New Key"
|
|
261
|
-
)), /* @__PURE__ */ React2.createElement(TableContainer, null, /* @__PURE__ */ React2.createElement(Table, null, /* @__PURE__ */ React2.createElement(TableHead, null, /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, null, "Alias"), /* @__PURE__ */ React2.createElement(TableCell, null, "Key ID"), /* @__PURE__ */ React2.createElement(TableCell, null, "Created"), /* @__PURE__ */ React2.createElement(TableCell, null, "
|
|
400
|
+
))), /* @__PURE__ */ React2.createElement(TableContainer, null, /* @__PURE__ */ React2.createElement(Table, null, /* @__PURE__ */ React2.createElement(TableHead, null, /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, null, "Alias"), /* @__PURE__ */ React2.createElement(TableCell, null, "Key ID"), /* @__PURE__ */ React2.createElement(TableCell, null, "Created"), /* @__PURE__ */ React2.createElement(TableCell, null, "Expires"), /* @__PURE__ */ React2.createElement(TableCell, null, "Budget"), /* @__PURE__ */ React2.createElement(TableCell, null, "TPM / RPM"), /* @__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 }))) : filteredKeys.length === 0 ? /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ React2.createElement(Typography2, { color: "text.secondary" }, filterText ? "No keys match filter" : "No keys found"))) : filteredKeys.map((key) => {
|
|
262
401
|
const keyId = key.token ?? key.key;
|
|
263
|
-
|
|
402
|
+
const isRotating = rotatingKeyId === keyId;
|
|
403
|
+
const isBlocking = blockingKeyId === keyId;
|
|
404
|
+
return /* @__PURE__ */ React2.createElement(TableRow, { key: keyId, sx: key.blocked ? { bgcolor: "action.disabledBackground" } : void 0 }, /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", alignItems: "center", gap: 0.5 }, key.key_alias || "-", key.blocked && /* @__PURE__ */ React2.createElement(Chip2, { label: "Blocked", color: "error", size: "small" }))), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", alignItems: "center", gap: 0.5 }, /* @__PURE__ */ React2.createElement(
|
|
264
405
|
Typography2,
|
|
265
406
|
{
|
|
266
407
|
variant: "body2",
|
|
@@ -276,7 +417,32 @@ var init_KeysTable = __esm({
|
|
|
276
417
|
}
|
|
277
418
|
},
|
|
278
419
|
shortKeyId(keyId)
|
|
279
|
-
), /* @__PURE__ */ React2.createElement(IconButton, { size: "small", onClick: () => copyToClipboard(keyId), title: "Copy Key ID" }, /* @__PURE__ */ React2.createElement(ContentCopy, { fontSize: "small" })))), /* @__PURE__ */ React2.createElement(TableCell, null, formatDate(key.created_at)), /* @__PURE__ */ React2.createElement(TableCell, null,
|
|
420
|
+
), /* @__PURE__ */ React2.createElement(IconButton, { size: "small", onClick: () => copyToClipboard(keyId), title: "Copy Key ID" }, /* @__PURE__ */ React2.createElement(ContentCopy, { fontSize: "small" })))), /* @__PURE__ */ React2.createElement(TableCell, null, formatDate(key.created_at)), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(ExpiryChip, { expiresAt: key.expires_at })), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(BudgetCell, { spend: key.spend ?? 0, maxBudget: key.max_budget })), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2" }, key.tpm_limit ?? "-", " / ", key.rpm_limit ?? "-")), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", gap: 0.5, flexWrap: "wrap" }, key.models?.slice(0, 2).map((model) => /* @__PURE__ */ React2.createElement(Chip2, { key: model, label: model, size: "small" })), (key.models?.length || 0) > 2 && /* @__PURE__ */ React2.createElement(Chip2, { label: `+${(key.models?.length || 0) - 2}`, size: "small", variant: "outlined" }))), /* @__PURE__ */ React2.createElement(TableCell, { align: "right" }, /* @__PURE__ */ React2.createElement(IconButton, { onClick: () => handleOpenEdit(key), title: "Edit key" }, /* @__PURE__ */ React2.createElement(Edit, { fontSize: "small" })), /* @__PURE__ */ React2.createElement(
|
|
421
|
+
IconButton,
|
|
422
|
+
{
|
|
423
|
+
onClick: () => handleRotate(keyId),
|
|
424
|
+
disabled: isRotating,
|
|
425
|
+
title: "Rotate key \u2014 generates a new secret, same settings"
|
|
426
|
+
},
|
|
427
|
+
isRotating ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 18 }) : /* @__PURE__ */ React2.createElement(Autorenew, { fontSize: "small" })
|
|
428
|
+
), /* @__PURE__ */ React2.createElement(
|
|
429
|
+
IconButton,
|
|
430
|
+
{
|
|
431
|
+
onClick: () => handleToggleBlock(key),
|
|
432
|
+
disabled: isBlocking,
|
|
433
|
+
color: key.blocked ? "warning" : "default",
|
|
434
|
+
title: key.blocked ? "Unblock key" : "Block key \u2014 suspends without revoking"
|
|
435
|
+
},
|
|
436
|
+
isBlocking ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 18 }) : key.blocked ? /* @__PURE__ */ React2.createElement(LockOpen, { fontSize: "small" }) : /* @__PURE__ */ React2.createElement(Lock, { fontSize: "small" })
|
|
437
|
+
), /* @__PURE__ */ React2.createElement(
|
|
438
|
+
IconButton,
|
|
439
|
+
{
|
|
440
|
+
color: "error",
|
|
441
|
+
onClick: () => setDeleteConfirmId(keyId),
|
|
442
|
+
title: "Revoke key"
|
|
443
|
+
},
|
|
444
|
+
/* @__PURE__ */ React2.createElement(Delete, null)
|
|
445
|
+
)));
|
|
280
446
|
}))))), /* @__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(
|
|
281
447
|
Box2,
|
|
282
448
|
{
|
|
@@ -351,7 +517,7 @@ var init_KeysTable = __esm({
|
|
|
351
517
|
getOptionLabel: (m) => m.model_name,
|
|
352
518
|
value: selectedModels,
|
|
353
519
|
onChange: (_e, selected) => setFormData({ ...formData, models: selected.map((m) => m.model_name) }),
|
|
354
|
-
renderOption: (props, m) => /* @__PURE__ */ React2.createElement("li", { ...props }, m
|
|
520
|
+
renderOption: (props, m) => /* @__PURE__ */ React2.createElement("li", { ...props }, modelOption(m)),
|
|
355
521
|
renderInput: (params) => /* @__PURE__ */ React2.createElement(TextField, { ...params, label: "Models", helperText: "Leave empty to allow all models", fullWidth: true })
|
|
356
522
|
}
|
|
357
523
|
), /* @__PURE__ */ React2.createElement(
|
|
@@ -373,13 +539,22 @@ var init_KeysTable = __esm({
|
|
|
373
539
|
onChange: (e) => setFormData({ ...formData, tpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
|
|
374
540
|
fullWidth: true
|
|
375
541
|
}
|
|
542
|
+
), /* @__PURE__ */ React2.createElement(
|
|
543
|
+
TextField,
|
|
544
|
+
{
|
|
545
|
+
label: "RPM Limit",
|
|
546
|
+
type: "number",
|
|
547
|
+
value: formData.rpm_limit ?? "",
|
|
548
|
+
onChange: (e) => setFormData({ ...formData, rpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
|
|
549
|
+
fullWidth: true
|
|
550
|
+
}
|
|
376
551
|
))), /* @__PURE__ */ React2.createElement(DialogActions, null, /* @__PURE__ */ React2.createElement(Button, { onClick: handleCloseModal }, newKeyValue ? "Done" : "Cancel"), !newKeyValue && /* @__PURE__ */ React2.createElement(
|
|
377
552
|
Button,
|
|
378
553
|
{
|
|
379
554
|
onClick: handleGenerate,
|
|
380
555
|
variant: "contained",
|
|
381
556
|
color: "primary",
|
|
382
|
-
disabled: submitting
|
|
557
|
+
disabled: submitting
|
|
383
558
|
},
|
|
384
559
|
submitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 24 }) : "Generate"
|
|
385
560
|
), newKeyValue && /* @__PURE__ */ React2.createElement(Button, { onClick: handleCloseModal, variant: "contained", color: "success" }, "Done"))), /* @__PURE__ */ React2.createElement(Dialog, { open: !!editingKey, onClose: handleCloseEdit, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ React2.createElement(DialogTitle, null, "Edit Key"), /* @__PURE__ */ React2.createElement(DialogContent, null, editingKey && /* @__PURE__ */ React2.createElement(Box2, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "text.secondary" }, /* @__PURE__ */ React2.createElement("code", { style: { fontFamily: "monospace", color: "inherit" } }, maskKey(editingKey.key))), /* @__PURE__ */ React2.createElement(
|
|
@@ -399,6 +574,7 @@ var init_KeysTable = __esm({
|
|
|
399
574
|
getOptionLabel: (m) => m.model_name,
|
|
400
575
|
value: editSelectedModels,
|
|
401
576
|
onChange: (_e, selected) => setEditForm({ ...editForm, models: selected.map((m) => m.model_name) }),
|
|
577
|
+
renderOption: (props, m) => /* @__PURE__ */ React2.createElement("li", { ...props }, modelOption(m)),
|
|
402
578
|
renderInput: (params) => /* @__PURE__ */ React2.createElement(TextField, { ...params, label: "Models", fullWidth: true })
|
|
403
579
|
}
|
|
404
580
|
), /* @__PURE__ */ React2.createElement(
|
|
@@ -428,13 +604,60 @@ var init_KeysTable = __esm({
|
|
|
428
604
|
onChange: (e) => setEditForm({ ...editForm, rpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
|
|
429
605
|
fullWidth: true
|
|
430
606
|
}
|
|
431
|
-
)
|
|
607
|
+
), /* @__PURE__ */ React2.createElement(Box2, { mt: 1, pt: 2, borderTop: "1px solid", sx: { borderColor: "divider" } }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "caption", color: "text.secondary", display: "block", mb: 1 }, "Danger Zone"), !resetSpendConfirm ? /* @__PURE__ */ React2.createElement(
|
|
608
|
+
Button,
|
|
609
|
+
{
|
|
610
|
+
size: "small",
|
|
611
|
+
color: "warning",
|
|
612
|
+
variant: "outlined",
|
|
613
|
+
onClick: () => setResetSpendConfirm(true)
|
|
614
|
+
},
|
|
615
|
+
"Reset Spend to $0"
|
|
616
|
+
) : /* @__PURE__ */ React2.createElement(Box2, { display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "warning.main" }, "Zero out spend counter?"), /* @__PURE__ */ React2.createElement(
|
|
617
|
+
Button,
|
|
618
|
+
{
|
|
619
|
+
size: "small",
|
|
620
|
+
color: "warning",
|
|
621
|
+
variant: "contained",
|
|
622
|
+
disabled: resetSpendSubmitting,
|
|
623
|
+
onClick: handleResetSpend
|
|
624
|
+
},
|
|
625
|
+
resetSpendSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 16 }) : "Confirm"
|
|
626
|
+
), /* @__PURE__ */ React2.createElement(Button, { size: "small", onClick: () => setResetSpendConfirm(false) }, "Cancel"))))), /* @__PURE__ */ React2.createElement(DialogActions, null, /* @__PURE__ */ React2.createElement(Button, { onClick: handleCloseEdit }, "Cancel"), /* @__PURE__ */ React2.createElement(Button, { onClick: handleUpdate, variant: "contained", color: "primary", disabled: editSubmitting }, editSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 24 }) : "Save"))), /* @__PURE__ */ React2.createElement(Dialog, { open: !!rotatedKeyValue, onClose: () => setRotatedKeyValue(null), maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ React2.createElement(DialogTitle, null, "Key Rotated"), /* @__PURE__ */ React2.createElement(DialogContent, null, /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "text.secondary", gutterBottom: true }, "The old secret is now invalid. Copy the new one \u2014 you won't see it again."), /* @__PURE__ */ React2.createElement(
|
|
627
|
+
Box2,
|
|
628
|
+
{
|
|
629
|
+
display: "flex",
|
|
630
|
+
alignItems: "center",
|
|
631
|
+
gap: 1,
|
|
632
|
+
mt: 2,
|
|
633
|
+
p: 2,
|
|
634
|
+
sx: { backgroundColor: "action.hover", border: "1px solid", borderColor: "divider", borderRadius: 1 }
|
|
635
|
+
},
|
|
636
|
+
/* @__PURE__ */ React2.createElement(
|
|
637
|
+
Typography2,
|
|
638
|
+
{
|
|
639
|
+
component: "code",
|
|
640
|
+
sx: { fontFamily: "monospace", wordBreak: "break-all", flex: 1 }
|
|
641
|
+
},
|
|
642
|
+
rotatedKeyValue
|
|
643
|
+
),
|
|
644
|
+
/* @__PURE__ */ React2.createElement(IconButton, { onClick: () => copyToClipboard(rotatedKeyValue) }, /* @__PURE__ */ React2.createElement(ContentCopy, null))
|
|
645
|
+
)), /* @__PURE__ */ React2.createElement(DialogActions, null, /* @__PURE__ */ React2.createElement(Button, { onClick: () => setRotatedKeyValue(null), variant: "contained", color: "success" }, "Done"))), /* @__PURE__ */ React2.createElement(Dialog, { open: !!deleteConfirmId, onClose: () => setDeleteConfirmId(null), maxWidth: "xs", fullWidth: true }, /* @__PURE__ */ React2.createElement(DialogTitle, null, "Revoke Key?"), /* @__PURE__ */ React2.createElement(DialogContent, null, /* @__PURE__ */ React2.createElement(Typography2, null, "This will permanently revoke the key. Any integrations using it will stop working immediately.")), /* @__PURE__ */ React2.createElement(DialogActions, null, /* @__PURE__ */ React2.createElement(Button, { onClick: () => setDeleteConfirmId(null), disabled: deleteSubmitting }, "Cancel"), /* @__PURE__ */ React2.createElement(
|
|
646
|
+
Button,
|
|
647
|
+
{
|
|
648
|
+
onClick: handleConfirmDelete,
|
|
649
|
+
variant: "contained",
|
|
650
|
+
color: "error",
|
|
651
|
+
disabled: deleteSubmitting
|
|
652
|
+
},
|
|
653
|
+
deleteSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 20 }) : "Revoke"
|
|
654
|
+
))));
|
|
432
655
|
};
|
|
433
656
|
}
|
|
434
657
|
});
|
|
435
658
|
|
|
436
659
|
// src/components/UsageStats.tsx
|
|
437
|
-
import React3, { useMemo, useState as useState2 } from "react";
|
|
660
|
+
import React3, { useMemo as useMemo2, useState as useState2 } from "react";
|
|
438
661
|
import {
|
|
439
662
|
Paper as Paper3,
|
|
440
663
|
Box as Box3,
|
|
@@ -453,7 +676,7 @@ import {
|
|
|
453
676
|
TableHead as TableHead2,
|
|
454
677
|
TableRow as TableRow2,
|
|
455
678
|
Chip as Chip3,
|
|
456
|
-
LinearProgress as
|
|
679
|
+
LinearProgress as LinearProgress3,
|
|
457
680
|
Skeleton
|
|
458
681
|
} from "@mui/material";
|
|
459
682
|
import {
|
|
@@ -513,7 +736,7 @@ var init_UsageStats = __esm({
|
|
|
513
736
|
}) => {
|
|
514
737
|
const [selectedModel, setSelectedModel] = useState2("all");
|
|
515
738
|
const [tab, setTab] = useState2("costs");
|
|
516
|
-
const selectedPreset =
|
|
739
|
+
const selectedPreset = useMemo2(() => {
|
|
517
740
|
if (dateRange.start.toDateString() === dateRange.end.toDateString()) return "today";
|
|
518
741
|
const diffMs = dateRange.end.getTime() - dateRange.start.getTime();
|
|
519
742
|
const diffHours = diffMs / (1e3 * 60 * 60);
|
|
@@ -535,7 +758,7 @@ var init_UsageStats = __esm({
|
|
|
535
758
|
}
|
|
536
759
|
onDateRangeChange({ start, end });
|
|
537
760
|
};
|
|
538
|
-
const dailyData =
|
|
761
|
+
const dailyData = useMemo2(
|
|
539
762
|
() => (usage?.daily_usage ?? []).map((d) => ({
|
|
540
763
|
date: d.date,
|
|
541
764
|
spend: d.spend,
|
|
@@ -548,21 +771,21 @@ var init_UsageStats = __esm({
|
|
|
548
771
|
})),
|
|
549
772
|
[usage]
|
|
550
773
|
);
|
|
551
|
-
const cumulativeData =
|
|
774
|
+
const cumulativeData = useMemo2(() => {
|
|
552
775
|
let cum = 0;
|
|
553
776
|
return dailyData.map((d) => {
|
|
554
777
|
cum += d.spend;
|
|
555
778
|
return { date: d.date, cumulative: cum };
|
|
556
779
|
});
|
|
557
780
|
}, [dailyData]);
|
|
558
|
-
const successRateData =
|
|
781
|
+
const successRateData = useMemo2(
|
|
559
782
|
() => dailyData.filter((d) => d.apiRequests > 0).map((d) => ({
|
|
560
783
|
date: d.date,
|
|
561
784
|
successRate: parseFloat((d.successfulRequests / d.apiRequests * 100).toFixed(1))
|
|
562
785
|
})),
|
|
563
786
|
[dailyData]
|
|
564
787
|
);
|
|
565
|
-
const { modelSpendByDate, topModels: topSpendModels } =
|
|
788
|
+
const { modelSpendByDate, topModels: topSpendModels } = useMemo2(() => {
|
|
566
789
|
const rows = usage?.daily_by_model ?? [];
|
|
567
790
|
const modelTotals = {};
|
|
568
791
|
for (const r of rows) modelTotals[r.model] = (modelTotals[r.model] ?? 0) + r.spend;
|
|
@@ -577,7 +800,7 @@ var init_UsageStats = __esm({
|
|
|
577
800
|
const hasOther = sorted.some((r) => r.Other > 0);
|
|
578
801
|
return { modelSpendByDate: sorted, topModels: hasOther ? [...top, "Other"] : top };
|
|
579
802
|
}, [usage]);
|
|
580
|
-
const modelRows =
|
|
803
|
+
const modelRows = useMemo2(() => {
|
|
581
804
|
const entries = Object.entries(usage?.usage_by_model ?? {}).map(([model, d]) => ({
|
|
582
805
|
model,
|
|
583
806
|
spend: d.total_spend,
|
|
@@ -591,11 +814,11 @@ var init_UsageStats = __esm({
|
|
|
591
814
|
}));
|
|
592
815
|
return selectedModel === "all" ? entries : entries.filter((e) => e.model === selectedModel);
|
|
593
816
|
}, [usage, selectedModel]);
|
|
594
|
-
const topModelSpendBars =
|
|
817
|
+
const topModelSpendBars = useMemo2(
|
|
595
818
|
() => [...modelRows].sort((a, b) => b.spend - a.spend).slice(0, 10),
|
|
596
819
|
[modelRows]
|
|
597
820
|
);
|
|
598
|
-
const keyRows =
|
|
821
|
+
const keyRows = useMemo2(
|
|
599
822
|
() => Object.entries(usage?.usage_by_key ?? {}).map(([keyHash, d]) => ({
|
|
600
823
|
keyHash,
|
|
601
824
|
keyAlias: d.key_alias ?? keyHash.slice(0, 8),
|
|
@@ -612,7 +835,7 @@ var init_UsageStats = __esm({
|
|
|
612
835
|
})),
|
|
613
836
|
[usage]
|
|
614
837
|
);
|
|
615
|
-
const topKeySpendBars =
|
|
838
|
+
const topKeySpendBars = useMemo2(
|
|
616
839
|
() => [...keyRows].sort((a, b) => b.spend - a.spend).slice(0, 10),
|
|
617
840
|
[keyRows]
|
|
618
841
|
);
|
|
@@ -658,15 +881,15 @@ var init_UsageStats = __esm({
|
|
|
658
881
|
fill: modelColor(m),
|
|
659
882
|
fillOpacity: 0.6
|
|
660
883
|
}
|
|
661
|
-
)))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Token Usage"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : dailyData.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: 260 }, /* @__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: fmtInt }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [fmtInt(v), void 0] }), /* @__PURE__ */ React3.createElement(Legend, null), /* @__PURE__ */ React3.createElement(Area, { type: "monotone", dataKey: "promptTokens", name: "Input (prompt)", stackId: "tok", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.5 }), /* @__PURE__ */ React3.createElement(Area, { type: "monotone", dataKey: "completionTokens", name: "Output (completion)", stackId: "tok", stroke: "#82ca9d", fill: "#82ca9d", fillOpacity: 0.5 }))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Requests"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : dailyData.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: 260 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(BarChart, { 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 } }), /* @__PURE__ */ React3.createElement(Tooltip, null), /* @__PURE__ */ React3.createElement(Legend, null), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "successfulRequests", name: "Successful", fill: "#82ca9d", stackId: "r" }), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "failedRequests", name: "Failed", fill: "#e57373", stackId: "r" }))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Success Rate"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : successRateData.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: 260 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(LineChart, { data: successRateData }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "date", tick: { fontSize: 12 } }), /* @__PURE__ */ React3.createElement(YAxis, { domain: [0, 100], tick: { fontSize: 12 }, tickFormatter: (v) => `${v}%` }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [`${v}%`, "Success rate"] }), /* @__PURE__ */ React3.createElement(ReferenceLine, { y: 100, stroke: "#82ca9d", strokeDasharray: "4 2" }), /* @__PURE__ */ React3.createElement(Line, { type: "monotone", dataKey: "successRate", name: "Success rate", stroke: "#8884d8", dot: { r: 3 } }))))), (maxBudget > 0 || cumulativeData.length > 0) && /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Cumulative Spend", maxBudget > 0 ? ` vs Budget (${fmtUsd(maxBudget)})` : "", maxBudget > 0 && /* @__PURE__ */ React3.createElement(Typography3, { component: "span", variant: "caption", color: "text.secondary", sx: { ml: 1 } }, fmtUsd(totalCumSpend), " used \xB7 ", fmtUsd(Math.max(0, maxBudget - totalCumSpend)), " remaining")), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, { height: 180 }) : cumulativeData.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, { height: 180 }) : /* @__PURE__ */ React3.createElement(Box3, { height: 180 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(AreaChart, { data: cumulativeData }, /* @__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: (v) => [`$${v.toFixed(4)}`, "Cumulative spend"] }), maxBudget > 0 && /* @__PURE__ */ React3.createElement(ReferenceLine, { y: maxBudget, stroke: "#e57373", strokeDasharray: "6 3", label: { value: `Budget ${fmtUsd(maxBudget)}`, position: "insideTopRight", fontSize: 11 } }), /* @__PURE__ */ React3.createElement(Area, { type: "monotone", dataKey: "cumulative", name: "Cumulative spend", stroke: "#ffc658", fill: "#ffc658", fillOpacity: 0.3 })))))), tab === "models" && /* @__PURE__ */ React3.createElement(Grid, { container: true, spacing: 3 }, /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Spend by Model"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : topModelSpendBars.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: Math.max(200, topModelSpendBars.length * 36) }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(BarChart, { data: topModelSpendBars, layout: "vertical", margin: { left: 20 } }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ React3.createElement(XAxis, { type: "number", tick: { fontSize: 12 }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React3.createElement(YAxis, { type: "category", dataKey: "model", tick: { fontSize: 11 }, width: 200 }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [fmtUsd(v), "Spend"] }), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "spend", name: "Spend", radius: [0, 4, 4, 0] }, topModelSpendBars.map((r) => /* @__PURE__ */ React3.createElement("rect", { key: r.model, fill: modelColor(r.model) }))))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Tokens by Model"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : modelRows.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: 260 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(BarChart, { data: modelRows }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "model", tick: { fontSize: 10 }, interval: 0, angle: -15, textAnchor: "end", height: 60 }), /* @__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", fill: "#8884d8", stackId: "t" }), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "completionTokens", name: "Completion", fill: "#82ca9d", stackId: "t" }))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(TableContainer2, { component: Paper3, variant: "outlined" }, /* @__PURE__ */ React3.createElement(Table2, { size: "small" }, /* @__PURE__ */ React3.createElement(TableHead2, null, /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, null, "Model"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Spend"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Requests"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Success"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Failed"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Prompt"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Completion"), /* @__PURE__ */ React3.createElement(TableCell2, { sx: { minWidth: 120 } }, "Success rate"))), /* @__PURE__ */ React3.createElement(TableBody2, null, loading ? /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, { colSpan: 8 }, /* @__PURE__ */ React3.createElement(LinearProgress2, null))) : modelRows.length === 0 ? /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, { colSpan: 8, align: "center" }, "No model activity")) : [...modelRows].sort((a, b) => b.spend - a.spend || b.totalTokens - a.totalTokens).map((r) => /* @__PURE__ */ React3.createElement(TableRow2, { key: r.model }, /* @__PURE__ */ React3.createElement(TableCell2, null, /* @__PURE__ */ React3.createElement(Box3, { display: "flex", alignItems: "center", gap: 0.75 }, /* @__PURE__ */ React3.createElement(Box3, { sx: { width: 10, height: 10, borderRadius: "50%", bgcolor: modelColor(r.model), flexShrink: 0 } }), r.model)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtUsd(r.spend)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.apiRequests)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.successfulRequests)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.failedRequests)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.promptTokens)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.completionTokens)), /* @__PURE__ */ React3.createElement(TableCell2, null, r.apiRequests > 0 ? /* @__PURE__ */ React3.createElement(Box3, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React3.createElement(
|
|
662
|
-
|
|
884
|
+
)))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Token Usage"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : dailyData.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: 260 }, /* @__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: fmtInt }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [fmtInt(v), void 0] }), /* @__PURE__ */ React3.createElement(Legend, null), /* @__PURE__ */ React3.createElement(Area, { type: "monotone", dataKey: "promptTokens", name: "Input (prompt)", stackId: "tok", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.5 }), /* @__PURE__ */ React3.createElement(Area, { type: "monotone", dataKey: "completionTokens", name: "Output (completion)", stackId: "tok", stroke: "#82ca9d", fill: "#82ca9d", fillOpacity: 0.5 }))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Requests"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : dailyData.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: 260 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(BarChart, { 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 } }), /* @__PURE__ */ React3.createElement(Tooltip, null), /* @__PURE__ */ React3.createElement(Legend, null), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "successfulRequests", name: "Successful", fill: "#82ca9d", stackId: "r" }), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "failedRequests", name: "Failed", fill: "#e57373", stackId: "r" }))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Success Rate"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : successRateData.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: 260 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(LineChart, { data: successRateData }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "date", tick: { fontSize: 12 } }), /* @__PURE__ */ React3.createElement(YAxis, { domain: [0, 100], tick: { fontSize: 12 }, tickFormatter: (v) => `${v}%` }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [`${v}%`, "Success rate"] }), /* @__PURE__ */ React3.createElement(ReferenceLine, { y: 100, stroke: "#82ca9d", strokeDasharray: "4 2" }), /* @__PURE__ */ React3.createElement(Line, { type: "monotone", dataKey: "successRate", name: "Success rate", stroke: "#8884d8", dot: { r: 3 } }))))), (maxBudget > 0 || cumulativeData.length > 0) && /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Cumulative Spend", maxBudget > 0 ? ` vs Budget (${fmtUsd(maxBudget)})` : "", maxBudget > 0 && /* @__PURE__ */ React3.createElement(Typography3, { component: "span", variant: "caption", color: "text.secondary", sx: { ml: 1 } }, fmtUsd(totalCumSpend), " used \xB7 ", fmtUsd(Math.max(0, maxBudget - totalCumSpend)), " remaining")), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, { height: 180 }) : cumulativeData.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, { height: 180 }) : /* @__PURE__ */ React3.createElement(Box3, { height: 180 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(AreaChart, { data: cumulativeData }, /* @__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: (v) => [`$${v.toFixed(4)}`, "Cumulative spend"] }), maxBudget > 0 && /* @__PURE__ */ React3.createElement(ReferenceLine, { y: maxBudget, stroke: "#e57373", strokeDasharray: "6 3", label: { value: `Budget ${fmtUsd(maxBudget)}`, position: "insideTopRight", fontSize: 11 } }), /* @__PURE__ */ React3.createElement(Area, { type: "monotone", dataKey: "cumulative", name: "Cumulative spend", stroke: "#ffc658", fill: "#ffc658", fillOpacity: 0.3 })))))), tab === "models" && /* @__PURE__ */ React3.createElement(Grid, { container: true, spacing: 3 }, /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Spend by Model"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : topModelSpendBars.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: Math.max(200, topModelSpendBars.length * 36) }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(BarChart, { data: topModelSpendBars, layout: "vertical", margin: { left: 20 } }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ React3.createElement(XAxis, { type: "number", tick: { fontSize: 12 }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React3.createElement(YAxis, { type: "category", dataKey: "model", tick: { fontSize: 11 }, width: 200 }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [fmtUsd(v), "Spend"] }), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "spend", name: "Spend", radius: [0, 4, 4, 0] }, topModelSpendBars.map((r) => /* @__PURE__ */ React3.createElement("rect", { key: r.model, fill: modelColor(r.model) }))))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Tokens by Model"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : modelRows.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: 260 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(BarChart, { data: modelRows }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "model", tick: { fontSize: 10 }, interval: 0, angle: -15, textAnchor: "end", height: 60 }), /* @__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", fill: "#8884d8", stackId: "t" }), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "completionTokens", name: "Completion", fill: "#82ca9d", stackId: "t" }))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(TableContainer2, { component: Paper3, variant: "outlined" }, /* @__PURE__ */ React3.createElement(Table2, { size: "small" }, /* @__PURE__ */ React3.createElement(TableHead2, null, /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, null, "Model"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Spend"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Requests"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Success"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Failed"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Prompt"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Completion"), /* @__PURE__ */ React3.createElement(TableCell2, { sx: { minWidth: 120 } }, "Success rate"))), /* @__PURE__ */ React3.createElement(TableBody2, null, loading ? /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, { colSpan: 8 }, /* @__PURE__ */ React3.createElement(LinearProgress3, null))) : modelRows.length === 0 ? /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, { colSpan: 8, align: "center" }, "No model activity")) : [...modelRows].sort((a, b) => b.spend - a.spend || b.totalTokens - a.totalTokens).map((r) => /* @__PURE__ */ React3.createElement(TableRow2, { key: r.model }, /* @__PURE__ */ React3.createElement(TableCell2, null, /* @__PURE__ */ React3.createElement(Box3, { display: "flex", alignItems: "center", gap: 0.75 }, /* @__PURE__ */ React3.createElement(Box3, { sx: { width: 10, height: 10, borderRadius: "50%", bgcolor: modelColor(r.model), flexShrink: 0 } }), r.model)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtUsd(r.spend)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.apiRequests)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.successfulRequests)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.failedRequests)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.promptTokens)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.completionTokens)), /* @__PURE__ */ React3.createElement(TableCell2, null, r.apiRequests > 0 ? /* @__PURE__ */ React3.createElement(Box3, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React3.createElement(
|
|
885
|
+
LinearProgress3,
|
|
663
886
|
{
|
|
664
887
|
variant: "determinate",
|
|
665
888
|
value: r.successRate * 100,
|
|
666
889
|
sx: { flex: 1, height: 6, borderRadius: 3 }
|
|
667
890
|
}
|
|
668
|
-
), /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption" }, fmtPct(r.successRate))) : "\u2014")))))))), tab === "keys" && /* @__PURE__ */ React3.createElement(Grid, { container: true, spacing: 3 }, (loading || topKeySpendBars.length > 0) && /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Spend by Key"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : /* @__PURE__ */ React3.createElement(Box3, { height: Math.max(160, topKeySpendBars.length * 36) }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(BarChart, { data: topKeySpendBars, layout: "vertical", margin: { left: 20 } }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ React3.createElement(XAxis, { type: "number", tick: { fontSize: 12 }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React3.createElement(YAxis, { type: "category", dataKey: "keyAlias", tick: { fontSize: 11 }, width: 160 }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [fmtUsd(v), "Spend"] }), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "spend", name: "Spend", fill: "#8884d8", radius: [0, 4, 4, 0] }))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(TableContainer2, { component: Paper3, variant: "outlined" }, /* @__PURE__ */ React3.createElement(Table2, { size: "small" }, /* @__PURE__ */ React3.createElement(TableHead2, null, /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, null, "Key"), /* @__PURE__ */ React3.createElement(TableCell2, null, "Models"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Spend"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Requests"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Tokens"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Failed"), /* @__PURE__ */ React3.createElement(TableCell2, { sx: { minWidth: 120 } }, "Success rate"))), /* @__PURE__ */ React3.createElement(TableBody2, null, loading ? /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, { colSpan: 7 }, /* @__PURE__ */ React3.createElement(
|
|
669
|
-
|
|
891
|
+
), /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption" }, fmtPct(r.successRate))) : "\u2014")))))))), tab === "keys" && /* @__PURE__ */ React3.createElement(Grid, { container: true, spacing: 3 }, (loading || topKeySpendBars.length > 0) && /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Spend by Key"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : /* @__PURE__ */ React3.createElement(Box3, { height: Math.max(160, topKeySpendBars.length * 36) }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(BarChart, { data: topKeySpendBars, layout: "vertical", margin: { left: 20 } }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ React3.createElement(XAxis, { type: "number", tick: { fontSize: 12 }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React3.createElement(YAxis, { type: "category", dataKey: "keyAlias", tick: { fontSize: 11 }, width: 160 }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [fmtUsd(v), "Spend"] }), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "spend", name: "Spend", fill: "#8884d8", radius: [0, 4, 4, 0] }))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(TableContainer2, { component: Paper3, variant: "outlined" }, /* @__PURE__ */ React3.createElement(Table2, { size: "small" }, /* @__PURE__ */ React3.createElement(TableHead2, null, /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, null, "Key"), /* @__PURE__ */ React3.createElement(TableCell2, null, "Models"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Spend"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Requests"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Tokens"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Failed"), /* @__PURE__ */ React3.createElement(TableCell2, { sx: { minWidth: 120 } }, "Success rate"))), /* @__PURE__ */ React3.createElement(TableBody2, null, loading ? /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, { colSpan: 7 }, /* @__PURE__ */ React3.createElement(LinearProgress3, null))) : keyRows.length === 0 ? /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, { colSpan: 7, align: "center" }, "No key activity")) : [...keyRows].sort((a, b) => b.spend - a.spend || b.apiRequests - a.apiRequests).map((r) => /* @__PURE__ */ React3.createElement(TableRow2, { key: r.keyHash }, /* @__PURE__ */ React3.createElement(TableCell2, null, /* @__PURE__ */ React3.createElement(Typography3, { variant: "body2" }, r.keyAlias), r.teamId ? /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption", color: "text.secondary" }, "team: ", r.teamId) : null), /* @__PURE__ */ React3.createElement(TableCell2, null, /* @__PURE__ */ React3.createElement(Box3, { display: "flex", gap: 0.5, flexWrap: "wrap" }, r.models.map((m) => /* @__PURE__ */ React3.createElement(Chip3, { key: m, label: m, size: "small", variant: "outlined" })))), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtUsd(r.spend)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.apiRequests)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.totalTokens)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.failedRequests)), /* @__PURE__ */ React3.createElement(TableCell2, null, r.apiRequests > 0 ? /* @__PURE__ */ React3.createElement(Box3, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React3.createElement(
|
|
892
|
+
LinearProgress3,
|
|
670
893
|
{
|
|
671
894
|
variant: "determinate",
|
|
672
895
|
value: r.successRate * 100,
|
|
@@ -683,7 +906,7 @@ import {
|
|
|
683
906
|
Paper as Paper4,
|
|
684
907
|
Box as Box4,
|
|
685
908
|
Typography as Typography4,
|
|
686
|
-
LinearProgress as
|
|
909
|
+
LinearProgress as LinearProgress4,
|
|
687
910
|
Chip as Chip4,
|
|
688
911
|
Table as Table3,
|
|
689
912
|
TableBody as TableBody3,
|
|
@@ -718,7 +941,7 @@ var init_TeamUsage = __esm({
|
|
|
718
941
|
const isNear = budget > 0 && spend >= budget * 0.8 && !isOver;
|
|
719
942
|
const dailyData = usage?.daily_usage?.map((d) => ({ date: d.date, spend: d.spend })) ?? [];
|
|
720
943
|
return /* @__PURE__ */ React4.createElement(Paper4, { variant: "outlined", sx: { p: 2, mb: 2 } }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React4.createElement(Group, { color: "action" }), /* @__PURE__ */ React4.createElement(Box4, { flexGrow: 1 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle1", fontWeight: 600 }, team.team_alias ?? team.team_id), team.models?.length ? /* @__PURE__ */ React4.createElement(Box4, { display: "flex", gap: 0.5, flexWrap: "wrap", mt: 0.5 }, team.models.map((m) => /* @__PURE__ */ React4.createElement(Chip4, { key: m, label: m, size: "small", variant: "outlined" }))) : /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption", color: "text.secondary" }, "All models")), /* @__PURE__ */ React4.createElement(IconButton2, { size: "small", onClick: () => setExpanded((e) => !e) }, expanded ? /* @__PURE__ */ React4.createElement(ExpandLess, null) : /* @__PURE__ */ React4.createElement(ExpandMore, null))), budget > 0 && /* @__PURE__ */ React4.createElement(Box4, { mt: 1.5 }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", justifyContent: "space-between", mb: 0.5 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "body2" }, "$", spend.toFixed(2), " / $", budget.toFixed(2)), isOver && /* @__PURE__ */ React4.createElement(Chip4, { label: "Over Budget", size: "small", color: "error" }), isNear && /* @__PURE__ */ React4.createElement(Chip4, { label: "Near Limit", size: "small", color: "warning" })), /* @__PURE__ */ React4.createElement(
|
|
721
|
-
|
|
944
|
+
LinearProgress4,
|
|
722
945
|
{
|
|
723
946
|
variant: "determinate",
|
|
724
947
|
value: budgetPct,
|
|
@@ -741,7 +964,7 @@ var init_TeamUsage = __esm({
|
|
|
741
964
|
getTeamUsageLoading
|
|
742
965
|
}) => {
|
|
743
966
|
if (loading) {
|
|
744
|
-
return /* @__PURE__ */ React4.createElement(Paper4, { sx: { p: 2 } }, /* @__PURE__ */ React4.createElement(
|
|
967
|
+
return /* @__PURE__ */ React4.createElement(Paper4, { sx: { p: 2 } }, /* @__PURE__ */ React4.createElement(LinearProgress4, null));
|
|
745
968
|
}
|
|
746
969
|
if (!teams.length) {
|
|
747
970
|
return /* @__PURE__ */ React4.createElement(Paper4, { sx: { p: 2 } }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React4.createElement(Group, { color: "disabled" }), /* @__PURE__ */ React4.createElement(Typography4, { color: "text.secondary", variant: "body2" }, "No team membership found in LiteLLM for this account.")));
|
|
@@ -759,14 +982,147 @@ var init_TeamUsage = __esm({
|
|
|
759
982
|
}
|
|
760
983
|
});
|
|
761
984
|
|
|
985
|
+
// src/components/AuditLog.tsx
|
|
986
|
+
import React5, { useState as useState4, useCallback } from "react";
|
|
987
|
+
import {
|
|
988
|
+
Paper as Paper5,
|
|
989
|
+
Box as Box5,
|
|
990
|
+
Typography as Typography5,
|
|
991
|
+
Table as Table4,
|
|
992
|
+
TableBody as TableBody4,
|
|
993
|
+
TableCell as TableCell4,
|
|
994
|
+
TableContainer as TableContainer4,
|
|
995
|
+
TableHead as TableHead4,
|
|
996
|
+
TableRow as TableRow4,
|
|
997
|
+
TablePagination,
|
|
998
|
+
TextField as TextField2,
|
|
999
|
+
MenuItem as MenuItem3,
|
|
1000
|
+
Chip as Chip5,
|
|
1001
|
+
CircularProgress as CircularProgress3,
|
|
1002
|
+
Collapse as Collapse2,
|
|
1003
|
+
IconButton as IconButton3,
|
|
1004
|
+
Alert
|
|
1005
|
+
} from "@mui/material";
|
|
1006
|
+
import { KeyboardArrowDown, KeyboardArrowUp } from "@mui/icons-material";
|
|
1007
|
+
import { useAsync } from "react-use";
|
|
1008
|
+
function actionColor(action) {
|
|
1009
|
+
if (!action) return "default";
|
|
1010
|
+
for (const [key, color] of Object.entries(ACTION_COLORS)) {
|
|
1011
|
+
if (action.toLowerCase().includes(key)) return color;
|
|
1012
|
+
}
|
|
1013
|
+
return "info";
|
|
1014
|
+
}
|
|
1015
|
+
function formatDateTime(iso) {
|
|
1016
|
+
try {
|
|
1017
|
+
return new Date(iso).toLocaleString();
|
|
1018
|
+
} catch {
|
|
1019
|
+
return iso;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
var ACTION_COLORS, DetailRow, AuditLog;
|
|
1023
|
+
var init_AuditLog = __esm({
|
|
1024
|
+
"src/components/AuditLog.tsx"() {
|
|
1025
|
+
"use strict";
|
|
1026
|
+
ACTION_COLORS = {
|
|
1027
|
+
created: "success",
|
|
1028
|
+
deleted: "error",
|
|
1029
|
+
updated: "warning",
|
|
1030
|
+
blocked: "error",
|
|
1031
|
+
unblocked: "success"
|
|
1032
|
+
};
|
|
1033
|
+
DetailRow = ({ entry }) => {
|
|
1034
|
+
const [open, setOpen] = useState4(false);
|
|
1035
|
+
const hasDetail = entry.before_value || entry.updated_values;
|
|
1036
|
+
return /* @__PURE__ */ React5.createElement(React5.Fragment, null, /* @__PURE__ */ React5.createElement(TableRow4, { hover: true }, /* @__PURE__ */ React5.createElement(TableCell4, { sx: { width: 40, pr: 0 } }, hasDetail && /* @__PURE__ */ React5.createElement(IconButton3, { size: "small", onClick: () => setOpen((o) => !o) }, open ? /* @__PURE__ */ React5.createElement(KeyboardArrowUp, { fontSize: "small" }) : /* @__PURE__ */ React5.createElement(KeyboardArrowDown, { fontSize: "small" }))), /* @__PURE__ */ React5.createElement(TableCell4, { sx: { whiteSpace: "nowrap" } }, formatDateTime(entry.updated_at)), /* @__PURE__ */ React5.createElement(TableCell4, null, entry.action && /* @__PURE__ */ React5.createElement(Chip5, { label: entry.action, color: actionColor(entry.action), size: "small" })), /* @__PURE__ */ React5.createElement(TableCell4, null, entry.table_name ?? "-"), /* @__PURE__ */ React5.createElement(TableCell4, null, /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2", component: "code", sx: { fontFamily: "monospace", fontSize: "0.75rem" } }, entry.object_id ? entry.object_id.slice(0, 20) + (entry.object_id.length > 20 ? "\u2026" : "") : "-")), /* @__PURE__ */ React5.createElement(TableCell4, null, entry.changed_by ?? "-")), hasDetail && /* @__PURE__ */ React5.createElement(TableRow4, null, /* @__PURE__ */ React5.createElement(TableCell4, { colSpan: 6, sx: { py: 0 } }, /* @__PURE__ */ React5.createElement(Collapse2, { in: open, unmountOnExit: true }, /* @__PURE__ */ React5.createElement(Box5, { p: 2, display: "flex", gap: 2, flexWrap: "wrap" }, entry.before_value && /* @__PURE__ */ React5.createElement(Box5, { flex: 1, minWidth: 200 }, /* @__PURE__ */ React5.createElement(Typography5, { variant: "caption", color: "text.secondary", display: "block", mb: 0.5 }, "Before"), /* @__PURE__ */ React5.createElement(Typography5, { component: "pre", variant: "caption", sx: { fontFamily: "monospace", whiteSpace: "pre-wrap", wordBreak: "break-all" } }, JSON.stringify(entry.before_value, null, 2))), entry.updated_values && /* @__PURE__ */ React5.createElement(Box5, { flex: 1, minWidth: 200 }, /* @__PURE__ */ React5.createElement(Typography5, { variant: "caption", color: "text.secondary", display: "block", mb: 0.5 }, "After"), /* @__PURE__ */ React5.createElement(Typography5, { component: "pre", variant: "caption", sx: { fontFamily: "monospace", whiteSpace: "pre-wrap", wordBreak: "break-all" } }, JSON.stringify(entry.updated_values, null, 2))))))));
|
|
1037
|
+
};
|
|
1038
|
+
AuditLog = ({ api }) => {
|
|
1039
|
+
const [page, setPage] = useState4(0);
|
|
1040
|
+
const [pageSize, setPageSize] = useState4(25);
|
|
1041
|
+
const [filters, setFilters] = useState4({});
|
|
1042
|
+
const fetchParams = useCallback(
|
|
1043
|
+
() => ({ page: page + 1, page_size: pageSize, ...filters }),
|
|
1044
|
+
[page, pageSize, filters]
|
|
1045
|
+
)();
|
|
1046
|
+
const { value, loading, error } = useAsync(
|
|
1047
|
+
() => api.getAuditLogs(fetchParams),
|
|
1048
|
+
[api, fetchParams]
|
|
1049
|
+
);
|
|
1050
|
+
const entries = value?.audit_logs ?? [];
|
|
1051
|
+
const total = value?.total ?? 0;
|
|
1052
|
+
return /* @__PURE__ */ React5.createElement(Paper5, null, /* @__PURE__ */ React5.createElement(Box5, { p: 2, display: "flex", gap: 2, flexWrap: "wrap", alignItems: "center" }, /* @__PURE__ */ React5.createElement(Typography5, { variant: "h6", sx: { flex: "0 0 auto", mr: 1 } }, "Audit Log"), /* @__PURE__ */ React5.createElement(
|
|
1053
|
+
TextField2,
|
|
1054
|
+
{
|
|
1055
|
+
size: "small",
|
|
1056
|
+
label: "Action",
|
|
1057
|
+
select: true,
|
|
1058
|
+
value: filters.action ?? "",
|
|
1059
|
+
onChange: (e) => {
|
|
1060
|
+
setFilters((f) => ({ ...f, action: e.target.value || void 0 }));
|
|
1061
|
+
setPage(0);
|
|
1062
|
+
},
|
|
1063
|
+
sx: { minWidth: 160 }
|
|
1064
|
+
},
|
|
1065
|
+
/* @__PURE__ */ React5.createElement(MenuItem3, { value: "" }, "All actions"),
|
|
1066
|
+
/* @__PURE__ */ React5.createElement(MenuItem3, { value: "created" }, "Created"),
|
|
1067
|
+
/* @__PURE__ */ React5.createElement(MenuItem3, { value: "updated" }, "Updated"),
|
|
1068
|
+
/* @__PURE__ */ React5.createElement(MenuItem3, { value: "deleted" }, "Deleted"),
|
|
1069
|
+
/* @__PURE__ */ React5.createElement(MenuItem3, { value: "blocked" }, "Blocked")
|
|
1070
|
+
), /* @__PURE__ */ React5.createElement(
|
|
1071
|
+
TextField2,
|
|
1072
|
+
{
|
|
1073
|
+
size: "small",
|
|
1074
|
+
label: "Table",
|
|
1075
|
+
select: true,
|
|
1076
|
+
value: filters.table_name ?? "",
|
|
1077
|
+
onChange: (e) => {
|
|
1078
|
+
setFilters((f) => ({ ...f, table_name: e.target.value || void 0 }));
|
|
1079
|
+
setPage(0);
|
|
1080
|
+
},
|
|
1081
|
+
sx: { minWidth: 160 }
|
|
1082
|
+
},
|
|
1083
|
+
/* @__PURE__ */ React5.createElement(MenuItem3, { value: "" }, "All tables"),
|
|
1084
|
+
/* @__PURE__ */ React5.createElement(MenuItem3, { value: "LiteLLM_VerificationToken" }, "Key"),
|
|
1085
|
+
/* @__PURE__ */ React5.createElement(MenuItem3, { value: "LiteLLM_TeamTable" }, "Team"),
|
|
1086
|
+
/* @__PURE__ */ React5.createElement(MenuItem3, { value: "LiteLLM_UserTable" }, "User")
|
|
1087
|
+
), /* @__PURE__ */ React5.createElement(
|
|
1088
|
+
TextField2,
|
|
1089
|
+
{
|
|
1090
|
+
size: "small",
|
|
1091
|
+
label: "Changed by",
|
|
1092
|
+
value: filters.changed_by ?? "",
|
|
1093
|
+
onChange: (e) => {
|
|
1094
|
+
setFilters((f) => ({ ...f, changed_by: e.target.value || void 0 }));
|
|
1095
|
+
setPage(0);
|
|
1096
|
+
},
|
|
1097
|
+
sx: { minWidth: 200 }
|
|
1098
|
+
}
|
|
1099
|
+
)), error && /* @__PURE__ */ React5.createElement(Box5, { px: 2, pb: 2 }, /* @__PURE__ */ React5.createElement(Alert, { severity: "error" }, error.message)), /* @__PURE__ */ React5.createElement(TableContainer4, null, /* @__PURE__ */ React5.createElement(Table4, { size: "small" }, /* @__PURE__ */ React5.createElement(TableHead4, null, /* @__PURE__ */ React5.createElement(TableRow4, null, /* @__PURE__ */ React5.createElement(TableCell4, { sx: { width: 40 } }), /* @__PURE__ */ React5.createElement(TableCell4, null, "Time"), /* @__PURE__ */ React5.createElement(TableCell4, null, "Action"), /* @__PURE__ */ React5.createElement(TableCell4, null, "Table"), /* @__PURE__ */ React5.createElement(TableCell4, null, "Object ID"), /* @__PURE__ */ React5.createElement(TableCell4, null, "Changed By"))), /* @__PURE__ */ React5.createElement(TableBody4, null, loading ? /* @__PURE__ */ React5.createElement(TableRow4, null, /* @__PURE__ */ React5.createElement(TableCell4, { colSpan: 6, align: "center", sx: { py: 4 } }, /* @__PURE__ */ React5.createElement(CircularProgress3, { size: 24 }))) : entries.length === 0 ? /* @__PURE__ */ React5.createElement(TableRow4, null, /* @__PURE__ */ React5.createElement(TableCell4, { colSpan: 6, align: "center", sx: { py: 4 } }, /* @__PURE__ */ React5.createElement(Typography5, { color: "text.secondary" }, "No audit events found"))) : entries.map((entry) => /* @__PURE__ */ React5.createElement(DetailRow, { key: entry.id, entry }))))), /* @__PURE__ */ React5.createElement(
|
|
1100
|
+
TablePagination,
|
|
1101
|
+
{
|
|
1102
|
+
component: "div",
|
|
1103
|
+
count: total,
|
|
1104
|
+
page,
|
|
1105
|
+
onPageChange: (_, p) => setPage(p),
|
|
1106
|
+
rowsPerPage: pageSize,
|
|
1107
|
+
onRowsPerPageChange: (e) => {
|
|
1108
|
+
setPageSize(Number(e.target.value));
|
|
1109
|
+
setPage(0);
|
|
1110
|
+
},
|
|
1111
|
+
rowsPerPageOptions: [10, 25, 50]
|
|
1112
|
+
}
|
|
1113
|
+
));
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
|
|
762
1118
|
// src/components/LiteLLMPage.tsx
|
|
763
1119
|
var LiteLLMPage_exports = {};
|
|
764
1120
|
__export(LiteLLMPage_exports, {
|
|
765
1121
|
LiteLLMPage: () => LiteLLMPage
|
|
766
1122
|
});
|
|
767
|
-
import
|
|
768
|
-
import { Grid as Grid2, Box as
|
|
769
|
-
import { useAsync, useAsyncRetry } from "react-use";
|
|
1123
|
+
import React6, { useState as useState5, useCallback as useCallback2, useMemo as useMemo3 } from "react";
|
|
1124
|
+
import { Grid as Grid2, Box as Box6, Snackbar, Alert as Alert2, CircularProgress as CircularProgress4, Typography as Typography6, Paper as Paper6, Tabs as Tabs2, Tab as Tab2 } from "@mui/material";
|
|
1125
|
+
import { useAsync as useAsync2, useAsyncRetry } from "react-use";
|
|
770
1126
|
import { useApi } from "@backstage/core-plugin-api";
|
|
771
1127
|
function initDateRange() {
|
|
772
1128
|
let preset = "7d";
|
|
@@ -790,15 +1146,17 @@ var init_LiteLLMPage = __esm({
|
|
|
790
1146
|
init_KeysTable();
|
|
791
1147
|
init_UsageStats();
|
|
792
1148
|
init_TeamUsage();
|
|
1149
|
+
init_AuditLog();
|
|
793
1150
|
init_api();
|
|
794
1151
|
PERIOD_LS_KEY2 = "litellm_usage_period";
|
|
795
1152
|
LiteLLMPage = () => {
|
|
796
1153
|
const api = useApi(liteLlmApiRef);
|
|
797
|
-
const [dateRange, setDateRange] =
|
|
798
|
-
const [
|
|
799
|
-
const [
|
|
800
|
-
const [
|
|
801
|
-
const
|
|
1154
|
+
const [dateRange, setDateRange] = useState5(initDateRange);
|
|
1155
|
+
const [activeTab, setActiveTab] = useState5("overview");
|
|
1156
|
+
const [snackbar, setSnackbar] = useState5(null);
|
|
1157
|
+
const [teamUsageCache, setTeamUsageCache] = useState5({});
|
|
1158
|
+
const [teamUsageLoading, setTeamUsageLoading] = useState5({});
|
|
1159
|
+
const { value: userInfo, loading: userLoading, error: userError } = useAsync2(
|
|
802
1160
|
() => api.getUserInfo(),
|
|
803
1161
|
[api]
|
|
804
1162
|
);
|
|
@@ -813,15 +1171,15 @@ var init_LiteLLMPage = __esm({
|
|
|
813
1171
|
},
|
|
814
1172
|
[api]
|
|
815
1173
|
);
|
|
816
|
-
const { value: allModels, loading: modelsLoading } =
|
|
1174
|
+
const { value: allModels, loading: modelsLoading } = useAsync2(
|
|
817
1175
|
() => api.listModels().catch(() => []),
|
|
818
1176
|
[api]
|
|
819
1177
|
);
|
|
820
|
-
const { value: allTeams, loading: teamsLoading } =
|
|
1178
|
+
const { value: allTeams, loading: teamsLoading } = useAsync2(
|
|
821
1179
|
() => api.getTeams().catch(() => []),
|
|
822
1180
|
[api]
|
|
823
1181
|
);
|
|
824
|
-
const teams =
|
|
1182
|
+
const teams = useMemo3(() => {
|
|
825
1183
|
if (!allTeams?.length) return [];
|
|
826
1184
|
if (!userInfo) return allTeams;
|
|
827
1185
|
const userId = userInfo.user_id;
|
|
@@ -833,12 +1191,17 @@ var init_LiteLLMPage = __esm({
|
|
|
833
1191
|
);
|
|
834
1192
|
return byMembership.length > 0 ? byMembership : allTeams;
|
|
835
1193
|
}, [allTeams, userInfo]);
|
|
836
|
-
const { value: usage, loading: usageLoading } =
|
|
1194
|
+
const { value: usage, loading: usageLoading } = useAsync2(async () => {
|
|
837
1195
|
const startDate = dateRange.start.toISOString().split("T")[0];
|
|
838
1196
|
const endDate = dateRange.end.toISOString().split("T")[0];
|
|
839
1197
|
return api.getUsage(startDate, endDate);
|
|
840
1198
|
}, [api, dateRange]);
|
|
841
|
-
const
|
|
1199
|
+
const handleDateRangeChange = useCallback2((range) => {
|
|
1200
|
+
setDateRange(range);
|
|
1201
|
+
setTeamUsageCache({});
|
|
1202
|
+
setTeamUsageLoading({});
|
|
1203
|
+
}, [setDateRange]);
|
|
1204
|
+
const loadTeamUsage = useCallback2(async (teamId) => {
|
|
842
1205
|
if (teamUsageCache[teamId] !== void 0 || teamUsageLoading[teamId]) return;
|
|
843
1206
|
setTeamUsageLoading((prev) => ({ ...prev, [teamId]: true }));
|
|
844
1207
|
try {
|
|
@@ -852,7 +1215,7 @@ var init_LiteLLMPage = __esm({
|
|
|
852
1215
|
setTeamUsageLoading((prev) => ({ ...prev, [teamId]: false }));
|
|
853
1216
|
}
|
|
854
1217
|
}, [api, dateRange, teamUsageCache, teamUsageLoading]);
|
|
855
|
-
const allowedModels =
|
|
1218
|
+
const allowedModels = useMemo3(() => {
|
|
856
1219
|
if (!allModels?.length) return [];
|
|
857
1220
|
const userModels = userInfo?.models;
|
|
858
1221
|
const teamModels = teams?.flatMap((t) => t.models ?? []);
|
|
@@ -865,7 +1228,7 @@ var init_LiteLLMPage = __esm({
|
|
|
865
1228
|
]);
|
|
866
1229
|
return allModels.filter((m) => allowed.has(m.model_name));
|
|
867
1230
|
}, [allModels, userInfo, teams]);
|
|
868
|
-
const handleGenerateKey =
|
|
1231
|
+
const handleGenerateKey = useCallback2(
|
|
869
1232
|
async (request) => {
|
|
870
1233
|
const response = await api.generateKey(request);
|
|
871
1234
|
setSnackbar({ message: "Key generated successfully", severity: "success" });
|
|
@@ -874,7 +1237,7 @@ var init_LiteLLMPage = __esm({
|
|
|
874
1237
|
},
|
|
875
1238
|
[api, refreshKeys]
|
|
876
1239
|
);
|
|
877
|
-
const handleUpdateKey =
|
|
1240
|
+
const handleUpdateKey = useCallback2(
|
|
878
1241
|
async (keyId, request) => {
|
|
879
1242
|
try {
|
|
880
1243
|
await api.updateKey(keyId, request);
|
|
@@ -887,44 +1250,87 @@ var init_LiteLLMPage = __esm({
|
|
|
887
1250
|
},
|
|
888
1251
|
[api, refreshKeys]
|
|
889
1252
|
);
|
|
890
|
-
const
|
|
1253
|
+
const handleBlockKey = useCallback2(
|
|
1254
|
+
async (keyId) => {
|
|
1255
|
+
try {
|
|
1256
|
+
await api.blockKey(keyId);
|
|
1257
|
+
setSnackbar({ message: "Key blocked \u2014 requests will be rejected until unblocked", severity: "warning" });
|
|
1258
|
+
refreshKeys();
|
|
1259
|
+
} catch (e) {
|
|
1260
|
+
setSnackbar({ message: `Failed to block key: ${e.message}`, severity: "error" });
|
|
1261
|
+
}
|
|
1262
|
+
},
|
|
1263
|
+
[api, refreshKeys]
|
|
1264
|
+
);
|
|
1265
|
+
const handleUnblockKey = useCallback2(
|
|
1266
|
+
async (keyId) => {
|
|
1267
|
+
try {
|
|
1268
|
+
await api.unblockKey(keyId);
|
|
1269
|
+
setSnackbar({ message: "Key unblocked", severity: "success" });
|
|
1270
|
+
refreshKeys();
|
|
1271
|
+
} catch (e) {
|
|
1272
|
+
setSnackbar({ message: `Failed to unblock key: ${e.message}`, severity: "error" });
|
|
1273
|
+
}
|
|
1274
|
+
},
|
|
1275
|
+
[api, refreshKeys]
|
|
1276
|
+
);
|
|
1277
|
+
const handleResetKeySpend = useCallback2(
|
|
1278
|
+
async (keyId) => {
|
|
1279
|
+
try {
|
|
1280
|
+
await api.resetKeySpend(keyId);
|
|
1281
|
+
setSnackbar({ message: "Spend counter reset to $0", severity: "success" });
|
|
1282
|
+
refreshKeys();
|
|
1283
|
+
} catch (e) {
|
|
1284
|
+
setSnackbar({ message: `Failed to reset spend: ${e.message}`, severity: "error" });
|
|
1285
|
+
}
|
|
1286
|
+
},
|
|
1287
|
+
[api, refreshKeys]
|
|
1288
|
+
);
|
|
1289
|
+
const handleRotateKey = useCallback2(
|
|
1290
|
+
async (keyId) => {
|
|
1291
|
+
const response = await api.rotateKey(keyId);
|
|
1292
|
+
setSnackbar({ message: "Key rotated \u2014 copy the new secret now", severity: "success" });
|
|
1293
|
+
refreshKeys();
|
|
1294
|
+
return response;
|
|
1295
|
+
},
|
|
1296
|
+
[api, refreshKeys]
|
|
1297
|
+
);
|
|
1298
|
+
const handleDeleteKey = useCallback2(
|
|
891
1299
|
async (keyId) => {
|
|
892
1300
|
try {
|
|
893
1301
|
await api.deleteKey(keyId);
|
|
894
|
-
setSnackbar({
|
|
895
|
-
message: "Key revoked successfully",
|
|
896
|
-
severity: "success"
|
|
897
|
-
});
|
|
1302
|
+
setSnackbar({ message: "Key revoked successfully", severity: "success" });
|
|
898
1303
|
refreshKeys();
|
|
899
1304
|
} catch (e) {
|
|
900
1305
|
if (e.body?.success && (e.body?.message?.includes("already deleted") || e.body?.message?.includes("never existed"))) {
|
|
901
|
-
setSnackbar({
|
|
902
|
-
message: "Key was already deleted",
|
|
903
|
-
severity: "warning"
|
|
904
|
-
});
|
|
1306
|
+
setSnackbar({ message: "Key was already deleted", severity: "warning" });
|
|
905
1307
|
refreshKeys();
|
|
906
1308
|
return;
|
|
907
1309
|
}
|
|
908
|
-
setSnackbar({
|
|
909
|
-
message: `Failed to revoke key: ${e.message}`,
|
|
910
|
-
severity: "error"
|
|
911
|
-
});
|
|
912
|
-
} finally {
|
|
913
|
-
refreshKeys();
|
|
1310
|
+
setSnackbar({ message: `Failed to revoke key: ${e.message}`, severity: "error" });
|
|
914
1311
|
}
|
|
915
1312
|
},
|
|
916
1313
|
[api, refreshKeys]
|
|
917
1314
|
);
|
|
918
1315
|
const isInitialLoading = userLoading && !userInfo;
|
|
919
1316
|
if (isInitialLoading) {
|
|
920
|
-
return /* @__PURE__ */
|
|
1317
|
+
return /* @__PURE__ */ React6.createElement(Box6, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "50vh" }, /* @__PURE__ */ React6.createElement(CircularProgress4, null));
|
|
921
1318
|
}
|
|
922
1319
|
if (userError || !userInfo) {
|
|
923
1320
|
const isProvisioningEnabled = userError?.body?.provisioning === true;
|
|
924
1321
|
const hint = userError?.body?.hint;
|
|
925
|
-
return /* @__PURE__ */
|
|
1322
|
+
return /* @__PURE__ */ React6.createElement(Box6, { p: 3 }, /* @__PURE__ */ React6.createElement(Paper6, { sx: { p: 3 } }, /* @__PURE__ */ React6.createElement(Typography6, { variant: "h6", gutterBottom: true }, "Account not provisioned"), /* @__PURE__ */ React6.createElement(Typography6, { color: "text.secondary", paragraph: true }, "Your Backstage account is not linked to a LiteLLM user."), hint ? /* @__PURE__ */ React6.createElement(Typography6, { variant: "body2", color: "text.secondary" }, hint) : /* @__PURE__ */ React6.createElement(Typography6, { 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.")));
|
|
926
1323
|
}
|
|
927
|
-
return /* @__PURE__ */
|
|
1324
|
+
return /* @__PURE__ */ React6.createElement(Box6, { p: 3 }, userInfo.can_view_audit && /* @__PURE__ */ React6.createElement(
|
|
1325
|
+
Tabs2,
|
|
1326
|
+
{
|
|
1327
|
+
value: activeTab,
|
|
1328
|
+
onChange: (_, v) => setActiveTab(v),
|
|
1329
|
+
sx: { mb: 2, borderBottom: 1, borderColor: "divider" }
|
|
1330
|
+
},
|
|
1331
|
+
/* @__PURE__ */ React6.createElement(Tab2, { label: "Overview", value: "overview" }),
|
|
1332
|
+
/* @__PURE__ */ React6.createElement(Tab2, { label: "Audit Log", value: "audit" })
|
|
1333
|
+
), activeTab === "audit" ? /* @__PURE__ */ React6.createElement(AuditLog, { api }) : /* @__PURE__ */ React6.createElement(Grid2, { container: true, spacing: 2 }, /* @__PURE__ */ React6.createElement(Grid2, { item: true, xs: 12 }, /* @__PURE__ */ React6.createElement(DashboardHeader, { userInfo, teams: teams ?? [], loading: userLoading || teamsLoading })), /* @__PURE__ */ React6.createElement(Grid2, { item: true, xs: 12 }, /* @__PURE__ */ React6.createElement(
|
|
928
1334
|
TeamUsage,
|
|
929
1335
|
{
|
|
930
1336
|
teams: teams ?? [],
|
|
@@ -936,7 +1342,17 @@ var init_LiteLLMPage = __esm({
|
|
|
936
1342
|
},
|
|
937
1343
|
getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false
|
|
938
1344
|
}
|
|
939
|
-
)), /* @__PURE__ */
|
|
1345
|
+
)), /* @__PURE__ */ React6.createElement(Grid2, { item: true, xs: 12 }, /* @__PURE__ */ React6.createElement(
|
|
1346
|
+
UsageStats,
|
|
1347
|
+
{
|
|
1348
|
+
usage: usage ?? null,
|
|
1349
|
+
models: allModels ?? [],
|
|
1350
|
+
dateRange,
|
|
1351
|
+
onDateRangeChange: handleDateRangeChange,
|
|
1352
|
+
loading: usageLoading,
|
|
1353
|
+
userInfo
|
|
1354
|
+
}
|
|
1355
|
+
)), /* @__PURE__ */ React6.createElement(Grid2, { item: true, xs: 12 }, /* @__PURE__ */ React6.createElement(
|
|
940
1356
|
KeysTable,
|
|
941
1357
|
{
|
|
942
1358
|
keys: keys ?? [],
|
|
@@ -945,19 +1361,13 @@ var init_LiteLLMPage = __esm({
|
|
|
945
1361
|
loading: keysLoading || modelsLoading,
|
|
946
1362
|
onGenerateKey: handleGenerateKey,
|
|
947
1363
|
onUpdateKey: handleUpdateKey,
|
|
1364
|
+
onRotateKey: handleRotateKey,
|
|
1365
|
+
onBlockKey: handleBlockKey,
|
|
1366
|
+
onUnblockKey: handleUnblockKey,
|
|
1367
|
+
onResetKeySpend: handleResetKeySpend,
|
|
948
1368
|
onDeleteKey: handleDeleteKey
|
|
949
1369
|
}
|
|
950
|
-
)), /* @__PURE__ */
|
|
951
|
-
UsageStats,
|
|
952
|
-
{
|
|
953
|
-
usage: usage ?? null,
|
|
954
|
-
models: allModels ?? [],
|
|
955
|
-
dateRange,
|
|
956
|
-
onDateRangeChange: setDateRange,
|
|
957
|
-
loading: usageLoading,
|
|
958
|
-
userInfo
|
|
959
|
-
}
|
|
960
|
-
))), /* @__PURE__ */ React5.createElement(
|
|
1370
|
+
))), /* @__PURE__ */ React6.createElement(
|
|
961
1371
|
Snackbar,
|
|
962
1372
|
{
|
|
963
1373
|
open: !!snackbar,
|
|
@@ -965,7 +1375,7 @@ var init_LiteLLMPage = __esm({
|
|
|
965
1375
|
onClose: () => setSnackbar(null),
|
|
966
1376
|
anchorOrigin: { vertical: "bottom", horizontal: "right" }
|
|
967
1377
|
},
|
|
968
|
-
snackbar ? /* @__PURE__ */
|
|
1378
|
+
snackbar ? /* @__PURE__ */ React6.createElement(Alert2, { severity: snackbar.severity, onClose: () => setSnackbar(null) }, snackbar.message) : void 0
|
|
969
1379
|
));
|
|
970
1380
|
};
|
|
971
1381
|
}
|
|
@@ -973,7 +1383,7 @@ var init_LiteLLMPage = __esm({
|
|
|
973
1383
|
|
|
974
1384
|
// src/plugin.tsx
|
|
975
1385
|
init_api();
|
|
976
|
-
import
|
|
1386
|
+
import React7 from "react";
|
|
977
1387
|
import { TrendingUp as TrendingUpIcon } from "@mui/icons-material";
|
|
978
1388
|
import {
|
|
979
1389
|
createFrontendPlugin,
|
|
@@ -992,10 +1402,10 @@ var liteLlmPage = PageBlueprint.make({
|
|
|
992
1402
|
params: {
|
|
993
1403
|
path: "/litellm",
|
|
994
1404
|
title: "LiteLLM",
|
|
995
|
-
icon: /* @__PURE__ */
|
|
1405
|
+
icon: /* @__PURE__ */ React7.createElement(TrendingUpIcon, null),
|
|
996
1406
|
loader: async () => {
|
|
997
1407
|
const { LiteLLMPage: LiteLLMPage2 } = await Promise.resolve().then(() => (init_LiteLLMPage(), LiteLLMPage_exports));
|
|
998
|
-
return /* @__PURE__ */
|
|
1408
|
+
return /* @__PURE__ */ React7.createElement(LiteLLMPage2, null);
|
|
999
1409
|
}
|
|
1000
1410
|
}
|
|
1001
1411
|
});
|
|
@@ -1013,17 +1423,17 @@ init_TeamUsage();
|
|
|
1013
1423
|
|
|
1014
1424
|
// src/components/LiteLLMHomeWidget.tsx
|
|
1015
1425
|
init_api();
|
|
1016
|
-
import
|
|
1426
|
+
import React8, { useState as useState6, useEffect } from "react";
|
|
1017
1427
|
import {
|
|
1018
|
-
Paper as
|
|
1019
|
-
Box as
|
|
1020
|
-
Typography as
|
|
1428
|
+
Paper as Paper7,
|
|
1429
|
+
Box as Box7,
|
|
1430
|
+
Typography as Typography7,
|
|
1021
1431
|
FormControl as FormControl2,
|
|
1022
1432
|
Select as Select2,
|
|
1023
|
-
MenuItem as
|
|
1433
|
+
MenuItem as MenuItem4,
|
|
1024
1434
|
Grid as Grid3,
|
|
1025
|
-
CircularProgress as
|
|
1026
|
-
Alert as
|
|
1435
|
+
CircularProgress as CircularProgress5,
|
|
1436
|
+
Alert as Alert3
|
|
1027
1437
|
} from "@mui/material";
|
|
1028
1438
|
import { AreaChart as AreaChart3, Area as Area3, ResponsiveContainer as ResponsiveContainer3 } from "recharts";
|
|
1029
1439
|
import { useApi as useApi2 } from "@backstage/core-plugin-api";
|
|
@@ -1041,17 +1451,17 @@ function presetToDateRange(preset) {
|
|
|
1041
1451
|
}
|
|
1042
1452
|
return { start, end };
|
|
1043
1453
|
}
|
|
1044
|
-
var Kpi = ({ label, value }) => /* @__PURE__ */
|
|
1454
|
+
var Kpi = ({ label, value }) => /* @__PURE__ */ React8.createElement(Box7, null, /* @__PURE__ */ React8.createElement(Typography7, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ React8.createElement(Typography7, { variant: "subtitle1", fontWeight: 600 }, value));
|
|
1045
1455
|
var LiteLLMHomeWidget = ({
|
|
1046
1456
|
defaultPeriod = "7d",
|
|
1047
1457
|
title = "LiteLLM Usage"
|
|
1048
1458
|
}) => {
|
|
1049
1459
|
const api = useApi2(liteLlmApiRef);
|
|
1050
|
-
const [period, setPeriod] =
|
|
1051
|
-
const [loading, setLoading] =
|
|
1052
|
-
const [error, setError] =
|
|
1053
|
-
const [usage, setUsage] =
|
|
1054
|
-
const [keys, setKeys] =
|
|
1460
|
+
const [period, setPeriod] = useState6(defaultPeriod);
|
|
1461
|
+
const [loading, setLoading] = useState6(true);
|
|
1462
|
+
const [error, setError] = useState6(null);
|
|
1463
|
+
const [usage, setUsage] = useState6(null);
|
|
1464
|
+
const [keys, setKeys] = useState6([]);
|
|
1055
1465
|
useEffect(() => {
|
|
1056
1466
|
let cancelled = false;
|
|
1057
1467
|
setLoading(true);
|
|
@@ -1080,17 +1490,17 @@ var LiteLLMHomeWidget = ({
|
|
|
1080
1490
|
spend: d.spend
|
|
1081
1491
|
}));
|
|
1082
1492
|
const hasSparkline = dailyData.length > 0;
|
|
1083
|
-
return /* @__PURE__ */
|
|
1493
|
+
return /* @__PURE__ */ React8.createElement(Paper7, { sx: { p: 2 } }, /* @__PURE__ */ React8.createElement(Box7, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1.5 }, /* @__PURE__ */ React8.createElement(Typography7, { variant: "h6" }, title), /* @__PURE__ */ React8.createElement(FormControl2, { size: "small", sx: { minWidth: 90 } }, /* @__PURE__ */ React8.createElement(
|
|
1084
1494
|
Select2,
|
|
1085
1495
|
{
|
|
1086
1496
|
value: period,
|
|
1087
1497
|
onChange: (e) => setPeriod(e.target.value),
|
|
1088
1498
|
displayEmpty: true
|
|
1089
1499
|
},
|
|
1090
|
-
/* @__PURE__ */
|
|
1091
|
-
/* @__PURE__ */
|
|
1092
|
-
/* @__PURE__ */
|
|
1093
|
-
))), loading && /* @__PURE__ */
|
|
1500
|
+
/* @__PURE__ */ React8.createElement(MenuItem4, { value: "today" }, "Today"),
|
|
1501
|
+
/* @__PURE__ */ React8.createElement(MenuItem4, { value: "7d" }, "7d"),
|
|
1502
|
+
/* @__PURE__ */ React8.createElement(MenuItem4, { value: "30d" }, "30d")
|
|
1503
|
+
))), loading && /* @__PURE__ */ React8.createElement(Box7, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ React8.createElement(CircularProgress5, { size: 32 })), !loading && error && /* @__PURE__ */ React8.createElement(Alert3, { severity: "error", sx: { mt: 1 } }, error), !loading && !error && /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(Grid3, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ React8.createElement(Grid3, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "USD Spent", value: fmtUsd2(usage?.total_spend ?? 0) })), /* @__PURE__ */ React8.createElement(Grid3, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "Tokens In", value: fmtInt2(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ React8.createElement(Grid3, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "Tokens Out", value: fmtInt2(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ React8.createElement(Grid3, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "Keys", value: fmtInt2(keys.length) }))), hasSparkline && /* @__PURE__ */ React8.createElement(Box7, { height: 120 }, /* @__PURE__ */ React8.createElement(ResponsiveContainer3, { width: "100%", height: "100%" }, /* @__PURE__ */ React8.createElement(AreaChart3, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 } }, /* @__PURE__ */ React8.createElement(
|
|
1094
1504
|
Area3,
|
|
1095
1505
|
{
|
|
1096
1506
|
type: "monotone",
|