@acarmisc/backstage-plugin-litellm 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -147,6 +147,9 @@ var init_api = __esm({
147
147
  end_date: endDate
148
148
  });
149
149
  }
150
+ async getConfig() {
151
+ return this.get("/config");
152
+ }
150
153
  };
151
154
  }
152
155
  });
@@ -177,6 +180,20 @@ var init_DashboardHeader = __esm({
177
180
  }
178
181
  });
179
182
 
183
+ // src/format.ts
184
+ var fmtUsd, fmtInt, estimateTokensFromBudget;
185
+ var init_format = __esm({
186
+ "src/format.ts"() {
187
+ "use strict";
188
+ fmtUsd = (n) => `$${(n ?? 0).toFixed(n < 1 ? 4 : 2)}`;
189
+ fmtInt = (n) => (n ?? 0).toLocaleString();
190
+ estimateTokensFromBudget = (budget, pricePerToken) => {
191
+ if (!pricePerToken || pricePerToken <= 0 || !budget || budget <= 0) return null;
192
+ return Math.floor(budget / pricePerToken);
193
+ };
194
+ }
195
+ });
196
+
180
197
  // src/components/KeysTable.tsx
181
198
  import React2, { useState, useMemo } from "react";
182
199
  import {
@@ -201,9 +218,13 @@ import {
201
218
  CircularProgress,
202
219
  Autocomplete,
203
220
  LinearProgress as LinearProgress2,
204
- InputAdornment
221
+ InputAdornment,
222
+ Checkbox,
223
+ FormControlLabel,
224
+ Tabs,
225
+ Tab
205
226
  } from "@mui/material";
206
- import { ContentCopy, Delete, Add, Edit, Autorenew, Search, Warning, Lock, LockOpen } from "@mui/icons-material";
227
+ import { ContentCopy, Delete, Add, Edit, Autorenew, Search, Warning, Lock, LockOpen, Code } from "@mui/icons-material";
207
228
  function ExpiryChip({ expiresAt }) {
208
229
  const status = expiryStatus(expiresAt);
209
230
  if (!status) return /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "text.secondary" }, "-");
@@ -223,11 +244,54 @@ function fmtCost(perToken) {
223
244
  const per1k = perToken * 1e3;
224
245
  return per1k < 0.01 ? `$${(perToken * 1e6).toFixed(2)}/M` : `$${per1k.toFixed(3)}/1K`;
225
246
  }
