@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.cjs.js CHANGED
@@ -175,13 +175,26 @@ var init_api = __esm({
175
175
  });
176
176
 
177
177
  // src/components/DashboardHeader.tsx
178
- var import_react, import_material, DashboardHeader;
178
+ var import_react, import_material, import_icons_material, DashboardHeader;
179
179
  var init_DashboardHeader = __esm({
180
180
  "src/components/DashboardHeader.tsx"() {
181
181
  "use strict";
182
182
  import_react = __toESM(require("react"));
183
183
  import_material = require("@mui/material");
184
- DashboardHeader = ({ userInfo, teams, loading }) => {
184
+ import_icons_material = require("@mui/icons-material");
185
+ init_api();
186
+ DashboardHeader = ({
187
+ userInfo,
188
+ teams,
189
+ keys,
190
+ loading,
191
+ onGenerateKeyClick
192
+ }) => {
193
+ const counts = (0, import_react.useMemo)(() => {
194
+ const expired = keys.filter((k) => expiryStatus(k.expires_at) === "expired").length;
195
+ const expiringSoon = keys.filter((k) => expiryStatus(k.expires_at) === "soon").length;
196
+ return { total: keys.length, expired, expiringSoon };
197
+ }, [keys]);
185
198
  if (loading) {
186
199
  return /* @__PURE__ */ import_react.default.createElement(import_material.Paper, { sx: { p: 2, mb: 2 } }, /* @__PURE__ */ import_react.default.createElement(import_material.LinearProgress, null));
187
200
  }
@@ -195,21 +208,32 @@ var init_DashboardHeader = __esm({
195
208
  variant: "outlined",
196
209
  color: "primary"
197
210
  }
198
- ))))));
199
- };
200
- }
201
- });
202
-
203
- // src/format.ts
204
- var fmtUsd, fmtInt, estimateTokensFromBudget;
205
- var init_format = __esm({
206
- "src/format.ts"() {
207
- "use strict";
208
- fmtUsd = (n) => `$${(n ?? 0).toFixed(n < 1 ? 4 : 2)}`;
209
- fmtInt = (n) => (n ?? 0).toLocaleString();
210
- estimateTokensFromBudget = (budget, pricePerToken) => {
211
- if (!pricePerToken || pricePerToken <= 0 || !budget || budget <= 0) return null;
212
- return Math.floor(budget / pricePerToken);
211
+ )))), /* @__PURE__ */ import_react.default.createElement(import_material.Box, { display: "flex", alignItems: "center", gap: 2, flexWrap: "wrap" }, /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "row", spacing: 1 }, /* @__PURE__ */ import_react.default.createElement(import_material.Chip, { icon: /* @__PURE__ */ import_react.default.createElement(import_icons_material.Key, { fontSize: "small" }), label: `${counts.total} keys`, size: "small" }), /* @__PURE__ */ import_react.default.createElement(
212
+ import_material.Chip,
213
+ {
214
+ icon: /* @__PURE__ */ import_react.default.createElement(import_icons_material.Warning, { fontSize: "small" }),
215
+ label: `${counts.expired} expired`,
216
+ size: "small",
217
+ color: counts.expired > 0 ? "error" : "default"
218
+ }
219
+ ), /* @__PURE__ */ import_react.default.createElement(
220
+ import_material.Chip,
221
+ {
222
+ icon: /* @__PURE__ */ import_react.default.createElement(import_icons_material.Schedule, { fontSize: "small" }),
223
+ label: `${counts.expiringSoon} expiring soon`,
224
+ size: "small",
225
+ color: counts.expiringSoon > 0 ? "warning" : "default"
226
+ }
227
+ )), /* @__PURE__ */ import_react.default.createElement(
228
+ import_material.Button,
229
+ {
230
+ variant: "contained",
231
+ color: "primary",
232
+ startIcon: /* @__PURE__ */ import_react.default.createElement(import_icons_material.Add, null),
233
+ onClick: onGenerateKeyClick
234
+ },
235
+ "Generate New Key"
236
+ ))));
213
237
  };
214
238
  }
215
239
  });
@@ -220,7 +244,7 @@ function ExpiryChip({ expiresAt }) {
220
244
  if (!status) return /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2", color: "text.secondary" }, "-");
221
245
  const label = status === "expired" ? "Expired" : status === "soon" ? `${Math.ceil((new Date(expiresAt).getTime() - Date.now()) / 864e5)}d left` : formatDate(expiresAt);
222
246
  const color = status === "expired" ? "error" : status === "soon" ? "warning" : "default";
223
- const icon = status === "expired" || status === "soon" ? /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Warning, { fontSize: "small" }) : void 0;
247
+ const icon = status === "expired" || status === "soon" ? /* @__PURE__ */ import_react2.default.createElement(import_icons_material2.Warning, { fontSize: "small" }) : void 0;
224
248
  return /* @__PURE__ */ import_react2.default.createElement(import_material2.Chip, { label, color, size: "small", icon });
225
249
  }
226
250
  function BudgetCell({ spend, maxBudget }) {
@@ -249,51 +273,18 @@ function formatContextWindow(maxInput, maxOutput) {
249
273
  if (outPart) return `ctx ${outPart} out`;
250
274
  return null;
251
275
  }
252
- function trimSlash(url) {
253
- return url.replace(/\/+$/, "");
254
- }
255
- function buildSnippets(baseUrl, key, model) {
256
- const base = trimSlash(baseUrl);
257
- return {
258
- curl: `curl ${base}/v1/chat/completions \\
259
- -H "Authorization: Bearer ${key}" \\
260
- -H "Content-Type: application/json" \\
261
- -d '{
262
- "model": "${model}",
263
- "messages": [{ "role": "user", "content": "Hello!" }]
264
- }'`,
265
- openai: `from openai import OpenAI
266
-
267
- client = OpenAI(
268
- api_key="${key}",
269
- base_url="${base}/v1",
270
- )
271
-
272
- response = client.chat.completions.create(
273
- model="${model}",
274
- messages=[{ "role": "user", "content": "Hello!" }],
275
- )
276
- print(response.choices[0].message.content)`
277
- };
278
- }
279
- var import_react2, import_material2, import_icons_material, maskKey, generateDefaultAlias, shortKeyId, formatDate, emptyForm, keyToEditForm, KeysTable, SnippetTabs;
276
+ var import_react2, import_material2, import_icons_material2, maskKey, shortKeyId, formatDate, keyToEditForm, KeysTable;
280
277
  var init_KeysTable = __esm({
281
278
  "src/components/KeysTable.tsx"() {
282
279
  "use strict";
283
280
  import_react2 = __toESM(require("react"));
284
281
  import_material2 = require("@mui/material");
285
- import_icons_material = require("@mui/icons-material");
282
+ import_icons_material2 = require("@mui/icons-material");
286
283
  init_api();
287
- init_format();
288
284
  maskKey = (key) => {
289
285
  if (key.length <= 8) return "***";
290
286
  return `${key.slice(0, 4)}...${key.slice(-4)}`;
291
287
  };
292
- generateDefaultAlias = (username) => {
293
- const base = (username || "user").split("@")[0].toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "user";
294
- const hash = Math.random().toString(36).slice(2, 8);
295
- return `${base}-${hash}`;
296
- };
297
288
  shortKeyId = (token) => {
298
289
  if (!token) return "-";
299
290
  if (token.length <= 16) return token;
@@ -306,16 +297,6 @@ var init_KeysTable = __esm({
306
297
  return dateStr;
307
298
  }
308
299
  };
309
- emptyForm = () => ({
310
- alias: "",
311
- models: [],
312
- duration: "30d",
313
- max_budget: 100,
314
- tpm_limit: void 0,
315
- rpm_limit: void 0,
316
- team_id: void 0,
317
- key_type: "llm_api"
318
- });
319
300
  keyToEditForm = (k) => ({
320
301
  key_alias: k.key_alias ?? "",
321
302
  models: k.models ?? [],
@@ -326,26 +307,15 @@ var init_KeysTable = __esm({
326
307
  KeysTable = ({
327
308
  keys,
328
309
  models,
329
- teams,
330
- username,
331
- keyGenerationSettings,
332
310
  loading,
333
- onGenerateKey,
311
+ onGenerateKeyClick,
334
312
  onUpdateKey,
335
313
  onBlockKey,
336
314
  onUnblockKey,
337
315
  onResetKeySpend,
338
316
  onDeleteKey,
339
- onPruneExpiredKeys,
340
- onGetConfig
317
+ onPruneExpiredKeys
341
318
  }) => {
342
- const [generateModalOpen, setGenerateModalOpen] = (0, import_react2.useState)(false);
343
- const [newKeyValue, setNewKeyValue] = (0, import_react2.useState)(null);
344
- const [newKeySnippets, setNewKeySnippets] = (0, import_react2.useState)(null);
345
- const [newKeyModel, setNewKeyModel] = (0, import_react2.useState)("");
346
- const [formData, setFormData] = (0, import_react2.useState)(emptyForm());
347
- const [unlimitedBudget, setUnlimitedBudget] = (0, import_react2.useState)(false);
348
- const [submitting, setSubmitting] = (0, import_react2.useState)(false);
349
319
  const [editingKey, setEditingKey] = (0, import_react2.useState)(null);
350
320
  const [editForm, setEditForm] = (0, import_react2.useState)({});
351
321
  const [editSubmitting, setEditSubmitting] = (0, import_react2.useState)(false);
@@ -362,66 +332,7 @@ var init_KeysTable = __esm({
362
332
  (k) => (k.key_alias ?? "").toLowerCase().includes(q) || k.models?.some((m) => m.toLowerCase().includes(q))
363
333
  );
364
334
  }, [keys, filterText]);
365
- const allowUnlimitedBudget = keyGenerationSettings?.allowUnlimitedBudget ?? false;
366
- const teamRequired = keyGenerationSettings?.teamRequired ?? true;
367
- const selectedTeam = teams.find((t) => t.team_id === formData.team_id) ?? null;
368
- const availableModels = (0, import_react2.useMemo)(() => {
369
- const teamModels = selectedTeam?.models;
370
- if (teamModels && teamModels.length > 0) {
371
- const allowed = new Set(teamModels);
372
- return models.filter((m) => allowed.has(m.model_name));
373
- }
374
- return models;
375
- }, [models, selectedTeam]);
376
- const selectedModels = availableModels.filter((m) => (formData.models || []).includes(m.model_name));
377
335
  const editSelectedModels = models.filter((m) => (editForm.models || []).includes(m.model_name));
378
- const aliasError = !(formData.alias || "").trim();
379
- const teamError = teamRequired && !formData.team_id;
380
- const budgetInvalid = !unlimitedBudget && (formData.max_budget === void 0 || formData.max_budget === null || formData.max_budget <= 0);
381
- const canGenerate = !aliasError && !teamError && !budgetInvalid && !submitting;
382
- const budgetEstimate = (0, import_react2.useMemo)(() => {
383
- if (unlimitedBudget || budgetInvalid) return null;
384
- const inputCosts = selectedModels.map((m) => m.input_cost_per_token).filter((c) => typeof c === "number" && c > 0);
385
- if (inputCosts.length === 0) return null;
386
- const pricePerToken = Math.max(...inputCosts);
387
- return estimateTokensFromBudget(formData.max_budget ?? 0, pricePerToken);
388
- }, [formData.max_budget, selectedModels, unlimitedBudget, budgetInvalid]);
389
- const handleGenerate = async () => {
390
- setSubmitting(true);
391
- try {
392
- const request = {
393
- ...formData,
394
- max_budget: unlimitedBudget ? null : formData.max_budget
395
- };
396
- const response = await onGenerateKey(request);
397
- setNewKeyValue(response.key);
398
- setNewKeyModel(formData.models?.[0] ?? "");
399
- setNewKeySnippets(null);
400
- try {
401
- const config = await onGetConfig();
402
- const model = formData.models?.[0] ?? "";
403
- setNewKeySnippets(buildSnippets(config.baseUrl, response.key, model));
404
- } catch {
405
- }
406
- setFormData(emptyForm());
407
- setUnlimitedBudget(false);
408
- } catch (error) {
409
- console.error("Failed to generate key:", error);
410
- } finally {
411
- setSubmitting(false);
412
- }
413
- };
414
- const handleOpenGenerate = () => {
415
- setFormData({ ...emptyForm(), alias: generateDefaultAlias(username) });
416
- setGenerateModalOpen(true);
417
- };
418
- const handleCloseModal = () => {
419
- setGenerateModalOpen(false);
420
- setNewKeyValue(null);
421
- setNewKeySnippets(null);
422
- setFormData(emptyForm());
423
- setUnlimitedBudget(false);
424
- };
425
336
  const handleOpenEdit = (k) => {
426
337
  setEditingKey(k);
427
338
  setEditForm(keyToEditForm(k));
@@ -473,117 +384,388 @@ var init_KeysTable = __esm({
473
384
  try {
474
385
  await onDeleteKey(deleteConfirmId);
475
386
  } finally {
476
- setDeleteSubmitting(false);
477
- setDeleteConfirmId(null);
387
+ setDeleteSubmitting(false);
388
+ setDeleteConfirmId(null);
389
+ }
390
+ };
391
+ const copyToClipboard = (text) => {
392
+ navigator.clipboard.writeText(text);
393
+ };
394
+ const [pruneConfirmCount, setPruneConfirmCount] = (0, import_react2.useState)(null);
395
+ const [pruneSubmitting, setPruneSubmitting] = (0, import_react2.useState)(false);
396
+ const expiredKeys = (0, import_react2.useMemo)(() => {
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__ */ import_react2.default.createElement(import_material2.Box, null, /* @__PURE__ */ import_react2.default.createElement("span", null, m.model_name), m.supports_function_calling && " \u{1F527}", m.supports_vision && " \u{1F441}\uFE0F", ctx && /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, ctx), (inCost || outCost) && /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, inCost, " in \xB7 ", outCost, " out"));
416
+ };
417
+ return /* @__PURE__ */ import_react2.default.createElement(import_react2.default.Fragment, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Paper, { sx: { mb: 2 } }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", justifyContent: "space-between", alignItems: "center", p: 2, gap: 2 }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "h6" }, "Virtual Keys"), /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", gap: 1, alignItems: "center", flex: 1, justifyContent: "flex-end" }, /* @__PURE__ */ import_react2.default.createElement(
418
+ import_material2.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__ */ import_react2.default.createElement(import_material2.InputAdornment, { position: "start" }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material2.Search, { fontSize: "small" }))
427
+ }
428
+ }
429
+ ), expiredKeys.length > 0 && /* @__PURE__ */ import_react2.default.createElement(
430
+ import_material2.Button,
431
+ {
432
+ variant: "outlined",
433
+ color: "error",
434
+ startIcon: /* @__PURE__ */ import_react2.default.createElement(import_icons_material2.Autorenew, null),
435
+ onClick: () => setPruneConfirmCount(expiredKeys.length)
436
+ },
437
+ "Prune expired (",
438
+ expiredKeys.length,
439
+ ")"
440
+ ), /* @__PURE__ */ import_react2.default.createElement(
441
+ import_material2.Button,
442
+ {
443
+ variant: "contained",
444
+ color: "primary",
445
+ startIcon: /* @__PURE__ */ import_react2.default.createElement(import_icons_material2.Add, null),
446
+ onClick: onGenerateKeyClick
447
+ },
448
+ "Generate New Key"
449
+ ))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableContainer, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Table, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableHead, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Alias"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Key ID"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Created"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Expires"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Budget"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "TPM / RPM"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Models"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { align: "right" }, "Actions"))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableBody, null, loading ? /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 24 }))) : filteredKeys.length === 0 ? /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { colSpan: 8, align: "center", sx: { py: 4 } }, filterText ? /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { color: "text.secondary" }, "No keys match filter") : /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { color: "text.secondary", gutterBottom: true }, "No keys yet \u2014 generate your first key to start calling models."), /* @__PURE__ */ import_react2.default.createElement(
450
+ import_material2.Button,
451
+ {
452
+ variant: "contained",
453
+ color: "primary",
454
+ size: "small",
455
+ startIcon: /* @__PURE__ */ import_react2.default.createElement(import_icons_material2.Add, 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__ */ import_react2.default.createElement(import_material2.TableRow, { key: keyId, sx: key.blocked ? { bgcolor: "action.disabledBackground" } : void 0 }, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", alignItems: "center", gap: 0.5 }, key.key_alias || "-", key.blocked && /* @__PURE__ */ import_react2.default.createElement(import_material2.Chip, { label: "Blocked", color: "error", size: "small" }))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", alignItems: "center", gap: 0.5 }, /* @__PURE__ */ import_react2.default.createElement(
463
+ import_material2.Typography,
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__ */ import_react2.default.createElement(import_material2.IconButton, { size: "small", onClick: () => copyToClipboard(keyId), title: "Copy Key ID" }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material2.ContentCopy, { fontSize: "small" })))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, formatDate(key.created_at)), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(ExpiryChip, { expiresAt: key.expires_at })), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(BudgetCell, { spend: key.spend ?? 0, maxBudget: key.max_budget })), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2" }, key.tpm_limit ?? "-", " / ", key.rpm_limit ?? "-")), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", gap: 0.5, flexWrap: "wrap" }, key.models?.slice(0, 2).map((model) => /* @__PURE__ */ import_react2.default.createElement(import_material2.Chip, { key: model, label: model, size: "small" })), (key.models?.length || 0) > 2 && /* @__PURE__ */ import_react2.default.createElement(import_material2.Chip, { label: `+${(key.models?.length || 0) - 2}`, size: "small", variant: "outlined" }))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { align: "right" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.IconButton, { onClick: () => handleOpenEdit(key), title: "Edit key" }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material2.Edit, { fontSize: "small" })), /* @__PURE__ */ import_react2.default.createElement(
479
+ import_material2.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__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 18 }) : key.blocked ? /* @__PURE__ */ import_react2.default.createElement(import_icons_material2.LockOpen, { fontSize: "small" }) : /* @__PURE__ */ import_react2.default.createElement(import_icons_material2.Lock, { fontSize: "small" })
487
+ ), /* @__PURE__ */ import_react2.default.createElement(
488
+ import_material2.IconButton,
489
+ {
490
+ color: "error",
491
+ onClick: () => setDeleteConfirmId(keyId),
492
+ title: "Revoke key"
493
+ },
494
+ /* @__PURE__ */ import_react2.default.createElement(import_icons_material2.Delete, null)
495
+ )));
496
+ }))))), /* @__PURE__ */ import_react2.default.createElement(import_material2.Dialog, { open: !!editingKey, onClose: handleCloseEdit, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogTitle, null, "Edit Key"), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogContent, null, editingKey && /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2", color: "text.secondary" }, /* @__PURE__ */ import_react2.default.createElement("code", { style: { fontFamily: "monospace", color: "inherit" } }, maskKey(editingKey.key))), /* @__PURE__ */ import_react2.default.createElement(
497
+ import_material2.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__ */ import_react2.default.createElement(
505
+ import_material2.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__ */ import_react2.default.createElement("li", { ...props }, modelOption(m)),
514
+ renderInput: (params) => /* @__PURE__ */ import_react2.default.createElement(import_material2.TextField, { ...params, label: "Models", fullWidth: true })
515
+ }
516
+ ), /* @__PURE__ */ import_react2.default.createElement(
517
+ import_material2.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__ */ import_react2.default.createElement(
526
+ import_material2.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__ */ import_react2.default.createElement(
536
+ import_material2.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__ */ import_react2.default.createElement(import_material2.Box, { mt: 1, pt: 2, borderTop: "1px solid", sx: { borderColor: "divider" } }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "caption", color: "text.secondary", display: "block", mb: 1 }, "Danger Zone"), !resetSpendConfirm ? /* @__PURE__ */ import_react2.default.createElement(
546
+ import_material2.Button,
547
+ {
548
+ size: "small",
549
+ color: "warning",
550
+ variant: "outlined",
551
+ onClick: () => setResetSpendConfirm(true)
552
+ },
553
+ "Reset Spend to $0"
554
+ ) : /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2", color: "warning.main" }, "Zero out spend counter?"), /* @__PURE__ */ import_react2.default.createElement(
555
+ import_material2.Button,
556
+ {
557
+ size: "small",
558
+ color: "warning",
559
+ variant: "contained",
560
+ disabled: resetSpendSubmitting,
561
+ onClick: handleResetSpend
562
+ },
563
+ resetSpendSubmitting ? /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 16 }) : "Confirm"
564
+ ), /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { size: "small", onClick: () => setResetSpendConfirm(false) }, "Cancel"))))), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogActions, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: handleCloseEdit }, "Cancel"), /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: handleUpdate, variant: "contained", color: "primary", disabled: editSubmitting }, editSubmitting ? /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 24 }) : "Save"))), /* @__PURE__ */ import_react2.default.createElement(import_material2.Dialog, { open: !!deleteConfirmId, onClose: () => setDeleteConfirmId(null), maxWidth: "xs", fullWidth: true }, /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogTitle, null, "Revoke Key?"), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogContent, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, null, "This will permanently revoke the key. Any integrations using it will stop working immediately.")), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogActions, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: () => setDeleteConfirmId(null), disabled: deleteSubmitting }, "Cancel"), /* @__PURE__ */ import_react2.default.createElement(
565
+ import_material2.Button,
566
+ {
567
+ onClick: handleConfirmDelete,
568
+ variant: "contained",
569
+ color: "error",
570
+ disabled: deleteSubmitting
571
+ },
572
+ deleteSubmitting ? /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 20 }) : "Revoke"
573
+ ))), /* @__PURE__ */ import_react2.default.createElement(import_material2.Dialog, { open: pruneConfirmCount !== null, onClose: () => setPruneConfirmCount(null), maxWidth: "xs", fullWidth: true }, /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogTitle, null, "Prune Expired Keys?"), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogContent, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, null, "This will permanently delete ", pruneConfirmCount, " expired key", pruneConfirmCount !== 1 ? "s" : "", " from LiteLLM. Any integrations using them will stop working immediately.")), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogActions, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: () => setPruneConfirmCount(null), disabled: pruneSubmitting }, "Cancel"), /* @__PURE__ */ import_react2.default.createElement(
574
+ import_material2.Button,
575
+ {
576
+ onClick: handlePruneExpired,
577
+ variant: "contained",
578
+ color: "error",
579
+ disabled: pruneSubmitting
580
+ },
581
+ pruneSubmitting ? /* @__PURE__ */ import_react2.default.createElement(import_material2.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
+ function trimSlash(url) {
603
+ return url.replace(/\/+$/, "");
604
+ }
605
+ function buildSnippets(baseUrl, key, model) {
606
+ const base = trimSlash(baseUrl);
607
+ return {
608
+ curl: `curl ${base}/v1/chat/completions \\
609
+ -H "Authorization: Bearer ${key}" \\
610
+ -H "Content-Type: application/json" \\
611
+ -d '{
612
+ "model": "${model}",
613
+ "messages": [{ "role": "user", "content": "Hello!" }]
614
+ }'`,
615
+ openai: `from openai import OpenAI
616
+
617
+ client = OpenAI(
618
+ api_key="${key}",
619
+ base_url="${base}/v1",
620
+ )
621
+
622
+ response = client.chat.completions.create(
623
+ model="${model}",
624
+ messages=[{ "role": "user", "content": "Hello!" }],
625
+ )
626
+ print(response.choices[0].message.content)`
627
+ };
628
+ }
629
+ function fmtCost2(perToken) {
630
+ if (!perToken) return null;
631
+ const per1k = perToken * 1e3;
632
+ return per1k < 0.01 ? `$${(perToken * 1e6).toFixed(2)}/M` : `$${per1k.toFixed(3)}/1K`;
633
+ }
634
+ function formatContextWindow2(maxInput, maxOutput) {
635
+ if (!maxInput && !maxOutput) return null;
636
+ const fmt = (n) => {
637
+ if (!n) return null;
638
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
639
+ if (n >= 1e3) return `${Math.round(n / 1e3)}K`;
640
+ return String(n);
641
+ };
642
+ const inPart = fmt(maxInput);
643
+ const outPart = fmt(maxOutput);
644
+ if (inPart && outPart) return `ctx ${inPart} in / ${outPart} out`;
645
+ if (inPart) return `ctx ${inPart}`;
646
+ if (outPart) return `ctx ${outPart} out`;
647
+ return null;
648
+ }
649
+ var import_react3, import_material3, import_icons_material3, generateDefaultAlias, emptyForm, GenerateKeyDialog, SnippetTabs;
650
+ var init_GenerateKeyDialog = __esm({
651
+ "src/components/GenerateKeyDialog.tsx"() {
652
+ "use strict";
653
+ import_react3 = __toESM(require("react"));
654
+ import_material3 = require("@mui/material");
655
+ import_icons_material3 = require("@mui/icons-material");
656
+ init_format();
657
+ generateDefaultAlias = (username) => {
658
+ const base = (username || "user").split("@")[0].toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "user";
659
+ const hash = Math.random().toString(36).slice(2, 8);
660
+ return `${base}-${hash}`;
661
+ };
662
+ emptyForm = () => ({
663
+ alias: "",
664
+ models: [],
665
+ duration: "30d",
666
+ max_budget: 100,
667
+ tpm_limit: void 0,
668
+ rpm_limit: void 0,
669
+ team_id: void 0,
670
+ key_type: "llm_api"
671
+ });
672
+ GenerateKeyDialog = ({
673
+ open,
674
+ onClose,
675
+ keys,
676
+ models,
677
+ teams,
678
+ username,
679
+ keyGenerationSettings,
680
+ onGenerateKey,
681
+ onGetConfig
682
+ }) => {
683
+ const [formData, setFormData] = (0, import_react3.useState)(emptyForm());
684
+ const [unlimitedBudget, setUnlimitedBudget] = (0, import_react3.useState)(false);
685
+ const [submitting, setSubmitting] = (0, import_react3.useState)(false);
686
+ const [newKeyValue, setNewKeyValue] = (0, import_react3.useState)(null);
687
+ const [newKeySnippets, setNewKeySnippets] = (0, import_react3.useState)(null);
688
+ const [newKeyModel, setNewKeyModel] = (0, import_react3.useState)("");
689
+ const [generateError, setGenerateError] = (0, import_react3.useState)(null);
690
+ (0, import_react3.useEffect)(() => {
691
+ if (open) {
692
+ setFormData({ ...emptyForm(), alias: generateDefaultAlias(username) });
693
+ setUnlimitedBudget(false);
694
+ setNewKeyValue(null);
695
+ setNewKeySnippets(null);
696
+ setNewKeyModel("");
697
+ setGenerateError(null);
698
+ }
699
+ }, [open, username]);
700
+ const allowUnlimitedBudget = keyGenerationSettings?.allowUnlimitedBudget ?? false;
701
+ const teamRequired = keyGenerationSettings?.teamRequired ?? true;
702
+ const selectedTeam = teams.find((t) => t.team_id === formData.team_id) ?? null;
703
+ const availableModels = (0, import_react3.useMemo)(() => {
704
+ const teamModels = selectedTeam?.models;
705
+ if (teamModels && teamModels.length > 0) {
706
+ const allowed = new Set(teamModels);
707
+ return models.filter((m) => allowed.has(m.model_name));
708
+ }
709
+ return models;
710
+ }, [models, selectedTeam]);
711
+ const selectedModels = availableModels.filter((m) => (formData.models || []).includes(m.model_name));
712
+ const aliasError = !(formData.alias || "").trim();
713
+ const teamError = teamRequired && !formData.team_id;
714
+ const budgetInvalid = !unlimitedBudget && (formData.max_budget === void 0 || formData.max_budget === null || formData.max_budget <= 0);
715
+ const canGenerate = !aliasError && !teamError && !budgetInvalid && !submitting;
716
+ const aliasDuplicate = !!formData.alias && keys.some((k) => k.key_alias === formData.alias);
717
+ const budgetEstimate = (0, import_react3.useMemo)(() => {
718
+ if (unlimitedBudget || budgetInvalid) return null;
719
+ const inputCosts = selectedModels.map((m) => m.input_cost_per_token).filter((c) => typeof c === "number" && c > 0);
720
+ if (inputCosts.length === 0) return null;
721
+ const pricePerToken = Math.max(...inputCosts);
722
+ return estimateTokensFromBudget(formData.max_budget ?? 0, pricePerToken);
723
+ }, [formData.max_budget, selectedModels, unlimitedBudget, budgetInvalid]);
724
+ const handleGenerate = async () => {
725
+ setSubmitting(true);
726
+ setGenerateError(null);
727
+ try {
728
+ const request = {
729
+ ...formData,
730
+ max_budget: unlimitedBudget ? null : formData.max_budget
731
+ };
732
+ const response = await onGenerateKey(request);
733
+ setNewKeyValue(response.key);
734
+ setNewKeyModel(formData.models?.[0] ?? "");
735
+ setNewKeySnippets(null);
736
+ try {
737
+ const config = await onGetConfig();
738
+ const model = formData.models?.[0] ?? "";
739
+ setNewKeySnippets(buildSnippets(config.baseUrl, response.key, model));
740
+ } catch {
741
+ }
742
+ setFormData(emptyForm());
743
+ setUnlimitedBudget(false);
744
+ } catch (error) {
745
+ setGenerateError(error?.message ?? "Failed to generate key");
746
+ } finally {
747
+ setSubmitting(false);
478
748
  }
479
749
  };
750
+ const handleClose = () => {
751
+ onClose();
752
+ setGenerateError(null);
753
+ setNewKeyValue(null);
754
+ setNewKeySnippets(null);
755
+ setFormData(emptyForm());
756
+ setUnlimitedBudget(false);
757
+ };
480
758
  const copyToClipboard = (text) => {
481
759
  navigator.clipboard.writeText(text);
482
760
  };
483
- const [pruneConfirmCount, setPruneConfirmCount] = (0, import_react2.useState)(null);
484
- const [pruneSubmitting, setPruneSubmitting] = (0, import_react2.useState)(false);
485
- const expiredKeys = (0, import_react2.useMemo)(() => {
486
- return keys.filter((k) => expiryStatus(k.expires_at) === "expired");
487
- }, [keys]);
488
- const handlePruneExpired = async () => {
489
- if (pruneConfirmCount === null || pruneConfirmCount === 0) return;
490
- setPruneSubmitting(true);
491
- try {
492
- await onPruneExpiredKeys();
493
- } catch (error) {
494
- console.error("Failed to prune expired keys:", error);
495
- } finally {
496
- setPruneSubmitting(false);
497
- setPruneConfirmCount(null);
498
- }
499
- };
500
761
  const modelOption = (m) => {
501
- const inCost = fmtCost(m.input_cost_per_token);
502
- const outCost = fmtCost(m.output_cost_per_token);
503
- const ctx = formatContextWindow(m.max_input_tokens, m.max_output_tokens);
504
- return /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, null, /* @__PURE__ */ import_react2.default.createElement("span", null, m.model_name), m.supports_function_calling && " \u{1F527}", m.supports_vision && " \u{1F441}\uFE0F", ctx && /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, ctx), (inCost || outCost) && /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, inCost, " in \xB7 ", outCost, " out"));
762
+ const inCost = fmtCost2(m.input_cost_per_token);
763
+ const outCost = fmtCost2(m.output_cost_per_token);
764
+ const ctx = formatContextWindow2(m.max_input_tokens, m.max_output_tokens);
765
+ return /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, null, /* @__PURE__ */ import_react3.default.createElement("span", null, m.model_name), m.supports_function_calling && " \u{1F527}", m.supports_vision && " \u{1F441}\uFE0F", ctx && /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, ctx), (inCost || outCost) && /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, inCost, " in \xB7 ", outCost, " out"));
505
766
  };
