@acarmisc/backstage-plugin-litellm 0.4.0 → 0.5.0

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