226
- var maskKey, shortKeyId, formatDate, emptyForm, keyToEditForm, KeysTable;
247
+ function formatContextWindow(maxInput, maxOutput) {
248
+ if (!maxInput && !maxOutput) return null;
249
+ const fmt = (n) => {
250
+ if (!n) return null;
251
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
252
+ if (n >= 1e3) return `${Math.round(n / 1e3)}K`;
253
+ return String(n);
254
+ };
255
+ const inPart = fmt(maxInput);
256
+ const outPart = fmt(maxOutput);
257
+ if (inPart && outPart) return `ctx ${inPart} in / ${outPart} out`;
258
+ if (inPart) return `ctx ${inPart}`;
259
+ if (outPart) return `ctx ${outPart} out`;
260
+ return null;
261
+ }
262
+ function trimSlash(url) {
263
+ return url.replace(/\/+$/, "");
264
+ }
265
+ function buildSnippets(baseUrl, key, model) {
266
+ const base = trimSlash(baseUrl);
267
+ return {
268
+ curl: `curl ${base}/v1/chat/completions \\
269
+ -H "Authorization: Bearer ${key}" \\
270
+ -H "Content-Type: application/json" \\
271
+ -d '{
272
+ "model": "${model}",
273
+ "messages": [{ "role": "user", "content": "Hello!" }]
274
+ }'`,
275
+ openai: `from openai import OpenAI
276
+
277
+ client = OpenAI(
278
+ api_key="${key}",
279
+ base_url="${base}/v1",
280
+ )
281
+
282
+ response = client.chat.completions.create(
283
+ model="${model}",
284
+ messages=[{ "role": "user", "content": "Hello!" }],
285
+ )
286
+ print(response.choices[0].message.content)`
287
+ };
288
+ }
289
+ var maskKey, shortKeyId, formatDate, emptyForm, keyToEditForm, KeysTable, SnippetTabs;
227
290
  var init_KeysTable = __esm({
228
291
  "src/components/KeysTable.tsx"() {
229
292
  "use strict";
230
293
  init_api();
294
+ init_format();
231
295
  maskKey = (key) => {
232
296
  if (key.length <= 8) return "***";
233
297
  return `${key.slice(0, 4)}...${key.slice(-4)}`;
@@ -252,7 +316,9 @@ var init_KeysTable = __esm({
252
316
  tpm_limit: void 0,
253
317
  rpm_limit: void 0,
254
318
  team_id: void 0,
255
- key_type: "llm_api"
319
+ key_type: "llm_api",
320
+ auto_rotate: false,
321
+ rotation_interval_days: void 0
256
322
  });
257
323
  keyToEditForm = (k) => ({
258
324
  key_alias: k.key_alias ?? "",
@@ -273,11 +339,15 @@ var init_KeysTable = __esm({
273
339
  onUnblockKey,
274
340
  onResetKeySpend,
275
341
  onDeleteKey,
276
- onPruneExpiredKeys
342
+ onPruneExpiredKeys,
343
+ onGetConfig
277
344
  }) => {
278
345
  const [generateModalOpen, setGenerateModalOpen] = useState(false);
279
346
  const [newKeyValue, setNewKeyValue] = useState(null);
347
+ const [newKeySnippets, setNewKeySnippets] = useState(null);
348
+ const [newKeyModel, setNewKeyModel] = useState("");
280
349
  const [formData, setFormData] = useState(emptyForm());
350
+ const [unlimitedBudget, setUnlimitedBudget] = useState(false);
281
351
  const [submitting, setSubmitting] = useState(false);
282
352
  const [editingKey, setEditingKey] = useState(null);
283
353
  const [editForm, setEditForm] = useState({});
@@ -300,12 +370,35 @@ var init_KeysTable = __esm({
300
370
  const selectedModels = models.filter((m) => (formData.models || []).includes(m.model_name));
301
371
  const selectedTeam = teams.find((t) => t.team_id === formData.team_id) ?? null;
302
372
  const editSelectedModels = models.filter((m) => (editForm.models || []).includes(m.model_name));
373
+ const aliasError = !(formData.alias || "").trim();
374
+ const budgetInvalid = !unlimitedBudget && (formData.max_budget === void 0 || formData.max_budget === null || formData.max_budget <= 0);
375
+ const canGenerate = !aliasError && !budgetInvalid && !submitting;
376
+ const budgetEstimate = useMemo(() => {
377
+ if (unlimitedBudget || budgetInvalid) return null;
378
+ const inputCosts = selectedModels.map((m) => m.input_cost_per_token).filter((c) => typeof c === "number" && c > 0);
379
+ if (inputCosts.length === 0) return null;
380
+ const pricePerToken = Math.max(...inputCosts);
381
+ return estimateTokensFromBudget(formData.max_budget ?? 0, pricePerToken);
382
+ }, [formData.max_budget, selectedModels, unlimitedBudget, budgetInvalid]);
303
383
  const handleGenerate = async () => {
304
384
  setSubmitting(true);
305
385
  try {
306
- const response = await onGenerateKey(formData);
386
+ const request = {
387
+ ...formData,
388
+ max_budget: unlimitedBudget ? null : formData.max_budget
389
+ };
390
+ const response = await onGenerateKey(request);
307
391
  setNewKeyValue(response.key);
392
+ setNewKeyModel(formData.models?.[0] ?? "");
393
+ setNewKeySnippets(null);
394
+ try {
395
+ const config = await onGetConfig();
396
+ const model = formData.models?.[0] ?? "";
397
+ setNewKeySnippets(buildSnippets(config.baseUrl, response.key, model));
398
+ } catch {
399
+ }
308
400
  setFormData(emptyForm());
401
+ setUnlimitedBudget(false);
309
402
  } catch (error) {
310
403
  console.error("Failed to generate key:", error);
311
404
  } finally {
@@ -315,7 +408,9 @@ var init_KeysTable = __esm({
315
408
  const handleCloseModal = () => {
316
409
  setGenerateModalOpen(false);
317
410
  setNewKeyValue(null);
411
+ setNewKeySnippets(null);
318
412
  setFormData(emptyForm());
413
+ setUnlimitedBudget(false);
319
414
  };
320
415
  const handleOpenEdit = (k) => {
321
416
  setEditingKey(k);
@@ -406,7 +501,8 @@ var init_KeysTable = __esm({
406
501
  const modelOption = (m) => {
407
502
  const inCost = fmtCost(m.input_cost_per_token);
408
503
  const outCost = fmtCost(m.output_cost_per_token);
409
- return /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement("span", null, m.model_name), m.supports_function_calling && " \u{1F527}", m.supports_vision && " \u{1F441}\uFE0F", (inCost || outCost) && /* @__PURE__ */ React2.createElement(Typography2, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, inCost, " in \xB7 ", outCost, " out"));
504
+ const ctx = formatContextWindow(m.max_input_tokens, m.max_output_tokens);
505
+ return /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement("span", null, m.model_name), m.supports_function_calling && " \u{1F527}", m.supports_vision && " \u{1F441}\uFE0F", ctx && /* @__PURE__ */ React2.createElement(Typography2, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, ctx), (inCost || outCost) && /* @__PURE__ */ React2.createElement(Typography2, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, inCost, " in \xB7 ", outCost, " out"));
410
506
  };
411
507
  return /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement(Paper2, { sx: { mb: 2 } }, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", justifyContent: "space-between", alignItems: "center", p: 2, gap: 2 }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "h6" }, "Virtual Keys"), /* @__PURE__ */ React2.createElement(Box2, { display: "flex", gap: 1, alignItems: "center", flex: 1, justifyContent: "flex-end" }, /* @__PURE__ */ React2.createElement(
412
508
  TextField,
@@ -440,7 +536,17 @@ var init_KeysTable = __esm({
440
536
  onClick: () => setGenerateModalOpen(true)
441
537
  },
442
538
  "Generate New Key"
443
- ))), /* @__PURE__ */ React2.createElement(TableContainer, null, /* @__PURE__ */ React2.createElement(Table, null, /* @__PURE__ */ React2.createElement(TableHead, null, /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, null, "Alias"), /* @__PURE__ */ React2.createElement(TableCell, null, "Key ID"), /* @__PURE__ */ React2.createElement(TableCell, null, "Created"), /* @__PURE__ */ React2.createElement(TableCell, null, "Expires"), /* @__PURE__ */ React2.createElement(TableCell, null, "Budget"), /* @__PURE__ */ React2.createElement(TableCell, null, "TPM / RPM"), /* @__PURE__ */ React2.createElement(TableCell, null, "Models"), /* @__PURE__ */ React2.createElement(TableCell, { align: "right" }, "Actions"))), /* @__PURE__ */ React2.createElement(TableBody, null, loading ? /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ React2.createElement(CircularProgress, { size: 24 }))) : filteredKeys.length === 0 ? /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ React2.createElement(Typography2, { color: "text.secondary" }, filterText ? "No keys match filter" : "No keys found"))) : filteredKeys.map((key) => {
539
+ ))), /* @__PURE__ */ React2.createElement(TableContainer, null, /* @__PURE__ */ React2.createElement(Table, null, /* @__PURE__ */ React2.createElement(TableHead, null, /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, null, "Alias"), /* @__PURE__ */ React2.createElement(TableCell, null, "Key ID"), /* @__PURE__ */ React2.createElement(TableCell, null, "Created"), /* @__PURE__ */ React2.createElement(TableCell, null, "Expires"), /* @__PURE__ */ React2.createElement(TableCell, null, "Budget"), /* @__PURE__ */ React2.createElement(TableCell, null, "TPM / RPM"), /* @__PURE__ */ React2.createElement(TableCell, null, "Models"), /* @__PURE__ */ React2.createElement(TableCell, { align: "right" }, "Actions"))), /* @__PURE__ */ React2.createElement(TableBody, null, loading ? /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ React2.createElement(CircularProgress, { size: 24 }))) : filteredKeys.length === 0 ? /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, { colSpan: 8, align: "center", sx: { py: 4 } }, filterText ? /* @__PURE__ */ React2.createElement(Typography2, { color: "text.secondary" }, "No keys match filter") : /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Typography2, { color: "text.secondary", gutterBottom: true }, "No keys yet \u2014 generate your first key to start calling models."), /* @__PURE__ */ React2.createElement(
540
+ Button,
541
+ {
542
+ variant: "contained",
543
+ color: "primary",
544
+ size: "small",
545
+ startIcon: /* @__PURE__ */ React2.createElement(Add, null),
546
+ onClick: () => setGenerateModalOpen(true)
547
+ },
548
+ "Generate Your First Key"
549
+ )))) : filteredKeys.map((key) => {
444
550
  const keyId = key.token ?? key.key;
445
551
  const isRotating = rotatingKeyId === keyId;
446
552
  const isBlocking = blockingKeyId === keyId;
@@ -511,12 +617,14 @@ var init_KeysTable = __esm({
511
617
  newKeyValue
512
618
  ),
513
619
  /* @__PURE__ */ React2.createElement(IconButton, { onClick: () => copyToClipboard(newKeyValue) }, /* @__PURE__ */ React2.createElement(ContentCopy, null))
514
- )) : /* @__PURE__ */ React2.createElement(Box2, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, /* @__PURE__ */ React2.createElement(
620
+ ), newKeySnippets && /* @__PURE__ */ React2.createElement(Box2, { mt: 3 }, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React2.createElement(Code, { fontSize: "small", color: "action" }), /* @__PURE__ */ React2.createElement(Typography2, { variant: "subtitle2" }, "Start calling the proxy \u2014 paste and run")), /* @__PURE__ */ React2.createElement(SnippetTabs, { snippets: newKeySnippets, model: newKeyModel, onCopy: copyToClipboard }))) : /* @__PURE__ */ React2.createElement(Box2, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, /* @__PURE__ */ React2.createElement(
515
621
  TextField,
516
622
  {
517
623
  label: "Alias",
518
624
  value: formData.alias || "",
519
625
  onChange: (e) => setFormData({ ...formData, alias: e.target.value }),
626
+ error: aliasError,
627
+ helperText: aliasError ? "Alias is required" : void 0,
520
628
  required: true,
521
629
  fullWidth: true
522
630
  }
@@ -563,13 +671,35 @@ var init_KeysTable = __esm({
563
671
  renderOption: (props, m) => /* @__PURE__ */ React2.createElement("li", { ...props }, modelOption(m)),
564
672
  renderInput: (params) => /* @__PURE__ */ React2.createElement(TextField, { ...params, label: "Models", helperText: "Leave empty to allow all models", fullWidth: true })
565
673
  }
674
+ ), /* @__PURE__ */ React2.createElement(
675
+ FormControlLabel,
676
+ {
677
+ control: /* @__PURE__ */ React2.createElement(
678
+ Checkbox,
679
+ {
680
+ checked: unlimitedBudget,
681
+ onChange: (e) => {
682
+ setUnlimitedBudget(e.target.checked);
683
+ if (e.target.checked) {
684
+ setFormData({ ...formData, max_budget: null });
685
+ } else {
686
+ setFormData({ ...formData, max_budget: 100 });
687
+ }
688
+ }
689
+ }
690
+ ),
691
+ label: "Unlimited budget"
692
+ }
566
693
  ), /* @__PURE__ */ React2.createElement(
567
694
  TextField,
568
695
  {
569
696
  label: "Max Budget (USD)",
570
697
  type: "number",
571
- value: formData.max_budget ?? "",
698
+ value: unlimitedBudget ? "" : formData.max_budget ?? "",
572
699
  onChange: (e) => setFormData({ ...formData, max_budget: e.target.value ? Number(e.target.value) : void 0 }),
700
+ error: budgetInvalid,
701
+ helperText: budgetInvalid ? 'Enter a positive budget or tick "Unlimited"' : budgetEstimate !== null ? `\u2248 ${fmtInt(budgetEstimate)} tokens at the selected model's rate` : void 0,
702
+ disabled: unlimitedBudget,
573
703
  required: true,
574
704
  fullWidth: true
575
705
  }
@@ -591,13 +721,42 @@ var init_KeysTable = __esm({
591
721
  onChange: (e) => setFormData({ ...formData, rpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
592
722
  fullWidth: true
593
723
  }
724
+ ), /* @__PURE__ */ React2.createElement(
725
+ FormControlLabel,
726
+ {
727
+ control: /* @__PURE__ */ React2.createElement(
728
+ Checkbox,
729
+ {
730
+ checked: !!formData.auto_rotate,
731
+ onChange: (e) => setFormData({
732
+ ...formData,
733
+ auto_rotate: e.target.checked,
734
+ rotation_interval_days: e.target.checked ? formData.rotation_interval_days ?? 90 : void 0
735
+ })
736
+ }
737
+ ),
738
+ label: "Auto-rotate (LiteLLM rotates the secret on a schedule)"
739
+ }
740
+ ), formData.auto_rotate && /* @__PURE__ */ React2.createElement(
741
+ TextField,
742
+ {
743
+ label: "Rotate every (days)",
744
+ type: "number",
745
+ value: formData.rotation_interval_days ?? "",
746
+ onChange: (e) => setFormData({
747
+ ...formData,
748
+ rotation_interval_days: e.target.value ? Number(e.target.value) : void 0
749
+ }),
750
+ helperText: "LiteLLM enforces this interval server-side; the old secret stops working the moment it rotates.",
751
+ fullWidth: true
752
+ }
594
753
  ))), /* @__PURE__ */ React2.createElement(DialogActions, null, /* @__PURE__ */ React2.createElement(Button, { onClick: handleCloseModal }, newKeyValue ? "Done" : "Cancel"), !newKeyValue && /* @__PURE__ */ React2.createElement(
595
754
  Button,
596
755
  {
597
756
  onClick: handleGenerate,
598
757
  variant: "contained",
599
758
  color: "primary",
600
- disabled: submitting
759
+ disabled: !canGenerate
601
760
  },
602
761
  submitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 24 }) : "Generate"
603
762
  ), newKeyValue && /* @__PURE__ */ React2.createElement(Button, { onClick: handleCloseModal, variant: "contained", color: "success" }, "Done"))), /* @__PURE__ */ React2.createElement(Dialog, { open: !!editingKey, onClose: handleCloseEdit, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ React2.createElement(DialogTitle, null, "Edit Key"), /* @__PURE__ */ React2.createElement(DialogContent, null, editingKey && /* @__PURE__ */ React2.createElement(Box2, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "text.secondary" }, /* @__PURE__ */ React2.createElement("code", { style: { fontFamily: "monospace", color: "inherit" } }, maskKey(editingKey.key))), /* @__PURE__ */ React2.createElement(
@@ -705,16 +864,44 @@ var init_KeysTable = __esm({
705
864
  pruneSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 20 }) : "Prune"
706
865
  ))));
707
866
  };
708
- }
709
- });
710
-
711
- // src/format.ts
712
- var fmtUsd, fmtInt;
713
- var init_format = __esm({
714
- "src/format.ts"() {
715
- "use strict";
716
- fmtUsd = (n) => `$${(n ?? 0).toFixed(n < 1 ? 4 : 2)}`;
717
- fmtInt = (n) => (n ?? 0).toLocaleString();
867
+ SnippetTabs = ({ snippets, model, onCopy }) => {
868
+ const [tab, setTab] = useState("curl");
869
+ const code = tab === "curl" ? snippets.curl : snippets.openai;
870
+ return /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Tabs, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 1 } }, /* @__PURE__ */ React2.createElement(Tab, { label: "curl", value: "curl" }), /* @__PURE__ */ React2.createElement(Tab, { label: "OpenAI SDK", value: "openai" })), /* @__PURE__ */ React2.createElement(
871
+ Box2,
872
+ {
873
+ position: "relative",
874
+ p: 1.5,
875
+ sx: { backgroundColor: "action.hover", border: "1px solid", borderColor: "divider", borderRadius: 1 }
876
+ },
877
+ /* @__PURE__ */ React2.createElement(
878
+ IconButton,
879
+ {
880
+ size: "small",
881
+ onClick: () => onCopy(code),
882
+ title: "Copy snippet",
883
+ sx: { position: "absolute", top: 4, right: 4 }
884
+ },
885
+ /* @__PURE__ */ React2.createElement(ContentCopy, { fontSize: "small" })
886
+ ),
887
+ /* @__PURE__ */ React2.createElement(
888
+ Typography2,
889
+ {
890
+ component: "pre",
891
+ sx: {
892
+ fontFamily: "monospace",
893
+ fontSize: 12,
894
+ whiteSpace: "pre-wrap",
895
+ wordBreak: "break-all",
896
+ mb: 0,
897
+ pr: 4
898
+ }
899
+ },
900
+ code
901
+ ),
902
+ model && /* @__PURE__ */ React2.createElement(Typography2, { variant: "caption", color: "text.secondary", display: "block", mt: 1 }, "Using model \u201C", model, "\u201D \u2014 swap it for any model you have access to.")
903
+ ));
904
+ };
718
905
  }
