@acarmisc/backstage-plugin-litellm 0.10.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -153,13 +153,26 @@ var init_api = __esm({
153
153
  });
154
154
 
155
155
  // src/components/DashboardHeader.tsx
156
- import React from "react";
157
- import { Box, Typography, LinearProgress, Paper, Chip } from "@mui/material";
156
+ import React, { useMemo } from "react";
157
+ import { Box, Typography, LinearProgress, Paper, Chip, Button, Stack } from "@mui/material";
158
+ import { Add, Key, Warning, Schedule } from "@mui/icons-material";
158
159
  var DashboardHeader;
159
160
  var init_DashboardHeader = __esm({
160
161
  "src/components/DashboardHeader.tsx"() {
161
162
  "use strict";
162
- DashboardHeader = ({ userInfo, teams, loading }) => {
163
+ init_api();
164
+ DashboardHeader = ({
165
+ userInfo,
166
+ teams,
167
+ keys,
168
+ loading,
169
+ onGenerateKeyClick
170
+ }) => {
171
+ const counts = useMemo(() => {
172
+ const expired = keys.filter((k) => expiryStatus(k.expires_at) === "expired").length;
173
+ const expiringSoon = keys.filter((k) => expiryStatus(k.expires_at) === "soon").length;
174
+ return { total: keys.length, expired, expiringSoon };
175
+ }, [keys]);
163
176
  if (loading) {
164
177
  return /* @__PURE__ */ React.createElement(Paper, { sx: { p: 2, mb: 2 } }, /* @__PURE__ */ React.createElement(LinearProgress, null));
165
178
  }
@@ -173,27 +186,38 @@ var init_DashboardHeader = __esm({
173
186
  variant: "outlined",
174
187
  color: "primary"
175
188
  }
176
- ))))));
177
- };
178
- }
179
- });
180
-
181
- // src/format.ts
182
- var fmtUsd, fmtInt, estimateTokensFromBudget;
183
- var init_format = __esm({
184
- "src/format.ts"() {
185
- "use strict";
186
- fmtUsd = (n) => `$${(n ?? 0).toFixed(n < 1 ? 4 : 2)}`;
187
- fmtInt = (n) => (n ?? 0).toLocaleString();
188
- estimateTokensFromBudget = (budget, pricePerToken) => {
189
- if (!pricePerToken || pricePerToken <= 0 || !budget || budget <= 0) return null;
190
- return Math.floor(budget / pricePerToken);
189
+ )))), /* @__PURE__ */ React.createElement(Box, { display: "flex", alignItems: "center", gap: 2, flexWrap: "wrap" }, /* @__PURE__ */ React.createElement(Stack, { direction: "row", spacing: 1 }, /* @__PURE__ */ React.createElement(Chip, { icon: /* @__PURE__ */ React.createElement(Key, { fontSize: "small" }), label: `${counts.total} keys`, size: "small" }), /* @__PURE__ */ React.createElement(
190
+ Chip,
191
+ {
192
+ icon: /* @__PURE__ */ React.createElement(Warning, { fontSize: "small" }),
193
+ label: `${counts.expired} expired`,
194
+ size: "small",
195
+ color: counts.expired > 0 ? "error" : "default"
196
+ }
197
+ ), /* @__PURE__ */ React.createElement(
198
+ Chip,
199
+ {
200
+ icon: /* @__PURE__ */ React.createElement(Schedule, { fontSize: "small" }),
201
+ label: `${counts.expiringSoon} expiring soon`,
202
+ size: "small",
203
+ color: counts.expiringSoon > 0 ? "warning" : "default"
204
+ }
205
+ )), /* @__PURE__ */ React.createElement(
206
+ Button,
207
+ {
208
+ variant: "contained",
209
+ color: "primary",
210
+ startIcon: /* @__PURE__ */ React.createElement(Add, null),
211
+ onClick: onGenerateKeyClick
212
+ },
213
+ "Generate New Key"
214
+ ))));
191
215
  };
192
216
  }
193
217
  });
194
218
 
195
219
  // src/components/KeysTable.tsx
196
- import React2, { useState, useMemo } from "react";
220
+ import React2, { useState, useMemo as useMemo2 } from "react";
197
221
  import {
198
222
  Paper as Paper2,
199
223
  Table,
@@ -202,7 +226,7 @@ import {
202
226
  TableContainer,
203
227
  TableHead,
204
228
  TableRow,
205
- Button,
229
+ Button as Button2,
206
230
  IconButton,
207
231
  Box as Box2,
208
232
  Typography as Typography2,
@@ -211,24 +235,19 @@ import {
211
235
  DialogContent,
212
236
  DialogActions,
213
237
  TextField,
214
- MenuItem,
215
238
  Chip as Chip2,
216
239
  CircularProgress,
217
240
  Autocomplete,
218
241
  LinearProgress as LinearProgress2,
219
- InputAdornment,
220
- Checkbox,
221
- FormControlLabel,
222
- Tabs,
223
- Tab
242
+ InputAdornment
224
243
  } from "@mui/material";
225
- import { ContentCopy, Delete, Add, Edit, Autorenew, Search, Warning, Lock, LockOpen, Code } from "@mui/icons-material";
244
+ import { ContentCopy, Delete, Add as Add2, Edit, Autorenew, Search, Warning as Warning2, Lock, LockOpen } from "@mui/icons-material";
226
245
  function ExpiryChip({ expiresAt }) {
227
246
  const status = expiryStatus(expiresAt);
228
247
  if (!status) return /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "text.secondary" }, "-");
229
248
  const label = status === "expired" ? "Expired" : status === "soon" ? `${Math.ceil((new Date(expiresAt).getTime() - Date.now()) / 864e5)}d left` : formatDate(expiresAt);
230
249
  const color = status === "expired" ? "error" : status === "soon" ? "warning" : "default";
231
- const icon = status === "expired" || status === "soon" ? /* @__PURE__ */ React2.createElement(Warning, { fontSize: "small" }) : void 0;
250
+ const icon = status === "expired" || status === "soon" ? /* @__PURE__ */ React2.createElement(Warning2, { fontSize: "small" }) : void 0;
232
251
  return /* @__PURE__ */ React2.createElement(Chip2, { label, color, size: "small", icon });
233
252
  }
