@acarmisc/backstage-plugin-litellm 0.4.0 → 0.6.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.cjs.js CHANGED
@@ -5,8 +5,13 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __esm = (fn, res) => function __init() {
9
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ var __esm = (fn, res, err) => function __init() {
9
+ if (err) throw err[0];
10
+ try {
11
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
12
+ } catch (e) {
13
+ throw err = [e], e;
14
+ }
10
15
  };
11
16
  var __export = (target, all) => {
12
17
  for (var name in all)
@@ -105,6 +110,29 @@ var init_api = __esm({
105
110
  async deleteKey(keyId) {
106
111
  return this.del(`/keys/${encodeURIComponent(keyId)}`);
107
112
  }
113
+ async rotateKey(keyId) {
114
+ return this.post(`/keys/${encodeURIComponent(keyId)}/regenerate`, {});
115
+ }
116
+ async blockKey(keyId) {
117
+ await this.post(`/keys/${encodeURIComponent(keyId)}/block`, {});
118
+ }
119
+ async unblockKey(keyId) {
120
+ await this.post(`/keys/${encodeURIComponent(keyId)}/unblock`, {});
121
+ }
122
+ async resetKeySpend(keyId) {
123
+ await this.post(`/keys/${encodeURIComponent(keyId)}/reset_spend`, {});
124
+ }
125
+ async getAuditLogs(params) {
126
+ const strParams = {};
127
+ if (params.page !== void 0) strParams.page = String(params.page);
128
+ if (params.page_size !== void 0) strParams.page_size = String(params.page_size);
129
+ if (params.start_date) strParams.start_date = params.start_date;
130
+ if (params.end_date) strParams.end_date = params.end_date;
131
+ if (params.action) strParams.action = params.action;
132
+ if (params.table_name) strParams.table_name = params.table_name;
133
+ if (params.changed_by) strParams.changed_by = params.changed_by;
134
+ return this.get("/audit", strParams);
135
+ }
108
136
  async listModels() {
109
137
  return this.get("/models");
110
138
  }
@@ -151,6 +179,33 @@ var init_DashboardHeader = __esm({
151
179
  });
152
180
 
153
181
  // src/components/KeysTable.tsx
182
+ function expiryStatus(expiresAt) {
183
+ if (!expiresAt) return null;
184
+ const diff = new Date(expiresAt).getTime() - Date.now();
185
+ const days = diff / (1e3 * 60 * 60 * 24);
186
+ if (days < 0) return "expired";
187
+ if (days < 7) return "soon";
188
+ return "ok";
189
+ }
190
+ function ExpiryChip({ expiresAt }) {
191
+ const status = expiryStatus(expiresAt);
192
+ if (!status) return /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2", color: "text.secondary" }, "-");
193
+ const label = status === "expired" ? "Expired" : status === "soon" ? `${Math.ceil((new Date(expiresAt).getTime() - Date.now()) / 864e5)}d left` : formatDate(expiresAt);
194
+ const color = status === "expired" ? "error" : status === "soon" ? "warning" : "default";
195
+ const icon = status === "expired" || status === "soon" ? /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Warning, { fontSize: "small" }) : void 0;
196
+ return /* @__PURE__ */ import_react2.default.createElement(import_material2.Chip, { label, color, size: "small", icon });
197
+ }
198
+ function BudgetCell({ spend, maxBudget }) {
199
+ if (!maxBudget) return /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2", color: "text.secondary" }, "-");
200
+ const pct = Math.min(100, spend / maxBudget * 100);
201
+ const color = pct >= 100 ? "error" : pct >= 80 ? "warning" : "primary";
202
+ return /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { minWidth: 100 }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", justifyContent: "space-between" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "caption" }, "$", spend.toFixed(2)), /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "caption", color: "text.secondary" }, "$", maxBudget)), /* @__PURE__ */ import_react2.default.createElement(import_material2.LinearProgress, { variant: "determinate", value: pct, color, sx: { height: 5, borderRadius: 1, mt: 0.25 } }));
203
+ }
204
+ function fmtCost(perToken) {
205
+ if (!perToken) return null;
206
+ const per1k = perToken * 1e3;
207
+ return per1k < 0.01 ? `$${(perToken * 1e6).toFixed(2)}/M` : `$${per1k.toFixed(3)}/1K`;
208
+ }
154
209
  var import_react2, import_material2, import_icons_material, maskKey, shortKeyId, formatDate, emptyForm, keyToEditForm, KeysTable;
155
210
  var init_KeysTable = __esm({
156
211
  "src/components/KeysTable.tsx"() {
@@ -180,6 +235,7 @@ var init_KeysTable = __esm({
180
235
  duration: "30d",
181
236
  max_budget: 100,
182
237
  tpm_limit: void 0,
238
+ rpm_limit: void 0,
183
239
  team_id: void 0,
184
240
  key_type: "llm_api"
185
241
  });
@@ -197,16 +253,34 @@ var init_KeysTable = __esm({
197
253
  loading,
198
254
  onGenerateKey,
199
255
  onUpdateKey,
256
+ onRotateKey,
257
+ onBlockKey,
258
+ onUnblockKey,
259
+ onResetKeySpend,
200
260
  onDeleteKey
201
261
  }) => {
202
262
  const [generateModalOpen, setGenerateModalOpen] = (0, import_react2.useState)(false);
203
263
  const [newKeyValue, setNewKeyValue] = (0, import_react2.useState)(null);
204
264
  const [formData, setFormData] = (0, import_react2.useState)(emptyForm());
205
265
  const [submitting, setSubmitting] = (0, import_react2.useState)(false);
206
- const canGenerate = true;
207
266
  const [editingKey, setEditingKey] = (0, import_react2.useState)(null);
208
267
  const [editForm, setEditForm] = (0, import_react2.useState)({});
209
268
  const [editSubmitting, setEditSubmitting] = (0, import_react2.useState)(false);
269
+ const [rotatingKeyId, setRotatingKeyId] = (0, import_react2.useState)(null);
270
+ const [rotatedKeyValue, setRotatedKeyValue] = (0, import_react2.useState)(null);
271
+ const [blockingKeyId, setBlockingKeyId] = (0, import_react2.useState)(null);
272
+ const [deleteConfirmId, setDeleteConfirmId] = (0, import_react2.useState)(null);
273
+ const [deleteSubmitting, setDeleteSubmitting] = (0, import_react2.useState)(false);
274
+ const [resetSpendConfirm, setResetSpendConfirm] = (0, import_react2.useState)(false);
275
+ const [resetSpendSubmitting, setResetSpendSubmitting] = (0, import_react2.useState)(false);
276
+ const [filterText, setFilterText] = (0, import_react2.useState)("");
277
+ const filteredKeys = (0, import_react2.useMemo)(() => {
278
+ if (!filterText.trim()) return keys;
279
+ const q = filterText.toLowerCase();
280
+ return keys.filter(
281
+ (k) => (k.key_alias ?? "").toLowerCase().includes(q) || k.models?.some((m) => m.toLowerCase().includes(q))
282
+ );
283
+ }, [keys, filterText]);
210
284
  const selectedModels = models.filter((m) => (formData.models || []).includes(m.model_name));
211
285
  const selectedTeam = teams.find((t) => t.team_id === formData.team_id) ?? null;
212
286
  const editSelectedModels = models.filter((m) => (editForm.models || []).includes(m.model_name));
@@ -234,6 +308,7 @@ var init_KeysTable = __esm({
234
308
  const handleCloseEdit = () => {
235
309
  setEditingKey(null);
236
310
  setEditForm({});
311
+ setResetSpendConfirm(false);
237
312
  };
238
313
  const handleUpdate = async () => {
239
314
  if (!editingKey) return;
@@ -247,10 +322,72 @@ var init_KeysTable = __esm({
247
322
  setEditSubmitting(false);
248
323
  }
249
324
  };
325
+ const handleRotate = async (keyId) => {
326
+ setRotatingKeyId(keyId);
327
+ try {
328
+ const response = await onRotateKey(keyId);
329
+ setRotatedKeyValue(response.key);
330
+ } catch (error) {
331
+ console.error("Failed to rotate key:", error);
332
+ } finally {
333
+ setRotatingKeyId(null);
334
+ }
335
+ };
336
+ const handleToggleBlock = async (key) => {
337
+ const keyId = key.token ?? key.key;
338
+ setBlockingKeyId(keyId);
339
+ try {
340
+ if (key.blocked) {
341
+ await onUnblockKey(keyId);
342
+ } else {
343
+ await onBlockKey(keyId);
344
+ }
345
+ } finally {
346
+ setBlockingKeyId(null);
347
+ }
348
+ };
349
+ const handleResetSpend = async () => {
350
+ if (!editingKey) return;
351
+ setResetSpendSubmitting(true);
352
+ try {
353
+ await onResetKeySpend(editingKey.token ?? editingKey.key);
354
+ setResetSpendConfirm(false);
355
+ handleCloseEdit();
356
+ } finally {
357
+ setResetSpendSubmitting(false);
358
+ }
359
+ };
360
+ const handleConfirmDelete = async () => {
361
+ if (!deleteConfirmId) return;
362
+ setDeleteSubmitting(true);
363
+ try {
364
+ await onDeleteKey(deleteConfirmId);
365
+ } finally {
366
+ setDeleteSubmitting(false);
367
+ setDeleteConfirmId(null);
368
+ }
369
+ };
250
370
  const copyToClipboard = (text) => {
251
371
  navigator.clipboard.writeText(text);
252
372
  };
253
- return /* @__PURE__ */ import_react2.default.createElement(import_react2.default.Fragment, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Paper, { sx: { mb: 2 } }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", justifyContent: "space-between", alignItems: "center", p: 2 }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "h6" }, "Virtual Keys"), /* @__PURE__ */ import_react2.default.createElement(
373
+ const modelOption = (m) => {
374
+ const inCost = fmtCost(m.input_cost_per_token);
375
+ const outCost = fmtCost(m.output_cost_per_token);
376
+ return /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, null, /* @__PURE__ */ import_react2.default.createElement("span", null, m.model_name), m.supports_function_calling && " \u{1F527}", m.supports_vision && " \u{1F441}\uFE0F", (inCost || outCost) && /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, inCost, " in \xB7 ", outCost, " out"));
377
+ };
378
+ return /* @__PURE__ */ import_react2.default.createElement(import_react2.default.Fragment, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Paper, { sx: { mb: 2 } }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", justifyContent: "space-between", alignItems: "center", p: 2, gap: 2 }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "h6" }, "Virtual Keys"), /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", gap: 1, alignItems: "center", flex: 1, justifyContent: "flex-end" }, /* @__PURE__ */ import_react2.default.createElement(
379
+ import_material2.TextField,
380
+ {
381
+ size: "small",
382
+ placeholder: "Filter by alias or model\u2026",
383
+ value: filterText,
384
+ onChange: (e) => setFilterText(e.target.value),
385
+ sx: { minWidth: 240 },
386
+ InputProps: {
387
+ startAdornment: /* @__PURE__ */ import_react2.default.createElement(import_material2.InputAdornment, { position: "start" }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Search, { fontSize: "small" }))
388
+ }
389
+ }
390
+ ), /* @__PURE__ */ import_react2.default.createElement(
254
391
  import_material2.Button,
255
392
  {
256
393
  variant: "contained",
@@ -259,9 +396,11 @@ var init_KeysTable = __esm({
259
396
  onClick: () => setGenerateModalOpen(true)
260
397
  },
261
398
  "Generate New Key"
262
- )), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableContainer, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Table, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableHead, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Alias"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Key ID"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Created"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Spend"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Budget"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "TPM Limit"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Models"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { align: "right" }, "Actions"))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableBody, null, loading ? /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 24 }))) : keys.length === 0 ? /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { color: "text.secondary" }, "No keys found"))) : keys.map((key) => {
399
+ ))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableContainer, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Table, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableHead, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Alias"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Key ID"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Created"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Expires"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Budget"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "TPM / RPM"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Models"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { align: "right" }, "Actions"))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableBody, null, loading ? /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 24 }))) : filteredKeys.length === 0 ? /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { color: "text.secondary" }, filterText ? "No keys match filter" : "No keys found"))) : filteredKeys.map((key) => {
263
400
  const keyId = key.token ?? key.key;
264
- return /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, { key: keyId }, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, key.key_alias || "-"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", alignItems: "center", gap: 0.5 }, /* @__PURE__ */ import_react2.default.createElement(
401
+ const isRotating = rotatingKeyId === keyId;
402
+ const isBlocking = blockingKeyId === keyId;
403
+ return /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, { key: keyId, sx: key.blocked ? { bgcolor: "action.disabledBackground" } : void 0 }, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", alignItems: "center", gap: 0.5 }, key.key_alias || "-", key.blocked && /* @__PURE__ */ import_react2.default.createElement(import_material2.Chip, { label: "Blocked", color: "error", size: "small" }))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", alignItems: "center", gap: 0.5 }, /* @__PURE__ */ import_react2.default.createElement(
265
404
  import_material2.Typography,
266
405
  {
267
406
  variant: "body2",
@@ -277,7 +416,32 @@ var init_KeysTable = __esm({
277
416
  }
278
417
  },
279
418
  shortKeyId(keyId)
280
- ), /* @__PURE__ */ import_react2.default.createElement(import_material2.IconButton, { size: "small", onClick: () => copyToClipboard(keyId), title: "Copy Key ID" }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material.ContentCopy, { fontSize: "small" })))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, formatDate(key.created_at)), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "$", key.spend?.toFixed(4) || "0.00"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, key.max_budget ? `$${key.max_budget}` : "-"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, key.tpm_limit || "-"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", gap: 0.5, flexWrap: "wrap" }, key.models?.slice(0, 2).map((model) => /* @__PURE__ */ import_react2.default.createElement(import_material2.Chip, { key: model, label: model, size: "small" })), (key.models?.length || 0) > 2 && /* @__PURE__ */ import_react2.default.createElement(import_material2.Chip, { label: `+${(key.models?.length || 0) - 2}`, size: "small", variant: "outlined" }))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { align: "right" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.IconButton, { onClick: () => handleOpenEdit(key) }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Edit, { fontSize: "small" })), /* @__PURE__ */ import_react2.default.createElement(import_material2.IconButton, { color: "error", onClick: () => onDeleteKey(keyId) }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Delete, null))));
419
+ ), /* @__PURE__ */ import_react2.default.createElement(import_material2.IconButton, { size: "small", onClick: () => copyToClipboard(keyId), title: "Copy Key ID" }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material.ContentCopy, { fontSize: "small" })))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, formatDate(key.created_at)), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(ExpiryChip, { expiresAt: key.expires_at })), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(BudgetCell, { spend: key.spend ?? 0, maxBudget: key.max_budget })), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2" }, key.tpm_limit ?? "-", " / ", key.rpm_limit ?? "-")), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", gap: 0.5, flexWrap: "wrap" }, key.models?.slice(0, 2).map((model) => /* @__PURE__ */ import_react2.default.createElement(import_material2.Chip, { key: model, label: model, size: "small" })), (key.models?.length || 0) > 2 && /* @__PURE__ */ import_react2.default.createElement(import_material2.Chip, { label: `+${(key.models?.length || 0) - 2}`, size: "small", variant: "outlined" }))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { align: "right" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.IconButton, { onClick: () => handleOpenEdit(key), title: "Edit key" }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Edit, { fontSize: "small" })), /* @__PURE__ */ import_react2.default.createElement(
420
+ import_material2.IconButton,
421
+ {
422
+ onClick: () => handleRotate(keyId),
423
+ disabled: isRotating,
424
+ title: "Rotate key \u2014 generates a new secret, same settings"
425
+ },
426
+ isRotating ? /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 18 }) : /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Autorenew, { fontSize: "small" })
427
+ ), /* @__PURE__ */ import_react2.default.createElement(
428
+ import_material2.IconButton,
429
+ {
430
+ onClick: () => handleToggleBlock(key),
431
+ disabled: isBlocking,
432
+ color: key.blocked ? "warning" : "default",
433
+ title: key.blocked ? "Unblock key" : "Block key \u2014 suspends without revoking"
434
+ },
435
+ isBlocking ? /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 18 }) : key.blocked ? /* @__PURE__ */ import_react2.default.createElement(import_icons_material.LockOpen, { fontSize: "small" }) : /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Lock, { fontSize: "small" })
436
+ ), /* @__PURE__ */ import_react2.default.createElement(
437
+ import_material2.IconButton,
438
+ {
439
+ color: "error",
440
+ onClick: () => setDeleteConfirmId(keyId),
441
+ title: "Revoke key"
442
+ },
443
+ /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Delete, null)
444
+ )));
281
445
  }))))), /* @__PURE__ */ import_react2.default.createElement(import_material2.Dialog, { open: generateModalOpen, onClose: handleCloseModal, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogTitle, null, newKeyValue ? "Key Generated" : "Generate New Key"), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogContent, null, newKeyValue ? /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2", color: "text.secondary", gutterBottom: true }, "Copy this key now. You won't be able to see it again."), /* @__PURE__ */ import_react2.default.createElement(
282
446
  import_material2.Box,
283
447
  {
@@ -352,7 +516,7 @@ var init_KeysTable = __esm({
352
516
  getOptionLabel: (m) => m.model_name,
353
517
  value: selectedModels,
354
518
  onChange: (_e, selected) => setFormData({ ...formData, models: selected.map((m) => m.model_name) }),
355
- renderOption: (props, m) => /* @__PURE__ */ import_react2.default.createElement("li", { ...props }, m.model_name, m.supports_function_calling && " \u{1F527}", m.supports_vision && " \u{1F441}\uFE0F"),
519
+ renderOption: (props, m) => /* @__PURE__ */ import_react2.default.createElement("li", { ...props }, modelOption(m)),
356
520
  renderInput: (params) => /* @__PURE__ */ import_react2.default.createElement(import_material2.TextField, { ...params, label: "Models", helperText: "Leave empty to allow all models", fullWidth: true })
357
521
  }
358
522
  ), /* @__PURE__ */ import_react2.default.createElement(
@@ -374,13 +538,22 @@ var init_KeysTable = __esm({
374
538
  onChange: (e) => setFormData({ ...formData, tpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
375
539
  fullWidth: true
376
540
  }
541
+ ), /* @__PURE__ */ import_react2.default.createElement(
542
+ import_material2.TextField,
543
+ {
544
+ label: "RPM Limit",
545
+ type: "number",
546
+ value: formData.rpm_limit ?? "",
547
+ onChange: (e) => setFormData({ ...formData, rpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
548
+ fullWidth: true
549
+ }
377
550
  ))), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogActions, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: handleCloseModal }, newKeyValue ? "Done" : "Cancel"), !newKeyValue && /* @__PURE__ */ import_react2.default.createElement(
378
551
  import_material2.Button,
379
552
  {
380
553
  onClick: handleGenerate,
381
554
  variant: "contained",
382
555
  color: "primary",
383
- disabled: submitting || !canGenerate
556
+ disabled: submitting
384
557
  },
385
558
  submitting ? /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 24 }) : "Generate"
386
559
  ), newKeyValue && /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: handleCloseModal, variant: "contained", color: "success" }, "Done"))), /* @__PURE__ */ import_react2.default.createElement(import_material2.Dialog, { open: !!editingKey, onClose: handleCloseEdit, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogTitle, null, "Edit Key"), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogContent, null, editingKey && /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2", color: "text.secondary" }, /* @__PURE__ */ import_react2.default.createElement("code", { style: { fontFamily: "monospace", color: "inherit" } }, maskKey(editingKey.key))), /* @__PURE__ */ import_react2.default.createElement(
@@ -400,6 +573,7 @@ var init_KeysTable = __esm({
400
573
  getOptionLabel: (m) => m.model_name,
401
574
  value: editSelectedModels,
402
575
  onChange: (_e, selected) => setEditForm({ ...editForm, models: selected.map((m) => m.model_name) }),
576
+ renderOption: (props, m) => /* @__PURE__ */ import_react2.default.createElement("li", { ...props }, modelOption(m)),
403
577
  renderInput: (params) => /* @__PURE__ */ import_react2.default.createElement(import_material2.TextField, { ...params, label: "Models", fullWidth: true })
404
578
  }
405
579
  ), /* @__PURE__ */ import_react2.default.createElement(
@@ -429,7 +603,54 @@ var init_KeysTable = __esm({
429
603
  onChange: (e) => setEditForm({ ...editForm, rpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
430
604
  fullWidth: true
431
605
  }
432
- ))), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogActions, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: handleCloseEdit }, "Cancel"), /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: handleUpdate, variant: "contained", color: "primary", disabled: editSubmitting }, editSubmitting ? /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 24 }) : "Save"))));
606
+ ), /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { mt: 1, pt: 2, borderTop: "1px solid", sx: { borderColor: "divider" } }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "caption", color: "text.secondary", display: "block", mb: 1 }, "Danger Zone"), !resetSpendConfirm ? /* @__PURE__ */ import_react2.default.createElement(
607
+ import_material2.Button,
608
+ {
609
+ size: "small",
610
+ color: "warning",
611
+ variant: "outlined",
612
+ onClick: () => setResetSpendConfirm(true)
613
+ },
614
+ "Reset Spend to $0"
615
+ ) : /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2", color: "warning.main" }, "Zero out spend counter?"), /* @__PURE__ */ import_react2.default.createElement(
616
+ import_material2.Button,
617
+ {
618
+ size: "small",
619
+ color: "warning",
620
+ variant: "contained",
621
+ disabled: resetSpendSubmitting,
622
+ onClick: handleResetSpend
623
+ },
624
+ resetSpendSubmitting ? /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 16 }) : "Confirm"
625
+ ), /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { size: "small", onClick: () => setResetSpendConfirm(false) }, "Cancel"))))), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogActions, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: handleCloseEdit }, "Cancel"), /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: handleUpdate, variant: "contained", color: "primary", disabled: editSubmitting }, editSubmitting ? /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 24 }) : "Save"))), /* @__PURE__ */ import_react2.default.createElement(import_material2.Dialog, { open: !!rotatedKeyValue, onClose: () => setRotatedKeyValue(null), maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogTitle, null, "Key Rotated"), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogContent, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { 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__ */ import_react2.default.createElement(
626
+ import_material2.Box,
627
+ {
628
+ display: "flex",
629
+ alignItems: "center",
630
+ gap: 1,
631
+ mt: 2,
632
+ p: 2,
633
+ sx: { backgroundColor: "action.hover", border: "1px solid", borderColor: "divider", borderRadius: 1 }
634
+ },
635
+ /* @__PURE__ */ import_react2.default.createElement(
636
+ import_material2.Typography,
637
+ {
638
+ component: "code",
639
+ sx: { fontFamily: "monospace", wordBreak: "break-all", flex: 1 }
640
+ },
641
+ rotatedKeyValue
642
+ ),
643
+ /* @__PURE__ */ import_react2.default.createElement(import_material2.IconButton, { onClick: () => copyToClipboard(rotatedKeyValue) }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material.ContentCopy, null))
644
+ )), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogActions, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: () => setRotatedKeyValue(null), variant: "contained", color: "success" }, "Done"))), /* @__PURE__ */ import_react2.default.createElement(import_material2.Dialog, { open: !!deleteConfirmId, onClose: () => setDeleteConfirmId(null), maxWidth: "xs", fullWidth: true }, /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogTitle, null, "Revoke Key?"), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogContent, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, null, "This will permanently revoke the key. Any integrations using it will stop working immediately.")), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogActions, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: () => setDeleteConfirmId(null), disabled: deleteSubmitting }, "Cancel"), /* @__PURE__ */ import_react2.default.createElement(
645
+ import_material2.Button,
646
+ {
647
+ onClick: handleConfirmDelete,
648
+ variant: "contained",
649
+ color: "error",
650
+ disabled: deleteSubmitting
651
+ },
652
+ deleteSubmitting ? /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 20 }) : "Revoke"
653
+ ))));
433
654
  };