719
906
  });
720
907
 
@@ -729,8 +916,8 @@ import {
729
916
  Select,
730
917
  MenuItem as MenuItem2,
731
918
  Grid,
732
- Tabs,
733
- Tab,
919
+ Tabs as Tabs2,
920
+ Tab as Tab2,
734
921
  Table as Table2,
735
922
  TableBody as TableBody2,
736
923
  TableCell as TableCell2,
@@ -739,7 +926,8 @@ import {
739
926
  TableRow as TableRow2,
740
927
  Chip as Chip3,
741
928
  LinearProgress as LinearProgress3,
742
- Skeleton
929
+ Skeleton,
930
+ useTheme
743
931
  } from "@mui/material";
744
932
  import {
745
933
  AreaChart,
@@ -795,8 +983,14 @@ var init_UsageStats = __esm({
795
983
  loading,
796
984
  userInfo
797
985
  }) => {
986
+ const theme = useTheme();
798
987
  const [selectedModel, setSelectedModel] = useState2("all");
799
988
  const [tab, setTab] = useState2("costs");
989
+ const gridStroke = theme.palette.divider;
990
+ const tickFill = theme.palette.text.secondary;
991
+ const tickStyle = { fontSize: 12, fill: tickFill };
992
+ const tickStyleSmall = { fontSize: 11, fill: tickFill };
993
+ const tickStyleTiny = { fontSize: 10, fill: tickFill };
800
994
  const selectedPreset = useMemo2(() => {
801
995
  if (dateRange.start.toDateString() === dateRange.end.toDateString()) return "today";
802
996
  const diffMs = dateRange.end.getTime() - dateRange.start.getTime();
@@ -931,7 +1125,7 @@ var init_UsageStats = __esm({
931
1125
  value: fmtInt(usage?.total_tokens ?? 0),
932
1126
  hint: `${fmtInt(usage?.prompt_tokens ?? 0)} in \xB7 ${fmtInt(usage?.completion_tokens ?? 0)} out`
933
1127
  }
934
- ))), /* @__PURE__ */ React3.createElement(Tabs, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 2 } }, /* @__PURE__ */ React3.createElement(Tab, { value: "costs", label: "Costs" }), /* @__PURE__ */ React3.createElement(Tab, { value: "models", label: "Model Activity" }), /* @__PURE__ */ React3.createElement(Tab, { value: "keys", label: "Key Activity" })), tab === "costs" && /* @__PURE__ */ React3.createElement(Grid, { container: true, spacing: 3 }, /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend by Model"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : modelSpendByDate.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: 260 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(AreaChart, { data: modelSpendByDate }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "date", tick: { fontSize: 12 } }), /* @__PURE__ */ React3.createElement(YAxis, { tick: { fontSize: 12 }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [`$${v.toFixed(4)}`, void 0] }), /* @__PURE__ */ React3.createElement(Legend, null), topSpendModels.map((m) => /* @__PURE__ */ React3.createElement(
1128
+ ))), /* @__PURE__ */ React3.createElement(Tabs2, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 2 } }, /* @__PURE__ */ React3.createElement(Tab2, { value: "costs", label: "Costs" }), /* @__PURE__ */ React3.createElement(Tab2, { value: "models", label: "Model Activity" }), /* @__PURE__ */ React3.createElement(Tab2, { value: "keys", label: "Key Activity" })), tab === "costs" && /* @__PURE__ */ React3.createElement(Grid, { container: true, spacing: 3 }, /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend by Model"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : modelSpendByDate.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: 260 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(AreaChart, { data: modelSpendByDate }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ React3.createElement(YAxis, { tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [`$${v.toFixed(4)}`, void 0] }), /* @__PURE__ */ React3.createElement(Legend, null), topSpendModels.map((m) => /* @__PURE__ */ React3.createElement(
935
1129
  Area,
936
1130
  {
937
1131
  key: m,
@@ -942,14 +1136,14 @@ var init_UsageStats = __esm({
942
1136
  fill: modelColor(m),
943
1137
  fillOpacity: 0.6
944
1138
  }
945
- )))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Token Usage"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : dailyData.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: 260 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(AreaChart, { data: dailyData }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "date", tick: { fontSize: 12 } }), /* @__PURE__ */ React3.createElement(YAxis, { tick: { fontSize: 12 }, tickFormatter: fmtInt }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [fmtInt(v), void 0] }), /* @__PURE__ */ React3.createElement(Legend, null), /* @__PURE__ */ React3.createElement(Area, { type: "monotone", dataKey: "promptTokens", name: "Input (prompt)", stackId: "tok", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.5 }), /* @__PURE__ */ React3.createElement(Area, { type: "monotone", dataKey: "completionTokens", name: "Output (completion)", stackId: "tok", stroke: "#82ca9d", fill: "#82ca9d", fillOpacity: 0.5 }))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Requests"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : dailyData.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: 260 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(BarChart, { data: dailyData }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "date", tick: { fontSize: 12 } }), /* @__PURE__ */ React3.createElement(YAxis, { tick: { fontSize: 12 } }), /* @__PURE__ */ React3.createElement(Tooltip, null), /* @__PURE__ */ React3.createElement(Legend, null), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "successfulRequests", name: "Successful", fill: "#82ca9d", stackId: "r" }), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "failedRequests", name: "Failed", fill: "#e57373", stackId: "r" }))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Success Rate"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : successRateData.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: 260 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(LineChart, { data: successRateData }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "date", tick: { fontSize: 12 } }), /* @__PURE__ */ React3.createElement(YAxis, { domain: [0, 100], tick: { fontSize: 12 }, tickFormatter: (v) => `${v}%` }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [`${v}%`, "Success rate"] }), /* @__PURE__ */ React3.createElement(ReferenceLine, { y: 100, stroke: "#82ca9d", strokeDasharray: "4 2" }), /* @__PURE__ */ React3.createElement(Line, { type: "monotone", dataKey: "successRate", name: "Success rate", stroke: "#8884d8", dot: { r: 3 } }))))), (maxBudget > 0 || cumulativeData.length > 0) && /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Cumulative Spend", maxBudget > 0 ? ` vs Budget (${fmtUsd(maxBudget)})` : "", maxBudget > 0 && /* @__PURE__ */ React3.createElement(Typography3, { component: "span", variant: "caption", color: "text.secondary", sx: { ml: 1 } }, fmtUsd(totalCumSpend), " used \xB7 ", fmtUsd(Math.max(0, maxBudget - totalCumSpend)), " remaining")), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, { height: 180 }) : cumulativeData.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, { height: 180 }) : /* @__PURE__ */ React3.createElement(Box3, { height: 180 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(AreaChart, { data: cumulativeData }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "date", tick: { fontSize: 12 } }), /* @__PURE__ */ React3.createElement(YAxis, { tick: { fontSize: 12 }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [`$${v.toFixed(4)}`, "Cumulative spend"] }), maxBudget > 0 && /* @__PURE__ */ React3.createElement(ReferenceLine, { y: maxBudget, stroke: "#e57373", strokeDasharray: "6 3", label: { value: `Budget ${fmtUsd(maxBudget)}`, position: "insideTopRight", fontSize: 11 } }), /* @__PURE__ */ React3.createElement(Area, { type: "monotone", dataKey: "cumulative", name: "Cumulative spend", stroke: "#ffc658", fill: "#ffc658", fillOpacity: 0.3 })))))), tab === "models" && /* @__PURE__ */ React3.createElement(Grid, { container: true, spacing: 3 }, /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Spend by Model"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : topModelSpendBars.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: Math.max(200, topModelSpendBars.length * 36) }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(BarChart, { data: topModelSpendBars, layout: "vertical", margin: { left: 20 } }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ React3.createElement(XAxis, { type: "number", tick: { fontSize: 12 }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React3.createElement(YAxis, { type: "category", dataKey: "model", tick: { fontSize: 11 }, width: 200 }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [fmtUsd(v), "Spend"] }), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "spend", name: "Spend", radius: [0, 4, 4, 0] }, topModelSpendBars.map((r) => /* @__PURE__ */ React3.createElement("rect", { key: r.model, fill: modelColor(r.model) }))))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Tokens by Model"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : modelRows.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: 260 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(BarChart, { data: modelRows }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "model", tick: { fontSize: 10 }, interval: 0, angle: -15, textAnchor: "end", height: 60 }), /* @__PURE__ */ React3.createElement(YAxis, { tick: { fontSize: 12 } }), /* @__PURE__ */ React3.createElement(Tooltip, null), /* @__PURE__ */ React3.createElement(Legend, null), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "promptTokens", name: "Prompt", fill: "#8884d8", stackId: "t" }), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "completionTokens", name: "Completion", fill: "#82ca9d", stackId: "t" }))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(TableContainer2, { component: Paper3, variant: "outlined" }, /* @__PURE__ */ React3.createElement(Table2, { size: "small" }, /* @__PURE__ */ React3.createElement(TableHead2, null, /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, null, "Model"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Spend"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Requests"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Success"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Failed"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Prompt"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Completion"), /* @__PURE__ */ React3.createElement(TableCell2, { sx: { minWidth: 120 } }, "Success rate"))), /* @__PURE__ */ React3.createElement(TableBody2, null, loading ? /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, { colSpan: 8 }, /* @__PURE__ */ React3.createElement(LinearProgress3, null))) : modelRows.length === 0 ? /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, { colSpan: 8, align: "center" }, "No model activity")) : [...modelRows].sort((a, b) => b.spend - a.spend || b.totalTokens - a.totalTokens).map((r) => /* @__PURE__ */ React3.createElement(TableRow2, { key: r.model }, /* @__PURE__ */ React3.createElement(TableCell2, null, /* @__PURE__ */ React3.createElement(Box3, { display: "flex", alignItems: "center", gap: 0.75 }, /* @__PURE__ */ React3.createElement(Box3, { sx: { width: 10, height: 10, borderRadius: "50%", bgcolor: modelColor(r.model), flexShrink: 0 } }), r.model)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtUsd(r.spend)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.apiRequests)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.successfulRequests)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.failedRequests)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.promptTokens)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.completionTokens)), /* @__PURE__ */ React3.createElement(TableCell2, null, r.apiRequests > 0 ? /* @__PURE__ */ React3.createElement(Box3, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React3.createElement(
1139
+ )))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Token Usage"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : dailyData.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: 260 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(AreaChart, { data: dailyData }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ React3.createElement(YAxis, { tick: tickStyle, tickFormatter: fmtInt }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [fmtInt(v), void 0] }), /* @__PURE__ */ React3.createElement(Legend, null), /* @__PURE__ */ React3.createElement(Area, { type: "monotone", dataKey: "promptTokens", name: "Input (prompt)", stackId: "tok", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.5 }), /* @__PURE__ */ React3.createElement(Area, { type: "monotone", dataKey: "completionTokens", name: "Output (completion)", stackId: "tok", stroke: "#82ca9d", fill: "#82ca9d", fillOpacity: 0.5 }))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Requests"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : dailyData.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: 260 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(BarChart, { data: dailyData }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ React3.createElement(YAxis, { tick: tickStyle }), /* @__PURE__ */ React3.createElement(Tooltip, null), /* @__PURE__ */ React3.createElement(Legend, null), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "successfulRequests", name: "Successful", fill: "#82ca9d", stackId: "r" }), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "failedRequests", name: "Failed", fill: "#e57373", stackId: "r" }))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Success Rate"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : successRateData.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: 260 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(LineChart, { data: successRateData }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ React3.createElement(YAxis, { domain: [0, 100], tick: tickStyle, tickFormatter: (v) => `${v}%` }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [`${v}%`, "Success rate"] }), /* @__PURE__ */ React3.createElement(ReferenceLine, { y: 100, stroke: "#82ca9d", strokeDasharray: "4 2" }), /* @__PURE__ */ React3.createElement(Line, { type: "monotone", dataKey: "successRate", name: "Success rate", stroke: "#8884d8", dot: { r: 3 } }))))), (maxBudget > 0 || cumulativeData.length > 0) && /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Cumulative Spend", maxBudget > 0 ? ` vs Budget (${fmtUsd(maxBudget)})` : "", maxBudget > 0 && /* @__PURE__ */ React3.createElement(Typography3, { component: "span", variant: "caption", color: "text.secondary", sx: { ml: 1 } }, fmtUsd(totalCumSpend), " used \xB7 ", fmtUsd(Math.max(0, maxBudget - totalCumSpend)), " remaining")), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, { height: 180 }) : cumulativeData.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, { height: 180 }) : /* @__PURE__ */ React3.createElement(Box3, { height: 180 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(AreaChart, { data: cumulativeData }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ React3.createElement(YAxis, { tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [`$${v.toFixed(4)}`, "Cumulative spend"] }), maxBudget > 0 && /* @__PURE__ */ React3.createElement(ReferenceLine, { y: maxBudget, stroke: "#e57373", strokeDasharray: "6 3", label: { value: `Budget ${fmtUsd(maxBudget)}`, position: "insideTopRight", fontSize: 11 } }), /* @__PURE__ */ React3.createElement(Area, { type: "monotone", dataKey: "cumulative", name: "Cumulative spend", stroke: "#ffc658", fill: "#ffc658", fillOpacity: 0.3 })))))), tab === "models" && /* @__PURE__ */ React3.createElement(Grid, { container: true, spacing: 3 }, /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Spend by Model"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : topModelSpendBars.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: Math.max(200, topModelSpendBars.length * 36) }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(BarChart, { data: topModelSpendBars, layout: "vertical", margin: { left: 20 } }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React3.createElement(XAxis, { type: "number", tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React3.createElement(YAxis, { type: "category", dataKey: "model", tick: tickStyleSmall, width: 200 }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [fmtUsd(v), "Spend"] }), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "spend", name: "Spend", radius: [0, 4, 4, 0] }, topModelSpendBars.map((r) => /* @__PURE__ */ React3.createElement("rect", { key: r.model, fill: modelColor(r.model) }))))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Tokens by Model"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : modelRows.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: 260 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(BarChart, { data: modelRows }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "model", tick: tickStyleTiny, interval: 0, angle: -15, textAnchor: "end", height: 60 }), /* @__PURE__ */ React3.createElement(YAxis, { tick: tickStyle }), /* @__PURE__ */ React3.createElement(Tooltip, null), /* @__PURE__ */ React3.createElement(Legend, null), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "promptTokens", name: "Prompt", fill: "#8884d8", stackId: "t" }), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "completionTokens", name: "Completion", fill: "#82ca9d", stackId: "t" }))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(TableContainer2, { component: Paper3, variant: "outlined" }, /* @__PURE__ */ React3.createElement(Table2, { size: "small" }, /* @__PURE__ */ React3.createElement(TableHead2, null, /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, null, "Model"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Spend"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Requests"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Success"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Failed"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Prompt"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Completion"), /* @__PURE__ */ React3.createElement(TableCell2, { sx: { minWidth: 120 } }, "Success rate"))), /* @__PURE__ */ React3.createElement(TableBody2, null, loading ? /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, { colSpan: 8 }, /* @__PURE__ */ React3.createElement(LinearProgress3, null))) : modelRows.length === 0 ? /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, { colSpan: 8, align: "center" }, "No model activity")) : [...modelRows].sort((a, b) => b.spend - a.spend || b.totalTokens - a.totalTokens).map((r) => /* @__PURE__ */ React3.createElement(TableRow2, { key: r.model }, /* @__PURE__ */ React3.createElement(TableCell2, null, /* @__PURE__ */ React3.createElement(Box3, { display: "flex", alignItems: "center", gap: 0.75 }, /* @__PURE__ */ React3.createElement(Box3, { sx: { width: 10, height: 10, borderRadius: "50%", bgcolor: modelColor(r.model), flexShrink: 0 } }), r.model)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtUsd(r.spend)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.apiRequests)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.successfulRequests)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.failedRequests)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.promptTokens)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.completionTokens)), /* @__PURE__ */ React3.createElement(TableCell2, null, r.apiRequests > 0 ? /* @__PURE__ */ React3.createElement(Box3, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React3.createElement(
946
1140
  LinearProgress3,