234
253
  function BudgetCell({ spend, maxBudget }) {
@@ -257,39 +276,11 @@ function formatContextWindow(maxInput, maxOutput) {
257
276
  if (outPart) return `ctx ${outPart} out`;
258
277
  return null;
259
278
  }
260
- function trimSlash(url) {
261
- return url.replace(/\/+$/, "");
262
- }
263
- function buildSnippets(baseUrl, key, model) {
264
- const base = trimSlash(baseUrl);
265
- return {
266
- curl: `curl ${base}/v1/chat/completions \\
267
- -H "Authorization: Bearer ${key}" \\
268
- -H "Content-Type: application/json" \\
269
- -d '{
270
- "model": "${model}",
271
- "messages": [{ "role": "user", "content": "Hello!" }]
272
- }'`,
273
- openai: `from openai import OpenAI
274
-
275
- client = OpenAI(
276
- api_key="${key}",
277
- base_url="${base}/v1",
278
- )
279
-
280
- response = client.chat.completions.create(
281
- model="${model}",
282
- messages=[{ "role": "user", "content": "Hello!" }],
283
- )
284
- print(response.choices[0].message.content)`
285
- };
286
- }
287
- var maskKey, shortKeyId, formatDate, emptyForm, keyToEditForm, KeysTable, SnippetTabs;
279
+ var maskKey, shortKeyId, formatDate, keyToEditForm, KeysTable;
288
280
  var init_KeysTable = __esm({
289
281
  "src/components/KeysTable.tsx"() {
290
282
  "use strict";
291
283
  init_api();
292
- init_format();
293
284
  maskKey = (key) => {
294
285
  if (key.length <= 8) return "***";
295
286
  return `${key.slice(0, 4)}...${key.slice(-4)}`;
@@ -306,16 +297,6 @@ var init_KeysTable = __esm({
306
297
  return dateStr;
307
298
  }
308
299
  };
309
- emptyForm = () => ({
310
- alias: "",
311
- models: [],
312
- duration: "30d",
313
- max_budget: 100,
314
- tpm_limit: void 0,
315
- rpm_limit: void 0,
316
- team_id: void 0,
317
- key_type: "llm_api"
318
- });
319
300
  keyToEditForm = (k) => ({
320
301
  key_alias: k.key_alias ?? "",
321
302
  models: k.models ?? [],
@@ -326,24 +307,15 @@ var init_KeysTable = __esm({
326
307
  KeysTable = ({
327
308
  keys,
328
309
  models,
329
- teams,
330
310
  loading,
331
- onGenerateKey,
311
+ onGenerateKeyClick,
332
312
  onUpdateKey,
333
313
  onBlockKey,
334
314
  onUnblockKey,
335
315
  onResetKeySpend,
336
316
  onDeleteKey,
337
- onPruneExpiredKeys,
338
- onGetConfig
317
+ onPruneExpiredKeys
339
318
  }) => {
340
- const [generateModalOpen, setGenerateModalOpen] = useState(false);
341
- const [newKeyValue, setNewKeyValue] = useState(null);
342
- const [newKeySnippets, setNewKeySnippets] = useState(null);
343
- const [newKeyModel, setNewKeyModel] = useState("");
344
- const [formData, setFormData] = useState(emptyForm());
345
- const [unlimitedBudget, setUnlimitedBudget] = useState(false);
346
- const [submitting, setSubmitting] = useState(false);
347
319
  const [editingKey, setEditingKey] = useState(null);
348
320
  const [editForm, setEditForm] = useState({});
349
321
  const [editSubmitting, setEditSubmitting] = useState(false);
@@ -353,58 +325,14 @@ var init_KeysTable = __esm({
353
325
  const [resetSpendConfirm, setResetSpendConfirm] = useState(false);
354
326
  const [resetSpendSubmitting, setResetSpendSubmitting] = useState(false);
355
327
  const [filterText, setFilterText] = useState("");
356
- const filteredKeys = useMemo(() => {
328
+ const filteredKeys = useMemo2(() => {
357
329
  if (!filterText.trim()) return keys;
358
330
  const q = filterText.toLowerCase();
359
331
  return keys.filter(
360
332
  (k) => (k.key_alias ?? "").toLowerCase().includes(q) || k.models?.some((m) => m.toLowerCase().includes(q))
361
333
  );
362
334
  }, [keys, filterText]);
363
- const selectedModels = models.filter((m) => (formData.models || []).includes(m.model_name));
364
- const selectedTeam = teams.find((t) => t.team_id === formData.team_id) ?? null;
365
335
  const editSelectedModels = models.filter((m) => (editForm.models || []).includes(m.model_name));
366
- const aliasError = !(formData.alias || "").trim();
367
- const budgetInvalid = !unlimitedBudget && (formData.max_budget === void 0 || formData.max_budget === null || formData.max_budget <= 0);
368
- const canGenerate = !aliasError && !budgetInvalid && !submitting;
369
- const budgetEstimate = useMemo(() => {
370
- if (unlimitedBudget || budgetInvalid) return null;
371
- const inputCosts = selectedModels.map((m) => m.input_cost_per_token).filter((c) => typeof c === "number" && c > 0);
372
- if (inputCosts.length === 0) return null;
373
- const pricePerToken = Math.max(...inputCosts);
374
- return estimateTokensFromBudget(formData.max_budget ?? 0, pricePerToken);
375
- }, [formData.max_budget, selectedModels, unlimitedBudget, budgetInvalid]);
376
- const handleGenerate = async () => {
377
- setSubmitting(true);
378
- try {
379
- const request = {
380
- ...formData,
381
- max_budget: unlimitedBudget ? null : formData.max_budget
382
- };
383
- const response = await onGenerateKey(request);
384
- setNewKeyValue(response.key);
385
- setNewKeyModel(formData.models?.[0] ?? "");
386
- setNewKeySnippets(null);
387
- try {
388
- const config = await onGetConfig();
389
- const model = formData.models?.[0] ?? "";
390
- setNewKeySnippets(buildSnippets(config.baseUrl, response.key, model));
391
- } catch {
392
- }
393
- setFormData(emptyForm());
394
- setUnlimitedBudget(false);
395
- } catch (error) {
396
- console.error("Failed to generate key:", error);
397
- } finally {
398
- setSubmitting(false);
399
- }
400
- };
401
- const handleCloseModal = () => {
402
- setGenerateModalOpen(false);
403
- setNewKeyValue(null);
404
- setNewKeySnippets(null);
405
- setFormData(emptyForm());
406
- setUnlimitedBudget(false);
407
- };
408
336
  const handleOpenEdit = (k) => {
409
337
  setEditingKey(k);
410
338
  setEditForm(keyToEditForm(k));
@@ -465,7 +393,7 @@ var init_KeysTable = __esm({
465
393
  };
466
394
  const [pruneConfirmCount, setPruneConfirmCount] = useState(null);
467
395
  const [pruneSubmitting, setPruneSubmitting] = useState(false);
468
- const expiredKeys = useMemo(() => {
396
+ const expiredKeys = useMemo2(() => {
469
397
  return keys.filter((k) => expiryStatus(k.expires_at) === "expired");
470
398
  }, [keys]);
471
399
  const handlePruneExpired = async () => {
@@ -499,7 +427,7 @@ var init_KeysTable = __esm({
499
427
  }
500
428
  }
501
429
  ), expiredKeys.length > 0 && /* @__PURE__ */ React2.createElement(
502
- Button,
430
+ Button2,
503
431
  {
504
432
  variant: "outlined",
505
433
  color: "error",
@@ -510,22 +438,22 @@ var init_KeysTable = __esm({
510
438
  expiredKeys.length,
511
439
  ")"
512
440
  ), /* @__PURE__ */ React2.createElement(
513
- Button,
441
+ Button2,
514
442
  {
515
443
  variant: "contained",
516
444
  color: "primary",
517
- startIcon: /* @__PURE__ */ React2.createElement(Add, null),
518
- onClick: () => setGenerateModalOpen(true)
445
+ startIcon: /* @__PURE__ */ React2.createElement(Add2, null),
446
+ onClick: onGenerateKeyClick
519
447
  },
520
448
  "Generate New Key"
521
449
  ))), /* @__PURE__ */ React2.createElement(TableContainer, null, /* @__PURE__ */ React2.createElement(Table, null, /* @__PURE__ */ React2.createElement(TableHead, null, /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, null, "Alias"), /* @__PURE__ */ React2.createElement(TableCell, null, "Key ID"), /* @__PURE__ */ React2.createElement(TableCell, null, "Created"), /* @__PURE__ */ React2.createElement(TableCell, null, "Expires"), /* @__PURE__ */ React2.createElement(TableCell, null, "Budget"), /* @__PURE__ */ React2.createElement(TableCell, null, "TPM / RPM"), /* @__PURE__ */ React2.createElement(TableCell, null, "Models"), /* @__PURE__ */ React2.createElement(TableCell, { align: "right" }, "Actions"))), /* @__PURE__ */ React2.createElement(TableBody, null, loading ? /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ React2.createElement(CircularProgress, { size: 24 }))) : filteredKeys.length === 0 ? /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, { colSpan: 8, align: "center", sx: { py: 4 } }, filterText ? /* @__PURE__ */ React2.createElement(Typography2, { color: "text.secondary" }, "No keys match filter") : /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Typography2, { color: "text.secondary", gutterBottom: true }, "No keys yet \u2014 generate your first key to start calling models."), /* @__PURE__ */ React2.createElement(
522
- Button,
450
+ Button2,
523
451
  {
524
452
  variant: "contained",
525
453
  color: "primary",
526
454
  size: "small",
527
- startIcon: /* @__PURE__ */ React2.createElement(Add, null),
528
- onClick: () => setGenerateModalOpen(true)
455
+ startIcon: /* @__PURE__ */ React2.createElement(Add2, null),
456
+ onClick: onGenerateKeyClick
529
457
  },
530
458
  "Generate Your First Key"
531
459
  )))) : filteredKeys.map((key) => {
@@ -565,8 +493,297 @@ var init_KeysTable = __esm({
565
493
  },
566
494
  /* @__PURE__ */ React2.createElement(Delete, null)
567
495
  )));
568
- }))))), /* @__PURE__ */ React2.createElement(Dialog, { open: generateModalOpen, onClose: handleCloseModal, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ React2.createElement(DialogTitle, null, newKeyValue ? "Key Generated" : "Generate New Key"), /* @__PURE__ */ React2.createElement(DialogContent, null, newKeyValue ? /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "text.secondary", gutterBottom: true }, "Copy this key now. You won't be able to see it again."), /* @__PURE__ */ React2.createElement(
569
- Box2,
496
+ }))))), /* @__PURE__ */ React2.createElement(Dialog, { open: !!editingKey, onClose: handleCloseEdit, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ React2.createElement(DialogTitle, null, "Edit Key"), /* @__PURE__ */ React2.createElement(DialogContent, null, editingKey && /* @__PURE__ */ React2.createElement(Box2, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "text.secondary" }, /* @__PURE__ */ React2.createElement("code", { style: { fontFamily: "monospace", color: "inherit" } }, maskKey(editingKey.key))), /* @__PURE__ */ React2.createElement(
497
+ TextField,
498
+ {
499
+ label: "Alias",
500
+ value: editForm.key_alias || "",
501
+ onChange: (e) => setEditForm({ ...editForm, key_alias: e.target.value }),
502
+ fullWidth: true
503
+ }
504
+ ), models.length > 0 && /* @__PURE__ */ React2.createElement(
505
+ Autocomplete,
506
+ {
507
+ multiple: true,
508
+ options: models,
509
+ groupBy: (m) => m.mode || "other",
510
+ getOptionLabel: (m) => m.model_name,
511
+ value: editSelectedModels,
512
+ onChange: (_e, selected) => setEditForm({ ...editForm, models: selected.map((m) => m.model_name) }),
513
+ renderOption: (props, m) => /* @__PURE__ */ React2.createElement("li", { ...props }, modelOption(m)),
514
+ renderInput: (params) => /* @__PURE__ */ React2.createElement(TextField, { ...params, label: "Models", fullWidth: true })
515
+ }
516
+ ), /* @__PURE__ */ React2.createElement(
517
+ TextField,
518
+ {
519
+ label: "Max Budget (USD)",
520
+ type: "number",
521
+ value: editForm.max_budget ?? "",
522
+ onChange: (e) => setEditForm({ ...editForm, max_budget: e.target.value ? Number(e.target.value) : void 0 }),
523
+ fullWidth: true
524
+ }
525
+ ), /* @__PURE__ */ React2.createElement(
526
+ TextField,
527
+ {
528
+ label: "TPM Limit",
529
+ type: "number",
530
+ value: editForm.tpm_limit ?? "",
531
+ onChange: (e) => setEditForm({ ...editForm, tpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
532
+ helperText: "Max tokens per minute this key can consume across all models. Leave blank for no limit.",
533
+ fullWidth: true
534
+ }
535
+ ), /* @__PURE__ */ React2.createElement(
536
+ TextField,
537
+ {
538
+ label: "RPM Limit",
539
+ type: "number",
540
+ value: editForm.rpm_limit ?? "",
541
+ onChange: (e) => setEditForm({ ...editForm, rpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
542
+ helperText: "Max requests per minute this key can make across all models. Leave blank for no limit.",
543
+ fullWidth: true
544
+ }
545
+ ), /* @__PURE__ */ React2.createElement(Box2, { mt: 1, pt: 2, borderTop: "1px solid", sx: { borderColor: "divider" } }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "caption", color: "text.secondary", display: "block", mb: 1 }, "Danger Zone"), !resetSpendConfirm ? /* @__PURE__ */ React2.createElement(
546
+ Button2,
547
+ {
548
+ size: "small",
549
+ color: "warning",
550
+ variant: "outlined",
551
+ onClick: () => setResetSpendConfirm(true)
552
+ },
553
+ "Reset Spend to $0"
554
+ ) : /* @__PURE__ */ React2.createElement(Box2, { display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "warning.main" }, "Zero out spend counter?"), /* @__PURE__ */ React2.createElement(
555
+ Button2,
556
+ {
557
+ size: "small",
558
+ color: "warning",
559
+ variant: "contained",
560
+ disabled: resetSpendSubmitting,
561
+ onClick: handleResetSpend
562
+ },
563
+ resetSpendSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 16 }) : "Confirm"
564
+ ), /* @__PURE__ */ React2.createElement(Button2, { size: "small", onClick: () => setResetSpendConfirm(false) }, "Cancel"))))), /* @__PURE__ */ React2.createElement(DialogActions, null, /* @__PURE__ */ React2.createElement(Button2, { onClick: handleCloseEdit }, "Cancel"), /* @__PURE__ */ React2.createElement(Button2, { onClick: handleUpdate, variant: "contained", color: "primary", disabled: editSubmitting }, editSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 24 }) : "Save"))), /* @__PURE__ */ React2.createElement(Dialog, { open: !!deleteConfirmId, onClose: () => setDeleteConfirmId(null), maxWidth: "xs", fullWidth: true }, /* @__PURE__ */ React2.createElement(DialogTitle, null, "Revoke Key?"), /* @__PURE__ */ React2.createElement(DialogContent, null, /* @__PURE__ */ React2.createElement(Typography2, null, "This will permanently revoke the key. Any integrations using it will stop working immediately.")), /* @__PURE__ */ React2.createElement(DialogActions, null, /* @__PURE__ */ React2.createElement(Button2, { onClick: () => setDeleteConfirmId(null), disabled: deleteSubmitting }, "Cancel"), /* @__PURE__ */ React2.createElement(
565
+ Button2,
566
+ {
567
+ onClick: handleConfirmDelete,
568
+ variant: "contained",
569
+ color: "error",
570
+ disabled: deleteSubmitting
571
+ },
572
+ deleteSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 20 }) : "Revoke"
573
+ ))), /* @__PURE__ */ React2.createElement(Dialog, { open: pruneConfirmCount !== null, onClose: () => setPruneConfirmCount(null), maxWidth: "xs", fullWidth: true }, /* @__PURE__ */ React2.createElement(DialogTitle, null, "Prune Expired Keys?"), /* @__PURE__ */ React2.createElement(DialogContent, null, /* @__PURE__ */ React2.createElement(Typography2, null, "This will permanently delete ", pruneConfirmCount, " expired key", pruneConfirmCount !== 1 ? "s" : "", " from LiteLLM. Any integrations using them will stop working immediately.")), /* @__PURE__ */ React2.createElement(DialogActions, null, /* @__PURE__ */ React2.createElement(Button2, { onClick: () => setPruneConfirmCount(null), disabled: pruneSubmitting }, "Cancel"), /* @__PURE__ */ React2.createElement(
574
+ Button2,
575
+ {
576
+ onClick: handlePruneExpired,
577
+ variant: "contained",
578
+ color: "error",
579
+ disabled: pruneSubmitting
580
+ },
581
+ pruneSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 20 }) : "Prune"
582
+ ))));
583
+ };
584
+ }
585
+ });
586
+
587
+ // src/format.ts
588
+ var fmtUsd, fmtInt, estimateTokensFromBudget;
589
+ var init_format = __esm({
590
+ "src/format.ts"() {
591
+ "use strict";
592
+ fmtUsd = (n) => `$${(n ?? 0).toFixed(n < 1 ? 4 : 2)}`;
593
+ fmtInt = (n) => (n ?? 0).toLocaleString();
594
+ estimateTokensFromBudget = (budget, pricePerToken) => {
595
+ if (!pricePerToken || pricePerToken <= 0 || !budget || budget <= 0) return null;
596
+ return Math.floor(budget / pricePerToken);
597
+ };
598
+ }
599
+ });
600
+
601
+ // src/components/GenerateKeyDialog.tsx
602
+ import React3, { useState as useState2, useEffect, useMemo as useMemo3 } from "react";
603
+ import {
604
+ Box as Box3,
605
+ Typography as Typography3,
606
+ Dialog as Dialog2,
607
+ DialogTitle as DialogTitle2,
608
+ DialogContent as DialogContent2,
609
+ DialogActions as DialogActions2,
610
+ TextField as TextField2,
611
+ MenuItem,
612
+ Button as Button3,
613
+ IconButton as IconButton2,
614
+ CircularProgress as CircularProgress2,
615
+ Autocomplete as Autocomplete2,
616
+ Checkbox,
617
+ FormControlLabel,
618
+ Alert,
619
+ Tabs,
620
+ Tab
621
+ } from "@mui/material";
622
+ import { ContentCopy as ContentCopy2, Code } from "@mui/icons-material";
623
+ function trimSlash(url) {
624
+ return url.replace(/\/+$/, "");
625
+ }
626
+ function buildSnippets(baseUrl, key, model) {
627
+ const base = trimSlash(baseUrl);
628
+ return {
629
+ curl: `curl ${base}/v1/chat/completions \\
630
+ -H "Authorization: Bearer ${key}" \\
631
+ -H "Content-Type: application/json" \\
632
+ -d '{
633
+ "model": "${model}",
634
+ "messages": [{ "role": "user", "content": "Hello!" }]
635
+ }'`,
636
+ openai: `from openai import OpenAI
637
+
638
+ client = OpenAI(
639
+ api_key="${key}",
640
+ base_url="${base}/v1",
641
+ )
642
+
643
+ response = client.chat.completions.create(
644
+ model="${model}",
645
+ messages=[{ "role": "user", "content": "Hello!" }],
646
+ )
647
+ print(response.choices[0].message.content)`
648
+ };
649
+ }
650
+ function fmtCost2(perToken) {
651
+ if (!perToken) return null;
652
+ const per1k = perToken * 1e3;
653
+ return per1k < 0.01 ? `$${(perToken * 1e6).toFixed(2)}/M` : `$${per1k.toFixed(3)}/1K`;
654
+ }
655
+ function formatContextWindow2(maxInput, maxOutput) {
656
+ if (!maxInput && !maxOutput) return null;
657
+ const fmt = (n) => {
658
+ if (!n) return null;
659
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
660
+ if (n >= 1e3) return `${Math.round(n / 1e3)}K`;
661
+ return String(n);
662
+ };
663
+ const inPart = fmt(maxInput);
664
+ const outPart = fmt(maxOutput);
665
+ if (inPart && outPart) return `ctx ${inPart} in / ${outPart} out`;
666
+ if (inPart) return `ctx ${inPart}`;
667
+ if (outPart) return `ctx ${outPart} out`;
668
+ return null;
669
+ }
670
+ var generateDefaultAlias, emptyForm, GenerateKeyDialog, SnippetTabs;
671
+ var init_GenerateKeyDialog = __esm({
672
+ "src/components/GenerateKeyDialog.tsx"() {
673
+ "use strict";
674
+ init_format();
675
+ generateDefaultAlias = (username) => {
676
+ const base = (username || "user").split("@")[0].toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "user";
677
+ const hash = Math.random().toString(36).slice(2, 8);
678
+ return `${base}-${hash}`;
679
+ };
680
+ emptyForm = () => ({
681
+ alias: "",
682
+ models: [],
683
+ duration: "30d",
684
+ max_budget: 100,
685
+ tpm_limit: void 0,
686
+ rpm_limit: void 0,
687
+ team_id: void 0,
688
+ key_type: "llm_api"
689
+ });
690
+ GenerateKeyDialog = ({
691
+ open,
692
+ onClose,
693
+ keys,
694
+ models,
695
+ teams,
696
+ username,
697
+ keyGenerationSettings,
698
+ onGenerateKey,
699
+ onGetConfig
700
+ }) => {
701
+ const [formData, setFormData] = useState2(emptyForm());
702
+ const [unlimitedBudget, setUnlimitedBudget] = useState2(false);
703
+ const [submitting, setSubmitting] = useState2(false);
704
+ const [newKeyValue, setNewKeyValue] = useState2(null);
705
+ const [newKeySnippets, setNewKeySnippets] = useState2(null);
706
+ const [newKeyModel, setNewKeyModel] = useState2("");
707
+ const [generateError, setGenerateError] = useState2(null);
708
+ useEffect(() => {
709
+ if (open) {
710
+ setFormData({ ...emptyForm(), alias: generateDefaultAlias(username) });
711
+ setUnlimitedBudget(false);
712
+ setNewKeyValue(null);
713
+ setNewKeySnippets(null);
714
+ setNewKeyModel("");
715
+ setGenerateError(null);
716
+ }
717
+ }, [open, username]);
718
+ const allowUnlimitedBudget = keyGenerationSettings?.allowUnlimitedBudget ?? false;
719
+ const teamRequired = keyGenerationSettings?.teamRequired ?? true;
720
+ const selectedTeam = teams.find((t) => t.team_id === formData.team_id) ?? null;
721
+ const availableModels = useMemo3(() => {
722
+ const teamModels = selectedTeam?.models;
723
+ if (teamModels && teamModels.length > 0) {
724
+ const allowed = new Set(teamModels);
725
+ return models.filter((m) => allowed.has(m.model_name));
726
+ }
727
+ return models;
728
+ }, [models, selectedTeam]);
729
+ const selectedModels = availableModels.filter((m) => (formData.models || []).includes(m.model_name));
730
+ const aliasError = !(formData.alias || "").trim();
731
+ const teamError = teamRequired && !formData.team_id;
732
+ const budgetInvalid = !unlimitedBudget && (formData.max_budget === void 0 || formData.max_budget === null || formData.max_budget <= 0);
733
+ const canGenerate = !aliasError && !teamError && !budgetInvalid && !submitting;
734
+ const aliasDuplicate = !!formData.alias && keys.some((k) => k.key_alias === formData.alias);
735
+ const budgetEstimate = useMemo3(() => {
736
+ if (unlimitedBudget || budgetInvalid) return null;
737
+ const inputCosts = selectedModels.map((m) => m.input_cost_per_token).filter((c) => typeof c === "number" && c > 0);
738
+ if (inputCosts.length === 0) return null;
739
+ const pricePerToken = Math.max(...inputCosts);
740
+ return estimateTokensFromBudget(formData.max_budget ?? 0, pricePerToken);
741
+ }, [formData.max_budget, selectedModels, unlimitedBudget, budgetInvalid]);
742
+ const handleGenerate = async () => {
743
+ setSubmitting(true);
744
+ setGenerateError(null);
745
+ try {
746
+ const request = {
747
+ ...formData,
748
+ max_budget: unlimitedBudget ? null : formData.max_budget
749
+ };
750
+ const response = await onGenerateKey(request);
751
+ setNewKeyValue(response.key);
752
+ setNewKeyModel(formData.models?.[0] ?? "");
753
+ setNewKeySnippets(null);
754
+ try {
755
+ const config = await onGetConfig();
756
+ const model = formData.models?.[0] ?? "";
757
+ setNewKeySnippets(buildSnippets(config.baseUrl, response.key, model));
758
+ } catch {
759
+ }
760
+ setFormData(emptyForm());
761
+ setUnlimitedBudget(false);
762
+ } catch (error) {
763
+ setGenerateError(error?.message ?? "Failed to generate key");
764
+ } finally {
765
+ setSubmitting(false);
766
+ }
767
+ };
768
+ const handleClose = () => {
769
+ onClose();
770
+ setGenerateError(null);
771
+ setNewKeyValue(null);
772
+ setNewKeySnippets(null);
773
+ setFormData(emptyForm());
774
+ setUnlimitedBudget(false);
775
+ };
776
+ const copyToClipboard = (text) => {
777
+ navigator.clipboard.writeText(text);
778
+ };
779
+ const modelOption = (m) => {
780
+ const inCost = fmtCost2(m.input_cost_per_token);
781
+ const outCost = fmtCost2(m.output_cost_per_token);
782
+ const ctx = formatContextWindow2(m.max_input_tokens, m.max_output_tokens);
783
+ return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement("span", null, m.model_name), m.supports_function_calling && " \u{1F527}", m.supports_vision && " \u{1F441}\uFE0F", ctx && /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, ctx), (inCost || outCost) && /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, inCost, " in \xB7 ", outCost, " out"));
784
+ };
785
+ return /* @__PURE__ */ React3.createElement(Dialog2, { open, onClose: handleClose, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ React3.createElement(DialogTitle2, null, newKeyValue ? "Key Generated" : "Generate New Key"), /* @__PURE__ */ React3.createElement(DialogContent2, null, newKeyValue ? /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Typography3, { variant: "body2", color: "text.secondary", gutterBottom: true }, "Copy this key now. You won't be able to see it again."), /* @__PURE__ */ React3.createElement(
786
+ Box3,
570
787
  {
571
788
  display: "flex",
572
789
  alignItems: "center",
@@ -580,8 +797,8 @@ var init_KeysTable = __esm({
580
797
  borderRadius: 1
581
798
  }
582
799
  },
583
- /* @__PURE__ */ React2.createElement(
584
- Typography2,
800
+ /* @__PURE__ */ React3.createElement(
801
+ Typography3,
585
802
  {
586
803
  component: "code",
587
804
  color: "text.primary",
@@ -589,20 +806,24 @@ var init_KeysTable = __esm({
589
806
  },
590
807
  newKeyValue
591
808
  ),
592
- /* @__PURE__ */ React2.createElement(IconButton, { onClick: () => copyToClipboard(newKeyValue) }, /* @__PURE__ */ React2.createElement(ContentCopy, null))
593
- ), newKeySnippets && /* @__PURE__ */ React2.createElement(Box2, { mt: 3 }, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React2.createElement(Code, { fontSize: "small", color: "action" }), /* @__PURE__ */ React2.createElement(Typography2, { variant: "subtitle2" }, "Start calling the proxy \u2014 paste and run")), /* @__PURE__ */ React2.createElement(SnippetTabs, { snippets: newKeySnippets, model: newKeyModel, onCopy: copyToClipboard }))) : /* @__PURE__ */ React2.createElement(Box2, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, /* @__PURE__ */ React2.createElement(
594
- TextField,
809
+ /* @__PURE__ */ React3.createElement(IconButton2, { onClick: () => copyToClipboard(newKeyValue) }, /* @__PURE__ */ React3.createElement(ContentCopy2, null))
810
+ ), newKeySnippets && /* @__PURE__ */ React3.createElement(Box3, { mt: 3 }, /* @__PURE__ */ React3.createElement(Box3, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React3.createElement(Code, { fontSize: "small", color: "action" }), /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2" }, "Start calling the proxy \u2014 paste and run")), /* @__PURE__ */ React3.createElement(SnippetTabs, { snippets: newKeySnippets, model: newKeyModel, onCopy: copyToClipboard }))) : /* @__PURE__ */ React3.createElement(Box3, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, generateError && /* @__PURE__ */ React3.createElement(Alert, { severity: "error", onClose: () => setGenerateError(null) }, generateError), /* @__PURE__ */ React3.createElement(
811
+ TextField2,
595
812
  {
596
813
  label: "Alias",
597
814
  value: formData.alias || "",
598
- onChange: (e) => setFormData({ ...formData, alias: e.target.value }),
815
+ onChange: (e) => {
816
+ setFormData({ ...formData, alias: e.target.value });
817
+ setGenerateError(null);
818
+ },
599
819
  error: aliasError,
600
- helperText: aliasError ? "Alias is required" : void 0,
820
+ color: aliasDuplicate ? "warning" : void 0,
821
+ helperText: aliasError ? "Alias is required" : aliasDuplicate ? "This alias is already used by one of your keys \u2014 LiteLLM requires aliases to be unique across all keys" : void 0,
601
822
  required: true,
602
823
  fullWidth: true
603
824
  }
604
- ), /* @__PURE__ */ React2.createElement(
605
- TextField,
825
+ ), /* @__PURE__ */ React3.createElement(
826
+ TextField2,
606
827
  {
607
828
  select: true,
608
829
  label: "Duration",
@@ -610,44 +831,58 @@ var init_KeysTable = __esm({
610
831
  onChange: (e) => setFormData({ ...formData, duration: e.target.value }),
611
832
  fullWidth: true
612
833
  },
613
- /* @__PURE__ */ React2.createElement(MenuItem, { value: "1d" }, "1 Day"),
614
- /* @__PURE__ */ React2.createElement(MenuItem, { value: "7d" }, "7 Days"),
615
- /* @__PURE__ */ React2.createElement(MenuItem, { value: "30d" }, "30 Days"),
616
- /* @__PURE__ */ React2.createElement(MenuItem, { value: "90d" }, "90 Days"),
617
- /* @__PURE__ */ React2.createElement(MenuItem, { value: "1y" }, "1 Year")
618
- ), teams.length > 0 && /* @__PURE__ */ React2.createElement(
619
- Autocomplete,
834
+ /* @__PURE__ */ React3.createElement(MenuItem, { value: "1d" }, "1 Day"),
835
+ /* @__PURE__ */ React3.createElement(MenuItem, { value: "7d" }, "7 Days"),
836
+ /* @__PURE__ */ React3.createElement(MenuItem, { value: "30d" }, "30 Days"),
837
+ /* @__PURE__ */ React3.createElement(MenuItem, { value: "90d" }, "90 Days"),
838
+ /* @__PURE__ */ React3.createElement(MenuItem, { value: "1y" }, "1 Year")
839
+ ), teams.length > 0 ? /* @__PURE__ */ React3.createElement(
840
+ Autocomplete2,
620
841
  {
621
842
  options: teams,
622
843
  getOptionLabel: (t) => t.team_alias || t.team_id,
623
844
  value: selectedTeam,
624
- onChange: (_e, team) => setFormData({ ...formData, team_id: team?.team_id }),
625
- renderInput: (params) => /* @__PURE__ */ React2.createElement(
626
- TextField,
845
+ onChange: (_e, team) => {
846
+ const teamModels = team?.models;
847
+ const restrictedModels = teamModels && teamModels.length > 0 ? (formData.models || []).filter((m) => teamModels.includes(m)) : formData.models;
848
+ setFormData({ ...formData, team_id: team?.team_id, models: restrictedModels });
849
+ },
850
+ renderInput: (params) => /* @__PURE__ */ React3.createElement(
851
+ TextField2,
627
852
  {
628
853
  ...params,
629
854
  label: "Team",
630
- helperText: "Optional: bind this key to a specific team for scoped access",
855
+ error: teamError,
856
+ helperText: teamError ? "Team is required" : teamRequired ? "Bind this key to a team for scoped access" : "Optional: bind this key to a specific team for scoped access",
857
+ required: teamRequired,
631
858
  fullWidth: true
632
859
  }
633
860
  )
634
861
  }
635
- ), models.length > 0 && /* @__PURE__ */ React2.createElement(
636
- Autocomplete,
862
+ ) : teamRequired ? /* @__PURE__ */ React3.createElement(Typography3, { variant: "body2", color: "error" }, "Team selection is required, but you don't belong to any team yet \u2014 contact your administrator.") : null, availableModels.length > 0 && /* @__PURE__ */ React3.createElement(
863
+ Autocomplete2,
637
864
  {
638
865
  multiple: true,
639
- options: models,
866
+ options: availableModels,
640
867
  groupBy: (m) => m.mode || "other",
641
868
  getOptionLabel: (m) => m.model_name,
642
869
  value: selectedModels,
643
870
  onChange: (_e, selected) => setFormData({ ...formData, models: selected.map((m) => m.model_name) }),
644
- renderOption: (props, m) => /* @__PURE__ */ React2.createElement("li", { ...props }, modelOption(m)),
645
- renderInput: (params) => /* @__PURE__ */ React2.createElement(TextField, { ...params, label: "Models", helperText: "Leave empty to allow all models", fullWidth: true })
871
+ renderOption: (props, m) => /* @__PURE__ */ React3.createElement("li", { ...props }, modelOption(m)),
872
+ renderInput: (params) => /* @__PURE__ */ React3.createElement(
873
+ TextField2,
874
+ {
875
+ ...params,
876
+ label: "Models",
877
+ helperText: selectedTeam ? "Leave empty to allow all models available to this team" : "Leave empty to allow all models",
878
+ fullWidth: true
879
+ }
880
+ )
646
881
  }
647
- ), /* @__PURE__ */ React2.createElement(
882
+ ), allowUnlimitedBudget && /* @__PURE__ */ React3.createElement(
648
883
  FormControlLabel,
649
884
  {
650
- control: /* @__PURE__ */ React2.createElement(
885
+ control: /* @__PURE__ */ React3.createElement(
651
886
  Checkbox,
652
887
  {
653
888
  checked: unlimitedBudget,
@@ -663,8 +898,8 @@ var init_KeysTable = __esm({
663
898
  ),
664
899
  label: "Unlimited budget"
665
900
  }
666
- ), /* @__PURE__ */ React2.createElement(
667
- TextField,
901
+ ), /* @__PURE__ */ React3.createElement(
902
+ TextField2,
668
903
  {
669
904
  label: "Max Budget (USD)",
670
905
  type: "number",
@@ -676,141 +911,59 @@ var init_KeysTable = __esm({
676
911
  required: true,
677
912
  fullWidth: true
678
913
  }
679
- ), /* @__PURE__ */ React2.createElement(
680
- TextField,
914
+ ), /* @__PURE__ */ React3.createElement(
915
+ TextField2,
681
916
  {
682
917
  label: "TPM Limit",
683
918
  type: "number",
684
919
  value: formData.tpm_limit ?? "",
685
920
  onChange: (e) => setFormData({ ...formData, tpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
921
+ helperText: "Max tokens per minute this key can consume across all models. Leave blank for no limit.",
686
922
  fullWidth: true
687
923
  }
688
- ), /* @__PURE__ */ React2.createElement(
689
- TextField,
924
+ ), /* @__PURE__ */ React3.createElement(
925
+ TextField2,
690
926
  {
691
927
  label: "RPM Limit",
692
928
  type: "number",
693
929
  value: formData.rpm_limit ?? "",
694
930
  onChange: (e) => setFormData({ ...formData, rpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
931
+ helperText: "Max requests per minute this key can make across all models. Leave blank for no limit.",
695
932
  fullWidth: true
696
933
  }
697
- ))), /* @__PURE__ */ React2.createElement(DialogActions, null, newKeyValue ? /* @__PURE__ */ React2.createElement(Button, { onClick: handleCloseModal, variant: "contained", color: "success" }, "Done") : /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement(Button, { onClick: handleCloseModal }, "Cancel"), /* @__PURE__ */ React2.createElement(
698
- Button,
934
+ ))), /* @__PURE__ */ React3.createElement(DialogActions2, null, newKeyValue ? /* @__PURE__ */ React3.createElement(Button3, { onClick: handleClose, variant: "contained", color: "success" }, "Done") : /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement(Button3, { onClick: handleClose }, "Cancel"), /* @__PURE__ */ React3.createElement(
935
+ Button3,
699
936
  {
700
937
  onClick: handleGenerate,
701
938
  variant: "contained",
702
939
  color: "primary",
703
940
  disabled: !canGenerate
704
941
  },
705
- submitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 24 }) : "Generate"
706
- )))), /* @__PURE__ */ React2.createElement(Dialog, { open: !!editingKey, onClose: handleCloseEdit, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ React2.createElement(DialogTitle, null, "Edit Key"), /* @__PURE__ */ React2.createElement(DialogContent, null, editingKey && /* @__PURE__ */ React2.createElement(Box2, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "text.secondary" }, /* @__PURE__ */ React2.createElement("code", { style: { fontFamily: "monospace", color: "inherit" } }, maskKey(editingKey.key))), /* @__PURE__ */ React2.createElement(
707
- TextField,
708
- {
709
- label: "Alias",
710
- value: editForm.key_alias || "",
711
- onChange: (e) => setEditForm({ ...editForm, key_alias: e.target.value }),
712
- fullWidth: true
713
- }
714
- ), models.length > 0 && /* @__PURE__ */ React2.createElement(
715
- Autocomplete,
716
- {
717
- multiple: true,
718
- options: models,
719
- groupBy: (m) => m.mode || "other",
720
- getOptionLabel: (m) => m.model_name,
721
- value: editSelectedModels,
722
- onChange: (_e, selected) => setEditForm({ ...editForm, models: selected.map((m) => m.model_name) }),
723
- renderOption: (props, m) => /* @__PURE__ */ React2.createElement("li", { ...props }, modelOption(m)),
724
- renderInput: (params) => /* @__PURE__ */ React2.createElement(TextField, { ...params, label: "Models", fullWidth: true })
725
- }
726
- ), /* @__PURE__ */ React2.createElement(
727
- TextField,
728
- {
729
- label: "Max Budget (USD)",
730
- type: "number",
731
- value: editForm.max_budget ?? "",
732
- onChange: (e) => setEditForm({ ...editForm, max_budget: e.target.value ? Number(e.target.value) : void 0 }),
733
- fullWidth: true
734
- }
735
- ), /* @__PURE__ */ React2.createElement(
736
- TextField,
737
- {
738
- label: "TPM Limit",
739
- type: "number",
740
- value: editForm.tpm_limit ?? "",
741
- onChange: (e) => setEditForm({ ...editForm, tpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
742
- fullWidth: true
743
- }
744
- ), /* @__PURE__ */ React2.createElement(
745
- TextField,
746
- {
747
- label: "RPM Limit",
748
- type: "number",
749
- value: editForm.rpm_limit ?? "",
750
- onChange: (e) => setEditForm({ ...editForm, rpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
751
- fullWidth: true
752
- }
753
- ), /* @__PURE__ */ React2.createElement(Box2, { mt: 1, pt: 2, borderTop: "1px solid", sx: { borderColor: "divider" } }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "caption", color: "text.secondary", display: "block", mb: 1 }, "Danger Zone"), !resetSpendConfirm ? /* @__PURE__ */ React2.createElement(
754
- Button,
755
- {
756
- size: "small",
757
- color: "warning",
758
- variant: "outlined",
759
- onClick: () => setResetSpendConfirm(true)
760
- },
761
- "Reset Spend to $0"
762
- ) : /* @__PURE__ */ React2.createElement(Box2, { display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "warning.main" }, "Zero out spend counter?"), /* @__PURE__ */ React2.createElement(
763
- Button,
764
- {
765
- size: "small",
766
- color: "warning",
767
- variant: "contained",
768
- disabled: resetSpendSubmitting,
769
- onClick: handleResetSpend
770
- },
771
- resetSpendSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 16 }) : "Confirm"
772
- ), /* @__PURE__ */ React2.createElement(Button, { size: "small", onClick: () => setResetSpendConfirm(false) }, "Cancel"))))), /* @__PURE__ */ React2.createElement(DialogActions, null, /* @__PURE__ */ React2.createElement(Button, { onClick: handleCloseEdit }, "Cancel"), /* @__PURE__ */ React2.createElement(Button, { onClick: handleUpdate, variant: "contained", color: "primary", disabled: editSubmitting }, editSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 24 }) : "Save"))), /* @__PURE__ */ React2.createElement(Dialog, { open: !!deleteConfirmId, onClose: () => setDeleteConfirmId(null), maxWidth: "xs", fullWidth: true }, /* @__PURE__ */ React2.createElement(DialogTitle, null, "Revoke Key?"), /* @__PURE__ */ React2.createElement(DialogContent, null, /* @__PURE__ */ React2.createElement(Typography2, null, "This will permanently revoke the key. Any integrations using it will stop working immediately.")), /* @__PURE__ */ React2.createElement(DialogActions, null, /* @__PURE__ */ React2.createElement(Button, { onClick: () => setDeleteConfirmId(null), disabled: deleteSubmitting }, "Cancel"), /* @__PURE__ */ React2.createElement(
773
- Button,
774
- {
775
- onClick: handleConfirmDelete,
776
- variant: "contained",
777
- color: "error",
778
- disabled: deleteSubmitting
779
- },
780
- deleteSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 20 }) : "Revoke"
781
- ))), /* @__PURE__ */ React2.createElement(Dialog, { open: pruneConfirmCount !== null, onClose: () => setPruneConfirmCount(null), maxWidth: "xs", fullWidth: true }, /* @__PURE__ */ React2.createElement(DialogTitle, null, "Prune Expired Keys?"), /* @__PURE__ */ React2.createElement(DialogContent, null, /* @__PURE__ */ React2.createElement(Typography2, null, "This will permanently delete ", pruneConfirmCount, " expired key", pruneConfirmCount !== 1 ? "s" : "", " from LiteLLM. Any integrations using them will stop working immediately.")), /* @__PURE__ */ React2.createElement(DialogActions, null, /* @__PURE__ */ React2.createElement(Button, { onClick: () => setPruneConfirmCount(null), disabled: pruneSubmitting }, "Cancel"), /* @__PURE__ */ React2.createElement(
782
- Button,
783
- {
784
- onClick: handlePruneExpired,
785
- variant: "contained",
786
- color: "error",
787
- disabled: pruneSubmitting
788
- },
789
- pruneSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 20 }) : "Prune"
942
+ submitting ? /* @__PURE__ */ React3.createElement(CircularProgress2, { size: 24 }) : "Generate"
790
943
  ))));
791
944
  };
792
945
  SnippetTabs = ({ snippets, model, onCopy }) => {
793
- const [tab, setTab] = useState("curl");
946
+ const [tab, setTab] = useState2("curl");
794
947
  const code = tab === "curl" ? snippets.curl : snippets.openai;
795
- return /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Tabs, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 1 } }, /* @__PURE__ */ React2.createElement(Tab, { label: "curl", value: "curl" }), /* @__PURE__ */ React2.createElement(Tab, { label: "OpenAI SDK", value: "openai" })), /* @__PURE__ */ React2.createElement(
796
- Box2,
948
+ return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Tabs, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 1 } }, /* @__PURE__ */ React3.createElement(Tab, { label: "curl", value: "curl" }), /* @__PURE__ */ React3.createElement(Tab, { label: "OpenAI SDK", value: "openai" })), /* @__PURE__ */ React3.createElement(
949
+ Box3,
797
950
  {
798
951
  position: "relative",
799
952
  p: 1.5,
800
953
  sx: { backgroundColor: "action.hover", border: "1px solid", borderColor: "divider", borderRadius: 1 }
801
954
  },
802
- /* @__PURE__ */ React2.createElement(
803
- IconButton,
955
+ /* @__PURE__ */ React3.createElement(
956
+ IconButton2,
804
957
  {
805
958
  size: "small",
806
959
  onClick: () => onCopy(code),
807
960
  title: "Copy snippet",
808
961
  sx: { position: "absolute", top: 4, right: 4 }
809
962
  },
810
- /* @__PURE__ */ React2.createElement(ContentCopy, { fontSize: "small" })
963
+ /* @__PURE__ */ React3.createElement(ContentCopy2, { fontSize: "small" })
811
964
  ),
812
- /* @__PURE__ */ React2.createElement(
813
- Typography2,
965
+ /* @__PURE__ */ React3.createElement(
966
+ Typography3,
814
967
  {
815
968
  component: "pre",
816
969
  sx: {
@@ -824,18 +977,18 @@ var init_KeysTable = __esm({
824
977
  },
825
978
  code
826
979
  ),
827
- model && /* @__PURE__ */ React2.createElement(Typography2, { variant: "caption", color: "text.secondary", display: "block", mt: 1 }, "Using model \u201C", model, "\u201D \u2014 swap it for any model you have access to.")
980
+ model && /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption", color: "text.secondary", display: "block", mt: 1 }, "Using model \u201C", model, "\u201D \u2014 swap it for any model you have access to.")
828
981
  ));
829
982
  };
830
983
  }
831
984
  });