434
655
  }
435
656
  });
@@ -703,6 +924,121 @@ var init_TeamUsage = __esm({
703
924
  }
704
925
  });
705
926
 
927
+ // src/components/AuditLog.tsx
928
+ function actionColor(action) {
929
+ if (!action) return "default";
930
+ for (const [key, color] of Object.entries(ACTION_COLORS)) {
931
+ if (action.toLowerCase().includes(key)) return color;
932
+ }
933
+ return "info";
934
+ }
935
+ function formatDateTime(iso) {
936
+ try {
937
+ return new Date(iso).toLocaleString();
938
+ } catch {
939
+ return iso;
940
+ }
941
+ }
942
+ var import_react5, import_material5, import_icons_material3, import_react_use, ACTION_COLORS, DetailRow, AuditLog;
943
+ var init_AuditLog = __esm({
944
+ "src/components/AuditLog.tsx"() {
945
+ "use strict";
946
+ import_react5 = __toESM(require("react"));
947
+ import_material5 = require("@mui/material");
948
+ import_icons_material3 = require("@mui/icons-material");
949
+ import_react_use = require("react-use");
950
+ ACTION_COLORS = {
951
+ created: "success",
952
+ deleted: "error",
953
+ updated: "warning",
954
+ blocked: "error",
955
+ unblocked: "success"
956
+ };
957
+ DetailRow = ({ entry }) => {
958
+ const [open, setOpen] = (0, import_react5.useState)(false);
959
+ const hasDetail = entry.before_value || entry.updated_values;
960
+ return /* @__PURE__ */ import_react5.default.createElement(import_react5.default.Fragment, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.TableRow, { hover: true }, /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, { sx: { width: 40, pr: 0 } }, hasDetail && /* @__PURE__ */ import_react5.default.createElement(import_material5.IconButton, { size: "small", onClick: () => setOpen((o) => !o) }, open ? /* @__PURE__ */ import_react5.default.createElement(import_icons_material3.KeyboardArrowUp, { fontSize: "small" }) : /* @__PURE__ */ import_react5.default.createElement(import_icons_material3.KeyboardArrowDown, { fontSize: "small" }))), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, { sx: { whiteSpace: "nowrap" } }, formatDateTime(entry.updated_at)), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, entry.action && /* @__PURE__ */ import_react5.default.createElement(import_material5.Chip, { label: entry.action, color: actionColor(entry.action), size: "small" })), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, entry.table_name ?? "-"), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { 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__ */ import_react5.default.createElement(import_material5.TableCell, null, entry.changed_by ?? "-")), hasDetail && /* @__PURE__ */ import_react5.default.createElement(import_material5.TableRow, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, { colSpan: 6, sx: { py: 0 } }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Collapse, { in: open, unmountOnExit: true }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { p: 2, display: "flex", gap: 2, flexWrap: "wrap" }, entry.before_value && /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { flex: 1, minWidth: 200 }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "caption", color: "text.secondary", display: "block", mb: 0.5 }, "Before"), /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { component: "pre", variant: "caption", sx: { fontFamily: "monospace", whiteSpace: "pre-wrap", wordBreak: "break-all" } }, JSON.stringify(entry.before_value, null, 2))), entry.updated_values && /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { flex: 1, minWidth: 200 }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "caption", color: "text.secondary", display: "block", mb: 0.5 }, "After"), /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { component: "pre", variant: "caption", sx: { fontFamily: "monospace", whiteSpace: "pre-wrap", wordBreak: "break-all" } }, JSON.stringify(entry.updated_values, null, 2))))))));
961
+ };
962
+ AuditLog = ({ api }) => {
963
+ const [page, setPage] = (0, import_react5.useState)(0);
964
+ const [pageSize, setPageSize] = (0, import_react5.useState)(25);
965
+ const [filters, setFilters] = (0, import_react5.useState)({});
966
+ const fetchParams = (0, import_react5.useCallback)(
967
+ () => ({ page: page + 1, page_size: pageSize, ...filters }),
968
+ [page, pageSize, filters]
969
+ )();
970
+ const { value, loading, error } = (0, import_react_use.useAsync)(
971
+ () => api.getAuditLogs(fetchParams),
972
+ [api, fetchParams]
973
+ );
974
+ const entries = value?.audit_logs ?? [];
975
+ const total = value?.total ?? 0;
976
+ return /* @__PURE__ */ import_react5.default.createElement(import_material5.Paper, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { p: 2, display: "flex", gap: 2, flexWrap: "wrap", alignItems: "center" }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "h6", sx: { flex: "0 0 auto", mr: 1 } }, "Audit Log"), /* @__PURE__ */ import_react5.default.createElement(
977
+ import_material5.TextField,
978
+ {
979
+ size: "small",
980
+ label: "Action",
981
+ select: true,
982
+ value: filters.action ?? "",
983
+ onChange: (e) => {
984
+ setFilters((f) => ({ ...f, action: e.target.value || void 0 }));
985
+ setPage(0);
986
+ },
987
+ sx: { minWidth: 160 }
988
+ },
989
+ /* @__PURE__ */ import_react5.default.createElement(import_material5.MenuItem, { value: "" }, "All actions"),
990
+ /* @__PURE__ */ import_react5.default.createElement(import_material5.MenuItem, { value: "created" }, "Created"),
991
+ /* @__PURE__ */ import_react5.default.createElement(import_material5.MenuItem, { value: "updated" }, "Updated"),
992
+ /* @__PURE__ */ import_react5.default.createElement(import_material5.MenuItem, { value: "deleted" }, "Deleted"),
993
+ /* @__PURE__ */ import_react5.default.createElement(import_material5.MenuItem, { value: "blocked" }, "Blocked")
994
+ ), /* @__PURE__ */ import_react5.default.createElement(
995
+ import_material5.TextField,
996
+ {
997
+ size: "small",
998
+ label: "Table",
999
+ select: true,
1000
+ value: filters.table_name ?? "",
1001
+ onChange: (e) => {
1002
+ setFilters((f) => ({ ...f, table_name: e.target.value || void 0 }));
1003
+ setPage(0);
1004
+ },
1005
+ sx: { minWidth: 160 }
1006
+ },
1007
+ /* @__PURE__ */ import_react5.default.createElement(import_material5.MenuItem, { value: "" }, "All tables"),
1008
+ /* @__PURE__ */ import_react5.default.createElement(import_material5.MenuItem, { value: "LiteLLM_VerificationToken" }, "Key"),
1009
+ /* @__PURE__ */ import_react5.default.createElement(import_material5.MenuItem, { value: "LiteLLM_TeamTable" }, "Team"),
1010
+ /* @__PURE__ */ import_react5.default.createElement(import_material5.MenuItem, { value: "LiteLLM_UserTable" }, "User")
1011
+ ), /* @__PURE__ */ import_react5.default.createElement(
1012
+ import_material5.TextField,
1013
+ {
1014
+ size: "small",
1015
+ label: "Changed by",
1016
+ value: filters.changed_by ?? "",
1017
+ onChange: (e) => {
1018
+ setFilters((f) => ({ ...f, changed_by: e.target.value || void 0 }));
1019
+ setPage(0);
1020
+ },
1021
+ sx: { minWidth: 200 }
1022
+ }
1023
+ )), error && /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { px: 2, pb: 2 }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Alert, { severity: "error" }, error.message)), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableContainer, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.Table, { size: "small" }, /* @__PURE__ */ import_react5.default.createElement(import_material5.TableHead, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.TableRow, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, { sx: { width: 40 } }), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, "Time"), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, "Action"), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, "Table"), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, "Object ID"), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, "Changed By"))), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableBody, null, loading ? /* @__PURE__ */ import_react5.default.createElement(import_material5.TableRow, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, { colSpan: 6, align: "center", sx: { py: 4 } }, /* @__PURE__ */ import_react5.default.createElement(import_material5.CircularProgress, { size: 24 }))) : entries.length === 0 ? /* @__PURE__ */ import_react5.default.createElement(import_material5.TableRow, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, { colSpan: 6, align: "center", sx: { py: 4 } }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { color: "text.secondary" }, "No audit events found"))) : entries.map((entry) => /* @__PURE__ */ import_react5.default.createElement(DetailRow, { key: entry.id, entry }))))), /* @__PURE__ */ import_react5.default.createElement(
1024
+ import_material5.TablePagination,
1025
+ {
1026
+ component: "div",
1027
+ count: total,
1028
+ page,
1029
+ onPageChange: (_, p) => setPage(p),
1030
+ rowsPerPage: pageSize,
1031
+ onRowsPerPageChange: (e) => {
1032
+ setPageSize(Number(e.target.value));
1033
+ setPage(0);
1034
+ },
1035
+ rowsPerPageOptions: [10, 25, 50]
1036
+ }
1037
+ ));
1038
+ };
1039
+ }
1040
+ });
1041
+
706
1042
  // src/components/LiteLLMPage.tsx
