@acarmisc/backstage-plugin-litellm 0.11.0 → 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,48 +276,15 @@ 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, generateDefaultAlias, 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)}`;
296
287
  };
297
- generateDefaultAlias = (username) => {
298
- const base = (username || "user").split("@")[0].toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "user";
299
- const hash = Math.random().toString(36).slice(2, 8);
300
- return `${base}-${hash}`;
301
- };
302
288
  shortKeyId = (token) => {
303
289
  if (!token) return "-";
304
290
  if (token.length <= 16) return token;
@@ -311,16 +297,6 @@ var init_KeysTable = __esm({
311
297
  return dateStr;
312
298
  }
313
299
  };
314
- emptyForm = () => ({
315
- alias: "",
316
- models: [],
317
- duration: "30d",
318
- max_budget: 100,
319
- tpm_limit: void 0,
320
- rpm_limit: void 0,
321
- team_id: void 0,
322
- key_type: "llm_api"
323
- });
324
300
  keyToEditForm = (k) => ({
325
301
  key_alias: k.key_alias ?? "",
326
302
  models: k.models ?? [],
@@ -331,26 +307,15 @@ var init_KeysTable = __esm({
331
307
  KeysTable = ({
332
308
  keys,
333
309
  models,
334
- teams,
335
- username,
336
- keyGenerationSettings,
337
310
  loading,
338
- onGenerateKey,
311
+ onGenerateKeyClick,
339
312
  onUpdateKey,
340
313
  onBlockKey,
341
314
  onUnblockKey,
342
315
  onResetKeySpend,
343
316
  onDeleteKey,
344
- onPruneExpiredKeys,
345
- onGetConfig
317
+ onPruneExpiredKeys
346
318
  }) => {
347
- const [generateModalOpen, setGenerateModalOpen] = useState(false);
348
- const [newKeyValue, setNewKeyValue] = useState(null);
349
- const [newKeySnippets, setNewKeySnippets] = useState(null);
350
- const [newKeyModel, setNewKeyModel] = useState("");
351
- const [formData, setFormData] = useState(emptyForm());
352
- const [unlimitedBudget, setUnlimitedBudget] = useState(false);
353
- const [submitting, setSubmitting] = useState(false);
354
319
  const [editingKey, setEditingKey] = useState(null);
355
320
  const [editForm, setEditForm] = useState({});
356
321
  const [editSubmitting, setEditSubmitting] = useState(false);
@@ -360,73 +325,14 @@ var init_KeysTable = __esm({
360
325
  const [resetSpendConfirm, setResetSpendConfirm] = useState(false);
361
326
  const [resetSpendSubmitting, setResetSpendSubmitting] = useState(false);
362
327
  const [filterText, setFilterText] = useState("");
363
- const filteredKeys = useMemo(() => {
328
+ const filteredKeys = useMemo2(() => {
364
329
  if (!filterText.trim()) return keys;
365
330
  const q = filterText.toLowerCase();
366
331
  return keys.filter(
367
332
  (k) => (k.key_alias ?? "").toLowerCase().includes(q) || k.models?.some((m) => m.toLowerCase().includes(q))
368
333
  );
369
334
  }, [keys, filterText]);
370
- const allowUnlimitedBudget = keyGenerationSettings?.allowUnlimitedBudget ?? false;
371
- const teamRequired = keyGenerationSettings?.teamRequired ?? true;
372
- const selectedTeam = teams.find((t) => t.team_id === formData.team_id) ?? null;
373
- const availableModels = useMemo(() => {
374
- const teamModels = selectedTeam?.models;
375
- if (teamModels && teamModels.length > 0) {
376
- const allowed = new Set(teamModels);
377
- return models.filter((m) => allowed.has(m.model_name));
378
- }
379
- return models;
380
- }, [models, selectedTeam]);
381
- const selectedModels = availableModels.filter((m) => (formData.models || []).includes(m.model_name));
382
335
  const editSelectedModels = models.filter((m) => (editForm.models || []).includes(m.model_name));
383
- const aliasError = !(formData.alias || "").trim();
384
- const teamError = teamRequired && !formData.team_id;
385
- const budgetInvalid = !unlimitedBudget && (formData.max_budget === void 0 || formData.max_budget === null || formData.max_budget <= 0);
386
- const canGenerate = !aliasError && !teamError && !budgetInvalid && !submitting;
387
- const budgetEstimate = useMemo(() => {
388
- if (unlimitedBudget || budgetInvalid) return null;
389
- const inputCosts = selectedModels.map((m) => m.input_cost_per_token).filter((c) => typeof c === "number" && c > 0);
390
- if (inputCosts.length === 0) return null;
391
- const pricePerToken = Math.max(...inputCosts);
392
- return estimateTokensFromBudget(formData.max_budget ?? 0, pricePerToken);
393
- }, [formData.max_budget, selectedModels, unlimitedBudget, budgetInvalid]);
394
- const handleGenerate = async () => {
395
- setSubmitting(true);
396
- try {
397
- const request = {
398
- ...formData,
399
- max_budget: unlimitedBudget ? null : formData.max_budget
400
- };
401
- const response = await onGenerateKey(request);
402
- setNewKeyValue(response.key);
403
- setNewKeyModel(formData.models?.[0] ?? "");
404
- setNewKeySnippets(null);
405
- try {
406
- const config = await onGetConfig();
407
- const model = formData.models?.[0] ?? "";
408
- setNewKeySnippets(buildSnippets(config.baseUrl, response.key, model));
409
- } catch {
410
- }
411
- setFormData(emptyForm());
412
- setUnlimitedBudget(false);
413
- } catch (error) {
414
- console.error("Failed to generate key:", error);
415
- } finally {
416
- setSubmitting(false);
417
- }
418
- };
419
- const handleOpenGenerate = () => {
420
- setFormData({ ...emptyForm(), alias: generateDefaultAlias(username) });
421
- setGenerateModalOpen(true);
422
- };
423
- const handleCloseModal = () => {
424
- setGenerateModalOpen(false);
425
- setNewKeyValue(null);
426
- setNewKeySnippets(null);
427
- setFormData(emptyForm());
428
- setUnlimitedBudget(false);
429
- };
430
336
  const handleOpenEdit = (k) => {
431
337
  setEditingKey(k);
432
338
  setEditForm(keyToEditForm(k));
@@ -478,117 +384,406 @@ var init_KeysTable = __esm({
478
384
  try {
479
385
  await onDeleteKey(deleteConfirmId);
480
386
  } finally {
481
- setDeleteSubmitting(false);
482
- setDeleteConfirmId(null);
387
+ setDeleteSubmitting(false);
388
+ setDeleteConfirmId(null);
389
+ }
390
+ };
391
+ const copyToClipboard = (text) => {
392
+ navigator.clipboard.writeText(text);
393
+ };
394
+ const [pruneConfirmCount, setPruneConfirmCount] = useState(null);
395
+ const [pruneSubmitting, setPruneSubmitting] = useState(false);
396
+ const expiredKeys = useMemo2(() => {
397
+ return keys.filter((k) => expiryStatus(k.expires_at) === "expired");
398
+ }, [keys]);
399
+ const handlePruneExpired = async () => {
400
+ if (pruneConfirmCount === null || pruneConfirmCount === 0) return;
401
+ setPruneSubmitting(true);
402
+ try {
403
+ await onPruneExpiredKeys();
404
+ } catch (error) {
405
+ console.error("Failed to prune expired keys:", error);
406
+ } finally {
407
+ setPruneSubmitting(false);
408
+ setPruneConfirmCount(null);
409
+ }
410
+ };
411
+ const modelOption = (m) => {
412
+ const inCost = fmtCost(m.input_cost_per_token);
413
+ const outCost = fmtCost(m.output_cost_per_token);
414
+ const ctx = formatContextWindow(m.max_input_tokens, m.max_output_tokens);
415
+ return /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement("span", null, m.model_name), m.supports_function_calling && " \u{1F527}", m.supports_vision && " \u{1F441}\uFE0F", ctx && /* @__PURE__ */ React2.createElement(Typography2, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, ctx), (inCost || outCost) && /* @__PURE__ */ React2.createElement(Typography2, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, inCost, " in \xB7 ", outCost, " out"));
416
+ };
417
+ return /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement(Paper2, { sx: { mb: 2 } }, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", justifyContent: "space-between", alignItems: "center", p: 2, gap: 2 }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "h6" }, "Virtual Keys"), /* @__PURE__ */ React2.createElement(Box2, { display: "flex", gap: 1, alignItems: "center", flex: 1, justifyContent: "flex-end" }, /* @__PURE__ */ React2.createElement(
418
+ TextField,
419
+ {
420
+ size: "small",
421
+ placeholder: "Filter by alias or model\u2026",
422
+ value: filterText,
423
+ onChange: (e) => setFilterText(e.target.value),
424
+ sx: { minWidth: 240 },
425
+ InputProps: {
426
+ startAdornment: /* @__PURE__ */ React2.createElement(InputAdornment, { position: "start" }, /* @__PURE__ */ React2.createElement(Search, { fontSize: "small" }))
427
+ }
428
+ }
429
+ ), expiredKeys.length > 0 && /* @__PURE__ */ React2.createElement(
430
+ Button2,
431
+ {
432
+ variant: "outlined",
433
+ color: "error",
434
+ startIcon: /* @__PURE__ */ React2.createElement(Autorenew, null),
435
+ onClick: () => setPruneConfirmCount(expiredKeys.length)
436
+ },
437
+ "Prune expired (",
438
+ expiredKeys.length,
439
+ ")"
440
+ ), /* @__PURE__ */ React2.createElement(
441
+ Button2,
442
+ {
443
+ variant: "contained",
444
+ color: "primary",
445
+ startIcon: /* @__PURE__ */ React2.createElement(Add2, null),
446
+ onClick: onGenerateKeyClick
447
+ },
448
+ "Generate New Key"
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(
450
+ Button2,
451
+ {
452
+ variant: "contained",
453
+ color: "primary",
454
+ size: "small",
455
+ startIcon: /* @__PURE__ */ React2.createElement(Add2, null),
456
+ onClick: onGenerateKeyClick
457
+ },
458
+ "Generate Your First Key"
459
+ )))) : filteredKeys.map((key) => {
460
+ const keyId = key.token ?? key.key;
461
+ const isBlocking = blockingKeyId === keyId;
462
+ return /* @__PURE__ */ React2.createElement(TableRow, { key: keyId, sx: key.blocked ? { bgcolor: "action.disabledBackground" } : void 0 }, /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", alignItems: "center", gap: 0.5 }, key.key_alias || "-", key.blocked && /* @__PURE__ */ React2.createElement(Chip2, { label: "Blocked", color: "error", size: "small" }))), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", alignItems: "center", gap: 0.5 }, /* @__PURE__ */ React2.createElement(
463
+ Typography2,
464
+ {
465
+ variant: "body2",
466
+ component: "code",
467
+ color: "text.secondary",
468
+ title: keyId,
469
+ sx: {
470
+ fontFamily: "monospace",
471
+ backgroundColor: "background.default",
472
+ px: 1,
473
+ py: 0.5,
474
+ borderRadius: 1
475
+ }
476
+ },
477
+ shortKeyId(keyId)
478
+ ), /* @__PURE__ */ React2.createElement(IconButton, { size: "small", onClick: () => copyToClipboard(keyId), title: "Copy Key ID" }, /* @__PURE__ */ React2.createElement(ContentCopy, { fontSize: "small" })))), /* @__PURE__ */ React2.createElement(TableCell, null, formatDate(key.created_at)), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(ExpiryChip, { expiresAt: key.expires_at })), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(BudgetCell, { spend: key.spend ?? 0, maxBudget: key.max_budget })), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2" }, key.tpm_limit ?? "-", " / ", key.rpm_limit ?? "-")), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", gap: 0.5, flexWrap: "wrap" }, key.models?.slice(0, 2).map((model) => /* @__PURE__ */ React2.createElement(Chip2, { key: model, label: model, size: "small" })), (key.models?.length || 0) > 2 && /* @__PURE__ */ React2.createElement(Chip2, { label: `+${(key.models?.length || 0) - 2}`, size: "small", variant: "outlined" }))), /* @__PURE__ */ React2.createElement(TableCell, { align: "right" }, /* @__PURE__ */ React2.createElement(IconButton, { onClick: () => handleOpenEdit(key), title: "Edit key" }, /* @__PURE__ */ React2.createElement(Edit, { fontSize: "small" })), /* @__PURE__ */ React2.createElement(
479
+ IconButton,
480
+ {
481
+ onClick: () => handleToggleBlock(key),
482
+ disabled: isBlocking,
483
+ color: key.blocked ? "warning" : "default",
484
+ title: key.blocked ? "Unblock key" : "Block key \u2014 suspends without revoking"
485
+ },
486
+ isBlocking ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 18 }) : key.blocked ? /* @__PURE__ */ React2.createElement(LockOpen, { fontSize: "small" }) : /* @__PURE__ */ React2.createElement(Lock, { fontSize: "small" })
487
+ ), /* @__PURE__ */ React2.createElement(
488
+ IconButton,
489
+ {
490
+ color: "error",
491
+ onClick: () => setDeleteConfirmId(keyId),
492
+ title: "Revoke key"
493
+ },
494
+ /* @__PURE__ */ React2.createElement(Delete, null)
495
+ )));
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);
483
766
  }
484
767
  };
768
+ const handleClose = () => {
769
+ onClose();
770
+ setGenerateError(null);
771
+ setNewKeyValue(null);
772
+ setNewKeySnippets(null);
773
+ setFormData(emptyForm());
774
+ setUnlimitedBudget(false);
775
+ };
485
776
  const copyToClipboard = (text) => {
486
777
  navigator.clipboard.writeText(text);
487
778
  };
488
- const [pruneConfirmCount, setPruneConfirmCount] = useState(null);
489
- const [pruneSubmitting, setPruneSubmitting] = useState(false);
490
- const expiredKeys = useMemo(() => {
491
- return keys.filter((k) => expiryStatus(k.expires_at) === "expired");
492
- }, [keys]);
493
- const handlePruneExpired = async () => {
494
- if (pruneConfirmCount === null || pruneConfirmCount === 0) return;
495
- setPruneSubmitting(true);
496
- try {
497
- await onPruneExpiredKeys();
498
- } catch (error) {
499
- console.error("Failed to prune expired keys:", error);
500
- } finally {
501
- setPruneSubmitting(false);
502
- setPruneConfirmCount(null);
503
- }
504
- };
505
779
  const modelOption = (m) => {
506
- const inCost = fmtCost(m.input_cost_per_token);
507
- const outCost = fmtCost(m.output_cost_per_token);
508
- const ctx = formatContextWindow(m.max_input_tokens, m.max_output_tokens);
509
- return /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement("span", null, m.model_name), m.supports_function_calling && " \u{1F527}", m.supports_vision && " \u{1F441}\uFE0F", ctx && /* @__PURE__ */ React2.createElement(Typography2, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, ctx), (inCost || outCost) && /* @__PURE__ */ React2.createElement(Typography2, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, inCost, " in \xB7 ", outCost, " out"));
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"));
510
784
  };
511
- return /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement(Paper2, { sx: { mb: 2 } }, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", justifyContent: "space-between", alignItems: "center", p: 2, gap: 2 }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "h6" }, "Virtual Keys"), /* @__PURE__ */ React2.createElement(Box2, { display: "flex", gap: 1, alignItems: "center", flex: 1, justifyContent: "flex-end" }, /* @__PURE__ */ React2.createElement(
512
- TextField,
513
- {
514
- size: "small",
515
- placeholder: "Filter by alias or model\u2026",
516
- value: filterText,
517
- onChange: (e) => setFilterText(e.target.value),
518
- sx: { minWidth: 240 },
519
- InputProps: {
520
- startAdornment: /* @__PURE__ */ React2.createElement(InputAdornment, { position: "start" }, /* @__PURE__ */ React2.createElement(Search, { fontSize: "small" }))
521
- }
522
- }
523
- ), expiredKeys.length > 0 && /* @__PURE__ */ React2.createElement(
524
- Button,
525
- {
526
- variant: "outlined",
527
- color: "error",
528
- startIcon: /* @__PURE__ */ React2.createElement(Autorenew, null),
529
- onClick: () => setPruneConfirmCount(expiredKeys.length)
530
- },
531
- "Prune expired (",
532
- expiredKeys.length,
533
- ")"
534
- ), /* @__PURE__ */ React2.createElement(
535
- Button,
536
- {
537
- variant: "contained",
538
- color: "primary",
539
- startIcon: /* @__PURE__ */ React2.createElement(Add, null),
540
- onClick: handleOpenGenerate
541
- },
542
- "Generate New Key"
543
- ))), /* @__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(
544
- Button,
545
- {
546
- variant: "contained",
547
- color: "primary",
548
- size: "small",
549
- startIcon: /* @__PURE__ */ React2.createElement(Add, null),
550
- onClick: handleOpenGenerate
551
- },
552
- "Generate Your First Key"
553
- )))) : filteredKeys.map((key) => {
554
- const keyId = key.token ?? key.key;
555
- const isBlocking = blockingKeyId === keyId;
556
- return /* @__PURE__ */ React2.createElement(TableRow, { key: keyId, sx: key.blocked ? { bgcolor: "action.disabledBackground" } : void 0 }, /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", alignItems: "center", gap: 0.5 }, key.key_alias || "-", key.blocked && /* @__PURE__ */ React2.createElement(Chip2, { label: "Blocked", color: "error", size: "small" }))), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", alignItems: "center", gap: 0.5 }, /* @__PURE__ */ React2.createElement(
557
- Typography2,
558
- {
559
- variant: "body2",
560
- component: "code",
561
- color: "text.secondary",
562
- title: keyId,
563
- sx: {
564
- fontFamily: "monospace",
565
- backgroundColor: "background.default",
566
- px: 1,
567
- py: 0.5,
568
- borderRadius: 1
569
- }
570
- },
571
- shortKeyId(keyId)
572
- ), /* @__PURE__ */ React2.createElement(IconButton, { size: "small", onClick: () => copyToClipboard(keyId), title: "Copy Key ID" }, /* @__PURE__ */ React2.createElement(ContentCopy, { fontSize: "small" })))), /* @__PURE__ */ React2.createElement(TableCell, null, formatDate(key.created_at)), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(ExpiryChip, { expiresAt: key.expires_at })), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(BudgetCell, { spend: key.spend ?? 0, maxBudget: key.max_budget })), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2" }, key.tpm_limit ?? "-", " / ", key.rpm_limit ?? "-")), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", gap: 0.5, flexWrap: "wrap" }, key.models?.slice(0, 2).map((model) => /* @__PURE__ */ React2.createElement(Chip2, { key: model, label: model, size: "small" })), (key.models?.length || 0) > 2 && /* @__PURE__ */ React2.createElement(Chip2, { label: `+${(key.models?.length || 0) - 2}`, size: "small", variant: "outlined" }))), /* @__PURE__ */ React2.createElement(TableCell, { align: "right" }, /* @__PURE__ */ React2.createElement(IconButton, { onClick: () => handleOpenEdit(key), title: "Edit key" }, /* @__PURE__ */ React2.createElement(Edit, { fontSize: "small" })), /* @__PURE__ */ React2.createElement(
573
- IconButton,
574
- {
575
- onClick: () => handleToggleBlock(key),
576
- disabled: isBlocking,
577
- color: key.blocked ? "warning" : "default",
578
- title: key.blocked ? "Unblock key" : "Block key \u2014 suspends without revoking"
579
- },
580
- isBlocking ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 18 }) : key.blocked ? /* @__PURE__ */ React2.createElement(LockOpen, { fontSize: "small" }) : /* @__PURE__ */ React2.createElement(Lock, { fontSize: "small" })
581
- ), /* @__PURE__ */ React2.createElement(
582
- IconButton,
583
- {
584
- color: "error",
585
- onClick: () => setDeleteConfirmId(keyId),
586
- title: "Revoke key"
587
- },
588
- /* @__PURE__ */ React2.createElement(Delete, null)
589
- )));
590
- }))))), /* @__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(
591
- Box2,
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,
592
787
  {
593
788
  display: "flex",
594
789
  alignItems: "center",
@@ -602,8 +797,8 @@ var init_KeysTable = __esm({
602
797
  borderRadius: 1
603
798
  }
604
799
  },
605
- /* @__PURE__ */ React2.createElement(
606
- Typography2,
800
+ /* @__PURE__ */ React3.createElement(
801
+ Typography3,
607
802
  {
608
803
  component: "code",
609
804
  color: "text.primary",
@@ -611,20 +806,24 @@ var init_KeysTable = __esm({
611
806
  },
612
807
  newKeyValue
613
808
  ),
614
- /* @__PURE__ */ React2.createElement(IconButton, { onClick: () => copyToClipboard(newKeyValue) }, /* @__PURE__ */ React2.createElement(ContentCopy, null))
615
- ), 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(
616
- 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,
617
812
  {
618
813
  label: "Alias",
619
814
  value: formData.alias || "",
620
- onChange: (e) => setFormData({ ...formData, alias: e.target.value }),
815
+ onChange: (e) => {
816
+ setFormData({ ...formData, alias: e.target.value });
817
+ setGenerateError(null);
818
+ },
621
819
  error: aliasError,
622
- 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,
623
822
  required: true,
624
823
  fullWidth: true
625
824
  }
626
- ), /* @__PURE__ */ React2.createElement(
627
- TextField,
825
+ ), /* @__PURE__ */ React3.createElement(
826
+ TextField2,
628
827
  {
629
828
  select: true,
630
829
  label: "Duration",
@@ -632,13 +831,13 @@ var init_KeysTable = __esm({
632
831
  onChange: (e) => setFormData({ ...formData, duration: e.target.value }),
633
832
  fullWidth: true
634
833
  },
635
- /* @__PURE__ */ React2.createElement(MenuItem, { value: "1d" }, "1 Day"),
636
- /* @__PURE__ */ React2.createElement(MenuItem, { value: "7d" }, "7 Days"),
637
- /* @__PURE__ */ React2.createElement(MenuItem, { value: "30d" }, "30 Days"),
638
- /* @__PURE__ */ React2.createElement(MenuItem, { value: "90d" }, "90 Days"),
639
- /* @__PURE__ */ React2.createElement(MenuItem, { value: "1y" }, "1 Year")
640
- ), teams.length > 0 ? /* @__PURE__ */ React2.createElement(
641
- 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,
642
841
  {
643
842
  options: teams,
644
843
  getOptionLabel: (t) => t.team_alias || t.team_id,
@@ -648,8 +847,8 @@ var init_KeysTable = __esm({
648
847
  const restrictedModels = teamModels && teamModels.length > 0 ? (formData.models || []).filter((m) => teamModels.includes(m)) : formData.models;
649
848
  setFormData({ ...formData, team_id: team?.team_id, models: restrictedModels });
650
849
  },
651
- renderInput: (params) => /* @__PURE__ */ React2.createElement(
652
- TextField,
850
+ renderInput: (params) => /* @__PURE__ */ React3.createElement(
851
+ TextField2,
653
852
  {
654
853
  ...params,
655
854
  label: "Team",
@@ -660,8 +859,8 @@ var init_KeysTable = __esm({
660
859
  }
661
860
  )
662
861
  }
663
- ) : teamRequired ? /* @__PURE__ */ React2.createElement(Typography2, { 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__ */ React2.createElement(
664
- 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,
665
864
  {
666
865
  multiple: true,
667
866
  options: availableModels,
@@ -669,9 +868,9 @@ var init_KeysTable = __esm({
669
868
  getOptionLabel: (m) => m.model_name,
670
869
  value: selectedModels,
671
870
  onChange: (_e, selected) => setFormData({ ...formData, models: selected.map((m) => m.model_name) }),
672
- renderOption: (props, m) => /* @__PURE__ */ React2.createElement("li", { ...props }, modelOption(m)),
673
- renderInput: (params) => /* @__PURE__ */ React2.createElement(
674
- TextField,
871
+ renderOption: (props, m) => /* @__PURE__ */ React3.createElement("li", { ...props }, modelOption(m)),
872
+ renderInput: (params) => /* @__PURE__ */ React3.createElement(
873
+ TextField2,
675
874
  {
676
875
  ...params,
677
876
  label: "Models",
@@ -680,10 +879,10 @@ var init_KeysTable = __esm({
680
879
  }
681
880
  )
682
881
  }
683
- ), allowUnlimitedBudget && /* @__PURE__ */ React2.createElement(
882
+ ), allowUnlimitedBudget && /* @__PURE__ */ React3.createElement(
684
883
  FormControlLabel,
685
884
  {
686
- control: /* @__PURE__ */ React2.createElement(
885
+ control: /* @__PURE__ */ React3.createElement(
687
886
  Checkbox,
688
887
  {
689
888
  checked: unlimitedBudget,
@@ -699,8 +898,8 @@ var init_KeysTable = __esm({
699
898
  ),
700
899
  label: "Unlimited budget"
701
900
  }
702
- ), /* @__PURE__ */ React2.createElement(
703
- TextField,
901
+ ), /* @__PURE__ */ React3.createElement(
902
+ TextField2,
704
903
  {
705
904
  label: "Max Budget (USD)",
706
905
  type: "number",
@@ -712,8 +911,8 @@ var init_KeysTable = __esm({
712
911
  required: true,
713
912
  fullWidth: true
714
913
  }
715
- ), /* @__PURE__ */ React2.createElement(
716
- TextField,
914
+ ), /* @__PURE__ */ React3.createElement(
915
+ TextField2,
717
916
  {
718
917
  label: "TPM Limit",
719
918
  type: "number",
@@ -722,8 +921,8 @@ var init_KeysTable = __esm({
722
921
  helperText: "Max tokens per minute this key can consume across all models. Leave blank for no limit.",
723
922
  fullWidth: true
724
923
  }
725
- ), /* @__PURE__ */ React2.createElement(
726
- TextField,
924
+ ), /* @__PURE__ */ React3.createElement(
925
+ TextField2,
727
926
  {
728
927
  label: "RPM Limit",
729
928
  type: "number",
@@ -732,125 +931,39 @@ var init_KeysTable = __esm({
732
931
  helperText: "Max requests per minute this key can make across all models. Leave blank for no limit.",
733
932
  fullWidth: true
734
933
  }
735
- ))), /* @__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(
736
- 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,
737
936
  {
738
937
  onClick: handleGenerate,
739
938
  variant: "contained",
740
939
  color: "primary",
741
940
  disabled: !canGenerate
742
941
  },
743
- submitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 24 }) : "Generate"
744
- )))), /* @__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(
745
- TextField,
746
- {
747
- label: "Alias",
748
- value: editForm.key_alias || "",
749
- onChange: (e) => setEditForm({ ...editForm, key_alias: e.target.value }),
750
- fullWidth: true
751
- }
752
- ), models.length > 0 && /* @__PURE__ */ React2.createElement(
753
- Autocomplete,
754
- {
755
- multiple: true,
756
- options: models,
757
- groupBy: (m) => m.mode || "other",
758
- getOptionLabel: (m) => m.model_name,
759
- value: editSelectedModels,
760
- onChange: (_e, selected) => setEditForm({ ...editForm, models: selected.map((m) => m.model_name) }),
761
- renderOption: (props, m) => /* @__PURE__ */ React2.createElement("li", { ...props }, modelOption(m)),
762
- renderInput: (params) => /* @__PURE__ */ React2.createElement(TextField, { ...params, label: "Models", fullWidth: true })
763
- }
764
- ), /* @__PURE__ */ React2.createElement(
765
- TextField,
766
- {
767
- label: "Max Budget (USD)",
768
- type: "number",
769
- value: editForm.max_budget ?? "",
770
- onChange: (e) => setEditForm({ ...editForm, max_budget: e.target.value ? Number(e.target.value) : void 0 }),
771
- fullWidth: true
772
- }
773
- ), /* @__PURE__ */ React2.createElement(
774
- TextField,
775
- {
776
- label: "TPM Limit",
777
- type: "number",
778
- value: editForm.tpm_limit ?? "",
779
- onChange: (e) => setEditForm({ ...editForm, tpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
780
- helperText: "Max tokens per minute this key can consume across all models. Leave blank for no limit.",
781
- fullWidth: true
782
- }
783
- ), /* @__PURE__ */ React2.createElement(
784
- TextField,
785
- {
786
- label: "RPM Limit",
787
- type: "number",
788
- value: editForm.rpm_limit ?? "",
789
- onChange: (e) => setEditForm({ ...editForm, rpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
790
- helperText: "Max requests per minute this key can make across all models. Leave blank for no limit.",
791
- fullWidth: true
792
- }
793
- ), /* @__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(
794
- Button,
795
- {
796
- size: "small",
797
- color: "warning",
798
- variant: "outlined",
799
- onClick: () => setResetSpendConfirm(true)
800
- },
801
- "Reset Spend to $0"
802
- ) : /* @__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(
803
- Button,
804
- {
805
- size: "small",
806
- color: "warning",
807
- variant: "contained",
808
- disabled: resetSpendSubmitting,
809
- onClick: handleResetSpend
810
- },
811
- resetSpendSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 16 }) : "Confirm"
812
- ), /* @__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(
813
- Button,
814
- {
815
- onClick: handleConfirmDelete,
816
- variant: "contained",
817
- color: "error",
818
- disabled: deleteSubmitting
819
- },
820
- deleteSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 20 }) : "Revoke"
821
- ))), /* @__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(
822
- Button,
823
- {
824
- onClick: handlePruneExpired,
825
- variant: "contained",
826
- color: "error",
827
- disabled: pruneSubmitting
828
- },
829
- pruneSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 20 }) : "Prune"
942
+ submitting ? /* @__PURE__ */ React3.createElement(CircularProgress2, { size: 24 }) : "Generate"
830
943
  ))));
831
944
  };
832
945
  SnippetTabs = ({ snippets, model, onCopy }) => {
833
- const [tab, setTab] = useState("curl");
946
+ const [tab, setTab] = useState2("curl");
834
947
  const code = tab === "curl" ? snippets.curl : snippets.openai;
835
- 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(
836
- 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,
837
950
  {
838
951
  position: "relative",
839
952
  p: 1.5,
840
953
  sx: { backgroundColor: "action.hover", border: "1px solid", borderColor: "divider", borderRadius: 1 }
841
954
  },
842
- /* @__PURE__ */ React2.createElement(
843
- IconButton,
955
+ /* @__PURE__ */ React3.createElement(
956
+ IconButton2,
844
957
  {
845
958
  size: "small",
846
959
  onClick: () => onCopy(code),
847
960
  title: "Copy snippet",
848
961
  sx: { position: "absolute", top: 4, right: 4 }
849
962
  },
850
- /* @__PURE__ */ React2.createElement(ContentCopy, { fontSize: "small" })
963
+ /* @__PURE__ */ React3.createElement(ContentCopy2, { fontSize: "small" })
851
964
  ),
852
- /* @__PURE__ */ React2.createElement(
853
- Typography2,
965
+ /* @__PURE__ */ React3.createElement(
966
+ Typography3,
854
967
  {
855
968
  component: "pre",
856
969
  sx: {
@@ -864,18 +977,18 @@ var init_KeysTable = __esm({
864
977
  },
865
978
  code
866
979
  ),
867
- 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.")
868
981
  ));
869
982
  };
870
983
  }
871
984
  });
872
985
 
873
986
  // src/components/UsageStats.tsx
874
- import React3, { useMemo as useMemo2, useState as useState2 } from "react";
987
+ import React4, { useMemo as useMemo4, useState as useState3 } from "react";
875
988
  import {
876
989
  Paper as Paper3,
877
- Box as Box3,
878
- Typography as Typography3,
990
+ Box as Box4,
991
+ Typography as Typography4,
879
992
  FormControl,
880
993
  InputLabel,
881
994
  Select,
@@ -934,12 +1047,12 @@ var init_UsageStats = __esm({
934
1047
  "#d0ed57"
935
1048
  ];
936
1049
  fmtPct = (n) => `${(n * 100).toFixed(1)}%`;
937
- 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);
938
- 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 } });
939
1052
  EmptyChart = ({
940
1053
  height = 260,
941
1054
  message = "No data for this period"
942
- }) => /* @__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));
943
1056
  UsageStats = ({
944
1057
  usage,
945
1058
  models,
@@ -949,14 +1062,14 @@ var init_UsageStats = __esm({
949
1062
  userInfo
950
1063
  }) => {
951
1064
  const theme = useTheme();
952
- const [selectedModel, setSelectedModel] = useState2("all");
953
- const [tab, setTab] = useState2("costs");
1065
+ const [selectedModel, setSelectedModel] = useState3("all");
1066
+ const [tab, setTab] = useState3("costs");
954
1067
  const gridStroke = theme.palette.divider;
955
1068
  const tickFill = theme.palette.text.secondary;
956
1069
  const tickStyle = { fontSize: 12, fill: tickFill };
957
1070
  const tickStyleSmall = { fontSize: 11, fill: tickFill };
958
1071
  const tickStyleTiny = { fontSize: 10, fill: tickFill };
959
- const selectedPreset = useMemo2(() => {
1072
+ const selectedPreset = useMemo4(() => {
960
1073
  if (dateRange.start.toDateString() === dateRange.end.toDateString()) return "today";
961
1074
  const diffMs = dateRange.end.getTime() - dateRange.start.getTime();
962
1075
  const diffHours = diffMs / (1e3 * 60 * 60);
@@ -978,7 +1091,7 @@ var init_UsageStats = __esm({
978
1091
  }
979
1092
  onDateRangeChange({ start, end });
980
1093
  };
981
- const dailyData = useMemo2(
1094
+ const dailyData = useMemo4(
982
1095
  () => (usage?.daily_usage ?? []).map((d) => ({
983
1096
  date: d.date,
984
1097
  spend: d.spend,
@@ -991,21 +1104,21 @@ var init_UsageStats = __esm({
991
1104
  })),
992
1105
  [usage]
993
1106
  );
994
- const cumulativeData = useMemo2(() => {
1107
+ const cumulativeData = useMemo4(() => {
995
1108
  let cum = 0;
996
1109
  return dailyData.map((d) => {
997
1110
  cum += d.spend;
998
1111
  return { date: d.date, cumulative: cum };
999
1112
  });
1000
1113
  }, [dailyData]);
1001
- const successRateData = useMemo2(
1114
+ const successRateData = useMemo4(
1002
1115
  () => dailyData.filter((d) => d.apiRequests > 0).map((d) => ({
1003
1116
  date: d.date,
1004
1117
  successRate: parseFloat((d.successfulRequests / d.apiRequests * 100).toFixed(1))
1005
1118
  })),
1006
1119
  [dailyData]
1007
1120
  );
1008
- const { modelSpendByDate, topModels: topSpendModels } = useMemo2(() => {
1121
+ const { modelSpendByDate, topModels: topSpendModels } = useMemo4(() => {
1009
1122
  const rows = usage?.daily_by_model ?? [];
1010
1123
  const modelTotals = {};
1011
1124
  for (const r of rows) modelTotals[r.model] = (modelTotals[r.model] ?? 0) + r.spend;
@@ -1020,7 +1133,7 @@ var init_UsageStats = __esm({
1020
1133
  const hasOther = sorted.some((r) => r.Other > 0);
1021
1134
  return { modelSpendByDate: sorted, topModels: hasOther ? [...top, "Other"] : top };
1022
1135
  }, [usage]);
1023
- const modelRows = useMemo2(() => {
1136
+ const modelRows = useMemo4(() => {
1024
1137
  const entries = Object.entries(usage?.usage_by_model ?? {}).map(([model, d]) => ({
1025
1138
  model,
1026
1139
  spend: d.total_spend,
@@ -1034,11 +1147,11 @@ var init_UsageStats = __esm({
1034
1147
  }));
1035
1148
  return selectedModel === "all" ? entries : entries.filter((e) => e.model === selectedModel);
1036
1149
  }, [usage, selectedModel]);
1037
- const topModelSpendBars = useMemo2(
1150
+ const topModelSpendBars = useMemo4(
1038
1151
  () => [...modelRows].sort((a, b) => b.spend - a.spend).slice(0, 10),
1039
1152
  [modelRows]
1040
1153
  );
1041
- const keyRows = useMemo2(
1154
+ const keyRows = useMemo4(
1042
1155
  () => Object.entries(usage?.usage_by_key ?? {}).map(([keyHash, d]) => ({
1043
1156
  keyHash,
1044
1157
  keyAlias: d.key_alias ?? keyHash.slice(0, 8),
@@ -1055,7 +1168,7 @@ var init_UsageStats = __esm({
1055
1168
  })),
1056
1169
  [usage]
1057
1170
  );
1058
- const topKeySpendBars = useMemo2(
1171
+ const topKeySpendBars = useMemo4(
1059
1172
  () => [...keyRows].sort((a, b) => b.spend - a.spend).slice(0, 10),
1060
1173
  [keyRows]
1061
1174
  );
@@ -1063,34 +1176,34 @@ var init_UsageStats = __esm({
1063
1176
  const overallSuccessRate = totalRequests > 0 ? (usage?.successful_requests ?? 0) / totalRequests : 0;
1064
1177
  const maxBudget = userInfo?.max_budget ?? 0;
1065
1178
  const totalCumSpend = cumulativeData[cumulativeData.length - 1]?.cumulative ?? 0;
1066
- 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(
1067
1180
  Select,
1068
1181
  {
1069
1182
  value: selectedPreset,
1070
1183
  label: "Period",
1071
1184
  onChange: (e) => handlePresetChange(e.target.value)
1072
1185
  },
1073
- /* @__PURE__ */ React3.createElement(MenuItem2, { value: "today" }, "Today"),
1074
- /* @__PURE__ */ React3.createElement(MenuItem2, { value: "24h" }, "Last 24h"),
1075
- /* @__PURE__ */ React3.createElement(MenuItem2, { value: "7d" }, "Last 7 days"),
1076
- /* @__PURE__ */ React3.createElement(MenuItem2, { value: "30d" }, "Last 30 days")
1077
- )), 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(
1078
1191
  Select,
1079
1192
  {
1080
1193
  value: selectedModel,
1081
1194
  label: "Model",
1082
1195
  onChange: (e) => setSelectedModel(e.target.value)
1083
1196
  },
1084
- /* @__PURE__ */ React3.createElement(MenuItem2, { value: "all" }, "All Models"),
1085
- models.map((m) => /* @__PURE__ */ React3.createElement(MenuItem2, { key: m.model_name, value: m.model_name }, m.model_name))
1086
- )))), /* @__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(
1087
1200
  KpiCard,
1088
1201
  {
1089
1202
  label: "Total Tokens",
1090
1203
  value: fmtInt(usage?.total_tokens ?? 0),
1091
1204
  hint: `${fmtInt(usage?.prompt_tokens ?? 0)} in \xB7 ${fmtInt(usage?.completion_tokens ?? 0)} out`
1092
1205
  }
1093
- ))), /* @__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(
1094
1207
  Area,
1095
1208
  {
1096
1209
  key: m,
@@ -1101,35 +1214,35 @@ var init_UsageStats = __esm({
1101
1214
  fill: modelColor(m),
1102
1215
  fillOpacity: 0.6
1103
1216
  }
1104
- )))))), /* @__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(
1105
1218
  LinearProgress3,
1106
1219
  {
1107
1220
  variant: "determinate",
1108
1221
  value: r.successRate * 100,
1109
1222
  sx: { flex: 1, height: 6, borderRadius: 3 }
1110
1223
  }
1111
- ), /* @__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(
1112
1225
  LinearProgress3,
1113
1226
  {
1114
1227
  variant: "determinate",
1115
1228
  value: r.successRate * 100,
1116
1229
  sx: { flex: 1, height: 6, borderRadius: 3 }
1117
1230
  }
1118
- ), /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption" }, fmtPct(r.successRate))) : "\u2014")))))))));
1231
+ ), /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption" }, fmtPct(r.successRate))) : "\u2014")))))))));
1119
1232
  };
1120
1233
  }
1121
1234
  });
1122
1235
 
1123
1236
  // src/components/TeamUsage.tsx
1124
- import React4, { useState as useState3 } from "react";
1237
+ import React5, { useState as useState4 } from "react";
1125
1238
  import {
1126
1239
  Paper as Paper4,
1127
- Box as Box4,
1128
- Typography as Typography4,
1240
+ Box as Box5,
1241
+ Typography as Typography5,
1129
1242
  LinearProgress as LinearProgress4,
1130
1243
  Chip as Chip4,
1131
1244
  Divider,
1132
- Stack,
1245
+ Stack as Stack2,
1133
1246
  Table as Table3,
1134
1247
  TableBody as TableBody3,
1135
1248
  TableCell as TableCell3,
@@ -1137,8 +1250,8 @@ import {
1137
1250
  TableRow as TableRow3,
1138
1251
  TableContainer as TableContainer3,
1139
1252
  Collapse,
1140
- IconButton as IconButton2,
1141
- CircularProgress as CircularProgress2,
1253
+ IconButton as IconButton3,
1254
+ CircularProgress as CircularProgress3,
1142
1255
  useTheme as useTheme2
1143
1256
  } from "@mui/material";
1144
1257
  import { ExpandMore, ExpandLess, Group, Speed, Memory } from "@mui/icons-material";
@@ -1155,9 +1268,9 @@ var Stat, TeamCard, TeamUsage;
1155
1268
  var init_TeamUsage = __esm({
1156
1269
  "src/components/TeamUsage.tsx"() {
1157
1270
  "use strict";
1158
- 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));
1159
1272
  TeamCard = ({ team, usage, usageLoading }) => {
1160
- const [expanded, setExpanded] = useState3(false);
1273
+ const [expanded, setExpanded] = useState4(false);
1161
1274
  const theme = useTheme2();
1162
1275
  const gridStroke = theme.palette.divider;
1163
1276
  const tickFill = theme.palette.text.secondary;
@@ -1167,43 +1280,43 @@ var init_TeamUsage = __esm({
1167
1280
  const isOver = budget > 0 && spend >= budget;
1168
1281
  const isNear = budget > 0 && spend >= budget * 0.8 && !isOver;
1169
1282
  const dailyData = usage?.daily_usage?.map((d) => ({ date: d.date, spend: d.spend })) ?? [];
1170
- 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(
1171
1284
  Stat,
1172
1285
  {
1173
1286
  label: "Members",
1174
1287
  value: team.members_with_roles?.length ? String(team.members_with_roles.length) : "\u2014"
1175
1288
  }
1176
- ), /* @__PURE__ */ React4.createElement(
1289
+ ), /* @__PURE__ */ React5.createElement(
1177
1290
  Stat,
1178
1291
  {
1179
1292
  label: "Models",
1180
1293
  value: team.models?.length ? String(team.models.length) : "All"
1181
1294
  }
1182
- ), /* @__PURE__ */ React4.createElement(
1295
+ ), /* @__PURE__ */ React5.createElement(
1183
1296
  Stat,
1184
1297
  {
1185
1298
  label: "Budget",
1186
1299
  value: budget > 0 ? `$${budget.toFixed(2)}` : "Unlimited"
1187
1300
  }
1188
- ), /* @__PURE__ */ React4.createElement(
1301
+ ), /* @__PURE__ */ React5.createElement(
1189
1302
  Stat,
1190
1303
  {
1191
1304
  label: "Spend",
1192
1305
  value: `$${spend.toFixed(2)}`
1193
1306
  }
1194
- ), /* @__PURE__ */ React4.createElement(
1307
+ ), /* @__PURE__ */ React5.createElement(
1195
1308
  Stat,
1196
1309
  {
1197
1310
  label: "TPM",
1198
1311
  value: team.tpm_limit != null && team.tpm_limit > 0 ? String(team.tpm_limit) : "\u2014"
1199
1312
  }
1200
- ), /* @__PURE__ */ React4.createElement(
1313
+ ), /* @__PURE__ */ React5.createElement(
1201
1314
  Stat,
1202
1315
  {
1203
1316
  label: "RPM",
1204
1317
  value: team.rpm_limit != null && team.rpm_limit > 0 ? String(team.rpm_limit) : "\u2014"
1205
1318
  }
1206
- )), 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(
1207
1320
  LinearProgress4,
1208
1321
  {
1209
1322
  variant: "determinate",
@@ -1211,22 +1324,22 @@ var init_TeamUsage = __esm({
1211
1324
  color: isOver ? "error" : isNear ? "warning" : "primary",
1212
1325
  sx: { height: 6, borderRadius: 1 }
1213
1326
  }
1214
- )), /* @__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(
1215
- 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,
1216
1329
  {
1217
1330
  variant: "caption",
1218
1331
  color: "text.secondary",
1219
1332
  sx: { fontFamily: "monospace" }
1220
1333
  },
1221
1334
  m.user_id
1222
- )) : /* @__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(
1223
1336
  Chip4,
1224
1337
  {
1225
1338
  label: m.role,
1226
1339
  size: "small",
1227
1340
  color: m.role === "admin" ? "primary" : "default"
1228
1341
  }
1229
- ))))))) : /* @__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."))));
1230
1343
  };
1231
1344
  TeamUsage = ({
1232
1345
  teams,
@@ -1235,12 +1348,12 @@ var init_TeamUsage = __esm({
1235
1348
  getTeamUsageLoading
1236
1349
  }) => {
1237
1350
  if (loading) {
1238
- 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));
1239
1352
  }
1240
1353
  if (!teams.length) {
1241
- 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."))));
1242
1355
  }
1243
- 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(
1244
1357
  TeamCard,
1245
1358
  {
1246
1359
  key: team.team_id,
@@ -1254,11 +1367,11 @@ var init_TeamUsage = __esm({
1254
1367
  });
1255
1368
 
1256
1369
  // src/components/AuditLog.tsx
1257
- import React5, { useState as useState4, useCallback } from "react";
1370
+ import React6, { useState as useState5, useCallback } from "react";
1258
1371
  import {
1259
1372
  Paper as Paper5,
1260
- Box as Box5,
1261
- Typography as Typography5,
1373
+ Box as Box6,
1374
+ Typography as Typography6,
1262
1375
  Table as Table4,
1263
1376
  TableBody as TableBody4,
1264
1377
  TableCell as TableCell4,
@@ -1266,13 +1379,13 @@ import {
1266
1379
  TableHead as TableHead4,
1267
1380
  TableRow as TableRow4,
1268
1381
  TablePagination,
1269
- TextField as TextField2,
1382
+ TextField as TextField3,
1270
1383
  MenuItem as MenuItem3,
1271
1384
  Chip as Chip5,
1272
- CircularProgress as CircularProgress3,
1385
+ CircularProgress as CircularProgress4,
1273
1386
  Collapse as Collapse2,
1274
- IconButton as IconButton3,
1275
- Alert
1387
+ IconButton as IconButton4,
1388
+ Alert as Alert2
1276
1389
  } from "@mui/material";
1277
1390
  import { KeyboardArrowDown, KeyboardArrowUp } from "@mui/icons-material";
1278
1391
  import { useAsync } from "react-use";
@@ -1302,14 +1415,14 @@ var init_AuditLog = __esm({
1302
1415
  unblocked: "success"
1303
1416
  };
1304
1417
  DetailRow = ({ entry }) => {
1305
- const [open, setOpen] = useState4(false);
1418
+ const [open, setOpen] = useState5(false);
1306
1419
  const hasDetail = entry.before_value || entry.updated_values;
1307
- 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))))))));
1308
1421
  };
1309
1422
  AuditLog = ({ api }) => {
1310
- const [page, setPage] = useState4(0);
1311
- const [pageSize, setPageSize] = useState4(25);
1312
- const [filters, setFilters] = useState4({});
1423
+ const [page, setPage] = useState5(0);
1424
+ const [pageSize, setPageSize] = useState5(25);
1425
+ const [filters, setFilters] = useState5({});
1313
1426
  const fetchParams = useCallback(
1314
1427
  () => ({ page: page + 1, page_size: pageSize, ...filters }),
1315
1428
  [page, pageSize, filters]
@@ -1320,8 +1433,8 @@ var init_AuditLog = __esm({
1320
1433
  );
1321
1434
  const entries = value?.audit_logs ?? [];
1322
1435
  const total = value?.total ?? 0;
1323
- 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(
1324
- 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,
1325
1438
  {
1326
1439
  size: "small",
1327
1440
  label: "Action",
@@ -1333,13 +1446,13 @@ var init_AuditLog = __esm({
1333
1446
  },
1334
1447
  sx: { minWidth: 160 }
1335
1448
  },
1336
- /* @__PURE__ */ React5.createElement(MenuItem3, { value: "" }, "All actions"),
1337
- /* @__PURE__ */ React5.createElement(MenuItem3, { value: "created" }, "Created"),
1338
- /* @__PURE__ */ React5.createElement(MenuItem3, { value: "updated" }, "Updated"),
1339
- /* @__PURE__ */ React5.createElement(MenuItem3, { value: "deleted" }, "Deleted"),
1340
- /* @__PURE__ */ React5.createElement(MenuItem3, { value: "blocked" }, "Blocked")
1341
- ), /* @__PURE__ */ React5.createElement(
1342
- 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,
1343
1456
  {
1344
1457
  size: "small",
1345
1458
  label: "Table",
@@ -1351,12 +1464,12 @@ var init_AuditLog = __esm({
1351
1464
  },
1352
1465
  sx: { minWidth: 160 }
1353
1466
  },
1354
- /* @__PURE__ */ React5.createElement(MenuItem3, { value: "" }, "All tables"),
1355
- /* @__PURE__ */ React5.createElement(MenuItem3, { value: "LiteLLM_VerificationToken" }, "Key"),
1356
- /* @__PURE__ */ React5.createElement(MenuItem3, { value: "LiteLLM_TeamTable" }, "Team"),
1357
- /* @__PURE__ */ React5.createElement(MenuItem3, { value: "LiteLLM_UserTable" }, "User")
1358
- ), /* @__PURE__ */ React5.createElement(
1359
- 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,
1360
1473
  {
1361
1474
  size: "small",
1362
1475
  label: "Changed by",
@@ -1367,7 +1480,7 @@ var init_AuditLog = __esm({
1367
1480
  },
1368
1481
  sx: { minWidth: 200 }
1369
1482
  }
1370
- )), 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(
1371
1484
  TablePagination,
1372
1485
  {
1373
1486
  component: "div",
@@ -1391,8 +1504,8 @@ var LiteLLMPage_exports = {};
1391
1504
  __export(LiteLLMPage_exports, {
1392
1505
  LiteLLMPage: () => LiteLLMPage
1393
1506
  });
1394
- import React6, { useState as useState5, useCallback as useCallback2, useMemo as useMemo3 } from "react";
1395
- 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";
1396
1509
  import { useAsync as useAsync2, useAsyncRetry } from "react-use";
1397
1510
  import { useApi } from "@backstage/core-plugin-api";
1398
1511
  function initDateRange() {
@@ -1415,6 +1528,7 @@ var init_LiteLLMPage = __esm({
1415
1528
  "use strict";
1416
1529
  init_DashboardHeader();
1417
1530
  init_KeysTable();
1531
+ init_GenerateKeyDialog();
1418
1532
  init_UsageStats();
1419
1533
  init_TeamUsage();
1420
1534
  init_AuditLog();
@@ -1422,11 +1536,12 @@ var init_LiteLLMPage = __esm({
1422
1536
  PERIOD_LS_KEY2 = "litellm_usage_period";
1423
1537
  LiteLLMPage = () => {
1424
1538
  const api = useApi(liteLlmApiRef);
1425
- const [dateRange, setDateRange] = useState5(initDateRange);
1426
- const [activeTab, setActiveTab] = useState5("overview");
1427
- const [snackbar, setSnackbar] = useState5(null);
1428
- const [teamUsageCache, setTeamUsageCache] = useState5({});
1429
- 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({});
1430
1545
  const { value: userInfo, loading: userLoading, error: userError } = useAsync2(
1431
1546
  () => api.getUserInfo(),
1432
1547
  [api]
@@ -1451,7 +1566,7 @@ var init_LiteLLMPage = __esm({
1451
1566
  [api]
1452
1567
  );
1453
1568
  const { value: liteLlmConfig } = useAsync2(() => api.getConfig(), [api]);
1454
- const teams = useMemo3(() => {
1569
+ const teams = useMemo5(() => {
1455
1570
  if (!allTeams?.length) return [];
1456
1571
  if (!userInfo) return allTeams;
1457
1572
  const userId = userInfo.user_id;
@@ -1487,7 +1602,7 @@ var init_LiteLLMPage = __esm({
1487
1602
  setTeamUsageLoading((prev) => ({ ...prev, [teamId]: false }));
1488
1603
  }
1489
1604
  }, [api, dateRange, teamUsageCache, teamUsageLoading]);
1490
- const allowedModels = useMemo3(() => {
1605
+ const allowedModels = useMemo5(() => {
1491
1606
  if (!allModels?.length) return [];
1492
1607
  const userModels = userInfo?.models;
1493
1608
  const teamModels = teams?.flatMap((t) => t.models ?? []);
@@ -1595,25 +1710,34 @@ var init_LiteLLMPage = __esm({
1595
1710
  );
1596
1711
  const isInitialLoading = userLoading && !userInfo;
1597
1712
  if (isInitialLoading) {
1598
- 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));
1599
1714
  }
1600
1715
  if (userError || !userInfo) {
1601
1716
  const isProvisioningEnabled = userError?.body?.provisioning === true;
1602
1717
  const hint = userError?.body?.hint;
1603
- 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.")));
1604
1719
  }
1605
- 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(
1606
1730
  Tabs3,
1607
1731
  {
1608
1732
  value: activeTab,
1609
1733
  onChange: (_, v) => setActiveTab(v),
1610
1734
  sx: { mb: 2, borderBottom: 1, borderColor: "divider" }
1611
1735
  },
1612
- /* @__PURE__ */ React6.createElement(Tab3, { label: "Overview", value: "overview" }),
1613
- /* @__PURE__ */ React6.createElement(Tab3, { label: "Keys", value: "keys" }),
1614
- /* @__PURE__ */ React6.createElement(Tab3, { label: "Teams", value: "teams" }),
1615
- userInfo.can_view_audit && /* @__PURE__ */ React6.createElement(Tab3, { label: "Audit Log", value: "audit" })
1616
- ), 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(
1617
1741
  UsageStats,
1618
1742
  {
1619
1743
  usage: usage ?? null,
@@ -1623,25 +1747,21 @@ var init_LiteLLMPage = __esm({
1623
1747
  loading: usageLoading,
1624
1748
  userInfo
1625
1749
  }
1626
- ), activeTab === "keys" && /* @__PURE__ */ React6.createElement(
1750
+ ), activeTab === "keys" && /* @__PURE__ */ React7.createElement(
1627
1751
  KeysTable,
1628
1752
  {
1629
1753
  keys: keys ?? [],
1630
1754
  models: allowedModels,
1631
- teams: teams ?? [],
1632
- username: userInfo.user_id,
1633
- keyGenerationSettings: liteLlmConfig?.keyGeneration,
1634
1755
  loading: keysLoading || modelsLoading,
1635
- onGenerateKey: handleGenerateKey,
1756
+ onGenerateKeyClick: () => setGenerateDialogOpen(true),
1636
1757
  onUpdateKey: handleUpdateKey,
1637
1758
  onBlockKey: handleBlockKey,
1638
1759
  onUnblockKey: handleUnblockKey,
1639
1760
  onResetKeySpend: handleResetKeySpend,
1640
1761
  onDeleteKey: handleDeleteKey,
1641
- onPruneExpiredKeys: handlePruneExpiredKeys,
1642
- onGetConfig: () => api.getConfig()
1762
+ onPruneExpiredKeys: handlePruneExpiredKeys
1643
1763
  }
1644
- ), activeTab === "teams" && /* @__PURE__ */ React6.createElement(
1764
+ ), activeTab === "teams" && /* @__PURE__ */ React7.createElement(
1645
1765
  TeamUsage,
1646
1766
  {
1647
1767
  teams: teams ?? [],
@@ -1652,7 +1772,20 @@ var init_LiteLLMPage = __esm({
1652
1772
  },
1653
1773
  getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false
1654
1774
  }
1655
- ), 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(
1656
1789
  Snackbar,
1657
1790
  {
1658
1791
  open: !!snackbar,
@@ -1660,7 +1793,7 @@ var init_LiteLLMPage = __esm({
1660
1793
  onClose: () => setSnackbar(null),
1661
1794
  anchorOrigin: { vertical: "bottom", horizontal: "right" }
1662
1795
  },
1663
- 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
1664
1797
  ));
1665
1798
  };
1666
1799
  }
@@ -1668,7 +1801,7 @@ var init_LiteLLMPage = __esm({
1668
1801
 
1669
1802
  // src/plugin.tsx
1670
1803
  init_api();
1671
- import React7 from "react";
1804
+ import React8 from "react";
1672
1805
  import { TrendingUp as TrendingUpIcon } from "@mui/icons-material";
1673
1806
  import {
1674
1807
  createFrontendPlugin,
@@ -1687,10 +1820,10 @@ var liteLlmPage = PageBlueprint.make({
1687
1820
  params: {
1688
1821
  path: "/litellm",
1689
1822
  title: "LiteLLM",
1690
- icon: /* @__PURE__ */ React7.createElement(TrendingUpIcon, null),
1823
+ icon: /* @__PURE__ */ React8.createElement(TrendingUpIcon, null),
1691
1824
  loader: async () => {
1692
1825
  const { LiteLLMPage: LiteLLMPage2 } = await Promise.resolve().then(() => (init_LiteLLMPage(), LiteLLMPage_exports));
1693
- return /* @__PURE__ */ React7.createElement(LiteLLMPage2, null);
1826
+ return /* @__PURE__ */ React8.createElement(LiteLLMPage2, null);
1694
1827
  }
1695
1828
  }
1696
1829
  });
@@ -1709,17 +1842,17 @@ init_TeamUsage();
1709
1842
  // src/components/LiteLLMHomeWidget.tsx
1710
1843
  init_api();
1711
1844
  init_format();
1712
- import React8, { useState as useState6, useEffect } from "react";
1845
+ import React9, { useState as useState7, useEffect as useEffect2 } from "react";
1713
1846
  import {
1714
1847
  Paper as Paper7,
1715
- Box as Box7,
1716
- Typography as Typography7,
1848
+ Box as Box8,
1849
+ Typography as Typography8,
1717
1850
  FormControl as FormControl2,
1718
1851
  Select as Select2,
1719
1852
  MenuItem as MenuItem4,
1720
1853
  Grid as Grid2,
1721
- CircularProgress as CircularProgress5,
1722
- Alert as Alert3
1854
+ CircularProgress as CircularProgress6,
1855
+ Alert as Alert4
1723
1856
  } from "@mui/material";
1724
1857
  import { AreaChart as AreaChart3, Area as Area3, ResponsiveContainer as ResponsiveContainer3 } from "recharts";
1725
1858
  import { useApi as useApi2 } from "@backstage/core-plugin-api";
@@ -1735,19 +1868,19 @@ function presetToDateRange(preset) {
1735
1868
  }
1736
1869
  return { start, end };
1737
1870
  }
1738
- 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));
1739
1872
  var LiteLLMHomeWidget = ({
1740
1873
  defaultPeriod = "7d",
1741
1874
  title = "LiteLLM Usage"
1742
1875
  }) => {
1743
1876
  const api = useApi2(liteLlmApiRef);
1744
- const [period, setPeriod] = useState6(defaultPeriod);
1745
- const [loading, setLoading] = useState6(true);
1746
- const [usageError, setUsageError] = useState6(null);
1747
- const [keysError, setKeysError] = useState6(null);
1748
- const [usage, setUsage] = useState6(null);
1749
- const [keys, setKeys] = useState6([]);
1750
- 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(() => {
1751
1884
  let cancelled = false;
1752
1885
  setLoading(true);
1753
1886
  setUsageError(null);
@@ -1784,17 +1917,17 @@ var LiteLLMHomeWidget = ({
1784
1917
  spend: d.spend
1785
1918
  }));
1786
1919
  const hasSparkline = dailyData.length > 0;
1787
- 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(
1788
1921
  Select2,
1789
1922
  {
1790
1923
  value: period,
1791
1924
  onChange: (e) => setPeriod(e.target.value),
1792
1925
  displayEmpty: true
1793
1926
  },
1794
- /* @__PURE__ */ React8.createElement(MenuItem4, { value: "today" }, "Today"),
1795
- /* @__PURE__ */ React8.createElement(MenuItem4, { value: "7d" }, "7d"),
1796
- /* @__PURE__ */ React8.createElement(MenuItem4, { value: "30d" }, "30d")
1797
- ))), 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(
1798
1931
  Area3,
1799
1932
  {
1800
1933
  type: "monotone",