@acarmisc/backstage-plugin-litellm 0.3.3 → 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/components/UsageStats.d.ts +2 -1
- package/dist/index.cjs.js +584 -134
- package/dist/index.cjs.js.map +4 -4
- package/dist/index.esm.js +616 -143
- 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
|
}
|
|
@@ -105,7 +133,6 @@ var init_api = __esm({
|
|
|
105
133
|
// src/components/DashboardHeader.tsx
|
|
106
134
|
import React from "react";
|
|
107
135
|
import { Box, Typography, LinearProgress, Paper, Chip } from "@mui/material";
|
|
108
|
-
import { Warning } from "@mui/icons-material";
|
|
109
136
|
var DashboardHeader;
|
|
110
137
|
var init_DashboardHeader = __esm({
|
|
111
138
|
"src/components/DashboardHeader.tsx"() {
|
|
@@ -115,11 +142,6 @@ var init_DashboardHeader = __esm({
|
|
|
115
142
|
return /* @__PURE__ */ React.createElement(Paper, { sx: { p: 2, mb: 2 } }, /* @__PURE__ */ React.createElement(LinearProgress, null));
|
|
116
143
|
}
|
|
117
144
|
const displayName = userInfo.user_email ?? userInfo.email ?? userInfo.user_id;
|
|
118
|
-
const budget = userInfo.max_budget ?? 0;
|
|
119
|
-
const spend = userInfo.spend ?? userInfo.current_spend ?? 0;
|
|
120
|
-
const budgetPct = budget > 0 ? Math.min(spend / budget * 100, 100) : 0;
|
|
121
|
-
const isOver = budget > 0 && spend >= budget;
|
|
122
|
-
const isNear = budget > 0 && spend >= budget * 0.8 && !isOver;
|
|
123
145
|
return /* @__PURE__ */ React.createElement(Paper, { sx: { p: 2, mb: 2 } }, /* @__PURE__ */ React.createElement(Box, { display: "flex", alignItems: "flex-start", gap: 2, flexWrap: "wrap" }, /* @__PURE__ */ React.createElement(Box, { flexGrow: 1 }, /* @__PURE__ */ React.createElement(Typography, { variant: "h6" }, displayName), /* @__PURE__ */ React.createElement(Typography, { variant: "caption", color: "text.secondary" }, userInfo.user_id), teams.length > 0 && /* @__PURE__ */ React.createElement(Box, { display: "flex", gap: 0.75, flexWrap: "wrap", mt: 1 }, teams.map((team) => /* @__PURE__ */ React.createElement(
|
|
124
146
|
Chip,
|
|
125
147
|
{
|
|
@@ -129,21 +151,13 @@ var init_DashboardHeader = __esm({
|
|
|
129
151
|
variant: "outlined",
|
|
130
152
|
color: "primary"
|
|
131
153
|
}
|
|
132
|
-
))))
|
|
133
|
-
LinearProgress,
|
|
134
|
-
{
|
|
135
|
-
variant: "determinate",
|
|
136
|
-
value: budgetPct,
|
|
137
|
-
color: isOver ? "error" : isNear ? "warning" : "primary",
|
|
138
|
-
sx: { height: 8, borderRadius: 1 }
|
|
139
|
-
}
|
|
140
|
-
))));
|
|
154
|
+
))))));
|
|
141
155
|
};
|
|
142
156
|
}
|
|
143
157
|
});
|
|
144
158
|
|
|
145
159
|
// src/components/KeysTable.tsx
|
|
146
|
-
import React2, { useState } from "react";
|
|
160
|
+
import React2, { useState, useMemo } from "react";
|
|
147
161
|
import {
|
|
148
162
|
Paper as Paper2,
|
|
149
163
|
Table,
|
|
@@ -164,9 +178,38 @@ import {
|
|
|
164
178
|
MenuItem,
|
|
165
179
|
Chip as Chip2,
|
|
166
180
|
CircularProgress,
|
|
167
|
-
Autocomplete
|
|
181
|
+
Autocomplete,
|
|
182
|
+
LinearProgress as LinearProgress2,
|
|
183
|
+
InputAdornment
|
|
168
184
|
} from "@mui/material";
|
|
169
|
-
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
|
+
}
|
|
170
213
|
var maskKey, shortKeyId, formatDate, emptyForm, keyToEditForm, KeysTable;
|
|
171
214
|
var init_KeysTable = __esm({
|
|
172
215
|
"src/components/KeysTable.tsx"() {
|
|
@@ -193,6 +236,7 @@ var init_KeysTable = __esm({
|
|
|
193
236
|
duration: "30d",
|
|
194
237
|
max_budget: 100,
|
|
195
238
|
tpm_limit: void 0,
|
|
239
|
+
rpm_limit: void 0,
|
|
196
240
|
team_id: void 0,
|
|
197
241
|
key_type: "llm_api"
|
|
198
242
|
});
|
|
@@ -210,16 +254,34 @@ var init_KeysTable = __esm({
|
|
|
210
254
|
loading,
|
|
211
255
|
onGenerateKey,
|
|
212
256
|
onUpdateKey,
|
|
257
|
+
onRotateKey,
|
|
258
|
+
onBlockKey,
|
|
259
|
+
onUnblockKey,
|
|
260
|
+
onResetKeySpend,
|
|
213
261
|
onDeleteKey
|
|
214
262
|
}) => {
|
|
215
263
|
const [generateModalOpen, setGenerateModalOpen] = useState(false);
|
|
216
264
|
const [newKeyValue, setNewKeyValue] = useState(null);
|
|
217
265
|
const [formData, setFormData] = useState(emptyForm());
|
|
218
266
|
const [submitting, setSubmitting] = useState(false);
|
|
219
|
-
const canGenerate = true;
|
|
220
267
|
const [editingKey, setEditingKey] = useState(null);
|
|
221
268
|
const [editForm, setEditForm] = useState({});
|
|
222
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]);
|
|
223
285
|
const selectedModels = models.filter((m) => (formData.models || []).includes(m.model_name));
|
|
224
286
|
const selectedTeam = teams.find((t) => t.team_id === formData.team_id) ?? null;
|
|
225
287
|
const editSelectedModels = models.filter((m) => (editForm.models || []).includes(m.model_name));
|
|
@@ -247,6 +309,7 @@ var init_KeysTable = __esm({
|
|
|
247
309
|
const handleCloseEdit = () => {
|
|
248
310
|
setEditingKey(null);
|
|
249
311
|
setEditForm({});
|
|
312
|
+
setResetSpendConfirm(false);
|
|
250
313
|
};
|
|
251
314
|
const handleUpdate = async () => {
|
|
252
315
|
if (!editingKey) return;
|
|
@@ -260,10 +323,72 @@ var init_KeysTable = __esm({
|
|
|
260
323
|
setEditSubmitting(false);
|
|
261
324
|
}
|
|
262
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
|
+
};
|
|
263
371
|
const copyToClipboard = (text) => {
|
|
264
372
|
navigator.clipboard.writeText(text);
|
|
265
373
|
};
|
|
266
|
-
|
|
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(
|
|
267
392
|
Button,
|
|
268
393
|
{
|
|
269
394
|
variant: "contained",
|
|
@@ -272,9 +397,11 @@ var init_KeysTable = __esm({
|
|
|
272
397
|
onClick: () => setGenerateModalOpen(true)
|
|
273
398
|
},
|
|
274
399
|
"Generate New Key"
|
|
275
|
-
)), /* @__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) => {
|
|
276
401
|
const keyId = key.token ?? key.key;
|
|
277
|
-
|
|
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(
|
|
278
405
|
Typography2,
|
|
279
406
|
{
|
|
280
407
|
variant: "body2",
|
|
@@ -290,7 +417,32 @@ var init_KeysTable = __esm({
|
|
|
290
417
|
}
|
|
291
418
|
},
|
|
292
419
|
shortKeyId(keyId)
|
|
293
|
-
), /* @__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
|
+
)));
|
|
294
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(
|
|
295
447
|
Box2,
|
|
296
448
|
{
|
|
@@ -365,7 +517,7 @@ var init_KeysTable = __esm({
|
|
|
365
517
|
getOptionLabel: (m) => m.model_name,
|
|
366
518
|
value: selectedModels,
|
|
367
519
|
onChange: (_e, selected) => setFormData({ ...formData, models: selected.map((m) => m.model_name) }),
|
|
368
|
-
renderOption: (props, m) => /* @__PURE__ */ React2.createElement("li", { ...props }, m
|
|
520
|
+
renderOption: (props, m) => /* @__PURE__ */ React2.createElement("li", { ...props }, modelOption(m)),
|
|
369
521
|
renderInput: (params) => /* @__PURE__ */ React2.createElement(TextField, { ...params, label: "Models", helperText: "Leave empty to allow all models", fullWidth: true })
|
|
370
522
|
}
|
|
371
523
|
), /* @__PURE__ */ React2.createElement(
|
|
@@ -387,13 +539,22 @@ var init_KeysTable = __esm({
|
|
|
387
539
|
onChange: (e) => setFormData({ ...formData, tpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
|
|
388
540
|
fullWidth: true
|
|
389
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
|
+
}
|
|
390
551
|
))), /* @__PURE__ */ React2.createElement(DialogActions, null, /* @__PURE__ */ React2.createElement(Button, { onClick: handleCloseModal }, newKeyValue ? "Done" : "Cancel"), !newKeyValue && /* @__PURE__ */ React2.createElement(
|
|
391
552
|
Button,
|
|
392
553
|
{
|
|
393
554
|
onClick: handleGenerate,
|
|
394
555
|
variant: "contained",
|
|
395
556
|
color: "primary",
|
|
396
|
-
disabled: submitting
|
|
557
|
+
disabled: submitting
|
|
397
558
|
},
|
|
398
559
|
submitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 24 }) : "Generate"
|
|
399
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(
|
|
@@ -413,6 +574,7 @@ var init_KeysTable = __esm({
|
|
|
413
574
|
getOptionLabel: (m) => m.model_name,
|
|
414
575
|
value: editSelectedModels,
|
|
415
576
|
onChange: (_e, selected) => setEditForm({ ...editForm, models: selected.map((m) => m.model_name) }),
|
|
577
|
+
renderOption: (props, m) => /* @__PURE__ */ React2.createElement("li", { ...props }, modelOption(m)),
|
|
416
578
|
renderInput: (params) => /* @__PURE__ */ React2.createElement(TextField, { ...params, label: "Models", fullWidth: true })
|
|
417
579
|
}
|
|
418
580
|
), /* @__PURE__ */ React2.createElement(
|
|
@@ -442,13 +604,60 @@ var init_KeysTable = __esm({
|
|
|
442
604
|
onChange: (e) => setEditForm({ ...editForm, rpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
|
|
443
605
|
fullWidth: true
|
|
444
606
|
}
|
|
445
|
-
)
|
|
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
|
+
))));
|
|
446
655
|
};
|
|
447
656
|
}
|
|
448
657
|
});
|
|
449
658
|
|
|
450
659
|
// src/components/UsageStats.tsx
|
|
451
|
-
import React3, { useMemo, useState as useState2 } from "react";
|
|
660
|
+
import React3, { useMemo as useMemo2, useState as useState2 } from "react";
|
|
452
661
|
import {
|
|
453
662
|
Paper as Paper3,
|
|
454
663
|
Box as Box3,
|
|
@@ -458,7 +667,6 @@ import {
|
|
|
458
667
|
Select,
|
|
459
668
|
MenuItem as MenuItem2,
|
|
460
669
|
Grid,
|
|
461
|
-
CircularProgress as CircularProgress2,
|
|
462
670
|
Tabs,
|
|
463
671
|
Tab,
|
|
464
672
|
Table as Table2,
|
|
@@ -468,42 +676,71 @@ import {
|
|
|
468
676
|
TableHead as TableHead2,
|
|
469
677
|
TableRow as TableRow2,
|
|
470
678
|
Chip as Chip3,
|
|
471
|
-
LinearProgress as
|
|
679
|
+
LinearProgress as LinearProgress3,
|
|
680
|
+
Skeleton
|
|
472
681
|
} from "@mui/material";
|
|
473
682
|
import {
|
|
474
683
|
AreaChart,
|
|
475
684
|
Area,
|
|
476
685
|
BarChart,
|
|
477
686
|
Bar,
|
|
687
|
+
LineChart,
|
|
688
|
+
Line,
|
|
478
689
|
XAxis,
|
|
479
690
|
YAxis,
|
|
480
691
|
CartesianGrid,
|
|
481
692
|
Tooltip,
|
|
482
693
|
ResponsiveContainer,
|
|
483
|
-
Legend
|
|
694
|
+
Legend,
|
|
695
|
+
ReferenceLine
|
|
484
696
|
} from "recharts";
|
|
485
|
-
|
|
697
|
+
function modelColor(model) {
|
|
698
|
+
let h = 5381;
|
|
699
|
+
for (let i = 0; i < model.length; i++) h = (h << 5) + h + model.charCodeAt(i) >>> 0;
|
|
700
|
+
return MODEL_COLORS[h % MODEL_COLORS.length];
|
|
701
|
+
}
|
|
702
|
+
var TOP_N_MODELS, PERIOD_LS_KEY, MODEL_COLORS, fmtUsd, fmtInt, fmtPct, KpiCard, ChartSkeleton, EmptyChart, UsageStats;
|
|
486
703
|
var init_UsageStats = __esm({
|
|
487
704
|
"src/components/UsageStats.tsx"() {
|
|
488
705
|
"use strict";
|
|
706
|
+
TOP_N_MODELS = 6;
|
|
707
|
+
PERIOD_LS_KEY = "litellm_usage_period";
|
|
708
|
+
MODEL_COLORS = [
|
|
709
|
+
"#8884d8",
|
|
710
|
+
"#82ca9d",
|
|
711
|
+
"#ffc658",
|
|
712
|
+
"#ff7300",
|
|
713
|
+
"#0088fe",
|
|
714
|
+
"#00C49F",
|
|
715
|
+
"#FFBB28",
|
|
716
|
+
"#FF8042",
|
|
717
|
+
"#a4de6c",
|
|
718
|
+
"#d0ed57"
|
|
719
|
+
];
|
|
489
720
|
fmtUsd = (n) => `$${(n ?? 0).toFixed(n < 1 ? 4 : 2)}`;
|
|
490
721
|
fmtInt = (n) => (n ?? 0).toLocaleString();
|
|
491
722
|
fmtPct = (n) => `${(n * 100).toFixed(1)}%`;
|
|
492
723
|
KpiCard = ({ label, value, hint }) => /* @__PURE__ */ React3.createElement(Paper3, { variant: "outlined", sx: { p: 2, height: "100%" } }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption", color: "text.secondary" }, label), /* @__PURE__ */ React3.createElement(Typography3, { variant: "h5", sx: { mt: 0.5 } }, value), hint ? /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption", color: "text.secondary" }, hint) : null);
|
|
724
|
+
ChartSkeleton = ({ height = 260 }) => /* @__PURE__ */ React3.createElement(Skeleton, { variant: "rectangular", height, sx: { borderRadius: 1 } });
|
|
725
|
+
EmptyChart = ({
|
|
726
|
+
height = 260,
|
|
727
|
+
message = "No data for this period"
|
|
728
|
+
}) => /* @__PURE__ */ React3.createElement(Box3, { height, display: "flex", alignItems: "center", justifyContent: "center" }, /* @__PURE__ */ React3.createElement(Typography3, { color: "text.secondary", variant: "body2" }, message));
|
|
493
729
|
UsageStats = ({
|
|
494
730
|
usage,
|
|
495
731
|
models,
|
|
496
732
|
dateRange,
|
|
497
733
|
onDateRangeChange,
|
|
498
|
-
loading
|
|
734
|
+
loading,
|
|
735
|
+
userInfo
|
|
499
736
|
}) => {
|
|
500
737
|
const [selectedModel, setSelectedModel] = useState2("all");
|
|
501
738
|
const [tab, setTab] = useState2("costs");
|
|
502
|
-
const selectedPreset =
|
|
503
|
-
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
739
|
+
const selectedPreset = useMemo2(() => {
|
|
740
|
+
if (dateRange.start.toDateString() === dateRange.end.toDateString()) return "today";
|
|
741
|
+
const diffMs = dateRange.end.getTime() - dateRange.start.getTime();
|
|
742
|
+
const diffHours = diffMs / (1e3 * 60 * 60);
|
|
743
|
+
if (diffHours <= 26) return "24h";
|
|
507
744
|
const diffDays = Math.ceil(diffMs / (1e3 * 60 * 60 * 24));
|
|
508
745
|
if (diffDays <= 7) return "7d";
|
|
509
746
|
return "30d";
|
|
@@ -512,26 +749,16 @@ var init_UsageStats = __esm({
|
|
|
512
749
|
const end = /* @__PURE__ */ new Date();
|
|
513
750
|
const start = /* @__PURE__ */ new Date();
|
|
514
751
|
if (preset === "today") start.setHours(0, 0, 0, 0);
|
|
752
|
+
else if (preset === "24h") start.setHours(start.getHours() - 24);
|
|
515
753
|
else if (preset === "7d") start.setDate(start.getDate() - 7);
|
|
516
754
|
else if (preset === "30d") start.setDate(start.getDate() - 30);
|
|
755
|
+
try {
|
|
756
|
+
localStorage.setItem(PERIOD_LS_KEY, preset);
|
|
757
|
+
} catch {
|
|
758
|
+
}
|
|
517
759
|
onDateRangeChange({ start, end });
|
|
518
760
|
};
|
|
519
|
-
const
|
|
520
|
-
const rows = usage?.daily_by_model ?? [];
|
|
521
|
-
if (rows.length === 0) return [];
|
|
522
|
-
const latestDate = rows.map((r) => r.date).sort().slice(-1)[0];
|
|
523
|
-
return rows.filter((r) => r.date === latestDate).map((r) => ({
|
|
524
|
-
model: r.model,
|
|
525
|
-
promptTokens: r.prompt_tokens,
|
|
526
|
-
completionTokens: r.completion_tokens
|
|
527
|
-
})).filter((r) => r.promptTokens + r.completionTokens > 0).sort((a, b) => b.promptTokens + b.completionTokens - (a.promptTokens + a.completionTokens));
|
|
528
|
-
}, [usage]);
|
|
529
|
-
const todayLabel = useMemo(() => {
|
|
530
|
-
const rows = usage?.daily_by_model ?? [];
|
|
531
|
-
if (rows.length === 0) return null;
|
|
532
|
-
return rows.map((r) => r.date).sort().slice(-1)[0];
|
|
533
|
-
}, [usage]);
|
|
534
|
-
const dailyData = useMemo(
|
|
761
|
+
const dailyData = useMemo2(
|
|
535
762
|
() => (usage?.daily_usage ?? []).map((d) => ({
|
|
536
763
|
date: d.date,
|
|
537
764
|
spend: d.spend,
|
|
@@ -544,7 +771,36 @@ var init_UsageStats = __esm({
|
|
|
544
771
|
})),
|
|
545
772
|
[usage]
|
|
546
773
|
);
|
|
547
|
-
const
|
|
774
|
+
const cumulativeData = useMemo2(() => {
|
|
775
|
+
let cum = 0;
|
|
776
|
+
return dailyData.map((d) => {
|
|
777
|
+
cum += d.spend;
|
|
778
|
+
return { date: d.date, cumulative: cum };
|
|
779
|
+
});
|
|
780
|
+
}, [dailyData]);
|
|
781
|
+
const successRateData = useMemo2(
|
|
782
|
+
() => dailyData.filter((d) => d.apiRequests > 0).map((d) => ({
|
|
783
|
+
date: d.date,
|
|
784
|
+
successRate: parseFloat((d.successfulRequests / d.apiRequests * 100).toFixed(1))
|
|
785
|
+
})),
|
|
786
|
+
[dailyData]
|
|
787
|
+
);
|
|
788
|
+
const { modelSpendByDate, topModels: topSpendModels } = useMemo2(() => {
|
|
789
|
+
const rows = usage?.daily_by_model ?? [];
|
|
790
|
+
const modelTotals = {};
|
|
791
|
+
for (const r of rows) modelTotals[r.model] = (modelTotals[r.model] ?? 0) + r.spend;
|
|
792
|
+
const top = Object.entries(modelTotals).sort(([, a], [, b]) => b - a).slice(0, TOP_N_MODELS).map(([m]) => m);
|
|
793
|
+
const dateMap = {};
|
|
794
|
+
for (const r of rows) {
|
|
795
|
+
if (!dateMap[r.date]) dateMap[r.date] = {};
|
|
796
|
+
const bucket = top.includes(r.model) ? r.model : "Other";
|
|
797
|
+
dateMap[r.date][bucket] = (dateMap[r.date][bucket] ?? 0) + r.spend;
|
|
798
|
+
}
|
|
799
|
+
const sorted = Object.entries(dateMap).sort(([a], [b]) => a.localeCompare(b)).map(([date, m]) => ({ date, ...m }));
|
|
800
|
+
const hasOther = sorted.some((r) => r.Other > 0);
|
|
801
|
+
return { modelSpendByDate: sorted, topModels: hasOther ? [...top, "Other"] : top };
|
|
802
|
+
}, [usage]);
|
|
803
|
+
const modelRows = useMemo2(() => {
|
|
548
804
|
const entries = Object.entries(usage?.usage_by_model ?? {}).map(([model, d]) => ({
|
|
549
805
|
model,
|
|
550
806
|
spend: d.total_spend,
|
|
@@ -558,7 +814,11 @@ var init_UsageStats = __esm({
|
|
|
558
814
|
}));
|
|
559
815
|
return selectedModel === "all" ? entries : entries.filter((e) => e.model === selectedModel);
|
|
560
816
|
}, [usage, selectedModel]);
|
|
561
|
-
const
|
|
817
|
+
const topModelSpendBars = useMemo2(
|
|
818
|
+
() => [...modelRows].sort((a, b) => b.spend - a.spend).slice(0, 10),
|
|
819
|
+
[modelRows]
|
|
820
|
+
);
|
|
821
|
+
const keyRows = useMemo2(
|
|
562
822
|
() => Object.entries(usage?.usage_by_key ?? {}).map(([keyHash, d]) => ({
|
|
563
823
|
keyHash,
|
|
564
824
|
keyAlias: d.key_alias ?? keyHash.slice(0, 8),
|
|
@@ -575,11 +835,14 @@ var init_UsageStats = __esm({
|
|
|
575
835
|
})),
|
|
576
836
|
[usage]
|
|
577
837
|
);
|
|
838
|
+
const topKeySpendBars = useMemo2(
|
|
839
|
+
() => [...keyRows].sort((a, b) => b.spend - a.spend).slice(0, 10),
|
|
840
|
+
[keyRows]
|
|
841
|
+
);
|
|
578
842
|
const totalRequests = usage?.api_requests ?? 0;
|
|
579
843
|
const overallSuccessRate = totalRequests > 0 ? (usage?.successful_requests ?? 0) / totalRequests : 0;
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
}
|
|
844
|
+
const maxBudget = userInfo?.max_budget ?? 0;
|
|
845
|
+
const totalCumSpend = cumulativeData[cumulativeData.length - 1]?.cumulative ?? 0;
|
|
583
846
|
return /* @__PURE__ */ React3.createElement(Paper3, { sx: { p: 2 } }, /* @__PURE__ */ React3.createElement(Box3, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2, flexWrap: "wrap", gap: 2 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "h6" }, "Usage Analytics"), /* @__PURE__ */ React3.createElement(Box3, { display: "flex", gap: 2, alignItems: "center", flexWrap: "wrap" }, /* @__PURE__ */ React3.createElement(FormControl, { size: "small", sx: { minWidth: 140 } }, /* @__PURE__ */ React3.createElement(InputLabel, null, "Period"), /* @__PURE__ */ React3.createElement(
|
|
584
847
|
Select,
|
|
585
848
|
{
|
|
@@ -588,6 +851,7 @@ var init_UsageStats = __esm({
|
|
|
588
851
|
onChange: (e) => handlePresetChange(e.target.value)
|
|
589
852
|
},
|
|
590
853
|
/* @__PURE__ */ React3.createElement(MenuItem2, { value: "today" }, "Today"),
|
|
854
|
+
/* @__PURE__ */ React3.createElement(MenuItem2, { value: "24h" }, "Last 24h"),
|
|
591
855
|
/* @__PURE__ */ React3.createElement(MenuItem2, { value: "7d" }, "Last 7 days"),
|
|
592
856
|
/* @__PURE__ */ React3.createElement(MenuItem2, { value: "30d" }, "Last 30 days")
|
|
593
857
|
)), tab === "models" && /* @__PURE__ */ React3.createElement(FormControl, { size: "small", sx: { minWidth: 180 } }, /* @__PURE__ */ React3.createElement(InputLabel, null, "Model"), /* @__PURE__ */ React3.createElement(
|
|
@@ -599,28 +863,39 @@ var init_UsageStats = __esm({
|
|
|
599
863
|
},
|
|
600
864
|
/* @__PURE__ */ React3.createElement(MenuItem2, { value: "all" }, "All Models"),
|
|
601
865
|
models.map((m) => /* @__PURE__ */ React3.createElement(MenuItem2, { key: m.model_name, value: m.model_name }, m.model_name))
|
|
602
|
-
)))), /* @__PURE__ */ React3.createElement(Grid, { container: true, spacing: 2, sx: { mb: 2 } }, /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 6, sm: 3 }, /* @__PURE__ */ React3.createElement(KpiCard, { label: "Total Spend", value: fmtUsd(usage?.total_spend ?? 0) })), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 6, sm: 3 }, /* @__PURE__ */ React3.createElement(KpiCard, { label: "Total Requests", value: fmtInt(totalRequests), hint: `${fmtInt(usage?.failed_requests ?? 0)} failed` })), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 6, sm: 3 }, /* @__PURE__ */ React3.createElement(KpiCard, { label: "Success Rate", value: totalRequests > 0 ? fmtPct(overallSuccessRate) : "\u2014" })), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 6, sm: 3 }, /* @__PURE__ */ React3.createElement(
|
|
866
|
+
)))), /* @__PURE__ */ React3.createElement(Grid, { container: true, spacing: 2, sx: { mb: 2 } }, /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 6, sm: 3 }, loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, { height: 80 }) : /* @__PURE__ */ React3.createElement(KpiCard, { label: "Total Spend", value: fmtUsd(usage?.total_spend ?? 0) })), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 6, sm: 3 }, loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, { height: 80 }) : /* @__PURE__ */ React3.createElement(KpiCard, { label: "Total Requests", value: fmtInt(totalRequests), hint: `${fmtInt(usage?.failed_requests ?? 0)} failed` })), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 6, sm: 3 }, loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, { height: 80 }) : /* @__PURE__ */ React3.createElement(KpiCard, { label: "Success Rate", value: totalRequests > 0 ? fmtPct(overallSuccessRate) : "\u2014" })), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 6, sm: 3 }, loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, { height: 80 }) : /* @__PURE__ */ React3.createElement(
|
|
603
867
|
KpiCard,
|
|
604
868
|
{
|
|
605
869
|
label: "Total Tokens",
|
|
606
870
|
value: fmtInt(usage?.total_tokens ?? 0),
|
|
607
871
|
hint: `${fmtInt(usage?.prompt_tokens ?? 0)} in \xB7 ${fmtInt(usage?.completion_tokens ?? 0)} out`
|
|
608
872
|
}
|
|
609
|
-
))), /* @__PURE__ */ React3.createElement(Tabs, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 2 } }, /* @__PURE__ */ React3.createElement(Tab, { value: "costs", label: "Costs" }), /* @__PURE__ */ React3.createElement(Tab, { value: "models", label: "Model Activity" }), /* @__PURE__ */ React3.createElement(Tab, { value: "keys", label: "Key Activity" })), tab === "costs" && /* @__PURE__ */ React3.createElement(Grid, { container: true, spacing: 3 }, /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend
|
|
610
|
-
|
|
873
|
+
))), /* @__PURE__ */ React3.createElement(Tabs, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 2 } }, /* @__PURE__ */ React3.createElement(Tab, { value: "costs", label: "Costs" }), /* @__PURE__ */ React3.createElement(Tab, { value: "models", label: "Model Activity" }), /* @__PURE__ */ React3.createElement(Tab, { value: "keys", label: "Key Activity" })), tab === "costs" && /* @__PURE__ */ React3.createElement(Grid, { container: true, spacing: 3 }, /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend by Model"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : modelSpendByDate.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: modelSpendByDate }, /* @__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)}`, void 0] }), /* @__PURE__ */ React3.createElement(Legend, null), topSpendModels.map((m) => /* @__PURE__ */ React3.createElement(
|
|
874
|
+
Area,
|
|
875
|
+
{
|
|
876
|
+
key: m,
|
|
877
|
+
type: "monotone",
|
|
878
|
+
dataKey: m,
|
|
879
|
+
stackId: "spend",
|
|
880
|
+
stroke: modelColor(m),
|
|
881
|
+
fill: modelColor(m),
|
|
882
|
+
fillOpacity: 0.6
|
|
883
|
+
}
|
|
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,
|
|
611
886
|
{
|
|
612
887
|
variant: "determinate",
|
|
613
888
|
value: r.successRate * 100,
|
|
614
889
|
sx: { flex: 1, height: 6, borderRadius: 3 }
|
|
615
890
|
}
|
|
616
|
-
), /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption" }, fmtPct(r.successRate))) : "\u2014")))))))), tab === "keys" && /* @__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, 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(
|
|
617
|
-
|
|
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,
|
|
618
893
|
{
|
|
619
894
|
variant: "determinate",
|
|
620
895
|
value: r.successRate * 100,
|
|
621
896
|
sx: { flex: 1, height: 6, borderRadius: 3 }
|
|
622
897
|
}
|
|
623
|
-
), /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption" }, fmtPct(r.successRate))) : "\u2014")))))));
|
|
898
|
+
), /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption" }, fmtPct(r.successRate))) : "\u2014")))))))));
|
|
624
899
|
};
|
|
625
900
|
}
|
|
626
901
|
});
|
|
@@ -631,7 +906,7 @@ import {
|
|
|
631
906
|
Paper as Paper4,
|
|
632
907
|
Box as Box4,
|
|
633
908
|
Typography as Typography4,
|
|
634
|
-
LinearProgress as
|
|
909
|
+
LinearProgress as LinearProgress4,
|
|
635
910
|
Chip as Chip4,
|
|
636
911
|
Table as Table3,
|
|
637
912
|
TableBody as TableBody3,
|
|
@@ -641,7 +916,7 @@ import {
|
|
|
641
916
|
TableContainer as TableContainer3,
|
|
642
917
|
Collapse,
|
|
643
918
|
IconButton as IconButton2,
|
|
644
|
-
CircularProgress as
|
|
919
|
+
CircularProgress as CircularProgress2
|
|
645
920
|
} from "@mui/material";
|
|
646
921
|
import { ExpandMore, ExpandLess, Group } from "@mui/icons-material";
|
|
647
922
|
import {
|
|
@@ -666,14 +941,14 @@ var init_TeamUsage = __esm({
|
|
|
666
941
|
const isNear = budget > 0 && spend >= budget * 0.8 && !isOver;
|
|
667
942
|
const dailyData = usage?.daily_usage?.map((d) => ({ date: d.date, spend: d.spend })) ?? [];
|
|
668
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(
|
|
669
|
-
|
|
944
|
+
LinearProgress4,
|
|
670
945
|
{
|
|
671
946
|
variant: "determinate",
|
|
672
947
|
value: budgetPct,
|
|
673
948
|
color: isOver ? "error" : isNear ? "warning" : "primary",
|
|
674
949
|
sx: { height: 6, borderRadius: 1 }
|
|
675
950
|
}
|
|
676
|
-
)), /* @__PURE__ */ React4.createElement(Collapse, { in: expanded }, /* @__PURE__ */ React4.createElement(Box4, { mt: 2 }, usageLoading ? /* @__PURE__ */ React4.createElement(Box4, { display: "flex", justifyContent: "center", p: 2 }, /* @__PURE__ */ React4.createElement(
|
|
951
|
+
)), /* @__PURE__ */ React4.createElement(Collapse, { in: expanded }, /* @__PURE__ */ React4.createElement(Box4, { mt: 2 }, usageLoading ? /* @__PURE__ */ React4.createElement(Box4, { display: "flex", justifyContent: "center", p: 2 }, /* @__PURE__ */ React4.createElement(CircularProgress2, { size: 24 })) : dailyData.length > 0 ? /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend"), /* @__PURE__ */ React4.createElement(Box4, { height: 160 }, /* @__PURE__ */ React4.createElement(ResponsiveContainer2, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(AreaChart2, { data: dailyData }, /* @__PURE__ */ React4.createElement(CartesianGrid2, { strokeDasharray: "3 3" }), /* @__PURE__ */ React4.createElement(XAxis2, { dataKey: "date", tick: { fontSize: 11 } }), /* @__PURE__ */ React4.createElement(YAxis2, { tick: { fontSize: 11 }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React4.createElement(Tooltip2, { formatter: (v) => [`$${v.toFixed(4)}`, "Spend"] }), /* @__PURE__ */ React4.createElement(Area2, { type: "monotone", dataKey: "spend", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.3 }))))) : null, team.members_with_roles?.length ? /* @__PURE__ */ React4.createElement(Box4, { mt: 2 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Members"), /* @__PURE__ */ React4.createElement(TableContainer3, null, /* @__PURE__ */ React4.createElement(Table3, { size: "small" }, /* @__PURE__ */ React4.createElement(TableHead3, null, /* @__PURE__ */ React4.createElement(TableRow3, null, /* @__PURE__ */ React4.createElement(TableCell3, null, "User"), /* @__PURE__ */ React4.createElement(TableCell3, null, "Role"))), /* @__PURE__ */ React4.createElement(TableBody3, null, team.members_with_roles.map((m) => /* @__PURE__ */ React4.createElement(TableRow3, { key: m.user_id }, /* @__PURE__ */ React4.createElement(TableCell3, { sx: { fontFamily: "monospace", fontSize: 12 } }, m.user_id), /* @__PURE__ */ React4.createElement(TableCell3, null, /* @__PURE__ */ React4.createElement(
|
|
677
952
|
Chip4,
|
|
678
953
|
{
|
|
679
954
|
label: m.role,
|
|
@@ -689,7 +964,7 @@ var init_TeamUsage = __esm({
|
|
|
689
964
|
getTeamUsageLoading
|
|
690
965
|
}) => {
|
|
691
966
|
if (loading) {
|
|
692
|
-
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));
|
|
693
968
|
}
|
|
694
969
|
if (!teams.length) {
|
|
695
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.")));
|
|
@@ -707,16 +982,163 @@ var init_TeamUsage = __esm({
|
|
|
707
982
|
}
|
|
708
983
|
});
|
|
709
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
|
+
|
|
710
1118
|
// src/components/LiteLLMPage.tsx
|
|
711
1119
|
var LiteLLMPage_exports = {};
|
|
712
1120
|
__export(LiteLLMPage_exports, {
|
|
713
1121
|
LiteLLMPage: () => LiteLLMPage
|
|
714
1122
|
});
|
|
715
|
-
import
|
|
716
|
-
import { Grid as Grid2, Box as
|
|
717
|
-
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";
|
|
718
1126
|
import { useApi } from "@backstage/core-plugin-api";
|
|
719
|
-
|
|
1127
|
+
function initDateRange() {
|
|
1128
|
+
let preset = "7d";
|
|
1129
|
+
try {
|
|
1130
|
+
preset = localStorage.getItem(PERIOD_LS_KEY2) ?? "7d";
|
|
1131
|
+
} catch {
|
|
1132
|
+
}
|
|
1133
|
+
const end = /* @__PURE__ */ new Date();
|
|
1134
|
+
const start = /* @__PURE__ */ new Date();
|
|
1135
|
+
if (preset === "today") start.setHours(0, 0, 0, 0);
|
|
1136
|
+
else if (preset === "24h") start.setHours(start.getHours() - 24);
|
|
1137
|
+
else if (preset === "30d") start.setDate(start.getDate() - 30);
|
|
1138
|
+
else start.setDate(start.getDate() - 7);
|
|
1139
|
+
return { start, end };
|
|
1140
|
+
}
|
|
1141
|
+
var PERIOD_LS_KEY2, LiteLLMPage;
|
|
720
1142
|
var init_LiteLLMPage = __esm({
|
|
721
1143
|
"src/components/LiteLLMPage.tsx"() {
|
|
722
1144
|
"use strict";
|
|
@@ -724,19 +1146,17 @@ var init_LiteLLMPage = __esm({
|
|
|
724
1146
|
init_KeysTable();
|
|
725
1147
|
init_UsageStats();
|
|
726
1148
|
init_TeamUsage();
|
|
1149
|
+
init_AuditLog();
|
|
727
1150
|
init_api();
|
|
1151
|
+
PERIOD_LS_KEY2 = "litellm_usage_period";
|
|
728
1152
|
LiteLLMPage = () => {
|
|
729
1153
|
const api = useApi(liteLlmApiRef);
|
|
730
|
-
const [dateRange, setDateRange] =
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
}
|
|
736
|
-
const [snackbar, setSnackbar] = useState4(null);
|
|
737
|
-
const [teamUsageCache, setTeamUsageCache] = useState4({});
|
|
738
|
-
const [teamUsageLoading, setTeamUsageLoading] = useState4({});
|
|
739
|
-
const { value: userInfo, loading: userLoading, error: userError } = useAsync(
|
|
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(
|
|
740
1160
|
() => api.getUserInfo(),
|
|
741
1161
|
[api]
|
|
742
1162
|
);
|
|
@@ -751,15 +1171,15 @@ var init_LiteLLMPage = __esm({
|
|
|
751
1171
|
},
|
|
752
1172
|
[api]
|
|
753
1173
|
);
|
|
754
|
-
const { value: allModels, loading: modelsLoading } =
|
|
1174
|
+
const { value: allModels, loading: modelsLoading } = useAsync2(
|
|
755
1175
|
() => api.listModels().catch(() => []),
|
|
756
1176
|
[api]
|
|
757
1177
|
);
|
|
758
|
-
const { value: allTeams, loading: teamsLoading } =
|
|
1178
|
+
const { value: allTeams, loading: teamsLoading } = useAsync2(
|
|
759
1179
|
() => api.getTeams().catch(() => []),
|
|
760
1180
|
[api]
|
|
761
1181
|
);
|
|
762
|
-
const teams =
|
|
1182
|
+
const teams = useMemo3(() => {
|
|
763
1183
|
if (!allTeams?.length) return [];
|
|
764
1184
|
if (!userInfo) return allTeams;
|
|
765
1185
|
const userId = userInfo.user_id;
|
|
@@ -771,12 +1191,17 @@ var init_LiteLLMPage = __esm({
|
|
|
771
1191
|
);
|
|
772
1192
|
return byMembership.length > 0 ? byMembership : allTeams;
|
|
773
1193
|
}, [allTeams, userInfo]);
|
|
774
|
-
const { value: usage, loading: usageLoading } =
|
|
1194
|
+
const { value: usage, loading: usageLoading } = useAsync2(async () => {
|
|
775
1195
|
const startDate = dateRange.start.toISOString().split("T")[0];
|
|
776
1196
|
const endDate = dateRange.end.toISOString().split("T")[0];
|
|
777
1197
|
return api.getUsage(startDate, endDate);
|
|
778
1198
|
}, [api, dateRange]);
|
|
779
|
-
const
|
|
1199
|
+
const handleDateRangeChange = useCallback2((range) => {
|
|
1200
|
+
setDateRange(range);
|
|
1201
|
+
setTeamUsageCache({});
|
|
1202
|
+
setTeamUsageLoading({});
|
|
1203
|
+
}, [setDateRange]);
|
|
1204
|
+
const loadTeamUsage = useCallback2(async (teamId) => {
|
|
780
1205
|
if (teamUsageCache[teamId] !== void 0 || teamUsageLoading[teamId]) return;
|
|
781
1206
|
setTeamUsageLoading((prev) => ({ ...prev, [teamId]: true }));
|
|
782
1207
|
try {
|
|
@@ -790,7 +1215,7 @@ var init_LiteLLMPage = __esm({
|
|
|
790
1215
|
setTeamUsageLoading((prev) => ({ ...prev, [teamId]: false }));
|
|
791
1216
|
}
|
|
792
1217
|
}, [api, dateRange, teamUsageCache, teamUsageLoading]);
|
|
793
|
-
const allowedModels =
|
|
1218
|
+
const allowedModels = useMemo3(() => {
|
|
794
1219
|
if (!allModels?.length) return [];
|
|
795
1220
|
const userModels = userInfo?.models;
|
|
796
1221
|
const teamModels = teams?.flatMap((t) => t.models ?? []);
|
|
@@ -803,7 +1228,7 @@ var init_LiteLLMPage = __esm({
|
|
|
803
1228
|
]);
|
|
804
1229
|
return allModels.filter((m) => allowed.has(m.model_name));
|
|
805
1230
|
}, [allModels, userInfo, teams]);
|
|
806
|
-
const handleGenerateKey =
|
|
1231
|
+
const handleGenerateKey = useCallback2(
|
|
807
1232
|
async (request) => {
|
|
808
1233
|
const response = await api.generateKey(request);
|
|
809
1234
|
setSnackbar({ message: "Key generated successfully", severity: "success" });
|
|
@@ -812,7 +1237,7 @@ var init_LiteLLMPage = __esm({
|
|
|
812
1237
|
},
|
|
813
1238
|
[api, refreshKeys]
|
|
814
1239
|
);
|
|
815
|
-
const handleUpdateKey =
|
|
1240
|
+
const handleUpdateKey = useCallback2(
|
|
816
1241
|
async (keyId, request) => {
|
|
817
1242
|
try {
|
|
818
1243
|
await api.updateKey(keyId, request);
|
|
@@ -825,44 +1250,87 @@ var init_LiteLLMPage = __esm({
|
|
|
825
1250
|
},
|
|
826
1251
|
[api, refreshKeys]
|
|
827
1252
|
);
|
|
828
|
-
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(
|
|
829
1299
|
async (keyId) => {
|
|
830
1300
|
try {
|
|
831
1301
|
await api.deleteKey(keyId);
|
|
832
|
-
setSnackbar({
|
|
833
|
-
message: "Key revoked successfully",
|
|
834
|
-
severity: "success"
|
|
835
|
-
});
|
|
1302
|
+
setSnackbar({ message: "Key revoked successfully", severity: "success" });
|
|
836
1303
|
refreshKeys();
|
|
837
1304
|
} catch (e) {
|
|
838
1305
|
if (e.body?.success && (e.body?.message?.includes("already deleted") || e.body?.message?.includes("never existed"))) {
|
|
839
|
-
setSnackbar({
|
|
840
|
-
message: "Key was already deleted",
|
|
841
|
-
severity: "warning"
|
|
842
|
-
});
|
|
1306
|
+
setSnackbar({ message: "Key was already deleted", severity: "warning" });
|
|
843
1307
|
refreshKeys();
|
|
844
1308
|
return;
|
|
845
1309
|
}
|
|
846
|
-
setSnackbar({
|
|
847
|
-
message: `Failed to revoke key: ${e.message}`,
|
|
848
|
-
severity: "error"
|
|
849
|
-
});
|
|
850
|
-
} finally {
|
|
851
|
-
refreshKeys();
|
|
1310
|
+
setSnackbar({ message: `Failed to revoke key: ${e.message}`, severity: "error" });
|
|
852
1311
|
}
|
|
853
1312
|
},
|
|
854
1313
|
[api, refreshKeys]
|
|
855
1314
|
);
|
|
856
1315
|
const isInitialLoading = userLoading && !userInfo;
|
|
857
1316
|
if (isInitialLoading) {
|
|
858
|
-
return /* @__PURE__ */
|
|
1317
|
+
return /* @__PURE__ */ React6.createElement(Box6, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "50vh" }, /* @__PURE__ */ React6.createElement(CircularProgress4, null));
|
|
859
1318
|
}
|
|
860
1319
|
if (userError || !userInfo) {
|
|
861
1320
|
const isProvisioningEnabled = userError?.body?.provisioning === true;
|
|
862
1321
|
const hint = userError?.body?.hint;
|
|
863
|
-
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.")));
|
|
864
1323
|
}
|
|
865
|
-
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(
|
|
866
1334
|
TeamUsage,
|
|
867
1335
|
{
|
|
868
1336
|
teams: teams ?? [],
|
|
@@ -874,7 +1342,17 @@ var init_LiteLLMPage = __esm({
|
|
|
874
1342
|
},
|
|
875
1343
|
getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false
|
|
876
1344
|
}
|
|
877
|
-
)), /* @__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(
|
|
878
1356
|
KeysTable,
|
|
879
1357
|
{
|
|
880
1358
|
keys: keys ?? [],
|
|
@@ -883,18 +1361,13 @@ var init_LiteLLMPage = __esm({
|
|
|
883
1361
|
loading: keysLoading || modelsLoading,
|
|
884
1362
|
onGenerateKey: handleGenerateKey,
|
|
885
1363
|
onUpdateKey: handleUpdateKey,
|
|
1364
|
+
onRotateKey: handleRotateKey,
|
|
1365
|
+
onBlockKey: handleBlockKey,
|
|
1366
|
+
onUnblockKey: handleUnblockKey,
|
|
1367
|
+
onResetKeySpend: handleResetKeySpend,
|
|
886
1368
|
onDeleteKey: handleDeleteKey
|
|
887
1369
|
}
|
|
888
|
-
)), /* @__PURE__ */
|
|
889
|
-
UsageStats,
|
|
890
|
-
{
|
|
891
|
-
usage: usage ?? null,
|
|
892
|
-
models: allModels ?? [],
|
|
893
|
-
dateRange,
|
|
894
|
-
onDateRangeChange: setDateRange,
|
|
895
|
-
loading: usageLoading
|
|
896
|
-
}
|
|
897
|
-
))), /* @__PURE__ */ React5.createElement(
|
|
1370
|
+
))), /* @__PURE__ */ React6.createElement(
|
|
898
1371
|
Snackbar,
|
|
899
1372
|
{
|
|
900
1373
|
open: !!snackbar,
|
|
@@ -902,7 +1375,7 @@ var init_LiteLLMPage = __esm({
|
|
|
902
1375
|
onClose: () => setSnackbar(null),
|
|
903
1376
|
anchorOrigin: { vertical: "bottom", horizontal: "right" }
|
|
904
1377
|
},
|
|
905
|
-
snackbar ? /* @__PURE__ */
|
|
1378
|
+
snackbar ? /* @__PURE__ */ React6.createElement(Alert2, { severity: snackbar.severity, onClose: () => setSnackbar(null) }, snackbar.message) : void 0
|
|
906
1379
|
));
|
|
907
1380
|
};
|
|
908
1381
|
}
|
|
@@ -910,7 +1383,7 @@ var init_LiteLLMPage = __esm({
|
|
|
910
1383
|
|
|
911
1384
|
// src/plugin.tsx
|
|
912
1385
|
init_api();
|
|
913
|
-
import
|
|
1386
|
+
import React7 from "react";
|
|
914
1387
|
import { TrendingUp as TrendingUpIcon } from "@mui/icons-material";
|
|
915
1388
|
import {
|
|
916
1389
|
createFrontendPlugin,
|
|
@@ -929,10 +1402,10 @@ var liteLlmPage = PageBlueprint.make({
|
|
|
929
1402
|
params: {
|
|
930
1403
|
path: "/litellm",
|
|
931
1404
|
title: "LiteLLM",
|
|
932
|
-
icon: /* @__PURE__ */
|
|
1405
|
+
icon: /* @__PURE__ */ React7.createElement(TrendingUpIcon, null),
|
|
933
1406
|
loader: async () => {
|
|
934
1407
|
const { LiteLLMPage: LiteLLMPage2 } = await Promise.resolve().then(() => (init_LiteLLMPage(), LiteLLMPage_exports));
|
|
935
|
-
return /* @__PURE__ */
|
|
1408
|
+
return /* @__PURE__ */ React7.createElement(LiteLLMPage2, null);
|
|
936
1409
|
}
|
|
937
1410
|
}
|
|
938
1411
|
});
|
|
@@ -950,17 +1423,17 @@ init_TeamUsage();
|
|
|
950
1423
|
|
|
951
1424
|
// src/components/LiteLLMHomeWidget.tsx
|
|
952
1425
|
init_api();
|
|
953
|
-
import
|
|
1426
|
+
import React8, { useState as useState6, useEffect } from "react";
|
|
954
1427
|
import {
|
|
955
|
-
Paper as
|
|
956
|
-
Box as
|
|
957
|
-
Typography as
|
|
1428
|
+
Paper as Paper7,
|
|
1429
|
+
Box as Box7,
|
|
1430
|
+
Typography as Typography7,
|
|
958
1431
|
FormControl as FormControl2,
|
|
959
1432
|
Select as Select2,
|
|
960
|
-
MenuItem as
|
|
1433
|
+
MenuItem as MenuItem4,
|
|
961
1434
|
Grid as Grid3,
|
|
962
1435
|
CircularProgress as CircularProgress5,
|
|
963
|
-
Alert as
|
|
1436
|
+
Alert as Alert3
|
|
964
1437
|
} from "@mui/material";
|
|
965
1438
|
import { AreaChart as AreaChart3, Area as Area3, ResponsiveContainer as ResponsiveContainer3 } from "recharts";
|
|
966
1439
|
import { useApi as useApi2 } from "@backstage/core-plugin-api";
|
|
@@ -978,17 +1451,17 @@ function presetToDateRange(preset) {
|
|
|
978
1451
|
}
|
|
979
1452
|
return { start, end };
|
|
980
1453
|
}
|
|
981
|
-
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));
|
|
982
1455
|
var LiteLLMHomeWidget = ({
|
|
983
1456
|
defaultPeriod = "7d",
|
|
984
1457
|
title = "LiteLLM Usage"
|
|
985
1458
|
}) => {
|
|
986
1459
|
const api = useApi2(liteLlmApiRef);
|
|
987
|
-
const [period, setPeriod] =
|
|
988
|
-
const [loading, setLoading] =
|
|
989
|
-
const [error, setError] =
|
|
990
|
-
const [usage, setUsage] =
|
|
991
|
-
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([]);
|
|
992
1465
|
useEffect(() => {
|
|
993
1466
|
let cancelled = false;
|
|
994
1467
|
setLoading(true);
|
|
@@ -1017,17 +1490,17 @@ var LiteLLMHomeWidget = ({
|
|
|
1017
1490
|
spend: d.spend
|
|
1018
1491
|
}));
|
|
1019
1492
|
const hasSparkline = dailyData.length > 0;
|
|
1020
|
-
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(
|
|
1021
1494
|
Select2,
|
|
1022
1495
|
{
|
|
1023
1496
|
value: period,
|
|
1024
1497
|
onChange: (e) => setPeriod(e.target.value),
|
|
1025
1498
|
displayEmpty: true
|
|
1026
1499
|
},
|
|
1027
|
-
/* @__PURE__ */
|
|
1028
|
-
/* @__PURE__ */
|
|
1029
|
-
/* @__PURE__ */
|
|
1030
|
-
))), 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(
|
|
1031
1504
|
Area3,
|
|
1032
1505
|
{
|
|
1033
1506
|
type: "monotone",
|