506
- return /* @__PURE__ */ import_react2.default.createElement(import_react2.default.Fragment, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Paper, { sx: { mb: 2 } }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", justifyContent: "space-between", alignItems: "center", p: 2, gap: 2 }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "h6" }, "Virtual Keys"), /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", gap: 1, alignItems: "center", flex: 1, justifyContent: "flex-end" }, /* @__PURE__ */ import_react2.default.createElement(
507
- import_material2.TextField,
508
- {
509
- size: "small",
510
- placeholder: "Filter by alias or model\u2026",
511
- value: filterText,
512
- onChange: (e) => setFilterText(e.target.value),
513
- sx: { minWidth: 240 },
514
- InputProps: {
515
- startAdornment: /* @__PURE__ */ import_react2.default.createElement(import_material2.InputAdornment, { position: "start" }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Search, { fontSize: "small" }))
516
- }
517
- }
518
- ), expiredKeys.length > 0 && /* @__PURE__ */ import_react2.default.createElement(
519
- import_material2.Button,
520
- {
521
- variant: "outlined",
522
- color: "error",
523
- startIcon: /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Autorenew, null),
524
- onClick: () => setPruneConfirmCount(expiredKeys.length)
525
- },
526
- "Prune expired (",
527
- expiredKeys.length,
528
- ")"
529
- ), /* @__PURE__ */ import_react2.default.createElement(
530
- import_material2.Button,
531
- {
532
- variant: "contained",
533
- color: "primary",
534
- startIcon: /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Add, null),
535
- onClick: handleOpenGenerate
536
- },
537
- "Generate New Key"
538
- ))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableContainer, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Table, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableHead, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Alias"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Key ID"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Created"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Expires"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Budget"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "TPM / RPM"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Models"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { align: "right" }, "Actions"))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableBody, null, loading ? /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 24 }))) : filteredKeys.length === 0 ? /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { colSpan: 8, align: "center", sx: { py: 4 } }, filterText ? /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { color: "text.secondary" }, "No keys match filter") : /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { color: "text.secondary", gutterBottom: true }, "No keys yet \u2014 generate your first key to start calling models."), /* @__PURE__ */ import_react2.default.createElement(
539
- import_material2.Button,
540
- {
541
- variant: "contained",
542
- color: "primary",
543
- size: "small",
544
- startIcon: /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Add, null),
545
- onClick: handleOpenGenerate
546
- },
547
- "Generate Your First Key"
548
- )))) : filteredKeys.map((key) => {
549
- const keyId = key.token ?? key.key;
550
- const isBlocking = blockingKeyId === keyId;
551
- return /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, { key: keyId, sx: key.blocked ? { bgcolor: "action.disabledBackground" } : void 0 }, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", alignItems: "center", gap: 0.5 }, key.key_alias || "-", key.blocked && /* @__PURE__ */ import_react2.default.createElement(import_material2.Chip, { label: "Blocked", color: "error", size: "small" }))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", alignItems: "center", gap: 0.5 }, /* @__PURE__ */ import_react2.default.createElement(
552
- import_material2.Typography,
553
- {
554
- variant: "body2",
555
- component: "code",
556
- color: "text.secondary",
557
- title: keyId,
558
- sx: {
559
- fontFamily: "monospace",
560
- backgroundColor: "background.default",
561
- px: 1,
562
- py: 0.5,
563
- borderRadius: 1
564
- }
565
- },
566
- shortKeyId(keyId)
567
- ), /* @__PURE__ */ import_react2.default.createElement(import_material2.IconButton, { size: "small", onClick: () => copyToClipboard(keyId), title: "Copy Key ID" }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material.ContentCopy, { fontSize: "small" })))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, formatDate(key.created_at)), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(ExpiryChip, { expiresAt: key.expires_at })), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(BudgetCell, { spend: key.spend ?? 0, maxBudget: key.max_budget })), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2" }, key.tpm_limit ?? "-", " / ", key.rpm_limit ?? "-")), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", gap: 0.5, flexWrap: "wrap" }, key.models?.slice(0, 2).map((model) => /* @__PURE__ */ import_react2.default.createElement(import_material2.Chip, { key: model, label: model, size: "small" })), (key.models?.length || 0) > 2 && /* @__PURE__ */ import_react2.default.createElement(import_material2.Chip, { label: `+${(key.models?.length || 0) - 2}`, size: "small", variant: "outlined" }))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { align: "right" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.IconButton, { onClick: () => handleOpenEdit(key), title: "Edit key" }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Edit, { fontSize: "small" })), /* @__PURE__ */ import_react2.default.createElement(
568
- import_material2.IconButton,
569
- {
570
- onClick: () => handleToggleBlock(key),
571
- disabled: isBlocking,
572
- color: key.blocked ? "warning" : "default",
573
- title: key.blocked ? "Unblock key" : "Block key \u2014 suspends without revoking"
574
- },
575
- isBlocking ? /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 18 }) : key.blocked ? /* @__PURE__ */ import_react2.default.createElement(import_icons_material.LockOpen, { fontSize: "small" }) : /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Lock, { fontSize: "small" })
576
- ), /* @__PURE__ */ import_react2.default.createElement(
577
- import_material2.IconButton,
578
- {
579
- color: "error",
580
- onClick: () => setDeleteConfirmId(keyId),
581
- title: "Revoke key"
582
- },
583
- /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Delete, null)
584
- )));
585
- }))))), /* @__PURE__ */ import_react2.default.createElement(import_material2.Dialog, { open: generateModalOpen, onClose: handleCloseModal, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogTitle, null, newKeyValue ? "Key Generated" : "Generate New Key"), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogContent, null, newKeyValue ? /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2", color: "text.secondary", gutterBottom: true }, "Copy this key now. You won't be able to see it again."), /* @__PURE__ */ import_react2.default.createElement(
586
- import_material2.Box,
767
+ return /* @__PURE__ */ import_react3.default.createElement(import_material3.Dialog, { open, onClose: handleClose, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ import_react3.default.createElement(import_material3.DialogTitle, null, newKeyValue ? "Key Generated" : "Generate New Key"), /* @__PURE__ */ import_react3.default.createElement(import_material3.DialogContent, null, newKeyValue ? /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, null, /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "body2", color: "text.secondary", gutterBottom: true }, "Copy this key now. You won't be able to see it again."), /* @__PURE__ */ import_react3.default.createElement(
768
+ import_material3.Box,
587
769
  {
588
770
  display: "flex",
589
771
  alignItems: "center",
@@ -597,8 +779,8 @@ var init_KeysTable = __esm({
597
779
  borderRadius: 1
598
780
  }
599
781
  },
600
- /* @__PURE__ */ import_react2.default.createElement(
601
- import_material2.Typography,
782
+ /* @__PURE__ */ import_react3.default.createElement(
783
+ import_material3.Typography,
602
784
  {
603
785
  component: "code",
604
786
  color: "text.primary",
@@ -606,20 +788,24 @@ var init_KeysTable = __esm({
606
788
  },
607
789
  newKeyValue
608
790
  ),
609
- /* @__PURE__ */ import_react2.default.createElement(import_material2.IconButton, { onClick: () => copyToClipboard(newKeyValue) }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material.ContentCopy, null))
610
- ), newKeySnippets && /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { mt: 3 }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Code, { fontSize: "small", color: "action" }), /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "subtitle2" }, "Start calling the proxy \u2014 paste and run")), /* @__PURE__ */ import_react2.default.createElement(SnippetTabs, { snippets: newKeySnippets, model: newKeyModel, onCopy: copyToClipboard }))) : /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, /* @__PURE__ */ import_react2.default.createElement(
611
- import_material2.TextField,
791
+ /* @__PURE__ */ import_react3.default.createElement(import_material3.IconButton, { onClick: () => copyToClipboard(newKeyValue) }, /* @__PURE__ */ import_react3.default.createElement(import_icons_material3.ContentCopy, null))
792
+ ), newKeySnippets && /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { mt: 3 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ import_react3.default.createElement(import_icons_material3.Code, { fontSize: "small", color: "action" }), /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "subtitle2" }, "Start calling the proxy \u2014 paste and run")), /* @__PURE__ */ import_react3.default.createElement(SnippetTabs, { snippets: newKeySnippets, model: newKeyModel, onCopy: copyToClipboard }))) : /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, generateError && /* @__PURE__ */ import_react3.default.createElement(import_material3.Alert, { severity: "error", onClose: () => setGenerateError(null) }, generateError), /* @__PURE__ */ import_react3.default.createElement(
793
+ import_material3.TextField,
612
794
  {
613
795
  label: "Alias",
614
796
  value: formData.alias || "",
615
- onChange: (e) => setFormData({ ...formData, alias: e.target.value }),
797
+ onChange: (e) => {
798
+ setFormData({ ...formData, alias: e.target.value });
799
+ setGenerateError(null);
800
+ },
616
801
  error: aliasError,
617
- helperText: aliasError ? "Alias is required" : void 0,
802
+ color: aliasDuplicate ? "warning" : void 0,
803
+ 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,
618
804
  required: true,
619
805
  fullWidth: true
620
806
  }
621
- ), /* @__PURE__ */ import_react2.default.createElement(
622
- import_material2.TextField,
807
+ ), /* @__PURE__ */ import_react3.default.createElement(
808
+ import_material3.TextField,
623
809
  {
624
810
  select: true,
625
811
  label: "Duration",
@@ -627,13 +813,13 @@ var init_KeysTable = __esm({
627
813
  onChange: (e) => setFormData({ ...formData, duration: e.target.value }),
628
814
  fullWidth: true
629
815
  },
630
- /* @__PURE__ */ import_react2.default.createElement(import_material2.MenuItem, { value: "1d" }, "1 Day"),
631
- /* @__PURE__ */ import_react2.default.createElement(import_material2.MenuItem, { value: "7d" }, "7 Days"),
632
- /* @__PURE__ */ import_react2.default.createElement(import_material2.MenuItem, { value: "30d" }, "30 Days"),
633
- /* @__PURE__ */ import_react2.default.createElement(import_material2.MenuItem, { value: "90d" }, "90 Days"),
634
- /* @__PURE__ */ import_react2.default.createElement(import_material2.MenuItem, { value: "1y" }, "1 Year")
635
- ), teams.length > 0 ? /* @__PURE__ */ import_react2.default.createElement(
636
- import_material2.Autocomplete,
816
+ /* @__PURE__ */ import_react3.default.createElement(import_material3.MenuItem, { value: "1d" }, "1 Day"),
817
+ /* @__PURE__ */ import_react3.default.createElement(import_material3.MenuItem, { value: "7d" }, "7 Days"),
818
+ /* @__PURE__ */ import_react3.default.createElement(import_material3.MenuItem, { value: "30d" }, "30 Days"),
819
+ /* @__PURE__ */ import_react3.default.createElement(import_material3.MenuItem, { value: "90d" }, "90 Days"),
820
+ /* @__PURE__ */ import_react3.default.createElement(import_material3.MenuItem, { value: "1y" }, "1 Year")
821
+ ), teams.length > 0 ? /* @__PURE__ */ import_react3.default.createElement(
822
+ import_material3.Autocomplete,
637
823
  {
638
824
  options: teams,
639
825
  getOptionLabel: (t) => t.team_alias || t.team_id,
@@ -643,8 +829,8 @@ var init_KeysTable = __esm({
643
829
  const restrictedModels = teamModels && teamModels.length > 0 ? (formData.models || []).filter((m) => teamModels.includes(m)) : formData.models;
644
830
  setFormData({ ...formData, team_id: team?.team_id, models: restrictedModels });
645
831
  },
646
- renderInput: (params) => /* @__PURE__ */ import_react2.default.createElement(
647
- import_material2.TextField,
832
+ renderInput: (params) => /* @__PURE__ */ import_react3.default.createElement(
833
+ import_material3.TextField,
648
834
  {
649
835
  ...params,
650
836
  label: "Team",
@@ -655,8 +841,8 @@ var init_KeysTable = __esm({
655
841
  }
656
842
  )
657
843
  }
658
- ) : teamRequired ? /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { 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__ */ import_react2.default.createElement(
659
- import_material2.Autocomplete,
844
+ ) : teamRequired ? /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { 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__ */ import_react3.default.createElement(
845
+ import_material3.Autocomplete,
660
846
  {
661
847
  multiple: true,
662
848
  options: availableModels,
@@ -664,9 +850,9 @@ var init_KeysTable = __esm({
664
850
  getOptionLabel: (m) => m.model_name,
665
851
  value: selectedModels,
666
852
  onChange: (_e, selected) => setFormData({ ...formData, models: selected.map((m) => m.model_name) }),
667
- renderOption: (props, m) => /* @__PURE__ */ import_react2.default.createElement("li", { ...props }, modelOption(m)),
668
- renderInput: (params) => /* @__PURE__ */ import_react2.default.createElement(
669
- import_material2.TextField,
853
+ renderOption: (props, m) => /* @__PURE__ */ import_react3.default.createElement("li", { ...props }, modelOption(m)),
854
+ renderInput: (params) => /* @__PURE__ */ import_react3.default.createElement(
855
+ import_material3.TextField,
670
856
  {
671
857
  ...params,
672
858
  label: "Models",
@@ -675,11 +861,11 @@ var init_KeysTable = __esm({
675
861
  }
676
862
  )
677
863
  }
678
- ), allowUnlimitedBudget && /* @__PURE__ */ import_react2.default.createElement(
679
- import_material2.FormControlLabel,
864
+ ), allowUnlimitedBudget && /* @__PURE__ */ import_react3.default.createElement(
865
+ import_material3.FormControlLabel,
680
866
  {
681
- control: /* @__PURE__ */ import_react2.default.createElement(
682
- import_material2.Checkbox,
867
+ control: /* @__PURE__ */ import_react3.default.createElement(
868
+ import_material3.Checkbox,
683
869
  {
684
870
  checked: unlimitedBudget,
685
871
  onChange: (e) => {
@@ -694,8 +880,8 @@ var init_KeysTable = __esm({
694
880
  ),
695
881
  label: "Unlimited budget"
696
882
  }
697
- ), /* @__PURE__ */ import_react2.default.createElement(
698
- import_material2.TextField,
883
+ ), /* @__PURE__ */ import_react3.default.createElement(
884
+ import_material3.TextField,
699
885
  {
700
886
  label: "Max Budget (USD)",
701
887
  type: "number",
@@ -707,8 +893,8 @@ var init_KeysTable = __esm({
707
893
  required: true,
708
894
  fullWidth: true
709
895
  }
710
- ), /* @__PURE__ */ import_react2.default.createElement(
711
- import_material2.TextField,
896
+ ), /* @__PURE__ */ import_react3.default.createElement(
897
+ import_material3.TextField,
712
898
  {
713
899
  label: "TPM Limit",
714
900
  type: "number",
@@ -717,8 +903,8 @@ var init_KeysTable = __esm({
717
903
  helperText: "Max tokens per minute this key can consume across all models. Leave blank for no limit.",
718
904
  fullWidth: true
719
905
  }
720
- ), /* @__PURE__ */ import_react2.default.createElement(
721
- import_material2.TextField,
906
+ ), /* @__PURE__ */ import_react3.default.createElement(
907
+ import_material3.TextField,
722
908
  {
723
909
  label: "RPM Limit",
724
910
  type: "number",
@@ -727,125 +913,39 @@ var init_KeysTable = __esm({
727
913
  helperText: "Max requests per minute this key can make across all models. Leave blank for no limit.",
728
914
  fullWidth: true
729
915
  }
730
- ))), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogActions, null, newKeyValue ? /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: handleCloseModal, variant: "contained", color: "success" }, "Done") : /* @__PURE__ */ import_react2.default.createElement(import_react2.default.Fragment, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: handleCloseModal }, "Cancel"), /* @__PURE__ */ import_react2.default.createElement(
731
- import_material2.Button,
916
+ ))), /* @__PURE__ */ import_react3.default.createElement(import_material3.DialogActions, null, newKeyValue ? /* @__PURE__ */ import_react3.default.createElement(import_material3.Button, { onClick: handleClose, variant: "contained", color: "success" }, "Done") : /* @__PURE__ */ import_react3.default.createElement(import_react3.default.Fragment, null, /* @__PURE__ */ import_react3.default.createElement(import_material3.Button, { onClick: handleClose }, "Cancel"), /* @__PURE__ */ import_react3.default.createElement(
917
+ import_material3.Button,
732
918
  {
733
919
  onClick: handleGenerate,
734
920
  variant: "contained",
735
921
  color: "primary",
736
922
  disabled: !canGenerate
737
923
  },
738
- submitting ? /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 24 }) : "Generate"
739
- )))), /* @__PURE__ */ import_react2.default.createElement(import_material2.Dialog, { open: !!editingKey, onClose: handleCloseEdit, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogTitle, null, "Edit Key"), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogContent, null, editingKey && /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2", color: "text.secondary" }, /* @__PURE__ */ import_react2.default.createElement("code", { style: { fontFamily: "monospace", color: "inherit" } }, maskKey(editingKey.key))), /* @__PURE__ */ import_react2.default.createElement(
740
- import_material2.TextField,
741
- {
742
- label: "Alias",
743
- value: editForm.key_alias || "",
744
- onChange: (e) => setEditForm({ ...editForm, key_alias: e.target.value }),
745
- fullWidth: true
746
- }
747
- ), models.length > 0 && /* @__PURE__ */ import_react2.default.createElement(
748
- import_material2.Autocomplete,
749
- {
750
- multiple: true,
751
- options: models,
752
- groupBy: (m) => m.mode || "other",
753
- getOptionLabel: (m) => m.model_name,
754
- value: editSelectedModels,
755
- onChange: (_e, selected) => setEditForm({ ...editForm, models: selected.map((m) => m.model_name) }),
756
- renderOption: (props, m) => /* @__PURE__ */ import_react2.default.createElement("li", { ...props }, modelOption(m)),
757
- renderInput: (params) => /* @__PURE__ */ import_react2.default.createElement(import_material2.TextField, { ...params, label: "Models", fullWidth: true })
758
- }
759
- ), /* @__PURE__ */ import_react2.default.createElement(
760
- import_material2.TextField,
761
- {
762
- label: "Max Budget (USD)",
763
- type: "number",
764
- value: editForm.max_budget ?? "",
765
- onChange: (e) => setEditForm({ ...editForm, max_budget: e.target.value ? Number(e.target.value) : void 0 }),
766
- fullWidth: true
767
- }
768
- ), /* @__PURE__ */ import_react2.default.createElement(
769
- import_material2.TextField,
770
- {
771
- label: "TPM Limit",
772
- type: "number",
773
- value: editForm.tpm_limit ?? "",
774
- onChange: (e) => setEditForm({ ...editForm, tpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
775
- helperText: "Max tokens per minute this key can consume across all models. Leave blank for no limit.",
776
- fullWidth: true
777
- }
778
- ), /* @__PURE__ */ import_react2.default.createElement(
779
- import_material2.TextField,
780
- {
781
- label: "RPM Limit",
782
- type: "number",
783
- value: editForm.rpm_limit ?? "",
784
- onChange: (e) => setEditForm({ ...editForm, rpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
785
- helperText: "Max requests per minute this key can make across all models. Leave blank for no limit.",
786
- fullWidth: true
787
- }
788
- ), /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { mt: 1, pt: 2, borderTop: "1px solid", sx: { borderColor: "divider" } }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "caption", color: "text.secondary", display: "block", mb: 1 }, "Danger Zone"), !resetSpendConfirm ? /* @__PURE__ */ import_react2.default.createElement(
789
- import_material2.Button,
790
- {
791
- size: "small",
792
- color: "warning",
793
- variant: "outlined",
794
- onClick: () => setResetSpendConfirm(true)
795
- },
796
- "Reset Spend to $0"
797
- ) : /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2", color: "warning.main" }, "Zero out spend counter?"), /* @__PURE__ */ import_react2.default.createElement(
798
- import_material2.Button,
799
- {
800
- size: "small",
801
- color: "warning",
802
- variant: "contained",
803
- disabled: resetSpendSubmitting,
804
- onClick: handleResetSpend
805
- },
806
- resetSpendSubmitting ? /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 16 }) : "Confirm"
807
- ), /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { size: "small", onClick: () => setResetSpendConfirm(false) }, "Cancel"))))), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogActions, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: handleCloseEdit }, "Cancel"), /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: handleUpdate, variant: "contained", color: "primary", disabled: editSubmitting }, editSubmitting ? /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 24 }) : "Save"))), /* @__PURE__ */ import_react2.default.createElement(import_material2.Dialog, { open: !!deleteConfirmId, onClose: () => setDeleteConfirmId(null), maxWidth: "xs", fullWidth: true }, /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogTitle, null, "Revoke Key?"), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogContent, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, null, "This will permanently revoke the key. Any integrations using it will stop working immediately.")), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogActions, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: () => setDeleteConfirmId(null), disabled: deleteSubmitting }, "Cancel"), /* @__PURE__ */ import_react2.default.createElement(
808
- import_material2.Button,
809
- {
810
- onClick: handleConfirmDelete,
811
- variant: "contained",
812
- color: "error",
813
- disabled: deleteSubmitting
814
- },
815
- deleteSubmitting ? /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 20 }) : "Revoke"
816
- ))), /* @__PURE__ */ import_react2.default.createElement(import_material2.Dialog, { open: pruneConfirmCount !== null, onClose: () => setPruneConfirmCount(null), maxWidth: "xs", fullWidth: true }, /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogTitle, null, "Prune Expired Keys?"), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogContent, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, null, "This will permanently delete ", pruneConfirmCount, " expired key", pruneConfirmCount !== 1 ? "s" : "", " from LiteLLM. Any integrations using them will stop working immediately.")), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogActions, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: () => setPruneConfirmCount(null), disabled: pruneSubmitting }, "Cancel"), /* @__PURE__ */ import_react2.default.createElement(
817
- import_material2.Button,
818
- {
819
- onClick: handlePruneExpired,
820
- variant: "contained",
821
- color: "error",
822
- disabled: pruneSubmitting
823
- },
824
- pruneSubmitting ? /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 20 }) : "Prune"
924
+ submitting ? /* @__PURE__ */ import_react3.default.createElement(import_material3.CircularProgress, { size: 24 }) : "Generate"
825
925
  ))));