832
985
 
833
986
  // src/components/UsageStats.tsx
834
- import React3, { useMemo as useMemo2, useState as useState2 } from "react";
987
+ import React4, { useMemo as useMemo4, useState as useState3 } from "react";
835
988
  import {
836
989
  Paper as Paper3,
837
- Box as Box3,
838
- Typography as Typography3,
990
+ Box as Box4,
991
+ Typography as Typography4,
839
992
  FormControl,
840
993
  InputLabel,
841
994
  Select,
@@ -894,12 +1047,12 @@ var init_UsageStats = __esm({
894
1047
  "#d0ed57"
895
1048
  ];
896
1049
  fmtPct = (n) => `${(n * 100).toFixed(1)}%`;
897
- KpiCard = ({ label, value, hint }) => /* @__PURE__ */ React3.createElement(Paper3, { variant: "outlined", sx: { p: 2, height: "100%" } }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption", color: "text.secondary" }, label), /* @__PURE__ */ React3.createElement(Typography3, { variant: "h5", sx: { mt: 0.5 } }, value), hint ? /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption", color: "text.secondary" }, hint) : null);
898
- ChartSkeleton = ({ height = 260 }) => /* @__PURE__ */ React3.createElement(Skeleton, { variant: "rectangular", height, sx: { borderRadius: 1 } });
1050
+ KpiCard = ({ label, value, hint }) => /* @__PURE__ */ React4.createElement(Paper3, { variant: "outlined", sx: { p: 2, height: "100%" } }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption", color: "text.secondary" }, label), /* @__PURE__ */ React4.createElement(Typography4, { variant: "h5", sx: { mt: 0.5 } }, value), hint ? /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption", color: "text.secondary" }, hint) : null);
1051
+ ChartSkeleton = ({ height = 260 }) => /* @__PURE__ */ React4.createElement(Skeleton, { variant: "rectangular", height, sx: { borderRadius: 1 } });
899
1052
  EmptyChart = ({
900
1053
  height = 260,
901
1054
  message = "No data for this period"
902
- }) => /* @__PURE__ */ React3.createElement(Box3, { height, display: "flex", alignItems: "center", justifyContent: "center" }, /* @__PURE__ */ React3.createElement(Typography3, { color: "text.secondary", variant: "body2" }, message));
1055
+ }) => /* @__PURE__ */ React4.createElement(Box4, { height, display: "flex", alignItems: "center", justifyContent: "center" }, /* @__PURE__ */ React4.createElement(Typography4, { color: "text.secondary", variant: "body2" }, message));
903
1056
  UsageStats = ({
904
1057
  usage,
905
1058
  models,
@@ -909,14 +1062,14 @@ var init_UsageStats = __esm({
909
1062
  userInfo
910
1063
  }) => {
911
1064
  const theme = useTheme();
912
- const [selectedModel, setSelectedModel] = useState2("all");
913
- const [tab, setTab] = useState2("costs");
1065
+ const [selectedModel, setSelectedModel] = useState3("all");
1066
+ const [tab, setTab] = useState3("costs");
914
1067
  const gridStroke = theme.palette.divider;
915
1068
  const tickFill = theme.palette.text.secondary;
916
1069
  const tickStyle = { fontSize: 12, fill: tickFill };
917
1070
  const tickStyleSmall = { fontSize: 11, fill: tickFill };
918
1071
  const tickStyleTiny = { fontSize: 10, fill: tickFill };
919
- const selectedPreset = useMemo2(() => {
1072
+ const selectedPreset = useMemo4(() => {
920
1073
  if (dateRange.start.toDateString() === dateRange.end.toDateString()) return "today";
921
1074
  const diffMs = dateRange.end.getTime() - dateRange.start.getTime();
922
1075
  const diffHours = diffMs / (1e3 * 60 * 60);
@@ -938,7 +1091,7 @@ var init_UsageStats = __esm({
938
1091
  }
939
1092
  onDateRangeChange({ start, end });
940
1093
  };
941
- const dailyData = useMemo2(
1094
+ const dailyData = useMemo4(
942
1095
  () => (usage?.daily_usage ?? []).map((d) => ({
943
1096
  date: d.date,
944
1097
  spend: d.spend,
@@ -951,21 +1104,21 @@ var init_UsageStats = __esm({
951
1104
  })),
952
1105
  [usage]
953
1106
  );
954
- const cumulativeData = useMemo2(() => {
1107
+ const cumulativeData = useMemo4(() => {
955
1108
  let cum = 0;
956
1109
  return dailyData.map((d) => {
957
1110
  cum += d.spend;
958
1111
  return { date: d.date, cumulative: cum };
959
1112
  });
960
1113
  }, [dailyData]);
961
- const successRateData = useMemo2(
1114
+ const successRateData = useMemo4(
962
1115
  () => dailyData.filter((d) => d.apiRequests > 0).map((d) => ({
963
1116
  date: d.date,
964
1117
  successRate: parseFloat((d.successfulRequests / d.apiRequests * 100).toFixed(1))
965
1118
  })),
966
1119
  [dailyData]
967
1120
  );
968
- const { modelSpendByDate, topModels: topSpendModels } = useMemo2(() => {
1121
+ const { modelSpendByDate, topModels: topSpendModels } = useMemo4(() => {
969
1122
  const rows = usage?.daily_by_model ?? [];
970
1123
  const modelTotals = {};
971
1124
  for (const r of rows) modelTotals[r.model] = (modelTotals[r.model] ?? 0) + r.spend;
@@ -980,7 +1133,7 @@ var init_UsageStats = __esm({
980
1133
  const hasOther = sorted.some((r) => r.Other > 0);
981
1134
  return { modelSpendByDate: sorted, topModels: hasOther ? [...top, "Other"] : top };
982
1135
  }, [usage]);
983
- const modelRows = useMemo2(() => {
1136
+ const modelRows = useMemo4(() => {
984
1137
  const entries = Object.entries(usage?.usage_by_model ?? {}).map(([model, d]) => ({
985
1138
  model,
986
1139
  spend: d.total_spend,
@@ -994,11 +1147,11 @@ var init_UsageStats = __esm({
994
1147
  }));
995
1148
  return selectedModel === "all" ? entries : entries.filter((e) => e.model === selectedModel);
996
1149
  }, [usage, selectedModel]);
997
- const topModelSpendBars = useMemo2(
1150
+ const topModelSpendBars = useMemo4(
998
1151
  () => [...modelRows].sort((a, b) => b.spend - a.spend).slice(0, 10),
999
1152
  [modelRows]
1000
1153
  );
1001
- const keyRows = useMemo2(
1154
+ const keyRows = useMemo4(
1002
1155
  () => Object.entries(usage?.usage_by_key ?? {}).map(([keyHash, d]) => ({
1003
1156
  keyHash,
1004
1157
  keyAlias: d.key_alias ?? keyHash.slice(0, 8),
@@ -1015,7 +1168,7 @@ var init_UsageStats = __esm({
1015
1168
  })),
1016
1169
  [usage]
1017
1170
  );
1018
- const topKeySpendBars = useMemo2(
1171
+ const topKeySpendBars = useMemo4(
1019
1172
  () => [...keyRows].sort((a, b) => b.spend - a.spend).slice(0, 10),
1020
1173
  [keyRows]
1021
1174
  );
@@ -1023,34 +1176,34 @@ var init_UsageStats = __esm({
1023
1176
  const overallSuccessRate = totalRequests > 0 ? (usage?.successful_requests ?? 0) / totalRequests : 0;
1024
1177
  const maxBudget = userInfo?.max_budget ?? 0;
1025
1178
  const totalCumSpend = cumulativeData[cumulativeData.length - 1]?.cumulative ?? 0;
1026
- return /* @__PURE__ */ React3.createElement(Paper3, { sx: { p: 2 } }, /* @__PURE__ */ React3.createElement(Box3, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2, flexWrap: "wrap", gap: 2 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "h6" }, "Usage Analytics"), /* @__PURE__ */ React3.createElement(Box3, { display: "flex", gap: 2, alignItems: "center", flexWrap: "wrap" }, /* @__PURE__ */ React3.createElement(FormControl, { size: "small", sx: { minWidth: 140 } }, /* @__PURE__ */ React3.createElement(InputLabel, null, "Period"), /* @__PURE__ */ React3.createElement(
1179
+ return /* @__PURE__ */ React4.createElement(Paper3, { sx: { p: 2 } }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2, flexWrap: "wrap", gap: 2 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "h6" }, "Usage Analytics"), /* @__PURE__ */ React4.createElement(Box4, { display: "flex", gap: 2, alignItems: "center", flexWrap: "wrap" }, /* @__PURE__ */ React4.createElement(FormControl, { size: "small", sx: { minWidth: 140 } }, /* @__PURE__ */ React4.createElement(InputLabel, null, "Period"), /* @__PURE__ */ React4.createElement(
1027
1180
  Select,
1028
1181
  {
1029
1182
  value: selectedPreset,
1030
1183
  label: "Period",
1031
1184
  onChange: (e) => handlePresetChange(e.target.value)
1032
1185
  },
1033
- /* @__PURE__ */ React3.createElement(MenuItem2, { value: "today" }, "Today"),
1034
- /* @__PURE__ */ React3.createElement(MenuItem2, { value: "24h" }, "Last 24h"),
1035
- /* @__PURE__ */ React3.createElement(MenuItem2, { value: "7d" }, "Last 7 days"),
1036
- /* @__PURE__ */ React3.createElement(MenuItem2, { value: "30d" }, "Last 30 days")
1037
- )), tab === "models" && /* @__PURE__ */ React3.createElement(FormControl, { size: "small", sx: { minWidth: 180 } }, /* @__PURE__ */ React3.createElement(InputLabel, null, "Model"), /* @__PURE__ */ React3.createElement(
1186
+ /* @__PURE__ */ React4.createElement(MenuItem2, { value: "today" }, "Today"),
1187
+ /* @__PURE__ */ React4.createElement(MenuItem2, { value: "24h" }, "Last 24h"),
1188
+ /* @__PURE__ */ React4.createElement(MenuItem2, { value: "7d" }, "Last 7 days"),
1189
+ /* @__PURE__ */ React4.createElement(MenuItem2, { value: "30d" }, "Last 30 days")
1190
+ )), tab === "models" && /* @__PURE__ */ React4.createElement(FormControl, { size: "small", sx: { minWidth: 180 } }, /* @__PURE__ */ React4.createElement(InputLabel, null, "Model"), /* @__PURE__ */ React4.createElement(
1038
1191
  Select,
1039
1192
  {
1040
1193
  value: selectedModel,
1041
1194
  label: "Model",
1042
1195
  onChange: (e) => setSelectedModel(e.target.value)
1043
1196
  },
1044
- /* @__PURE__ */ React3.createElement(MenuItem2, { value: "all" }, "All Models"),
1045
- models.map((m) => /* @__PURE__ */ React3.createElement(MenuItem2, { key: m.model_name, value: m.model_name }, m.model_name))
1046
- )))), /* @__PURE__ */ React3.createElement(Grid, { container: true, spacing: 2, sx: { mb: 2 } }, /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 6, sm: 3 }, loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, { height: 80 }) : /* @__PURE__ */ React3.createElement(KpiCard, { label: "Total Spend", value: fmtUsd(usage?.total_spend ?? 0) })), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 6, sm: 3 }, loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, { height: 80 }) : /* @__PURE__ */ React3.createElement(KpiCard, { label: "Total Requests", value: fmtInt(totalRequests), hint: `${fmtInt(usage?.failed_requests ?? 0)} failed` })), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 6, sm: 3 }, loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, { height: 80 }) : /* @__PURE__ */ React3.createElement(KpiCard, { label: "Success Rate", value: totalRequests > 0 ? fmtPct(overallSuccessRate) : "\u2014" })), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 6, sm: 3 }, loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, { height: 80 }) : /* @__PURE__ */ React3.createElement(
1197
+ /* @__PURE__ */ React4.createElement(MenuItem2, { value: "all" }, "All Models"),
1198
+ models.map((m) => /* @__PURE__ */ React4.createElement(MenuItem2, { key: m.model_name, value: m.model_name }, m.model_name))
1199
+ )))), /* @__PURE__ */ React4.createElement(Grid, { container: true, spacing: 2, sx: { mb: 2 } }, /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 6, sm: 3 }, loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, { height: 80 }) : /* @__PURE__ */ React4.createElement(KpiCard, { label: "Total Spend", value: fmtUsd(usage?.total_spend ?? 0) })), /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 6, sm: 3 }, loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, { height: 80 }) : /* @__PURE__ */ React4.createElement(KpiCard, { label: "Total Requests", value: fmtInt(totalRequests), hint: `${fmtInt(usage?.failed_requests ?? 0)} failed` })), /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 6, sm: 3 }, loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, { height: 80 }) : /* @__PURE__ */ React4.createElement(KpiCard, { label: "Success Rate", value: totalRequests > 0 ? fmtPct(overallSuccessRate) : "\u2014" })), /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 6, sm: 3 }, loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, { height: 80 }) : /* @__PURE__ */ React4.createElement(
1047
1200
  KpiCard,
1048
1201
  {
1049
1202
  label: "Total Tokens",
1050
1203
  value: fmtInt(usage?.total_tokens ?? 0),
1051
1204
  hint: `${fmtInt(usage?.prompt_tokens ?? 0)} in \xB7 ${fmtInt(usage?.completion_tokens ?? 0)} out`
1052
1205
  }
1053
- ))), /* @__PURE__ */ React3.createElement(Tabs2, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 2 } }, /* @__PURE__ */ React3.createElement(Tab2, { value: "costs", label: "Costs" }), /* @__PURE__ */ React3.createElement(Tab2, { value: "models", label: "Model Activity" }), /* @__PURE__ */ React3.createElement(Tab2, { value: "keys", label: "Key Activity" })), tab === "costs" && /* @__PURE__ */ React3.createElement(Grid, { container: true, spacing: 3 }, /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend by Model"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : modelSpendByDate.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: 260 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(AreaChart, { data: modelSpendByDate }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ React3.createElement(YAxis, { tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [`$${v.toFixed(4)}`, void 0] }), /* @__PURE__ */ React3.createElement(Legend, null), topSpendModels.map((m) => /* @__PURE__ */ React3.createElement(
1206
+ ))), /* @__PURE__ */ React4.createElement(Tabs2, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 2 } }, /* @__PURE__ */ React4.createElement(Tab2, { value: "costs", label: "Costs" }), /* @__PURE__ */ React4.createElement(Tab2, { value: "models", label: "Model Activity" }), /* @__PURE__ */ React4.createElement(Tab2, { value: "keys", label: "Key Activity" })), tab === "costs" && /* @__PURE__ */ React4.createElement(Grid, { container: true, spacing: 3 }, /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend by Model"), loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, null) : modelSpendByDate.length === 0 ? /* @__PURE__ */ React4.createElement(EmptyChart, null) : /* @__PURE__ */ React4.createElement(Box4, { height: 260 }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(AreaChart, { data: modelSpendByDate }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React4.createElement(XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ React4.createElement(YAxis, { tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React4.createElement(Tooltip, { formatter: (v) => [`$${v.toFixed(4)}`, void 0] }), /* @__PURE__ */ React4.createElement(Legend, null), topSpendModels.map((m) => /* @__PURE__ */ React4.createElement(
1054
1207
  Area,
1055
1208
  {
1056
1209
  key: m,
@@ -1061,35 +1214,35 @@ var init_UsageStats = __esm({
1061
1214
  fill: modelColor(m),
1062
1215
  fillOpacity: 0.6
1063
1216
  }
1064
- )))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Token Usage"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : dailyData.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: 260 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(AreaChart, { data: dailyData }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ React3.createElement(YAxis, { tick: tickStyle, tickFormatter: fmtInt }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [fmtInt(v), void 0] }), /* @__PURE__ */ React3.createElement(Legend, null), /* @__PURE__ */ React3.createElement(Area, { type: "monotone", dataKey: "promptTokens", name: "Input (prompt)", stackId: "tok", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.5 }), /* @__PURE__ */ React3.createElement(Area, { type: "monotone", dataKey: "completionTokens", name: "Output (completion)", stackId: "tok", stroke: "#82ca9d", fill: "#82ca9d", fillOpacity: 0.5 }))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Requests"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : dailyData.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: 260 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(BarChart, { data: dailyData }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ React3.createElement(YAxis, { tick: tickStyle }), /* @__PURE__ */ React3.createElement(Tooltip, null), /* @__PURE__ */ React3.createElement(Legend, null), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "successfulRequests", name: "Successful", fill: "#82ca9d", stackId: "r" }), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "failedRequests", name: "Failed", fill: "#e57373", stackId: "r" }))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Success Rate"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : successRateData.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: 260 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(LineChart, { data: successRateData }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ React3.createElement(YAxis, { domain: [0, 100], tick: tickStyle, tickFormatter: (v) => `${v}%` }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [`${v}%`, "Success rate"] }), /* @__PURE__ */ React3.createElement(ReferenceLine, { y: 100, stroke: "#82ca9d", strokeDasharray: "4 2" }), /* @__PURE__ */ React3.createElement(Line, { type: "monotone", dataKey: "successRate", name: "Success rate", stroke: "#8884d8", dot: { r: 3 } }))))), (maxBudget > 0 || cumulativeData.length > 0) && /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Cumulative Spend", maxBudget > 0 ? ` vs Budget (${fmtUsd(maxBudget)})` : "", maxBudget > 0 && /* @__PURE__ */ React3.createElement(Typography3, { component: "span", variant: "caption", color: "text.secondary", sx: { ml: 1 } }, fmtUsd(totalCumSpend), " used \xB7 ", fmtUsd(Math.max(0, maxBudget - totalCumSpend)), " remaining")), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, { height: 180 }) : cumulativeData.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, { height: 180 }) : /* @__PURE__ */ React3.createElement(Box3, { height: 180 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(AreaChart, { data: cumulativeData }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ React3.createElement(YAxis, { tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [`$${v.toFixed(4)}`, "Cumulative spend"] }), maxBudget > 0 && /* @__PURE__ */ React3.createElement(ReferenceLine, { y: maxBudget, stroke: "#e57373", strokeDasharray: "6 3", label: { value: `Budget ${fmtUsd(maxBudget)}`, position: "insideTopRight", fontSize: 11 } }), /* @__PURE__ */ React3.createElement(Area, { type: "monotone", dataKey: "cumulative", name: "Cumulative spend", stroke: "#ffc658", fill: "#ffc658", fillOpacity: 0.3 })))))), tab === "models" && /* @__PURE__ */ React3.createElement(Grid, { container: true, spacing: 3 }, /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Spend by Model"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : topModelSpendBars.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: Math.max(200, topModelSpendBars.length * 36) }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(BarChart, { data: topModelSpendBars, layout: "vertical", margin: { left: 20 } }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React3.createElement(XAxis, { type: "number", tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React3.createElement(YAxis, { type: "category", dataKey: "model", tick: tickStyleSmall, width: 200 }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [fmtUsd(v), "Spend"] }), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "spend", name: "Spend", radius: [0, 4, 4, 0] }, topModelSpendBars.map((r) => /* @__PURE__ */ React3.createElement("rect", { key: r.model, fill: modelColor(r.model) }))))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Tokens by Model"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : modelRows.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: 260 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(BarChart, { data: modelRows }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "model", tick: tickStyleTiny, interval: 0, angle: -15, textAnchor: "end", height: 60 }), /* @__PURE__ */ React3.createElement(YAxis, { tick: tickStyle }), /* @__PURE__ */ React3.createElement(Tooltip, null), /* @__PURE__ */ React3.createElement(Legend, null), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "promptTokens", name: "Prompt", fill: "#8884d8", stackId: "t" }), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "completionTokens", name: "Completion", fill: "#82ca9d", stackId: "t" }))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(TableContainer2, { component: Paper3, variant: "outlined" }, /* @__PURE__ */ React3.createElement(Table2, { size: "small" }, /* @__PURE__ */ React3.createElement(TableHead2, null, /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, null, "Model"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Spend"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Requests"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Success"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Failed"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Prompt"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Completion"), /* @__PURE__ */ React3.createElement(TableCell2, { sx: { minWidth: 120 } }, "Success rate"))), /* @__PURE__ */ React3.createElement(TableBody2, null, loading ? /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, { colSpan: 8 }, /* @__PURE__ */ React3.createElement(LinearProgress3, null))) : modelRows.length === 0 ? /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, { colSpan: 8, align: "center" }, "No model activity")) : [...modelRows].sort((a, b) => b.spend - a.spend || b.totalTokens - a.totalTokens).map((r) => /* @__PURE__ */ React3.createElement(TableRow2, { key: r.model }, /* @__PURE__ */ React3.createElement(TableCell2, null, /* @__PURE__ */ React3.createElement(Box3, { display: "flex", alignItems: "center", gap: 0.75 }, /* @__PURE__ */ React3.createElement(Box3, { sx: { width: 10, height: 10, borderRadius: "50%", bgcolor: modelColor(r.model), flexShrink: 0 } }), r.model)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtUsd(r.spend)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.apiRequests)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.successfulRequests)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.failedRequests)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.promptTokens)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.completionTokens)), /* @__PURE__ */ React3.createElement(TableCell2, null, r.apiRequests > 0 ? /* @__PURE__ */ React3.createElement(Box3, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React3.createElement(
1217
+ )))))), /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Token Usage"), loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, null) : dailyData.length === 0 ? /* @__PURE__ */ React4.createElement(EmptyChart, null) : /* @__PURE__ */ React4.createElement(Box4, { height: 260 }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(AreaChart, { data: dailyData }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React4.createElement(XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ React4.createElement(YAxis, { tick: tickStyle, tickFormatter: fmtInt }), /* @__PURE__ */ React4.createElement(Tooltip, { formatter: (v) => [fmtInt(v), void 0] }), /* @__PURE__ */ React4.createElement(Legend, null), /* @__PURE__ */ React4.createElement(Area, { type: "monotone", dataKey: "promptTokens", name: "Input (prompt)", stackId: "tok", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.5 }), /* @__PURE__ */ React4.createElement(Area, { type: "monotone", dataKey: "completionTokens", name: "Output (completion)", stackId: "tok", stroke: "#82ca9d", fill: "#82ca9d", fillOpacity: 0.5 }))))), /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Requests"), loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, null) : dailyData.length === 0 ? /* @__PURE__ */ React4.createElement(EmptyChart, null) : /* @__PURE__ */ React4.createElement(Box4, { height: 260 }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(BarChart, { data: dailyData }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React4.createElement(XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ React4.createElement(YAxis, { tick: tickStyle }), /* @__PURE__ */ React4.createElement(Tooltip, null), /* @__PURE__ */ React4.createElement(Legend, null), /* @__PURE__ */ React4.createElement(Bar, { dataKey: "successfulRequests", name: "Successful", fill: "#82ca9d", stackId: "r" }), /* @__PURE__ */ React4.createElement(Bar, { dataKey: "failedRequests", name: "Failed", fill: "#e57373", stackId: "r" }))))), /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Success Rate"), loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, null) : successRateData.length === 0 ? /* @__PURE__ */ React4.createElement(EmptyChart, null) : /* @__PURE__ */ React4.createElement(Box4, { height: 260 }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(LineChart, { data: successRateData }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React4.createElement(XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ React4.createElement(YAxis, { domain: [0, 100], tick: tickStyle, tickFormatter: (v) => `${v}%` }), /* @__PURE__ */ React4.createElement(Tooltip, { formatter: (v) => [`${v}%`, "Success rate"] }), /* @__PURE__ */ React4.createElement(ReferenceLine, { y: 100, stroke: "#82ca9d", strokeDasharray: "4 2" }), /* @__PURE__ */ React4.createElement(Line, { type: "monotone", dataKey: "successRate", name: "Success rate", stroke: "#8884d8", dot: { r: 3 } }))))), (maxBudget > 0 || cumulativeData.length > 0) && /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Cumulative Spend", maxBudget > 0 ? ` vs Budget (${fmtUsd(maxBudget)})` : "", maxBudget > 0 && /* @__PURE__ */ React4.createElement(Typography4, { component: "span", variant: "caption", color: "text.secondary", sx: { ml: 1 } }, fmtUsd(totalCumSpend), " used \xB7 ", fmtUsd(Math.max(0, maxBudget - totalCumSpend)), " remaining")), loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, { height: 180 }) : cumulativeData.length === 0 ? /* @__PURE__ */ React4.createElement(EmptyChart, { height: 180 }) : /* @__PURE__ */ React4.createElement(Box4, { height: 180 }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(AreaChart, { data: cumulativeData }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React4.createElement(XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ React4.createElement(YAxis, { tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React4.createElement(Tooltip, { formatter: (v) => [`$${v.toFixed(4)}`, "Cumulative spend"] }), maxBudget > 0 && /* @__PURE__ */ React4.createElement(ReferenceLine, { y: maxBudget, stroke: "#e57373", strokeDasharray: "6 3", label: { value: `Budget ${fmtUsd(maxBudget)}`, position: "insideTopRight", fontSize: 11 } }), /* @__PURE__ */ React4.createElement(Area, { type: "monotone", dataKey: "cumulative", name: "Cumulative spend", stroke: "#ffc658", fill: "#ffc658", fillOpacity: 0.3 })))))), tab === "models" && /* @__PURE__ */ React4.createElement(Grid, { container: true, spacing: 3 }, /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Spend by Model"), loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, null) : topModelSpendBars.length === 0 ? /* @__PURE__ */ React4.createElement(EmptyChart, null) : /* @__PURE__ */ React4.createElement(Box4, { height: Math.max(200, topModelSpendBars.length * 36) }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(BarChart, { data: topModelSpendBars, layout: "vertical", margin: { left: 20 } }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React4.createElement(XAxis, { type: "number", tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React4.createElement(YAxis, { type: "category", dataKey: "model", tick: tickStyleSmall, width: 200 }), /* @__PURE__ */ React4.createElement(Tooltip, { formatter: (v) => [fmtUsd(v), "Spend"] }), /* @__PURE__ */ React4.createElement(Bar, { dataKey: "spend", name: "Spend", radius: [0, 4, 4, 0] }, topModelSpendBars.map((r) => /* @__PURE__ */ React4.createElement("rect", { key: r.model, fill: modelColor(r.model) }))))))), /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Tokens by Model"), loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, null) : modelRows.length === 0 ? /* @__PURE__ */ React4.createElement(EmptyChart, null) : /* @__PURE__ */ React4.createElement(Box4, { height: 260 }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(BarChart, { data: modelRows }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React4.createElement(XAxis, { dataKey: "model", tick: tickStyleTiny, interval: 0, angle: -15, textAnchor: "end", height: 60 }), /* @__PURE__ */ React4.createElement(YAxis, { tick: tickStyle }), /* @__PURE__ */ React4.createElement(Tooltip, null), /* @__PURE__ */ React4.createElement(Legend, null), /* @__PURE__ */ React4.createElement(Bar, { dataKey: "promptTokens", name: "Prompt", fill: "#8884d8", stackId: "t" }), /* @__PURE__ */ React4.createElement(Bar, { dataKey: "completionTokens", name: "Completion", fill: "#82ca9d", stackId: "t" }))))), /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React4.createElement(TableContainer2, { component: Paper3, variant: "outlined" }, /* @__PURE__ */ React4.createElement(Table2, { size: "small" }, /* @__PURE__ */ React4.createElement(TableHead2, null, /* @__PURE__ */ React4.createElement(TableRow2, null, /* @__PURE__ */ React4.createElement(TableCell2, null, "Model"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Spend"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Requests"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Success"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Failed"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Prompt"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Completion"), /* @__PURE__ */ React4.createElement(TableCell2, { sx: { minWidth: 120 } }, "Success rate"))), /* @__PURE__ */ React4.createElement(TableBody2, null, loading ? /* @__PURE__ */ React4.createElement(TableRow2, null, /* @__PURE__ */ React4.createElement(TableCell2, { colSpan: 8 }, /* @__PURE__ */ React4.createElement(LinearProgress3, null))) : modelRows.length === 0 ? /* @__PURE__ */ React4.createElement(TableRow2, null, /* @__PURE__ */ React4.createElement(TableCell2, { colSpan: 8, align: "center" }, "No model activity")) : [...modelRows].sort((a, b) => b.spend - a.spend || b.totalTokens - a.totalTokens).map((r) => /* @__PURE__ */ React4.createElement(TableRow2, { key: r.model }, /* @__PURE__ */ React4.createElement(TableCell2, null, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 0.75 }, /* @__PURE__ */ React4.createElement(Box4, { sx: { width: 10, height: 10, borderRadius: "50%", bgcolor: modelColor(r.model), flexShrink: 0 } }), r.model)), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtUsd(r.spend)), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtInt(r.apiRequests)), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtInt(r.successfulRequests)), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtInt(r.failedRequests)), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtInt(r.promptTokens)), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtInt(r.completionTokens)), /* @__PURE__ */ React4.createElement(TableCell2, null, r.apiRequests > 0 ? /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React4.createElement(
1065
1218
  LinearProgress3,
