@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.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,44 +1174,87 @@ 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(
1248
+ return /* @__PURE__ */ import_react6.default.createElement(import_material6.Box, { p: 3 }, userInfo.can_view_audit && /* @__PURE__ */ import_react6.default.createElement(
1249
+ import_material6.Tabs,
1250
+ {
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: "Audit Log", value: "audit" })
1257
+ ), activeTab === "audit" ? /* @__PURE__ */ import_react6.default.createElement(AuditLog, { api }) : /* @__PURE__ */ import_react6.default.createElement(import_material6.Grid, { container: true, spacing: 2 }, /* @__PURE__ */ import_react6.default.createElement(import_material6.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react6.default.createElement(DashboardHeader, { userInfo, teams: teams ?? [], loading: userLoading || teamsLoading })), /* @__PURE__ */ import_react6.default.createElement(import_material6.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react6.default.createElement(
872
1258
  TeamUsage,
873
1259
  {
874
1260
  teams: teams ?? [],
@@ -880,7 +1266,17 @@ var init_LiteLLMPage = __esm({
880
1266
  },
881
1267
  getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false
882
1268
  }
883
- )), /* @__PURE__ */ import_react5.default.createElement(import_material5.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react5.default.createElement(
1269
+ )), /* @__PURE__ */ import_react6.default.createElement(import_material6.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react6.default.createElement(
1270
+ UsageStats,
1271
+ {
1272
+ usage: usage ?? null,
1273
+ models: allModels ?? [],
1274
+ dateRange,
1275
+ onDateRangeChange: handleDateRangeChange,
1276
+ loading: usageLoading,
1277
+ userInfo
1278
+ }
1279
+ )), /* @__PURE__ */ import_react6.default.createElement(import_material6.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react6.default.createElement(
884
1280
  KeysTable,
885
1281
  {
886
1282
  keys: keys ?? [],
@@ -889,27 +1285,21 @@ var init_LiteLLMPage = __esm({
889
1285
  loading: keysLoading || modelsLoading,
890
1286
  onGenerateKey: handleGenerateKey,
891
1287
  onUpdateKey: handleUpdateKey,
1288
+ onRotateKey: handleRotateKey,
1289
+ onBlockKey: handleBlockKey,
1290
+ onUnblockKey: handleUnblockKey,
1291
+ onResetKeySpend: handleResetKeySpend,
892
1292
  onDeleteKey: handleDeleteKey
893
1293
  }
894
- )), /* @__PURE__ */ import_react5.default.createElement(import_material5.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react5.default.createElement(
895
- UsageStats,
896
- {
897
- usage: usage ?? null,
898
- models: allModels ?? [],
899
- dateRange,
900
- onDateRangeChange: setDateRange,
901
- loading: usageLoading,
902
- userInfo
903
- }
904
- ))), /* @__PURE__ */ import_react5.default.createElement(
905
- import_material5.Snackbar,
1294
+ ))), /* @__PURE__ */ import_react6.default.createElement(
1295
+ import_material6.Snackbar,
906
1296
  {
907
1297
  open: !!snackbar,
908
1298
  autoHideDuration: 5e3,
909
1299
  onClose: () => setSnackbar(null),
910
1300
  anchorOrigin: { vertical: "bottom", horizontal: "right" }
911
1301
  },
912
- snackbar ? /* @__PURE__ */ import_react5.default.createElement(import_material5.Alert, { severity: snackbar.severity, onClose: () => setSnackbar(null) }, snackbar.message) : void 0
1302
+ snackbar ? /* @__PURE__ */ import_react6.default.createElement(import_material6.Alert, { severity: snackbar.severity, onClose: () => setSnackbar(null) }, snackbar.message) : void 0
913
1303
  ));
914
1304
  };
915
1305
  }