947
1141
  {
948
1142
  variant: "determinate",
949
1143
  value: r.successRate * 100,
950
1144
  sx: { flex: 1, height: 6, borderRadius: 3 }
951
1145
  }
952
- ), /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption" }, fmtPct(r.successRate))) : "\u2014")))))))), tab === "keys" && /* @__PURE__ */ React3.createElement(Grid, { container: true, spacing: 3 }, (loading || topKeySpendBars.length > 0) && /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Spend by Key"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : /* @__PURE__ */ React3.createElement(Box3, { height: Math.max(160, topKeySpendBars.length * 36) }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(BarChart, { data: topKeySpendBars, layout: "vertical", margin: { left: 20 } }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ React3.createElement(XAxis, { type: "number", tick: { fontSize: 12 }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React3.createElement(YAxis, { type: "category", dataKey: "keyAlias", tick: { fontSize: 11 }, width: 160 }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [fmtUsd(v), "Spend"] }), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "spend", name: "Spend", fill: "#8884d8", radius: [0, 4, 4, 0] }))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(TableContainer2, { component: Paper3, variant: "outlined" }, /* @__PURE__ */ React3.createElement(Table2, { size: "small" }, /* @__PURE__ */ React3.createElement(TableHead2, null, /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, null, "Key"), /* @__PURE__ */ React3.createElement(TableCell2, null, "Models"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Spend"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Requests"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Tokens"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Failed"), /* @__PURE__ */ React3.createElement(TableCell2, { sx: { minWidth: 120 } }, "Success rate"))), /* @__PURE__ */ React3.createElement(TableBody2, null, loading ? /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, { colSpan: 7 }, /* @__PURE__ */ React3.createElement(LinearProgress3, null))) : keyRows.length === 0 ? /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, { colSpan: 7, align: "center" }, "No key activity")) : [...keyRows].sort((a, b) => b.spend - a.spend || b.apiRequests - a.apiRequests).map((r) => /* @__PURE__ */ React3.createElement(TableRow2, { key: r.keyHash }, /* @__PURE__ */ React3.createElement(TableCell2, null, /* @__PURE__ */ React3.createElement(Typography3, { variant: "body2" }, r.keyAlias), r.teamId ? /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption", color: "text.secondary" }, "team: ", r.teamId) : null), /* @__PURE__ */ React3.createElement(TableCell2, null, /* @__PURE__ */ React3.createElement(Box3, { display: "flex", gap: 0.5, flexWrap: "wrap" }, r.models.map((m) => /* @__PURE__ */ React3.createElement(Chip3, { key: m, label: m, size: "small", variant: "outlined" })))), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtUsd(r.spend)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.apiRequests)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.totalTokens)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.failedRequests)), /* @__PURE__ */ React3.createElement(TableCell2, null, r.apiRequests > 0 ? /* @__PURE__ */ React3.createElement(Box3, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React3.createElement(
1146
+ ), /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption" }, fmtPct(r.successRate))) : "\u2014")))))))), tab === "keys" && /* @__PURE__ */ React3.createElement(Grid, { container: true, spacing: 3 }, (loading || topKeySpendBars.length > 0) && /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Spend by Key"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : /* @__PURE__ */ React3.createElement(Box3, { height: Math.max(160, topKeySpendBars.length * 36) }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(BarChart, { data: topKeySpendBars, layout: "vertical", margin: { left: 20 } }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React3.createElement(XAxis, { type: "number", tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React3.createElement(YAxis, { type: "category", dataKey: "keyAlias", tick: tickStyleSmall, width: 160 }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [fmtUsd(v), "Spend"] }), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "spend", name: "Spend", fill: "#8884d8", radius: [0, 4, 4, 0] }))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(TableContainer2, { component: Paper3, variant: "outlined" }, /* @__PURE__ */ React3.createElement(Table2, { size: "small" }, /* @__PURE__ */ React3.createElement(TableHead2, null, /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, null, "Key"), /* @__PURE__ */ React3.createElement(TableCell2, null, "Models"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Spend"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Requests"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Tokens"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Failed"), /* @__PURE__ */ React3.createElement(TableCell2, { sx: { minWidth: 120 } }, "Success rate"))), /* @__PURE__ */ React3.createElement(TableBody2, null, loading ? /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, { colSpan: 7 }, /* @__PURE__ */ React3.createElement(LinearProgress3, null))) : keyRows.length === 0 ? /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, { colSpan: 7, align: "center" }, "No key activity")) : [...keyRows].sort((a, b) => b.spend - a.spend || b.apiRequests - a.apiRequests).map((r) => /* @__PURE__ */ React3.createElement(TableRow2, { key: r.keyHash }, /* @__PURE__ */ React3.createElement(TableCell2, null, /* @__PURE__ */ React3.createElement(Typography3, { variant: "body2" }, r.keyAlias), r.teamId ? /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption", color: "text.secondary" }, "team: ", r.teamId) : null), /* @__PURE__ */ React3.createElement(TableCell2, null, /* @__PURE__ */ React3.createElement(Box3, { display: "flex", gap: 0.5, flexWrap: "wrap" }, r.models.map((m) => /* @__PURE__ */ React3.createElement(Chip3, { key: m, label: m, size: "small", variant: "outlined" })))), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtUsd(r.spend)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.apiRequests)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.totalTokens)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.failedRequests)), /* @__PURE__ */ React3.createElement(TableCell2, null, r.apiRequests > 0 ? /* @__PURE__ */ React3.createElement(Box3, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React3.createElement(
953
1147
  LinearProgress3,
954
1148
  {
955
1149
  variant: "determinate",
@@ -979,7 +1173,8 @@ import {
979
1173
  TableContainer as TableContainer3,
980
1174
  Collapse,
981
1175
  IconButton as IconButton2,
982
- CircularProgress as CircularProgress2
1176
+ CircularProgress as CircularProgress2,
1177
+ useTheme as useTheme2
983
1178
  } from "@mui/material";
984
1179
  import { ExpandMore, ExpandLess, Group, Speed, Memory } from "@mui/icons-material";
985
1180
  import {
@@ -998,6 +1193,9 @@ var init_TeamUsage = __esm({
998
1193
  Stat = ({ label, value }) => /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ React4.createElement(Typography4, { variant: "body2", fontWeight: 600, sx: { fontFamily: "monospace" } }, value));
999
1194
  TeamCard = ({ team, usage, usageLoading }) => {
1000
1195
  const [expanded, setExpanded] = useState3(false);
1196
+ const theme = useTheme2();
1197
+ const gridStroke = theme.palette.divider;
1198
+ const tickFill = theme.palette.text.secondary;
1001
1199
  const budget = team.max_budget ?? 0;
1002
1200
  const spend = team.spend ?? 0;
1003
1201
  const budgetPct = budget > 0 ? Math.min(spend / budget * 100, 100) : 0;
@@ -1048,7 +1246,7 @@ var init_TeamUsage = __esm({
1048
1246
  color: isOver ? "error" : isNear ? "warning" : "primary",
1049
1247
  sx: { height: 6, borderRadius: 1 }
1050
1248
  }
1051
- )), /* @__PURE__ */ React4.createElement(Collapse, { in: expanded }, /* @__PURE__ */ React4.createElement(Box4, { mt: 2 }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React4.createElement(Speed, { fontSize: "small", color: "action" }), /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary" }, "Rate limits")), /* @__PURE__ */ React4.createElement(Stack, { direction: "row", spacing: 3, flexWrap: "wrap", useFlexGap: true, gap: 1.5, mb: 2 }, /* @__PURE__ */ React4.createElement(Stat, { label: "TPM limit", value: team.tpm_limit != null && team.tpm_limit > 0 ? String(team.tpm_limit) : "Unlimited" }), /* @__PURE__ */ React4.createElement(Stat, { label: "RPM limit", value: team.rpm_limit != null && team.rpm_limit > 0 ? String(team.rpm_limit) : "Unlimited" }), /* @__PURE__ */ React4.createElement(Stat, { label: "Max budget", value: budget > 0 ? `$${budget.toFixed(2)}` : "Unlimited" })), /* @__PURE__ */ React4.createElement(Divider, { sx: { mb: 2 } }), /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Models"), team.models?.length ? /* @__PURE__ */ React4.createElement(Box4, { display: "flex", gap: 0.5, flexWrap: "wrap", mb: 2 }, team.models.map((m) => /* @__PURE__ */ React4.createElement(Chip4, { key: m, label: m, size: "small", variant: "outlined" }))) : /* @__PURE__ */ React4.createElement(Typography4, { variant: "body2", color: "text.secondary", mb: 2 }, "All models allowed"), /* @__PURE__ */ React4.createElement(Divider, { sx: { mb: 2 } }), usageLoading ? /* @__PURE__ */ React4.createElement(Box4, { display: "flex", justifyContent: "center", p: 2 }, /* @__PURE__ */ React4.createElement(CircularProgress2, { size: 24 })) : dailyData.length > 0 ? /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend"), /* @__PURE__ */ React4.createElement(Box4, { height: 160, mb: 2 }, /* @__PURE__ */ React4.createElement(ResponsiveContainer2, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(AreaChart2, { data: dailyData }, /* @__PURE__ */ React4.createElement(CartesianGrid2, { strokeDasharray: "3 3" }), /* @__PURE__ */ React4.createElement(XAxis2, { dataKey: "date", tick: { fontSize: 11 } }), /* @__PURE__ */ React4.createElement(YAxis2, { tick: { fontSize: 11 }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React4.createElement(Tooltip2, { formatter: (v) => [`$${v.toFixed(4)}`, "Spend"] }), /* @__PURE__ */ React4.createElement(Area2, { type: "monotone", dataKey: "spend", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.3 })))), /* @__PURE__ */ React4.createElement(Divider, { sx: { mb: 2 } })) : null, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React4.createElement(Memory, { fontSize: "small", color: "action" }), /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary" }, "Members")), team.members_with_roles?.length ? /* @__PURE__ */ React4.createElement(TableContainer3, null, /* @__PURE__ */ React4.createElement(Table3, { size: "small" }, /* @__PURE__ */ React4.createElement(TableHead3, null, /* @__PURE__ */ React4.createElement(TableRow3, null, /* @__PURE__ */ React4.createElement(TableCell3, null, "User"), /* @__PURE__ */ React4.createElement(TableCell3, null, "Role"))), /* @__PURE__ */ React4.createElement(TableBody3, null, team.members_with_roles.map((m) => /* @__PURE__ */ React4.createElement(TableRow3, { key: m.user_id }, /* @__PURE__ */ React4.createElement(TableCell3, { sx: { fontFamily: "monospace", fontSize: 12 } }, m.user_id), /* @__PURE__ */ React4.createElement(TableCell3, null, /* @__PURE__ */ React4.createElement(
1249
+ )), /* @__PURE__ */ React4.createElement(Collapse, { in: expanded }, /* @__PURE__ */ React4.createElement(Box4, { mt: 2 }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React4.createElement(Speed, { fontSize: "small", color: "action" }), /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary" }, "Rate limits")), /* @__PURE__ */ React4.createElement(Stack, { direction: "row", spacing: 3, flexWrap: "wrap", useFlexGap: true, gap: 1.5, mb: 2 }, /* @__PURE__ */ React4.createElement(Stat, { label: "TPM limit", value: team.tpm_limit != null && team.tpm_limit > 0 ? String(team.tpm_limit) : "Unlimited" }), /* @__PURE__ */ React4.createElement(Stat, { label: "RPM limit", value: team.rpm_limit != null && team.rpm_limit > 0 ? String(team.rpm_limit) : "Unlimited" }), /* @__PURE__ */ React4.createElement(Stat, { label: "Max budget", value: budget > 0 ? `$${budget.toFixed(2)}` : "Unlimited" })), /* @__PURE__ */ React4.createElement(Divider, { sx: { mb: 2 } }), /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Models"), team.models?.length ? /* @__PURE__ */ React4.createElement(Box4, { display: "flex", gap: 0.5, flexWrap: "wrap", mb: 2 }, team.models.map((m) => /* @__PURE__ */ React4.createElement(Chip4, { key: m, label: m, size: "small", variant: "outlined" }))) : /* @__PURE__ */ React4.createElement(Typography4, { variant: "body2", color: "text.secondary", mb: 2 }, "All models allowed"), /* @__PURE__ */ React4.createElement(Divider, { sx: { mb: 2 } }), usageLoading ? /* @__PURE__ */ React4.createElement(Box4, { display: "flex", justifyContent: "center", p: 2 }, /* @__PURE__ */ React4.createElement(CircularProgress2, { size: 24 })) : dailyData.length > 0 ? /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend"), /* @__PURE__ */ React4.createElement(Box4, { height: 160, mb: 2 }, /* @__PURE__ */ React4.createElement(ResponsiveContainer2, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(AreaChart2, { data: dailyData }, /* @__PURE__ */ React4.createElement(CartesianGrid2, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React4.createElement(XAxis2, { dataKey: "date", tick: { fontSize: 11, fill: tickFill } }), /* @__PURE__ */ React4.createElement(YAxis2, { tick: { fontSize: 11, fill: tickFill }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React4.createElement(Tooltip2, { formatter: (v) => [`$${v.toFixed(4)}`, "Spend"] }), /* @__PURE__ */ React4.createElement(Area2, { type: "monotone", dataKey: "spend", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.3 })))), /* @__PURE__ */ React4.createElement(Divider, { sx: { mb: 2 } })) : null, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React4.createElement(Memory, { fontSize: "small", color: "action" }), /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary" }, "Members")), team.members_with_roles?.length ? /* @__PURE__ */ React4.createElement(TableContainer3, null, /* @__PURE__ */ React4.createElement(Table3, { size: "small" }, /* @__PURE__ */ React4.createElement(TableHead3, null, /* @__PURE__ */ React4.createElement(TableRow3, null, /* @__PURE__ */ React4.createElement(TableCell3, null, "User"), /* @__PURE__ */ React4.createElement(TableCell3, null, "Role"))), /* @__PURE__ */ React4.createElement(TableBody3, null, team.members_with_roles.map((m) => /* @__PURE__ */ React4.createElement(TableRow3, { key: m.user_id }, /* @__PURE__ */ React4.createElement(TableCell3, { sx: { fontFamily: "monospace", fontSize: 12 } }, m.user_id), /* @__PURE__ */ React4.createElement(TableCell3, null, /* @__PURE__ */ React4.createElement(
1052
1250
  Chip4,
1053
1251
  {
1054
1252
  label: m.role,
@@ -1067,7 +1265,7 @@ var init_TeamUsage = __esm({
1067
1265
  return /* @__PURE__ */ React4.createElement(Paper4, { sx: { p: 2 } }, /* @__PURE__ */ React4.createElement(LinearProgress4, null));
1068
1266
  }
1069
1267
  if (!teams.length) {
1070
- return /* @__PURE__ */ React4.createElement(Paper4, { sx: { p: 2 } }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React4.createElement(Group, { color: "disabled" }), /* @__PURE__ */ React4.createElement(Typography4, { color: "text.secondary", variant: "body2" }, "No team membership found in LiteLLM for this account.")));
1268
+ return /* @__PURE__ */ React4.createElement(Paper4, { sx: { p: 2 } }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "flex-start", gap: 1.5 }, /* @__PURE__ */ React4.createElement(Group, { color: "disabled", sx: { mt: 0.5 } }), /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Typography4, { color: "text.secondary", variant: "body2" }, "You're not a member of any LiteLLM team yet."), /* @__PURE__ */ React4.createElement(Typography4, { color: "text.secondary", variant: "body2", mt: 0.5 }, "That's fine \u2014 your account is provisioned, so you can still generate personal keys and use models. If you need a shared budget with colleagues, ask an admin to add you to a team."))));
1071
1269
  }
1072
1270
  return /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Typography4, { variant: "h6", mb: 1 }, "Teams"), teams.map((team) => /* @__PURE__ */ React4.createElement(
1073
1271
  TeamCard,
@@ -1221,7 +1419,7 @@ __export(LiteLLMPage_exports, {
1221
1419
  LiteLLMPage: () => LiteLLMPage
1222
1420
  });
1223
1421
  import React6, { useState as useState5, useCallback as useCallback2, useMemo as useMemo3 } from "react";
1224
- import { Box as Box6, Snackbar, Alert as Alert2, CircularProgress as CircularProgress4, Typography as Typography6, Paper as Paper6, Tabs as Tabs2, Tab as Tab2 } from "@mui/material";
1422
+ import { Box as Box6, Snackbar, Alert as Alert2, CircularProgress as CircularProgress4, Typography as Typography6, Paper as Paper6, Tabs as Tabs3, Tab as Tab3 } from "@mui/material";
1225
1423
  import { useAsync as useAsync2, useAsyncRetry } from "react-use";
1226
1424
  import { useApi } from "@backstage/core-plugin-api";
1227
1425
  function initDateRange() {
@@ -1435,16 +1633,16 @@ var init_LiteLLMPage = __esm({
1435
1633
  return /* @__PURE__ */ React6.createElement(Box6, { p: 3 }, /* @__PURE__ */ React6.createElement(Paper6, { sx: { p: 3 } }, /* @__PURE__ */ React6.createElement(Typography6, { variant: "h6", gutterBottom: true }, "Account not provisioned"), /* @__PURE__ */ React6.createElement(Typography6, { color: "text.secondary", paragraph: true }, "Your Backstage account is not linked to a LiteLLM user."), hint ? /* @__PURE__ */ React6.createElement(Typography6, { variant: "body2", color: "text.secondary" }, hint) : /* @__PURE__ */ React6.createElement(Typography6, { variant: "body2", color: "text.secondary" }, isProvisioningEnabled ? "Auto-provisioning is enabled but failed. Check the backend logs." : "Set litellm.provisioning.enabled: true in app-config.yaml to enable auto-provisioning, or ask your administrator to create the account manually.")));
1436
1634
  }
1437
1635
  return /* @__PURE__ */ React6.createElement(Box6, { p: 3 }, /* @__PURE__ */ React6.createElement(Box6, { mb: 2 }, /* @__PURE__ */ React6.createElement(DashboardHeader, { userInfo, teams: teams ?? [], loading: userLoading || teamsLoading })), /* @__PURE__ */ React6.createElement(
1438
- Tabs2,
1636
+ Tabs3,
1439
1637
  {
1440
1638
  value: activeTab,
1441
1639
  onChange: (_, v) => setActiveTab(v),
1442
1640
  sx: { mb: 2, borderBottom: 1, borderColor: "divider" }
1443
1641
  },
1444
- /* @__PURE__ */ React6.createElement(Tab2, { label: "Overview", value: "overview" }),
1445
- /* @__PURE__ */ React6.createElement(Tab2, { label: "Keys", value: "keys" }),
1446
- /* @__PURE__ */ React6.createElement(Tab2, { label: "Teams", value: "teams" }),
1447
- userInfo.can_view_audit && /* @__PURE__ */ React6.createElement(Tab2, { label: "Audit Log", value: "audit" })
1642
+ /* @__PURE__ */ React6.createElement(Tab3, { label: "Overview", value: "overview" }),
1643
+ /* @__PURE__ */ React6.createElement(Tab3, { label: "Keys", value: "keys" }),
1644
+ /* @__PURE__ */ React6.createElement(Tab3, { label: "Teams", value: "teams" }),
1645
+ userInfo.can_view_audit && /* @__PURE__ */ React6.createElement(Tab3, { label: "Audit Log", value: "audit" })
1448
1646
  ), activeTab === "overview" && /* @__PURE__ */ React6.createElement(
1449
1647
  UsageStats,
1450
1648
  {
@@ -1469,7 +1667,8 @@ var init_LiteLLMPage = __esm({
1469
1667
  onUnblockKey: handleUnblockKey,
1470
1668
  onResetKeySpend: handleResetKeySpend,
1471
1669
  onDeleteKey: handleDeleteKey,
1472
- onPruneExpiredKeys: handlePruneExpiredKeys
1670
+ onPruneExpiredKeys: handlePruneExpiredKeys,
1671
+ onGetConfig: () => api.getConfig()
1473
1672
  }
1474
1673
  ), activeTab === "teams" && /* @__PURE__ */ React6.createElement(
1475
1674
  TeamUsage,
@@ -1573,32 +1772,42 @@ var LiteLLMHomeWidget = ({
1573
1772
  const api = useApi2(liteLlmApiRef);
1574
1773
  const [period, setPeriod] = useState6(defaultPeriod);
1575
1774
  const [loading, setLoading] = useState6(true);
1576
- const [error, setError] = useState6(null);
1775
+ const [usageError, setUsageError] = useState6(null);
1776
+ const [keysError, setKeysError] = useState6(null);
1577
1777
  const [usage, setUsage] = useState6(null);
1578
1778
  const [keys, setKeys] = useState6([]);
1579
1779
  useEffect(() => {
1580
1780
  let cancelled = false;
1581
1781
  setLoading(true);
1582
- setError(null);
1782
+ setUsageError(null);
1783
+ setKeysError(null);
1583
1784
  const { start, end } = presetToDateRange(period);
1584
1785
  const startDate = start.toISOString().split("T")[0];
1585
1786
  const endDate = end.toISOString().split("T")[0];
1586
- Promise.all([api.getUsage(startDate, endDate), api.listKeys()]).then(([usageData, keysData]) => {
1587
- if (!cancelled) {
1588
- setUsage(usageData);
1589
- setKeys(keysData);
1590
- setLoading(false);
1591
- }
1592
- }).catch((err) => {
1593
- if (!cancelled) {
1594
- setError(err.message ?? "Failed to load usage data");
1787
+ Promise.allSettled([api.getUsage(startDate, endDate), api.listKeys()]).then(
1788
+ ([usageResult, keysResult]) => {
1789
+ if (cancelled) return;
1790
+ if (usageResult.status === "fulfilled") {
1791
+ setUsage(usageResult.value);
1792
+ } else {
1793
+ setUsage(null);
1794
+ setUsageError(usageResult.reason?.message ?? "Failed to load usage data");
1795
+ }
1796
+ if (keysResult.status === "fulfilled") {
1797
+ setKeys(keysResult.value);
1798
+ } else {
1799
+ setKeys([]);
1800
+ setKeysError(keysResult.reason?.message ?? "Failed to load keys");
1801
+ }
1595
1802
  setLoading(false);
1596
1803
  }
1597
- });
1804
+ );
1598
1805
  return () => {
1599
1806
  cancelled = true;
1600
1807
  };
1601
1808
  }, [api, period]);
1809
+ const partialFailure = !loading && (usageError || keysError) && (usage || keys.length > 0);
1810
+ const totalFailure = !loading && usageError && keysError;
1602
1811
  const dailyData = (usage?.daily_usage ?? []).map((d) => ({
1603
1812
  date: d.date,
1604
1813
  spend: d.spend
@@ -1614,7 +1823,7 @@ var LiteLLMHomeWidget = ({
1614
1823
  /* @__PURE__ */ React8.createElement(MenuItem4, { value: "today" }, "Today"),
1615
1824
  /* @__PURE__ */ React8.createElement(MenuItem4, { value: "7d" }, "7d"),
1616
1825
  /* @__PURE__ */ React8.createElement(MenuItem4, { value: "30d" }, "30d")
1617
- ))), loading && /* @__PURE__ */ React8.createElement(Box7, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ React8.createElement(CircularProgress5, { size: 32 })), !loading && error && /* @__PURE__ */ React8.createElement(Alert3, { severity: "error", sx: { mt: 1 } }, error), !loading && !error && /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(Grid2, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "USD Spent", value: fmtUsd(usage?.total_spend ?? 0) })), /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "Tokens In", value: fmtInt(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "Tokens Out", value: fmtInt(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "Keys", value: fmtInt(keys.length) }))), hasSparkline && /* @__PURE__ */ React8.createElement(Box7, { height: 120 }, /* @__PURE__ */ React8.createElement(ResponsiveContainer3, { width: "100%", height: "100%" }, /* @__PURE__ */ React8.createElement(AreaChart3, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 } }, /* @__PURE__ */ React8.createElement(
1826
+ ))), loading && /* @__PURE__ */ React8.createElement(Box7, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ React8.createElement(CircularProgress5, { size: 32 })), !loading && totalFailure && /* @__PURE__ */ React8.createElement(Alert3, { severity: "error", sx: { mt: 1 } }, usageError ?? "Failed to load usage data"), !loading && !totalFailure && /* @__PURE__ */ React8.createElement(React8.Fragment, null, partialFailure && /* @__PURE__ */ React8.createElement(Alert3, { severity: "warning", sx: { mt: 1, mb: 1 } }, usageError ? `Usage data unavailable (${usageError}).` : "", keysError ? ` Key list unavailable (${keysError}).` : "", " Showing what loaded."), /* @__PURE__ */ React8.createElement(Grid2, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "USD Spent", value: fmtUsd(usage?.total_spend ?? 0) })), /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "Tokens In", value: fmtInt(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "Tokens Out", value: fmtInt(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "Keys", value: fmtInt(keys.length) }))), hasSparkline && /* @__PURE__ */ React8.createElement(Box7, { height: 120 }, /* @__PURE__ */ React8.createElement(ResponsiveContainer3, { width: "100%", height: "100%" }, /* @__PURE__ */ React8.createElement(AreaChart3, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 } }, /* @__PURE__ */ React8.createElement(
1618
1827
  Area3,
1619
1828
  {
1620
1829
  type: "monotone",