1066
1219
  {
1067
1220
  variant: "determinate",
1068
1221
  value: r.successRate * 100,
1069
1222
  sx: { flex: 1, height: 6, borderRadius: 3 }
1070
1223
  }
1071
- ), /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption" }, fmtPct(r.successRate))) : "\u2014")))))))), tab === "keys" && /* @__PURE__ */ React3.createElement(Grid, { container: true, spacing: 3 }, (loading || topKeySpendBars.length > 0) && /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Spend by Key"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : /* @__PURE__ */ React3.createElement(Box3, { height: Math.max(160, topKeySpendBars.length * 36) }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(BarChart, { data: topKeySpendBars, layout: "vertical", margin: { left: 20 } }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React3.createElement(XAxis, { type: "number", tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React3.createElement(YAxis, { type: "category", dataKey: "keyAlias", tick: tickStyleSmall, width: 160 }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [fmtUsd(v), "Spend"] }), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "spend", name: "Spend", fill: "#8884d8", radius: [0, 4, 4, 0] }))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(TableContainer2, { component: Paper3, variant: "outlined" }, /* @__PURE__ */ React3.createElement(Table2, { size: "small" }, /* @__PURE__ */ React3.createElement(TableHead2, null, /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, null, "Key"), /* @__PURE__ */ React3.createElement(TableCell2, null, "Models"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Spend"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Requests"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Tokens"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Failed"), /* @__PURE__ */ React3.createElement(TableCell2, { sx: { minWidth: 120 } }, "Success rate"))), /* @__PURE__ */ React3.createElement(TableBody2, null, loading ? /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, { colSpan: 7 }, /* @__PURE__ */ React3.createElement(LinearProgress3, null))) : keyRows.length === 0 ? /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, { colSpan: 7, align: "center" }, "No key activity")) : [...keyRows].sort((a, b) => b.spend - a.spend || b.apiRequests - a.apiRequests).map((r) => /* @__PURE__ */ React3.createElement(TableRow2, { key: r.keyHash }, /* @__PURE__ */ React3.createElement(TableCell2, null, /* @__PURE__ */ React3.createElement(Typography3, { variant: "body2" }, r.keyAlias), r.teamId ? /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption", color: "text.secondary" }, "team: ", r.teamId) : null), /* @__PURE__ */ React3.createElement(TableCell2, null, /* @__PURE__ */ React3.createElement(Box3, { display: "flex", gap: 0.5, flexWrap: "wrap" }, r.models.map((m) => /* @__PURE__ */ React3.createElement(Chip3, { key: m, label: m, size: "small", variant: "outlined" })))), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtUsd(r.spend)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.apiRequests)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.totalTokens)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.failedRequests)), /* @__PURE__ */ React3.createElement(TableCell2, null, r.apiRequests > 0 ? /* @__PURE__ */ React3.createElement(Box3, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React3.createElement(
1224
+ ), /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption" }, fmtPct(r.successRate))) : "\u2014")))))))), tab === "keys" && /* @__PURE__ */ React4.createElement(Grid, { container: true, spacing: 3 }, (loading || topKeySpendBars.length > 0) && /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Spend by Key"), loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, null) : /* @__PURE__ */ React4.createElement(Box4, { height: Math.max(160, topKeySpendBars.length * 36) }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(BarChart, { data: topKeySpendBars, layout: "vertical", margin: { left: 20 } }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React4.createElement(XAxis, { type: "number", tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React4.createElement(YAxis, { type: "category", dataKey: "keyAlias", tick: tickStyleSmall, width: 160 }), /* @__PURE__ */ React4.createElement(Tooltip, { formatter: (v) => [fmtUsd(v), "Spend"] }), /* @__PURE__ */ React4.createElement(Bar, { dataKey: "spend", name: "Spend", fill: "#8884d8", radius: [0, 4, 4, 0] }))))), /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React4.createElement(TableContainer2, { component: Paper3, variant: "outlined" }, /* @__PURE__ */ React4.createElement(Table2, { size: "small" }, /* @__PURE__ */ React4.createElement(TableHead2, null, /* @__PURE__ */ React4.createElement(TableRow2, null, /* @__PURE__ */ React4.createElement(TableCell2, null, "Key"), /* @__PURE__ */ React4.createElement(TableCell2, null, "Models"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Spend"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Requests"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Tokens"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Failed"), /* @__PURE__ */ React4.createElement(TableCell2, { sx: { minWidth: 120 } }, "Success rate"))), /* @__PURE__ */ React4.createElement(TableBody2, null, loading ? /* @__PURE__ */ React4.createElement(TableRow2, null, /* @__PURE__ */ React4.createElement(TableCell2, { colSpan: 7 }, /* @__PURE__ */ React4.createElement(LinearProgress3, null))) : keyRows.length === 0 ? /* @__PURE__ */ React4.createElement(TableRow2, null, /* @__PURE__ */ React4.createElement(TableCell2, { colSpan: 7, align: "center" }, "No key activity")) : [...keyRows].sort((a, b) => b.spend - a.spend || b.apiRequests - a.apiRequests).map((r) => /* @__PURE__ */ React4.createElement(TableRow2, { key: r.keyHash }, /* @__PURE__ */ React4.createElement(TableCell2, null, /* @__PURE__ */ React4.createElement(Typography4, { variant: "body2" }, r.keyAlias), r.teamId ? /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption", color: "text.secondary" }, "team: ", r.teamId) : null), /* @__PURE__ */ React4.createElement(TableCell2, null, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", gap: 0.5, flexWrap: "wrap" }, r.models.map((m) => /* @__PURE__ */ React4.createElement(Chip3, { key: m, label: m, size: "small", variant: "outlined" })))), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtUsd(r.spend)), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtInt(r.apiRequests)), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtInt(r.totalTokens)), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtInt(r.failedRequests)), /* @__PURE__ */ React4.createElement(TableCell2, null, r.apiRequests > 0 ? /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React4.createElement(
1072
1225
  LinearProgress3,
1073
1226
  {
1074
1227
  variant: "determinate",
1075
1228
  value: r.successRate * 100,
1076
1229
  sx: { flex: 1, height: 6, borderRadius: 3 }
1077
1230
  }
1078
- ), /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption" }, fmtPct(r.successRate))) : "\u2014")))))))));
1231
+ ), /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption" }, fmtPct(r.successRate))) : "\u2014")))))))));
1079
1232
  };
