@acarmisc/backstage-plugin-litellm 0.12.3 → 0.13.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
@@ -174,84 +174,724 @@ var init_api = __esm({
174
174
  }
175
175
  });
176
176
 
177
+ // src/components/ui.tsx
178
+ function chartColor(name) {
179
+ if (name === "Other") return OTHER_COLOR;
180
+ let h = 5381;
181
+ for (let i = 0; i < name.length; i++) h = (h << 5) + h + name.charCodeAt(i) >>> 0;
182
+ return CHART_COLORS[h % CHART_COLORS.length];
183
+ }
184
+ function useChartTheme() {
185
+ const theme = (0, import_styles.useTheme)();
186
+ const tickFill = theme.palette.text.secondary;
187
+ return {
188
+ grid: {
189
+ stroke: (0, import_styles.alpha)(theme.palette.text.primary, theme.palette.mode === "dark" ? 0.12 : 0.08),
190
+ strokeDasharray: "2 4",
191
+ vertical: false
192
+ },
193
+ axis: {
194
+ tick: { fontSize: 11, fill: tickFill },
195
+ tickLine: false,
196
+ axisLine: { stroke: (0, import_styles.alpha)(theme.palette.text.primary, 0.12) }
197
+ },
198
+ legend: {
199
+ wrapperStyle: {
200
+ fontSize: 11,
201
+ color: tickFill,
202
+ paddingTop: 8
203
+ },
204
+ iconType: "circle",
205
+ iconSize: 8
206
+ },
207
+ cursor: { fill: (0, import_styles.alpha)(theme.palette.text.primary, 0.05) }
208
+ };
209
+ }
210
+ function toneColor(theme, tone) {
211
+ switch (tone) {
212
+ case "success":
213
+ return theme.palette.success.main;
214
+ case "warning":
215
+ return theme.palette.warning.main;
216
+ case "danger":
217
+ return theme.palette.error.main;
218
+ case "info":
219
+ return theme.palette.info.main;
220
+ case "accent":
221
+ return theme.palette.primary.main;
222
+ default:
223
+ return theme.palette.text.secondary;
224
+ }
225
+ }
226
+ function SegmentedControl({
227
+ value,
228
+ onChange,
229
+ options
230
+ }) {
231
+ return /* @__PURE__ */ import_react.default.createElement(
232
+ import_Box.default,
233
+ {
234
+ role: "tablist",
235
+ sx: (theme) => ({
236
+ display: "inline-flex",
237
+ gap: 0.25,
238
+ p: 0.375,
239
+ borderRadius: 1.5,
240
+ border: "1px solid",
241
+ borderColor: theme.palette.divider
242
+ })
243
+ },
244
+ options.map((opt) => {
245
+ const selected = opt.value === value;
246
+ return /* @__PURE__ */ import_react.default.createElement(
247
+ import_ButtonBase.default,
248
+ {
249
+ key: opt.value,
250
+ role: "tab",
251
+ "aria-selected": selected,
252
+ onClick: () => onChange(opt.value),
253
+ sx: (theme) => ({
254
+ px: 1.5,
255
+ height: 28,
256
+ borderRadius: 1,
257
+ fontSize: 13,
258
+ fontWeight: selected ? 700 : 500,
259
+ // Tint the selection with the accent rather than swapping in a
260
+ // surface colour: on a dark theme `background.paper` sits *behind*
261
+ // the track, so the selected chip would read as recessed.
262
+ color: selected ? theme.palette.primary.main : theme.palette.text.secondary,
263
+ bgcolor: selected ? (0, import_styles.alpha)(theme.palette.primary.main, theme.palette.mode === "dark" ? 0.2 : 0.11) : "transparent",
264
+ transition: theme.transitions.create(["background-color", "color"]),
265
+ "&:hover": {
266
+ color: selected ? theme.palette.primary.main : theme.palette.text.primary,
267
+ bgcolor: selected ? (0, import_styles.alpha)(theme.palette.primary.main, theme.palette.mode === "dark" ? 0.26 : 0.15) : (0, import_styles.alpha)(theme.palette.text.primary, 0.05)
268
+ }
269
+ })
270
+ },
271
+ opt.label
272
+ );
273
+ })
274
+ );
275
+ }
276
+ var import_react, import_Box, import_ButtonBase, import_Paper, import_Typography, import_styles, MONTHS, fmtDateShort, fmtCompact, fmtUsdCompact, CHART_COLORS, SERIES, OTHER_COLOR, ChartTooltip, SeriesLegend, StatusPill, TagChip, SectionCard, ChartCard, MetricStrip, Stat, Meter, dataTableSx, quietIconButtonSx, EmptyState;
277
+ var init_ui = __esm({
278
+ "src/components/ui.tsx"() {
279
+ "use strict";
280
+ import_react = __toESM(require("react"));
281
+ import_Box = __toESM(require("@mui/material/Box"));
282
+ import_ButtonBase = __toESM(require("@mui/material/ButtonBase"));
283
+ import_Paper = __toESM(require("@mui/material/Paper"));
284
+ import_Typography = __toESM(require("@mui/material/Typography"));
285
+ import_styles = require("@mui/material/styles");
286
+ MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
287
+ fmtDateShort = (value) => {
288
+ const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(value ?? "");
289
+ if (!m) return value;
290
+ return `${MONTHS[Number(m[2]) - 1]} ${Number(m[3])}`;
291
+ };
292
+ fmtCompact = (n) => {
293
+ const v = n ?? 0;
294
+ const abs = Math.abs(v);
295
+ if (abs >= 1e9) return `${(v / 1e9).toFixed(abs >= 1e10 ? 0 : 1)}B`;
296
+ if (abs >= 1e6) return `${(v / 1e6).toFixed(abs >= 1e7 ? 0 : 1)}M`;
297
+ if (abs >= 1e3) return `${(v / 1e3).toFixed(abs >= 1e4 ? 0 : 1)}K`;
298
+ return String(Math.round(v));
299
+ };
300
+ fmtUsdCompact = (n) => {
301
+ const v = n ?? 0;
302
+ if (Math.abs(v) >= 1e3) return `$${fmtCompact(v)}`;
303
+ if (Math.abs(v) >= 10) return `$${v.toFixed(0)}`;
304
+ return `$${v.toFixed(2)}`;
305
+ };
306
+ CHART_COLORS = [
307
+ "#6366F1",
308
+ // indigo
309
+ "#14B8A6",
310
+ // teal
311
+ "#F59E0B",
312
+ // amber
313
+ "#EC4899",
314
+ // pink
315
+ "#38BDF8",
316
+ // sky
317
+ "#84CC16",
318
+ // lime
319
+ "#F97316",
320
+ // orange
321
+ "#A855F7"
322
+ // violet
323
+ ];
324
+ SERIES = {
325
+ input: "#6366F1",
326
+ output: "#14B8A6",
327
+ success: "#10B981",
328
+ failure: "#EF4444",
329
+ spend: "#F59E0B",
330
+ budget: "#EF4444"
331
+ };
332
+ OTHER_COLOR = "#94A3B8";
333
+ ChartTooltip = ({
334
+ active,
335
+ label,
336
+ payload,
337
+ valueFormatter = (v) => v.toLocaleString(),
338
+ labelFormatter = fmtDateShort,
339
+ hideZero = false
340
+ }) => {
341
+ if (!active || !payload?.length) return null;
342
+ const rows = payload.filter(
343
+ (p) => p.value !== void 0 && p.value !== null && (!hideZero || p.value !== 0)
344
+ );
345
+ if (!rows.length) return null;
346
+ return /* @__PURE__ */ import_react.default.createElement(
347
+ import_Paper.default,
348
+ {
349
+ elevation: 8,
350
+ sx: {
351
+ px: 1.5,
352
+ py: 1,
353
+ borderRadius: 1.5,
354
+ border: "1px solid",
355
+ borderColor: "divider",
356
+ minWidth: 150,
357
+ pointerEvents: "none"
358
+ }
359
+ },
360
+ /* @__PURE__ */ import_react.default.createElement(
361
+ import_Typography.default,
362
+ {
363
+ variant: "caption",
364
+ sx: { display: "block", fontWeight: 700, mb: 0.75, letterSpacing: "0.02em" }
365
+ },
366
+ typeof label === "string" ? labelFormatter(label) : label
367
+ ),
368
+ rows.map((row, i) => /* @__PURE__ */ import_react.default.createElement(
369
+ import_Box.default,
370
+ {
371
+ key: `${row.dataKey ?? row.name ?? i}`,
372
+ sx: { display: "flex", alignItems: "center", gap: 1, mt: i === 0 ? 0 : 0.5 }
373
+ },
374
+ /* @__PURE__ */ import_react.default.createElement(
375
+ import_Box.default,
376
+ {
377
+ sx: {
378
+ width: 8,
379
+ height: 8,
380
+ borderRadius: "50%",
381
+ bgcolor: row.color,
382
+ flexShrink: 0
383
+ }
384
+ }
385
+ ),
386
+ /* @__PURE__ */ import_react.default.createElement(
387
+ import_Typography.default,
388
+ {
389
+ variant: "caption",
390
+ color: "text.secondary",
391
+ sx: { flex: 1, mr: 1.5, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }
392
+ },
393
+ row.name
394
+ ),
395
+ /* @__PURE__ */ import_react.default.createElement(import_Typography.default, { variant: "caption", sx: { fontWeight: 700, fontVariantNumeric: "tabular-nums" } }, valueFormatter(row.value))
396
+ ))
397
+ );
398
+ };
399
+ SeriesLegend = ({
400
+ series
401
+ }) => /* @__PURE__ */ import_react.default.createElement(
402
+ import_Box.default,
403
+ {
404
+ sx: {
405
+ display: "flex",
406
+ flexWrap: "wrap",
407
+ gap: 0.5,
408
+ columnGap: 2,
409
+ rowGap: 0.5,
410
+ mt: 1.5
411
+ }
412
+ },
413
+ series.map((s) => /* @__PURE__ */ import_react.default.createElement(import_Box.default, { key: s.name, sx: { display: "flex", alignItems: "center", gap: 0.75, minWidth: 0 } }, /* @__PURE__ */ import_react.default.createElement(
414
+ import_Box.default,
415
+ {
416
+ sx: { width: 8, height: 8, borderRadius: "50%", bgcolor: s.color, flexShrink: 0 }
417
+ }
418
+ ), /* @__PURE__ */ import_react.default.createElement(
419
+ import_Typography.default,
420
+ {
421
+ variant: "caption",
422
+ color: "text.secondary",
423
+ title: s.name,
424
+ sx: {
425
+ fontSize: 11,
426
+ maxWidth: 190,
427
+ overflow: "hidden",
428
+ textOverflow: "ellipsis",
429
+ whiteSpace: "nowrap"
430
+ }
431
+ },
432
+ s.name
433
+ )))
434
+ );
435
+ StatusPill = ({ label, tone = "neutral", dot = true, title, sx }) => /* @__PURE__ */ import_react.default.createElement(
436
+ import_Box.default,
437
+ {
438
+ component: "span",
439
+ title,
440
+ sx: [
441
+ (theme) => {
442
+ const c = toneColor(theme, tone);
443
+ return {
444
+ display: "inline-flex",
445
+ alignItems: "center",
446
+ gap: 0.625,
447
+ height: 22,
448
+ px: 0.875,
449
+ borderRadius: "6px",
450
+ border: "1px solid",
451
+ borderColor: (0, import_styles.alpha)(c, tone === "neutral" ? 0.25 : 0.35),
452
+ bgcolor: (0, import_styles.alpha)(c, tone === "neutral" ? 0.06 : 0.12),
453
+ color: tone === "neutral" ? theme.palette.text.secondary : c,
454
+ fontSize: 11.5,
455
+ fontWeight: 600,
456
+ lineHeight: 1,
457
+ letterSpacing: "0.01em",
458
+ whiteSpace: "nowrap",
459
+ maxWidth: "100%"
460
+ };
461
+ },
462
+ ...Array.isArray(sx) ? sx : [sx]
463
+ ]
464
+ },
465
+ dot && /* @__PURE__ */ import_react.default.createElement(
466
+ import_Box.default,
467
+ {
468
+ component: "span",
469
+ sx: (theme) => ({
470
+ width: 6,
471
+ height: 6,
472
+ borderRadius: "50%",
473
+ bgcolor: toneColor(theme, tone),
474
+ flexShrink: 0
475
+ })
476
+ }
477
+ ),
478
+ /* @__PURE__ */ import_react.default.createElement(import_Box.default, { component: "span", sx: { overflow: "hidden", textOverflow: "ellipsis" } }, label)
479
+ );
480
+ TagChip = ({
481
+ label,
482
+ title,
483
+ mono = true
484
+ }) => /* @__PURE__ */ import_react.default.createElement(
485
+ import_Box.default,
486
+ {
487
+ component: "span",
488
+ title,
489
+ sx: (theme) => ({
490
+ display: "inline-flex",
491
+ alignItems: "center",
492
+ height: 20,
493
+ px: 0.75,
494
+ borderRadius: "5px",
495
+ bgcolor: (0, import_styles.alpha)(theme.palette.text.primary, theme.palette.mode === "dark" ? 0.08 : 0.05),
496
+ color: theme.palette.text.secondary,
497
+ fontFamily: mono ? "ui-monospace, SFMono-Regular, Menlo, monospace" : void 0,
498
+ fontSize: 11,
499
+ lineHeight: 1,
500
+ whiteSpace: "nowrap",
501
+ maxWidth: 220,
502
+ overflow: "hidden",
503
+ textOverflow: "ellipsis"
504
+ })
505
+ },
506
+ label
507
+ );
508
+ SectionCard = ({ title, subtitle, actions, flush = false, children }) => /* @__PURE__ */ import_react.default.createElement(import_Paper.default, { sx: { borderRadius: 2, overflow: "hidden" } }, (title || actions) && /* @__PURE__ */ import_react.default.createElement(
509
+ import_Box.default,
510
+ {
511
+ sx: {
512
+ display: "flex",
513
+ alignItems: "center",
514
+ justifyContent: "space-between",
515
+ gap: 2,
516
+ flexWrap: "wrap",
517
+ px: 2.5,
518
+ py: 2
519
+ }
520
+ },
521
+ /* @__PURE__ */ import_react.default.createElement(import_Box.default, { sx: { minWidth: 0 } }, /* @__PURE__ */ import_react.default.createElement(import_Typography.default, { variant: "h6", sx: { lineHeight: 1.2 } }, title), subtitle && /* @__PURE__ */ import_react.default.createElement(import_Typography.default, { variant: "caption", color: "text.secondary" }, subtitle)),
522
+ actions && /* @__PURE__ */ import_react.default.createElement(import_Box.default, { sx: { display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" } }, actions)
523
+ ), /* @__PURE__ */ import_react.default.createElement(import_Box.default, { sx: flush ? void 0 : { px: 2.5, pb: 2.5, pt: title ? 0 : 2.5 } }, children));
524
+ ChartCard = ({ title, meta, height = 240, children }) => (
525
+ // The plot area must have an explicit height, never a flex-derived one:
526
+ // Recharts' ResponsiveContainer measures its parent, so a parent that sizes
527
+ // itself from its children instead spins in a measure/resize loop.
528
+ /* @__PURE__ */ import_react.default.createElement(import_Paper.default, { variant: "outlined", sx: { p: 2, borderRadius: 2, height: "100%" } }, /* @__PURE__ */ import_react.default.createElement(import_Box.default, { sx: { display: "flex", alignItems: "baseline", gap: 1, mb: 1.5, flexWrap: "wrap" } }, /* @__PURE__ */ import_react.default.createElement(
529
+ import_Typography.default,
530
+ {
531
+ variant: "subtitle2",
532
+ sx: { fontSize: 12, fontWeight: 700, letterSpacing: "0.06em", textTransform: "uppercase" }
533
+ },
534
+ title
535
+ ), meta && /* @__PURE__ */ import_react.default.createElement(import_Typography.default, { variant: "caption", color: "text.secondary" }, meta)), /* @__PURE__ */ import_react.default.createElement(import_Box.default, { sx: { height, width: "100%" } }, children))
536
+ );
537
+ MetricStrip = ({ metrics }) => /* @__PURE__ */ import_react.default.createElement(
538
+ import_Paper.default,
539
+ {
540
+ variant: "outlined",
541
+ sx: {
542
+ borderRadius: 2,
543
+ display: "grid",
544
+ gridTemplateColumns: { xs: "1fr 1fr", md: `repeat(${metrics.length}, 1fr)` },
545
+ overflow: "hidden"
546
+ }
547
+ },
548
+ metrics.map((m, i) => /* @__PURE__ */ import_react.default.createElement(
549
+ import_Box.default,
550
+ {
551
+ key: m.label,
552
+ sx: (theme) => ({
553
+ px: 2.5,
554
+ py: 2,
555
+ borderLeft: i === 0 ? 0 : "1px solid",
556
+ borderTop: 0,
557
+ borderColor: "divider",
558
+ [theme.breakpoints.down("md")]: {
559
+ borderLeft: i % 2 === 0 ? 0 : "1px solid",
560
+ borderTop: i < 2 ? 0 : "1px solid",
561
+ borderColor: "divider"
562
+ }
563
+ })
564
+ },
565
+ /* @__PURE__ */ import_react.default.createElement(import_Box.default, { sx: { display: "flex", alignItems: "center", gap: 0.875, mb: 0.75 } }, /* @__PURE__ */ import_react.default.createElement(
566
+ import_Box.default,
567
+ {
568
+ sx: (theme) => ({
569
+ width: 6,
570
+ height: 6,
571
+ borderRadius: "50%",
572
+ bgcolor: toneColor(theme, m.tone ?? "accent"),
573
+ flexShrink: 0
574
+ })
575
+ }
576
+ ), /* @__PURE__ */ import_react.default.createElement(
577
+ import_Typography.default,
578
+ {
579
+ variant: "caption",
580
+ color: "text.secondary",
581
+ sx: { fontSize: 11, fontWeight: 600, letterSpacing: "0.07em", textTransform: "uppercase" }
582
+ },
583
+ m.label
584
+ )),
585
+ /* @__PURE__ */ import_react.default.createElement(
586
+ import_Typography.default,
587
+ {
588
+ sx: { fontSize: 26, fontWeight: 700, lineHeight: 1.15, fontVariantNumeric: "tabular-nums" }
589
+ },
590
+ m.value
591
+ ),
592
+ /* @__PURE__ */ import_react.default.createElement(
593
+ import_Typography.default,
594
+ {
595
+ variant: "caption",
596
+ color: "text.secondary",
597
+ sx: { display: "block", mt: 0.5, minHeight: 16 }
598
+ },
599
+ m.hint ?? "\xA0"
600
+ )
601
+ ))
602
+ );
603
+ Stat = ({ label, value }) => /* @__PURE__ */ import_react.default.createElement(import_Box.default, { sx: { minWidth: 0 } }, /* @__PURE__ */ import_react.default.createElement(
604
+ import_Typography.default,
605
+ {
606
+ variant: "caption",
607
+ color: "text.secondary",
608
+ sx: { display: "block", fontSize: 10.5, fontWeight: 600, letterSpacing: "0.07em", textTransform: "uppercase" }
609
+ },
610
+ label
611
+ ), /* @__PURE__ */ import_react.default.createElement(
612
+ import_Typography.default,
613
+ {
614
+ variant: "body2",
615
+ sx: { fontWeight: 600, fontVariantNumeric: "tabular-nums", mt: 0.25 }
616
+ },
617
+ value
618
+ ));
619
+ Meter = ({
620
+ value,
621
+ tone = "accent",
622
+ height = 5
623
+ }) => /* @__PURE__ */ import_react.default.createElement(
624
+ import_Box.default,
625
+ {
626
+ sx: (theme) => ({
627
+ height,
628
+ borderRadius: height,
629
+ bgcolor: (0, import_styles.alpha)(theme.palette.text.primary, theme.palette.mode === "dark" ? 0.12 : 0.08),
630
+ overflow: "hidden"
631
+ })
632
+ },
633
+ /* @__PURE__ */ import_react.default.createElement(
634
+ import_Box.default,
635
+ {
636
+ sx: (theme) => ({
637
+ height: "100%",
638
+ width: `${Math.max(0, Math.min(100, value))}%`,
639
+ borderRadius: height,
640
+ bgcolor: toneColor(theme, tone),
641
+ transition: theme.transitions.create("width")
642
+ })
643
+ }
644
+ )
645
+ );
646
+ dataTableSx = (theme) => ({
647
+ // Element selectors, not `.MuiTableCell-head`: host apps may set a MUI
648
+ // classname prefix (Backstage ships `v5-`), which breaks global class targeting.
649
+ "& thead th": {
650
+ fontSize: 11,
651
+ fontWeight: 700,
652
+ letterSpacing: "0.07em",
653
+ textTransform: "uppercase",
654
+ color: theme.palette.text.secondary,
655
+ backgroundColor: (0, import_styles.alpha)(theme.palette.text.primary, theme.palette.mode === "dark" ? 0.04 : 0.02),
656
+ borderBottom: `1px solid ${theme.palette.divider}`,
657
+ whiteSpace: "nowrap",
658
+ paddingTop: theme.spacing(1.25),
659
+ paddingBottom: theme.spacing(1.25)
660
+ },
661
+ "& tbody td": {
662
+ borderBottom: `1px solid ${(0, import_styles.alpha)(theme.palette.divider, 0.6)}`,
663
+ paddingTop: theme.spacing(1.25),
664
+ paddingBottom: theme.spacing(1.25),
665
+ fontVariantNumeric: "tabular-nums"
666
+ },
667
+ "& tbody tr:last-of-type td": {
668
+ borderBottom: 0
669
+ },
670
+ "& tbody tr:hover": {
671
+ backgroundColor: (0, import_styles.alpha)(theme.palette.text.primary, 0.03)
672
+ }
673
+ });
674
+ quietIconButtonSx = (tone = "neutral") => (theme) => ({
675
+ color: theme.palette.text.secondary,
676
+ "&:hover": {
677
+ color: toneColor(theme, tone),
678
+ bgcolor: (0, import_styles.alpha)(toneColor(theme, tone), 0.1)
679
+ }
680
+ });
681
+ EmptyState = ({ message, hint, height, action }) => /* @__PURE__ */ import_react.default.createElement(
682
+ import_Box.default,
683
+ {
684
+ sx: {
685
+ height,
686
+ minHeight: height ? void 0 : 120,
687
+ display: "flex",
688
+ flexDirection: "column",
689
+ alignItems: "center",
690
+ justifyContent: "center",
691
+ gap: 1,
692
+ textAlign: "center",
693
+ py: 3
694
+ }
695
+ },
696
+ /* @__PURE__ */ import_react.default.createElement(import_Typography.default, { variant: "body2", color: "text.secondary" }, message),
697
+ hint && /* @__PURE__ */ import_react.default.createElement(import_Typography.default, { variant: "caption", color: "text.secondary" }, hint),
698
+ action
699
+ );
700
+ }
701
+ });
702
+
177
703
  // src/components/DashboardHeader.tsx
178
- var import_react, import_material, import_icons_material, DashboardHeader;
704
+ function initials(name) {
705
+ const local = name.split("@")[0] ?? name;
706
+ const parts = local.split(/[.\-_\s]+/).filter(Boolean);
707
+ if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
708
+ return local.slice(0, 2).toUpperCase();
709
+ }
710
+ var import_react2, import_Box2, import_Typography2, import_Skeleton, import_Paper2, import_Button, import_Divider, import_styles2, import_icons_material, CountStat, DashboardHeader;
179
711
  var init_DashboardHeader = __esm({
180
712
  "src/components/DashboardHeader.tsx"() {
181
713
  "use strict";
182
- import_react = __toESM(require("react"));
183
- import_material = require("@mui/material");
714
+ import_react2 = __toESM(require("react"));
715
+ import_Box2 = __toESM(require("@mui/material/Box"));
716
+ import_Typography2 = __toESM(require("@mui/material/Typography"));
717
+ import_Skeleton = __toESM(require("@mui/material/Skeleton"));
718
+ import_Paper2 = __toESM(require("@mui/material/Paper"));
719
+ import_Button = __toESM(require("@mui/material/Button"));
720
+ import_Divider = __toESM(require("@mui/material/Divider"));
721
+ import_styles2 = require("@mui/material/styles");
184
722
  import_icons_material = require("@mui/icons-material");
185
723
  init_api();
724
+ init_ui();
725
+ CountStat = ({ value, label, tone }) => {
726
+ const active = value > 0;
727
+ return /* @__PURE__ */ import_react2.default.createElement(import_Box2.default, { sx: { display: "flex", alignItems: "center", gap: 1 } }, /* @__PURE__ */ import_react2.default.createElement(
728
+ import_Box2.default,
729
+ {
730
+ sx: (theme) => ({
731
+ width: 7,
732
+ height: 7,
733
+ borderRadius: "50%",
734
+ flexShrink: 0,
735
+ bgcolor: active ? tone === "danger" && theme.palette.error.main || tone === "warning" && theme.palette.warning.main || theme.palette.primary.main : (0, import_styles2.alpha)(theme.palette.text.primary, 0.25)
736
+ })
737
+ }
738
+ ), /* @__PURE__ */ import_react2.default.createElement(
739
+ import_Typography2.default,
740
+ {
741
+ component: "span",
742
+ sx: { fontSize: 15, fontWeight: 700, lineHeight: 1, fontVariantNumeric: "tabular-nums" }
743
+ },
744
+ value
745
+ ), /* @__PURE__ */ import_react2.default.createElement(
746
+ import_Typography2.default,
747
+ {
748
+ component: "span",
749
+ color: "text.secondary",
750
+ sx: { fontSize: 11, fontWeight: 600, letterSpacing: "0.07em", textTransform: "uppercase" }
751
+ },
752
+ label
753
+ ));
754
+ };
186
755
  DashboardHeader = ({
187
756
  userInfo,
188
757
  teams,
189
758
  keys,
190
759
  loading,
191
- onGenerateKeyClick
760
+ onGenerateKeyClick,
761
+ tabs
192
762
  }) => {
193
- const counts = (0, import_react.useMemo)(() => {
763
+ const counts = (0, import_react2.useMemo)(() => {
194
764
  const expired = keys.filter((k) => expiryStatus(k.expires_at) === "expired").length;
195
765
  const expiringSoon = keys.filter((k) => expiryStatus(k.expires_at) === "soon").length;
196
766
  return { total: keys.length, expired, expiringSoon };
197
767
  }, [keys]);
198
- if (loading) {
199
- return /* @__PURE__ */ import_react.default.createElement(import_material.Paper, { sx: { p: 2, mb: 2 } }, /* @__PURE__ */ import_react.default.createElement(import_material.LinearProgress, null));
200
- }
201
768
  const displayName = userInfo.user_email ?? userInfo.email ?? userInfo.user_id;
202
- return /* @__PURE__ */ import_react.default.createElement(import_material.Paper, { sx: { p: 2, mb: 2 } }, /* @__PURE__ */ import_react.default.createElement(import_material.Box, { display: "flex", alignItems: "flex-start", gap: 2, flexWrap: "wrap" }, /* @__PURE__ */ import_react.default.createElement(import_material.Box, { flexGrow: 1 }, /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "h6" }, displayName), /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "caption", color: "text.secondary" }, userInfo.user_id), teams.length > 0 && /* @__PURE__ */ import_react.default.createElement(import_material.Box, { display: "flex", gap: 0.75, flexWrap: "wrap", mt: 1 }, teams.map((team) => /* @__PURE__ */ import_react.default.createElement(
203
- import_material.Chip,
769
+ return /* @__PURE__ */ import_react2.default.createElement(import_Paper2.default, { sx: { borderRadius: 2, overflow: "hidden" } }, /* @__PURE__ */ import_react2.default.createElement(
770
+ import_Box2.default,
204
771
  {
205
- key: team.team_id,
206
- label: team.team_alias || team.team_id,
207
- size: "small",
208
- variant: "outlined",
209
- color: "primary"
210
- }
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,
772
+ sx: {
773
+ display: "flex",
774
+ alignItems: "flex-start",
775
+ gap: 2,
776
+ flexWrap: "wrap",
777
+ px: 2.5,
778
+ pt: 2.5,
779
+ pb: 2
780
+ }
781
+ },
782
+ /* @__PURE__ */ import_react2.default.createElement(
783
+ import_Box2.default,
784
+ {
785
+ sx: (theme) => ({
786
+ width: 44,
787
+ height: 44,
788
+ borderRadius: 1.5,
789
+ flexShrink: 0,
790
+ display: "flex",
791
+ alignItems: "center",
792
+ justifyContent: "center",
793
+ bgcolor: (0, import_styles2.alpha)(theme.palette.primary.main, 0.12),
794
+ color: theme.palette.primary.main,
795
+ fontSize: 15,
796
+ fontWeight: 700,
797
+ letterSpacing: "0.02em"
798
+ })
799
+ },
800
+ initials(displayName)
801
+ ),
802
+ /* @__PURE__ */ import_react2.default.createElement(import_Box2.default, { sx: { flexGrow: 1, minWidth: 220 } }, /* @__PURE__ */ import_react2.default.createElement(import_Typography2.default, { variant: "h6", sx: { lineHeight: 1.25, wordBreak: "break-word" } }, displayName), /* @__PURE__ */ import_react2.default.createElement(
803
+ import_Typography2.default,
804
+ {
805
+ variant: "caption",
806
+ color: "text.secondary",
807
+ sx: { display: "block", fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace" }
808
+ },
809
+ userInfo.user_id
810
+ ), /* @__PURE__ */ import_react2.default.createElement(import_Box2.default, { sx: { display: "flex", gap: 0.75, flexWrap: "wrap", mt: 1, minHeight: 20 } }, loading ? /* @__PURE__ */ import_react2.default.createElement(import_Skeleton.default, { variant: "rounded", width: 140, height: 20 }) : teams.map((team) => /* @__PURE__ */ import_react2.default.createElement(
811
+ TagChip,
812
+ {
813
+ key: team.team_id,
814
+ label: team.team_alias || team.team_id,
815
+ title: team.team_id
816
+ }
817
+ )))),
818
+ /* @__PURE__ */ import_react2.default.createElement(
819
+ import_Button.default,
820
+ {
821
+ variant: "contained",
822
+ color: "primary",
823
+ disableElevation: true,
824
+ startIcon: /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Add, null),
825
+ onClick: onGenerateKeyClick
826
+ },
827
+ "Generate New Key"
828
+ )
829
+ ), /* @__PURE__ */ import_react2.default.createElement(import_Divider.default, null), /* @__PURE__ */ import_react2.default.createElement(
830
+ import_Box2.default,
229
831
  {
230
- variant: "contained",
231
- color: "primary",
232
- startIcon: /* @__PURE__ */ import_react.default.createElement(import_icons_material.Add, null),
233
- onClick: onGenerateKeyClick
832
+ sx: {
833
+ display: "flex",
834
+ alignItems: "center",
835
+ gap: 2.5,
836
+ flexWrap: "wrap",
837
+ px: 2.5,
838
+ py: 1.5
839
+ }
234
840
  },
235
- "Generate New Key"
236
- ))));
841
+ /* @__PURE__ */ import_react2.default.createElement(CountStat, { value: counts.total, label: "keys", tone: "accent" }),
842
+ /* @__PURE__ */ import_react2.default.createElement(import_Divider.default, { orientation: "vertical", flexItem: true, sx: { my: 0.25 } }),
843
+ /* @__PURE__ */ import_react2.default.createElement(CountStat, { value: counts.expired, label: "expired", tone: "danger" }),
844
+ /* @__PURE__ */ import_react2.default.createElement(import_Divider.default, { orientation: "vertical", flexItem: true, sx: { my: 0.25 } }),
845
+ /* @__PURE__ */ import_react2.default.createElement(CountStat, { value: counts.expiringSoon, label: "expiring soon", tone: "warning" })
846
+ ), tabs && /* @__PURE__ */ import_react2.default.createElement(import_react2.default.Fragment, null, /* @__PURE__ */ import_react2.default.createElement(import_Divider.default, null), /* @__PURE__ */ import_react2.default.createElement(import_Box2.default, { sx: { px: 1 } }, tabs)));
237
847
  };
238
848
  }
239
849
  });
240
850
 
241
851
  // src/components/KeysTable.tsx
242
- function ExpiryChip({ expiresAt }) {
852
+ function expiryChipLabel(status, expiresAt) {
853
+ if (status === "expired") return "Expired";
854
+ if (status === "soon") return `${Math.ceil((new Date(expiresAt).getTime() - Date.now()) / 864e5)}d left`;
855
+ return formatDate(expiresAt);
856
+ }
857
+ function expiryTone(status) {
858
+ if (status === "expired") return "danger";
859
+ if (status === "soon") return "warning";
860
+ return "neutral";
861
+ }
862
+ function ExpiryCell({ expiresAt }) {
243
863
  const status = expiryStatus(expiresAt);
244
- if (!status) return /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2", color: "text.secondary" }, "-");
245
- const label = status === "expired" ? "Expired" : status === "soon" ? `${Math.ceil((new Date(expiresAt).getTime() - Date.now()) / 864e5)}d left` : formatDate(expiresAt);
246
- const color = status === "expired" ? "error" : status === "soon" ? "warning" : "default";
247
- const icon = status === "expired" || status === "soon" ? /* @__PURE__ */ import_react2.default.createElement(import_icons_material2.Warning, { fontSize: "small" }) : void 0;
248
- return /* @__PURE__ */ import_react2.default.createElement(import_material2.Chip, { label, color, size: "small", icon });
864
+ if (!status) {
865
+ return /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, { variant: "body2", color: "text.secondary" }, "Never");
866
+ }
867
+ if (status === "ok") {
868
+ return /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, { variant: "body2", color: "text.secondary" }, formatDate(expiresAt));
869
+ }
870
+ return /* @__PURE__ */ import_react3.default.createElement(
871
+ StatusPill,
872
+ {
873
+ label: expiryChipLabel(status, expiresAt),
874
+ tone: expiryTone(status),
875
+ title: `Expires ${formatDate(expiresAt)}`
876
+ }
877
+ );
878
+ }
879
+ function budgetTone(pct) {
880
+ if (pct >= 100) return "danger";
881
+ if (pct >= 80) return "warning";
882
+ return "accent";
883
+ }
884
+ function keyBlockIcon(isBlocking, blocked) {
885
+ if (isBlocking) return /* @__PURE__ */ import_react3.default.createElement(import_CircularProgress.default, { size: 18 });
886
+ if (blocked) return /* @__PURE__ */ import_react3.default.createElement(import_icons_material2.LockOpen, { fontSize: "small" });
887
+ return /* @__PURE__ */ import_react3.default.createElement(import_icons_material2.Lock, { fontSize: "small" });
249
888
  }
250
889
  function BudgetCell({ spend, maxBudget }) {
251
- if (!maxBudget) return /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2", color: "text.secondary" }, "-");
890
+ if (!maxBudget) {
891
+ return /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, { variant: "body2", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, "$", spend.toFixed(2), /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, { component: "span", variant: "caption", sx: { ml: 0.5, opacity: 0.7 } }, "/ \u221E"));
892
+ }
252
893
  const pct = Math.min(100, spend / maxBudget * 100);
253
- const color = pct >= 100 ? "error" : pct >= 80 ? "warning" : "primary";
254
- return /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { minWidth: 100 }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", justifyContent: "space-between" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "caption" }, "$", spend.toFixed(2)), /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "caption", color: "text.secondary" }, "$", maxBudget)), /* @__PURE__ */ import_react2.default.createElement(import_material2.LinearProgress, { variant: "determinate", value: pct, color, sx: { height: 5, borderRadius: 1, mt: 0.25 } }));
894
+ return /* @__PURE__ */ import_react3.default.createElement(import_Box3.default, { minWidth: 104 }, /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, { variant: "body2", sx: { fontVariantNumeric: "tabular-nums", mb: 0.5 } }, "$", spend.toFixed(2), /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, { component: "span", variant: "caption", color: "text.secondary", sx: { ml: 0.5 } }, "/ $", maxBudget)), /* @__PURE__ */ import_react3.default.createElement(Meter, { value: pct, tone: budgetTone(pct), height: 4 }));
255
895
  }
