@acarmisc/backstage-plugin-litellm 0.10.1 → 0.11.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.
@@ -4,6 +4,13 @@ interface KeysTableProps {
4
4
  keys: VirtualKey[];
5
5
  models: ModelInfo[];
6
6
  teams: TeamInfo[];
7
+ /** Current user's LiteLLM user id, used to pre-fill a default key alias. */
8
+ username?: string;
9
+ /** Controls for the "Generate New Key" form; see litellm.keyGeneration in app-config.yaml. */
10
+ keyGenerationSettings?: {
11
+ allowUnlimitedBudget: boolean;
12
+ teamRequired: boolean;
13
+ };
7
14
  loading: boolean;
8
15
  onGenerateKey: (request: GenerateKeyRequest) => Promise<GenerateKeyResponse>;
9
16
  onUpdateKey: (keyId: string, request: UpdateKeyRequest) => Promise<void>;
package/dist/index.cjs.js CHANGED
@@ -72,7 +72,8 @@ var init_api = __esm({
72
72
  } catch {
73
73
  body = await response.text().catch(() => "");
74
74
  }
75
- throw new ApiError(`${response.status} ${response.statusText}`, response.status, body);
75
+ const message = (typeof body === "object" && body && typeof body.error === "string" ? body.error : void 0) ?? `${response.status} ${response.statusText}`;
76
+ throw new ApiError(message, response.status, body);
76
77
  }
77
78
  }