1080
1233
  }
1081
1234
  });
1082
1235
 
1083
1236
  // src/components/TeamUsage.tsx
1084
- import React4, { useState as useState3 } from "react";
1237
+ import React5, { useState as useState4 } from "react";
1085
1238
  import {
1086
1239
  Paper as Paper4,
1087
- Box as Box4,
1088
- Typography as Typography4,
1240
+ Box as Box5,
1241
+ Typography as Typography5,
1089
1242
  LinearProgress as LinearProgress4,
1090
1243
  Chip as Chip4,
1091
1244
  Divider,
1092
- Stack,
1245
+ Stack as Stack2,
1093
1246
  Table as Table3,
1094
1247
  TableBody as TableBody3,
1095
1248
  TableCell as TableCell3,
@@ -1097,8 +1250,8 @@ import {
1097
1250
  TableRow as TableRow3,
1098
1251
  TableContainer as TableContainer3,
1099
1252
  Collapse,
1100
- IconButton as IconButton2,
1101
- CircularProgress as CircularProgress2,
1253
+ IconButton as IconButton3,
1254
+ CircularProgress as CircularProgress3,
1102
1255
  useTheme as useTheme2
1103
1256
  } from "@mui/material";
1104
1257
  import { ExpandMore, ExpandLess, Group, Speed, Memory } from "@mui/icons-material";
@@ -1115,9 +1268,9 @@ var Stat, TeamCard, TeamUsage;
1115
1268
  var init_TeamUsage = __esm({
1116
1269
  "src/components/TeamUsage.tsx"() {
1117
1270
  "use strict";
1118
- Stat = ({ label, value }) => /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ React4.createElement(Typography4, { variant: "body2", fontWeight: 600, sx: { fontFamily: "monospace" } }, value));
1271
+ Stat = ({ label, value }) => /* @__PURE__ */ React5.createElement(Box5, null, /* @__PURE__ */ React5.createElement(Typography5, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2", fontWeight: 600, sx: { fontFamily: "monospace" } }, value));
1119
1272
  TeamCard = ({ team, usage, usageLoading }) => {
1120
- const [expanded, setExpanded] = useState3(false);
1273
+ const [expanded, setExpanded] = useState4(false);
1121
1274
  const theme = useTheme2();
1122
1275
  const gridStroke = theme.palette.divider;
1123
1276
  const tickFill = theme.palette.text.secondary;
@@ -1127,43 +1280,43 @@ var init_TeamUsage = __esm({
1127
1280
  const isOver = budget > 0 && spend >= budget;
1128
1281
  const isNear = budget > 0 && spend >= budget * 0.8 && !isOver;
1129
1282
  const dailyData = usage?.daily_usage?.map((d) => ({ date: d.date, spend: d.spend })) ?? [];
1130
- return /* @__PURE__ */ React4.createElement(Paper4, { variant: "outlined", sx: { p: 2, mb: 2 } }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React4.createElement(Group, { color: "action" }), /* @__PURE__ */ React4.createElement(Box4, { flexGrow: 1 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle1", fontWeight: 600 }, team.team_alias || "Untitled team"), /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption", color: "text.secondary", sx: { fontFamily: "monospace" } }, team.team_id)), /* @__PURE__ */ React4.createElement(IconButton2, { size: "small", onClick: () => setExpanded((e) => !e), "aria-label": "toggle details" }, expanded ? /* @__PURE__ */ React4.createElement(ExpandLess, null) : /* @__PURE__ */ React4.createElement(ExpandMore, null))), /* @__PURE__ */ React4.createElement(Stack, { direction: "row", spacing: 3, mt: 1.5, flexWrap: "wrap", useFlexGap: true, gap: 1.5 }, /* @__PURE__ */ React4.createElement(
1283
+ return /* @__PURE__ */ React5.createElement(Paper4, { variant: "outlined", sx: { p: 2, mb: 2 } }, /* @__PURE__ */ React5.createElement(Box5, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React5.createElement(Group, { color: "action" }), /* @__PURE__ */ React5.createElement(Box5, { flexGrow: 1 }, /* @__PURE__ */ React5.createElement(Typography5, { variant: "subtitle1", fontWeight: 600 }, team.team_alias || "Untitled team"), /* @__PURE__ */ React5.createElement(Typography5, { variant: "caption", color: "text.secondary", sx: { fontFamily: "monospace" } }, team.team_id)), /* @__PURE__ */ React5.createElement(IconButton3, { size: "small", onClick: () => setExpanded((e) => !e), "aria-label": "toggle details" }, expanded ? /* @__PURE__ */ React5.createElement(ExpandLess, null) : /* @__PURE__ */ React5.createElement(ExpandMore, null))), /* @__PURE__ */ React5.createElement(Stack2, { direction: "row", spacing: 3, mt: 1.5, flexWrap: "wrap", useFlexGap: true, gap: 1.5 }, /* @__PURE__ */ React5.createElement(
1131
1284
  Stat,
1132
1285
  {
1133
1286
  label: "Members",
1134
1287
  value: team.members_with_roles?.length ? String(team.members_with_roles.length) : "\u2014"
1135
1288
  }
1136
- ), /* @__PURE__ */ React4.createElement(
1289
+ ), /* @__PURE__ */ React5.createElement(
1137
1290
  Stat,
1138
1291
  {
1139
1292
  label: "Models",
1140
1293
  value: team.models?.length ? String(team.models.length) : "All"
1141
1294
  }
1142
- ), /* @__PURE__ */ React4.createElement(
1295
+ ), /* @__PURE__ */ React5.createElement(
1143
1296
  Stat,
1144
1297
  {
1145
1298
  label: "Budget",
1146
1299
  value: budget > 0 ? `$${budget.toFixed(2)}` : "Unlimited"
1147
1300
  }
1148
- ), /* @__PURE__ */ React4.createElement(
1301
+ ), /* @__PURE__ */ React5.createElement(
1149
1302
  Stat,
1150
1303
  {
1151
1304
  label: "Spend",
1152
1305
  value: `$${spend.toFixed(2)}`
1153
1306
  }
1154
- ), /* @__PURE__ */ React4.createElement(
1307
+ ), /* @__PURE__ */ React5.createElement(
1155
1308
  Stat,
1156
1309
  {
1157
1310
  label: "TPM",
1158
1311
  value: team.tpm_limit != null && team.tpm_limit > 0 ? String(team.tpm_limit) : "\u2014"
1159
1312
  }
1160
- ), /* @__PURE__ */ React4.createElement(
1313
+ ), /* @__PURE__ */ React5.createElement(
1161
1314
  Stat,
1162
1315
  {
1163
1316
  label: "RPM",
1164
1317
  value: team.rpm_limit != null && team.rpm_limit > 0 ? String(team.rpm_limit) : "\u2014"
1165
1318
  }
1166
- )), budget > 0 && /* @__PURE__ */ React4.createElement(Box4, { mt: 1.5 }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", justifyContent: "space-between", mb: 0.5 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "body2" }, "$", spend.toFixed(2), " / $", budget.toFixed(2)), isOver && /* @__PURE__ */ React4.createElement(Chip4, { label: "Over Budget", size: "small", color: "error" }), isNear && /* @__PURE__ */ React4.createElement(Chip4, { label: "Near Limit", size: "small", color: "warning" })), /* @__PURE__ */ React4.createElement(
1319
+ )), budget > 0 && /* @__PURE__ */ React5.createElement(Box5, { mt: 1.5 }, /* @__PURE__ */ React5.createElement(Box5, { display: "flex", justifyContent: "space-between", mb: 0.5 }, /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2" }, "$", spend.toFixed(2), " / $", budget.toFixed(2)), isOver && /* @__PURE__ */ React5.createElement(Chip4, { label: "Over Budget", size: "small", color: "error" }), isNear && /* @__PURE__ */ React5.createElement(Chip4, { label: "Near Limit", size: "small", color: "warning" })), /* @__PURE__ */ React5.createElement(
1167
1320
  LinearProgress4,
1168
1321
  {
1169
1322
  variant: "determinate",
@@ -1171,22 +1324,22 @@ var init_TeamUsage = __esm({
1171
1324
  color: isOver ? "error" : isNear ? "warning" : "primary",
1172
1325
  sx: { height: 6, borderRadius: 1 }
1173
1326
  }
1174
- )), /* @__PURE__ */ React4.createElement(Collapse, { in: expanded }, /* @__PURE__ */ React4.createElement(Box4, { mt: 2 }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React4.createElement(Speed, { fontSize: "small", color: "action" }), /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary" }, "Rate limits")), /* @__PURE__ */ React4.createElement(Stack, { direction: "row", spacing: 3, flexWrap: "wrap", useFlexGap: true, gap: 1.5, mb: 2 }, /* @__PURE__ */ React4.createElement(Stat, { label: "TPM limit", value: team.tpm_limit != null && team.tpm_limit > 0 ? String(team.tpm_limit) : "Unlimited" }), /* @__PURE__ */ React4.createElement(Stat, { label: "RPM limit", value: team.rpm_limit != null && team.rpm_limit > 0 ? String(team.rpm_limit) : "Unlimited" }), /* @__PURE__ */ React4.createElement(Stat, { label: "Max budget", value: budget > 0 ? `$${budget.toFixed(2)}` : "Unlimited" })), /* @__PURE__ */ React4.createElement(Divider, { sx: { mb: 2 } }), /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Models"), team.models?.length ? /* @__PURE__ */ React4.createElement(Box4, { display: "flex", gap: 0.5, flexWrap: "wrap", mb: 2 }, team.models.map((m) => /* @__PURE__ */ React4.createElement(Chip4, { key: m, label: m, size: "small", variant: "outlined" }))) : /* @__PURE__ */ React4.createElement(Typography4, { variant: "body2", color: "text.secondary", mb: 2 }, "All models allowed"), /* @__PURE__ */ React4.createElement(Divider, { sx: { mb: 2 } }), usageLoading ? /* @__PURE__ */ React4.createElement(Box4, { display: "flex", justifyContent: "center", p: 2 }, /* @__PURE__ */ React4.createElement(CircularProgress2, { size: 24 })) : dailyData.length > 0 ? /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend"), /* @__PURE__ */ React4.createElement(Box4, { height: 160, mb: 2 }, /* @__PURE__ */ React4.createElement(ResponsiveContainer2, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(AreaChart2, { data: dailyData }, /* @__PURE__ */ React4.createElement(CartesianGrid2, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React4.createElement(XAxis2, { dataKey: "date", tick: { fontSize: 11, fill: tickFill } }), /* @__PURE__ */ React4.createElement(YAxis2, { tick: { fontSize: 11, fill: tickFill }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React4.createElement(Tooltip2, { formatter: (v) => [`$${v.toFixed(4)}`, "Spend"] }), /* @__PURE__ */ React4.createElement(Area2, { type: "monotone", dataKey: "spend", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.3 })))), /* @__PURE__ */ React4.createElement(Divider, { sx: { mb: 2 } })) : null, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React4.createElement(Memory, { fontSize: "small", color: "action" }), /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary" }, "Members")), team.members_with_roles?.length ? /* @__PURE__ */ React4.createElement(TableContainer3, null, /* @__PURE__ */ React4.createElement(Table3, { size: "small" }, /* @__PURE__ */ React4.createElement(TableHead3, null, /* @__PURE__ */ React4.createElement(TableRow3, null, /* @__PURE__ */ React4.createElement(TableCell3, null, "User"), /* @__PURE__ */ React4.createElement(TableCell3, null, "Role"))), /* @__PURE__ */ React4.createElement(TableBody3, null, team.members_with_roles.map((m) => /* @__PURE__ */ React4.createElement(TableRow3, { key: m.user_id }, /* @__PURE__ */ React4.createElement(TableCell3, null, m.user_email ? /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement(Typography4, { variant: "body2" }, m.user_email), /* @__PURE__ */ React4.createElement(
1175
- Typography4,
1327
+ )), /* @__PURE__ */ React5.createElement(Collapse, { in: expanded }, /* @__PURE__ */ React5.createElement(Box5, { mt: 2 }, /* @__PURE__ */ React5.createElement(Box5, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React5.createElement(Speed, { fontSize: "small", color: "action" }), /* @__PURE__ */ React5.createElement(Typography5, { variant: "subtitle2", color: "text.secondary" }, "Rate limits")), /* @__PURE__ */ React5.createElement(Stack2, { direction: "row", spacing: 3, flexWrap: "wrap", useFlexGap: true, gap: 1.5, mb: 2 }, /* @__PURE__ */ React5.createElement(Stat, { label: "TPM limit", value: team.tpm_limit != null && team.tpm_limit > 0 ? String(team.tpm_limit) : "Unlimited" }), /* @__PURE__ */ React5.createElement(Stat, { label: "RPM limit", value: team.rpm_limit != null && team.rpm_limit > 0 ? String(team.rpm_limit) : "Unlimited" }), /* @__PURE__ */ React5.createElement(Stat, { label: "Max budget", value: budget > 0 ? `$${budget.toFixed(2)}` : "Unlimited" })), /* @__PURE__ */ React5.createElement(Divider, { sx: { mb: 2 } }), /* @__PURE__ */ React5.createElement(Typography5, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Models"), team.models?.length ? /* @__PURE__ */ React5.createElement(Box5, { display: "flex", gap: 0.5, flexWrap: "wrap", mb: 2 }, team.models.map((m) => /* @__PURE__ */ React5.createElement(Chip4, { key: m, label: m, size: "small", variant: "outlined" }))) : /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2", color: "text.secondary", mb: 2 }, "All models allowed"), /* @__PURE__ */ React5.createElement(Divider, { sx: { mb: 2 } }), usageLoading ? /* @__PURE__ */ React5.createElement(Box5, { display: "flex", justifyContent: "center", p: 2 }, /* @__PURE__ */ React5.createElement(CircularProgress3, { size: 24 })) : dailyData.length > 0 ? /* @__PURE__ */ React5.createElement(React5.Fragment, null, /* @__PURE__ */ React5.createElement(Typography5, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend"), /* @__PURE__ */ React5.createElement(Box5, { height: 160, mb: 2 }, /* @__PURE__ */ React5.createElement(ResponsiveContainer2, { width: "100%", height: "100%" }, /* @__PURE__ */ React5.createElement(AreaChart2, { data: dailyData }, /* @__PURE__ */ React5.createElement(CartesianGrid2, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React5.createElement(XAxis2, { dataKey: "date", tick: { fontSize: 11, fill: tickFill } }), /* @__PURE__ */ React5.createElement(YAxis2, { tick: { fontSize: 11, fill: tickFill }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React5.createElement(Tooltip2, { formatter: (v) => [`$${v.toFixed(4)}`, "Spend"] }), /* @__PURE__ */ React5.createElement(Area2, { type: "monotone", dataKey: "spend", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.3 })))), /* @__PURE__ */ React5.createElement(Divider, { sx: { mb: 2 } })) : null, /* @__PURE__ */ React5.createElement(Box5, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React5.createElement(Memory, { fontSize: "small", color: "action" }), /* @__PURE__ */ React5.createElement(Typography5, { variant: "subtitle2", color: "text.secondary" }, "Members")), team.members_with_roles?.length ? /* @__PURE__ */ React5.createElement(TableContainer3, null, /* @__PURE__ */ React5.createElement(Table3, { size: "small" }, /* @__PURE__ */ React5.createElement(TableHead3, null, /* @__PURE__ */ React5.createElement(TableRow3, null, /* @__PURE__ */ React5.createElement(TableCell3, null, "User"), /* @__PURE__ */ React5.createElement(TableCell3, null, "Role"))), /* @__PURE__ */ React5.createElement(TableBody3, null, team.members_with_roles.map((m) => /* @__PURE__ */ React5.createElement(TableRow3, { key: m.user_id }, /* @__PURE__ */ React5.createElement(TableCell3, null, m.user_email ? /* @__PURE__ */ React5.createElement(React5.Fragment, null, /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2" }, m.user_email), /* @__PURE__ */ React5.createElement(
1328
+ Typography5,
1176
1329
  {
1177
1330
  variant: "caption",
1178
1331
  color: "text.secondary",
1179
1332
  sx: { fontFamily: "monospace" }
1180
1333
  },
1181
1334
  m.user_id
1182
- )) : /* @__PURE__ */ React4.createElement(Typography4, { variant: "body2", sx: { fontFamily: "monospace" } }, m.user_id)), /* @__PURE__ */ React4.createElement(TableCell3, null, /* @__PURE__ */ React4.createElement(
1335
+ )) : /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2", sx: { fontFamily: "monospace" } }, m.user_id)), /* @__PURE__ */ React5.createElement(TableCell3, null, /* @__PURE__ */ React5.createElement(
1183
1336
  Chip4,
1184
1337
  {
1185
1338
  label: m.role,
1186
1339
  size: "small",
1187
1340
  color: m.role === "admin" ? "primary" : "default"
1188
1341
  }
1189
- ))))))) : /* @__PURE__ */ React4.createElement(Typography4, { variant: "body2", color: "text.secondary" }, "No members assigned."))));
1342
+ ))))))) : /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2", color: "text.secondary" }, "No members assigned."))));
1190
1343
  };
