@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/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
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
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
- )))), budget > 0 && /* @__PURE__ */ React.createElement(Box, { minWidth: 220 }, /* @__PURE__ */ React.createElement(Box, { display: "flex", justifyContent: "space-between", mb: 0.5 }, /* @__PURE__ */ React.createElement(Typography, { variant: "body2" }, "$", spend.toFixed(2), " / $", budget.toFixed(2)), isOver && /* @__PURE__ */ React.createElement(Chip, { icon: /* @__PURE__ */ React.createElement(Warning, null), label: "Over Budget", size: "small", color: "error" }), isNear && /* @__PURE__ */ React.createElement(Chip, { label: "Near Limit", size: "small", color: "warning" })), /* @__PURE__ */ React.createElement(
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
- return /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement(Paper2, { sx: { mb: 2 } }, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", justifyContent: "space-between", alignItems: "center", p: 2 }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "h6" }, "Virtual Keys"), /* @__PURE__ */ React2.createElement(
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, "Spend"), /* @__PURE__ */ React2.createElement(TableCell, null, "Budget"), /* @__PURE__ */ React2.createElement(TableCell, null, "TPM Limit"), /* @__PURE__ */ React2.createElement(TableCell, null, "Models"), /* @__PURE__ */ React2.createElement(TableCell, { align: "right" }, "Actions"))), /* @__PURE__ */ React2.createElement(TableBody, null, loading ? /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ React2.createElement(CircularProgress, { size: 24 }))) : keys.length === 0 ? /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ React2.createElement(Typography2, { color: "text.secondary" }, "No keys found"))) : keys.map((key) => {
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
- return /* @__PURE__ */ React2.createElement(TableRow, { key: keyId }, /* @__PURE__ */ React2.createElement(TableCell, null, key.key_alias || "-"), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", alignItems: "center", gap: 0.5 }, /* @__PURE__ */ React2.createElement(
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, "$", key.spend?.toFixed(4) || "0.00"), /* @__PURE__ */ React2.createElement(TableCell, null, key.max_budget ? `$${key.max_budget}` : "-"), /* @__PURE__ */ React2.createElement(TableCell, null, key.tpm_limit || "-"), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", gap: 0.5, flexWrap: "wrap" }, key.models?.slice(0, 2).map((model) => /* @__PURE__ */ React2.createElement(Chip2, { key: model, label: model, size: "small" })), (key.models?.length || 0) > 2 && /* @__PURE__ */ React2.createElement(Chip2, { label: `+${(key.models?.length || 0) - 2}`, size: "small", variant: "outlined" }))), /* @__PURE__ */ React2.createElement(TableCell, { align: "right" }, /* @__PURE__ */ React2.createElement(IconButton, { onClick: () => handleOpenEdit(key) }, /* @__PURE__ */ React2.createElement(Edit, { fontSize: "small" })), /* @__PURE__ */ React2.createElement(IconButton, { color: "error", onClick: () => onDeleteKey(keyId) }, /* @__PURE__ */ React2.createElement(Delete, 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.model_name, m.supports_function_calling && " \u{1F527}", m.supports_vision && " \u{1F441}\uFE0F"),
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 || !canGenerate
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
- ))), /* @__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"))));
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 LinearProgress2
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
- var fmtUsd, fmtInt, fmtPct, KpiCard, UsageStats;
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 = useMemo(() => {
503
- const start = dateRange.start;
504
- const end = dateRange.end;
505
- if (start.toDateString() === end.toDateString()) return "today";
506
- const diffMs = end.getTime() - start.getTime();
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 todayRows = useMemo(() => {
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 modelRows = useMemo(() => {
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 keyRows = useMemo(
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
- if (loading) {
581
- return /* @__PURE__ */ React3.createElement(Paper3, { sx: { p: 2 } }, /* @__PURE__ */ React3.createElement(Box3, { display: "flex", justifyContent: "center", p: 4 }, /* @__PURE__ */ React3.createElement(CircularProgress2, null)));
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"), /* @__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: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (value) => [`$${value.toFixed(4)}`, "Spend"] }), /* @__PURE__ */ React3.createElement(Area, { type: "monotone", dataKey: "spend", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.3 }))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Requests"), /* @__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 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, todayLabel ? `Tokens In / Out by Model \u2014 ${todayLabel}` : "Tokens In / Out by Model \u2014 Today"), /* @__PURE__ */ React3.createElement(Box3, { height: 300 }, todayRows.length === 0 ? /* @__PURE__ */ React3.createElement(Box3, { display: "flex", alignItems: "center", justifyContent: "center", height: "100%" }, /* @__PURE__ */ React3.createElement(Typography3, { color: "text.secondary" }, "No token activity for the latest day")) : /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(BarChart, { data: todayRows, layout: "vertical", margin: { left: 60 } }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ React3.createElement(XAxis, { type: "number", tick: { fontSize: 12 }, tickFormatter: fmtInt }), /* @__PURE__ */ React3.createElement(YAxis, { type: "category", dataKey: "model", tick: { fontSize: 11 }, width: 180 }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (value) => fmtInt(value) }), /* @__PURE__ */ React3.createElement(Legend, null), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "promptTokens", name: "Input (prompt)", fill: "#8884d8", stackId: "io" }), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "completionTokens", name: "Output (completion)", fill: "#82ca9d", stackId: "io" })))))), 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 }, "Tokens by Model"), /* @__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, 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, 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(
610
- LinearProgress2,
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
- LinearProgress2,
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 LinearProgress3,
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 CircularProgress3
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
- LinearProgress3,
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(CircularProgress3, { 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(
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(LinearProgress3, null));
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 React5, { useState as useState4, useCallback, useMemo as useMemo2 } from "react";
716
- import { Grid as Grid2, Box as Box5, Snackbar, Alert, CircularProgress as CircularProgress4, Typography as Typography5, Paper as Paper5 } from "@mui/material";
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
- var LiteLLMPage;
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] = useState4(() => {
731
- const end = /* @__PURE__ */ new Date();
732
- const start = /* @__PURE__ */ new Date();
733
- start.setDate(start.getDate() - 7);
734
- return { start, end };
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 } = useAsync(
1174
+ const { value: allModels, loading: modelsLoading } = useAsync2(
755
1175
  () => api.listModels().catch(() => []),
756
1176
  [api]
757
1177
  );
758
- const { value: allTeams, loading: teamsLoading } = useAsync(
1178
+ const { value: allTeams, loading: teamsLoading } = useAsync2(
759
1179
  () => api.getTeams().catch(() => []),
760
1180
  [api]
761
1181
  );
762
- const teams = useMemo2(() => {
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 } = useAsync(async () => {
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 loadTeamUsage = useCallback(async (teamId) => {
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 = useMemo2(() => {
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 = useCallback(
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 = useCallback(
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 handleDeleteKey = useCallback(
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__ */ React5.createElement(Box5, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "50vh" }, /* @__PURE__ */ React5.createElement(CircularProgress4, null));
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__ */ React5.createElement(Box5, { p: 3 }, /* @__PURE__ */ React5.createElement(Paper5, { sx: { p: 3 } }, /* @__PURE__ */ React5.createElement(Typography5, { variant: "h6", gutterBottom: true }, "Account not provisioned"), /* @__PURE__ */ React5.createElement(Typography5, { color: "text.secondary", paragraph: true }, "Your Backstage account is not linked to a LiteLLM user."), hint ? /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2", color: "text.secondary" }, hint) : /* @__PURE__ */ React5.createElement(Typography5, { 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.")));
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__ */ React5.createElement(Box5, { p: 3 }, /* @__PURE__ */ React5.createElement(Grid2, { container: true, spacing: 2 }, /* @__PURE__ */ React5.createElement(Grid2, { item: true, xs: 12 }, /* @__PURE__ */ React5.createElement(DashboardHeader, { userInfo, teams: teams ?? [], loading: userLoading || teamsLoading })), /* @__PURE__ */ React5.createElement(Grid2, { item: true, xs: 12 }, /* @__PURE__ */ React5.createElement(
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__ */ React5.createElement(Grid2, { item: true, xs: 12 }, /* @__PURE__ */ React5.createElement(
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__ */ React5.createElement(Grid2, { item: true, xs: 12 }, /* @__PURE__ */ React5.createElement(
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__ */ React5.createElement(Alert, { severity: snackbar.severity, onClose: () => setSnackbar(null) }, snackbar.message) : void 0
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 React6 from "react";
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__ */ React6.createElement(TrendingUpIcon, null),
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__ */ React6.createElement(LiteLLMPage2, null);
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 React7, { useState as useState5, useEffect } from "react";
1426
+ import React8, { useState as useState6, useEffect } from "react";
954
1427
  import {
955
- Paper as Paper6,
956
- Box as Box6,
957
- Typography as Typography6,
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 MenuItem3,
1433
+ MenuItem as MenuItem4,
961
1434
  Grid as Grid3,
962
1435
  CircularProgress as CircularProgress5,
963
- Alert as Alert2
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__ */ React7.createElement(Box6, null, /* @__PURE__ */ React7.createElement(Typography6, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ React7.createElement(Typography6, { variant: "subtitle1", fontWeight: 600 }, value));
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] = useState5(defaultPeriod);
988
- const [loading, setLoading] = useState5(true);
989
- const [error, setError] = useState5(null);
990
- const [usage, setUsage] = useState5(null);
991
- const [keys, setKeys] = useState5([]);
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__ */ React7.createElement(Paper6, { sx: { p: 2 } }, /* @__PURE__ */ React7.createElement(Box6, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1.5 }, /* @__PURE__ */ React7.createElement(Typography6, { variant: "h6" }, title), /* @__PURE__ */ React7.createElement(FormControl2, { size: "small", sx: { minWidth: 90 } }, /* @__PURE__ */ React7.createElement(
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__ */ React7.createElement(MenuItem3, { value: "today" }, "Today"),
1028
- /* @__PURE__ */ React7.createElement(MenuItem3, { value: "7d" }, "7d"),
1029
- /* @__PURE__ */ React7.createElement(MenuItem3, { value: "30d" }, "30d")
1030
- ))), loading && /* @__PURE__ */ React7.createElement(Box6, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ React7.createElement(CircularProgress5, { size: 32 })), !loading && error && /* @__PURE__ */ React7.createElement(Alert2, { severity: "error", sx: { mt: 1 } }, error), !loading && !error && /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement(Grid3, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ React7.createElement(Grid3, { item: true, xs: 6 }, /* @__PURE__ */ React7.createElement(Kpi, { label: "USD Spent", value: fmtUsd2(usage?.total_spend ?? 0) })), /* @__PURE__ */ React7.createElement(Grid3, { item: true, xs: 6 }, /* @__PURE__ */ React7.createElement(Kpi, { label: "Tokens In", value: fmtInt2(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ React7.createElement(Grid3, { item: true, xs: 6 }, /* @__PURE__ */ React7.createElement(Kpi, { label: "Tokens Out", value: fmtInt2(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ React7.createElement(Grid3, { item: true, xs: 6 }, /* @__PURE__ */ React7.createElement(Kpi, { label: "Keys", value: fmtInt2(keys.length) }))), hasSparkline && /* @__PURE__ */ React7.createElement(Box6, { height: 120 }, /* @__PURE__ */ React7.createElement(ResponsiveContainer3, { width: "100%", height: "100%" }, /* @__PURE__ */ React7.createElement(AreaChart3, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 } }, /* @__PURE__ */ React7.createElement(
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",