826
926
  };
827
927
  SnippetTabs = ({ snippets, model, onCopy }) => {
828
- const [tab, setTab] = (0, import_react2.useState)("curl");
928
+ const [tab, setTab] = (0, import_react3.useState)("curl");
829
929
  const code = tab === "curl" ? snippets.curl : snippets.openai;
830
- return /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Tabs, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 1 } }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Tab, { label: "curl", value: "curl" }), /* @__PURE__ */ import_react2.default.createElement(import_material2.Tab, { label: "OpenAI SDK", value: "openai" })), /* @__PURE__ */ import_react2.default.createElement(
831
- import_material2.Box,
930
+ return /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, null, /* @__PURE__ */ import_react3.default.createElement(import_material3.Tabs, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 1 } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Tab, { label: "curl", value: "curl" }), /* @__PURE__ */ import_react3.default.createElement(import_material3.Tab, { label: "OpenAI SDK", value: "openai" })), /* @__PURE__ */ import_react3.default.createElement(
931
+ import_material3.Box,
832
932
  {
833
933
  position: "relative",
834
934
  p: 1.5,
835
935
  sx: { backgroundColor: "action.hover", border: "1px solid", borderColor: "divider", borderRadius: 1 }
836
936
  },
837
- /* @__PURE__ */ import_react2.default.createElement(
838
- import_material2.IconButton,
937
+ /* @__PURE__ */ import_react3.default.createElement(
938
+ import_material3.IconButton,
839
939
  {
840
940
  size: "small",
841
941
  onClick: () => onCopy(code),
842
942
  title: "Copy snippet",
843
943
  sx: { position: "absolute", top: 4, right: 4 }
844
944
  },
845
- /* @__PURE__ */ import_react2.default.createElement(import_icons_material.ContentCopy, { fontSize: "small" })
945
+ /* @__PURE__ */ import_react3.default.createElement(import_icons_material3.ContentCopy, { fontSize: "small" })
846
946
  ),
847
- /* @__PURE__ */ import_react2.default.createElement(
848
- import_material2.Typography,
947
+ /* @__PURE__ */ import_react3.default.createElement(
948
+ import_material3.Typography,
849
949
  {
850
950
  component: "pre",
851
951
  sx: {
@@ -859,7 +959,7 @@ var init_KeysTable = __esm({
859
959
  },
860
960
  code
861
961
  ),
862
- model && /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "caption", color: "text.secondary", display: "block", mt: 1 }, "Using model \u201C", model, "\u201D \u2014 swap it for any model you have access to.")
962
+ model && /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "caption", color: "text.secondary", display: "block", mt: 1 }, "Using model \u201C", model, "\u201D \u2014 swap it for any model you have access to.")
863
963
  ));
864
964
  };
865
965
  }
@@ -871,12 +971,12 @@ function modelColor(model) {
871
971
  for (let i = 0; i < model.length; i++) h = (h << 5) + h + model.charCodeAt(i) >>> 0;
872
972
  return MODEL_COLORS[h % MODEL_COLORS.length];
873
973
  }