256
896
  function fmtCost(perToken) {
257
897
  if (!perToken) return null;
@@ -273,14 +913,34 @@ function formatContextWindow(maxInput, maxOutput) {
273
913
  if (outPart) return `ctx ${outPart} out`;
274
914
  return null;
275
915
  }
276
- var import_react2, import_material2, import_icons_material2, maskKey, shortKeyId, formatDate, keyToEditForm, KeysTable;
916
+ var import_react3, import_Table, import_TableBody, import_TableCell, import_TableContainer, import_TableHead, import_TableRow, import_Button2, import_IconButton, import_Box3, import_Typography3, import_Dialog, import_DialogTitle, import_DialogContent, import_DialogActions, import_TextField, import_CircularProgress, import_Autocomplete, import_Skeleton2, import_InputAdornment, import_styles3, import_icons_material2, maskKey, shortKeyId, formatDate, keyToEditForm, KeysTable;
277
917
  var init_KeysTable = __esm({
278
918
  "src/components/KeysTable.tsx"() {
279
919
  "use strict";
280
- import_react2 = __toESM(require("react"));
281
- import_material2 = require("@mui/material");
920
+ import_react3 = __toESM(require("react"));
921
+ import_Table = __toESM(require("@mui/material/Table"));
922
+ import_TableBody = __toESM(require("@mui/material/TableBody"));
923
+ import_TableCell = __toESM(require("@mui/material/TableCell"));
924
+ import_TableContainer = __toESM(require("@mui/material/TableContainer"));
925
+ import_TableHead = __toESM(require("@mui/material/TableHead"));
926
+ import_TableRow = __toESM(require("@mui/material/TableRow"));
927
+ import_Button2 = __toESM(require("@mui/material/Button"));
928
+ import_IconButton = __toESM(require("@mui/material/IconButton"));
929
+ import_Box3 = __toESM(require("@mui/material/Box"));
930
+ import_Typography3 = __toESM(require("@mui/material/Typography"));
931
+ import_Dialog = __toESM(require("@mui/material/Dialog"));
932
+ import_DialogTitle = __toESM(require("@mui/material/DialogTitle"));
933
+ import_DialogContent = __toESM(require("@mui/material/DialogContent"));
934
+ import_DialogActions = __toESM(require("@mui/material/DialogActions"));
935
+ import_TextField = __toESM(require("@mui/material/TextField"));
936
+ import_CircularProgress = __toESM(require("@mui/material/CircularProgress"));
937
+ import_Autocomplete = __toESM(require("@mui/material/Autocomplete"));
938
+ import_Skeleton2 = __toESM(require("@mui/material/Skeleton"));
939
+ import_InputAdornment = __toESM(require("@mui/material/InputAdornment"));
940
+ import_styles3 = require("@mui/material/styles");
282
941
  import_icons_material2 = require("@mui/icons-material");
283
942
  init_api();
943
+ init_ui();
284
944
  maskKey = (key) => {
285
945
  if (key.length <= 8) return "***";
286
946
  return `${key.slice(0, 4)}...${key.slice(-4)}`;
@@ -316,16 +976,16 @@ var init_KeysTable = __esm({
316
976
  onDeleteKey,
317
977
  onPruneExpiredKeys
318
978
  }) => {
319
- const [editingKey, setEditingKey] = (0, import_react2.useState)(null);
320
- const [editForm, setEditForm] = (0, import_react2.useState)({});
321
- const [editSubmitting, setEditSubmitting] = (0, import_react2.useState)(false);
322
- const [blockingKeyId, setBlockingKeyId] = (0, import_react2.useState)(null);
323
- const [deleteConfirmId, setDeleteConfirmId] = (0, import_react2.useState)(null);
324
- const [deleteSubmitting, setDeleteSubmitting] = (0, import_react2.useState)(false);
325
- const [resetSpendConfirm, setResetSpendConfirm] = (0, import_react2.useState)(false);
326
- const [resetSpendSubmitting, setResetSpendSubmitting] = (0, import_react2.useState)(false);
327
- const [filterText, setFilterText] = (0, import_react2.useState)("");
328
- const filteredKeys = (0, import_react2.useMemo)(() => {
979
+ const [editingKey, setEditingKey] = (0, import_react3.useState)(null);
980
+ const [editForm, setEditForm] = (0, import_react3.useState)({});
981
+ const [editSubmitting, setEditSubmitting] = (0, import_react3.useState)(false);
982
+ const [blockingKeyId, setBlockingKeyId] = (0, import_react3.useState)(null);
983
+ const [deleteConfirmId, setDeleteConfirmId] = (0, import_react3.useState)(null);
984
+ const [deleteSubmitting, setDeleteSubmitting] = (0, import_react3.useState)(false);
985
+ const [resetSpendConfirm, setResetSpendConfirm] = (0, import_react3.useState)(false);
986
+ const [resetSpendSubmitting, setResetSpendSubmitting] = (0, import_react3.useState)(false);
987
+ const [filterText, setFilterText] = (0, import_react3.useState)("");
988
+ const filteredKeys = (0, import_react3.useMemo)(() => {
329
989
  if (!filterText.trim()) return keys;
330
990
  const q = filterText.toLowerCase();
331
991
  return keys.filter(
@@ -391,9 +1051,9 @@ var init_KeysTable = __esm({
391
1051
  const copyToClipboard = (text) => {
392
1052
  navigator.clipboard.writeText(text);
393
1053
  };
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)(() => {
1054
+ const [pruneConfirmCount, setPruneConfirmCount] = (0, import_react3.useState)(null);
1055
+ const [pruneSubmitting, setPruneSubmitting] = (0, import_react3.useState)(false);
1056
+ const expiredKeys = (0, import_react3.useMemo)(() => {
397
1057
  return keys.filter((k) => expiryStatus(k.expires_at) === "expired");
398
1058
  }, [keys]);
399
1059
  const handlePruneExpired = async () => {
@@ -412,97 +1072,188 @@ var init_KeysTable = __esm({
412
1072
  const inCost = fmtCost(m.input_cost_per_token);
413
1073
  const outCost = fmtCost(m.output_cost_per_token);
414
1074
  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"));
1075
+ return /* @__PURE__ */ import_react3.default.createElement(import_Box3.default, 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_Typography3.default, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, ctx), (inCost || outCost) && /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, inCost, " in \xB7 ", outCost, " out"));
416
1076
  };
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
- }
1077
+ const renderTableBody = () => {
1078
+ if (loading) {
1079
+ return /* @__PURE__ */ import_react3.default.createElement(import_TableRow.default, null, /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, { colSpan: 8, sx: { py: 2 } }, /* @__PURE__ */ import_react3.default.createElement(import_Skeleton2.default, { variant: "rounded", height: 140 })));
428
1080
  }
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,
1081
+ if (filteredKeys.length === 0) {
1082
+ return /* @__PURE__ */ import_react3.default.createElement(import_TableRow.default, null, /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, { colSpan: 8 }, filterText ? /* @__PURE__ */ import_react3.default.createElement(
1083
+ EmptyState,
1084
+ {
1085
+ message: "No keys match that filter",
1086
+ hint: "Try a different alias or model name."
1087
+ }
1088
+ ) : /* @__PURE__ */ import_react3.default.createElement(
1089
+ EmptyState,
1090
+ {
1091
+ message: "No keys yet",
1092
+ hint: "Generate your first key to start calling models.",
1093
+ action: /* @__PURE__ */ import_react3.default.createElement(
1094
+ import_Button2.default,
1095
+ {
1096
+ variant: "contained",
1097
+ color: "primary",
1098
+ size: "small",
1099
+ disableElevation: true,
1100
+ startIcon: /* @__PURE__ */ import_react3.default.createElement(import_icons_material2.Add, null),
1101
+ onClick: onGenerateKeyClick,
1102
+ sx: { mt: 1 }
1103
+ },
1104
+ "Generate Your First Key"
1105
+ )
1106
+ }
1107
+ )));
1108
+ }
1109
+ return filteredKeys.map((key) => {
1110
+ const keyId = key.token ?? key.key;
1111
+ const isBlocking = blockingKeyId === keyId;
1112
+ const keyModels = key.models ?? [];
1113
+ return /* @__PURE__ */ import_react3.default.createElement(
1114
+ import_TableRow.default,
1115
+ {
1116
+ key: keyId,
1117
+ sx: key.blocked ? (theme) => ({
1118
+ // A left rule reads as "suspended" without greying the row into
1119
+ // illegibility the way a full background tint does.
1120
+ boxShadow: `inset 3px 0 0 0 ${theme.palette.error.main}`,
1121
+ "& td": { color: theme.palette.text.secondary }
1122
+ }) : void 0
1123
+ },
1124
+ /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, /* @__PURE__ */ import_react3.default.createElement(import_Box3.default, { display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }, /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, { variant: "body2", sx: { fontWeight: 600 } }, key.key_alias || "\u2014"), key.blocked && /* @__PURE__ */ import_react3.default.createElement(StatusPill, { label: "Blocked", tone: "danger" }))),
1125
+ /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, /* @__PURE__ */ import_react3.default.createElement(import_Box3.default, { display: "flex", alignItems: "center", gap: 0.5 }, /* @__PURE__ */ import_react3.default.createElement(
1126
+ import_Typography3.default,
1127
+ {
1128
+ variant: "body2",
1129
+ component: "code",
1130
+ color: "text.secondary",
1131
+ title: keyId,
1132
+ sx: (theme) => ({
1133
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
1134
+ fontSize: 12,
1135
+ bgcolor: (0, import_styles3.alpha)(theme.palette.text.primary, theme.palette.mode === "dark" ? 0.08 : 0.05),
1136
+ px: 0.75,
1137
+ py: 0.375,
1138
+ borderRadius: "5px"
1139
+ })
1140
+ },
1141
+ shortKeyId(keyId)
1142
+ ), /* @__PURE__ */ import_react3.default.createElement(
1143
+ import_IconButton.default,
1144
+ {
1145
+ size: "small",
1146
+ onClick: () => copyToClipboard(keyId),
1147
+ title: "Copy Key ID",
1148
+ sx: quietIconButtonSx("accent")
1149
+ },
1150
+ /* @__PURE__ */ import_react3.default.createElement(import_icons_material2.ContentCopy, { sx: { fontSize: 15 } })
1151
+ ))),
1152
+ /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, { variant: "body2", color: "text.secondary" }, formatDate(key.created_at))),
1153
+ /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, /* @__PURE__ */ import_react3.default.createElement(ExpiryCell, { expiresAt: key.expires_at })),
1154
+ /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, /* @__PURE__ */ import_react3.default.createElement(BudgetCell, { spend: key.spend ?? 0, maxBudget: key.max_budget })),
1155
+ /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, { variant: "body2", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, key.tpm_limit ?? "\u221E", " / ", key.rpm_limit ?? "\u221E")),
1156
+ /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, keyModels.length === 0 ? /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, { variant: "body2", color: "text.secondary" }, "All models") : /* @__PURE__ */ import_react3.default.createElement(import_Box3.default, { display: "flex", gap: 0.5, flexWrap: "wrap" }, keyModels.slice(0, 2).map((model) => /* @__PURE__ */ import_react3.default.createElement(TagChip, { key: model, label: model, title: model })), keyModels.length > 2 && /* @__PURE__ */ import_react3.default.createElement(
1157
+ TagChip,
1158
+ {
1159
+ label: `+${keyModels.length - 2}`,
1160
+ mono: false,
1161
+ title: keyModels.slice(2).join(", ")
1162
+ }
1163
+ ))),
1164
+ /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, { align: "right" }, /* @__PURE__ */ import_react3.default.createElement(import_Box3.default, { display: "flex", justifyContent: "flex-end", gap: 0.25 }, /* @__PURE__ */ import_react3.default.createElement(
1165
+ import_IconButton.default,
1166
+ {
1167
+ size: "small",
1168
+ onClick: () => handleOpenEdit(key),
1169
+ title: "Edit key",
1170
+ sx: quietIconButtonSx("accent")
1171
+ },
1172
+ /* @__PURE__ */ import_react3.default.createElement(import_icons_material2.Edit, { fontSize: "small" })
1173
+ ), /* @__PURE__ */ import_react3.default.createElement(
1174
+ import_IconButton.default,
1175
+ {
1176
+ size: "small",
1177
+ onClick: () => handleToggleBlock(key),
1178
+ disabled: isBlocking,
1179
+ title: key.blocked ? "Unblock key" : "Block key \u2014 suspends without revoking",
1180
+ sx: quietIconButtonSx("warning")
1181
+ },
1182
+ keyBlockIcon(isBlocking, key.blocked)
1183
+ ), /* @__PURE__ */ import_react3.default.createElement(
1184
+ import_IconButton.default,
1185
+ {
1186
+ size: "small",
1187
+ onClick: () => setDeleteConfirmId(keyId),
1188
+ title: "Revoke key",
1189
+ sx: quietIconButtonSx("danger")
1190
+ },
1191
+ /* @__PURE__ */ import_react3.default.createElement(import_icons_material2.Delete, { fontSize: "small" })
1192
+ )))
1193
+ );
1194
+ });
1195
+ };
1196
+ return /* @__PURE__ */ import_react3.default.createElement(import_react3.default.Fragment, null, /* @__PURE__ */ import_react3.default.createElement(
1197
+ SectionCard,
451
1198
  {
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
1199
+ title: "Virtual Keys",
1200
+ subtitle: loading ? "Loading\u2026" : `${filteredKeys.length}${filterText ? ` of ${keys.length}` : ""} key${filteredKeys.length === 1 ? "" : "s"}`,
1201
+ flush: true,
1202
+ actions: /* @__PURE__ */ import_react3.default.createElement(import_react3.default.Fragment, null, /* @__PURE__ */ import_react3.default.createElement(
1203
+ import_TextField.default,
1204
+ {
1205
+ size: "small",
1206
+ placeholder: "Filter by alias or model\u2026",
1207
+ value: filterText,
1208
+ onChange: (e) => setFilterText(e.target.value),
1209
+ sx: { minWidth: 240 },
1210
+ InputProps: {
1211
+ startAdornment: /* @__PURE__ */ import_react3.default.createElement(import_InputAdornment.default, { position: "start" }, /* @__PURE__ */ import_react3.default.createElement(import_icons_material2.Search, { fontSize: "small", color: "disabled" }))
1212
+ }
475
1213
  }
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,
1214
+ ), expiredKeys.length > 0 && /* @__PURE__ */ import_react3.default.createElement(
1215
+ import_Button2.default,
1216
+ {
1217
+ variant: "outlined",
1218
+ color: "inherit",
1219
+ startIcon: /* @__PURE__ */ import_react3.default.createElement(import_icons_material2.Autorenew, null),
1220
+ onClick: () => setPruneConfirmCount(expiredKeys.length),
1221
+ sx: (theme) => ({
1222
+ color: theme.palette.text.secondary,
1223
+ borderColor: theme.palette.divider,
1224
+ "&:hover": {
1225
+ color: theme.palette.error.main,
1226
+ borderColor: (0, import_styles3.alpha)(theme.palette.error.main, 0.5),
1227
+ bgcolor: (0, import_styles3.alpha)(theme.palette.error.main, 0.06)
1228
+ }
1229
+ })
1230
+ },
1231
+ "Prune expired (",
1232
+ expiredKeys.length,
1233
+ ")"
1234
+ ), /* @__PURE__ */ import_react3.default.createElement(
1235
+ import_Button2.default,
1236
+ {
1237
+ variant: "contained",
1238
+ color: "primary",
1239
+ disableElevation: true,
1240
+ startIcon: /* @__PURE__ */ import_react3.default.createElement(import_icons_material2.Add, null),
1241
+ onClick: onGenerateKeyClick
1242
+ },
1243
+ "Generate New Key"
1244
+ ))
1245
+ },
1246
+ /* @__PURE__ */ import_react3.default.createElement(import_TableContainer.default, null, /* @__PURE__ */ import_react3.default.createElement(import_Table.default, { size: "small", sx: dataTableSx }, /* @__PURE__ */ import_react3.default.createElement(import_TableHead.default, null, /* @__PURE__ */ import_react3.default.createElement(import_TableRow.default, null, /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, "Alias"), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, "Key ID"), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, "Created"), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, "Expires"), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, "Budget"), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, "TPM / RPM"), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, "Models"), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, { align: "right" }, "Actions"))), /* @__PURE__ */ import_react3.default.createElement(import_TableBody.default, null, renderTableBody())))
1247
+ ), /* @__PURE__ */ import_react3.default.createElement(import_Dialog.default, { open: !!editingKey, onClose: handleCloseEdit, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ import_react3.default.createElement(import_DialogTitle.default, null, "Edit Key"), /* @__PURE__ */ import_react3.default.createElement(import_DialogContent.default, null, editingKey && /* @__PURE__ */ import_react3.default.createElement(import_Box3.default, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, { variant: "body2", color: "text.secondary" }, /* @__PURE__ */ import_react3.default.createElement("code", { style: { fontFamily: "monospace", color: "inherit" } }, maskKey(editingKey.key))), /* @__PURE__ */ import_react3.default.createElement(
1248
+ import_TextField.default,
498
1249
  {
499
1250
  label: "Alias",
500
1251
  value: editForm.key_alias || "",
501
1252
  onChange: (e) => setEditForm({ ...editForm, key_alias: e.target.value }),
502
1253
  fullWidth: true
503
1254
  }
504
- ), models.length > 0 && /* @__PURE__ */ import_react2.default.createElement(
505
- import_material2.Autocomplete,
1255
+ ), models.length > 0 && /* @__PURE__ */ import_react3.default.createElement(
1256
+ import_Autocomplete.default,
506
1257
  {
507
1258
  multiple: true,
508
1259
  options: models,
@@ -510,11 +1261,11 @@ var init_KeysTable = __esm({
510
1261
  getOptionLabel: (m) => m.model_name,
511
1262
  value: editSelectedModels,
512
1263
  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 })
1264
+ renderOption: (props, m) => /* @__PURE__ */ import_react3.default.createElement("li", { ...props }, modelOption(m)),
1265
+ renderInput: (params) => /* @__PURE__ */ import_react3.default.createElement(import_TextField.default, { ...params, label: "Models", fullWidth: true })
515
1266
  }
516
- ), /* @__PURE__ */ import_react2.default.createElement(
517
- import_material2.TextField,
1267
+ ), /* @__PURE__ */ import_react3.default.createElement(
1268
+ import_TextField.default,
518
1269
  {
519
1270
  label: "Max Budget (USD)",
520
1271
  type: "number",
@@ -522,8 +1273,8 @@ var init_KeysTable = __esm({
522
1273
  onChange: (e) => setEditForm({ ...editForm, max_budget: e.target.value ? Number(e.target.value) : void 0 }),
523
1274
  fullWidth: true
524
1275
  }
525
- ), /* @__PURE__ */ import_react2.default.createElement(
526
- import_material2.TextField,
1276
+ ), /* @__PURE__ */ import_react3.default.createElement(
1277
+ import_TextField.default,
527
1278
  {
528
1279
  label: "TPM Limit",
529
1280
  type: "number",
@@ -532,8 +1283,8 @@ var init_KeysTable = __esm({
532
1283
  helperText: "Max tokens per minute this key can consume across all models. Leave blank for no limit.",
533
1284
  fullWidth: true
534
1285
  }
535
- ), /* @__PURE__ */ import_react2.default.createElement(
536
- import_material2.TextField,
1286
+ ), /* @__PURE__ */ import_react3.default.createElement(
1287
+ import_TextField.default,
537
1288
  {
538
1289
  label: "RPM Limit",
539
1290
  type: "number",
@@ -542,8 +1293,8 @@ var init_KeysTable = __esm({
542
1293
  helperText: "Max requests per minute this key can make across all models. Leave blank for no limit.",
543
1294
  fullWidth: true
544
1295
  }
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,
1296
+ ), /* @__PURE__ */ import_react3.default.createElement(import_Box3.default, { mt: 1, pt: 2, borderTop: "1px solid", sx: { borderColor: "divider" } }, /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, { variant: "caption", color: "text.secondary", display: "block", mb: 1 }, "Danger Zone"), !resetSpendConfirm ? /* @__PURE__ */ import_react3.default.createElement(
1297
+ import_Button2.default,
547
1298
  {
548
1299
  size: "small",
549
1300
  color: "warning",
@@ -551,8 +1302,8 @@ var init_KeysTable = __esm({
551
1302
  onClick: () => setResetSpendConfirm(true)
552
1303
  },
553
1304
  "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,
1305
+ ) : /* @__PURE__ */ import_react3.default.createElement(import_Box3.default, { display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }, /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, { variant: "body2", color: "warning.main" }, "Zero out spend counter?"), /* @__PURE__ */ import_react3.default.createElement(
1306
+ import_Button2.default,
556
1307
  {
557
1308
  size: "small",
558
1309
  color: "warning",
@@ -560,25 +1311,25 @@ var init_KeysTable = __esm({
560
1311
  disabled: resetSpendSubmitting,
561
1312
  onClick: handleResetSpend
562
1313
  },
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,
1314
+ resetSpendSubmitting ? /* @__PURE__ */ import_react3.default.createElement(import_CircularProgress.default, { size: 16 }) : "Confirm"
1315
+ ), /* @__PURE__ */ import_react3.default.createElement(import_Button2.default, { size: "small", onClick: () => setResetSpendConfirm(false) }, "Cancel"))))), /* @__PURE__ */ import_react3.default.createElement(import_DialogActions.default, null, /* @__PURE__ */ import_react3.default.createElement(import_Button2.default, { onClick: handleCloseEdit }, "Cancel"), /* @__PURE__ */ import_react3.default.createElement(import_Button2.default, { onClick: handleUpdate, variant: "contained", color: "primary", disabled: editSubmitting }, editSubmitting ? /* @__PURE__ */ import_react3.default.createElement(import_CircularProgress.default, { size: 24 }) : "Save"))), /* @__PURE__ */ import_react3.default.createElement(import_Dialog.default, { open: !!deleteConfirmId, onClose: () => setDeleteConfirmId(null), maxWidth: "xs", fullWidth: true }, /* @__PURE__ */ import_react3.default.createElement(import_DialogTitle.default, null, "Revoke Key?"), /* @__PURE__ */ import_react3.default.createElement(import_DialogContent.default, null, /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, null, "This will permanently revoke the key. Any integrations using it will stop working immediately.")), /* @__PURE__ */ import_react3.default.createElement(import_DialogActions.default, null, /* @__PURE__ */ import_react3.default.createElement(import_Button2.default, { onClick: () => setDeleteConfirmId(null), disabled: deleteSubmitting }, "Cancel"), /* @__PURE__ */ import_react3.default.createElement(
1316
+ import_Button2.default,
566
1317
  {
567
1318
  onClick: handleConfirmDelete,
568
1319
  variant: "contained",
569
1320
  color: "error",
570
1321
  disabled: deleteSubmitting
571
1322
  },
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,
1323
+ deleteSubmitting ? /* @__PURE__ */ import_react3.default.createElement(import_CircularProgress.default, { size: 20 }) : "Revoke"
1324
+ ))), /* @__PURE__ */ import_react3.default.createElement(import_Dialog.default, { open: pruneConfirmCount !== null, onClose: () => setPruneConfirmCount(null), maxWidth: "xs", fullWidth: true }, /* @__PURE__ */ import_react3.default.createElement(import_DialogTitle.default, null, "Prune Expired Keys?"), /* @__PURE__ */ import_react3.default.createElement(import_DialogContent.default, null, /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, null, "This will permanently delete ", pruneConfirmCount, " expired key", pruneConfirmCount !== 1 ? "s" : "", " from LiteLLM. Any integrations using them will stop working immediately.")), /* @__PURE__ */ import_react3.default.createElement(import_DialogActions.default, null, /* @__PURE__ */ import_react3.default.createElement(import_Button2.default, { onClick: () => setPruneConfirmCount(null), disabled: pruneSubmitting }, "Cancel"), /* @__PURE__ */ import_react3.default.createElement(
1325
+ import_Button2.default,
575
1326
  {
576
1327
  onClick: handlePruneExpired,
577
1328
  variant: "contained",
578
1329
  color: "error",
579
1330
  disabled: pruneSubmitting
580
1331
  },
581
- pruneSubmitting ? /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 20 }) : "Prune"
1332
+ pruneSubmitting ? /* @__PURE__ */ import_react3.default.createElement(import_CircularProgress.default, { size: 20 }) : "Prune"
582
1333
  ))));
583
1334
  };
584
1335
  }
@@ -626,6 +1377,21 @@ response = client.chat.completions.create(
626
1377
  print(response.choices[0].message.content)`
627
1378
  };
628
1379
  }
1380
+ function aliasHelperText(aliasError, aliasDuplicate) {
1381
+ if (aliasError) return "Alias is required";
1382
+ if (aliasDuplicate) return "This alias is already used by one of your keys \u2014 LiteLLM requires aliases to be unique across all keys";
1383
+ return void 0;
1384
+ }
1385
+ function teamHelperText(teamError, teamRequired) {
1386
+ if (teamError) return "Team is required";
1387
+ if (teamRequired) return "Bind this key to a team for scoped access";
1388
+ return "Optional: bind this key to a specific team for scoped access";
1389
+ }
1390
+ function budgetHelperText(budgetInvalid, budgetEstimate) {
1391
+ if (budgetInvalid) return 'Enter a positive budget or tick "Unlimited"';
1392
+ if (budgetEstimate !== null) return `\u2248 ${fmtInt(budgetEstimate)} tokens at the selected model's rate`;
1393
+ return void 0;
1394
+ }
629
1395
  function fmtCost2(perToken) {
630
1396
  if (!perToken) return null;
631
1397
  const per1k = perToken * 1e3;
@@ -646,12 +1412,28 @@ function formatContextWindow2(maxInput, maxOutput) {
646
1412
  if (outPart) return `ctx ${outPart} out`;
647
1413
  return null;
648
1414
  }
649
- var import_react3, import_material3, import_icons_material3, generateDefaultAlias, emptyForm, GenerateKeyDialog, SnippetTabs;
1415
+ var import_react4, import_Box4, import_Typography4, import_Dialog2, import_DialogTitle2, import_DialogContent2, import_DialogActions2, import_TextField2, import_MenuItem, import_Button3, import_IconButton2, import_CircularProgress2, import_Autocomplete2, import_Checkbox, import_FormControlLabel, import_Alert, import_Tabs, import_Tab, import_icons_material3, generateDefaultAlias, emptyForm, SnippetTabs, GenerateKeyDialog;
650
1416
  var init_GenerateKeyDialog = __esm({
651
1417
  "src/components/GenerateKeyDialog.tsx"() {
652
1418
  "use strict";
653
- import_react3 = __toESM(require("react"));
654
- import_material3 = require("@mui/material");
1419
+ import_react4 = __toESM(require("react"));
1420
+ import_Box4 = __toESM(require("@mui/material/Box"));
1421
+ import_Typography4 = __toESM(require("@mui/material/Typography"));
1422
+ import_Dialog2 = __toESM(require("@mui/material/Dialog"));
1423
+ import_DialogTitle2 = __toESM(require("@mui/material/DialogTitle"));
1424
+ import_DialogContent2 = __toESM(require("@mui/material/DialogContent"));
1425
+ import_DialogActions2 = __toESM(require("@mui/material/DialogActions"));
1426
+ import_TextField2 = __toESM(require("@mui/material/TextField"));
1427
+ import_MenuItem = __toESM(require("@mui/material/MenuItem"));
1428
+ import_Button3 = __toESM(require("@mui/material/Button"));
1429
+ import_IconButton2 = __toESM(require("@mui/material/IconButton"));
1430
+ import_CircularProgress2 = __toESM(require("@mui/material/CircularProgress"));
1431
+ import_Autocomplete2 = __toESM(require("@mui/material/Autocomplete"));
1432
+ import_Checkbox = __toESM(require("@mui/material/Checkbox"));
1433
+ import_FormControlLabel = __toESM(require("@mui/material/FormControlLabel"));
1434
+ import_Alert = __toESM(require("@mui/material/Alert"));
1435
+ import_Tabs = __toESM(require("@mui/material/Tabs"));
1436
+ import_Tab = __toESM(require("@mui/material/Tab"));
655
1437
  import_icons_material3 = require("@mui/icons-material");
656
1438
  init_format();
657
1439
  generateDefaultAlias = (username) => {
@@ -669,6 +1451,44 @@ var init_GenerateKeyDialog = __esm({
669
1451
  team_id: void 0,
670
1452
  key_type: "llm_api"
671
1453
  });
1454
+ SnippetTabs = ({ snippets, model, onCopy }) => {
1455
+ const [tab, setTab] = (0, import_react4.useState)("curl");
1456
+ const code = tab === "curl" ? snippets.curl : snippets.openai;
1457
+ return /* @__PURE__ */ import_react4.default.createElement(import_Box4.default, null, /* @__PURE__ */ import_react4.default.createElement(import_Tabs.default, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 1 } }, /* @__PURE__ */ import_react4.default.createElement(import_Tab.default, { label: "curl", value: "curl" }), /* @__PURE__ */ import_react4.default.createElement(import_Tab.default, { label: "OpenAI SDK", value: "openai" })), /* @__PURE__ */ import_react4.default.createElement(
1458
+ import_Box4.default,
1459
+ {
1460
+ position: "relative",
1461
+ p: 1.5,
1462
+ sx: { backgroundColor: "action.hover", border: "1px solid", borderColor: "divider", borderRadius: 1 }
1463
+ },
1464
+ /* @__PURE__ */ import_react4.default.createElement(
1465
+ import_IconButton2.default,
1466
+ {
1467
+ size: "small",
1468
+ onClick: () => onCopy(code),
1469
+ title: "Copy snippet",
1470
+ sx: { position: "absolute", top: 4, right: 4 }
1471
+ },
1472
+ /* @__PURE__ */ import_react4.default.createElement(import_icons_material3.ContentCopy, { fontSize: "small" })
1473
+ ),
1474
+ /* @__PURE__ */ import_react4.default.createElement(
1475
+ import_Typography4.default,
1476
+ {
1477
+ component: "pre",
1478
+ sx: {
1479
+ fontFamily: "monospace",
1480
+ fontSize: 12,
1481
+ whiteSpace: "pre-wrap",
1482
+ wordBreak: "break-all",
1483
+ mb: 0,
1484
+ pr: 4
1485
+ }
1486
+ },
1487
+ code
1488
+ ),
1489
+ model && /* @__PURE__ */ import_react4.default.createElement(import_Typography4.default, { variant: "caption", color: "text.secondary", display: "block", mt: 1 }, "Using model \u201C", model, "\u201D \u2014 swap it for any model you have access to.")
1490
+ ));
1491
+ };
672
1492
  GenerateKeyDialog = ({
673
1493
  open,
674
1494
  onClose,
@@ -680,14 +1500,14 @@ var init_GenerateKeyDialog = __esm({
680
1500
  onGenerateKey,
681
1501
  onGetConfig
682
1502
  }) => {
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)(() => {
1503
+ const [formData, setFormData] = (0, import_react4.useState)(emptyForm());
1504
+ const [unlimitedBudget, setUnlimitedBudget] = (0, import_react4.useState)(false);
1505
+ const [submitting, setSubmitting] = (0, import_react4.useState)(false);
1506
+ const [newKeyValue, setNewKeyValue] = (0, import_react4.useState)(null);
1507
+ const [newKeySnippets, setNewKeySnippets] = (0, import_react4.useState)(null);
1508
+ const [newKeyModel, setNewKeyModel] = (0, import_react4.useState)("");
1509
+ const [generateError, setGenerateError] = (0, import_react4.useState)(null);
1510
+ (0, import_react4.useEffect)(() => {
691
1511
  if (open) {
692
1512
  setFormData({ ...emptyForm(), alias: generateDefaultAlias(username) });
693
1513
  setUnlimitedBudget(false);
@@ -700,7 +1520,7 @@ var init_GenerateKeyDialog = __esm({
700
1520
  const allowUnlimitedBudget = keyGenerationSettings?.allowUnlimitedBudget ?? false;
701
1521
  const teamRequired = keyGenerationSettings?.teamRequired ?? true;
702
1522
  const selectedTeam = teams.find((t) => t.team_id === formData.team_id) ?? null;
703
- const availableModels = (0, import_react3.useMemo)(() => {
1523
+ const availableModels = (0, import_react4.useMemo)(() => {
704
1524
  const teamModels = selectedTeam?.models;
705
1525
  if (teamModels && teamModels.length > 0) {
706
1526
  const allowed = new Set(teamModels);
@@ -714,7 +1534,7 @@ var init_GenerateKeyDialog = __esm({
714
1534
  const budgetInvalid = !unlimitedBudget && (formData.max_budget === void 0 || formData.max_budget === null || formData.max_budget <= 0);
715
1535
  const canGenerate = !aliasError && !teamError && !budgetInvalid && !submitting;
716
1536
  const aliasDuplicate = !!formData.alias && keys.some((k) => k.key_alias === formData.alias);
717
- const budgetEstimate = (0, import_react3.useMemo)(() => {
1537
+ const budgetEstimate = (0, import_react4.useMemo)(() => {
718
1538
  if (unlimitedBudget || budgetInvalid) return null;
719
1539
  const inputCosts = selectedModels.map((m) => m.input_cost_per_token).filter((c) => typeof c === "number" && c > 0);
720
1540
  if (inputCosts.length === 0) return null;
@@ -762,10 +1582,42 @@ var init_GenerateKeyDialog = __esm({
762
1582
  const inCost = fmtCost2(m.input_cost_per_token);
763
1583
  const outCost = fmtCost2(m.output_cost_per_token);
764
1584
  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"));
1585
+ return /* @__PURE__ */ import_react4.default.createElement(import_Box4.default, null, /* @__PURE__ */ import_react4.default.createElement("span", null, m.model_name), m.supports_function_calling && " \u{1F527}", m.supports_vision && " \u{1F441}\uFE0F", ctx && /* @__PURE__ */ import_react4.default.createElement(import_Typography4.default, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, ctx), (inCost || outCost) && /* @__PURE__ */ import_react4.default.createElement(import_Typography4.default, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, inCost, " in \xB7 ", outCost, " out"));
766
1586
  };
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,
1587
+ const renderTeamField = () => {
1588
+ if (teams.length > 0) {
1589
+ return /* @__PURE__ */ import_react4.default.createElement(
1590
+ import_Autocomplete2.default,
1591
+ {
1592
+ options: teams,
1593
+ getOptionLabel: (t) => t.team_alias || t.team_id,
1594
+ value: selectedTeam,
1595
+ onChange: (_e, team) => {
1596
+ const teamModels = team?.models;
1597
+ const restrictedModels = teamModels && teamModels.length > 0 ? (formData.models || []).filter((m) => teamModels.includes(m)) : formData.models;
1598
+ setFormData({ ...formData, team_id: team?.team_id, models: restrictedModels });
1599
+ },
1600
+ renderInput: (params) => /* @__PURE__ */ import_react4.default.createElement(
1601
+ import_TextField2.default,
1602
+ {
1603
+ ...params,
1604
+ label: "Team",
1605
+ error: teamError,
1606
+ helperText: teamHelperText(teamError, teamRequired),
1607
+ required: teamRequired,
1608
+ fullWidth: true
1609
+ }
1610
+ )
1611
+ }
1612
+ );
1613
+ }
1614
+ if (teamRequired) {
1615
+ return /* @__PURE__ */ import_react4.default.createElement(import_Typography4.default, { variant: "body2", color: "error" }, "Team selection is required, but you don't belong to any team yet \u2014 contact your administrator.");
1616
+ }
1617
+ return null;
1618
+ };
1619
+ return /* @__PURE__ */ import_react4.default.createElement(import_Dialog2.default, { open, onClose: handleClose, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ import_react4.default.createElement(import_DialogTitle2.default, null, newKeyValue ? "Key Generated" : "Generate New Key"), /* @__PURE__ */ import_react4.default.createElement(import_DialogContent2.default, null, newKeyValue ? /* @__PURE__ */ import_react4.default.createElement(import_Box4.default, null, /* @__PURE__ */ import_react4.default.createElement(import_Typography4.default, { variant: "body2", color: "text.secondary", gutterBottom: true }, "Copy this key now. You won't be able to see it again."), /* @__PURE__ */ import_react4.default.createElement(
1620
+ import_Box4.default,
769
1621
  {
770
1622
  display: "flex",
771
1623
  alignItems: "center",
@@ -779,8 +1631,8 @@ var init_GenerateKeyDialog = __esm({
779
1631
  borderRadius: 1
780
1632
  }
781
1633
  },
782
- /* @__PURE__ */ import_react3.default.createElement(
783
- import_material3.Typography,
1634
+ /* @__PURE__ */ import_react4.default.createElement(
1635
+ import_Typography4.default,
784
1636
  {
785
1637
  component: "code",
786
1638
  color: "text.primary",
@@ -788,9 +1640,9 @@ var init_GenerateKeyDialog = __esm({
788
1640
  },
789
1641
  newKeyValue
790
1642
  ),
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,
1643
+ /* @__PURE__ */ import_react4.default.createElement(import_IconButton2.default, { onClick: () => copyToClipboard(newKeyValue) }, /* @__PURE__ */ import_react4.default.createElement(import_icons_material3.ContentCopy, null))
1644
+ ), newKeySnippets && /* @__PURE__ */ import_react4.default.createElement(import_Box4.default, { mt: 3 }, /* @__PURE__ */ import_react4.default.createElement(import_Box4.default, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_icons_material3.Code, { fontSize: "small", color: "action" }), /* @__PURE__ */ import_react4.default.createElement(import_Typography4.default, { variant: "subtitle2" }, "Start calling the proxy \u2014 paste and run")), /* @__PURE__ */ import_react4.default.createElement(SnippetTabs, { snippets: newKeySnippets, model: newKeyModel, onCopy: copyToClipboard }))) : /* @__PURE__ */ import_react4.default.createElement(import_Box4.default, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, generateError && /* @__PURE__ */ import_react4.default.createElement(import_Alert.default, { severity: "error", onClose: () => setGenerateError(null) }, generateError), /* @__PURE__ */ import_react4.default.createElement(
1645
+ import_TextField2.default,
794
1646
  {
795
1647
  label: "Alias",
796
1648
  value: formData.alias || "",
@@ -800,12 +1652,12 @@ var init_GenerateKeyDialog = __esm({
800
1652
  },
801
1653
  error: aliasError,
802
1654
  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,
1655
+ helperText: aliasHelperText(aliasError, aliasDuplicate),
804
1656
  required: true,
805
1657
  fullWidth: true
806
1658
  }
807
- ), /* @__PURE__ */ import_react3.default.createElement(
808
- import_material3.TextField,
1659
+ ), /* @__PURE__ */ import_react4.default.createElement(
1660
+ import_TextField2.default,
809
1661
  {
810
1662
  select: true,
811
1663
  label: "Duration",
@@ -813,36 +1665,13 @@ var init_GenerateKeyDialog = __esm({
813
1665
  onChange: (e) => setFormData({ ...formData, duration: e.target.value }),
814
1666
  fullWidth: true
815
1667
  },
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,
823
- {
824
- options: teams,
825
- getOptionLabel: (t) => t.team_alias || t.team_id,
826
- value: selectedTeam,
827
- onChange: (_e, team) => {
828
- const teamModels = team?.models;
829
- const restrictedModels = teamModels && teamModels.length > 0 ? (formData.models || []).filter((m) => teamModels.includes(m)) : formData.models;
830
- setFormData({ ...formData, team_id: team?.team_id, models: restrictedModels });
831
- },
832
- renderInput: (params) => /* @__PURE__ */ import_react3.default.createElement(
833
- import_material3.TextField,
834
- {
835
- ...params,
836
- label: "Team",
837
- error: teamError,
838
- helperText: teamError ? "Team is required" : teamRequired ? "Bind this key to a team for scoped access" : "Optional: bind this key to a specific team for scoped access",
839
- required: teamRequired,
840
- fullWidth: true
841
- }
842
- )
843
- }
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,
1668
+ /* @__PURE__ */ import_react4.default.createElement(import_MenuItem.default, { value: "1d" }, "1 Day"),
1669
+ /* @__PURE__ */ import_react4.default.createElement(import_MenuItem.default, { value: "7d" }, "7 Days"),
1670
+ /* @__PURE__ */ import_react4.default.createElement(import_MenuItem.default, { value: "30d" }, "30 Days"),
1671
+ /* @__PURE__ */ import_react4.default.createElement(import_MenuItem.default, { value: "90d" }, "90 Days"),
1672
+ /* @__PURE__ */ import_react4.default.createElement(import_MenuItem.default, { value: "1y" }, "1 Year")
1673
+ ), renderTeamField(), availableModels.length > 0 && /* @__PURE__ */ import_react4.default.createElement(
1674
+ import_Autocomplete2.default,
846
1675
  {
847
1676
  multiple: true,
848
1677
  options: availableModels,
@@ -850,9 +1679,9 @@ var init_GenerateKeyDialog = __esm({
850
1679
  getOptionLabel: (m) => m.model_name,
851
1680
  value: selectedModels,
852
1681
  onChange: (_e, selected) => setFormData({ ...formData, models: selected.map((m) => m.model_name) }),
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,
1682
+ renderOption: (props, m) => /* @__PURE__ */ import_react4.default.createElement("li", { ...props }, modelOption(m)),
1683
+ renderInput: (params) => /* @__PURE__ */ import_react4.default.createElement(
1684
+ import_TextField2.default,
856
1685
  {
857
1686
  ...params,
858
1687
  label: "Models",
@@ -861,11 +1690,11 @@ var init_GenerateKeyDialog = __esm({
861
1690
  }
862
1691
  )
863
1692
  }
864
- ), allowUnlimitedBudget && /* @__PURE__ */ import_react3.default.createElement(
865
- import_material3.FormControlLabel,
1693
+ ), allowUnlimitedBudget && /* @__PURE__ */ import_react4.default.createElement(
1694
+ import_FormControlLabel.default,
866
1695
  {
867
- control: /* @__PURE__ */ import_react3.default.createElement(
868
- import_material3.Checkbox,
1696
+ control: /* @__PURE__ */ import_react4.default.createElement(
1697
+ import_Checkbox.default,
869
1698
  {
870
1699
  checked: unlimitedBudget,
871
1700
  onChange: (e) => {
@@ -880,21 +1709,21 @@ var init_GenerateKeyDialog = __esm({
880
1709
  ),
881
1710
  label: "Unlimited budget"
882
1711
  }
883
- ), /* @__PURE__ */ import_react3.default.createElement(
884
- import_material3.TextField,
1712
+ ), /* @__PURE__ */ import_react4.default.createElement(
1713
+ import_TextField2.default,
885
1714
  {
886
1715
  label: "Max Budget (USD)",
887
1716
  type: "number",
888
1717
  value: unlimitedBudget ? "" : formData.max_budget ?? "",
889
1718
  onChange: (e) => setFormData({ ...formData, max_budget: e.target.value ? Number(e.target.value) : void 0 }),
890
1719
  error: budgetInvalid,
891
- helperText: budgetInvalid ? 'Enter a positive budget or tick "Unlimited"' : budgetEstimate !== null ? `\u2248 ${fmtInt(budgetEstimate)} tokens at the selected model's rate` : void 0,
1720
+ helperText: budgetHelperText(budgetInvalid, budgetEstimate),
892
1721
  disabled: unlimitedBudget,
893
1722
  required: true,
894
1723
  fullWidth: true
895
1724
  }
896
- ), /* @__PURE__ */ import_react3.default.createElement(
897
- import_material3.TextField,
1725
+ ), /* @__PURE__ */ import_react4.default.createElement(
1726
+ import_TextField2.default,
898
1727
  {
899
1728
  label: "TPM Limit",
900
1729
  type: "number",
@@ -903,8 +1732,8 @@ var init_GenerateKeyDialog = __esm({
903
1732
  helperText: "Max tokens per minute this key can consume across all models. Leave blank for no limit.",
904
1733
  fullWidth: true
905
1734
  }
906
- ), /* @__PURE__ */ import_react3.default.createElement(
907
- import_material3.TextField,
1735
+ ), /* @__PURE__ */ import_react4.default.createElement(
1736
+ import_TextField2.default,
908
1737
  {
909
1738
  label: "RPM Limit",
910
1739
  type: "number",
@@ -913,93 +1742,74 @@ var init_GenerateKeyDialog = __esm({
913
1742
  helperText: "Max requests per minute this key can make across all models. Leave blank for no limit.",
914
1743
  fullWidth: true
915
1744
  }
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,
1745
+ ))), /* @__PURE__ */ import_react4.default.createElement(import_DialogActions2.default, null, newKeyValue ? /* @__PURE__ */ import_react4.default.createElement(import_Button3.default, { onClick: handleClose, variant: "contained", color: "success" }, "Done") : /* @__PURE__ */ import_react4.default.createElement(import_react4.default.Fragment, null, /* @__PURE__ */ import_react4.default.createElement(import_Button3.default, { onClick: handleClose }, "Cancel"), /* @__PURE__ */ import_react4.default.createElement(
1746
+ import_Button3.default,
918
1747
  {
919
1748
  onClick: handleGenerate,
920
1749
  variant: "contained",
921
1750
  color: "primary",
922
1751
  disabled: !canGenerate
923
1752
  },
924
- submitting ? /* @__PURE__ */ import_react3.default.createElement(import_material3.CircularProgress, { size: 24 }) : "Generate"
1753
+ submitting ? /* @__PURE__ */ import_react4.default.createElement(import_CircularProgress2.default, { size: 24 }) : "Generate"
925
1754
  ))));
926
1755
  };
927
- SnippetTabs = ({ snippets, model, onCopy }) => {
928
- const [tab, setTab] = (0, import_react3.useState)("curl");
929
- const code = tab === "curl" ? snippets.curl : snippets.openai;
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,
932
- {
933
- position: "relative",
934
- p: 1.5,
935
- sx: { backgroundColor: "action.hover", border: "1px solid", borderColor: "divider", borderRadius: 1 }
936
- },
937
- /* @__PURE__ */ import_react3.default.createElement(
938
- import_material3.IconButton,
939
- {
940
- size: "small",
941
- onClick: () => onCopy(code),
942
- title: "Copy snippet",
943
- sx: { position: "absolute", top: 4, right: 4 }
944
- },
945
- /* @__PURE__ */ import_react3.default.createElement(import_icons_material3.ContentCopy, { fontSize: "small" })
946
- ),
947
- /* @__PURE__ */ import_react3.default.createElement(
948
- import_material3.Typography,
949
- {
950
- component: "pre",
951
- sx: {
952
- fontFamily: "monospace",
953
- fontSize: 12,
954
- whiteSpace: "pre-wrap",
955
- wordBreak: "break-all",
956
- mb: 0,
957
- pr: 4
958
- }
959
- },
960
- code
961
- ),
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.")
963
- ));
964
- };
965
1756
  }
966
1757
  });
967
1758
 
968
1759
  // src/components/UsageStats.tsx
969
- function modelColor(model) {
970
- let h = 5381;
971
- for (let i = 0; i < model.length; i++) h = (h << 5) + h + model.charCodeAt(i) >>> 0;
972
- return MODEL_COLORS[h % MODEL_COLORS.length];
1760
+ function rateTone(rate) {
1761
+ if (rate >= 0.99) return "success";
1762
+ if (rate >= 0.9) return "warning";
1763
+ return "danger";
973
1764
  }
974
- var import_react4, import_material4, import_recharts, TOP_N_MODELS, PERIOD_LS_KEY, MODEL_COLORS, fmtPct, KpiCard, ChartSkeleton, EmptyChart, UsageStats;
1765
+ var import_react5, import_Paper3, import_Box5, import_Typography5, import_TextField3, import_MenuItem2, import_Grid, import_Table2, import_TableBody2, import_TableCell2, import_TableContainer2, import_TableHead2, import_TableRow2, import_Skeleton3, import_recharts, TOP_N_MODELS, PERIOD_LS_KEY, PRESET_LABELS, fmtPct, ChartSkeleton, ChartOrFallback, SuccessRateCell, UsageStats;
975
1766
  var init_UsageStats = __esm({
976
1767
  "src/components/UsageStats.tsx"() {
977
1768
  "use strict";
978
- import_react4 = __toESM(require("react"));
979
- import_material4 = require("@mui/material");
1769
+ import_react5 = __toESM(require("react"));
1770
+ import_Paper3 = __toESM(require("@mui/material/Paper"));
1771
+ import_Box5 = __toESM(require("@mui/material/Box"));
1772
+ import_Typography5 = __toESM(require("@mui/material/Typography"));
1773
+ import_TextField3 = __toESM(require("@mui/material/TextField"));
1774
+ import_MenuItem2 = __toESM(require("@mui/material/MenuItem"));
1775
+ import_Grid = __toESM(require("@mui/material/Grid"));
1776
+ import_Table2 = __toESM(require("@mui/material/Table"));
1777
+ import_TableBody2 = __toESM(require("@mui/material/TableBody"));
1778
+ import_TableCell2 = __toESM(require("@mui/material/TableCell"));
1779
+ import_TableContainer2 = __toESM(require("@mui/material/TableContainer"));
1780
+ import_TableHead2 = __toESM(require("@mui/material/TableHead"));
1781
+ import_TableRow2 = __toESM(require("@mui/material/TableRow"));
1782
+ import_Skeleton3 = __toESM(require("@mui/material/Skeleton"));
980
1783
  import_recharts = require("recharts");
981
1784
  init_format();
1785
+ init_ui();
982
1786
  TOP_N_MODELS = 6;
983
1787
  PERIOD_LS_KEY = "litellm_usage_period";
984
- MODEL_COLORS = [
985
- "#8884d8",
986
- "#82ca9d",
987
- "#ffc658",
988
- "#ff7300",
989
- "#0088fe",
990
- "#00C49F",
991
- "#FFBB28",
992
- "#FF8042",
993
- "#a4de6c",
994
- "#d0ed57"
995
- ];
1788
+ PRESET_LABELS = {
1789
+ today: "Today",
1790
+ "24h": "Last 24 hours",
1791
+ "7d": "Last 7 days",
1792
+ "30d": "Last 30 days"
1793
+ };
996
1794
  fmtPct = (n) => `${(n * 100).toFixed(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 } });
999
- EmptyChart = ({
1000
- height = 260,
1001
- message = "No data for this period"
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));
1795
+ ChartSkeleton = ({ height = 240 }) => /* @__PURE__ */ import_react5.default.createElement(import_Skeleton3.default, { variant: "rounded", height });
1796
+ ChartOrFallback = ({ loading, empty, height = 240, children }) => {
1797
+ if (loading) return /* @__PURE__ */ import_react5.default.createElement(ChartSkeleton, { height });
1798
+ if (empty) return /* @__PURE__ */ import_react5.default.createElement(EmptyState, { message: "No data for this period", height });
1799
+ return /* @__PURE__ */ import_react5.default.createElement(import_react5.default.Fragment, null, children);
1800
+ };
1801
+ SuccessRateCell = ({ rate, requests }) => {
1802
+ if (requests === 0) return /* @__PURE__ */ import_react5.default.createElement(import_Typography5.default, { variant: "body2", color: "text.secondary" }, "\u2014");
1803
+ const tone = rateTone(rate);
1804
+ return /* @__PURE__ */ import_react5.default.createElement(import_Box5.default, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ import_react5.default.createElement(import_Box5.default, { width: 72, flexShrink: 0 }, /* @__PURE__ */ import_react5.default.createElement(Meter, { value: rate * 100, tone, height: 4 })), /* @__PURE__ */ import_react5.default.createElement(
1805
+ import_Typography5.default,
1806
+ {
1807
+ variant: "caption",
1808
+ sx: { fontVariantNumeric: "tabular-nums", minWidth: 44 }
1809
+ },
1810
+ fmtPct(rate)
1811
+ ));
1812
+ };
1003
1813
  UsageStats = ({
1004
1814
  usage,
1005
1815
  models,
@@ -1008,15 +1818,10 @@ var init_UsageStats = __esm({
1008
1818
  loading,
1009
1819
  userInfo
1010
1820
  }) => {
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");
1014
- const gridStroke = theme.palette.divider;
1015
- const tickFill = theme.palette.text.secondary;
1016
- const tickStyle = { fontSize: 12, fill: tickFill };
1017
- const tickStyleSmall = { fontSize: 11, fill: tickFill };
1018
- const tickStyleTiny = { fontSize: 10, fill: tickFill };
1019
- const selectedPreset = (0, import_react4.useMemo)(() => {
1821
+ const chart = useChartTheme();
1822
+ const [selectedModel, setSelectedModel] = (0, import_react5.useState)("all");
1823
+ const [tab, setTab] = (0, import_react5.useState)("costs");
1824
+ const selectedPreset = (0, import_react5.useMemo)(() => {
1020
1825
  if (dateRange.start.toDateString() === dateRange.end.toDateString()) return "today";
1021
1826
  const diffMs = dateRange.end.getTime() - dateRange.start.getTime();
1022
1827
  const diffHours = diffMs / (1e3 * 60 * 60);
@@ -1038,7 +1843,7 @@ var init_UsageStats = __esm({
1038
1843
  }
1039
1844
  onDateRangeChange({ start, end });
1040
1845
  };
1041
- const dailyData = (0, import_react4.useMemo)(
1846
+ const dailyData = (0, import_react5.useMemo)(
1042
1847
  () => (usage?.daily_usage ?? []).map((d) => ({
1043
1848
  date: d.date,
1044
1849
  spend: d.spend,
@@ -1051,21 +1856,21 @@ var init_UsageStats = __esm({
1051
1856
  })),
1052
1857
  [usage]
1053
1858
  );
1054
- const cumulativeData = (0, import_react4.useMemo)(() => {
1859
+ const cumulativeData = (0, import_react5.useMemo)(() => {
1055
1860
  let cum = 0;
1056
1861
  return dailyData.map((d) => {
1057
1862
  cum += d.spend;
1058
1863
  return { date: d.date, cumulative: cum };
1059
1864
  });
1060
1865
  }, [dailyData]);
1061
- const successRateData = (0, import_react4.useMemo)(
1866
+ const successRateData = (0, import_react5.useMemo)(
1062
1867
  () => dailyData.filter((d) => d.apiRequests > 0).map((d) => ({
1063
1868
  date: d.date,
1064
1869
  successRate: parseFloat((d.successfulRequests / d.apiRequests * 100).toFixed(1))
1065
1870
  })),
1066
1871
  [dailyData]
1067
1872
  );
1068
- const { modelSpendByDate, topModels: topSpendModels } = (0, import_react4.useMemo)(() => {
1873
+ const { modelSpendByDate, topModels: topSpendModels } = (0, import_react5.useMemo)(() => {
1069
1874
  const rows = usage?.daily_by_model ?? [];
1070
1875
  const modelTotals = {};
1071
1876
  for (const r of rows) modelTotals[r.model] = (modelTotals[r.model] ?? 0) + r.spend;
@@ -1080,7 +1885,7 @@ var init_UsageStats = __esm({
1080
1885
  const hasOther = sorted.some((r) => r.Other > 0);
1081
1886
  return { modelSpendByDate: sorted, topModels: hasOther ? [...top, "Other"] : top };
1082
1887
  }, [usage]);
1083
- const modelRows = (0, import_react4.useMemo)(() => {
1888
+ const modelRows = (0, import_react5.useMemo)(() => {
1084
1889
  const entries = Object.entries(usage?.usage_by_model ?? {}).map(([model, d]) => ({
1085
1890
  model,
1086
1891
  spend: d.total_spend,
@@ -1094,11 +1899,11 @@ var init_UsageStats = __esm({
1094
1899
  }));
1095
1900
  return selectedModel === "all" ? entries : entries.filter((e) => e.model === selectedModel);
1096
1901
  }, [usage, selectedModel]);
1097
- const topModelSpendBars = (0, import_react4.useMemo)(
1902
+ const topModelSpendBars = (0, import_react5.useMemo)(
1098
1903
  () => [...modelRows].sort((a, b) => b.spend - a.spend).slice(0, 10),
1099
1904
  [modelRows]
1100
1905
  );
1101
- const keyRows = (0, import_react4.useMemo)(
1906
+ const keyRows = (0, import_react5.useMemo)(
1102
1907
  () => Object.entries(usage?.usage_by_key ?? {}).map(([keyHash, d]) => ({
1103
1908
  keyHash,
1104
1909
  keyAlias: d.key_alias ?? keyHash.slice(0, 8),
@@ -1115,7 +1920,7 @@ var init_UsageStats = __esm({
1115
1920
  })),
1116
1921
  [usage]
1117
1922
  );
1118
- const topKeySpendBars = (0, import_react4.useMemo)(
1923
+ const topKeySpendBars = (0, import_react5.useMemo)(
1119
1924
  () => [...keyRows].sort((a, b) => b.spend - a.spend).slice(0, 10),
1120
1925
  [keyRows]
1121
1926
  );
@@ -1123,144 +1928,384 @@ var init_UsageStats = __esm({
1123
1928
  const overallSuccessRate = totalRequests > 0 ? (usage?.successful_requests ?? 0) / totalRequests : 0;
1124
1929
  const maxBudget = userInfo?.max_budget ?? 0;
1125
1930
  const totalCumSpend = cumulativeData[cumulativeData.length - 1]?.cumulative ?? 0;
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,
1931
+ const cumulativeDomain = maxBudget > 0 ? [0, (dataMax) => Math.max(dataMax, maxBudget) * 1.05] : [0, "auto"];
1932
+ const successTone = totalRequests === 0 ? "neutral" : rateTone(overallSuccessRate);
1933
+ const metrics = [
1128
1934
  {
1129
- value: selectedPreset,
1130
- label: "Period",
1131
- onChange: (e) => handlePresetChange(e.target.value)
1935
+ label: "Total spend",
1936
+ value: fmtUsd(usage?.total_spend ?? 0),
1937
+ hint: maxBudget > 0 ? `of ${fmtUsd(maxBudget)} budget` : void 0,
1938
+ tone: "accent"
1132
1939
  },
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,
1139
1940
  {
1140
- value: selectedModel,
1141
- label: "Model",
1142
- onChange: (e) => setSelectedModel(e.target.value)
1941
+ label: "Requests",
1942
+ value: fmtInt(totalRequests),
1943
+ hint: `${fmtInt(usage?.failed_requests ?? 0)} failed`,
1944
+ tone: "info"
1143
1945
  },
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(
1147
- KpiCard,
1148
1946
  {
1149
- label: "Total Tokens",
1150
- value: fmtInt(usage?.total_tokens ?? 0),
1151
- hint: `${fmtInt(usage?.prompt_tokens ?? 0)} in \xB7 ${fmtInt(usage?.completion_tokens ?? 0)} out`
1152
- }
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(
1154
- import_recharts.Area,
1947
+ label: "Success rate",
1948
+ value: totalRequests > 0 ? fmtPct(overallSuccessRate) : "\u2014",
1949
+ hint: totalRequests > 0 ? `${fmtInt(usage?.successful_requests ?? 0)} succeeded` : void 0,
1950
+ tone: successTone
1951
+ },
1155
1952
  {
1156
- key: m,
1157
- type: "monotone",
1158
- dataKey: m,
1159
- stackId: "spend",
1160
- stroke: modelColor(m),
1161
- fill: modelColor(m),
1162
- fillOpacity: 0.6
1953
+ label: "Tokens",
1954
+ value: fmtCompact(usage?.total_tokens ?? 0),
1955
+ hint: `${fmtCompact(usage?.prompt_tokens ?? 0)} in \xB7 ${fmtCompact(usage?.completion_tokens ?? 0)} out`,
1956
+ tone: "success"
1163
1957
  }
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,
1166
- {
1167
- variant: "determinate",
1168
- value: r.successRate * 100,
1169
- sx: { flex: 1, height: 6, borderRadius: 3 }
1958
+ ];
1959
+ const renderKeyRows = () => {
1960
+ if (loading) {
1961
+ return /* @__PURE__ */ import_react5.default.createElement(import_TableRow2.default, null, /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, { colSpan: 7, sx: { py: 3 } }, /* @__PURE__ */ import_react5.default.createElement(import_Skeleton3.default, { variant: "rounded", height: 80 })));
1170
1962
  }
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,
1173
- {
1174
- variant: "determinate",
1175
- value: r.successRate * 100,
1176
- sx: { flex: 1, height: 6, borderRadius: 3 }
1963
+ if (keyRows.length === 0) {
1964
+ return /* @__PURE__ */ import_react5.default.createElement(import_TableRow2.default, null, /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, { colSpan: 7 }, /* @__PURE__ */ import_react5.default.createElement(EmptyState, { message: "No key activity in this period" })));
1965
+ }
1966
+ return [...keyRows].sort((a, b) => b.spend - a.spend || b.apiRequests - a.apiRequests).map((r) => /* @__PURE__ */ import_react5.default.createElement(import_TableRow2.default, { key: r.keyHash }, /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, null, /* @__PURE__ */ import_react5.default.createElement(import_Typography5.default, { variant: "body2", sx: { fontWeight: 600 } }, r.keyAlias), r.teamId ? /* @__PURE__ */ import_react5.default.createElement(import_Typography5.default, { variant: "caption", color: "text.secondary" }, "team: ", r.teamId) : null), /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, null, /* @__PURE__ */ import_react5.default.createElement(import_Box5.default, { display: "flex", gap: 0.5, flexWrap: "wrap" }, r.models.slice(0, 3).map((m) => /* @__PURE__ */ import_react5.default.createElement(TagChip, { key: m, label: m, title: m })), r.models.length > 3 && /* @__PURE__ */ import_react5.default.createElement(TagChip, { label: `+${r.models.length - 3}`, mono: false }))), /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, { align: "right" }, fmtUsd(r.spend)), /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, { align: "right" }, fmtInt(r.apiRequests)), /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, { align: "right" }, fmtCompact(r.totalTokens)), /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, { align: "right" }, fmtInt(r.failedRequests)), /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, null, /* @__PURE__ */ import_react5.default.createElement(SuccessRateCell, { rate: r.successRate, requests: r.apiRequests }))));
1967
+ };
1968
+ const renderModelRows = () => {
1969
+ if (loading) {
1970
+ return /* @__PURE__ */ import_react5.default.createElement(import_TableRow2.default, null, /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, { colSpan: 8, sx: { py: 3 } }, /* @__PURE__ */ import_react5.default.createElement(import_Skeleton3.default, { variant: "rounded", height: 80 })));
1971
+ }
1972
+ if (modelRows.length === 0) {
1973
+ return /* @__PURE__ */ import_react5.default.createElement(import_TableRow2.default, null, /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, { colSpan: 8 }, /* @__PURE__ */ import_react5.default.createElement(EmptyState, { message: "No model activity in this period" })));
1177
1974
  }
1178
- ), /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "caption" }, fmtPct(r.successRate))) : "\u2014")))))))));
1975
+ return [...modelRows].sort((a, b) => b.spend - a.spend || b.totalTokens - a.totalTokens).map((r) => /* @__PURE__ */ import_react5.default.createElement(import_TableRow2.default, { key: r.model }, /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, null, /* @__PURE__ */ import_react5.default.createElement(import_Box5.default, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ import_react5.default.createElement(import_Box5.default, { sx: { width: 8, height: 8, borderRadius: "50%", bgcolor: chartColor(r.model), flexShrink: 0 } }), /* @__PURE__ */ import_react5.default.createElement(import_Typography5.default, { variant: "body2", sx: { fontWeight: 600 } }, r.model))), /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, { align: "right" }, fmtUsd(r.spend)), /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, { align: "right" }, fmtInt(r.apiRequests)), /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, { align: "right" }, fmtInt(r.successfulRequests)), /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, { align: "right" }, fmtInt(r.failedRequests)), /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, { align: "right" }, fmtCompact(r.promptTokens)), /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, { align: "right" }, fmtCompact(r.completionTokens)), /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, null, /* @__PURE__ */ import_react5.default.createElement(SuccessRateCell, { rate: r.successRate, requests: r.apiRequests }))));
1976
+ };
1977
+ const periodSelect = /* @__PURE__ */ import_react5.default.createElement(
1978
+ import_TextField3.default,
1979
+ {
1980
+ select: true,
1981
+ size: "small",
1982
+ label: "Period",
1983
+ value: selectedPreset,
1984
+ onChange: (e) => handlePresetChange(e.target.value),
1985
+ sx: { minWidth: 160 }
1986
+ },
1987
+ Object.keys(PRESET_LABELS).map((p) => /* @__PURE__ */ import_react5.default.createElement(import_MenuItem2.default, { key: p, value: p }, PRESET_LABELS[p]))
1988
+ );
1989
+ return /* @__PURE__ */ import_react5.default.createElement(
1990
+ SectionCard,
1991
+ {
1992
+ title: "Usage Analytics",
1993
+ subtitle: `${PRESET_LABELS[selectedPreset]} \xB7 ${dateRange.start.toLocaleDateString()} \u2013 ${dateRange.end.toLocaleDateString()}`,
1994
+ actions: periodSelect
1995
+ },
1996
+ /* @__PURE__ */ import_react5.default.createElement(MetricStrip, { metrics }),
1997
+ /* @__PURE__ */ import_react5.default.createElement(
1998
+ import_Box5.default,
1999
+ {
2000
+ sx: {
2001
+ display: "flex",
2002
+ alignItems: "center",
2003
+ justifyContent: "space-between",
2004
+ gap: 2,
2005
+ flexWrap: "wrap",
2006
+ mt: 2.5,
2007
+ mb: 2
2008
+ }
2009
+ },
2010
+ /* @__PURE__ */ import_react5.default.createElement(
2011
+ SegmentedControl,
2012
+ {
2013
+ value: tab,
2014
+ onChange: setTab,
2015
+ options: [
2016
+ { value: "costs", label: "Costs" },
2017
+ { value: "models", label: "Model Activity" },
2018
+ { value: "keys", label: "Key Activity" }
2019
+ ]
2020
+ }
2021
+ ),
2022
+ tab === "models" && /* @__PURE__ */ import_react5.default.createElement(
2023
+ import_TextField3.default,
2024
+ {
2025
+ select: true,
2026
+ size: "small",
2027
+ label: "Model",
2028
+ value: selectedModel,
2029
+ onChange: (e) => setSelectedModel(e.target.value),
2030
+ sx: { minWidth: 220 }
2031
+ },
2032
+ /* @__PURE__ */ import_react5.default.createElement(import_MenuItem2.default, { value: "all" }, "All models"),
2033
+ models.map((m) => /* @__PURE__ */ import_react5.default.createElement(import_MenuItem2.default, { key: m.model_name, value: m.model_name }, m.model_name))
2034
+ )
2035
+ ),
2036
+ tab === "costs" && /* @__PURE__ */ import_react5.default.createElement(import_Grid.default, { container: true, spacing: 2 }, /* @__PURE__ */ import_react5.default.createElement(import_Grid.default, { item: true, xs: 12 }, /* @__PURE__ */ import_react5.default.createElement(ChartCard, { title: "Daily spend by model", height: 260 }, /* @__PURE__ */ import_react5.default.createElement(ChartOrFallback, { loading, empty: modelSpendByDate.length === 0, height: 260 }, /* @__PURE__ */ import_react5.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react5.default.createElement(import_recharts.AreaChart, { data: modelSpendByDate, margin: { top: 4, right: 8, left: 0, bottom: 0 } }, /* @__PURE__ */ import_react5.default.createElement(import_recharts.CartesianGrid, { ...chart.grid }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.XAxis, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.YAxis, { tickFormatter: fmtUsdCompact, width: 56, ...chart.axis }), /* @__PURE__ */ import_react5.default.createElement(
2037
+ import_recharts.Tooltip,
2038
+ {
2039
+ cursor: chart.cursor,
2040
+ content: /* @__PURE__ */ import_react5.default.createElement(ChartTooltip, { valueFormatter: fmtUsd, hideZero: true })
2041
+ }
2042
+ ), topSpendModels.map((m) => /* @__PURE__ */ import_react5.default.createElement(
2043
+ import_recharts.Area,
2044
+ {
2045
+ key: m,
2046
+ type: "monotone",
2047
+ dataKey: m,
2048
+ stackId: "spend",
2049
+ stroke: chartColor(m),
2050
+ strokeWidth: 1.5,
2051
+ fill: chartColor(m),
2052
+ fillOpacity: 0.28
2053
+ }
2054
+ ))))), !loading && topSpendModels.length > 0 && /* @__PURE__ */ import_react5.default.createElement(SeriesLegend, { series: topSpendModels.map((m) => ({ name: m, color: chartColor(m) })) }))), /* @__PURE__ */ import_react5.default.createElement(import_Grid.default, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ import_react5.default.createElement(ChartCard, { title: "Daily token usage" }, /* @__PURE__ */ import_react5.default.createElement(ChartOrFallback, { loading, empty: dailyData.length === 0 }, /* @__PURE__ */ import_react5.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react5.default.createElement(import_recharts.AreaChart, { data: dailyData, margin: { top: 4, right: 8, left: 0, bottom: 0 } }, /* @__PURE__ */ import_react5.default.createElement(import_recharts.CartesianGrid, { ...chart.grid }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.XAxis, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.YAxis, { tickFormatter: fmtCompact, width: 48, ...chart.axis }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.Tooltip, { cursor: chart.cursor, content: /* @__PURE__ */ import_react5.default.createElement(ChartTooltip, { valueFormatter: fmtInt }) }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.Legend, { ...chart.legend }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.Area, { type: "monotone", dataKey: "promptTokens", name: "Input", stackId: "tok", stroke: SERIES.input, strokeWidth: 1.5, fill: SERIES.input, fillOpacity: 0.25 }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.Area, { type: "monotone", dataKey: "completionTokens", name: "Output", stackId: "tok", stroke: SERIES.output, strokeWidth: 1.5, fill: SERIES.output, fillOpacity: 0.25 })))))), /* @__PURE__ */ import_react5.default.createElement(import_Grid.default, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ import_react5.default.createElement(ChartCard, { title: "Daily requests" }, /* @__PURE__ */ import_react5.default.createElement(ChartOrFallback, { loading, empty: dailyData.length === 0 }, /* @__PURE__ */ import_react5.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react5.default.createElement(import_recharts.BarChart, { data: dailyData, margin: { top: 4, right: 8, left: 0, bottom: 0 }, barCategoryGap: "30%" }, /* @__PURE__ */ import_react5.default.createElement(import_recharts.CartesianGrid, { ...chart.grid }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.XAxis, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.YAxis, { tickFormatter: fmtCompact, width: 48, ...chart.axis }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.Tooltip, { cursor: chart.cursor, content: /* @__PURE__ */ import_react5.default.createElement(ChartTooltip, { valueFormatter: fmtInt }) }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.Legend, { ...chart.legend }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.Bar, { dataKey: "successfulRequests", name: "Successful", fill: SERIES.success, stackId: "r" }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.Bar, { dataKey: "failedRequests", name: "Failed", fill: SERIES.failure, stackId: "r", radius: [3, 3, 0, 0] })))))), /* @__PURE__ */ import_react5.default.createElement(import_Grid.default, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ import_react5.default.createElement(ChartCard, { title: "Daily success rate" }, /* @__PURE__ */ import_react5.default.createElement(ChartOrFallback, { loading, empty: successRateData.length === 0 }, /* @__PURE__ */ import_react5.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react5.default.createElement(import_recharts.LineChart, { data: successRateData, margin: { top: 4, right: 8, left: 0, bottom: 0 } }, /* @__PURE__ */ import_react5.default.createElement(import_recharts.CartesianGrid, { ...chart.grid }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.XAxis, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.YAxis, { domain: [0, 100], tickFormatter: (v) => `${v}%`, width: 44, ...chart.axis }), /* @__PURE__ */ import_react5.default.createElement(
2055
+ import_recharts.Tooltip,
2056
+ {
2057
+ cursor: chart.cursor,
2058
+ content: /* @__PURE__ */ import_react5.default.createElement(ChartTooltip, { valueFormatter: (v) => `${v}%` })
2059
+ }
2060
+ ), /* @__PURE__ */ import_react5.default.createElement(import_recharts.ReferenceLine, { y: 100, stroke: SERIES.success, strokeDasharray: "4 4", strokeOpacity: 0.5 }), /* @__PURE__ */ import_react5.default.createElement(
2061
+ import_recharts.Line,
2062
+ {
2063
+ type: "monotone",
2064
+ dataKey: "successRate",
2065
+ name: "Success rate",
2066
+ stroke: SERIES.input,
2067
+ strokeWidth: 2,
2068
+ dot: { r: 2.5, strokeWidth: 0, fill: SERIES.input },
2069
+ activeDot: { r: 4 }
2070
+ }
2071
+ )))))), /* @__PURE__ */ import_react5.default.createElement(import_Grid.default, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ import_react5.default.createElement(
2072
+ ChartCard,
2073
+ {
2074
+ title: maxBudget > 0 ? "Cumulative spend vs budget" : "Cumulative spend",
2075
+ meta: maxBudget > 0 ? `${fmtUsd(totalCumSpend)} used \xB7 ${fmtUsd(Math.max(0, maxBudget - totalCumSpend))} left` : void 0
2076
+ },
2077
+ /* @__PURE__ */ import_react5.default.createElement(ChartOrFallback, { loading, empty: cumulativeData.length === 0 }, /* @__PURE__ */ import_react5.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react5.default.createElement(import_recharts.AreaChart, { data: cumulativeData, margin: { top: 4, right: 8, left: 0, bottom: 0 } }, /* @__PURE__ */ import_react5.default.createElement(import_recharts.CartesianGrid, { ...chart.grid }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.XAxis, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }), /* @__PURE__ */ import_react5.default.createElement(
2078
+ import_recharts.YAxis,
2079
+ {
2080
+ tickFormatter: fmtUsdCompact,
2081
+ width: 56,
2082
+ domain: cumulativeDomain,
2083
+ ...chart.axis
2084
+ }
2085
+ ), /* @__PURE__ */ import_react5.default.createElement(
2086
+ import_recharts.Tooltip,
2087
+ {
2088
+ cursor: chart.cursor,
2089
+ content: /* @__PURE__ */ import_react5.default.createElement(ChartTooltip, { valueFormatter: fmtUsd })
2090
+ }
2091
+ ), maxBudget > 0 && /* @__PURE__ */ import_react5.default.createElement(
2092
+ import_recharts.ReferenceLine,
2093
+ {
2094
+ y: maxBudget,
2095
+ stroke: SERIES.budget,
2096
+ strokeDasharray: "5 4",
2097
+ label: { value: `Budget ${fmtUsd(maxBudget)}`, position: "insideTopRight", fontSize: 10, fill: SERIES.budget }
2098
+ }
2099
+ ), /* @__PURE__ */ import_react5.default.createElement(
2100
+ import_recharts.Area,
2101
+ {
2102
+ type: "monotone",
2103
+ dataKey: "cumulative",
2104
+ name: "Cumulative spend",
2105
+ stroke: SERIES.spend,
2106
+ strokeWidth: 2,
2107
+ fill: SERIES.spend,
2108
+ fillOpacity: 0.18
2109
+ }
2110
+ ))))
2111
+ ))),
2112
+ tab === "models" && /* @__PURE__ */ import_react5.default.createElement(import_Grid.default, { container: true, spacing: 2 }, /* @__PURE__ */ import_react5.default.createElement(import_Grid.default, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ import_react5.default.createElement(ChartCard, { title: "Spend by model", height: Math.max(200, topModelSpendBars.length * 34) }, /* @__PURE__ */ import_react5.default.createElement(
2113
+ ChartOrFallback,
2114
+ {
2115
+ loading,
2116
+ empty: topModelSpendBars.length === 0,
2117
+ height: Math.max(200, topModelSpendBars.length * 34)
2118
+ },
2119
+ /* @__PURE__ */ import_react5.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react5.default.createElement(import_recharts.BarChart, { data: topModelSpendBars, layout: "vertical", margin: { top: 0, right: 12, left: 0, bottom: 0 } }, /* @__PURE__ */ import_react5.default.createElement(import_recharts.CartesianGrid, { ...chart.grid, vertical: true, horizontal: false }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.XAxis, { type: "number", tickFormatter: fmtUsdCompact, ...chart.axis }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.YAxis, { type: "category", dataKey: "model", width: 170, ...chart.axis, tick: { fontSize: 10.5, fill: chart.axis.tick.fill } }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.Tooltip, { cursor: chart.cursor, content: /* @__PURE__ */ import_react5.default.createElement(ChartTooltip, { valueFormatter: fmtUsd, labelFormatter: (l) => l }) }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.Bar, { dataKey: "spend", name: "Spend", radius: [0, 3, 3, 0], barSize: 14 }, topModelSpendBars.map((r) => /* @__PURE__ */ import_react5.default.createElement(import_recharts.Cell, { key: r.model, fill: chartColor(r.model) })))))
2120
+ ))), /* @__PURE__ */ import_react5.default.createElement(import_Grid.default, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ import_react5.default.createElement(ChartCard, { title: "Tokens by model", height: Math.max(200, topModelSpendBars.length * 34) }, /* @__PURE__ */ import_react5.default.createElement(
2121
+ ChartOrFallback,
2122
+ {
2123
+ loading,
2124
+ empty: modelRows.length === 0,
2125
+ height: Math.max(200, topModelSpendBars.length * 34)
2126
+ },
2127
+ /* @__PURE__ */ import_react5.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react5.default.createElement(import_recharts.BarChart, { data: topModelSpendBars, layout: "vertical", margin: { top: 0, right: 12, left: 0, bottom: 0 } }, /* @__PURE__ */ import_react5.default.createElement(import_recharts.CartesianGrid, { ...chart.grid, vertical: true, horizontal: false }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.XAxis, { type: "number", tickFormatter: fmtCompact, ...chart.axis }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.YAxis, { type: "category", dataKey: "model", width: 170, ...chart.axis, tick: { fontSize: 10.5, fill: chart.axis.tick.fill } }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.Tooltip, { cursor: chart.cursor, content: /* @__PURE__ */ import_react5.default.createElement(ChartTooltip, { valueFormatter: fmtInt, labelFormatter: (l) => l }) }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.Legend, { ...chart.legend }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.Bar, { dataKey: "promptTokens", name: "Input", fill: SERIES.input, stackId: "t", barSize: 14 }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.Bar, { dataKey: "completionTokens", name: "Output", fill: SERIES.output, stackId: "t", barSize: 14, radius: [0, 3, 3, 0] })))
2128
+ ))), /* @__PURE__ */ import_react5.default.createElement(import_Grid.default, { item: true, xs: 12 }, /* @__PURE__ */ import_react5.default.createElement(import_TableContainer2.default, { component: import_Paper3.default, variant: "outlined", sx: { borderRadius: 2 } }, /* @__PURE__ */ import_react5.default.createElement(import_Table2.default, { size: "small", sx: dataTableSx }, /* @__PURE__ */ import_react5.default.createElement(import_TableHead2.default, null, /* @__PURE__ */ import_react5.default.createElement(import_TableRow2.default, null, /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, null, "Model"), /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, { align: "right" }, "Spend"), /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, { align: "right" }, "Requests"), /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, { align: "right" }, "Success"), /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, { align: "right" }, "Failed"), /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, { align: "right" }, "Input"), /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, { align: "right" }, "Output"), /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, { sx: { width: 160 } }, "Success rate"))), /* @__PURE__ */ import_react5.default.createElement(import_TableBody2.default, null, renderModelRows()))))),
2129
+ tab === "keys" && /* @__PURE__ */ import_react5.default.createElement(import_Grid.default, { container: true, spacing: 2 }, (loading || topKeySpendBars.length > 0) && /* @__PURE__ */ import_react5.default.createElement(import_Grid.default, { item: true, xs: 12 }, /* @__PURE__ */ import_react5.default.createElement(ChartCard, { title: "Spend by key", height: Math.max(160, topKeySpendBars.length * 32) }, /* @__PURE__ */ import_react5.default.createElement(
2130
+ ChartOrFallback,
2131
+ {
2132
+ loading,
2133
+ empty: topKeySpendBars.length === 0,
2134
+ height: Math.max(160, topKeySpendBars.length * 32)
2135
+ },
2136
+ /* @__PURE__ */ import_react5.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react5.default.createElement(import_recharts.BarChart, { data: topKeySpendBars, layout: "vertical", margin: { top: 0, right: 12, left: 0, bottom: 0 } }, /* @__PURE__ */ import_react5.default.createElement(import_recharts.CartesianGrid, { ...chart.grid, vertical: true, horizontal: false }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.XAxis, { type: "number", tickFormatter: fmtUsdCompact, ...chart.axis }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.YAxis, { type: "category", dataKey: "keyAlias", width: 160, ...chart.axis, tick: { fontSize: 10.5, fill: chart.axis.tick.fill } }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.Tooltip, { cursor: chart.cursor, content: /* @__PURE__ */ import_react5.default.createElement(ChartTooltip, { valueFormatter: fmtUsd, labelFormatter: (l) => l }) }), /* @__PURE__ */ import_react5.default.createElement(import_recharts.Bar, { dataKey: "spend", name: "Spend", fill: SERIES.input, radius: [0, 3, 3, 0], barSize: 14 })))
2137
+ ))), /* @__PURE__ */ import_react5.default.createElement(import_Grid.default, { item: true, xs: 12 }, /* @__PURE__ */ import_react5.default.createElement(import_TableContainer2.default, { component: import_Paper3.default, variant: "outlined", sx: { borderRadius: 2 } }, /* @__PURE__ */ import_react5.default.createElement(import_Table2.default, { size: "small", sx: dataTableSx }, /* @__PURE__ */ import_react5.default.createElement(import_TableHead2.default, null, /* @__PURE__ */ import_react5.default.createElement(import_TableRow2.default, null, /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, null, "Key"), /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, null, "Models"), /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, { align: "right" }, "Spend"), /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, { align: "right" }, "Requests"), /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, { align: "right" }, "Tokens"), /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, { align: "right" }, "Failed"), /* @__PURE__ */ import_react5.default.createElement(import_TableCell2.default, { sx: { width: 160 } }, "Success rate"))), /* @__PURE__ */ import_react5.default.createElement(import_TableBody2.default, null, renderKeyRows())))))
2138
+ );
1179
2139
  };
1180
2140
  }
1181
2141
  });
1182
2142
 
1183
2143
  // src/components/TeamUsage.tsx
1184
- var import_react5, import_material5, import_icons_material4, import_recharts2, Stat, TeamCard, TeamUsage;
2144
+ function budgetTone2(isOver, isNear) {
2145
+ if (isOver) return "danger";
2146
+ if (isNear) return "warning";
2147
+ return "accent";
2148
+ }
2149
+ var import_react6, import_Paper4, import_Box6, import_Typography6, import_Skeleton4, import_Divider2, import_Table3, import_TableBody3, import_TableCell3, import_TableHead3, import_TableRow3, import_TableContainer3, import_Collapse, import_IconButton3, import_CircularProgress3, import_styles4, import_icons_material4, import_recharts2, fmtUsd2, TeamCard, TeamUsage;
1185
2150
  var init_TeamUsage = __esm({
1186
2151
  "src/components/TeamUsage.tsx"() {
1187
2152
  "use strict";
1188
- import_react5 = __toESM(require("react"));
1189
- import_material5 = require("@mui/material");
2153
+ import_react6 = __toESM(require("react"));
2154
+ import_Paper4 = __toESM(require("@mui/material/Paper"));
2155
+ import_Box6 = __toESM(require("@mui/material/Box"));
2156
+ import_Typography6 = __toESM(require("@mui/material/Typography"));
2157
+ import_Skeleton4 = __toESM(require("@mui/material/Skeleton"));
2158
+ import_Divider2 = __toESM(require("@mui/material/Divider"));
2159
+ import_Table3 = __toESM(require("@mui/material/Table"));
2160
+ import_TableBody3 = __toESM(require("@mui/material/TableBody"));
2161
+ import_TableCell3 = __toESM(require("@mui/material/TableCell"));
2162
+ import_TableHead3 = __toESM(require("@mui/material/TableHead"));
2163
+ import_TableRow3 = __toESM(require("@mui/material/TableRow"));
2164
+ import_TableContainer3 = __toESM(require("@mui/material/TableContainer"));
2165
+ import_Collapse = __toESM(require("@mui/material/Collapse"));
2166
+ import_IconButton3 = __toESM(require("@mui/material/IconButton"));
2167
+ import_CircularProgress3 = __toESM(require("@mui/material/CircularProgress"));
2168
+ import_styles4 = require("@mui/material/styles");
1190
2169
  import_icons_material4 = require("@mui/icons-material");
1191
2170
  import_recharts2 = require("recharts");
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));
2171
+ init_ui();
2172
+ fmtUsd2 = (n) => `$${(n ?? 0).toFixed(2)}`;
1193
2173
  TeamCard = ({ team, usage, usageLoading }) => {
1194
- const [expanded, setExpanded] = (0, import_react5.useState)(false);
1195
- const theme = (0, import_material5.useTheme)();
1196
- const gridStroke = theme.palette.divider;
1197
- const tickFill = theme.palette.text.secondary;
2174
+ const [expanded, setExpanded] = (0, import_react6.useState)(false);
2175
+ const chart = useChartTheme();
1198
2176
  const budget = team.max_budget ?? 0;
1199
2177
  const spend = team.spend ?? 0;
1200
2178
  const budgetPct = budget > 0 ? Math.min(spend / budget * 100, 100) : 0;
1201
2179
  const isOver = budget > 0 && spend >= budget;
1202
2180
  const isNear = budget > 0 && spend >= budget * 0.8 && !isOver;
1203
2181
  const dailyData = usage?.daily_usage?.map((d) => ({ date: d.date, spend: d.spend })) ?? [];
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(
1205
- Stat,
1206
- {
1207
- label: "Members",
1208
- value: team.members_with_roles?.length ? String(team.members_with_roles.length) : "\u2014"
1209
- }
1210
- ), /* @__PURE__ */ import_react5.default.createElement(
1211
- Stat,
1212
- {
1213
- label: "Models",
1214
- value: team.models?.length ? String(team.models.length) : "All"
2182
+ const renderDailySpendSection = () => {
2183
+ if (usageLoading) {
2184
+ return /* @__PURE__ */ import_react6.default.createElement(import_Box6.default, { display: "flex", justifyContent: "center", py: 3 }, /* @__PURE__ */ import_react6.default.createElement(import_CircularProgress3.default, { size: 22 }));
1215
2185
  }
1216
- ), /* @__PURE__ */ import_react5.default.createElement(
1217
- Stat,
1218
- {
1219
- label: "Budget",
1220
- value: budget > 0 ? `$${budget.toFixed(2)}` : "Unlimited"
1221
- }
1222
- ), /* @__PURE__ */ import_react5.default.createElement(
1223
- Stat,
1224
- {
1225
- label: "Spend",
1226
- value: `$${spend.toFixed(2)}`
1227
- }
1228
- ), /* @__PURE__ */ import_react5.default.createElement(
1229
- Stat,
1230
- {
1231
- label: "TPM",
1232
- value: team.tpm_limit != null && team.tpm_limit > 0 ? String(team.tpm_limit) : "\u2014"
1233
- }
1234
- ), /* @__PURE__ */ import_react5.default.createElement(
1235
- Stat,
2186
+ if (dailyData.length === 0) return null;
2187
+ return /* @__PURE__ */ import_react6.default.createElement(import_Box6.default, { mb: 2.5 }, /* @__PURE__ */ import_react6.default.createElement(
2188
+ import_Typography6.default,
2189
+ {
2190
+ variant: "caption",
2191
+ color: "text.secondary",
2192
+ sx: { display: "block", fontSize: 11, fontWeight: 700, letterSpacing: "0.07em", textTransform: "uppercase", mb: 1 }
2193
+ },
2194
+ "Daily spend"
2195
+ ), /* @__PURE__ */ import_react6.default.createElement(import_Box6.default, { height: 150 }, /* @__PURE__ */ import_react6.default.createElement(import_recharts2.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react6.default.createElement(import_recharts2.AreaChart, { data: dailyData, margin: { top: 4, right: 8, left: 0, bottom: 0 } }, /* @__PURE__ */ import_react6.default.createElement(import_recharts2.CartesianGrid, { ...chart.grid }), /* @__PURE__ */ import_react6.default.createElement(import_recharts2.XAxis, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }), /* @__PURE__ */ import_react6.default.createElement(import_recharts2.YAxis, { tickFormatter: fmtUsdCompact, width: 52, ...chart.axis }), /* @__PURE__ */ import_react6.default.createElement(
2196
+ import_recharts2.Tooltip,
2197
+ {
2198
+ cursor: chart.cursor,
2199
+ content: /* @__PURE__ */ import_react6.default.createElement(ChartTooltip, { valueFormatter: (v) => `$${v.toFixed(4)}` })
2200
+ }
2201
+ ), /* @__PURE__ */ import_react6.default.createElement(
2202
+ import_recharts2.Area,
2203
+ {
2204
+ type: "monotone",
2205
+ dataKey: "spend",
2206
+ name: "Spend",
2207
+ stroke: SERIES.spend,
2208
+ strokeWidth: 2,
2209
+ fill: SERIES.spend,
2210
+ fillOpacity: 0.18
2211
+ }
2212
+ )))));
2213
+ };
2214
+ const sectionLabel = (label) => /* @__PURE__ */ import_react6.default.createElement(
2215
+ import_Typography6.default,
1236
2216
  {
1237
- label: "RPM",
1238
- value: team.rpm_limit != null && team.rpm_limit > 0 ? String(team.rpm_limit) : "\u2014"
1239
- }
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,
2217
+ variant: "caption",
2218
+ color: "text.secondary",
2219
+ sx: { display: "block", fontSize: 11, fontWeight: 700, letterSpacing: "0.07em", textTransform: "uppercase", mb: 1 }
2220
+ },
2221
+ label
2222
+ );
2223
+ return /* @__PURE__ */ import_react6.default.createElement(import_Paper4.default, { variant: "outlined", sx: { borderRadius: 2, p: 2.5 } }, /* @__PURE__ */ import_react6.default.createElement(import_Box6.default, { display: "flex", alignItems: "center", gap: 1.5 }, /* @__PURE__ */ import_react6.default.createElement(
2224
+ import_Box6.default,
1242
2225
  {
1243
- variant: "determinate",
1244
- value: budgetPct,
1245
- color: isOver ? "error" : isNear ? "warning" : "primary",
1246
- sx: { height: 6, borderRadius: 1 }
1247
- }
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,
2226
+ sx: (theme) => ({
2227
+ width: 36,
2228
+ height: 36,
2229
+ borderRadius: 1.5,
2230
+ flexShrink: 0,
2231
+ display: "flex",
2232
+ alignItems: "center",
2233
+ justifyContent: "center",
2234
+ bgcolor: (0, import_styles4.alpha)(theme.palette.primary.main, 0.1),
2235
+ color: theme.palette.primary.main
2236
+ })
2237
+ },
2238
+ /* @__PURE__ */ import_react6.default.createElement(import_icons_material4.Group, { fontSize: "small" })
2239
+ ), /* @__PURE__ */ import_react6.default.createElement(import_Box6.default, { flexGrow: 1, minWidth: 0 }, /* @__PURE__ */ import_react6.default.createElement(import_Typography6.default, { variant: "subtitle1", sx: { fontWeight: 600, lineHeight: 1.3 } }, team.team_alias || "Untitled team"), /* @__PURE__ */ import_react6.default.createElement(
2240
+ import_Typography6.default,
1250
2241
  {
1251
2242
  variant: "caption",
1252
2243
  color: "text.secondary",
1253
- sx: { fontFamily: "monospace" }
2244
+ sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 11 }
1254
2245
  },
1255
- m.user_id
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,
2246
+ team.team_id
2247
+ )), isOver && /* @__PURE__ */ import_react6.default.createElement(StatusPill, { label: "Over budget", tone: "danger" }), isNear && /* @__PURE__ */ import_react6.default.createElement(StatusPill, { label: "Near limit", tone: "warning" }), /* @__PURE__ */ import_react6.default.createElement(
2248
+ import_IconButton3.default,
1258
2249
  {
1259
- label: m.role,
1260
2250
  size: "small",
1261
- color: m.role === "admin" ? "primary" : "default"
1262
- }
1263
- ))))))) : /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "body2", color: "text.secondary" }, "No members assigned."))));
2251
+ onClick: () => setExpanded((e) => !e),
2252
+ "aria-label": expanded ? "Hide team details" : "Show team details",
2253
+ "aria-expanded": expanded,
2254
+ sx: (theme) => ({
2255
+ color: theme.palette.text.secondary,
2256
+ transform: expanded ? "rotate(180deg)" : "none",
2257
+ transition: theme.transitions.create("transform")
2258
+ })
2259
+ },
2260
+ /* @__PURE__ */ import_react6.default.createElement(import_icons_material4.ExpandMore, null)
2261
+ )), /* @__PURE__ */ import_react6.default.createElement(
2262
+ import_Box6.default,
2263
+ {
2264
+ sx: {
2265
+ display: "grid",
2266
+ gridTemplateColumns: { xs: "repeat(3, minmax(0, 1fr))", sm: "repeat(6, minmax(0, 1fr))" },
2267
+ gap: 2,
2268
+ mt: 2.5,
2269
+ maxWidth: 640
2270
+ }
2271
+ },
2272
+ /* @__PURE__ */ import_react6.default.createElement(Stat, { label: "Members", value: team.members_with_roles?.length ?? "\u2014" }),
2273
+ /* @__PURE__ */ import_react6.default.createElement(Stat, { label: "Models", value: team.models?.length ? team.models.length : "All" }),
2274
+ /* @__PURE__ */ import_react6.default.createElement(Stat, { label: "Budget", value: budget > 0 ? fmtUsd2(budget) : "Unlimited" }),
2275
+ /* @__PURE__ */ import_react6.default.createElement(Stat, { label: "Spend", value: fmtUsd2(spend) }),
2276
+ /* @__PURE__ */ import_react6.default.createElement(Stat, { label: "TPM", value: team.tpm_limit && team.tpm_limit > 0 ? team.tpm_limit.toLocaleString() : "\u2014" }),
2277
+ /* @__PURE__ */ import_react6.default.createElement(Stat, { label: "RPM", value: team.rpm_limit && team.rpm_limit > 0 ? team.rpm_limit.toLocaleString() : "\u2014" })
2278
+ ), budget > 0 && /* @__PURE__ */ import_react6.default.createElement(import_Box6.default, { mt: 2, maxWidth: 640 }, /* @__PURE__ */ import_react6.default.createElement(import_Box6.default, { display: "flex", justifyContent: "space-between", alignItems: "baseline", mb: 0.75 }, /* @__PURE__ */ import_react6.default.createElement(import_Typography6.default, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, fmtUsd2(spend), " of ", fmtUsd2(budget)), /* @__PURE__ */ import_react6.default.createElement(import_Typography6.default, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, budgetPct.toFixed(0), "%")), /* @__PURE__ */ import_react6.default.createElement(Meter, { value: budgetPct, tone: budgetTone2(isOver, isNear), height: 5 })), /* @__PURE__ */ import_react6.default.createElement(import_Collapse.default, { in: expanded, unmountOnExit: true }, /* @__PURE__ */ import_react6.default.createElement(import_Divider2.default, { sx: { my: 2.5 } }), /* @__PURE__ */ import_react6.default.createElement(import_Box6.default, { mb: 2.5 }, sectionLabel("Models"), team.models?.length ? /* @__PURE__ */ import_react6.default.createElement(import_Box6.default, { display: "flex", gap: 0.5, flexWrap: "wrap" }, team.models.map((m) => /* @__PURE__ */ import_react6.default.createElement(TagChip, { key: m, label: m, title: m }))) : /* @__PURE__ */ import_react6.default.createElement(import_Typography6.default, { variant: "body2", color: "text.secondary" }, "All models allowed")), renderDailySpendSection(), /* @__PURE__ */ import_react6.default.createElement(import_Box6.default, null, sectionLabel("Members"), team.members_with_roles?.length ? /* @__PURE__ */ import_react6.default.createElement(
2279
+ import_TableContainer3.default,
2280
+ {
2281
+ component: import_Paper4.default,
2282
+ variant: "outlined",
2283
+ sx: { borderRadius: 1.5 }
2284
+ },
2285
+ /* @__PURE__ */ import_react6.default.createElement(import_Table3.default, { size: "small", sx: dataTableSx }, /* @__PURE__ */ import_react6.default.createElement(import_TableHead3.default, null, /* @__PURE__ */ import_react6.default.createElement(import_TableRow3.default, null, /* @__PURE__ */ import_react6.default.createElement(import_TableCell3.default, null, "User"), /* @__PURE__ */ import_react6.default.createElement(import_TableCell3.default, { align: "right" }, "Role"))), /* @__PURE__ */ import_react6.default.createElement(import_TableBody3.default, null, team.members_with_roles.map((m) => /* @__PURE__ */ import_react6.default.createElement(import_TableRow3.default, { key: m.user_id }, /* @__PURE__ */ import_react6.default.createElement(import_TableCell3.default, null, m.user_email ? /* @__PURE__ */ import_react6.default.createElement(import_react6.default.Fragment, null, /* @__PURE__ */ import_react6.default.createElement(import_Typography6.default, { variant: "body2" }, m.user_email), /* @__PURE__ */ import_react6.default.createElement(
2286
+ import_Typography6.default,
2287
+ {
2288
+ variant: "caption",
2289
+ color: "text.secondary",
2290
+ sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 11 }
2291
+ },
2292
+ m.user_id
2293
+ )) : /* @__PURE__ */ import_react6.default.createElement(
2294
+ import_Typography6.default,
2295
+ {
2296
+ variant: "body2",
2297
+ sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace" }
2298
+ },
2299
+ m.user_id
2300
+ )), /* @__PURE__ */ import_react6.default.createElement(import_TableCell3.default, { align: "right" }, /* @__PURE__ */ import_react6.default.createElement(
2301
+ StatusPill,
2302
+ {
2303
+ label: m.role,
2304
+ tone: m.role === "admin" ? "accent" : "neutral",
2305
+ dot: false
2306
+ }
2307
+ ))))))
2308
+ ) : /* @__PURE__ */ import_react6.default.createElement(import_Typography6.default, { variant: "body2", color: "text.secondary" }, "No members assigned."))));
1264
2309
  };
1265
2310
  TeamUsage = ({
1266
2311
  teams,
@@ -1269,12 +2314,18 @@ var init_TeamUsage = __esm({
1269
2314
  getTeamUsageLoading
1270
2315
  }) => {
1271
2316
  if (loading) {
1272
- return /* @__PURE__ */ import_react5.default.createElement(import_material5.Paper, { sx: { p: 2 } }, /* @__PURE__ */ import_react5.default.createElement(import_material5.LinearProgress, null));
2317
+ return /* @__PURE__ */ import_react6.default.createElement(SectionCard, { title: "Teams" }, /* @__PURE__ */ import_react6.default.createElement(import_Skeleton4.default, { variant: "rounded", height: 120, sx: { mb: 2 } }), /* @__PURE__ */ import_react6.default.createElement(import_Skeleton4.default, { variant: "rounded", height: 120 }));
1273
2318
  }
1274
2319
  if (!teams.length) {
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."))));
2320
+ return /* @__PURE__ */ import_react6.default.createElement(SectionCard, { title: "Teams" }, /* @__PURE__ */ import_react6.default.createElement(
2321
+ EmptyState,
2322
+ {
2323
+ message: "You're not a member of any LiteLLM team yet.",
2324
+ hint: "Your account is provisioned, so you can still generate personal keys and use models. Ask an admin to add you to a team if you need a shared budget."
2325
+ }
2326
+ ));
1276
2327
  }
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(
2328
+ return /* @__PURE__ */ import_react6.default.createElement(SectionCard, { title: "Teams", subtitle: `${teams.length} team${teams.length === 1 ? "" : "s"}` }, /* @__PURE__ */ import_react6.default.createElement(import_Box6.default, { sx: { display: "flex", flexDirection: "column", gap: 2 } }, teams.map((team) => /* @__PURE__ */ import_react6.default.createElement(
1278
2329
  TeamCard,
1279
2330
  {
1280
2331
  key: team.team_id,
@@ -1282,19 +2333,23 @@ var init_TeamUsage = __esm({
1282
2333
  usage: getTeamUsage(team.team_id),
1283
2334
  usageLoading: getTeamUsageLoading(team.team_id)
1284
2335
  }
1285
- )));
2336
+ ))));
1286
2337
  };
1287
2338
  }
1288
2339
  });
1289
2340
 
1290
2341
  // src/components/AuditLog.tsx
1291
- function actionColor(action) {
1292
- if (!action) return "default";
1293
- for (const [key, color] of Object.entries(ACTION_COLORS)) {
1294
- if (action.toLowerCase().includes(key)) return color;
2342
+ function actionTone(action) {
2343
+ if (!action) return "neutral";
2344
+ for (const [key, tone] of Object.entries(ACTION_TONES)) {
2345
+ if (action.toLowerCase().includes(key)) return tone;
1295
2346
  }
1296
2347
  return "info";
1297
2348
  }
2349
+ function prettyTableName(name) {
2350
+ if (!name) return "\u2014";
2351
+ return TABLE_LABELS[name] ?? name;
2352
+ }
1298
2353
  function formatDateTime(iso) {
1299
2354
  try {
1300
2355
  return new Date(iso).toLocaleString();
@@ -1302,31 +2357,126 @@ function formatDateTime(iso) {
1302
2357
  return iso;
1303
2358
  }
1304
2359
  }
1305
- var import_react6, import_material6, import_icons_material5, import_react_use, ACTION_COLORS, DetailRow, AuditLog;
2360
+ function renderAuditLogBody(loading, entries) {
2361
+ if (loading) {
2362
+ return /* @__PURE__ */ import_react7.default.createElement(import_TableRow4.default, null, /* @__PURE__ */ import_react7.default.createElement(import_TableCell4.default, { colSpan: 6, sx: { py: 2 } }, /* @__PURE__ */ import_react7.default.createElement(import_Skeleton5.default, { variant: "rounded", height: 160 })));
2363
+ }
2364
+ if (entries.length === 0) {
2365
+ return /* @__PURE__ */ import_react7.default.createElement(import_TableRow4.default, null, /* @__PURE__ */ import_react7.default.createElement(import_TableCell4.default, { colSpan: 6 }, /* @__PURE__ */ import_react7.default.createElement(
2366
+ EmptyState,
2367
+ {
2368
+ message: "No audit events found",
2369
+ hint: "Try widening the filters or a different time window."
2370
+ }
2371
+ )));
2372
+ }
2373
+ return entries.map((entry) => /* @__PURE__ */ import_react7.default.createElement(DetailRow, { key: entry.id, entry }));
2374
+ }
2375
+ var import_react7, import_Box7, import_Typography7, import_Table4, import_TableBody4, import_TableCell4, import_TableContainer4, import_TableHead4, import_TableRow4, import_TablePagination, import_TextField4, import_MenuItem3, import_Skeleton5, import_Collapse2, import_IconButton4, import_Alert2, import_styles5, import_icons_material5, import_react_use, ACTION_TONES, TABLE_LABELS, DetailRow, AuditLog;
1306
2376
  var init_AuditLog = __esm({
1307
2377
  "src/components/AuditLog.tsx"() {
1308
2378
  "use strict";
1309
- import_react6 = __toESM(require("react"));
1310
- import_material6 = require("@mui/material");
2379
+ import_react7 = __toESM(require("react"));
2380
+ import_Box7 = __toESM(require("@mui/material/Box"));
2381
+ import_Typography7 = __toESM(require("@mui/material/Typography"));
2382
+ import_Table4 = __toESM(require("@mui/material/Table"));
2383
+ import_TableBody4 = __toESM(require("@mui/material/TableBody"));
2384
+ import_TableCell4 = __toESM(require("@mui/material/TableCell"));
2385
+ import_TableContainer4 = __toESM(require("@mui/material/TableContainer"));
2386
+ import_TableHead4 = __toESM(require("@mui/material/TableHead"));
2387
+ import_TableRow4 = __toESM(require("@mui/material/TableRow"));
2388
+ import_TablePagination = __toESM(require("@mui/material/TablePagination"));
2389
+ import_TextField4 = __toESM(require("@mui/material/TextField"));
2390
+ import_MenuItem3 = __toESM(require("@mui/material/MenuItem"));
2391
+ import_Skeleton5 = __toESM(require("@mui/material/Skeleton"));
2392
+ import_Collapse2 = __toESM(require("@mui/material/Collapse"));
2393
+ import_IconButton4 = __toESM(require("@mui/material/IconButton"));
2394
+ import_Alert2 = __toESM(require("@mui/material/Alert"));
2395
+ import_styles5 = require("@mui/material/styles");
1311
2396
  import_icons_material5 = require("@mui/icons-material");
1312
2397
  import_react_use = require("react-use");
1313
- ACTION_COLORS = {
2398
+ init_ui();
2399
+ ACTION_TONES = {
1314
2400
  created: "success",
1315
- deleted: "error",
2401
+ deleted: "danger",
1316
2402
  updated: "warning",
1317
- blocked: "error",
2403
+ blocked: "danger",
1318
2404
  unblocked: "success"
1319
2405
  };
2406
+ TABLE_LABELS = {
2407
+ LiteLLM_VerificationToken: "Key",
2408
+ LiteLLM_TeamTable: "Team",
2409
+ LiteLLM_UserTable: "User"
2410
+ };
1320
2411
  DetailRow = ({ entry }) => {
1321
- const [open, setOpen] = (0, import_react6.useState)(false);
2412
+ const [open, setOpen] = (0, import_react7.useState)(false);
1322
2413
  const hasDetail = entry.before_value || entry.updated_values;
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))))))));
2414
+ return /* @__PURE__ */ import_react7.default.createElement(import_react7.default.Fragment, null, /* @__PURE__ */ import_react7.default.createElement(import_TableRow4.default, null, /* @__PURE__ */ import_react7.default.createElement(import_TableCell4.default, { sx: { width: 40, pr: 0 } }, hasDetail && /* @__PURE__ */ import_react7.default.createElement(
2415
+ import_IconButton4.default,
2416
+ {
2417
+ size: "small",
2418
+ onClick: () => setOpen((o) => !o),
2419
+ "aria-label": open ? "Hide changes" : "Show changes",
2420
+ "aria-expanded": open,
2421
+ sx: (theme) => ({
2422
+ color: theme.palette.text.secondary,
2423
+ transform: open ? "rotate(180deg)" : "none",
2424
+ transition: theme.transitions.create("transform")
2425
+ })
2426
+ },
2427
+ /* @__PURE__ */ import_react7.default.createElement(import_icons_material5.KeyboardArrowDown, { fontSize: "small" })
2428
+ )), /* @__PURE__ */ import_react7.default.createElement(import_TableCell4.default, { sx: { whiteSpace: "nowrap" } }, /* @__PURE__ */ import_react7.default.createElement(import_Typography7.default, { variant: "body2", color: "text.secondary" }, formatDateTime(entry.updated_at))), /* @__PURE__ */ import_react7.default.createElement(import_TableCell4.default, null, entry.action && /* @__PURE__ */ import_react7.default.createElement(StatusPill, { label: entry.action, tone: actionTone(entry.action) })), /* @__PURE__ */ import_react7.default.createElement(import_TableCell4.default, null, /* @__PURE__ */ import_react7.default.createElement(import_Typography7.default, { variant: "body2" }, prettyTableName(entry.table_name))), /* @__PURE__ */ import_react7.default.createElement(import_TableCell4.default, null, /* @__PURE__ */ import_react7.default.createElement(
2429
+ import_Typography7.default,
2430
+ {
2431
+ variant: "body2",
2432
+ component: "code",
2433
+ color: "text.secondary",
2434
+ title: entry.object_id ?? void 0,
2435
+ sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 12 }
2436
+ },
2437
+ entry.object_id ? entry.object_id.slice(0, 20) + (entry.object_id.length > 20 ? "\u2026" : "") : "\u2014"
2438
+ )), /* @__PURE__ */ import_react7.default.createElement(import_TableCell4.default, null, entry.changed_by ?? "\u2014")), hasDetail && /* @__PURE__ */ import_react7.default.createElement(import_TableRow4.default, { sx: { "&&:hover": { bgcolor: "transparent" } } }, /* @__PURE__ */ import_react7.default.createElement(import_TableCell4.default, { colSpan: 6, sx: { "&&": { py: 0, border: 0 } } }, /* @__PURE__ */ import_react7.default.createElement(import_Collapse2.default, { in: open, unmountOnExit: true }, /* @__PURE__ */ import_react7.default.createElement(
2439
+ import_Box7.default,
2440
+ {
2441
+ display: "flex",
2442
+ gap: 2,
2443
+ flexWrap: "wrap",
2444
+ sx: (theme) => ({
2445
+ p: 2,
2446
+ mb: 1.5,
2447
+ borderRadius: 1.5,
2448
+ bgcolor: (0, import_styles5.alpha)(theme.palette.text.primary, theme.palette.mode === "dark" ? 0.05 : 0.03)
2449
+ })
2450
+ },
2451
+ entry.before_value && /* @__PURE__ */ import_react7.default.createElement(import_Box7.default, { flex: 1, minWidth: 200 }, /* @__PURE__ */ import_react7.default.createElement(
2452
+ import_Typography7.default,
2453
+ {
2454
+ variant: "caption",
2455
+ color: "text.secondary",
2456
+ display: "block",
2457
+ mb: 0.75,
2458
+ sx: { fontSize: 10.5, fontWeight: 700, letterSpacing: "0.07em", textTransform: "uppercase" }
2459
+ },
2460
+ "Before"
2461
+ ), /* @__PURE__ */ import_react7.default.createElement(import_Typography7.default, { component: "pre", variant: "caption", sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", whiteSpace: "pre-wrap", wordBreak: "break-all", m: 0 } }, JSON.stringify(entry.before_value, null, 2))),
2462
+ entry.updated_values && /* @__PURE__ */ import_react7.default.createElement(import_Box7.default, { flex: 1, minWidth: 200 }, /* @__PURE__ */ import_react7.default.createElement(
2463
+ import_Typography7.default,
2464
+ {
2465
+ variant: "caption",
2466
+ color: "text.secondary",
2467
+ display: "block",
2468
+ mb: 0.75,
2469
+ sx: { fontSize: 10.5, fontWeight: 700, letterSpacing: "0.07em", textTransform: "uppercase" }
2470
+ },
2471
+ "After"
2472
+ ), /* @__PURE__ */ import_react7.default.createElement(import_Typography7.default, { component: "pre", variant: "caption", sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", whiteSpace: "pre-wrap", wordBreak: "break-all", m: 0 } }, JSON.stringify(entry.updated_values, null, 2)))
2473
+ )))));
1324
2474
  };
1325
2475
  AuditLog = ({ api }) => {
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)(
2476
+ const [page, setPage] = (0, import_react7.useState)(0);
2477
+ const [pageSize, setPageSize] = (0, import_react7.useState)(25);
2478
+ const [filters, setFilters] = (0, import_react7.useState)({});
2479
+ const fetchParams = (0, import_react7.useCallback)(
1330
2480
  () => ({ page: page + 1, page_size: pageSize, ...filters }),
1331
2481
  [page, pageSize, filters]
1332
2482
  )();
@@ -1336,68 +2486,80 @@ var init_AuditLog = __esm({
1336
2486
  );
1337
2487
  const entries = value?.audit_logs ?? [];
1338
2488
  const total = value?.total ?? 0;
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,
1341
- {
1342
- size: "small",
1343
- label: "Action",
1344
- select: true,
1345
- value: filters.action ?? "",
1346
- onChange: (e) => {
1347
- setFilters((f) => ({ ...f, action: e.target.value || void 0 }));
1348
- setPage(0);
1349
- },
1350
- sx: { minWidth: 160 }
1351
- },
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,
2489
+ return /* @__PURE__ */ import_react7.default.createElement(
2490
+ SectionCard,
1359
2491
  {
1360
- size: "small",
1361
- label: "Table",
1362
- select: true,
1363
- value: filters.table_name ?? "",
1364
- onChange: (e) => {
1365
- setFilters((f) => ({ ...f, table_name: e.target.value || void 0 }));
1366
- setPage(0);
1367
- },
1368
- sx: { minWidth: 160 }
2492
+ title: "Audit Log",
2493
+ subtitle: loading ? "Loading\u2026" : `${total.toLocaleString()} event${total === 1 ? "" : "s"}`,
2494
+ flush: true,
2495
+ actions: /* @__PURE__ */ import_react7.default.createElement(import_react7.default.Fragment, null, /* @__PURE__ */ import_react7.default.createElement(
2496
+ import_TextField4.default,
2497
+ {
2498
+ size: "small",
2499
+ label: "Action",
2500
+ select: true,
2501
+ value: filters.action ?? "",
2502
+ onChange: (e) => {
2503
+ setFilters((f) => ({ ...f, action: e.target.value || void 0 }));
2504
+ setPage(0);
2505
+ },
2506
+ sx: { minWidth: 150 }
2507
+ },
2508
+ /* @__PURE__ */ import_react7.default.createElement(import_MenuItem3.default, { value: "" }, "All actions"),
2509
+ /* @__PURE__ */ import_react7.default.createElement(import_MenuItem3.default, { value: "created" }, "Created"),
2510
+ /* @__PURE__ */ import_react7.default.createElement(import_MenuItem3.default, { value: "updated" }, "Updated"),
2511
+ /* @__PURE__ */ import_react7.default.createElement(import_MenuItem3.default, { value: "deleted" }, "Deleted"),
2512
+ /* @__PURE__ */ import_react7.default.createElement(import_MenuItem3.default, { value: "blocked" }, "Blocked")
2513
+ ), /* @__PURE__ */ import_react7.default.createElement(
2514
+ import_TextField4.default,
2515
+ {
2516
+ size: "small",
2517
+ label: "Table",
2518
+ select: true,
2519
+ value: filters.table_name ?? "",
2520
+ onChange: (e) => {
2521
+ setFilters((f) => ({ ...f, table_name: e.target.value || void 0 }));
2522
+ setPage(0);
2523
+ },
2524
+ sx: { minWidth: 150 }
2525
+ },
2526
+ /* @__PURE__ */ import_react7.default.createElement(import_MenuItem3.default, { value: "" }, "All tables"),
2527
+ /* @__PURE__ */ import_react7.default.createElement(import_MenuItem3.default, { value: "LiteLLM_VerificationToken" }, "Key"),
2528
+ /* @__PURE__ */ import_react7.default.createElement(import_MenuItem3.default, { value: "LiteLLM_TeamTable" }, "Team"),
2529
+ /* @__PURE__ */ import_react7.default.createElement(import_MenuItem3.default, { value: "LiteLLM_UserTable" }, "User")
2530
+ ), /* @__PURE__ */ import_react7.default.createElement(
2531
+ import_TextField4.default,
2532
+ {
2533
+ size: "small",
2534
+ label: "Changed by",
2535
+ value: filters.changed_by ?? "",
2536
+ onChange: (e) => {
2537
+ setFilters((f) => ({ ...f, changed_by: e.target.value || void 0 }));
2538
+ setPage(0);
2539
+ },
2540
+ sx: { minWidth: 180 }
2541
+ }
2542
+ ))
1369
2543
  },
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,
1376
- {
1377
- size: "small",
1378
- label: "Changed by",
1379
- value: filters.changed_by ?? "",
1380
- onChange: (e) => {
1381
- setFilters((f) => ({ ...f, changed_by: e.target.value || void 0 }));
1382
- setPage(0);
1383
- },
1384
- sx: { minWidth: 200 }
1385
- }
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,
1388
- {
1389
- component: "div",
1390
- count: total,
1391
- page,
1392
- onPageChange: (_, p) => setPage(p),
1393
- rowsPerPage: pageSize,
1394
- onRowsPerPageChange: (e) => {
1395
- setPageSize(Number(e.target.value));
1396
- setPage(0);
1397
- },
1398
- rowsPerPageOptions: [10, 25, 50]
1399
- }
1400
- ));
2544
+ error && /* @__PURE__ */ import_react7.default.createElement(import_Box7.default, { px: 2.5, pb: 2 }, /* @__PURE__ */ import_react7.default.createElement(import_Alert2.default, { severity: "error" }, error.message)),
2545
+ /* @__PURE__ */ import_react7.default.createElement(import_TableContainer4.default, null, /* @__PURE__ */ import_react7.default.createElement(import_Table4.default, { size: "small", sx: dataTableSx }, /* @__PURE__ */ import_react7.default.createElement(import_TableHead4.default, null, /* @__PURE__ */ import_react7.default.createElement(import_TableRow4.default, null, /* @__PURE__ */ import_react7.default.createElement(import_TableCell4.default, { sx: { width: 40 } }), /* @__PURE__ */ import_react7.default.createElement(import_TableCell4.default, null, "Time"), /* @__PURE__ */ import_react7.default.createElement(import_TableCell4.default, null, "Action"), /* @__PURE__ */ import_react7.default.createElement(import_TableCell4.default, null, "Table"), /* @__PURE__ */ import_react7.default.createElement(import_TableCell4.default, null, "Object ID"), /* @__PURE__ */ import_react7.default.createElement(import_TableCell4.default, null, "Changed By"))), /* @__PURE__ */ import_react7.default.createElement(import_TableBody4.default, null, renderAuditLogBody(loading, entries)))),
2546
+ /* @__PURE__ */ import_react7.default.createElement(
2547
+ import_TablePagination.default,
2548
+ {
2549
+ component: "div",
2550
+ count: total,
2551
+ page,
2552
+ onPageChange: (_, p) => setPage(p),
2553
+ rowsPerPage: pageSize,
2554
+ onRowsPerPageChange: (e) => {
2555
+ setPageSize(Number(e.target.value));
2556
+ setPage(0);
2557
+ },
2558
+ rowsPerPageOptions: [10, 25, 50],
2559
+ sx: (theme) => ({ borderTop: `1px solid ${theme.palette.divider}` })
2560
+ }
2561
+ )
2562
+ );
1401
2563
  };
1402
2564
  }
1403
2565
  });
@@ -1421,12 +2583,19 @@ function initDateRange() {
1421
2583
  else start.setDate(start.getDate() - 7);
1422
2584
  return { start, end };
1423
2585
  }
1424
- var import_react7, import_material7, import_react_use2, import_core_plugin_api2, PERIOD_LS_KEY2, LiteLLMPage;
2586
+ var import_react8, import_Box8, import_Snackbar, import_Alert3, import_CircularProgress4, import_Typography8, import_Paper5, import_Tabs2, import_Tab2, import_react_use2, import_core_plugin_api2, PERIOD_LS_KEY2, LiteLLMPage;
1425
2587
  var init_LiteLLMPage = __esm({
1426
2588
  "src/components/LiteLLMPage.tsx"() {
1427
2589
  "use strict";
1428
- import_react7 = __toESM(require("react"));
1429
- import_material7 = require("@mui/material");
2590
+ import_react8 = __toESM(require("react"));
2591
+ import_Box8 = __toESM(require("@mui/material/Box"));
2592
+ import_Snackbar = __toESM(require("@mui/material/Snackbar"));
2593
+ import_Alert3 = __toESM(require("@mui/material/Alert"));
2594
+ import_CircularProgress4 = __toESM(require("@mui/material/CircularProgress"));
2595
+ import_Typography8 = __toESM(require("@mui/material/Typography"));
2596
+ import_Paper5 = __toESM(require("@mui/material/Paper"));
2597
+ import_Tabs2 = __toESM(require("@mui/material/Tabs"));
2598
+ import_Tab2 = __toESM(require("@mui/material/Tab"));
1430
2599
  import_react_use2 = require("react-use");
1431
2600
  import_core_plugin_api2 = require("@backstage/core-plugin-api");
1432
2601
  init_DashboardHeader();
@@ -1439,12 +2608,12 @@ var init_LiteLLMPage = __esm({
1439
2608
  PERIOD_LS_KEY2 = "litellm_usage_period";
1440
2609
  LiteLLMPage = () => {
1441
2610
  const api = (0, import_core_plugin_api2.useApi)(liteLlmApiRef);
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)({});
2611
+ const [dateRange, setDateRange] = (0, import_react8.useState)(initDateRange);
2612
+ const [activeTab, setActiveTab] = (0, import_react8.useState)("overview");
2613
+ const [snackbar, setSnackbar] = (0, import_react8.useState)(null);
2614
+ const [generateDialogOpen, setGenerateDialogOpen] = (0, import_react8.useState)(false);
2615
+ const [teamUsageCache, setTeamUsageCache] = (0, import_react8.useState)({});
2616
+ const [teamUsageLoading, setTeamUsageLoading] = (0, import_react8.useState)({});
1448
2617
  const { value: userInfo, loading: userLoading, error: userError } = (0, import_react_use2.useAsync)(
1449
2618
  () => api.getUserInfo(),
1450
2619
  [api]
@@ -1469,7 +2638,7 @@ var init_LiteLLMPage = __esm({
1469
2638
  [api]
1470
2639
  );
1471
2640
  const { value: liteLlmConfig } = (0, import_react_use2.useAsync)(() => api.getConfig(), [api]);
1472
- const teams = (0, import_react7.useMemo)(() => {
2641
+ const teams = (0, import_react8.useMemo)(() => {
1473
2642
  if (!allTeams?.length) return [];
1474
2643
  if (!userInfo) return allTeams;
1475
2644
  const userId = userInfo.user_id;
@@ -1486,12 +2655,12 @@ var init_LiteLLMPage = __esm({
1486
2655
  const endDate = dateRange.end.toISOString().split("T")[0];
1487
2656
  return api.getUsage(startDate, endDate);
1488
2657
  }, [api, dateRange]);
1489
- const handleDateRangeChange = (0, import_react7.useCallback)((range) => {
2658
+ const handleDateRangeChange = (0, import_react8.useCallback)((range) => {
1490
2659
  setDateRange(range);
1491
2660
  setTeamUsageCache({});
1492
2661
  setTeamUsageLoading({});
1493
2662
  }, [setDateRange]);
1494
- const loadTeamUsage = (0, import_react7.useCallback)(async (teamId) => {
2663
+ const loadTeamUsage = (0, import_react8.useCallback)(async (teamId) => {
1495
2664
  if (teamUsageCache[teamId] !== void 0 || teamUsageLoading[teamId]) return;
1496
2665
  setTeamUsageLoading((prev) => ({ ...prev, [teamId]: true }));
1497
2666
  try {
@@ -1505,7 +2674,7 @@ var init_LiteLLMPage = __esm({
1505
2674
  setTeamUsageLoading((prev) => ({ ...prev, [teamId]: false }));
1506
2675
  }
1507
2676
  }, [api, dateRange, teamUsageCache, teamUsageLoading]);
1508
- const allowedModels = (0, import_react7.useMemo)(() => {
2677
+ const allowedModels = (0, import_react8.useMemo)(() => {
1509
2678
  if (!allModels?.length) return [];
1510
2679
  const userModels = userInfo?.models;
1511
2680
  const teamModels = teams?.flatMap((t) => t.models ?? []);
@@ -1518,7 +2687,7 @@ var init_LiteLLMPage = __esm({
1518
2687
  ]);
1519
2688
  return allModels.filter((m) => allowed.has(m.model_name));
1520
2689
  }, [allModels, userInfo, teams]);
1521
- const handleGenerateKey = (0, import_react7.useCallback)(
2690
+ const handleGenerateKey = (0, import_react8.useCallback)(
1522
2691
  async (request) => {
1523
2692
  try {
1524
2693
  const response = await api.generateKey(request);
@@ -1532,7 +2701,7 @@ var init_LiteLLMPage = __esm({
1532
2701
  },
1533
2702
  [api, refreshKeys]
1534
2703
  );
1535
- const handleUpdateKey = (0, import_react7.useCallback)(
2704
+ const handleUpdateKey = (0, import_react8.useCallback)(
1536
2705
  async (keyId, request) => {
1537
2706
  try {
1538
2707
  await api.updateKey(keyId, request);
@@ -1545,7 +2714,7 @@ var init_LiteLLMPage = __esm({
1545
2714
  },
1546
2715
  [api, refreshKeys]
1547
2716
  );
1548
- const handleBlockKey = (0, import_react7.useCallback)(
2717
+ const handleBlockKey = (0, import_react8.useCallback)(
1549
2718
  async (keyId) => {
1550
2719
  try {
1551
2720
  await api.blockKey(keyId);
@@ -1557,7 +2726,7 @@ var init_LiteLLMPage = __esm({
1557
2726
  },
1558
2727
  [api, refreshKeys]
1559
2728
  );
1560
- const handleUnblockKey = (0, import_react7.useCallback)(
2729
+ const handleUnblockKey = (0, import_react8.useCallback)(
1561
2730
  async (keyId) => {
1562
2731
  try {
1563
2732
  await api.unblockKey(keyId);
@@ -1569,7 +2738,7 @@ var init_LiteLLMPage = __esm({
1569
2738
  },
1570
2739
  [api, refreshKeys]
1571
2740
  );
1572
- const handleResetKeySpend = (0, import_react7.useCallback)(
2741
+ const handleResetKeySpend = (0, import_react8.useCallback)(
1573
2742
  async (keyId) => {
1574
2743
  try {
1575
2744
  await api.resetKeySpend(keyId);
@@ -1581,7 +2750,7 @@ var init_LiteLLMPage = __esm({
1581
2750
  },
1582
2751
  [api, refreshKeys]
1583
2752
  );
1584
- const handleDeleteKey = (0, import_react7.useCallback)(
2753
+ const handleDeleteKey = (0, import_react8.useCallback)(
1585
2754
  async (keyId) => {
1586
2755
  try {
1587
2756
  await api.deleteKey(keyId);
@@ -1598,7 +2767,7 @@ var init_LiteLLMPage = __esm({
1598
2767
  },
1599
2768
  [api, refreshKeys]
1600
2769
  );
1601
- const handlePruneExpiredKeys = (0, import_react7.useCallback)(
2770
+ const handlePruneExpiredKeys = (0, import_react8.useCallback)(
1602
2771
  async () => {
1603
2772
  try {
1604
2773
  const result = await api.pruneExpiredKeys();
@@ -1613,34 +2782,42 @@ var init_LiteLLMPage = __esm({
1613
2782
  );
1614
2783
  const isInitialLoading = userLoading && !userInfo;
1615
2784
  if (isInitialLoading) {
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));
2785
+ return /* @__PURE__ */ import_react8.default.createElement(import_Box8.default, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "50vh" }, /* @__PURE__ */ import_react8.default.createElement(import_CircularProgress4.default, null));
1617
2786
  }
1618
2787
  if (userError || !userInfo) {
1619
2788
  const isProvisioningEnabled = userError?.body?.provisioning === true;
1620
2789
  const hint = userError?.body?.hint;
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.")));
2790
+ return /* @__PURE__ */ import_react8.default.createElement(import_Box8.default, { p: 3 }, /* @__PURE__ */ import_react8.default.createElement(import_Paper5.default, { sx: { p: 3 } }, /* @__PURE__ */ import_react8.default.createElement(import_Typography8.default, { variant: "h6", gutterBottom: true }, "Account not provisioned"), /* @__PURE__ */ import_react8.default.createElement(import_Typography8.default, { color: "text.secondary", paragraph: true }, "Your Backstage account is not linked to a LiteLLM user."), hint ? /* @__PURE__ */ import_react8.default.createElement(import_Typography8.default, { variant: "body2", color: "text.secondary" }, hint) : /* @__PURE__ */ import_react8.default.createElement(import_Typography8.default, { 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.")));
1622
2791
  }
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(
2792
+ const pageTabs = /* @__PURE__ */ import_react8.default.createElement(
2793
+ import_Tabs2.default,
2794
+ {
2795
+ value: activeTab,
2796
+ onChange: (_, v) => setActiveTab(v),
2797
+ variant: "scrollable",
2798
+ scrollButtons: "auto",
2799
+ sx: {
2800
+ minHeight: 44,
2801
+ '& [class*="MuiTabs-indicator"]': { height: 2, borderRadius: "2px 2px 0 0" },
2802
+ '& [class*="MuiTab-root"]': { minHeight: 44, textTransform: "none", fontSize: 14 }
2803
+ }
2804
+ },
2805
+ /* @__PURE__ */ import_react8.default.createElement(import_Tab2.default, { label: "Overview", value: "overview" }),
2806
+ /* @__PURE__ */ import_react8.default.createElement(import_Tab2.default, { label: "Keys", value: "keys" }),
2807
+ /* @__PURE__ */ import_react8.default.createElement(import_Tab2.default, { label: "Teams", value: "teams" }),
2808
+ userInfo.can_view_audit && /* @__PURE__ */ import_react8.default.createElement(import_Tab2.default, { label: "Audit Log", value: "audit" })
2809
+ );
2810
+ return /* @__PURE__ */ import_react8.default.createElement(import_Box8.default, { sx: { p: 3, display: "flex", flexDirection: "column", gap: 2 } }, /* @__PURE__ */ import_react8.default.createElement(
1624
2811
  DashboardHeader,
1625
2812
  {
1626
2813
  userInfo,
1627
2814
  teams: teams ?? [],
1628
2815
  keys: keys ?? [],
1629
2816
  loading: userLoading || teamsLoading,
1630
- onGenerateKeyClick: () => setGenerateDialogOpen(true)
2817
+ onGenerateKeyClick: () => setGenerateDialogOpen(true),
2818
+ tabs: pageTabs
1631
2819
  }
1632
- )), /* @__PURE__ */ import_react7.default.createElement(
1633
- import_material7.Tabs,
1634
- {
1635
- value: activeTab,
1636
- onChange: (_, v) => setActiveTab(v),
1637
- sx: { mb: 2, borderBottom: 1, borderColor: "divider" }
1638
- },
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(
2820
+ ), activeTab === "overview" && /* @__PURE__ */ import_react8.default.createElement(
1644
2821
  UsageStats,
1645
2822
  {
1646
2823
  usage: usage ?? null,
@@ -1650,7 +2827,7 @@ var init_LiteLLMPage = __esm({
1650
2827
  loading: usageLoading,
1651
2828
  userInfo
1652
2829
  }
1653
- ), activeTab === "keys" && /* @__PURE__ */ import_react7.default.createElement(
2830
+ ), activeTab === "keys" && /* @__PURE__ */ import_react8.default.createElement(
1654
2831
  KeysTable,
1655
2832
  {
1656
2833
  keys: keys ?? [],
@@ -1664,7 +2841,7 @@ var init_LiteLLMPage = __esm({
1664
2841
  onDeleteKey: handleDeleteKey,
1665
2842
  onPruneExpiredKeys: handlePruneExpiredKeys
1666
2843
  }
1667
- ), activeTab === "teams" && /* @__PURE__ */ import_react7.default.createElement(
2844
+ ), activeTab === "teams" && /* @__PURE__ */ import_react8.default.createElement(
1668
2845
  TeamUsage,
1669
2846
  {
1670
2847
  teams: teams ?? [],
@@ -1675,7 +2852,7 @@ var init_LiteLLMPage = __esm({
1675
2852
  },
1676
2853
  getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false
1677
2854
  }
1678
- ), activeTab === "audit" && userInfo.can_view_audit && /* @__PURE__ */ import_react7.default.createElement(AuditLog, { api }), /* @__PURE__ */ import_react7.default.createElement(
2855
+ ), activeTab === "audit" && userInfo.can_view_audit && /* @__PURE__ */ import_react8.default.createElement(AuditLog, { api }), /* @__PURE__ */ import_react8.default.createElement(
1679
2856
  GenerateKeyDialog,
1680
2857
  {
1681
2858
  open: generateDialogOpen,
@@ -1688,15 +2865,15 @@ var init_LiteLLMPage = __esm({
1688
2865
  onGenerateKey: handleGenerateKey,
1689
2866
  onGetConfig: () => api.getConfig()
1690
2867
  }
1691
- ), /* @__PURE__ */ import_react7.default.createElement(
1692
- import_material7.Snackbar,
2868
+ ), /* @__PURE__ */ import_react8.default.createElement(
2869
+ import_Snackbar.default,
1693
2870
  {
1694
2871
  open: !!snackbar,
1695
2872
  autoHideDuration: 5e3,
1696
2873
  onClose: () => setSnackbar(null),
1697
2874
  anchorOrigin: { vertical: "bottom", horizontal: "right" }
1698
2875
  },
1699
- snackbar ? /* @__PURE__ */ import_react7.default.createElement(import_material7.Alert, { severity: snackbar.severity, onClose: () => setSnackbar(null) }, snackbar.message) : void 0
2876
+ snackbar ? /* @__PURE__ */ import_react8.default.createElement(import_Alert3.default, { severity: snackbar.severity, onClose: () => setSnackbar(null) }, snackbar.message) : void 0
1700
2877
  ));
1701
2878
  };
1702
2879
  }
@@ -1718,7 +2895,7 @@ __export(index_exports, {
1718
2895
  module.exports = __toCommonJS(index_exports);
1719
2896
 
1720
2897
  // src/plugin.tsx
1721
- var import_react8 = __toESM(require("react"));
2898
+ var import_react9 = __toESM(require("react"));
1722
2899
  var import_icons_material6 = require("@mui/icons-material");
1723
2900
  var import_frontend_plugin_api = require("@backstage/frontend-plugin-api");
1724
2901
  init_api();
@@ -1733,10 +2910,10 @@ var liteLlmPage = import_frontend_plugin_api.PageBlueprint.make({
1733
2910
  params: {
1734
2911
  path: "/litellm",
1735
2912
  title: "LiteLLM",
1736
- icon: /* @__PURE__ */ import_react8.default.createElement(import_icons_material6.TrendingUp, null),
2913
+ icon: /* @__PURE__ */ import_react9.default.createElement(import_icons_material6.TrendingUp, null),
1737
2914
  loader: async () => {
1738
2915
  const { LiteLLMPage: LiteLLMPage2 } = await Promise.resolve().then(() => (init_LiteLLMPage(), LiteLLMPage_exports));
1739
- return /* @__PURE__ */ import_react8.default.createElement(LiteLLMPage2, null);
2916
+ return /* @__PURE__ */ import_react9.default.createElement(LiteLLMPage2, null);
1740
2917
  }
1741
2918
  }
1742
2919
  });
@@ -1753,8 +2930,16 @@ init_UsageStats();
1753
2930
  init_TeamUsage();
1754
2931
 
1755
2932
  // src/components/LiteLLMHomeWidget.tsx
1756
- var import_react9 = __toESM(require("react"));
1757
- var import_material8 = require("@mui/material");
2933
+ var import_react10 = __toESM(require("react"));
2934
+ var import_Paper6 = __toESM(require("@mui/material/Paper"));
2935
+ var import_Box9 = __toESM(require("@mui/material/Box"));
2936
+ var import_Typography9 = __toESM(require("@mui/material/Typography"));
2937
+ var import_FormControl = __toESM(require("@mui/material/FormControl"));
2938
+ var import_Select = __toESM(require("@mui/material/Select"));
2939
+ var import_MenuItem4 = __toESM(require("@mui/material/MenuItem"));
2940
+ var import_Grid2 = __toESM(require("@mui/material/Grid"));
2941
+ var import_CircularProgress5 = __toESM(require("@mui/material/CircularProgress"));
2942
+ var import_Alert4 = __toESM(require("@mui/material/Alert"));
1758
2943
  var import_recharts3 = require("recharts");
1759
2944
  var import_core_plugin_api3 = require("@backstage/core-plugin-api");
1760
2945
  init_api();
@@ -1771,19 +2956,19 @@ function presetToDateRange(preset) {
1771
2956
  }
1772
2957
  return { start, end };
1773
2958
  }
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));
2959
+ var Kpi = ({ label, value }) => /* @__PURE__ */ import_react10.default.createElement(import_Box9.default, null, /* @__PURE__ */ import_react10.default.createElement(import_Typography9.default, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ import_react10.default.createElement(import_Typography9.default, { variant: "subtitle1", fontWeight: 600 }, value));
1775
2960
  var LiteLLMHomeWidget = ({
1776
2961
  defaultPeriod = "7d",
1777
2962
  title = "LiteLLM Usage"
1778
2963
  }) => {
1779
2964
  const api = (0, import_core_plugin_api3.useApi)(liteLlmApiRef);
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)(() => {
2965
+ const [period, setPeriod] = (0, import_react10.useState)(defaultPeriod);
2966
+ const [loading, setLoading] = (0, import_react10.useState)(true);
2967
+ const [usageError, setUsageError] = (0, import_react10.useState)(null);
2968
+ const [keysError, setKeysError] = (0, import_react10.useState)(null);
2969
+ const [usage, setUsage] = (0, import_react10.useState)(null);
2970
+ const [keys, setKeys] = (0, import_react10.useState)([]);
2971
+ (0, import_react10.useEffect)(() => {
1787
2972
  let cancelled = false;
1788
2973
  setLoading(true);
1789
2974
  setUsageError(null);
@@ -1820,17 +3005,17 @@ var LiteLLMHomeWidget = ({
1820
3005
  spend: d.spend
1821
3006
  }));
1822
3007
  const hasSparkline = dailyData.length > 0;
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,
3008
+ return /* @__PURE__ */ import_react10.default.createElement(import_Paper6.default, { sx: { p: 2 } }, /* @__PURE__ */ import_react10.default.createElement(import_Box9.default, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1.5 }, /* @__PURE__ */ import_react10.default.createElement(import_Typography9.default, { variant: "h6" }, title), /* @__PURE__ */ import_react10.default.createElement(import_FormControl.default, { size: "small", sx: { minWidth: 90 } }, /* @__PURE__ */ import_react10.default.createElement(
3009
+ import_Select.default,
1825
3010
  {
1826
3011
  value: period,
1827
3012
  onChange: (e) => setPeriod(e.target.value),
1828
3013
  displayEmpty: true
1829
3014
  },
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(
3015
+ /* @__PURE__ */ import_react10.default.createElement(import_MenuItem4.default, { value: "today" }, "Today"),
3016
+ /* @__PURE__ */ import_react10.default.createElement(import_MenuItem4.default, { value: "7d" }, "7d"),
3017
+ /* @__PURE__ */ import_react10.default.createElement(import_MenuItem4.default, { value: "30d" }, "30d")
3018
+ ))), loading && /* @__PURE__ */ import_react10.default.createElement(import_Box9.default, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ import_react10.default.createElement(import_CircularProgress5.default, { size: 32 })), !loading && totalFailure && /* @__PURE__ */ import_react10.default.createElement(import_Alert4.default, { severity: "error", sx: { mt: 1 } }, usageError ?? "Failed to load usage data"), !loading && !totalFailure && /* @__PURE__ */ import_react10.default.createElement(import_react10.default.Fragment, null, partialFailure && /* @__PURE__ */ import_react10.default.createElement(import_Alert4.default, { severity: "warning", sx: { mt: 1, mb: 1 } }, usageError ? `Usage data unavailable (${usageError}).` : "", keysError ? ` Key list unavailable (${keysError}).` : "", " Showing what loaded."), /* @__PURE__ */ import_react10.default.createElement(import_Grid2.default, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ import_react10.default.createElement(import_Grid2.default, { item: true, xs: 6 }, /* @__PURE__ */ import_react10.default.createElement(Kpi, { label: "USD Spent", value: fmtUsd(usage?.total_spend ?? 0) })), /* @__PURE__ */ import_react10.default.createElement(import_Grid2.default, { item: true, xs: 6 }, /* @__PURE__ */ import_react10.default.createElement(Kpi, { label: "Tokens In", value: fmtInt(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ import_react10.default.createElement(import_Grid2.default, { item: true, xs: 6 }, /* @__PURE__ */ import_react10.default.createElement(Kpi, { label: "Tokens Out", value: fmtInt(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ import_react10.default.createElement(import_Grid2.default, { item: true, xs: 6 }, /* @__PURE__ */ import_react10.default.createElement(Kpi, { label: "Keys", value: fmtInt(keys.length) }))), hasSparkline && /* @__PURE__ */ import_react10.default.createElement(import_Box9.default, { height: 120 }, /* @__PURE__ */ import_react10.default.createElement(import_recharts3.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react10.default.createElement(import_recharts3.AreaChart, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 } }, /* @__PURE__ */ import_react10.default.createElement(
1834
3019
  import_recharts3.Area,
1835
3020
  {
1836
3021
  type: "monotone",