1191
1344
  TeamUsage = ({
1192
1345
  teams,
@@ -1195,12 +1348,12 @@ var init_TeamUsage = __esm({
1195
1348
  getTeamUsageLoading
1196
1349
  }) => {
1197
1350
  if (loading) {
1198
- return /* @__PURE__ */ React4.createElement(Paper4, { sx: { p: 2 } }, /* @__PURE__ */ React4.createElement(LinearProgress4, null));
1351
+ return /* @__PURE__ */ React5.createElement(Paper4, { sx: { p: 2 } }, /* @__PURE__ */ React5.createElement(LinearProgress4, null));
1199
1352
  }
1200
1353
  if (!teams.length) {
1201
- return /* @__PURE__ */ React4.createElement(Paper4, { sx: { p: 2 } }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "flex-start", gap: 1.5 }, /* @__PURE__ */ React4.createElement(Group, { color: "disabled", sx: { mt: 0.5 } }), /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Typography4, { color: "text.secondary", variant: "body2" }, "You're not a member of any LiteLLM team yet."), /* @__PURE__ */ React4.createElement(Typography4, { color: "text.secondary", variant: "body2", mt: 0.5 }, "That's fine \u2014 your account is provisioned, so you can still generate personal keys and use models. If you need a shared budget with colleagues, ask an admin to add you to a team."))));
1354
+ return /* @__PURE__ */ React5.createElement(Paper4, { sx: { p: 2 } }, /* @__PURE__ */ React5.createElement(Box5, { display: "flex", alignItems: "flex-start", gap: 1.5 }, /* @__PURE__ */ React5.createElement(Group, { color: "disabled", sx: { mt: 0.5 } }), /* @__PURE__ */ React5.createElement(Box5, null, /* @__PURE__ */ React5.createElement(Typography5, { color: "text.secondary", variant: "body2" }, "You're not a member of any LiteLLM team yet."), /* @__PURE__ */ React5.createElement(Typography5, { color: "text.secondary", variant: "body2", mt: 0.5 }, "That's fine \u2014 your account is provisioned, so you can still generate personal keys and use models. If you need a shared budget with colleagues, ask an admin to add you to a team."))));
1202
1355
  }