78
79
  async get(path, params) {
@@ -275,7 +276,7 @@ response = client.chat.completions.create(
275
276
  print(response.choices[0].message.content)`
276
277
  };
277
278
  }
278
- var import_react2, import_material2, import_icons_material, maskKey, shortKeyId, formatDate, emptyForm, keyToEditForm, KeysTable, SnippetTabs;
279
+ var import_react2, import_material2, import_icons_material, maskKey, generateDefaultAlias, shortKeyId, formatDate, emptyForm, keyToEditForm, KeysTable, SnippetTabs;
279
280
  var init_KeysTable = __esm({
280
281
  "src/components/KeysTable.tsx"() {
281
282
  "use strict";
@@ -288,6 +289,11 @@ var init_KeysTable = __esm({
288
289
  if (key.length <= 8) return "***";
289
290
  return `${key.slice(0, 4)}...${key.slice(-4)}`;
290
291
  };
292
+ generateDefaultAlias = (username) => {
293
+ const base = (username || "user").split("@")[0].toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "user";
294
+ const hash = Math.random().toString(36).slice(2, 8);
295
+ return `${base}-${hash}`;
296
+ };
291
297
  shortKeyId = (token) => {
292
298
  if (!token) return "-";
293
299
  if (token.length <= 16) return token;
@@ -321,6 +327,8 @@ var init_KeysTable = __esm({
321
327
  keys,
322
328
  models,
323
329
  teams,
330
+ username,
331
+ keyGenerationSettings,
324
332
  loading,
325
333
  onGenerateKey,
326
334
  onUpdateKey,
@@ -354,12 +362,23 @@ var init_KeysTable = __esm({
354
362
  (k) => (k.key_alias ?? "").toLowerCase().includes(q) || k.models?.some((m) => m.toLowerCase().includes(q))
355
363
  );
356
364
  }, [keys, filterText]);
357
- const selectedModels = models.filter((m) => (formData.models || []).includes(m.model_name));
365
+ const allowUnlimitedBudget = keyGenerationSettings?.allowUnlimitedBudget ?? false;
366
+ const teamRequired = keyGenerationSettings?.teamRequired ?? true;
358
367
  const selectedTeam = teams.find((t) => t.team_id === formData.team_id) ?? null;
368
+ const availableModels = (0, import_react2.useMemo)(() => {
369
+ const teamModels = selectedTeam?.models;
370
+ if (teamModels && teamModels.length > 0) {
371
+ const allowed = new Set(teamModels);
372
+ return models.filter((m) => allowed.has(m.model_name));
373
+ }
374
+ return models;
375
+ }, [models, selectedTeam]);
376
+ const selectedModels = availableModels.filter((m) => (formData.models || []).includes(m.model_name));
359
377
  const editSelectedModels = models.filter((m) => (editForm.models || []).includes(m.model_name));
360
378
  const aliasError = !(formData.alias || "").trim();
379
+ const teamError = teamRequired && !formData.team_id;
361
380
  const budgetInvalid = !unlimitedBudget && (formData.max_budget === void 0 || formData.max_budget === null || formData.max_budget <= 0);
362
- const canGenerate = !aliasError && !budgetInvalid && !submitting;
381
+ const canGenerate = !aliasError && !teamError && !budgetInvalid && !submitting;
363
382
  const budgetEstimate = (0, import_react2.useMemo)(() => {
364
383
  if (unlimitedBudget || budgetInvalid) return null;
365
384
  const inputCosts = selectedModels.map((m) => m.input_cost_per_token).filter((c) => typeof c === "number" && c > 0);
@@ -392,6 +411,10 @@ var init_KeysTable = __esm({
392
411
  setSubmitting(false);
393
412
  }
394
413
  };
414
+ const handleOpenGenerate = () => {
415
+ setFormData({ ...emptyForm(), alias: generateDefaultAlias(username) });
416
+ setGenerateModalOpen(true);
417
+ };
395
418
  const handleCloseModal = () => {
396
419
  setGenerateModalOpen(false);
397
420
  setNewKeyValue(null);
@@ -509,7 +532,7 @@ var init_KeysTable = __esm({
509
532
  variant: "contained",
510
533
  color: "primary",
511
534
  startIcon: /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Add, null),
512
- onClick: () => setGenerateModalOpen(true)
535
+ onClick: handleOpenGenerate
513
536
  },
514
537
  "Generate New Key"
515
538
  ))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableContainer, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Table, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableHead, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Alias"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Key ID"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Created"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Expires"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Budget"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "TPM / RPM"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Models"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { align: "right" }, "Actions"))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableBody, null, loading ? /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 24 }))) : filteredKeys.length === 0 ? /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { colSpan: 8, align: "center", sx: { py: 4 } }, filterText ? /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { color: "text.secondary" }, "No keys match filter") : /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { color: "text.secondary", gutterBottom: true }, "No keys yet \u2014 generate your first key to start calling models."), /* @__PURE__ */ import_react2.default.createElement(
@@ -519,7 +542,7 @@ var init_KeysTable = __esm({
519
542
  color: "primary",
520
543
  size: "small",
521
544
  startIcon: /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Add, null),
522
- onClick: () => setGenerateModalOpen(true)
545
+ onClick: handleOpenGenerate
523
546
  },
524
547
  "Generate Your First Key"
525
548
  )))) : filteredKeys.map((key) => {
@@ -609,36 +632,50 @@ var init_KeysTable = __esm({
609
632
  /* @__PURE__ */ import_react2.default.createElement(import_material2.MenuItem, { value: "30d" }, "30 Days"),
610
633
  /* @__PURE__ */ import_react2.default.createElement(import_material2.MenuItem, { value: "90d" }, "90 Days"),
611
634
  /* @__PURE__ */ import_react2.default.createElement(import_material2.MenuItem, { value: "1y" }, "1 Year")
612
- ), teams.length > 0 && /* @__PURE__ */ import_react2.default.createElement(
635
+ ), teams.length > 0 ? /* @__PURE__ */ import_react2.default.createElement(
613
636
  import_material2.Autocomplete,
614
637
  {
615
638
  options: teams,
616
639
  getOptionLabel: (t) => t.team_alias || t.team_id,
617
640
  value: selectedTeam,
618
- onChange: (_e, team) => setFormData({ ...formData, team_id: team?.team_id }),
641
+ onChange: (_e, team) => {
642
+ const teamModels = team?.models;
643
+ const restrictedModels = teamModels && teamModels.length > 0 ? (formData.models || []).filter((m) => teamModels.includes(m)) : formData.models;
644
+ setFormData({ ...formData, team_id: team?.team_id, models: restrictedModels });
645
+ },
619
646
  renderInput: (params) => /* @__PURE__ */ import_react2.default.createElement(
620
647
  import_material2.TextField,
621
648
  {
622
649
  ...params,
623
650
  label: "Team",
624
- helperText: "Optional: bind this key to a specific team for scoped access",
651
+ error: teamError,
652
+ 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",
653
+ required: teamRequired,
625
654
  fullWidth: true
626
655
  }
627
656
  )
628
657
  }
629
- ), models.length > 0 && /* @__PURE__ */ import_react2.default.createElement(
658
+ ) : teamRequired ? /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2", color: "error" }, "Team selection is required, but you don't belong to any team yet \u2014 contact your administrator.") : null, availableModels.length > 0 && /* @__PURE__ */ import_react2.default.createElement(
630
659
  import_material2.Autocomplete,
631
660
  {
632
661
  multiple: true,
633
- options: models,
662
+ options: availableModels,
634
663
  groupBy: (m) => m.mode || "other",
635
664
  getOptionLabel: (m) => m.model_name,
636
665
  value: selectedModels,
637
666
  onChange: (_e, selected) => setFormData({ ...formData, models: selected.map((m) => m.model_name) }),
638
667
  renderOption: (props, m) => /* @__PURE__ */ import_react2.default.createElement("li", { ...props }, modelOption(m)),
639
- renderInput: (params) => /* @__PURE__ */ import_react2.default.createElement(import_material2.TextField, { ...params, label: "Models", helperText: "Leave empty to allow all models", fullWidth: true })
668
+ renderInput: (params) => /* @__PURE__ */ import_react2.default.createElement(
669
+ import_material2.TextField,
670
+ {
671
+ ...params,
672
+ label: "Models",
673
+ helperText: selectedTeam ? "Leave empty to allow all models available to this team" : "Leave empty to allow all models",
674
+ fullWidth: true
675
+ }
676
+ )
640
677
  }
641
- ), /* @__PURE__ */ import_react2.default.createElement(
678
+ ), allowUnlimitedBudget && /* @__PURE__ */ import_react2.default.createElement(
642
679
  import_material2.FormControlLabel,
643
680
  {
644
681
  control: /* @__PURE__ */ import_react2.default.createElement(
@@ -677,6 +714,7 @@ var init_KeysTable = __esm({
677
714
  type: "number",
678
715
  value: formData.tpm_limit ?? "",
679
716
  onChange: (e) => setFormData({ ...formData, tpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
717
+ helperText: "Max tokens per minute this key can consume across all models. Leave blank for no limit.",
680
718
  fullWidth: true
681
719
  }
682
720
  ), /* @__PURE__ */ import_react2.default.createElement(
@@ -686,6 +724,7 @@ var init_KeysTable = __esm({
686
724
  type: "number",
687
725
  value: formData.rpm_limit ?? "",
688
726
  onChange: (e) => setFormData({ ...formData, rpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
727
+ helperText: "Max requests per minute this key can make across all models. Leave blank for no limit.",
689
728
  fullWidth: true
690
729
  }
691
730
  ))), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogActions, null, newKeyValue ? /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: handleCloseModal, variant: "contained", color: "success" }, "Done") : /* @__PURE__ */ import_react2.default.createElement(import_react2.default.Fragment, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: handleCloseModal }, "Cancel"), /* @__PURE__ */ import_react2.default.createElement(
@@ -733,6 +772,7 @@ var init_KeysTable = __esm({
733
772
  type: "number",
734
773
  value: editForm.tpm_limit ?? "",
735
774
  onChange: (e) => setEditForm({ ...editForm, tpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
775
+ helperText: "Max tokens per minute this key can consume across all models. Leave blank for no limit.",
736
776
  fullWidth: true
737
777
  }
738
778
  ), /* @__PURE__ */ import_react2.default.createElement(
@@ -742,6 +782,7 @@ var init_KeysTable = __esm({
742
782
  type: "number",
743
783
  value: editForm.rpm_limit ?? "",
744
784
  onChange: (e) => setEditForm({ ...editForm, rpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
785
+ helperText: "Max requests per minute this key can make across all models. Leave blank for no limit.",
745
786
  fullWidth: true
746
787
  }
747
788
  ), /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { mt: 1, pt: 2, borderTop: "1px solid", sx: { borderColor: "divider" } }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "caption", color: "text.secondary", display: "block", mb: 1 }, "Danger Zone"), !resetSpendConfirm ? /* @__PURE__ */ import_react2.default.createElement(
@@ -1104,7 +1145,15 @@ var init_TeamUsage = __esm({
1104
1145
  color: isOver ? "error" : isNear ? "warning" : "primary",
1105
1146
  sx: { height: 6, borderRadius: 1 }
1106
1147
  }
1107
- )), /* @__PURE__ */ import_react4.default.createElement(import_material4.Collapse, { in: expanded }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { mt: 2 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_icons_material2.Speed, { fontSize: "small", color: "action" }), /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary" }, "Rate limits")), /* @__PURE__ */ import_react4.default.createElement(import_material4.Stack, { direction: "row", spacing: 3, flexWrap: "wrap", useFlexGap: true, gap: 1.5, mb: 2 }, /* @__PURE__ */ import_react4.default.createElement(Stat, { label: "TPM limit", value: team.tpm_limit != null && team.tpm_limit > 0 ? String(team.tpm_limit) : "Unlimited" }), /* @__PURE__ */ import_react4.default.createElement(Stat, { label: "RPM limit", value: team.rpm_limit != null && team.rpm_limit > 0 ? String(team.rpm_limit) : "Unlimited" }), /* @__PURE__ */ import_react4.default.createElement(Stat, { label: "Max budget", value: budget > 0 ? `$${budget.toFixed(2)}` : "Unlimited" })), /* @__PURE__ */ import_react4.default.createElement(import_material4.Divider, { sx: { mb: 2 } }), /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Models"), team.models?.length ? /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", gap: 0.5, flexWrap: "wrap", mb: 2 }, team.models.map((m) => /* @__PURE__ */ import_react4.default.createElement(import_material4.Chip, { key: m, label: m, size: "small", variant: "outlined" }))) : /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "body2", color: "text.secondary", mb: 2 }, "All models allowed"), /* @__PURE__ */ import_react4.default.createElement(import_material4.Divider, { sx: { mb: 2 } }), usageLoading ? /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", justifyContent: "center", p: 2 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.CircularProgress, { size: 24 })) : dailyData.length > 0 ? /* @__PURE__ */ import_react4.default.createElement(import_react4.default.Fragment, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend"), /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { height: 160, mb: 2 }, /* @__PURE__ */ import_react4.default.createElement(import_recharts2.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react4.default.createElement(import_recharts2.AreaChart, { data: dailyData }, /* @__PURE__ */ import_react4.default.createElement(import_recharts2.CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.XAxis, { dataKey: "date", tick: { fontSize: 11, fill: tickFill } }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.YAxis, { tick: { fontSize: 11, fill: tickFill }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.Tooltip, { formatter: (v) => [`$${v.toFixed(4)}`, "Spend"] }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.Area, { type: "monotone", dataKey: "spend", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.3 })))), /* @__PURE__ */ import_react4.default.createElement(import_material4.Divider, { sx: { mb: 2 } })) : null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_icons_material2.Memory, { fontSize: "small", color: "action" }), /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary" }, "Members")), team.members_with_roles?.length ? /* @__PURE__ */ import_react4.default.createElement(import_material4.TableContainer, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Table, { size: "small" }, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableHead, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableRow, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, "User"), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, "Role"))), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableBody, null, team.members_with_roles.map((m) => /* @__PURE__ */ import_react4.default.createElement(import_material4.TableRow, { key: m.user_id }, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, { sx: { fontFamily: "monospace", fontSize: 12 } }, m.user_id), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, /* @__PURE__ */ import_react4.default.createElement(
1148
+ )), /* @__PURE__ */ import_react4.default.createElement(import_material4.Collapse, { in: expanded }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { mt: 2 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_icons_material2.Speed, { fontSize: "small", color: "action" }), /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary" }, "Rate limits")), /* @__PURE__ */ import_react4.default.createElement(import_material4.Stack, { direction: "row", spacing: 3, flexWrap: "wrap", useFlexGap: true, gap: 1.5, mb: 2 }, /* @__PURE__ */ import_react4.default.createElement(Stat, { label: "TPM limit", value: team.tpm_limit != null && team.tpm_limit > 0 ? String(team.tpm_limit) : "Unlimited" }), /* @__PURE__ */ import_react4.default.createElement(Stat, { label: "RPM limit", value: team.rpm_limit != null && team.rpm_limit > 0 ? String(team.rpm_limit) : "Unlimited" }), /* @__PURE__ */ import_react4.default.createElement(Stat, { label: "Max budget", value: budget > 0 ? `$${budget.toFixed(2)}` : "Unlimited" })), /* @__PURE__ */ import_react4.default.createElement(import_material4.Divider, { sx: { mb: 2 } }), /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Models"), team.models?.length ? /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", gap: 0.5, flexWrap: "wrap", mb: 2 }, team.models.map((m) => /* @__PURE__ */ import_react4.default.createElement(import_material4.Chip, { key: m, label: m, size: "small", variant: "outlined" }))) : /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "body2", color: "text.secondary", mb: 2 }, "All models allowed"), /* @__PURE__ */ import_react4.default.createElement(import_material4.Divider, { sx: { mb: 2 } }), usageLoading ? /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", justifyContent: "center", p: 2 }, /* @__PURE__ */ import_react4.default.createElement(import_material4.CircularProgress, { size: 24 })) : dailyData.length > 0 ? /* @__PURE__ */ import_react4.default.createElement(import_react4.default.Fragment, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend"), /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { height: 160, mb: 2 }, /* @__PURE__ */ import_react4.default.createElement(import_recharts2.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react4.default.createElement(import_recharts2.AreaChart, { data: dailyData }, /* @__PURE__ */ import_react4.default.createElement(import_recharts2.CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.XAxis, { dataKey: "date", tick: { fontSize: 11, fill: tickFill } }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.YAxis, { tick: { fontSize: 11, fill: tickFill }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.Tooltip, { formatter: (v) => [`$${v.toFixed(4)}`, "Spend"] }), /* @__PURE__ */ import_react4.default.createElement(import_recharts2.Area, { type: "monotone", dataKey: "spend", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.3 })))), /* @__PURE__ */ import_react4.default.createElement(import_material4.Divider, { sx: { mb: 2 } })) : null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_icons_material2.Memory, { fontSize: "small", color: "action" }), /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "subtitle2", color: "text.secondary" }, "Members")), team.members_with_roles?.length ? /* @__PURE__ */ import_react4.default.createElement(import_material4.TableContainer, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Table, { size: "small" }, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableHead, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableRow, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, "User"), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, "Role"))), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableBody, null, team.members_with_roles.map((m) => /* @__PURE__ */ import_react4.default.createElement(import_material4.TableRow, { key: m.user_id }, /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, m.user_email ? /* @__PURE__ */ import_react4.default.createElement(import_react4.default.Fragment, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "body2" }, m.user_email), /* @__PURE__ */ import_react4.default.createElement(
1149
+ import_material4.Typography,
1150
+ {
1151
+ variant: "caption",
1152
+ color: "text.secondary",
1153
+ sx: { fontFamily: "monospace" }
1154
+ },
1155
+ m.user_id
1156
+ )) : /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "body2", sx: { fontFamily: "monospace" } }, m.user_id)), /* @__PURE__ */ import_react4.default.createElement(import_material4.TableCell, null, /* @__PURE__ */ import_react4.default.createElement(
1108
1157
  import_material4.Chip,
1109
1158
  {
1110
1159
  label: m.role,
@@ -1317,6 +1366,7 @@ var init_LiteLLMPage = __esm({
1317
1366
  () => api.getTeams().catch(() => []),
1318
1367
  [api]
1319
1368
  );
1369
+ const { value: liteLlmConfig } = (0, import_react_use2.useAsync)(() => api.getConfig(), [api]);
1320
1370
  const teams = (0, import_react6.useMemo)(() => {
1321
1371
  if (!allTeams?.length) return [];
1322
1372
  if (!userInfo) return allTeams;
@@ -1368,10 +1418,15 @@ var init_LiteLLMPage = __esm({
1368
1418
  }, [allModels, userInfo, teams]);
1369
1419
  const handleGenerateKey = (0, import_react6.useCallback)(
1370
1420
  async (request) => {
1371
- const response = await api.generateKey(request);
1372
- setSnackbar({ message: "Key generated successfully", severity: "success" });
1373
- refreshKeys();
1374
- return response;
1421
+ try {
1422
+ const response = await api.generateKey(request);
1423
+ setSnackbar({ message: "Key generated successfully", severity: "success" });
1424
+ refreshKeys();
1425
+ return response;
1426
+ } catch (e) {
1427
+ setSnackbar({ message: `Failed to generate key: ${e.message}`, severity: "error" });
1428
+ throw e;
1429
+ }
1375
1430
  },
1376
1431
  [api, refreshKeys]
1377
1432
  );
@@ -1490,6 +1545,8 @@ var init_LiteLLMPage = __esm({
1490
1545
  keys: keys ?? [],
1491
1546
  models: allowedModels,
1492
1547
  teams: teams ?? [],
1548
+ username: userInfo.user_id,
1549
+ keyGenerationSettings: liteLlmConfig?.keyGeneration,
1493
1550
  loading: keysLoading || modelsLoading,
1494
1551
  onGenerateKey: handleGenerateKey,
1495
1552
  onUpdateKey: handleUpdateKey,