874
- var import_react3, import_material3, import_recharts, TOP_N_MODELS, PERIOD_LS_KEY, MODEL_COLORS, fmtPct, KpiCard, ChartSkeleton, EmptyChart, UsageStats;
974
+ var import_react4, import_material4, import_recharts, TOP_N_MODELS, PERIOD_LS_KEY, MODEL_COLORS, fmtPct, KpiCard, ChartSkeleton, EmptyChart, UsageStats;
875
975
  var init_UsageStats = __esm({
876
976
  "src/components/UsageStats.tsx"() {
877
977
  "use strict";
878
- import_react3 = __toESM(require("react"));
879
- import_material3 = require("@mui/material");
978
+ import_react4 = __toESM(require("react"));
979
+ import_material4 = require("@mui/material");
880
980
  import_recharts = require("recharts");
881
981
  init_format();
882
982
  TOP_N_MODELS = 6;
@@ -894,12 +994,12 @@ var init_UsageStats = __esm({
894
994
  "#d0ed57"
895
995
  ];
896
996
  fmtPct = (n) => `${(n * 100).toFixed(1)}%`;
897
- KpiCard = ({ label, value, hint }) => /* @__PURE__ */ import_react3.default.createElement(import_material3.Paper, { variant: "outlined", sx: { p: 2, height: "100%" } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "caption", color: "text.secondary" }, label), /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "h5", sx: { mt: 0.5 } }, value), hint ? /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "caption", color: "text.secondary" }, hint) : null);
898
- ChartSkeleton = ({ height = 260 }) => /* @__PURE__ */ import_react3.default.createElement(import_material3.Skeleton, { variant: "rectangular", height, sx: { borderRadius: 1 } });
997
+ KpiCard = ({ label, value, hint }) => /* @__PURE__ */ import_react4.default.createElement(import_material4.Paper, { variant: "outlined", sx: { p: 2, height: "100%" } }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "caption", color: "text.secondary" }, label), /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "h5", sx: { mt: 0.5 } }, value), hint ? /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "caption", color: "text.secondary" }, hint) : null);
998
+ ChartSkeleton = ({ height = 260 }) => /* @__PURE__ */ import_react4.default.createElement(import_material4.Skeleton, { variant: "rectangular", height, sx: { borderRadius: 1 } });
899
999
  EmptyChart = ({
900
1000
  height = 260,
901
1001
  message = "No data for this period"
902
- }) => /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { height, display: "flex", alignItems: "center", justifyContent: "center" }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { color: "text.secondary", variant: "body2" }, message));
1002
+ }) => /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { height, display: "flex", alignItems: "center", justifyContent: "center" }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { color: "text.secondary", variant: "body2" }, message));
903
1003
  UsageStats = ({
904
1004
  usage,
905
1005
  models,
@@ -908,15 +1008,15 @@ var init_UsageStats = __esm({
908
1008
  loading,
909
1009
  userInfo
910
1010
  }) => {
911
- const theme = (0, import_material3.useTheme)();
912
- const [selectedModel, setSelectedModel] = (0, import_react3.useState)("all");
913
- const [tab, setTab] = (0, import_react3.useState)("costs");
1011
+ const theme = (0, import_material4.useTheme)();
1012
+ const [selectedModel, setSelectedModel] = (0, import_react4.useState)("all");
1013
+ const [tab, setTab] = (0, import_react4.useState)("costs");
914
1014
  const gridStroke = theme.palette.divider;
915
1015
  const tickFill = theme.palette.text.secondary;
916
1016
  const tickStyle = { fontSize: 12, fill: tickFill };
917
1017
  const tickStyleSmall = { fontSize: 11, fill: tickFill };
918
1018
  const tickStyleTiny = { fontSize: 10, fill: tickFill };
919
- const selectedPreset = (0, import_react3.useMemo)(() => {
1019
+ const selectedPreset = (0, import_react4.useMemo)(() => {
920
1020
  if (dateRange.start.toDateString() === dateRange.end.toDateString()) return "today";
921
1021
  const diffMs = dateRange.end.getTime() - dateRange.start.getTime();
922
1022
  const diffHours = diffMs / (1e3 * 60 * 60);
@@ -938,7 +1038,7 @@ var init_UsageStats = __esm({
938
1038
  }
939
1039
  onDateRangeChange({ start, end });
940
1040
  };
941
- const dailyData = (0, import_react3.useMemo)(
1041
+ const dailyData = (0, import_react4.useMemo)(
942
1042
  () => (usage?.daily_usage ?? []).map((d) => ({
943
1043
  date: d.date,
944
1044
  spend: d.spend,
@@ -951,21 +1051,21 @@ var init_UsageStats = __esm({
951
1051
  })),
952
1052
  [usage]
953
1053
  );
954
- const cumulativeData = (0, import_react3.useMemo)(() => {
1054
+ const cumulativeData = (0, import_react4.useMemo)(() => {
955
1055
  let cum = 0;
956
1056
  return dailyData.map((d) => {
957
1057
  cum += d.spend;
958
1058
  return { date: d.date, cumulative: cum };
959
1059
  });
960
1060
  }, [dailyData]);
961
- const successRateData = (0, import_react3.useMemo)(
1061
+ const successRateData = (0, import_react4.useMemo)(
962
1062
  () => dailyData.filter((d) => d.apiRequests > 0).map((d) => ({
963
1063
  date: d.date,
964
1064
  successRate: parseFloat((d.successfulRequests / d.apiRequests * 100).toFixed(1))
965
1065
  })),
966
1066
  [dailyData]
967
1067
  );
968
- const { modelSpendByDate, topModels: topSpendModels } = (0, import_react3.useMemo)(() => {
1068
+ const { modelSpendByDate, topModels: topSpendModels } = (0, import_react4.useMemo)(() => {
969
1069
  const rows = usage?.daily_by_model ?? [];
970
1070
  const modelTotals = {};
971
1071
  for (const r of rows) modelTotals[r.model] = (modelTotals[r.model] ?? 0) + r.spend;
@@ -980,7 +1080,7 @@ var init_UsageStats = __esm({
980
1080
  const hasOther = sorted.some((r) => r.Other > 0);
981
1081
  return { modelSpendByDate: sorted, topModels: hasOther ? [...top, "Other"] : top };
982
1082
  }, [usage]);
983
- const modelRows = (0, import_react3.useMemo)(() => {
1083
+ const modelRows = (0, import_react4.useMemo)(() => {
984
1084
  const entries = Object.entries(usage?.usage_by_model ?? {}).map(([model, d]) => ({
985
1085
  model,
986
1086
  spend: d.total_spend,
@@ -994,11 +1094,11 @@ var init_UsageStats = __esm({
994
1094
  }));
995
1095
  return selectedModel === "all" ? entries : entries.filter((e) => e.model === selectedModel);
996
1096
  }, [usage, selectedModel]);
997
- const topModelSpendBars = (0, import_react3.useMemo)(
1097
+ const topModelSpendBars = (0, import_react4.useMemo)(
998
1098
  () => [...modelRows].sort((a, b) => b.spend - a.spend).slice(0, 10),
999
1099
  [modelRows]
1000
1100
  );
1001
- const keyRows = (0, import_react3.useMemo)(
1101
+ const keyRows = (0, import_react4.useMemo)(
1002
1102
  () => Object.entries(usage?.usage_by_key ?? {}).map(([keyHash, d]) => ({
1003
1103
  keyHash,
1004
1104
  keyAlias: d.key_alias ?? keyHash.slice(0, 8),
@@ -1015,7 +1115,7 @@ var init_UsageStats = __esm({
1015
1115
  })),
1016
1116
  [usage]
1017
1117
  );
1018
- const topKeySpendBars = (0, import_react3.useMemo)(
1118
+ const topKeySpendBars = (0, import_react4.useMemo)(
1019
1119
  () => [...keyRows].sort((a, b) => b.spend - a.spend).slice(0, 10),
1020
1120
  [keyRows]
1021
1121
  );
@@ -1023,34 +1123,34 @@ var init_UsageStats = __esm({
1023
1123
  const overallSuccessRate = totalRequests > 0 ? (usage?.successful_requests ?? 0) / totalRequests : 0;
1024
1124
  const maxBudget = userInfo?.max_budget ?? 0;
1025
1125
  const totalCumSpend = cumulativeData[cumulativeData.length - 1]?.cumulative ?? 0;
1026
- return /* @__PURE__ */ import_react3.default.createElement(import_material3.Paper, { sx: { p: 2 } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2, flexWrap: "wrap", gap: 2 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "h6" }, "Usage Analytics"), /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { display: "flex", gap: 2, alignItems: "center", flexWrap: "wrap" }, /* @__PURE__ */ import_react3.default.createElement(import_material3.FormControl, { size: "small", sx: { minWidth: 140 } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.InputLabel, null, "Period"), /* @__PURE__ */ import_react3.default.createElement(
1027
- import_material3.Select,
1126
+ return /* @__PURE__ */ import_react4.default.createElement(import_material4.Paper, { sx: { p: 2 } }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2, flexWrap: "wrap", gap: 2 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "h6" }, "Usage Analytics"), /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", gap: 2, alignItems: "center", flexWrap: "wrap" }, /* @__PURE__ */ import_react4.default.createElement(import_material4.FormControl, { size: "small", sx: { minWidth: 140 } }, /* @__PURE__ */ import_react4.default.createElement(import_material4.InputLabel, null, "Period"), /* @__PURE__ */ import_react4.default.createElement(
1127
+ import_material4.Select,
1028
1128
  {
1029
1129
  value: selectedPreset,
1030
1130
  label: "Period",
1031
1131
  onChange: (e) => handlePresetChange(e.target.value)
1032
1132
  },
1033
- /* @__PURE__ */ import_react3.default.createElement(import_material3.MenuItem, { value: "today" }, "Today"),
1034
- /* @__PURE__ */ import_react3.default.createElement(import_material3.MenuItem, { value: "24h" }, "Last 24h"),
1035
- /* @__PURE__ */ import_react3.default.createElement(import_material3.MenuItem, { value: "7d" }, "Last 7 days"),
1036
- /* @__PURE__ */ import_react3.default.createElement(import_material3.MenuItem, { value: "30d" }, "Last 30 days")
1037
- )), tab === "models" && /* @__PURE__ */ import_react3.default.createElement(import_material3.FormControl, { size: "small", sx: { minWidth: 180 } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.InputLabel, null, "Model"), /* @__PURE__ */ import_react3.default.createElement(
1038
- import_material3.Select,
1133
+ /* @__PURE__ */ import_react4.default.createElement(import_material4.MenuItem, { value: "today" }, "Today"),
1134
+ /* @__PURE__ */ import_react4.default.createElement(import_material4.MenuItem, { value: "24h" }, "Last 24h"),
1135
+ /* @__PURE__ */ import_react4.default.createElement(import_material4.MenuItem, { value: "7d" }, "Last 7 days"),
1136
+ /* @__PURE__ */ import_react4.default.createElement(import_material4.MenuItem, { value: "30d" }, "Last 30 days")
1137
+ )), tab === "models" && /* @__PURE__ */ import_react4.default.createElement(import_material4.FormControl, { size: "small", sx: { minWidth: 180 } }, /* @__PURE__ */ import_react4.default.createElement(import_material4.InputLabel, null, "Model"), /* @__PURE__ */ import_react4.default.createElement(
1138
+ import_material4.Select,
1039
1139
  {
1040
1140
  value: selectedModel,
1041
1141
  label: "Model",
1042
1142
  onChange: (e) => setSelectedModel(e.target.value)
1043
1143
  },
1044
- /* @__PURE__ */ import_react3.default.createElement(import_material3.MenuItem, { value: "all" }, "All Models"),
1045
- models.map((m) => /* @__PURE__ */ import_react3.default.createElement(import_material3.MenuItem, { key: m.model_name, value: m.model_name }, m.model_name))
1046
- )))), /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { container: true, spacing: 2, sx: { mb: 2 } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { item: true, xs: 6, sm: 3 }, loading ? /* @__PURE__ */ import_react3.default.createElement(ChartSkeleton, { height: 80 }) : /* @__PURE__ */ import_react3.default.createElement(KpiCard, { label: "Total Spend", value: fmtUsd(usage?.total_spend ?? 0) })), /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { item: true, xs: 6, sm: 3 }, loading ? /* @__PURE__ */ import_react3.default.createElement(ChartSkeleton, { height: 80 }) : /* @__PURE__ */ import_react3.default.createElement(KpiCard, { label: "Total Requests", value: fmtInt(totalRequests), hint: `${fmtInt(usage?.failed_requests ?? 0)} failed` })), /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { item: true, xs: 6, sm: 3 }, loading ? /* @__PURE__ */ import_react3.default.createElement(ChartSkeleton, { height: 80 }) : /* @__PURE__ */ import_react3.default.createElement(KpiCard, { label: "Success Rate", value: totalRequests > 0 ? fmtPct(overallSuccessRate) : "\u2014" })), /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { item: true, xs: 6, sm: 3 }, loading ? /* @__PURE__ */ import_react3.default.createElement(ChartSkeleton, { height: 80 }) : /* @__PURE__ */ import_react3.default.createElement(
1144
+ /* @__PURE__ */ import_react4.default.createElement(import_material4.MenuItem, { value: "all" }, "All Models"),
1145
+ models.map((m) => /* @__PURE__ */ import_react4.default.createElement(import_material4.MenuItem, { key: m.model_name, value: m.model_name }, m.model_name))
1146
+ )))), /* @__PURE__ */ import_react4.default.createElement(import_material4.Grid, { container: true, spacing: 2, sx: { mb: 2 } }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Grid, { item: true, xs: 6, sm: 3 }, loading ? /* @__PURE__ */ import_react4.default.createElement(ChartSkeleton, { height: 80 }) : /* @__PURE__ */ import_react4.default.createElement(KpiCard, { label: "Total Spend", value: fmtUsd(usage?.total_spend ?? 0) })), /* @__PURE__ */ import_react4.default.createElement(import_material4.Grid, { item: true, xs: 6, sm: 3 }, loading ? /* @__PURE__ */ import_react4.default.createElement(ChartSkeleton, { height: 80 }) : /* @__PURE__ */ import_react4.default.createElement(KpiCard, { label: "Total Requests", value: fmtInt(totalRequests), hint: `${fmtInt(usage?.failed_requests ?? 0)} failed` })), /* @__PURE__ */ import_react4.default.createElement(import_material4.Grid, { item: true, xs: 6, sm: 3 }, loading ? /* @__PURE__ */ import_react4.default.createElement(ChartSkeleton, { height: 80 }) : /* @__PURE__ */ import_react4.default.createElement(KpiCard, { label: "Success Rate", value: totalRequests > 0 ? fmtPct(overallSuccessRate) : "\u2014" })), /* @__PURE__ */ import_react4.default.createElement(import_material4.Grid, { item: true, xs: 6, sm: 3 }, loading ? /* @__PURE__ */ import_react4.default.createElement(ChartSkeleton, { height: 80 }) : /* @__PURE__ */ import_react4.default.createElement(
1047
1147
  KpiCard,
1048
1148
  {
1049
1149
  label: "Total Tokens",
1050
1150
  value: fmtInt(usage?.total_tokens ?? 0),
1051
1151
  hint: `${fmtInt(usage?.prompt_tokens ?? 0)} in \xB7 ${fmtInt(usage?.completion_tokens ?? 0)} out`
1052
1152
  }
1053
- ))), /* @__PURE__ */ import_react3.default.createElement(import_material3.Tabs, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 2 } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Tab, { value: "costs", label: "Costs" }), /* @__PURE__ */ import_react3.default.createElement(import_material3.Tab, { value: "models", label: "Model Activity" }), /* @__PURE__ */ import_react3.default.createElement(import_material3.Tab, { value: "keys", label: "Key Activity" })), tab === "costs" && /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { container: true, spacing: 3 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend by Model"), loading ? /* @__PURE__ */ import_react3.default.createElement(ChartSkeleton, null) : modelSpendByDate.length === 0 ? /* @__PURE__ */ import_react3.default.createElement(EmptyChart, null) : /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { height: 260 }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.AreaChart, { data: modelSpendByDate }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.YAxis, { tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Tooltip, { formatter: (v) => [`$${v.toFixed(4)}`, void 0] }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Legend, null), topSpendModels.map((m) => /* @__PURE__ */ import_react3.default.createElement(
1153
+ ))), /* @__PURE__ */ import_react4.default.createElement(import_material4.Tabs, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 2 } }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Tab, { value: "costs", label: "Costs" }), /* @__PURE__ */ import_react4.default.createElement(import_material4.Tab, { value: "models", label: "Model Activity" }), /* @__PURE__ */ import_react4.default.createElement(import_material4.Tab, { value: "keys", label: "Key Activity" })), tab === "costs" && /* @__PURE__ */ import_react4.default.createElement(import_material4.Grid, { container: true, spacing: 3 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend by Model"), loading ? /* @__PURE__ */ import_react4.default.createElement(ChartSkeleton, null) : modelSpendByDate.length === 0 ? /* @__PURE__ */ import_react4.default.createElement(EmptyChart, null) : /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { height: 260 }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.AreaChart, { data: modelSpendByDate }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.YAxis, { tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Tooltip, { formatter: (v) => [`$${v.toFixed(4)}`, void 0] }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Legend, null), topSpendModels.map((m) => /* @__PURE__ */ import_react4.default.createElement(
1054
1154
  import_recharts.Area,
1055
1155
  {
1056
1156
  key: m,
@@ -1061,38 +1161,38 @@ var init_UsageStats = __esm({
1061
1161
  fill: modelColor(m),
1062
1162
  fillOpacity: 0.6
1063
1163
  }
1064
- )))))), /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Token Usage"), loading ? /* @__PURE__ */ import_react3.default.createElement(ChartSkeleton, null) : dailyData.length === 0 ? /* @__PURE__ */ import_react3.default.createElement(EmptyChart, null) : /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { height: 260 }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.AreaChart, { data: dailyData }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.YAxis, { tick: tickStyle, tickFormatter: fmtInt }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Tooltip, { formatter: (v) => [fmtInt(v), void 0] }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Legend, null), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Area, { type: "monotone", dataKey: "promptTokens", name: "Input (prompt)", stackId: "tok", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.5 }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Area, { type: "monotone", dataKey: "completionTokens", name: "Output (completion)", stackId: "tok", stroke: "#82ca9d", fill: "#82ca9d", fillOpacity: 0.5 }))))), /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Requests"), loading ? /* @__PURE__ */ import_react3.default.createElement(ChartSkeleton, null) : dailyData.length === 0 ? /* @__PURE__ */ import_react3.default.createElement(EmptyChart, null) : /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { height: 260 }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.BarChart, { data: dailyData }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.YAxis, { tick: tickStyle }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Tooltip, null), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Legend, null), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Bar, { dataKey: "successfulRequests", name: "Successful", fill: "#82ca9d", stackId: "r" }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Bar, { dataKey: "failedRequests", name: "Failed", fill: "#e57373", stackId: "r" }))))), /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Success Rate"), loading ? /* @__PURE__ */ import_react3.default.createElement(ChartSkeleton, null) : successRateData.length === 0 ? /* @__PURE__ */ import_react3.default.createElement(EmptyChart, null) : /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { height: 260 }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.LineChart, { data: successRateData }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.YAxis, { domain: [0, 100], tick: tickStyle, tickFormatter: (v) => `${v}%` }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Tooltip, { formatter: (v) => [`${v}%`, "Success rate"] }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.ReferenceLine, { y: 100, stroke: "#82ca9d", strokeDasharray: "4 2" }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Line, { type: "monotone", dataKey: "successRate", name: "Success rate", stroke: "#8884d8", dot: { r: 3 } }))))), (maxBudget > 0 || cumulativeData.length > 0) && /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Cumulative Spend", maxBudget > 0 ? ` vs Budget (${fmtUsd(maxBudget)})` : "", maxBudget > 0 && /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { component: "span", variant: "caption", color: "text.secondary", sx: { ml: 1 } }, fmtUsd(totalCumSpend), " used \xB7 ", fmtUsd(Math.max(0, maxBudget - totalCumSpend)), " remaining")), loading ? /* @__PURE__ */ import_react3.default.createElement(ChartSkeleton, { height: 180 }) : cumulativeData.length === 0 ? /* @__PURE__ */ import_react3.default.createElement(EmptyChart, { height: 180 }) : /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { height: 180 }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.AreaChart, { data: cumulativeData }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.YAxis, { tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Tooltip, { formatter: (v) => [`$${v.toFixed(4)}`, "Cumulative spend"] }), maxBudget > 0 && /* @__PURE__ */ import_react3.default.createElement(import_recharts.ReferenceLine, { y: maxBudget, stroke: "#e57373", strokeDasharray: "6 3", label: { value: `Budget ${fmtUsd(maxBudget)}`, position: "insideTopRight", fontSize: 11 } }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Area, { type: "monotone", dataKey: "cumulative", name: "Cumulative spend", stroke: "#ffc658", fill: "#ffc658", fillOpacity: 0.3 })))))), tab === "models" && /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { container: true, spacing: 3 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Spend by Model"), loading ? /* @__PURE__ */ import_react3.default.createElement(ChartSkeleton, null) : topModelSpendBars.length === 0 ? /* @__PURE__ */ import_react3.default.createElement(EmptyChart, null) : /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { height: Math.max(200, topModelSpendBars.length * 36) }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.BarChart, { data: topModelSpendBars, layout: "vertical", margin: { left: 20 } }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.XAxis, { type: "number", tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.YAxis, { type: "category", dataKey: "model", tick: tickStyleSmall, width: 200 }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Tooltip, { formatter: (v) => [fmtUsd(v), "Spend"] }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Bar, { dataKey: "spend", name: "Spend", radius: [0, 4, 4, 0] }, topModelSpendBars.map((r) => /* @__PURE__ */ import_react3.default.createElement("rect", { key: r.model, fill: modelColor(r.model) }))))))), /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Tokens by Model"), loading ? /* @__PURE__ */ import_react3.default.createElement(ChartSkeleton, null) : modelRows.length === 0 ? /* @__PURE__ */ import_react3.default.createElement(EmptyChart, null) : /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { height: 260 }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.BarChart, { data: modelRows }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.XAxis, { dataKey: "model", tick: tickStyleTiny, interval: 0, angle: -15, textAnchor: "end", height: 60 }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.YAxis, { tick: tickStyle }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Tooltip, null), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Legend, null), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Bar, { dataKey: "promptTokens", name: "Prompt", fill: "#8884d8", stackId: "t" }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Bar, { dataKey: "completionTokens", name: "Completion", fill: "#82ca9d", stackId: "t" }))))), /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.TableContainer, { component: import_material3.Paper, variant: "outlined" }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Table, { size: "small" }, /* @__PURE__ */ import_react3.default.createElement(import_material3.TableHead, null, /* @__PURE__ */ import_react3.default.createElement(import_material3.TableRow, null, /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, null, "Model"), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, "Spend"), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, "Requests"), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, "Success"), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, "Failed"), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, "Prompt"), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, "Completion"), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { sx: { minWidth: 120 } }, "Success rate"))), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableBody, null, loading ? /* @__PURE__ */ import_react3.default.createElement(import_material3.TableRow, null, /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { colSpan: 8 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.LinearProgress, null))) : modelRows.length === 0 ? /* @__PURE__ */ import_react3.default.createElement(import_material3.TableRow, null, /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { colSpan: 8, align: "center" }, "No model activity")) : [...modelRows].sort((a, b) => b.spend - a.spend || b.totalTokens - a.totalTokens).map((r) => /* @__PURE__ */ import_react3.default.createElement(import_material3.TableRow, { key: r.model }, /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, null, /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { display: "flex", alignItems: "center", gap: 0.75 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { sx: { width: 10, height: 10, borderRadius: "50%", bgcolor: modelColor(r.model), flexShrink: 0 } }), r.model)), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, fmtUsd(r.spend)), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, fmtInt(r.apiRequests)), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, fmtInt(r.successfulRequests)), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, fmtInt(r.failedRequests)), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, fmtInt(r.promptTokens)), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, fmtInt(r.completionTokens)), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, null, r.apiRequests > 0 ? /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ import_react3.default.createElement(
1065
- import_material3.LinearProgress,
1164
+ )))))), /* @__PURE__ */ import_react4.default.createElement(import_material4.Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Token Usage"), loading ? /* @__PURE__ */ import_react4.default.createElement(ChartSkeleton, null) : dailyData.length === 0 ? /* @__PURE__ */ import_react4.default.createElement(EmptyChart, null) : /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { height: 260 }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.AreaChart, { data: dailyData }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.YAxis, { tick: tickStyle, tickFormatter: fmtInt }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Tooltip, { formatter: (v) => [fmtInt(v), void 0] }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Legend, null), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Area, { type: "monotone", dataKey: "promptTokens", name: "Input (prompt)", stackId: "tok", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.5 }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Area, { type: "monotone", dataKey: "completionTokens", name: "Output (completion)", stackId: "tok", stroke: "#82ca9d", fill: "#82ca9d", fillOpacity: 0.5 }))))), /* @__PURE__ */ import_react4.default.createElement(import_material4.Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Requests"), loading ? /* @__PURE__ */ import_react4.default.createElement(ChartSkeleton, null) : dailyData.length === 0 ? /* @__PURE__ */ import_react4.default.createElement(EmptyChart, null) : /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { height: 260 }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.BarChart, { data: dailyData }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.YAxis, { tick: tickStyle }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Tooltip, null), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Legend, null), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Bar, { dataKey: "successfulRequests", name: "Successful", fill: "#82ca9d", stackId: "r" }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Bar, { dataKey: "failedRequests", name: "Failed", fill: "#e57373", stackId: "r" }))))), /* @__PURE__ */ import_react4.default.createElement(import_material4.Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Success Rate"), loading ? /* @__PURE__ */ import_react4.default.createElement(ChartSkeleton, null) : successRateData.length === 0 ? /* @__PURE__ */ import_react4.default.createElement(EmptyChart, null) : /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { height: 260 }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.LineChart, { data: successRateData }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.YAxis, { domain: [0, 100], tick: tickStyle, tickFormatter: (v) => `${v}%` }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Tooltip, { formatter: (v) => [`${v}%`, "Success rate"] }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.ReferenceLine, { y: 100, stroke: "#82ca9d", strokeDasharray: "4 2" }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Line, { type: "monotone", dataKey: "successRate", name: "Success rate", stroke: "#8884d8", dot: { r: 3 } }))))), (maxBudget > 0 || cumulativeData.length > 0) && /* @__PURE__ */ import_react4.default.createElement(import_material4.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Cumulative Spend", maxBudget > 0 ? ` vs Budget (${fmtUsd(maxBudget)})` : "", maxBudget > 0 && /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { component: "span", variant: "caption", color: "text.secondary", sx: { ml: 1 } }, fmtUsd(totalCumSpend), " used \xB7 ", fmtUsd(Math.max(0, maxBudget - totalCumSpend)), " remaining")), loading ? /* @__PURE__ */ import_react4.default.createElement(ChartSkeleton, { height: 180 }) : cumulativeData.length === 0 ? /* @__PURE__ */ import_react4.default.createElement(EmptyChart, { height: 180 }) : /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { height: 180 }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.AreaChart, { data: cumulativeData }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.YAxis, { tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Tooltip, { formatter: (v) => [`$${v.toFixed(4)}`, "Cumulative spend"] }), maxBudget > 0 && /* @__PURE__ */ import_react4.default.createElement(import_recharts.ReferenceLine, { y: maxBudget, stroke: "#e57373", strokeDasharray: "6 3", label: { value: `Budget ${fmtUsd(maxBudget)}`, position: "insideTopRight", fontSize: 11 } }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Area, { type: "monotone", dataKey: "cumulative", name: "Cumulative spend", stroke: "#ffc658", fill: "#ffc658", fillOpacity: 0.3 })))))), tab === "models" && /* @__PURE__ */ import_react4.default.createElement(import_material4.Grid, { container: true, spacing: 3 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Spend by Model"), loading ? /* @__PURE__ */ import_react4.default.createElement(ChartSkeleton, null) : topModelSpendBars.length === 0 ? /* @__PURE__ */ import_react4.default.createElement(EmptyChart, null) : /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { height: Math.max(200, topModelSpendBars.length * 36) }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.BarChart, { data: topModelSpendBars, layout: "vertical", margin: { left: 20 } }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.XAxis, { type: "number", tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.YAxis, { type: "category", dataKey: "model", tick: tickStyleSmall, width: 200 }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Tooltip, { formatter: (v) => [fmtUsd(v), "Spend"] }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Bar, { dataKey: "spend", name: "Spend", radius: [0, 4, 4, 0] }, topModelSpendBars.map((r) => /* @__PURE__ */ import_react4.default.createElement("rect", { key: r.model, fill: modelColor(r.model) }))))))), /* @__PURE__ */ import_react4.default.createElement(import_material4.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Tokens by Model"), loading ? /* @__PURE__ */ import_react4.default.createElement(ChartSkeleton, null) : modelRows.length === 0 ? /* @__PURE__ */ import_react4.default.createElement(EmptyChart, null) : /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { height: 260 }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.BarChart, { data: modelRows }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.XAxis, { dataKey: "model", tick: tickStyleTiny, interval: 0, angle: -15, textAnchor: "end", height: 60 }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.YAxis, { tick: tickStyle }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Tooltip, null), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Legend, null), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Bar, { dataKey: "promptTokens", name: "Prompt", fill: "#8884d8", stackId: "t" }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Bar, { dataKey: "completionTokens", name: "Completion", fill: "#82ca9d", stackId: "t" }))))), /* @__PURE__ */ import_react4.default.createElement(import_material4.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableContainer, { component: import_material4.Paper, variant: "outlined" }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Table, { size: "small" }, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableHead, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableRow, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, "Model"), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { align: "right" }, "Spend"), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { align: "right" }, "Requests"), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { align: "right" }, "Success"), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { align: "right" }, "Failed"), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { align: "right" }, "Prompt"), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { align: "right" }, "Completion"), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { sx: { minWidth: 120 } }, "Success rate"))), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableBody, null, loading ? /* @__PURE__ */ import_react4.default.createElement(import_material4.TableRow, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { colSpan: 8 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.LinearProgress, null))) : modelRows.length === 0 ? /* @__PURE__ */ import_react4.default.createElement(import_material4.TableRow, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { colSpan: 8, align: "center" }, "No model activity")) : [...modelRows].sort((a, b) => b.spend - a.spend || b.totalTokens - a.totalTokens).map((r) => /* @__PURE__ */ import_react4.default.createElement(import_material4.TableRow, { key: r.model }, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", alignItems: "center", gap: 0.75 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { sx: { width: 10, height: 10, borderRadius: "50%", bgcolor: modelColor(r.model), flexShrink: 0 } }), r.model)), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { align: "right" }, fmtUsd(r.spend)), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { align: "right" }, fmtInt(r.apiRequests)), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { align: "right" }, fmtInt(r.successfulRequests)), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { align: "right" }, fmtInt(r.failedRequests)), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { align: "right" }, fmtInt(r.promptTokens)), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { align: "right" }, fmtInt(r.completionTokens)), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, r.apiRequests > 0 ? /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ import_react4.default.createElement(
1165
+ import_material4.LinearProgress,
1066
1166
  {
1067
1167
  variant: "determinate",
1068
1168
  value: r.successRate * 100,
1069
1169
  sx: { flex: 1, height: 6, borderRadius: 3 }
1070
1170
  }
1071
- ), /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "caption" }, fmtPct(r.successRate))) : "\u2014")))))))), tab === "keys" && /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { container: true, spacing: 3 }, (loading || topKeySpendBars.length > 0) && /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Spend by Key"), loading ? /* @__PURE__ */ import_react3.default.createElement(ChartSkeleton, null) : /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { height: Math.max(160, topKeySpendBars.length * 36) }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.BarChart, { data: topKeySpendBars, layout: "vertical", margin: { left: 20 } }, /* @__PURE__ */ import_react3.default.createElement(import_recharts.CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.XAxis, { type: "number", tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.YAxis, { type: "category", dataKey: "keyAlias", tick: tickStyleSmall, width: 160 }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Tooltip, { formatter: (v) => [fmtUsd(v), "Spend"] }), /* @__PURE__ */ import_react3.default.createElement(import_recharts.Bar, { dataKey: "spend", name: "Spend", fill: "#8884d8", radius: [0, 4, 4, 0] }))))), /* @__PURE__ */ import_react3.default.createElement(import_material3.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.TableContainer, { component: import_material3.Paper, variant: "outlined" }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Table, { size: "small" }, /* @__PURE__ */ import_react3.default.createElement(import_material3.TableHead, null, /* @__PURE__ */ import_react3.default.createElement(import_material3.TableRow, null, /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, null, "Key"), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, null, "Models"), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, "Spend"), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, "Requests"), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, "Tokens"), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, "Failed"), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { sx: { minWidth: 120 } }, "Success rate"))), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableBody, null, loading ? /* @__PURE__ */ import_react3.default.createElement(import_material3.TableRow, null, /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { colSpan: 7 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.LinearProgress, null))) : keyRows.length === 0 ? /* @__PURE__ */ import_react3.default.createElement(import_material3.TableRow, null, /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { colSpan: 7, align: "center" }, "No key activity")) : [...keyRows].sort((a, b) => b.spend - a.spend || b.apiRequests - a.apiRequests).map((r) => /* @__PURE__ */ import_react3.default.createElement(import_material3.TableRow, { key: r.keyHash }, /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, null, /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "body2" }, r.keyAlias), r.teamId ? /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "caption", color: "text.secondary" }, "team: ", r.teamId) : null), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, null, /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { display: "flex", gap: 0.5, flexWrap: "wrap" }, r.models.map((m) => /* @__PURE__ */ import_react3.default.createElement(import_material3.Chip, { key: m, label: m, size: "small", variant: "outlined" })))), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, fmtUsd(r.spend)), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, fmtInt(r.apiRequests)), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, fmtInt(r.totalTokens)), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, { align: "right" }, fmtInt(r.failedRequests)), /* @__PURE__ */ import_react3.default.createElement(import_material3.TableCell, null, r.apiRequests > 0 ? /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ import_react3.default.createElement(
1072
- import_material3.LinearProgress,
1171
+ ), /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "caption" }, fmtPct(r.successRate))) : "\u2014")))))))), tab === "keys" && /* @__PURE__ */ import_react4.default.createElement(import_material4.Grid, { container: true, spacing: 3 }, (loading || topKeySpendBars.length > 0) && /* @__PURE__ */ import_react4.default.createElement(import_material4.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Spend by Key"), loading ? /* @__PURE__ */ import_react4.default.createElement(ChartSkeleton, null) : /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { height: Math.max(160, topKeySpendBars.length * 36) }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.BarChart, { data: topKeySpendBars, layout: "vertical", margin: { left: 20 } }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.XAxis, { type: "number", tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.YAxis, { type: "category", dataKey: "keyAlias", tick: tickStyleSmall, width: 160 }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Tooltip, { formatter: (v) => [fmtUsd(v), "Spend"] }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Bar, { dataKey: "spend", name: "Spend", fill: "#8884d8", radius: [0, 4, 4, 0] }))))), /* @__PURE__ */ import_react4.default.createElement(import_material4.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableContainer, { component: import_material4.Paper, variant: "outlined" }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Table, { size: "small" }, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableHead, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableRow, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, "Key"), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, "Models"), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { align: "right" }, "Spend"), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { align: "right" }, "Requests"), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { align: "right" }, "Tokens"), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { align: "right" }, "Failed"), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { sx: { minWidth: 120 } }, "Success rate"))), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableBody, null, loading ? /* @__PURE__ */ import_react4.default.createElement(import_material4.TableRow, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { colSpan: 7 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.LinearProgress, null))) : keyRows.length === 0 ? /* @__PURE__ */ import_react4.default.createElement(import_material4.TableRow, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { colSpan: 7, align: "center" }, "No key activity")) : [...keyRows].sort((a, b) => b.spend - a.spend || b.apiRequests - a.apiRequests).map((r) => /* @__PURE__ */ import_react4.default.createElement(import_material4.TableRow, { key: r.keyHash }, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "body2" }, r.keyAlias), r.teamId ? /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "caption", color: "text.secondary" }, "team: ", r.teamId) : null), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", gap: 0.5, flexWrap: "wrap" }, r.models.map((m) => /* @__PURE__ */ import_react4.default.createElement(import_material4.Chip, { key: m, label: m, size: "small", variant: "outlined" })))), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { align: "right" }, fmtUsd(r.spend)), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { align: "right" }, fmtInt(r.apiRequests)), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { align: "right" }, fmtInt(r.totalTokens)), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { align: "right" }, fmtInt(r.failedRequests)), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, r.apiRequests > 0 ? /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ import_react4.default.createElement(
1172
+ import_material4.LinearProgress,
1073
1173
  {
1074
1174
  variant: "determinate",
1075
1175
  value: r.successRate * 100,
1076
1176
  sx: { flex: 1, height: 6, borderRadius: 3 }
1077
1177
  }
1078
- ), /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "caption" }, fmtPct(r.successRate))) : "\u2014")))))))));
1178
+ ), /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "caption" }, fmtPct(r.successRate))) : "\u2014")))))))));
1079
1179
  };