1203
- return /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Typography4, { variant: "h6", mb: 1 }, "Teams"), teams.map((team) => /* @__PURE__ */ React4.createElement(
1356
+ return /* @__PURE__ */ React5.createElement(Box5, null, /* @__PURE__ */ React5.createElement(Typography5, { variant: "h6", mb: 1 }, "Teams"), teams.map((team) => /* @__PURE__ */ React5.createElement(
1204
1357
  TeamCard,
1205
1358
  {
1206
1359
  key: team.team_id,
@@ -1214,11 +1367,11 @@ var init_TeamUsage = __esm({
1214
1367
  });
1215
1368
 
1216
1369
  // src/components/AuditLog.tsx
1217
- import React5, { useState as useState4, useCallback } from "react";
1370
+ import React6, { useState as useState5, useCallback } from "react";
1218
1371
  import {
1219
1372
  Paper as Paper5,
1220
- Box as Box5,
1221
- Typography as Typography5,
1373
+ Box as Box6,
1374
+ Typography as Typography6,
1222
1375
  Table as Table4,
1223
1376
  TableBody as TableBody4,
1224
1377
  TableCell as TableCell4,
@@ -1226,13 +1379,13 @@ import {
1226
1379
  TableHead as TableHead4,
1227
1380
  TableRow as TableRow4,
1228
1381
  TablePagination,
1229
- TextField as TextField2,
1382
+ TextField as TextField3,
1230
1383
  MenuItem as MenuItem3,
1231
1384
  Chip as Chip5,
1232
- CircularProgress as CircularProgress3,
1385
+ CircularProgress as CircularProgress4,
1233
1386
  Collapse as Collapse2,
1234
- IconButton as IconButton3,
1235
- Alert
1387
+ IconButton as IconButton4,
1388
+ Alert as Alert2
1236
1389
  } from "@mui/material";
1237
1390
  import { KeyboardArrowDown, KeyboardArrowUp } from "@mui/icons-material";
1238
1391
  import { useAsync } from "react-use";
@@ -1262,14 +1415,14 @@ var init_AuditLog = __esm({
1262
1415
  unblocked: "success"
1263
1416
  };
1264
1417
  DetailRow = ({ entry }) => {
1265
- const [open, setOpen] = useState4(false);
1418
+ const [open, setOpen] = useState5(false);
1266
1419
  const hasDetail = entry.before_value || entry.updated_values;
1267
- return /* @__PURE__ */ React5.createElement(React5.Fragment, null, /* @__PURE__ */ React5.createElement(TableRow4, { hover: true }, /* @__PURE__ */ React5.createElement(TableCell4, { sx: { width: 40, pr: 0 } }, hasDetail && /* @__PURE__ */ React5.createElement(IconButton3, { size: "small", onClick: () => setOpen((o) => !o) }, open ? /* @__PURE__ */ React5.createElement(KeyboardArrowUp, { fontSize: "small" }) : /* @__PURE__ */ React5.createElement(KeyboardArrowDown, { fontSize: "small" }))), /* @__PURE__ */ React5.createElement(TableCell4, { sx: { whiteSpace: "nowrap" } }, formatDateTime(entry.updated_at)), /* @__PURE__ */ React5.createElement(TableCell4, null, entry.action && /* @__PURE__ */ React5.createElement(Chip5, { label: entry.action, color: actionColor(entry.action), size: "small" })), /* @__PURE__ */ React5.createElement(TableCell4, null, entry.table_name ?? "-"), /* @__PURE__ */ React5.createElement(TableCell4, null, /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2", component: "code", sx: { fontFamily: "monospace", fontSize: "0.75rem" } }, entry.object_id ? entry.object_id.slice(0, 20) + (entry.object_id.length > 20 ? "\u2026" : "") : "-")), /* @__PURE__ */ React5.createElement(TableCell4, null, entry.changed_by ?? "-")), hasDetail && /* @__PURE__ */ React5.createElement(TableRow4, null, /* @__PURE__ */ React5.createElement(TableCell4, { colSpan: 6, sx: { py: 0 } }, /* @__PURE__ */ React5.createElement(Collapse2, { in: open, unmountOnExit: true }, /* @__PURE__ */ React5.createElement(Box5, { p: 2, display: "flex", gap: 2, flexWrap: "wrap" }, entry.before_value && /* @__PURE__ */ React5.createElement(Box5, { flex: 1, minWidth: 200 }, /* @__PURE__ */ React5.createElement(Typography5, { variant: "caption", color: "text.secondary", display: "block", mb: 0.5 }, "Before"), /* @__PURE__ */ React5.createElement(Typography5, { component: "pre", variant: "caption", sx: { fontFamily: "monospace", whiteSpace: "pre-wrap", wordBreak: "break-all" } }, JSON.stringify(entry.before_value, null, 2))), entry.updated_values && /* @__PURE__ */ React5.createElement(Box5, { flex: 1, minWidth: 200 }, /* @__PURE__ */ React5.createElement(Typography5, { variant: "caption", color: "text.secondary", display: "block", mb: 0.5 }, "After"), /* @__PURE__ */ React5.createElement(Typography5, { component: "pre", variant: "caption", sx: { fontFamily: "monospace", whiteSpace: "pre-wrap", wordBreak: "break-all" } }, JSON.stringify(entry.updated_values, null, 2))))))));
1420
+ return /* @__PURE__ */ React6.createElement(React6.Fragment, null, /* @__PURE__ */ React6.createElement(TableRow4, { hover: true }, /* @__PURE__ */ React6.createElement(TableCell4, { sx: { width: 40, pr: 0 } }, hasDetail && /* @__PURE__ */ React6.createElement(IconButton4, { size: "small", onClick: () => setOpen((o) => !o) }, open ? /* @__PURE__ */ React6.createElement(KeyboardArrowUp, { fontSize: "small" }) : /* @__PURE__ */ React6.createElement(KeyboardArrowDown, { fontSize: "small" }))), /* @__PURE__ */ React6.createElement(TableCell4, { sx: { whiteSpace: "nowrap" } }, formatDateTime(entry.updated_at)), /* @__PURE__ */ React6.createElement(TableCell4, null, entry.action && /* @__PURE__ */ React6.createElement(Chip5, { label: entry.action, color: actionColor(entry.action), size: "small" })), /* @__PURE__ */ React6.createElement(TableCell4, null, entry.table_name ?? "-"), /* @__PURE__ */ React6.createElement(TableCell4, null, /* @__PURE__ */ React6.createElement(Typography6, { 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__ */ React6.createElement(TableCell4, null, entry.changed_by ?? "-")), hasDetail && /* @__PURE__ */ React6.createElement(TableRow4, null, /* @__PURE__ */ React6.createElement(TableCell4, { colSpan: 6, sx: { py: 0 } }, /* @__PURE__ */ React6.createElement(Collapse2, { in: open, unmountOnExit: true }, /* @__PURE__ */ React6.createElement(Box6, { p: 2, display: "flex", gap: 2, flexWrap: "wrap" }, entry.before_value && /* @__PURE__ */ React6.createElement(Box6, { flex: 1, minWidth: 200 }, /* @__PURE__ */ React6.createElement(Typography6, { variant: "caption", color: "text.secondary", display: "block", mb: 0.5 }, "Before"), /* @__PURE__ */ React6.createElement(Typography6, { component: "pre", variant: "caption", sx: { fontFamily: "monospace", whiteSpace: "pre-wrap", wordBreak: "break-all" } }, JSON.stringify(entry.before_value, null, 2))), entry.updated_values && /* @__PURE__ */ React6.createElement(Box6, { flex: 1, minWidth: 200 }, /* @__PURE__ */ React6.createElement(Typography6, { variant: "caption", color: "text.secondary", display: "block", mb: 0.5 }, "After"), /* @__PURE__ */ React6.createElement(Typography6, { component: "pre", variant: "caption", sx: { fontFamily: "monospace", whiteSpace: "pre-wrap", wordBreak: "break-all" } }, JSON.stringify(entry.updated_values, null, 2))))))));
1268
1421
  };
1269
1422
  AuditLog = ({ api }) => {
1270
- const [page, setPage] = useState4(0);
1271
- const [pageSize, setPageSize] = useState4(25);
1272
- const [filters, setFilters] = useState4({});
1423
+ const [page, setPage] = useState5(0);
1424
+ const [pageSize, setPageSize] = useState5(25);
1425
+ const [filters, setFilters] = useState5({});
1273
1426
  const fetchParams = useCallback(
1274
1427
  () => ({ page: page + 1, page_size: pageSize, ...filters }),
1275
1428
  [page, pageSize, filters]
@@ -1280,8 +1433,8 @@ var init_AuditLog = __esm({
1280
1433
  );
1281
1434
  const entries = value?.audit_logs ?? [];
1282
1435
  const total = value?.total ?? 0;
1283
- return /* @__PURE__ */ React5.createElement(Paper5, null, /* @__PURE__ */ React5.createElement(Box5, { p: 2, display: "flex", gap: 2, flexWrap: "wrap", alignItems: "center" }, /* @__PURE__ */ React5.createElement(Typography5, { variant: "h6", sx: { flex: "0 0 auto", mr: 1 } }, "Audit Log"), /* @__PURE__ */ React5.createElement(
1284
- TextField2,
1436
+ return /* @__PURE__ */ React6.createElement(Paper5, null, /* @__PURE__ */ React6.createElement(Box6, { p: 2, display: "flex", gap: 2, flexWrap: "wrap", alignItems: "center" }, /* @__PURE__ */ React6.createElement(Typography6, { variant: "h6", sx: { flex: "0 0 auto", mr: 1 } }, "Audit Log"), /* @__PURE__ */ React6.createElement(
1437
+ TextField3,
1285
1438
  {
1286
1439
  size: "small",
1287
1440
  label: "Action",
@@ -1293,13 +1446,13 @@ var init_AuditLog = __esm({
1293
1446
  },
1294
1447
  sx: { minWidth: 160 }
1295
1448
  },
1296
- /* @__PURE__ */ React5.createElement(MenuItem3, { value: "" }, "All actions"),
1297
- /* @__PURE__ */ React5.createElement(MenuItem3, { value: "created" }, "Created"),
1298
- /* @__PURE__ */ React5.createElement(MenuItem3, { value: "updated" }, "Updated"),
1299
- /* @__PURE__ */ React5.createElement(MenuItem3, { value: "deleted" }, "Deleted"),
1300
- /* @__PURE__ */ React5.createElement(MenuItem3, { value: "blocked" }, "Blocked")
1301
- ), /* @__PURE__ */ React5.createElement(
1302
- TextField2,
1449
+ /* @__PURE__ */ React6.createElement(MenuItem3, { value: "" }, "All actions"),
1450
+ /* @__PURE__ */ React6.createElement(MenuItem3, { value: "created" }, "Created"),
1451
+ /* @__PURE__ */ React6.createElement(MenuItem3, { value: "updated" }, "Updated"),
1452
+ /* @__PURE__ */ React6.createElement(MenuItem3, { value: "deleted" }, "Deleted"),
1453
+ /* @__PURE__ */ React6.createElement(MenuItem3, { value: "blocked" }, "Blocked")
1454
+ ), /* @__PURE__ */ React6.createElement(
1455
+ TextField3,
1303
1456
  {
1304
1457
  size: "small",
1305
1458
  label: "Table",
@@ -1311,12 +1464,12 @@ var init_AuditLog = __esm({
1311
1464
  },
1312
1465
  sx: { minWidth: 160 }
1313
1466
  },
1314
- /* @__PURE__ */ React5.createElement(MenuItem3, { value: "" }, "All tables"),
1315
- /* @__PURE__ */ React5.createElement(MenuItem3, { value: "LiteLLM_VerificationToken" }, "Key"),
1316
- /* @__PURE__ */ React5.createElement(MenuItem3, { value: "LiteLLM_TeamTable" }, "Team"),
1317
- /* @__PURE__ */ React5.createElement(MenuItem3, { value: "LiteLLM_UserTable" }, "User")
1318
- ), /* @__PURE__ */ React5.createElement(
1319
- TextField2,
1467
+ /* @__PURE__ */ React6.createElement(MenuItem3, { value: "" }, "All tables"),
1468
+ /* @__PURE__ */ React6.createElement(MenuItem3, { value: "LiteLLM_VerificationToken" }, "Key"),
1469
+ /* @__PURE__ */ React6.createElement(MenuItem3, { value: "LiteLLM_TeamTable" }, "Team"),
1470
+ /* @__PURE__ */ React6.createElement(MenuItem3, { value: "LiteLLM_UserTable" }, "User")
1471
+ ), /* @__PURE__ */ React6.createElement(
1472
+ TextField3,
1320
1473
  {
1321
1474
  size: "small",
1322
1475
  label: "Changed by",
@@ -1327,7 +1480,7 @@ var init_AuditLog = __esm({
1327
1480
  },
1328
1481
  sx: { minWidth: 200 }
1329
1482
  }
1330
- )), error && /* @__PURE__ */ React5.createElement(Box5, { px: 2, pb: 2 }, /* @__PURE__ */ React5.createElement(Alert, { severity: "error" }, error.message)), /* @__PURE__ */ React5.createElement(TableContainer4, null, /* @__PURE__ */ React5.createElement(Table4, { size: "small" }, /* @__PURE__ */ React5.createElement(TableHead4, null, /* @__PURE__ */ React5.createElement(TableRow4, null, /* @__PURE__ */ React5.createElement(TableCell4, { sx: { width: 40 } }), /* @__PURE__ */ React5.createElement(TableCell4, null, "Time"), /* @__PURE__ */ React5.createElement(TableCell4, null, "Action"), /* @__PURE__ */ React5.createElement(TableCell4, null, "Table"), /* @__PURE__ */ React5.createElement(TableCell4, null, "Object ID"), /* @__PURE__ */ React5.createElement(TableCell4, null, "Changed By"))), /* @__PURE__ */ React5.createElement(TableBody4, null, loading ? /* @__PURE__ */ React5.createElement(TableRow4, null, /* @__PURE__ */ React5.createElement(TableCell4, { colSpan: 6, align: "center", sx: { py: 4 } }, /* @__PURE__ */ React5.createElement(CircularProgress3, { size: 24 }))) : entries.length === 0 ? /* @__PURE__ */ React5.createElement(TableRow4, null, /* @__PURE__ */ React5.createElement(TableCell4, { colSpan: 6, align: "center", sx: { py: 4 } }, /* @__PURE__ */ React5.createElement(Typography5, { color: "text.secondary" }, "No audit events found"))) : entries.map((entry) => /* @__PURE__ */ React5.createElement(DetailRow, { key: entry.id, entry }))))), /* @__PURE__ */ React5.createElement(
1483
+ )), error && /* @__PURE__ */ React6.createElement(Box6, { px: 2, pb: 2 }, /* @__PURE__ */ React6.createElement(Alert2, { severity: "error" }, error.message)), /* @__PURE__ */ React6.createElement(TableContainer4, null, /* @__PURE__ */ React6.createElement(Table4, { size: "small" }, /* @__PURE__ */ React6.createElement(TableHead4, null, /* @__PURE__ */ React6.createElement(TableRow4, null, /* @__PURE__ */ React6.createElement(TableCell4, { sx: { width: 40 } }), /* @__PURE__ */ React6.createElement(TableCell4, null, "Time"), /* @__PURE__ */ React6.createElement(TableCell4, null, "Action"), /* @__PURE__ */ React6.createElement(TableCell4, null, "Table"), /* @__PURE__ */ React6.createElement(TableCell4, null, "Object ID"), /* @__PURE__ */ React6.createElement(TableCell4, null, "Changed By"))), /* @__PURE__ */ React6.createElement(TableBody4, null, loading ? /* @__PURE__ */ React6.createElement(TableRow4, null, /* @__PURE__ */ React6.createElement(TableCell4, { colSpan: 6, align: "center", sx: { py: 4 } }, /* @__PURE__ */ React6.createElement(CircularProgress4, { size: 24 }))) : entries.length === 0 ? /* @__PURE__ */ React6.createElement(TableRow4, null, /* @__PURE__ */ React6.createElement(TableCell4, { colSpan: 6, align: "center", sx: { py: 4 } }, /* @__PURE__ */ React6.createElement(Typography6, { color: "text.secondary" }, "No audit events found"))) : entries.map((entry) => /* @__PURE__ */ React6.createElement(DetailRow, { key: entry.id, entry }))))), /* @__PURE__ */ React6.createElement(
1331
1484
  TablePagination,
1332
1485
  {
1333
1486
  component: "div",
@@ -1351,8 +1504,8 @@ var LiteLLMPage_exports = {};
1351
1504
  __export(LiteLLMPage_exports, {
1352
1505
  LiteLLMPage: () => LiteLLMPage
1353
1506
  });
1354
- import React6, { useState as useState5, useCallback as useCallback2, useMemo as useMemo3 } from "react";
1355
- import { Box as Box6, Snackbar, Alert as Alert2, CircularProgress as CircularProgress4, Typography as Typography6, Paper as Paper6, Tabs as Tabs3, Tab as Tab3 } from "@mui/material";
1507
+ import React7, { useState as useState6, useCallback as useCallback2, useMemo as useMemo5 } from "react";
1508
+ import { Box as Box7, Snackbar, Alert as Alert3, CircularProgress as CircularProgress5, Typography as Typography7, Paper as Paper6, Tabs as Tabs3, Tab as Tab3 } from "@mui/material";
1356
1509
  import { useAsync as useAsync2, useAsyncRetry } from "react-use";
1357
1510
  import { useApi } from "@backstage/core-plugin-api";
1358
1511
  function initDateRange() {
@@ -1375,6 +1528,7 @@ var init_LiteLLMPage = __esm({
1375
1528
  "use strict";
1376
1529
  init_DashboardHeader();
1377
1530
  init_KeysTable();
1531
+ init_GenerateKeyDialog();
1378
1532
  init_UsageStats();
1379
1533
  init_TeamUsage();
1380
1534
  init_AuditLog();
@@ -1382,11 +1536,12 @@ var init_LiteLLMPage = __esm({
1382
1536
  PERIOD_LS_KEY2 = "litellm_usage_period";
1383
1537
  LiteLLMPage = () => {
1384
1538
  const api = useApi(liteLlmApiRef);
1385
- const [dateRange, setDateRange] = useState5(initDateRange);
1386
- const [activeTab, setActiveTab] = useState5("overview");
1387
- const [snackbar, setSnackbar] = useState5(null);
1388
- const [teamUsageCache, setTeamUsageCache] = useState5({});
1389
- const [teamUsageLoading, setTeamUsageLoading] = useState5({});
1539
+ const [dateRange, setDateRange] = useState6(initDateRange);
1540
+ const [activeTab, setActiveTab] = useState6("overview");
1541
+ const [snackbar, setSnackbar] = useState6(null);
1542
+ const [generateDialogOpen, setGenerateDialogOpen] = useState6(false);
1543
+ const [teamUsageCache, setTeamUsageCache] = useState6({});
1544
+ const [teamUsageLoading, setTeamUsageLoading] = useState6({});
1390
1545
  const { value: userInfo, loading: userLoading, error: userError } = useAsync2(
1391
1546
  () => api.getUserInfo(),
1392
1547
  [api]
@@ -1410,7 +1565,8 @@ var init_LiteLLMPage = __esm({
1410
1565
  () => api.getTeams().catch(() => []),
1411
1566
  [api]
1412
1567
  );
1413
- const teams = useMemo3(() => {
1568
+ const { value: liteLlmConfig } = useAsync2(() => api.getConfig(), [api]);
1569
+ const teams = useMemo5(() => {
1414
1570
  if (!allTeams?.length) return [];
1415
1571
  if (!userInfo) return allTeams;
1416
1572
  const userId = userInfo.user_id;
@@ -1446,7 +1602,7 @@ var init_LiteLLMPage = __esm({
1446
1602
  setTeamUsageLoading((prev) => ({ ...prev, [teamId]: false }));
1447
1603
  }
1448
1604
  }, [api, dateRange, teamUsageCache, teamUsageLoading]);
1449
- const allowedModels = useMemo3(() => {
1605
+ const allowedModels = useMemo5(() => {
1450
1606
  if (!allModels?.length) return [];
1451
1607
  const userModels = userInfo?.models;
1452
1608
  const teamModels = teams?.flatMap((t) => t.models ?? []);
@@ -1554,25 +1710,34 @@ var init_LiteLLMPage = __esm({
1554
1710
  );
1555
1711
  const isInitialLoading = userLoading && !userInfo;
1556
1712
  if (isInitialLoading) {
1557
- return /* @__PURE__ */ React6.createElement(Box6, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "50vh" }, /* @__PURE__ */ React6.createElement(CircularProgress4, null));
1713
+ return /* @__PURE__ */ React7.createElement(Box7, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "50vh" }, /* @__PURE__ */ React7.createElement(CircularProgress5, null));
1558
1714
  }
1559
1715
  if (userError || !userInfo) {
1560
1716
  const isProvisioningEnabled = userError?.body?.provisioning === true;
1561
1717
  const hint = userError?.body?.hint;
1562
- return /* @__PURE__ */ React6.createElement(Box6, { p: 3 }, /* @__PURE__ */ React6.createElement(Paper6, { sx: { p: 3 } }, /* @__PURE__ */ React6.createElement(Typography6, { variant: "h6", gutterBottom: true }, "Account not provisioned"), /* @__PURE__ */ React6.createElement(Typography6, { color: "text.secondary", paragraph: true }, "Your Backstage account is not linked to a LiteLLM user."), hint ? /* @__PURE__ */ React6.createElement(Typography6, { variant: "body2", color: "text.secondary" }, hint) : /* @__PURE__ */ React6.createElement(Typography6, { variant: "body2", color: "text.secondary" }, isProvisioningEnabled ? "Auto-provisioning is enabled but failed. Check the backend logs." : "Set litellm.provisioning.enabled: true in app-config.yaml to enable auto-provisioning, or ask your administrator to create the account manually.")));
1718
+ return /* @__PURE__ */ React7.createElement(Box7, { p: 3 }, /* @__PURE__ */ React7.createElement(Paper6, { sx: { p: 3 } }, /* @__PURE__ */ React7.createElement(Typography7, { variant: "h6", gutterBottom: true }, "Account not provisioned"), /* @__PURE__ */ React7.createElement(Typography7, { color: "text.secondary", paragraph: true }, "Your Backstage account is not linked to a LiteLLM user."), hint ? /* @__PURE__ */ React7.createElement(Typography7, { variant: "body2", color: "text.secondary" }, hint) : /* @__PURE__ */ React7.createElement(Typography7, { 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.")));
1563
1719
  }
1564
- return /* @__PURE__ */ React6.createElement(Box6, { p: 3 }, /* @__PURE__ */ React6.createElement(Box6, { mb: 2 }, /* @__PURE__ */ React6.createElement(DashboardHeader, { userInfo, teams: teams ?? [], loading: userLoading || teamsLoading })), /* @__PURE__ */ React6.createElement(
1720
+ return /* @__PURE__ */ React7.createElement(Box7, { p: 3 }, /* @__PURE__ */ React7.createElement(Box7, { mb: 2 }, /* @__PURE__ */ React7.createElement(
1721
+ DashboardHeader,
1722
+ {
1723
+ userInfo,
1724
+ teams: teams ?? [],
1725
+ keys: keys ?? [],
1726
+ loading: userLoading || teamsLoading,
1727
+ onGenerateKeyClick: () => setGenerateDialogOpen(true)
1728
+ }
1729
+ )), /* @__PURE__ */ React7.createElement(
1565
1730
  Tabs3,
1566
1731
  {
1567
1732
  value: activeTab,
1568
1733
  onChange: (_, v) => setActiveTab(v),
1569
1734
  sx: { mb: 2, borderBottom: 1, borderColor: "divider" }
1570
1735
  },
1571
- /* @__PURE__ */ React6.createElement(Tab3, { label: "Overview", value: "overview" }),
1572
- /* @__PURE__ */ React6.createElement(Tab3, { label: "Keys", value: "keys" }),
1573
- /* @__PURE__ */ React6.createElement(Tab3, { label: "Teams", value: "teams" }),
1574
- userInfo.can_view_audit && /* @__PURE__ */ React6.createElement(Tab3, { label: "Audit Log", value: "audit" })
1575
- ), activeTab === "overview" && /* @__PURE__ */ React6.createElement(
1736
+ /* @__PURE__ */ React7.createElement(Tab3, { label: "Overview", value: "overview" }),
1737
+ /* @__PURE__ */ React7.createElement(Tab3, { label: "Keys", value: "keys" }),
1738
+ /* @__PURE__ */ React7.createElement(Tab3, { label: "Teams", value: "teams" }),
1739
+ userInfo.can_view_audit && /* @__PURE__ */ React7.createElement(Tab3, { label: "Audit Log", value: "audit" })
1740
+ ), activeTab === "overview" && /* @__PURE__ */ React7.createElement(
1576
1741
  UsageStats,
1577
1742
  {
1578
1743
  usage: usage ?? null,
@@ -1582,23 +1747,21 @@ var init_LiteLLMPage = __esm({
1582
1747
  loading: usageLoading,
1583
1748
  userInfo
1584
1749
  }
1585
- ), activeTab === "keys" && /* @__PURE__ */ React6.createElement(
1750
+ ), activeTab === "keys" && /* @__PURE__ */ React7.createElement(
1586
1751
  KeysTable,
1587
1752
  {
1588
1753
  keys: keys ?? [],
1589
1754
  models: allowedModels,
1590
- teams: teams ?? [],
1591
1755
  loading: keysLoading || modelsLoading,
1592
- onGenerateKey: handleGenerateKey,
1756
+ onGenerateKeyClick: () => setGenerateDialogOpen(true),
1593
1757
  onUpdateKey: handleUpdateKey,
1594
1758
  onBlockKey: handleBlockKey,
1595
1759
  onUnblockKey: handleUnblockKey,
1596
1760
  onResetKeySpend: handleResetKeySpend,
1597
1761
  onDeleteKey: handleDeleteKey,
1598
- onPruneExpiredKeys: handlePruneExpiredKeys,
1599
- onGetConfig: () => api.getConfig()
1762
+ onPruneExpiredKeys: handlePruneExpiredKeys
1600
1763
  }
1601
- ), activeTab === "teams" && /* @__PURE__ */ React6.createElement(
1764
+ ), activeTab === "teams" && /* @__PURE__ */ React7.createElement(
1602
1765
  TeamUsage,
1603
1766
  {
1604
1767
  teams: teams ?? [],
@@ -1609,7 +1772,20 @@ var init_LiteLLMPage = __esm({
1609
1772
  },
1610
1773
  getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false
1611
1774
  }
1612
- ), activeTab === "audit" && userInfo.can_view_audit && /* @__PURE__ */ React6.createElement(AuditLog, { api }), /* @__PURE__ */ React6.createElement(
1775
+ ), activeTab === "audit" && userInfo.can_view_audit && /* @__PURE__ */ React7.createElement(AuditLog, { api }), /* @__PURE__ */ React7.createElement(
1776
+ GenerateKeyDialog,
1777
+ {
1778
+ open: generateDialogOpen,
1779
+ onClose: () => setGenerateDialogOpen(false),
1780
+ keys: keys ?? [],
1781
+ models: allowedModels,
1782
+ teams: teams ?? [],
1783
+ username: userInfo.user_id,
1784
+ keyGenerationSettings: liteLlmConfig?.keyGeneration,
1785
+ onGenerateKey: handleGenerateKey,
1786
+ onGetConfig: () => api.getConfig()
1787
+ }
1788
+ ), /* @__PURE__ */ React7.createElement(
1613
1789
  Snackbar,
1614
1790
  {
1615
1791
  open: !!snackbar,
@@ -1617,7 +1793,7 @@ var init_LiteLLMPage = __esm({
1617
1793
  onClose: () => setSnackbar(null),
1618
1794
  anchorOrigin: { vertical: "bottom", horizontal: "right" }
1619
1795
  },
1620
- snackbar ? /* @__PURE__ */ React6.createElement(Alert2, { severity: snackbar.severity, onClose: () => setSnackbar(null) }, snackbar.message) : void 0
1796
+ snackbar ? /* @__PURE__ */ React7.createElement(Alert3, { severity: snackbar.severity, onClose: () => setSnackbar(null) }, snackbar.message) : void 0
1621
1797
  ));
1622
1798
  };
1623
1799
  }
@@ -1625,7 +1801,7 @@ var init_LiteLLMPage = __esm({
1625
1801
 
1626
1802
  // src/plugin.tsx
1627
1803
  init_api();
1628
- import React7 from "react";
1804
+ import React8 from "react";
1629
1805
  import { TrendingUp as TrendingUpIcon } from "@mui/icons-material";
1630
1806
  import {
1631
1807
  createFrontendPlugin,
@@ -1644,10 +1820,10 @@ var liteLlmPage = PageBlueprint.make({
1644
1820
  params: {
1645
1821
  path: "/litellm",
1646
1822
  title: "LiteLLM",
1647
- icon: /* @__PURE__ */ React7.createElement(TrendingUpIcon, null),
1823
+ icon: /* @__PURE__ */ React8.createElement(TrendingUpIcon, null),
1648
1824
  loader: async () => {
1649
1825
  const { LiteLLMPage: LiteLLMPage2 } = await Promise.resolve().then(() => (init_LiteLLMPage(), LiteLLMPage_exports));
1650
- return /* @__PURE__ */ React7.createElement(LiteLLMPage2, null);
1826
+ return /* @__PURE__ */ React8.createElement(LiteLLMPage2, null);
1651
1827
  }
1652
1828
  }
1653
1829
  });
@@ -1666,17 +1842,17 @@ init_TeamUsage();
1666
1842
  // src/components/LiteLLMHomeWidget.tsx
1667
1843
  init_api();
1668
1844
  init_format();
1669
- import React8, { useState as useState6, useEffect } from "react";
1845
+ import React9, { useState as useState7, useEffect as useEffect2 } from "react";
1670
1846
  import {
1671
1847
  Paper as Paper7,
1672
- Box as Box7,
1673
- Typography as Typography7,
1848
+ Box as Box8,
1849
+ Typography as Typography8,
1674
1850
  FormControl as FormControl2,
1675
1851
  Select as Select2,
1676
1852
  MenuItem as MenuItem4,
1677
1853
  Grid as Grid2,
1678
- CircularProgress as CircularProgress5,
1679
- Alert as Alert3
1854
+ CircularProgress as CircularProgress6,
1855
+ Alert as Alert4
1680
1856
  } from "@mui/material";
1681
1857
  import { AreaChart as AreaChart3, Area as Area3, ResponsiveContainer as ResponsiveContainer3 } from "recharts";
1682
1858
  import { useApi as useApi2 } from "@backstage/core-plugin-api";
@@ -1692,19 +1868,19 @@ function presetToDateRange(preset) {
1692
1868
  }
1693
1869
  return { start, end };
1694
1870
  }
1695
- var Kpi = ({ label, value }) => /* @__PURE__ */ React8.createElement(Box7, null, /* @__PURE__ */ React8.createElement(Typography7, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ React8.createElement(Typography7, { variant: "subtitle1", fontWeight: 600 }, value));
1871
+ var Kpi = ({ label, value }) => /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(Typography8, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ React9.createElement(Typography8, { variant: "subtitle1", fontWeight: 600 }, value));
1696
1872
  var LiteLLMHomeWidget = ({
1697
1873
  defaultPeriod = "7d",
1698
1874
  title = "LiteLLM Usage"
1699
1875
  }) => {
1700
1876
  const api = useApi2(liteLlmApiRef);
1701
- const [period, setPeriod] = useState6(defaultPeriod);
1702
- const [loading, setLoading] = useState6(true);
1703
- const [usageError, setUsageError] = useState6(null);
1704
- const [keysError, setKeysError] = useState6(null);
1705
- const [usage, setUsage] = useState6(null);
1706
- const [keys, setKeys] = useState6([]);
1707
- useEffect(() => {
1877
+ const [period, setPeriod] = useState7(defaultPeriod);
1878
+ const [loading, setLoading] = useState7(true);
1879
+ const [usageError, setUsageError] = useState7(null);
1880
+ const [keysError, setKeysError] = useState7(null);
1881
+ const [usage, setUsage] = useState7(null);
1882
+ const [keys, setKeys] = useState7([]);
1883
+ useEffect2(() => {
1708
1884
  let cancelled = false;
1709
1885
  setLoading(true);
1710
1886
  setUsageError(null);
@@ -1741,17 +1917,17 @@ var LiteLLMHomeWidget = ({
1741
1917
  spend: d.spend
1742
1918
  }));
1743
1919
  const hasSparkline = dailyData.length > 0;
1744
- return /* @__PURE__ */ React8.createElement(Paper7, { sx: { p: 2 } }, /* @__PURE__ */ React8.createElement(Box7, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1.5 }, /* @__PURE__ */ React8.createElement(Typography7, { variant: "h6" }, title), /* @__PURE__ */ React8.createElement(FormControl2, { size: "small", sx: { minWidth: 90 } }, /* @__PURE__ */ React8.createElement(
1920
+ return /* @__PURE__ */ React9.createElement(Paper7, { sx: { p: 2 } }, /* @__PURE__ */ React9.createElement(Box8, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1.5 }, /* @__PURE__ */ React9.createElement(Typography8, { variant: "h6" }, title), /* @__PURE__ */ React9.createElement(FormControl2, { size: "small", sx: { minWidth: 90 } }, /* @__PURE__ */ React9.createElement(
1745
1921
  Select2,
1746
1922
  {
1747
1923
  value: period,
1748
1924
  onChange: (e) => setPeriod(e.target.value),
1749
1925
  displayEmpty: true
1750
1926
  },
1751
- /* @__PURE__ */ React8.createElement(MenuItem4, { value: "today" }, "Today"),
1752
- /* @__PURE__ */ React8.createElement(MenuItem4, { value: "7d" }, "7d"),
1753
- /* @__PURE__ */ React8.createElement(MenuItem4, { value: "30d" }, "30d")
1754
- ))), loading && /* @__PURE__ */ React8.createElement(Box7, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ React8.createElement(CircularProgress5, { size: 32 })), !loading && totalFailure && /* @__PURE__ */ React8.createElement(Alert3, { severity: "error", sx: { mt: 1 } }, usageError ?? "Failed to load usage data"), !loading && !totalFailure && /* @__PURE__ */ React8.createElement(React8.Fragment, null, partialFailure && /* @__PURE__ */ React8.createElement(Alert3, { severity: "warning", sx: { mt: 1, mb: 1 } }, usageError ? `Usage data unavailable (${usageError}).` : "", keysError ? ` Key list unavailable (${keysError}).` : "", " Showing what loaded."), /* @__PURE__ */ React8.createElement(Grid2, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "USD Spent", value: fmtUsd(usage?.total_spend ?? 0) })), /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "Tokens In", value: fmtInt(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "Tokens Out", value: fmtInt(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "Keys", value: fmtInt(keys.length) }))), hasSparkline && /* @__PURE__ */ React8.createElement(Box7, { height: 120 }, /* @__PURE__ */ React8.createElement(ResponsiveContainer3, { width: "100%", height: "100%" }, /* @__PURE__ */ React8.createElement(AreaChart3, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 } }, /* @__PURE__ */ React8.createElement(
1927
+ /* @__PURE__ */ React9.createElement(MenuItem4, { value: "today" }, "Today"),
1928
+ /* @__PURE__ */ React9.createElement(MenuItem4, { value: "7d" }, "7d"),
1929
+ /* @__PURE__ */ React9.createElement(MenuItem4, { value: "30d" }, "30d")
1930
+ ))), loading && /* @__PURE__ */ React9.createElement(Box8, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ React9.createElement(CircularProgress6, { size: 32 })), !loading && totalFailure && /* @__PURE__ */ React9.createElement(Alert4, { severity: "error", sx: { mt: 1 } }, usageError ?? "Failed to load usage data"), !loading && !totalFailure && /* @__PURE__ */ React9.createElement(React9.Fragment, null, partialFailure && /* @__PURE__ */ React9.createElement(Alert4, { severity: "warning", sx: { mt: 1, mb: 1 } }, usageError ? `Usage data unavailable (${usageError}).` : "", keysError ? ` Key list unavailable (${keysError}).` : "", " Showing what loaded."), /* @__PURE__ */ React9.createElement(Grid2, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ React9.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React9.createElement(Kpi, { label: "USD Spent", value: fmtUsd(usage?.total_spend ?? 0) })), /* @__PURE__ */ React9.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React9.createElement(Kpi, { label: "Tokens In", value: fmtInt(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ React9.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React9.createElement(Kpi, { label: "Tokens Out", value: fmtInt(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ React9.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React9.createElement(Kpi, { label: "Keys", value: fmtInt(keys.length) }))), hasSparkline && /* @__PURE__ */ React9.createElement(Box8, { height: 120 }, /* @__PURE__ */ React9.createElement(ResponsiveContainer3, { width: "100%", height: "100%" }, /* @__PURE__ */ React9.createElement(AreaChart3, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 } }, /* @__PURE__ */ React9.createElement(
1755
1931
  Area3,
1756
1932
  {
1757
1933
  type: "monotone",