@@ -931,8 +1321,8 @@ __export(index_exports, {
931
1321
  module.exports = __toCommonJS(index_exports);
932
1322
 
933
1323
  // src/plugin.tsx
934
- var import_react6 = __toESM(require("react"));
935
- var import_icons_material3 = require("@mui/icons-material");
1324
+ var import_react7 = __toESM(require("react"));
1325
+ var import_icons_material4 = require("@mui/icons-material");
936
1326
  var import_frontend_plugin_api = require("@backstage/frontend-plugin-api");
937
1327
  init_api();
938
1328
  var liteLlmApi = import_frontend_plugin_api.ApiBlueprint.make({
@@ -946,10 +1336,10 @@ var liteLlmPage = import_frontend_plugin_api.PageBlueprint.make({
946
1336
  params: {
947
1337
  path: "/litellm",
948
1338
  title: "LiteLLM",
949
- icon: /* @__PURE__ */ import_react6.default.createElement(import_icons_material3.TrendingUp, null),
1339
+ icon: /* @__PURE__ */ import_react7.default.createElement(import_icons_material4.TrendingUp, null),
950
1340
  loader: async () => {
951
1341
  const { LiteLLMPage: LiteLLMPage2 } = await Promise.resolve().then(() => (init_LiteLLMPage(), LiteLLMPage_exports));
952
- return /* @__PURE__ */ import_react6.default.createElement(LiteLLMPage2, null);
1342
+ return /* @__PURE__ */ import_react7.default.createElement(LiteLLMPage2, null);
953
1343
  }
954
1344
  }
955
1345
  });
@@ -966,8 +1356,8 @@ init_UsageStats();
966
1356
  init_TeamUsage();
967
1357
 
968
1358
  // src/components/LiteLLMHomeWidget.tsx
969
- var import_react7 = __toESM(require("react"));
970
- var import_material6 = require("@mui/material");
1359
+ var import_react8 = __toESM(require("react"));
1360
+ var import_material7 = require("@mui/material");
971
1361
  var import_recharts3 = require("recharts");
972
1362
  var import_core_plugin_api3 = require("@backstage/core-plugin-api");
973
1363
  init_api();
@@ -985,18 +1375,18 @@ function presetToDateRange(preset) {
985
1375
  }
986
1376
  return { start, end };
987
1377
  }
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));
1378
+ 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
1379
  var LiteLLMHomeWidget = ({
990
1380
  defaultPeriod = "7d",
991
1381
  title = "LiteLLM Usage"
992
1382
  }) => {
993
1383
  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)(() => {
1384
+ const [period, setPeriod] = (0, import_react8.useState)(defaultPeriod);
1385
+ const [loading, setLoading] = (0, import_react8.useState)(true);
1386
+ const [error, setError] = (0, import_react8.useState)(null);
1387
+ const [usage, setUsage] = (0, import_react8.useState)(null);
1388
+ const [keys, setKeys] = (0, import_react8.useState)([]);
1389
+ (0, import_react8.useEffect)(() => {
1000
1390
  let cancelled = false;
1001
1391
  setLoading(true);
1002
1392
  setError(null);
@@ -1024,17 +1414,17 @@ var LiteLLMHomeWidget = ({
1024
1414
  spend: d.spend
1025
1415
  }));
1026
1416
  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,
1417
+ 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(
1418
+ import_material7.Select,
1029
1419
  {
1030
1420
  value: period,
1031
1421
  onChange: (e) => setPeriod(e.target.value),
1032
1422
  displayEmpty: true
1033
1423
  },
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(
1424
+ /* @__PURE__ */ import_react8.default.createElement(import_material7.MenuItem, { value: "today" }, "Today"),
1425
+ /* @__PURE__ */ import_react8.default.createElement(import_material7.MenuItem, { value: "7d" }, "7d"),
1426
+ /* @__PURE__ */ import_react8.default.createElement(import_material7.MenuItem, { value: "30d" }, "30d")
1427
+ ))), 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
1428
  import_recharts3.Area,
1039
1429
  {
1040
1430
  type: "monotone",