1080
1180
  }
1081
1181
  });
1082
1182
 
1083
1183
  // src/components/TeamUsage.tsx
1084
- var import_react4, import_material4, import_icons_material2, import_recharts2, Stat, TeamCard, TeamUsage;
1184
+ var import_react5, import_material5, import_icons_material4, import_recharts2, Stat, TeamCard, TeamUsage;
1085
1185
  var init_TeamUsage = __esm({
1086
1186
  "src/components/TeamUsage.tsx"() {
1087
1187
  "use strict";
1088
- import_react4 = __toESM(require("react"));
1089
- import_material4 = require("@mui/material");
1090
- import_icons_material2 = require("@mui/icons-material");
1188
+ import_react5 = __toESM(require("react"));
1189
+ import_material5 = require("@mui/material");
1190
+ import_icons_material4 = require("@mui/icons-material");
1091
1191
  import_recharts2 = require("recharts");
1092
- Stat = ({ label, value }) => /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "body2", fontWeight: 600, sx: { fontFamily: "monospace" } }, value));
1192
+ Stat = ({ label, value }) => /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "body2", fontWeight: 600, sx: { fontFamily: "monospace" } }, value));
1093
1193
  TeamCard = ({ team, usage, usageLoading }) => {
1094
- const [expanded, setExpanded] = (0, import_react4.useState)(false);
1095
- const theme = (0, import_material4.useTheme)();
1194
+ const [expanded, setExpanded] = (0, import_react5.useState)(false);
1195
+ const theme = (0, import_material5.useTheme)();
1096
1196
  const gridStroke = theme.palette.divider;
1097
1197
  const tickFill = theme.palette.text.secondary;
1098
1198
  const budget = team.max_budget ?? 0;
@@ -1101,66 +1201,66 @@ var init_TeamUsage = __esm({
1101
1201
  const isOver = budget > 0 && spend >= budget;
1102
1202
  const isNear = budget > 0 && spend >= budget * 0.8 && !isOver;
1103
1203
  const dailyData = usage?.daily_usage?.map((d) => ({ date: d.date, spend: d.spend })) ?? [];
1104
- return /* @__PURE__ */ import_react4.default.createElement(import_material4.Paper, { variant: "outlined", sx: { p: 2, mb: 2 } }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_icons_material2.Group, { color: "action" }), /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { flexGrow: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle1", fontWeight: 600 }, team.team_alias || "Untitled team"), /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "caption", color: "text.secondary", sx: { fontFamily: "monospace" } }, team.team_id)), /* @__PURE__ */ import_react4.default.createElement(import_material4.IconButton, { size: "small", onClick: () => setExpanded((e) => !e), "aria-label": "toggle details" }, expanded ? /* @__PURE__ */ import_react4.default.createElement(import_icons_material2.ExpandLess, null) : /* @__PURE__ */ import_react4.default.createElement(import_icons_material2.ExpandMore, null))), /* @__PURE__ */ import_react4.default.createElement(import_material4.Stack, { direction: "row", spacing: 3, mt: 1.5, flexWrap: "wrap", useFlexGap: true, gap: 1.5 }, /* @__PURE__ */ import_react4.default.createElement(
1204
+ return /* @__PURE__ */ import_react5.default.createElement(import_material5.Paper, { variant: "outlined", sx: { p: 2, mb: 2 } }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ import_react5.default.createElement(import_icons_material4.Group, { color: "action" }), /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { flexGrow: 1 }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "subtitle1", fontWeight: 600 }, team.team_alias || "Untitled team"), /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "caption", color: "text.secondary", sx: { fontFamily: "monospace" } }, team.team_id)), /* @__PURE__ */ import_react5.default.createElement(import_material5.IconButton, { size: "small", onClick: () => setExpanded((e) => !e), "aria-label": "toggle details" }, expanded ? /* @__PURE__ */ import_react5.default.createElement(import_icons_material4.ExpandLess, null) : /* @__PURE__ */ import_react5.default.createElement(import_icons_material4.ExpandMore, null))), /* @__PURE__ */ import_react5.default.createElement(import_material5.Stack, { direction: "row", spacing: 3, mt: 1.5, flexWrap: "wrap", useFlexGap: true, gap: 1.5 }, /* @__PURE__ */ import_react5.default.createElement(
1105
1205
  Stat,
1106
1206
  {
1107
1207
  label: "Members",
1108
1208
  value: team.members_with_roles?.length ? String(team.members_with_roles.length) : "\u2014"
1109
1209
  }
1110
- ), /* @__PURE__ */ import_react4.default.createElement(
1210
+ ), /* @__PURE__ */ import_react5.default.createElement(
1111
1211
  Stat,
1112
1212
  {
1113
1213
  label: "Models",
1114
1214
  value: team.models?.length ? String(team.models.length) : "All"
1115
1215
  }
1116
- ), /* @__PURE__ */ import_react4.default.createElement(
1216
+ ), /* @__PURE__ */ import_react5.default.createElement(
1117
1217
  Stat,
1118
1218
  {
1119
1219
  label: "Budget",
1120
1220
  value: budget > 0 ? `$${budget.toFixed(2)}` : "Unlimited"
1121
1221
  }
1122
- ), /* @__PURE__ */ import_react4.default.createElement(
1222
+ ), /* @__PURE__ */ import_react5.default.createElement(
1123
1223
  Stat,
1124
1224
  {
1125
1225
  label: "Spend",
1126
1226
  value: `$${spend.toFixed(2)}`
1127
1227
  }
1128
- ), /* @__PURE__ */ import_react4.default.createElement(
1228
+ ), /* @__PURE__ */ import_react5.default.createElement(
1129
1229
  Stat,
1130
1230
  {
1131
1231
  label: "TPM",
1132
1232
  value: team.tpm_limit != null && team.tpm_limit > 0 ? String(team.tpm_limit) : "\u2014"
1133
1233
  }
1134
- ), /* @__PURE__ */ import_react4.default.createElement(
1234
+ ), /* @__PURE__ */ import_react5.default.createElement(
1135
1235
  Stat,
1136
1236
  {
1137
1237
  label: "RPM",
1138
1238
  value: team.rpm_limit != null && team.rpm_limit > 0 ? String(team.rpm_limit) : "\u2014"
1139
1239
  }
1140
- )), budget > 0 && /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { mt: 1.5 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", justifyContent: "space-between", mb: 0.5 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "body2" }, "$", spend.toFixed(2), " / $", budget.toFixed(2)), isOver && /* @__PURE__ */ import_react4.default.createElement(import_material4.Chip, { label: "Over Budget", size: "small", color: "error" }), isNear && /* @__PURE__ */ import_react4.default.createElement(import_material4.Chip, { label: "Near Limit", size: "small", color: "warning" })), /* @__PURE__ */ import_react4.default.createElement(
1141
- import_material4.LinearProgress,
1240
+ )), budget > 0 && /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { mt: 1.5 }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { display: "flex", justifyContent: "space-between", mb: 0.5 }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "body2" }, "$", spend.toFixed(2), " / $", budget.toFixed(2)), isOver && /* @__PURE__ */ import_react5.default.createElement(import_material5.Chip, { label: "Over Budget", size: "small", color: "error" }), isNear && /* @__PURE__ */ import_react5.default.createElement(import_material5.Chip, { label: "Near Limit", size: "small", color: "warning" })), /* @__PURE__ */ import_react5.default.createElement(
1241
+ import_material5.LinearProgress,
1142
1242
  {
1143
1243
  variant: "determinate",
1144
1244
  value: budgetPct,
1145
1245
  color: isOver ? "error" : isNear ? "warning" : "primary",
1146
1246
  sx: { height: 6, borderRadius: 1 }
1147
1247
  }
1148
- )), /* @__PURE__ */ import_react4.default.createElement(import_material4.Collapse, { in: expanded }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { mt: 2 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_icons_material2.Speed, { fontSize: "small", color: "action" }), /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary" }, "Rate limits")), /* @__PURE__ */ import_react4.default.createElement(import_material4.Stack, { direction: "row", spacing: 3, flexWrap: "wrap", useFlexGap: true, gap: 1.5, mb: 2 }, /* @__PURE__ */ import_react4.default.createElement(Stat, { label: "TPM limit", value: team.tpm_limit != null && team.tpm_limit > 0 ? String(team.tpm_limit) : "Unlimited" }), /* @__PURE__ */ import_react4.default.createElement(Stat, { label: "RPM limit", value: team.rpm_limit != null && team.rpm_limit > 0 ? String(team.rpm_limit) : "Unlimited" }), /* @__PURE__ */ import_react4.default.createElement(Stat, { label: "Max budget", value: budget > 0 ? `$${budget.toFixed(2)}` : "Unlimited" })), /* @__PURE__ */ import_react4.default.createElement(import_material4.Divider, { sx: { mb: 2 } }), /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Models"), team.models?.length ? /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", gap: 0.5, flexWrap: "wrap", mb: 2 }, team.models.map((m) => /* @__PURE__ */ import_react4.default.createElement(import_material4.Chip, { key: m, label: m, size: "small", variant: "outlined" }))) : /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "body2", color: "text.secondary", mb: 2 }, "All models allowed"), /* @__PURE__ */ import_react4.default.createElement(import_material4.Divider, { sx: { mb: 2 } }), usageLoading ? /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", justifyContent: "center", p: 2 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.CircularProgress, { size: 24 })) : dailyData.length > 0 ? /* @__PURE__ */ import_react4.default.createElement(import_react4.default.Fragment, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend"), /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { height: 160, mb: 2 }, /* @__PURE__ */ import_react4.default.createElement(import_recharts2.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react4.default.createElement(import_recharts2.AreaChart, { data: dailyData }, /* @__PURE__ */ import_react4.default.createElement(import_recharts2.CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.XAxis, { dataKey: "date", tick: { fontSize: 11, fill: tickFill } }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.YAxis, { tick: { fontSize: 11, fill: tickFill }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.Tooltip, { formatter: (v) => [`$${v.toFixed(4)}`, "Spend"] }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.Area, { type: "monotone", dataKey: "spend", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.3 })))), /* @__PURE__ */ import_react4.default.createElement(import_material4.Divider, { sx: { mb: 2 } })) : null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_icons_material2.Memory, { fontSize: "small", color: "action" }), /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary" }, "Members")), team.members_with_roles?.length ? /* @__PURE__ */ import_react4.default.createElement(import_material4.TableContainer, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Table, { size: "small" }, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableHead, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableRow, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, "User"), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, "Role"))), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableBody, null, team.members_with_roles.map((m) => /* @__PURE__ */ import_react4.default.createElement(import_material4.TableRow, { key: m.user_id }, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, m.user_email ? /* @__PURE__ */ import_react4.default.createElement(import_react4.default.Fragment, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "body2" }, m.user_email), /* @__PURE__ */ import_react4.default.createElement(
1149
- import_material4.Typography,
1248
+ )), /* @__PURE__ */ import_react5.default.createElement(import_material5.Collapse, { in: expanded }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { mt: 2 }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ import_react5.default.createElement(import_icons_material4.Speed, { fontSize: "small", color: "action" }), /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "subtitle2", color: "text.secondary" }, "Rate limits")), /* @__PURE__ */ import_react5.default.createElement(import_material5.Stack, { direction: "row", spacing: 3, flexWrap: "wrap", useFlexGap: true, gap: 1.5, mb: 2 }, /* @__PURE__ */ import_react5.default.createElement(Stat, { label: "TPM limit", value: team.tpm_limit != null && team.tpm_limit > 0 ? String(team.tpm_limit) : "Unlimited" }), /* @__PURE__ */ import_react5.default.createElement(Stat, { label: "RPM limit", value: team.rpm_limit != null && team.rpm_limit > 0 ? String(team.rpm_limit) : "Unlimited" }), /* @__PURE__ */ import_react5.default.createElement(Stat, { label: "Max budget", value: budget > 0 ? `$${budget.toFixed(2)}` : "Unlimited" })), /* @__PURE__ */ import_react5.default.createElement(import_material5.Divider, { sx: { mb: 2 } }), /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Models"), team.models?.length ? /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { display: "flex", gap: 0.5, flexWrap: "wrap", mb: 2 }, team.models.map((m) => /* @__PURE__ */ import_react5.default.createElement(import_material5.Chip, { key: m, label: m, size: "small", variant: "outlined" }))) : /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "body2", color: "text.secondary", mb: 2 }, "All models allowed"), /* @__PURE__ */ import_react5.default.createElement(import_material5.Divider, { sx: { mb: 2 } }), usageLoading ? /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { display: "flex", justifyContent: "center", p: 2 }, /* @__PURE__ */ import_react5.default.createElement(import_material5.CircularProgress, { size: 24 })) : dailyData.length > 0 ? /* @__PURE__ */ import_react5.default.createElement(import_react5.default.Fragment, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend"), /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { height: 160, mb: 2 }, /* @__PURE__ */ import_react5.default.createElement(import_recharts2.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react5.default.createElement(import_recharts2.AreaChart, { data: dailyData }, /* @__PURE__ */ import_react5.default.createElement(import_recharts2.CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ import_react5.default.createElement(import_recharts2.XAxis, { dataKey: "date", tick: { fontSize: 11, fill: tickFill } }), /* @__PURE__ */ import_react5.default.createElement(import_recharts2.YAxis, { tick: { fontSize: 11, fill: tickFill }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ import_react5.default.createElement(import_recharts2.Tooltip, { formatter: (v) => [`$${v.toFixed(4)}`, "Spend"] }), /* @__PURE__ */ import_react5.default.createElement(import_recharts2.Area, { type: "monotone", dataKey: "spend", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.3 })))), /* @__PURE__ */ import_react5.default.createElement(import_material5.Divider, { sx: { mb: 2 } })) : null, /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ import_react5.default.createElement(import_icons_material4.Memory, { fontSize: "small", color: "action" }), /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "subtitle2", color: "text.secondary" }, "Members")), team.members_with_roles?.length ? /* @__PURE__ */ import_react5.default.createElement(import_material5.TableContainer, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.Table, { size: "small" }, /* @__PURE__ */ import_react5.default.createElement(import_material5.TableHead, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.TableRow, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, "User"), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, "Role"))), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableBody, null, team.members_with_roles.map((m) => /* @__PURE__ */ import_react5.default.createElement(import_material5.TableRow, { key: m.user_id }, /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, m.user_email ? /* @__PURE__ */ import_react5.default.createElement(import_react5.default.Fragment, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "body2" }, m.user_email), /* @__PURE__ */ import_react5.default.createElement(
1249
+ import_material5.Typography,
1150
1250
  {
1151
1251
  variant: "caption",
1152
1252
  color: "text.secondary",
1153
1253
  sx: { fontFamily: "monospace" }
1154
1254
  },
1155
1255
  m.user_id
1156
- )) : /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "body2", sx: { fontFamily: "monospace" } }, m.user_id)), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, /* @__PURE__ */ import_react4.default.createElement(
1157
- import_material4.Chip,
1256
+ )) : /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "body2", sx: { fontFamily: "monospace" } }, m.user_id)), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, /* @__PURE__ */ import_react5.default.createElement(
1257
+ import_material5.Chip,
1158
1258
  {
1159
1259
  label: m.role,
1160
1260
  size: "small",
1161
1261
  color: m.role === "admin" ? "primary" : "default"
1162
1262
  }
1163
- ))))))) : /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "body2", color: "text.secondary" }, "No members assigned."))));
1263
+ ))))))) : /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "body2", color: "text.secondary" }, "No members assigned."))));
1164
1264
  };