707
1043
  var LiteLLMPage_exports = {};
708
1044
  __export(LiteLLMPage_exports, {
@@ -722,31 +1058,33 @@ function initDateRange() {
722
1058
  else start.setDate(start.getDate() - 7);
723
1059
  return { start, end };
724
1060
  }
725
- var import_react5, import_material5, import_react_use, import_core_plugin_api2, PERIOD_LS_KEY2, LiteLLMPage;
1061
+ var import_react6, import_material6, import_react_use2, import_core_plugin_api2, PERIOD_LS_KEY2, LiteLLMPage;
726
1062
  var init_LiteLLMPage = __esm({
727
1063
  "src/components/LiteLLMPage.tsx"() {
728
1064
  "use strict";
729
- import_react5 = __toESM(require("react"));
730
- import_material5 = require("@mui/material");
731
- import_react_use = require("react-use");
1065
+ import_react6 = __toESM(require("react"));
1066
+ import_material6 = require("@mui/material");
1067
+ import_react_use2 = require("react-use");
732
1068
  import_core_plugin_api2 = require("@backstage/core-plugin-api");
733
1069
  init_DashboardHeader();
734
1070
  init_KeysTable();
735
1071
  init_UsageStats();
736
1072
  init_TeamUsage();
1073
+ init_AuditLog();
737
1074
  init_api();
738
1075
  PERIOD_LS_KEY2 = "litellm_usage_period";
739
1076
  LiteLLMPage = () => {
740
1077
  const api = (0, import_core_plugin_api2.useApi)(liteLlmApiRef);
741
- const [dateRange, setDateRange] = (0, import_react5.useState)(initDateRange);
742
- const [snackbar, setSnackbar] = (0, import_react5.useState)(null);
743
- const [teamUsageCache, setTeamUsageCache] = (0, import_react5.useState)({});
744
- const [teamUsageLoading, setTeamUsageLoading] = (0, import_react5.useState)({});
745
- const { value: userInfo, loading: userLoading, error: userError } = (0, import_react_use.useAsync)(
1078
+ const [dateRange, setDateRange] = (0, import_react6.useState)(initDateRange);
1079
+ const [activeTab, setActiveTab] = (0, import_react6.useState)("overview");
1080
+ const [snackbar, setSnackbar] = (0, import_react6.useState)(null);
1081
+ const [teamUsageCache, setTeamUsageCache] = (0, import_react6.useState)({});
1082
+ const [teamUsageLoading, setTeamUsageLoading] = (0, import_react6.useState)({});
1083
+ const { value: userInfo, loading: userLoading, error: userError } = (0, import_react_use2.useAsync)(
746
1084
  () => api.getUserInfo(),
747
1085
  [api]
748
1086
  );
749
- const { value: keys, loading: keysLoading, retry: refreshKeys } = (0, import_react_use.useAsyncRetry)(
1087
+ const { value: keys, loading: keysLoading, retry: refreshKeys } = (0, import_react_use2.useAsyncRetry)(
750
1088
  async () => {
751
1089
  try {
752
1090
  return await api.listKeys();
@@ -757,15 +1095,15 @@ var init_LiteLLMPage = __esm({
757
1095
  },
758
1096
  [api]
759
1097
  );
760
- const { value: allModels, loading: modelsLoading } = (0, import_react_use.useAsync)(
1098
+ const { value: allModels, loading: modelsLoading } = (0, import_react_use2.useAsync)(
761
1099
  () => api.listModels().catch(() => []),
762
1100
  [api]
763
1101
  );
764
- const { value: allTeams, loading: teamsLoading } = (0, import_react_use.useAsync)(
1102
+ const { value: allTeams, loading: teamsLoading } = (0, import_react_use2.useAsync)(
765
1103
  () => api.getTeams().catch(() => []),
766
1104
  [api]
767
1105
  );
768
- const teams = (0, import_react5.useMemo)(() => {
1106
+ const teams = (0, import_react6.useMemo)(() => {
769
1107
  if (!allTeams?.length) return [];
770
1108
  if (!userInfo) return allTeams;
771
1109
  const userId = userInfo.user_id;
@@ -777,12 +1115,17 @@ var init_LiteLLMPage = __esm({
777
1115
  );
778
1116
  return byMembership.length > 0 ? byMembership : allTeams;
779
1117
  }, [allTeams, userInfo]);
780
- const { value: usage, loading: usageLoading } = (0, import_react_use.useAsync)(async () => {
1118
+ const { value: usage, loading: usageLoading } = (0, import_react_use2.useAsync)(async () => {
781
1119
  const startDate = dateRange.start.toISOString().split("T")[0];
782
1120
  const endDate = dateRange.end.toISOString().split("T")[0];
783
1121
  return api.getUsage(startDate, endDate);
784
1122
  }, [api, dateRange]);
785
- const loadTeamUsage = (0, import_react5.useCallback)(async (teamId) => {
1123
+ const handleDateRangeChange = (0, import_react6.useCallback)((range) => {
1124
+ setDateRange(range);
1125
+ setTeamUsageCache({});
1126
+ setTeamUsageLoading({});
1127
+ }, [setDateRange]);
1128
+ const loadTeamUsage = (0, import_react6.useCallback)(async (teamId) => {
786
1129
  if (teamUsageCache[teamId] !== void 0 || teamUsageLoading[teamId]) return;
787
1130
  setTeamUsageLoading((prev) => ({ ...prev, [teamId]: true }));
788
1131
  try {
@@ -796,7 +1139,7 @@ var init_LiteLLMPage = __esm({
796
1139
  setTeamUsageLoading((prev) => ({ ...prev, [teamId]: false }));
797
1140
  }
798
1141
  }, [api, dateRange, teamUsageCache, teamUsageLoading]);
799
- const allowedModels = (0, import_react5.useMemo)(() => {
1142
+ const allowedModels = (0, import_react6.useMemo)(() => {
800
1143
  if (!allModels?.length) return [];
801
1144
  const userModels = userInfo?.models;
802
1145
  const teamModels = teams?.flatMap((t) => t.models ?? []);
@@ -809,7 +1152,7 @@ var init_LiteLLMPage = __esm({
809
1152
  ]);
810
1153
  return allModels.filter((m) => allowed.has(m.model_name));
811
1154
  }, [allModels, userInfo, teams]);
812
- const handleGenerateKey = (0, import_react5.useCallback)(
1155
+ const handleGenerateKey = (0, import_react6.useCallback)(
813
1156
  async (request) => {
814
1157
  const response = await api.generateKey(request);
815
1158
  setSnackbar({ message: "Key generated successfully", severity: "success" });
@@ -818,7 +1161,7 @@ var init_LiteLLMPage = __esm({
818
1161
  },
819
1162
  [api, refreshKeys]
820
1163
  );
821
- const handleUpdateKey = (0, import_react5.useCallback)(
1164
+ const handleUpdateKey = (0, import_react6.useCallback)(
822
1165
  async (keyId, request) => {
823
1166
  try {
824
1167
  await api.updateKey(keyId, request);
@@ -831,56 +1174,99 @@ var init_LiteLLMPage = __esm({
831
1174
  },
832
1175
  [api, refreshKeys]
833
1176
  );
834
- const handleDeleteKey = (0, import_react5.useCallback)(
1177
+ const handleBlockKey = (0, import_react6.useCallback)(
1178
+ async (keyId) => {
1179
+ try {
1180
+ await api.blockKey(keyId);
1181
+ setSnackbar({ message: "Key blocked \u2014 requests will be rejected until unblocked", severity: "warning" });
1182
+ refreshKeys();
1183
+ } catch (e) {
1184
+ setSnackbar({ message: `Failed to block key: ${e.message}`, severity: "error" });
1185
+ }
1186
+ },
1187
+ [api, refreshKeys]
1188
+ );
1189
+ const handleUnblockKey = (0, import_react6.useCallback)(
1190
+ async (keyId) => {
1191
+ try {
1192
+ await api.unblockKey(keyId);
1193
+ setSnackbar({ message: "Key unblocked", severity: "success" });
1194
+ refreshKeys();
1195
+ } catch (e) {
1196
+ setSnackbar({ message: `Failed to unblock key: ${e.message}`, severity: "error" });
1197
+ }
1198
+ },
1199
+ [api, refreshKeys]
1200
+ );
1201
+ const handleResetKeySpend = (0, import_react6.useCallback)(
1202
+ async (keyId) => {
1203
+ try {
1204
+ await api.resetKeySpend(keyId);
1205
+ setSnackbar({ message: "Spend counter reset to $0", severity: "success" });
1206
+ refreshKeys();
1207
+ } catch (e) {
1208
+ setSnackbar({ message: `Failed to reset spend: ${e.message}`, severity: "error" });
1209
+ }
1210
+ },
1211
+ [api, refreshKeys]
1212
+ );
1213
+ const handleRotateKey = (0, import_react6.useCallback)(
1214
+ async (keyId) => {
1215
+ const response = await api.rotateKey(keyId);
1216
+ setSnackbar({ message: "Key rotated \u2014 copy the new secret now", severity: "success" });
1217
+ refreshKeys();
1218
+ return response;
1219
+ },
1220
+ [api, refreshKeys]
1221
+ );
1222
+ const handleDeleteKey = (0, import_react6.useCallback)(
835
1223
  async (keyId) => {
836
1224
  try {
837
1225
  await api.deleteKey(keyId);
838
- setSnackbar({
839
- message: "Key revoked successfully",
840
- severity: "success"
841
- });
1226
+ setSnackbar({ message: "Key revoked successfully", severity: "success" });
842
1227
  refreshKeys();
843
1228
  } catch (e) {
844
1229
  if (e.body?.success && (e.body?.message?.includes("already deleted") || e.body?.message?.includes("never existed"))) {
845
- setSnackbar({
846
- message: "Key was already deleted",
847
- severity: "warning"
848
- });
1230
+ setSnackbar({ message: "Key was already deleted", severity: "warning" });
849
1231
  refreshKeys();
850
1232
  return;
851
1233
  }
852
- setSnackbar({
853
- message: `Failed to revoke key: ${e.message}`,
854
- severity: "error"
855
- });
856
- } finally {
857
- refreshKeys();
1234
+ setSnackbar({ message: `Failed to revoke key: ${e.message}`, severity: "error" });
858
1235
  }
859
1236
  },
860
1237
  [api, refreshKeys]
861
1238
  );
862
1239
  const isInitialLoading = userLoading && !userInfo;
863
1240
  if (isInitialLoading) {
864
- return /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "50vh" }, /* @__PURE__ */ import_react5.default.createElement(import_material5.CircularProgress, null));
1241
+ return /* @__PURE__ */ import_react6.default.createElement(import_material6.Box, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "50vh" }, /* @__PURE__ */ import_react6.default.createElement(import_material6.CircularProgress, null));
865
1242
  }
866
1243
  if (userError || !userInfo) {
867
1244
  const isProvisioningEnabled = userError?.body?.provisioning === true;
868
1245
  const hint = userError?.body?.hint;
869
- return /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { p: 3 }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Paper, { sx: { p: 3 } }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "h6", gutterBottom: true }, "Account not provisioned"), /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { color: "text.secondary", paragraph: true }, "Your Backstage account is not linked to a LiteLLM user."), hint ? /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "body2", color: "text.secondary" }, hint) : /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "body2", color: "text.secondary" }, isProvisioningEnabled ? "Auto-provisioning is enabled but failed. Check the backend logs." : "Set litellm.provisioning.enabled: true in app-config.yaml to enable auto-provisioning, or ask your administrator to create the account manually.")));
1246
+ return /* @__PURE__ */ import_react6.default.createElement(import_material6.Box, { p: 3 }, /* @__PURE__ */ import_react6.default.createElement(import_material6.Paper, { sx: { p: 3 } }, /* @__PURE__ */ import_react6.default.createElement(import_material6.Typography, { variant: "h6", gutterBottom: true }, "Account not provisioned"), /* @__PURE__ */ import_react6.default.createElement(import_material6.Typography, { color: "text.secondary", paragraph: true }, "Your Backstage account is not linked to a LiteLLM user."), hint ? /* @__PURE__ */ import_react6.default.createElement(import_material6.Typography, { variant: "body2", color: "text.secondary" }, hint) : /* @__PURE__ */ import_react6.default.createElement(import_material6.Typography, { variant: "body2", color: "text.secondary" }, isProvisioningEnabled ? "Auto-provisioning is enabled but failed. Check the backend logs." : "Set litellm.provisioning.enabled: true in app-config.yaml to enable auto-provisioning, or ask your administrator to create the account manually.")));
870
1247
  }
871
- return /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { p: 3 }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Grid, { container: true, spacing: 2 }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react5.default.createElement(DashboardHeader, { userInfo, teams: teams ?? [], loading: userLoading || teamsLoading })), /* @__PURE__ */ import_react5.default.createElement(import_material5.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react5.default.createElement(
872
- TeamUsage,
1248
+ return /* @__PURE__ */ import_react6.default.createElement(import_material6.Box, { p: 3 }, /* @__PURE__ */ import_react6.default.createElement(import_material6.Box, { mb: 2 }, /* @__PURE__ */ import_react6.default.createElement(DashboardHeader, { userInfo, teams: teams ?? [], loading: userLoading || teamsLoading })), /* @__PURE__ */ import_react6.default.createElement(
1249
+ import_material6.Tabs,
873
1250
  {
874
- teams: teams ?? [],
875
- loading: teamsLoading,
1251
+ value: activeTab,
1252
+ onChange: (_, v) => setActiveTab(v),
1253
+ sx: { mb: 2, borderBottom: 1, borderColor: "divider" }
1254
+ },
1255
+ /* @__PURE__ */ import_react6.default.createElement(import_material6.Tab, { label: "Overview", value: "overview" }),
1256
+ /* @__PURE__ */ import_react6.default.createElement(import_material6.Tab, { label: "Keys", value: "keys" }),
1257
+ /* @__PURE__ */ import_react6.default.createElement(import_material6.Tab, { label: "Teams", value: "teams" }),
1258
+ userInfo.can_view_audit && /* @__PURE__ */ import_react6.default.createElement(import_material6.Tab, { label: "Audit Log", value: "audit" })
1259
+ ), activeTab === "overview" && /* @__PURE__ */ import_react6.default.createElement(
1260
+ UsageStats,
1261
+ {
1262
+ usage: usage ?? null,
1263
+ models: allModels ?? [],
876
1264
  dateRange,
877
- getTeamUsage: (teamId) => {
878
- if (teamUsageCache[teamId] === void 0) loadTeamUsage(teamId);
879
- return teamUsageCache[teamId] ?? null;
880
- },
881
- getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false
1265
+ onDateRangeChange: handleDateRangeChange,
1266
+ loading: usageLoading,
1267
+ userInfo
882
1268
  }
883
- )), /* @__PURE__ */ import_react5.default.createElement(import_material5.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react5.default.createElement(
1269
+ ), activeTab === "keys" && /* @__PURE__ */ import_react6.default.createElement(
884
1270
  KeysTable,
885
1271
  {
886
1272
  keys: keys ?? [],
@@ -889,27 +1275,33 @@ var init_LiteLLMPage = __esm({
889
1275
  loading: keysLoading || modelsLoading,
890
1276
  onGenerateKey: handleGenerateKey,
891
1277
  onUpdateKey: handleUpdateKey,
1278
+ onRotateKey: handleRotateKey,
1279
+ onBlockKey: handleBlockKey,
1280
+ onUnblockKey: handleUnblockKey,
1281
+ onResetKeySpend: handleResetKeySpend,
892
1282
  onDeleteKey: handleDeleteKey
893
1283
  }
894
- )), /* @__PURE__ */ import_react5.default.createElement(import_material5.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react5.default.createElement(
895
- UsageStats,
1284
+ ), activeTab === "teams" && /* @__PURE__ */ import_react6.default.createElement(
1285
+ TeamUsage,
896
1286
  {
897
- usage: usage ?? null,
898
- models: allModels ?? [],
1287
+ teams: teams ?? [],
1288
+ loading: teamsLoading,
899
1289
  dateRange,
900
- onDateRangeChange: setDateRange,
901
- loading: usageLoading,
902
- userInfo
1290
+ getTeamUsage: (teamId) => {
1291
+ if (teamUsageCache[teamId] === void 0) loadTeamUsage(teamId);
1292
+ return teamUsageCache[teamId] ?? null;
1293
+ },
1294
+ getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false
903
1295
  }
904
- ))), /* @__PURE__ */ import_react5.default.createElement(
905
- import_material5.Snackbar,
1296
+ ), activeTab === "audit" && userInfo.can_view_audit && /* @__PURE__ */ import_react6.default.createElement(AuditLog, { api }), /* @__PURE__ */ import_react6.default.createElement(
1297
+ import_material6.Snackbar,
906
1298
  {
907
1299
  open: !!snackbar,
908
1300
  autoHideDuration: 5e3,
909
1301
  onClose: () => setSnackbar(null),
910
1302
  anchorOrigin: { vertical: "bottom", horizontal: "right" }
911
1303
  },
912
- snackbar ? /* @__PURE__ */ import_react5.default.createElement(import_material5.Alert, { severity: snackbar.severity, onClose: () => setSnackbar(null) }, snackbar.message) : void 0
1304
+ snackbar ? /* @__PURE__ */ import_react6.default.createElement(import_material6.Alert, { severity: snackbar.severity, onClose: () => setSnackbar(null) }, snackbar.message) : void 0
913
1305
  ));
914
1306
  };
915
1307
  }
@@ -931,8 +1323,8 @@ __export(index_exports, {
931
1323
  module.exports = __toCommonJS(index_exports);
932
1324
 
933
1325
  // src/plugin.tsx
934
- var import_react6 = __toESM(require("react"));
935
- var import_icons_material3 = require("@mui/icons-material");
1326
+ var import_react7 = __toESM(require("react"));
1327
+ var import_icons_material4 = require("@mui/icons-material");
936
1328
  var import_frontend_plugin_api = require("@backstage/frontend-plugin-api");
937
1329
  init_api();
938
1330
  var liteLlmApi = import_frontend_plugin_api.ApiBlueprint.make({
@@ -946,10 +1338,10 @@ var liteLlmPage = import_frontend_plugin_api.PageBlueprint.make({
946
1338
  params: {
947
1339
  path: "/litellm",
948
1340
  title: "LiteLLM",
949
- icon: /* @__PURE__ */ import_react6.default.createElement(import_icons_material3.TrendingUp, null),
1341
+ icon: /* @__PURE__ */ import_react7.default.createElement(import_icons_material4.TrendingUp, null),
950
1342
  loader: async () => {
951
1343
  const { LiteLLMPage: LiteLLMPage2 } = await Promise.resolve().then(() => (init_LiteLLMPage(), LiteLLMPage_exports));
952
- return /* @__PURE__ */ import_react6.default.createElement(LiteLLMPage2, null);
1344
+ return /* @__PURE__ */ import_react7.default.createElement(LiteLLMPage2, null);
953
1345
  }
954
1346
  }
955
1347
  });
@@ -966,8 +1358,8 @@ init_UsageStats();
966
1358
  init_TeamUsage();
967
1359
 
968
1360
  // src/components/LiteLLMHomeWidget.tsx
969
- var import_react7 = __toESM(require("react"));
970
- var import_material6 = require("@mui/material");
1361
+ var import_react8 = __toESM(require("react"));
1362
+ var import_material7 = require("@mui/material");
971
1363
  var import_recharts3 = require("recharts");
972
1364
  var import_core_plugin_api3 = require("@backstage/core-plugin-api");
973
1365
  init_api();
@@ -985,18 +1377,18 @@ function presetToDateRange(preset) {
985
1377
  }
986
1378
  return { start, end };
987
1379
  }
988
- var Kpi = ({ label, value }) => /* @__PURE__ */ import_react7.default.createElement(import_material6.Box, null, /* @__PURE__ */ import_react7.default.createElement(import_material6.Typography, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ import_react7.default.createElement(import_material6.Typography, { variant: "subtitle1", fontWeight: 600 }, value));
1380
+ var Kpi = ({ label, value }) => /* @__PURE__ */ import_react8.default.createElement(import_material7.Box, null, /* @__PURE__ */ import_react8.default.createElement(import_material7.Typography, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ import_react8.default.createElement(import_material7.Typography, { variant: "subtitle1", fontWeight: 600 }, value));
989
1381
  var LiteLLMHomeWidget = ({
990
1382
  defaultPeriod = "7d",
991
1383
  title = "LiteLLM Usage"
992
1384
  }) => {
993
1385
  const api = (0, import_core_plugin_api3.useApi)(liteLlmApiRef);
994
- const [period, setPeriod] = (0, import_react7.useState)(defaultPeriod);
995
- const [loading, setLoading] = (0, import_react7.useState)(true);
996
- const [error, setError] = (0, import_react7.useState)(null);
997
- const [usage, setUsage] = (0, import_react7.useState)(null);
998
- const [keys, setKeys] = (0, import_react7.useState)([]);
999
- (0, import_react7.useEffect)(() => {
1386
+ const [period, setPeriod] = (0, import_react8.useState)(defaultPeriod);
1387
+ const [loading, setLoading] = (0, import_react8.useState)(true);
1388
+ const [error, setError] = (0, import_react8.useState)(null);
1389
+ const [usage, setUsage] = (0, import_react8.useState)(null);
1390
+ const [keys, setKeys] = (0, import_react8.useState)([]);
1391
+ (0, import_react8.useEffect)(() => {
1000
1392
  let cancelled = false;
1001
1393
  setLoading(true);
1002
1394
  setError(null);
@@ -1024,17 +1416,17 @@ var LiteLLMHomeWidget = ({
1024
1416
  spend: d.spend
1025
1417
  }));
1026
1418
  const hasSparkline = dailyData.length > 0;
1027
- return /* @__PURE__ */ import_react7.default.createElement(import_material6.Paper, { sx: { p: 2 } }, /* @__PURE__ */ import_react7.default.createElement(import_material6.Box, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1.5 }, /* @__PURE__ */ import_react7.default.createElement(import_material6.Typography, { variant: "h6" }, title), /* @__PURE__ */ import_react7.default.createElement(import_material6.FormControl, { size: "small", sx: { minWidth: 90 } }, /* @__PURE__ */ import_react7.default.createElement(
1028
- import_material6.Select,
1419
+ return /* @__PURE__ */ import_react8.default.createElement(import_material7.Paper, { sx: { p: 2 } }, /* @__PURE__ */ import_react8.default.createElement(import_material7.Box, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1.5 }, /* @__PURE__ */ import_react8.default.createElement(import_material7.Typography, { variant: "h6" }, title), /* @__PURE__ */ import_react8.default.createElement(import_material7.FormControl, { size: "small", sx: { minWidth: 90 } }, /* @__PURE__ */ import_react8.default.createElement(
1420
+ import_material7.Select,
1029
1421
  {
1030
1422
  value: period,
1031
1423
  onChange: (e) => setPeriod(e.target.value),
1032
1424
  displayEmpty: true
1033
1425
  },
1034
- /* @__PURE__ */ import_react7.default.createElement(import_material6.MenuItem, { value: "today" }, "Today"),
1035
- /* @__PURE__ */ import_react7.default.createElement(import_material6.MenuItem, { value: "7d" }, "7d"),
1036
- /* @__PURE__ */ import_react7.default.createElement(import_material6.MenuItem, { value: "30d" }, "30d")
1037
- ))), loading && /* @__PURE__ */ import_react7.default.createElement(import_material6.Box, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ import_react7.default.createElement(import_material6.CircularProgress, { size: 32 })), !loading && error && /* @__PURE__ */ import_react7.default.createElement(import_material6.Alert, { severity: "error", sx: { mt: 1 } }, error), !loading && !error && /* @__PURE__ */ import_react7.default.createElement(import_react7.default.Fragment, null, /* @__PURE__ */ import_react7.default.createElement(import_material6.Grid, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ import_react7.default.createElement(import_material6.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react7.default.createElement(Kpi, { label: "USD Spent", value: fmtUsd2(usage?.total_spend ?? 0) })), /* @__PURE__ */ import_react7.default.createElement(import_material6.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react7.default.createElement(Kpi, { label: "Tokens In", value: fmtInt2(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ import_react7.default.createElement(import_material6.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react7.default.createElement(Kpi, { label: "Tokens Out", value: fmtInt2(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ import_react7.default.createElement(import_material6.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react7.default.createElement(Kpi, { label: "Keys", value: fmtInt2(keys.length) }))), hasSparkline && /* @__PURE__ */ import_react7.default.createElement(import_material6.Box, { height: 120 }, /* @__PURE__ */ import_react7.default.createElement(import_recharts3.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react7.default.createElement(import_recharts3.AreaChart, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 } }, /* @__PURE__ */ import_react7.default.createElement(
1426
+ /* @__PURE__ */ import_react8.default.createElement(import_material7.MenuItem, { value: "today" }, "Today"),
1427
+ /* @__PURE__ */ import_react8.default.createElement(import_material7.MenuItem, { value: "7d" }, "7d"),
1428
+ /* @__PURE__ */ import_react8.default.createElement(import_material7.MenuItem, { value: "30d" }, "30d")
1429
+ ))), loading && /* @__PURE__ */ import_react8.default.createElement(import_material7.Box, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ import_react8.default.createElement(import_material7.CircularProgress, { size: 32 })), !loading && error && /* @__PURE__ */ import_react8.default.createElement(import_material7.Alert, { severity: "error", sx: { mt: 1 } }, error), !loading && !error && /* @__PURE__ */ import_react8.default.createElement(import_react8.default.Fragment, null, /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "USD Spent", value: fmtUsd2(usage?.total_spend ?? 0) })), /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "Tokens In", value: fmtInt2(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "Tokens Out", value: fmtInt2(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "Keys", value: fmtInt2(keys.length) }))), hasSparkline && /* @__PURE__ */ import_react8.default.createElement(import_material7.Box, { height: 120 }, /* @__PURE__ */ import_react8.default.createElement(import_recharts3.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react8.default.createElement(import_recharts3.AreaChart, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 } }, /* @__PURE__ */ import_react8.default.createElement(
1038
1430
  import_recharts3.Area,
1039
1431
  {
1040
1432
  type: "monotone",