1165
1265
  TeamUsage = ({
1166
1266
  teams,
@@ -1169,12 +1269,12 @@ var init_TeamUsage = __esm({
1169
1269
  getTeamUsageLoading
1170
1270
  }) => {
1171
1271
  if (loading) {
1172
- return /* @__PURE__ */ import_react4.default.createElement(import_material4.Paper, { sx: { p: 2 } }, /* @__PURE__ */ import_react4.default.createElement(import_material4.LinearProgress, null));
1272
+ return /* @__PURE__ */ import_react5.default.createElement(import_material5.Paper, { sx: { p: 2 } }, /* @__PURE__ */ import_react5.default.createElement(import_material5.LinearProgress, null));
1173
1273
  }
1174
1274
  if (!teams.length) {
1175
- return /* @__PURE__ */ import_react4.default.createElement(import_material4.Paper, { sx: { p: 2 } }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", alignItems: "flex-start", gap: 1.5 }, /* @__PURE__ */ import_react4.default.createElement(import_icons_material2.Group, { color: "disabled", sx: { mt: 0.5 } }), /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { color: "text.secondary", variant: "body2" }, "You're not a member of any LiteLLM team yet."), /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { 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."))));
1275
+ return /* @__PURE__ */ import_react5.default.createElement(import_material5.Paper, { sx: { p: 2 } }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { display: "flex", alignItems: "flex-start", gap: 1.5 }, /* @__PURE__ */ import_react5.default.createElement(import_icons_material4.Group, { color: "disabled", sx: { mt: 0.5 } }), /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { color: "text.secondary", variant: "body2" }, "You're not a member of any LiteLLM team yet."), /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { 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."))));
1176
1276
  }
1177
- return /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "h6", mb: 1 }, "Teams"), teams.map((team) => /* @__PURE__ */ import_react4.default.createElement(
1277
+ return /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "h6", mb: 1 }, "Teams"), teams.map((team) => /* @__PURE__ */ import_react5.default.createElement(
1178
1278
  TeamCard,
1179
1279
  {
1180
1280
  key: team.team_id,
@@ -1202,13 +1302,13 @@ function formatDateTime(iso) {
1202
1302
  return iso;
1203
1303
  }
1204
1304
  }
1205
- var import_react5, import_material5, import_icons_material3, import_react_use, ACTION_COLORS, DetailRow, AuditLog;
1305
+ var import_react6, import_material6, import_icons_material5, import_react_use, ACTION_COLORS, DetailRow, AuditLog;
1206
1306
  var init_AuditLog = __esm({
1207
1307
  "src/components/AuditLog.tsx"() {
1208
1308
  "use strict";
1209
- import_react5 = __toESM(require("react"));
1210
- import_material5 = require("@mui/material");
1211
- import_icons_material3 = require("@mui/icons-material");
1309
+ import_react6 = __toESM(require("react"));
1310
+ import_material6 = require("@mui/material");
1311
+ import_icons_material5 = require("@mui/icons-material");
1212
1312
  import_react_use = require("react-use");
1213
1313
  ACTION_COLORS = {
1214
1314
  created: "success",
@@ -1218,15 +1318,15 @@ var init_AuditLog = __esm({
1218
1318
  unblocked: "success"
1219
1319
  };
1220
1320
  DetailRow = ({ entry }) => {
1221
- const [open, setOpen] = (0, import_react5.useState)(false);
1321
+ const [open, setOpen] = (0, import_react6.useState)(false);
1222
1322
  const hasDetail = entry.before_value || entry.updated_values;
1223
- return /* @__PURE__ */ import_react5.default.createElement(import_react5.default.Fragment, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.TableRow, { hover: true }, /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, { sx: { width: 40, pr: 0 } }, hasDetail && /* @__PURE__ */ import_react5.default.createElement(import_material5.IconButton, { size: "small", onClick: () => setOpen((o) => !o) }, open ? /* @__PURE__ */ import_react5.default.createElement(import_icons_material3.KeyboardArrowUp, { fontSize: "small" }) : /* @__PURE__ */ import_react5.default.createElement(import_icons_material3.KeyboardArrowDown, { fontSize: "small" }))), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, { sx: { whiteSpace: "nowrap" } }, formatDateTime(entry.updated_at)), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, entry.action && /* @__PURE__ */ import_react5.default.createElement(import_material5.Chip, { label: entry.action, color: actionColor(entry.action), size: "small" })), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, entry.table_name ?? "-"), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "body2", component: "code", sx: { fontFamily: "monospace", fontSize: "0.75rem" } }, entry.object_id ? entry.object_id.slice(0, 20) + (entry.object_id.length > 20 ? "\u2026" : "") : "-")), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, entry.changed_by ?? "-")), hasDetail && /* @__PURE__ */ import_react5.default.createElement(import_material5.TableRow, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, { colSpan: 6, sx: { py: 0 } }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Collapse, { in: open, unmountOnExit: true }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { p: 2, display: "flex", gap: 2, flexWrap: "wrap" }, entry.before_value && /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { flex: 1, minWidth: 200 }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "caption", color: "text.secondary", display: "block", mb: 0.5 }, "Before"), /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { component: "pre", variant: "caption", sx: { fontFamily: "monospace", whiteSpace: "pre-wrap", wordBreak: "break-all" } }, JSON.stringify(entry.before_value, null, 2))), entry.updated_values && /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { flex: 1, minWidth: 200 }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "caption", color: "text.secondary", display: "block", mb: 0.5 }, "After"), /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { component: "pre", variant: "caption", sx: { fontFamily: "monospace", whiteSpace: "pre-wrap", wordBreak: "break-all" } }, JSON.stringify(entry.updated_values, null, 2))))))));
1323
+ return /* @__PURE__ */ import_react6.default.createElement(import_react6.default.Fragment, null, /* @__PURE__ */ import_react6.default.createElement(import_material6.TableRow, { hover: true }, /* @__PURE__ */ import_react6.default.createElement(import_material6.TableCell, { sx: { width: 40, pr: 0 } }, hasDetail && /* @__PURE__ */ import_react6.default.createElement(import_material6.IconButton, { size: "small", onClick: () => setOpen((o) => !o) }, open ? /* @__PURE__ */ import_react6.default.createElement(import_icons_material5.KeyboardArrowUp, { fontSize: "small" }) : /* @__PURE__ */ import_react6.default.createElement(import_icons_material5.KeyboardArrowDown, { fontSize: "small" }))), /* @__PURE__ */ import_react6.default.createElement(import_material6.TableCell, { sx: { whiteSpace: "nowrap" } }, formatDateTime(entry.updated_at)), /* @__PURE__ */ import_react6.default.createElement(import_material6.TableCell, null, entry.action && /* @__PURE__ */ import_react6.default.createElement(import_material6.Chip, { label: entry.action, color: actionColor(entry.action), size: "small" })), /* @__PURE__ */ import_react6.default.createElement(import_material6.TableCell, null, entry.table_name ?? "-"), /* @__PURE__ */ import_react6.default.createElement(import_material6.TableCell, null, /* @__PURE__ */ import_react6.default.createElement(import_material6.Typography, { variant: "body2", component: "code", sx: { fontFamily: "monospace", fontSize: "0.75rem" } }, entry.object_id ? entry.object_id.slice(0, 20) + (entry.object_id.length > 20 ? "\u2026" : "") : "-")), /* @__PURE__ */ import_react6.default.createElement(import_material6.TableCell, null, entry.changed_by ?? "-")), hasDetail && /* @__PURE__ */ import_react6.default.createElement(import_material6.TableRow, null, /* @__PURE__ */ import_react6.default.createElement(import_material6.TableCell, { colSpan: 6, sx: { py: 0 } }, /* @__PURE__ */ import_react6.default.createElement(import_material6.Collapse, { in: open, unmountOnExit: true }, /* @__PURE__ */ import_react6.default.createElement(import_material6.Box, { p: 2, display: "flex", gap: 2, flexWrap: "wrap" }, entry.before_value && /* @__PURE__ */ import_react6.default.createElement(import_material6.Box, { flex: 1, minWidth: 200 }, /* @__PURE__ */ import_react6.default.createElement(import_material6.Typography, { variant: "caption", color: "text.secondary", display: "block", mb: 0.5 }, "Before"), /* @__PURE__ */ import_react6.default.createElement(import_material6.Typography, { component: "pre", variant: "caption", sx: { fontFamily: "monospace", whiteSpace: "pre-wrap", wordBreak: "break-all" } }, JSON.stringify(entry.before_value, null, 2))), entry.updated_values && /* @__PURE__ */ import_react6.default.createElement(import_material6.Box, { flex: 1, minWidth: 200 }, /* @__PURE__ */ import_react6.default.createElement(import_material6.Typography, { variant: "caption", color: "text.secondary", display: "block", mb: 0.5 }, "After"), /* @__PURE__ */ import_react6.default.createElement(import_material6.Typography, { component: "pre", variant: "caption", sx: { fontFamily: "monospace", whiteSpace: "pre-wrap", wordBreak: "break-all" } }, JSON.stringify(entry.updated_values, null, 2))))))));
1224
1324
  };
1225
1325
  AuditLog = ({ api }) => {
1226
- const [page, setPage] = (0, import_react5.useState)(0);
1227
- const [pageSize, setPageSize] = (0, import_react5.useState)(25);
1228
- const [filters, setFilters] = (0, import_react5.useState)({});
1229
- const fetchParams = (0, import_react5.useCallback)(
1326
+ const [page, setPage] = (0, import_react6.useState)(0);
1327
+ const [pageSize, setPageSize] = (0, import_react6.useState)(25);
1328
+ const [filters, setFilters] = (0, import_react6.useState)({});
1329
+ const fetchParams = (0, import_react6.useCallback)(
1230
1330
  () => ({ page: page + 1, page_size: pageSize, ...filters }),
1231
1331
  [page, pageSize, filters]
1232
1332
  )();
@@ -1236,8 +1336,8 @@ var init_AuditLog = __esm({
1236
1336
  );
1237
1337
  const entries = value?.audit_logs ?? [];
1238
1338
  const total = value?.total ?? 0;
1239
- return /* @__PURE__ */ import_react5.default.createElement(import_material5.Paper, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { p: 2, display: "flex", gap: 2, flexWrap: "wrap", alignItems: "center" }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "h6", sx: { flex: "0 0 auto", mr: 1 } }, "Audit Log"), /* @__PURE__ */ import_react5.default.createElement(
1240
- import_material5.TextField,
1339
+ return /* @__PURE__ */ import_react6.default.createElement(import_material6.Paper, null, /* @__PURE__ */ import_react6.default.createElement(import_material6.Box, { p: 2, display: "flex", gap: 2, flexWrap: "wrap", alignItems: "center" }, /* @__PURE__ */ import_react6.default.createElement(import_material6.Typography, { variant: "h6", sx: { flex: "0 0 auto", mr: 1 } }, "Audit Log"), /* @__PURE__ */ import_react6.default.createElement(
1340
+ import_material6.TextField,
1241
1341
  {
1242
1342
  size: "small",
1243
1343
  label: "Action",
@@ -1249,13 +1349,13 @@ var init_AuditLog = __esm({
1249
1349
  },
1250
1350
  sx: { minWidth: 160 }
1251
1351
  },
1252
- /* @__PURE__ */ import_react5.default.createElement(import_material5.MenuItem, { value: "" }, "All actions"),
1253
- /* @__PURE__ */ import_react5.default.createElement(import_material5.MenuItem, { value: "created" }, "Created"),
1254
- /* @__PURE__ */ import_react5.default.createElement(import_material5.MenuItem, { value: "updated" }, "Updated"),
1255
- /* @__PURE__ */ import_react5.default.createElement(import_material5.MenuItem, { value: "deleted" }, "Deleted"),
1256
- /* @__PURE__ */ import_react5.default.createElement(import_material5.MenuItem, { value: "blocked" }, "Blocked")
1257
- ), /* @__PURE__ */ import_react5.default.createElement(
1258
- import_material5.TextField,
1352
+ /* @__PURE__ */ import_react6.default.createElement(import_material6.MenuItem, { value: "" }, "All actions"),
1353
+ /* @__PURE__ */ import_react6.default.createElement(import_material6.MenuItem, { value: "created" }, "Created"),
1354
+ /* @__PURE__ */ import_react6.default.createElement(import_material6.MenuItem, { value: "updated" }, "Updated"),
1355
+ /* @__PURE__ */ import_react6.default.createElement(import_material6.MenuItem, { value: "deleted" }, "Deleted"),
1356
+ /* @__PURE__ */ import_react6.default.createElement(import_material6.MenuItem, { value: "blocked" }, "Blocked")
1357
+ ), /* @__PURE__ */ import_react6.default.createElement(
1358
+ import_material6.TextField,
1259
1359
  {
1260
1360
  size: "small",
1261
1361
  label: "Table",
@@ -1267,12 +1367,12 @@ var init_AuditLog = __esm({
1267
1367
  },
1268
1368
  sx: { minWidth: 160 }
1269
1369
  },
1270
- /* @__PURE__ */ import_react5.default.createElement(import_material5.MenuItem, { value: "" }, "All tables"),
1271
- /* @__PURE__ */ import_react5.default.createElement(import_material5.MenuItem, { value: "LiteLLM_VerificationToken" }, "Key"),
1272
- /* @__PURE__ */ import_react5.default.createElement(import_material5.MenuItem, { value: "LiteLLM_TeamTable" }, "Team"),
1273
- /* @__PURE__ */ import_react5.default.createElement(import_material5.MenuItem, { value: "LiteLLM_UserTable" }, "User")
1274
- ), /* @__PURE__ */ import_react5.default.createElement(
1275
- import_material5.TextField,
1370
+ /* @__PURE__ */ import_react6.default.createElement(import_material6.MenuItem, { value: "" }, "All tables"),
1371
+ /* @__PURE__ */ import_react6.default.createElement(import_material6.MenuItem, { value: "LiteLLM_VerificationToken" }, "Key"),
1372
+ /* @__PURE__ */ import_react6.default.createElement(import_material6.MenuItem, { value: "LiteLLM_TeamTable" }, "Team"),
1373
+ /* @__PURE__ */ import_react6.default.createElement(import_material6.MenuItem, { value: "LiteLLM_UserTable" }, "User")
1374
+ ), /* @__PURE__ */ import_react6.default.createElement(
1375
+ import_material6.TextField,
1276
1376
  {
1277
1377
  size: "small",
1278
1378
  label: "Changed by",
@@ -1283,8 +1383,8 @@ var init_AuditLog = __esm({
1283
1383
  },
1284
1384
  sx: { minWidth: 200 }
1285
1385
  }
1286
- )), error && /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { px: 2, pb: 2 }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Alert, { severity: "error" }, error.message)), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableContainer, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.Table, { size: "small" }, /* @__PURE__ */ import_react5.default.createElement(import_material5.TableHead, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.TableRow, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, { sx: { width: 40 } }), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, "Time"), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, "Action"), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, "Table"), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, "Object ID"), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, "Changed By"))), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableBody, null, loading ? /* @__PURE__ */ import_react5.default.createElement(import_material5.TableRow, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, { colSpan: 6, align: "center", sx: { py: 4 } }, /* @__PURE__ */ import_react5.default.createElement(import_material5.CircularProgress, { size: 24 }))) : entries.length === 0 ? /* @__PURE__ */ import_react5.default.createElement(import_material5.TableRow, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, { colSpan: 6, align: "center", sx: { py: 4 } }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { color: "text.secondary" }, "No audit events found"))) : entries.map((entry) => /* @__PURE__ */ import_react5.default.createElement(DetailRow, { key: entry.id, entry }))))), /* @__PURE__ */ import_react5.default.createElement(
1287
- import_material5.TablePagination,
1386
+ )), error && /* @__PURE__ */ import_react6.default.createElement(import_material6.Box, { px: 2, pb: 2 }, /* @__PURE__ */ import_react6.default.createElement(import_material6.Alert, { severity: "error" }, error.message)), /* @__PURE__ */ import_react6.default.createElement(import_material6.TableContainer, null, /* @__PURE__ */ import_react6.default.createElement(import_material6.Table, { size: "small" }, /* @__PURE__ */ import_react6.default.createElement(import_material6.TableHead, null, /* @__PURE__ */ import_react6.default.createElement(import_material6.TableRow, null, /* @__PURE__ */ import_react6.default.createElement(import_material6.TableCell, { sx: { width: 40 } }), /* @__PURE__ */ import_react6.default.createElement(import_material6.TableCell, null, "Time"), /* @__PURE__ */ import_react6.default.createElement(import_material6.TableCell, null, "Action"), /* @__PURE__ */ import_react6.default.createElement(import_material6.TableCell, null, "Table"), /* @__PURE__ */ import_react6.default.createElement(import_material6.TableCell, null, "Object ID"), /* @__PURE__ */ import_react6.default.createElement(import_material6.TableCell, null, "Changed By"))), /* @__PURE__ */ import_react6.default.createElement(import_material6.TableBody, null, loading ? /* @__PURE__ */ import_react6.default.createElement(import_material6.TableRow, null, /* @__PURE__ */ import_react6.default.createElement(import_material6.TableCell, { colSpan: 6, align: "center", sx: { py: 4 } }, /* @__PURE__ */ import_react6.default.createElement(import_material6.CircularProgress, { size: 24 }))) : entries.length === 0 ? /* @__PURE__ */ import_react6.default.createElement(import_material6.TableRow, null, /* @__PURE__ */ import_react6.default.createElement(import_material6.TableCell, { colSpan: 6, align: "center", sx: { py: 4 } }, /* @__PURE__ */ import_react6.default.createElement(import_material6.Typography, { color: "text.secondary" }, "No audit events found"))) : entries.map((entry) => /* @__PURE__ */ import_react6.default.createElement(DetailRow, { key: entry.id, entry }))))), /* @__PURE__ */ import_react6.default.createElement(
1387
+ import_material6.TablePagination,
1288
1388
  {
1289
1389
  component: "div",
1290
1390
  count: total,
@@ -1321,16 +1421,17 @@ function initDateRange() {
1321
1421
  else start.setDate(start.getDate() - 7);
1322
1422
  return { start, end };
1323
1423
  }
1324
- var import_react6, import_material6, import_react_use2, import_core_plugin_api2, PERIOD_LS_KEY2, LiteLLMPage;
1424
+ var import_react7, import_material7, import_react_use2, import_core_plugin_api2, PERIOD_LS_KEY2, LiteLLMPage;
1325
1425
  var init_LiteLLMPage = __esm({
1326
1426
  "src/components/LiteLLMPage.tsx"() {
1327
1427
  "use strict";
1328
- import_react6 = __toESM(require("react"));
1329
- import_material6 = require("@mui/material");
1428
+ import_react7 = __toESM(require("react"));
1429
+ import_material7 = require("@mui/material");
1330
1430
  import_react_use2 = require("react-use");
1331
1431
  import_core_plugin_api2 = require("@backstage/core-plugin-api");
1332
1432
  init_DashboardHeader();
1333
1433
  init_KeysTable();
1434
+ init_GenerateKeyDialog();
1334
1435
  init_UsageStats();
1335
1436
  init_TeamUsage();
1336
1437
  init_AuditLog();
@@ -1338,11 +1439,12 @@ var init_LiteLLMPage = __esm({
1338
1439
  PERIOD_LS_KEY2 = "litellm_usage_period";
1339
1440
  LiteLLMPage = () => {
1340
1441
  const api = (0, import_core_plugin_api2.useApi)(liteLlmApiRef);
1341
- const [dateRange, setDateRange] = (0, import_react6.useState)(initDateRange);
1342
- const [activeTab, setActiveTab] = (0, import_react6.useState)("overview");
1343
- const [snackbar, setSnackbar] = (0, import_react6.useState)(null);
1344
- const [teamUsageCache, setTeamUsageCache] = (0, import_react6.useState)({});
1345
- const [teamUsageLoading, setTeamUsageLoading] = (0, import_react6.useState)({});
1442
+ const [dateRange, setDateRange] = (0, import_react7.useState)(initDateRange);
1443
+ const [activeTab, setActiveTab] = (0, import_react7.useState)("overview");
1444
+ const [snackbar, setSnackbar] = (0, import_react7.useState)(null);
1445
+ const [generateDialogOpen, setGenerateDialogOpen] = (0, import_react7.useState)(false);
1446
+ const [teamUsageCache, setTeamUsageCache] = (0, import_react7.useState)({});
1447
+ const [teamUsageLoading, setTeamUsageLoading] = (0, import_react7.useState)({});
1346
1448
  const { value: userInfo, loading: userLoading, error: userError } = (0, import_react_use2.useAsync)(
1347
1449
  () => api.getUserInfo(),
1348
1450
  [api]
@@ -1367,7 +1469,7 @@ var init_LiteLLMPage = __esm({
1367
1469
  [api]
1368
1470
  );
1369
1471
  const { value: liteLlmConfig } = (0, import_react_use2.useAsync)(() => api.getConfig(), [api]);
1370
- const teams = (0, import_react6.useMemo)(() => {
1472
+ const teams = (0, import_react7.useMemo)(() => {
1371
1473
  if (!allTeams?.length) return [];
1372
1474
  if (!userInfo) return allTeams;
1373
1475
  const userId = userInfo.user_id;
@@ -1384,12 +1486,12 @@ var init_LiteLLMPage = __esm({
1384
1486
  const endDate = dateRange.end.toISOString().split("T")[0];
1385
1487
  return api.getUsage(startDate, endDate);
1386
1488
  }, [api, dateRange]);
1387
- const handleDateRangeChange = (0, import_react6.useCallback)((range) => {
1489
+ const handleDateRangeChange = (0, import_react7.useCallback)((range) => {
1388
1490
  setDateRange(range);
1389
1491
  setTeamUsageCache({});
1390
1492
  setTeamUsageLoading({});
1391
1493
  }, [setDateRange]);
1392
- const loadTeamUsage = (0, import_react6.useCallback)(async (teamId) => {
1494
+ const loadTeamUsage = (0, import_react7.useCallback)(async (teamId) => {
1393
1495
  if (teamUsageCache[teamId] !== void 0 || teamUsageLoading[teamId]) return;
1394
1496
  setTeamUsageLoading((prev) => ({ ...prev, [teamId]: true }));
1395
1497
  try {
@@ -1403,7 +1505,7 @@ var init_LiteLLMPage = __esm({
1403
1505
  setTeamUsageLoading((prev) => ({ ...prev, [teamId]: false }));
1404
1506
  }
1405
1507
  }, [api, dateRange, teamUsageCache, teamUsageLoading]);
1406
- const allowedModels = (0, import_react6.useMemo)(() => {
1508
+ const allowedModels = (0, import_react7.useMemo)(() => {
1407
1509
  if (!allModels?.length) return [];
1408
1510
  const userModels = userInfo?.models;
1409
1511
  const teamModels = teams?.flatMap((t) => t.models ?? []);
@@ -1416,7 +1518,7 @@ var init_LiteLLMPage = __esm({
1416
1518
  ]);
1417
1519
  return allModels.filter((m) => allowed.has(m.model_name));
1418
1520
  }, [allModels, userInfo, teams]);
1419
- const handleGenerateKey = (0, import_react6.useCallback)(
1521
+ const handleGenerateKey = (0, import_react7.useCallback)(
1420
1522
  async (request) => {
1421
1523
  try {
1422
1524
  const response = await api.generateKey(request);
@@ -1430,7 +1532,7 @@ var init_LiteLLMPage = __esm({
1430
1532
  },
1431
1533
  [api, refreshKeys]
1432
1534
  );
1433
- const handleUpdateKey = (0, import_react6.useCallback)(
1535
+ const handleUpdateKey = (0, import_react7.useCallback)(
1434
1536
  async (keyId, request) => {
1435
1537
  try {
1436
1538
  await api.updateKey(keyId, request);
@@ -1443,7 +1545,7 @@ var init_LiteLLMPage = __esm({
1443
1545
  },
1444
1546
  [api, refreshKeys]
1445
1547
  );
1446
- const handleBlockKey = (0, import_react6.useCallback)(
1548
+ const handleBlockKey = (0, import_react7.useCallback)(
1447
1549
  async (keyId) => {
1448
1550
  try {
1449
1551
  await api.blockKey(keyId);
@@ -1455,7 +1557,7 @@ var init_LiteLLMPage = __esm({
1455
1557
  },
1456
1558
  [api, refreshKeys]
1457
1559
  );
1458
- const handleUnblockKey = (0, import_react6.useCallback)(
1560
+ const handleUnblockKey = (0, import_react7.useCallback)(
1459
1561
  async (keyId) => {
1460
1562
  try {
1461
1563
  await api.unblockKey(keyId);
@@ -1467,7 +1569,7 @@ var init_LiteLLMPage = __esm({
1467
1569
  },
1468
1570
  [api, refreshKeys]
1469
1571
  );
1470
- const handleResetKeySpend = (0, import_react6.useCallback)(
1572
+ const handleResetKeySpend = (0, import_react7.useCallback)(
1471
1573
  async (keyId) => {
1472
1574
  try {
1473
1575
  await api.resetKeySpend(keyId);
@@ -1479,7 +1581,7 @@ var init_LiteLLMPage = __esm({
1479
1581
  },
1480
1582
  [api, refreshKeys]
1481
1583
  );
1482
- const handleDeleteKey = (0, import_react6.useCallback)(
1584
+ const handleDeleteKey = (0, import_react7.useCallback)(
1483
1585
  async (keyId) => {
1484
1586
  try {
1485
1587
  await api.deleteKey(keyId);
@@ -1496,7 +1598,7 @@ var init_LiteLLMPage = __esm({
1496
1598
  },
1497
1599
  [api, refreshKeys]
1498
1600
  );
1499
- const handlePruneExpiredKeys = (0, import_react6.useCallback)(
1601
+ const handlePruneExpiredKeys = (0, import_react7.useCallback)(
1500
1602
  async () => {
1501
1603
  try {
1502
1604
  const result = await api.pruneExpiredKeys();
@@ -1511,25 +1613,34 @@ var init_LiteLLMPage = __esm({
1511
1613
  );
1512
1614
  const isInitialLoading = userLoading && !userInfo;
1513
1615
  if (isInitialLoading) {
1514
- return /* @__PURE__ */ import_react6.default.createElement(import_material6.Box, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "50vh" }, /* @__PURE__ */ import_react6.default.createElement(import_material6.CircularProgress, null));
1616
+ return /* @__PURE__ */ import_react7.default.createElement(import_material7.Box, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "50vh" }, /* @__PURE__ */ import_react7.default.createElement(import_material7.CircularProgress, null));
1515
1617
  }
1516
1618
  if (userError || !userInfo) {
1517
1619
  const isProvisioningEnabled = userError?.body?.provisioning === true;
1518
1620
  const hint = userError?.body?.hint;
1519
- return /* @__PURE__ */ import_react6.default.createElement(import_material6.Box, { p: 3 }, /* @__PURE__ */ import_react6.default.createElement(import_material6.Paper, { sx: { p: 3 } }, /* @__PURE__ */ import_react6.default.createElement(import_material6.Typography, { variant: "h6", gutterBottom: true }, "Account not provisioned"), /* @__PURE__ */ import_react6.default.createElement(import_material6.Typography, { color: "text.secondary", paragraph: true }, "Your Backstage account is not linked to a LiteLLM user."), hint ? /* @__PURE__ */ import_react6.default.createElement(import_material6.Typography, { variant: "body2", color: "text.secondary" }, hint) : /* @__PURE__ */ import_react6.default.createElement(import_material6.Typography, { variant: "body2", color: "text.secondary" }, isProvisioningEnabled ? "Auto-provisioning is enabled but failed. Check the backend logs." : "Set litellm.provisioning.enabled: true in app-config.yaml to enable auto-provisioning, or ask your administrator to create the account manually.")));
1621
+ return /* @__PURE__ */ import_react7.default.createElement(import_material7.Box, { p: 3 }, /* @__PURE__ */ import_react7.default.createElement(import_material7.Paper, { sx: { p: 3 } }, /* @__PURE__ */ import_react7.default.createElement(import_material7.Typography, { variant: "h6", gutterBottom: true }, "Account not provisioned"), /* @__PURE__ */ import_react7.default.createElement(import_material7.Typography, { color: "text.secondary", paragraph: true }, "Your Backstage account is not linked to a LiteLLM user."), hint ? /* @__PURE__ */ import_react7.default.createElement(import_material7.Typography, { variant: "body2", color: "text.secondary" }, hint) : /* @__PURE__ */ import_react7.default.createElement(import_material7.Typography, { variant: "body2", color: "text.secondary" }, isProvisioningEnabled ? "Auto-provisioning is enabled but failed. Check the backend logs." : "Set litellm.provisioning.enabled: true in app-config.yaml to enable auto-provisioning, or ask your administrator to create the account manually.")));
1520
1622
  }
1521
- return /* @__PURE__ */ import_react6.default.createElement(import_material6.Box, { p: 3 }, /* @__PURE__ */ import_react6.default.createElement(import_material6.Box, { mb: 2 }, /* @__PURE__ */ import_react6.default.createElement(DashboardHeader, { userInfo, teams: teams ?? [], loading: userLoading || teamsLoading })), /* @__PURE__ */ import_react6.default.createElement(
1522
- import_material6.Tabs,
1623
+ return /* @__PURE__ */ import_react7.default.createElement(import_material7.Box, { p: 3 }, /* @__PURE__ */ import_react7.default.createElement(import_material7.Box, { mb: 2 }, /* @__PURE__ */ import_react7.default.createElement(
1624
+ DashboardHeader,
1625
+ {
1626
+ userInfo,
1627
+ teams: teams ?? [],
1628
+ keys: keys ?? [],
1629
+ loading: userLoading || teamsLoading,
1630
+ onGenerateKeyClick: () => setGenerateDialogOpen(true)
1631
+ }
1632
+ )), /* @__PURE__ */ import_react7.default.createElement(
1633
+ import_material7.Tabs,
1523
1634
  {
1524
1635
  value: activeTab,
1525
1636
  onChange: (_, v) => setActiveTab(v),
1526
1637
  sx: { mb: 2, borderBottom: 1, borderColor: "divider" }
1527
1638
  },
1528
- /* @__PURE__ */ import_react6.default.createElement(import_material6.Tab, { label: "Overview", value: "overview" }),
1529
- /* @__PURE__ */ import_react6.default.createElement(import_material6.Tab, { label: "Keys", value: "keys" }),
1530
- /* @__PURE__ */ import_react6.default.createElement(import_material6.Tab, { label: "Teams", value: "teams" }),
1531
- userInfo.can_view_audit && /* @__PURE__ */ import_react6.default.createElement(import_material6.Tab, { label: "Audit Log", value: "audit" })
1532
- ), activeTab === "overview" && /* @__PURE__ */ import_react6.default.createElement(
1639
+ /* @__PURE__ */ import_react7.default.createElement(import_material7.Tab, { label: "Overview", value: "overview" }),
1640
+ /* @__PURE__ */ import_react7.default.createElement(import_material7.Tab, { label: "Keys", value: "keys" }),
1641
+ /* @__PURE__ */ import_react7.default.createElement(import_material7.Tab, { label: "Teams", value: "teams" }),
1642
+ userInfo.can_view_audit && /* @__PURE__ */ import_react7.default.createElement(import_material7.Tab, { label: "Audit Log", value: "audit" })
1643
+ ), activeTab === "overview" && /* @__PURE__ */ import_react7.default.createElement(
1533
1644
  UsageStats,
1534
1645
  {
1535
1646
  usage: usage ?? null,
@@ -1539,25 +1650,21 @@ var init_LiteLLMPage = __esm({
1539
1650
  loading: usageLoading,
1540
1651
  userInfo
1541
1652
  }
1542
- ), activeTab === "keys" && /* @__PURE__ */ import_react6.default.createElement(
1653
+ ), activeTab === "keys" && /* @__PURE__ */ import_react7.default.createElement(
1543
1654
  KeysTable,
1544
1655
  {
1545
1656
  keys: keys ?? [],
1546
1657
  models: allowedModels,
1547
- teams: teams ?? [],
1548
- username: userInfo.user_id,
1549
- keyGenerationSettings: liteLlmConfig?.keyGeneration,
1550
1658
  loading: keysLoading || modelsLoading,
1551
- onGenerateKey: handleGenerateKey,
1659
+ onGenerateKeyClick: () => setGenerateDialogOpen(true),
1552
1660
  onUpdateKey: handleUpdateKey,
1553
1661
  onBlockKey: handleBlockKey,
1554
1662
  onUnblockKey: handleUnblockKey,
1555
1663
  onResetKeySpend: handleResetKeySpend,
1556
1664
  onDeleteKey: handleDeleteKey,
1557
- onPruneExpiredKeys: handlePruneExpiredKeys,
1558
- onGetConfig: () => api.getConfig()
1665
+ onPruneExpiredKeys: handlePruneExpiredKeys
1559
1666
  }
1560
- ), activeTab === "teams" && /* @__PURE__ */ import_react6.default.createElement(
1667
+ ), activeTab === "teams" && /* @__PURE__ */ import_react7.default.createElement(
1561
1668
  TeamUsage,
1562
1669
  {
1563
1670
  teams: teams ?? [],
@@ -1568,15 +1675,28 @@ var init_LiteLLMPage = __esm({
1568
1675
  },
1569
1676
  getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false
1570
1677
  }
1571
- ), activeTab === "audit" && userInfo.can_view_audit && /* @__PURE__ */ import_react6.default.createElement(AuditLog, { api }), /* @__PURE__ */ import_react6.default.createElement(
1572
- import_material6.Snackbar,
1678
+ ), activeTab === "audit" && userInfo.can_view_audit && /* @__PURE__ */ import_react7.default.createElement(AuditLog, { api }), /* @__PURE__ */ import_react7.default.createElement(
1679
+ GenerateKeyDialog,
1680
+ {
1681
+ open: generateDialogOpen,
1682
+ onClose: () => setGenerateDialogOpen(false),
1683
+ keys: keys ?? [],
1684
+ models: allowedModels,
1685
+ teams: teams ?? [],
1686
+ username: userInfo.user_id,
1687
+ keyGenerationSettings: liteLlmConfig?.keyGeneration,
1688
+ onGenerateKey: handleGenerateKey,
1689
+ onGetConfig: () => api.getConfig()
1690
+ }
1691
+ ), /* @__PURE__ */ import_react7.default.createElement(
1692
+ import_material7.Snackbar,
1573
1693
  {
1574
1694
  open: !!snackbar,
1575
1695
  autoHideDuration: 5e3,
1576
1696
  onClose: () => setSnackbar(null),
1577
1697
  anchorOrigin: { vertical: "bottom", horizontal: "right" }
1578
1698
  },
1579
- snackbar ? /* @__PURE__ */ import_react6.default.createElement(import_material6.Alert, { severity: snackbar.severity, onClose: () => setSnackbar(null) }, snackbar.message) : void 0
1699
+ snackbar ? /* @__PURE__ */ import_react7.default.createElement(import_material7.Alert, { severity: snackbar.severity, onClose: () => setSnackbar(null) }, snackbar.message) : void 0
1580
1700
  ));
1581
1701
  };
1582
1702
  }
@@ -1598,8 +1718,8 @@ __export(index_exports, {
1598
1718
  module.exports = __toCommonJS(index_exports);
1599
1719
 
1600
1720
  // src/plugin.tsx
1601
- var import_react7 = __toESM(require("react"));
1602
- var import_icons_material4 = require("@mui/icons-material");
1721
+ var import_react8 = __toESM(require("react"));
1722
+ var import_icons_material6 = require("@mui/icons-material");
1603
1723
  var import_frontend_plugin_api = require("@backstage/frontend-plugin-api");
1604
1724
  init_api();
1605
1725
  var liteLlmApi = import_frontend_plugin_api.ApiBlueprint.make({
@@ -1613,10 +1733,10 @@ var liteLlmPage = import_frontend_plugin_api.PageBlueprint.make({
1613
1733
  params: {
1614
1734
  path: "/litellm",
1615
1735
  title: "LiteLLM",
1616
- icon: /* @__PURE__ */ import_react7.default.createElement(import_icons_material4.TrendingUp, null),
1736
+ icon: /* @__PURE__ */ import_react8.default.createElement(import_icons_material6.TrendingUp, null),
1617
1737
  loader: async () => {
1618
1738
  const { LiteLLMPage: LiteLLMPage2 } = await Promise.resolve().then(() => (init_LiteLLMPage(), LiteLLMPage_exports));
1619
- return /* @__PURE__ */ import_react7.default.createElement(LiteLLMPage2, null);
1739
+ return /* @__PURE__ */ import_react8.default.createElement(LiteLLMPage2, null);
1620
1740
  }
1621
1741
  }
1622
1742
  });
@@ -1633,8 +1753,8 @@ init_UsageStats();
1633
1753
  init_TeamUsage();
1634
1754
 
1635
1755
  // src/components/LiteLLMHomeWidget.tsx
1636
- var import_react8 = __toESM(require("react"));
1637
- var import_material7 = require("@mui/material");
1756
+ var import_react9 = __toESM(require("react"));
1757
+ var import_material8 = require("@mui/material");
1638
1758
  var import_recharts3 = require("recharts");
1639
1759
  var import_core_plugin_api3 = require("@backstage/core-plugin-api");
1640
1760
  init_api();
@@ -1651,19 +1771,19 @@ function presetToDateRange(preset) {
1651
1771
  }
1652
1772
  return { start, end };
1653
1773
  }
1654
- var Kpi = ({ label, value }) => /* @__PURE__ */ import_react8.default.createElement(import_material7.Box, null, /* @__PURE__ */ import_react8.default.createElement(import_material7.Typography, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ import_react8.default.createElement(import_material7.Typography, { variant: "subtitle1", fontWeight: 600 }, value));
1774
+ var Kpi = ({ label, value }) => /* @__PURE__ */ import_react9.default.createElement(import_material8.Box, null, /* @__PURE__ */ import_react9.default.createElement(import_material8.Typography, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ import_react9.default.createElement(import_material8.Typography, { variant: "subtitle1", fontWeight: 600 }, value));
1655
1775
  var LiteLLMHomeWidget = ({
1656
1776
  defaultPeriod = "7d",
1657
1777
  title = "LiteLLM Usage"
1658
1778
  }) => {
1659
1779
  const api = (0, import_core_plugin_api3.useApi)(liteLlmApiRef);
1660
- const [period, setPeriod] = (0, import_react8.useState)(defaultPeriod);
1661
- const [loading, setLoading] = (0, import_react8.useState)(true);
1662
- const [usageError, setUsageError] = (0, import_react8.useState)(null);
1663
- const [keysError, setKeysError] = (0, import_react8.useState)(null);
1664
- const [usage, setUsage] = (0, import_react8.useState)(null);
1665
- const [keys, setKeys] = (0, import_react8.useState)([]);
1666
- (0, import_react8.useEffect)(() => {
1780
+ const [period, setPeriod] = (0, import_react9.useState)(defaultPeriod);
1781
+ const [loading, setLoading] = (0, import_react9.useState)(true);
1782
+ const [usageError, setUsageError] = (0, import_react9.useState)(null);
1783
+ const [keysError, setKeysError] = (0, import_react9.useState)(null);
1784
+ const [usage, setUsage] = (0, import_react9.useState)(null);
1785
+ const [keys, setKeys] = (0, import_react9.useState)([]);
1786
+ (0, import_react9.useEffect)(() => {
1667
1787
  let cancelled = false;
1668
1788
  setLoading(true);
1669
1789
  setUsageError(null);
@@ -1700,17 +1820,17 @@ var LiteLLMHomeWidget = ({
1700
1820
  spend: d.spend
1701
1821
  }));
1702
1822
  const hasSparkline = dailyData.length > 0;
1703
- return /* @__PURE__ */ import_react8.default.createElement(import_material7.Paper, { sx: { p: 2 } }, /* @__PURE__ */ import_react8.default.createElement(import_material7.Box, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1.5 }, /* @__PURE__ */ import_react8.default.createElement(import_material7.Typography, { variant: "h6" }, title), /* @__PURE__ */ import_react8.default.createElement(import_material7.FormControl, { size: "small", sx: { minWidth: 90 } }, /* @__PURE__ */ import_react8.default.createElement(
1704
- import_material7.Select,
1823
+ return /* @__PURE__ */ import_react9.default.createElement(import_material8.Paper, { sx: { p: 2 } }, /* @__PURE__ */ import_react9.default.createElement(import_material8.Box, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1.5 }, /* @__PURE__ */ import_react9.default.createElement(import_material8.Typography, { variant: "h6" }, title), /* @__PURE__ */ import_react9.default.createElement(import_material8.FormControl, { size: "small", sx: { minWidth: 90 } }, /* @__PURE__ */ import_react9.default.createElement(
1824
+ import_material8.Select,
1705
1825
  {
1706
1826
  value: period,
1707
1827
  onChange: (e) => setPeriod(e.target.value),
1708
1828
  displayEmpty: true
1709
1829
  },
1710
- /* @__PURE__ */ import_react8.default.createElement(import_material7.MenuItem, { value: "today" }, "Today"),
1711
- /* @__PURE__ */ import_react8.default.createElement(import_material7.MenuItem, { value: "7d" }, "7d"),
1712
- /* @__PURE__ */ import_react8.default.createElement(import_material7.MenuItem, { value: "30d" }, "30d")
1713
- ))), loading && /* @__PURE__ */ import_react8.default.createElement(import_material7.Box, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ import_react8.default.createElement(import_material7.CircularProgress, { size: 32 })), !loading && totalFailure && /* @__PURE__ */ import_react8.default.createElement(import_material7.Alert, { severity: "error", sx: { mt: 1 } }, usageError ?? "Failed to load usage data"), !loading && !totalFailure && /* @__PURE__ */ import_react8.default.createElement(import_react8.default.Fragment, null, partialFailure && /* @__PURE__ */ import_react8.default.createElement(import_material7.Alert, { severity: "warning", sx: { mt: 1, mb: 1 } }, usageError ? `Usage data unavailable (${usageError}).` : "", keysError ? ` Key list unavailable (${keysError}).` : "", " Showing what loaded."), /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "USD Spent", value: fmtUsd(usage?.total_spend ?? 0) })), /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "Tokens In", value: fmtInt(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "Tokens Out", value: fmtInt(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "Keys", value: fmtInt(keys.length) }))), hasSparkline && /* @__PURE__ */ import_react8.default.createElement(import_material7.Box, { height: 120 }, /* @__PURE__ */ import_react8.default.createElement(import_recharts3.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react8.default.createElement(import_recharts3.AreaChart, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 } }, /* @__PURE__ */ import_react8.default.createElement(
1830
+ /* @__PURE__ */ import_react9.default.createElement(import_material8.MenuItem, { value: "today" }, "Today"),
1831
+ /* @__PURE__ */ import_react9.default.createElement(import_material8.MenuItem, { value: "7d" }, "7d"),
1832
+ /* @__PURE__ */ import_react9.default.createElement(import_material8.MenuItem, { value: "30d" }, "30d")
1833
+ ))), loading && /* @__PURE__ */ import_react9.default.createElement(import_material8.Box, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ import_react9.default.createElement(import_material8.CircularProgress, { size: 32 })), !loading && totalFailure && /* @__PURE__ */ import_react9.default.createElement(import_material8.Alert, { severity: "error", sx: { mt: 1 } }, usageError ?? "Failed to load usage data"), !loading && !totalFailure && /* @__PURE__ */ import_react9.default.createElement(import_react9.default.Fragment, null, partialFailure && /* @__PURE__ */ import_react9.default.createElement(import_material8.Alert, { severity: "warning", sx: { mt: 1, mb: 1 } }, usageError ? `Usage data unavailable (${usageError}).` : "", keysError ? ` Key list unavailable (${keysError}).` : "", " Showing what loaded."), /* @__PURE__ */ import_react9.default.createElement(import_material8.Grid, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ import_react9.default.createElement(import_material8.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react9.default.createElement(Kpi, { label: "USD Spent", value: fmtUsd(usage?.total_spend ?? 0) })), /* @__PURE__ */ import_react9.default.createElement(import_material8.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react9.default.createElement(Kpi, { label: "Tokens In", value: fmtInt(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ import_react9.default.createElement(import_material8.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react9.default.createElement(Kpi, { label: "Tokens Out", value: fmtInt(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ import_react9.default.createElement(import_material8.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react9.default.createElement(Kpi, { label: "Keys", value: fmtInt(keys.length) }))), hasSparkline && /* @__PURE__ */ import_react9.default.createElement(import_material8.Box, { height: 120 }, /* @__PURE__ */ import_react9.default.createElement(import_recharts3.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react9.default.createElement(import_recharts3.AreaChart, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 } }, /* @__PURE__ */ import_react9.default.createElement(
1714
1834
  import_recharts3.Area,
1715
1835
  {
1716
1836
  type: "monotone",