@acarmisc/backstage-plugin-litellm 0.10.2 → 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.
package/dist/index.esm.js CHANGED
@@ -284,7 +284,7 @@ response = client.chat.completions.create(
284
284
  print(response.choices[0].message.content)`
285
285
  };
286
286
  }
287
- var maskKey, shortKeyId, formatDate, emptyForm, keyToEditForm, KeysTable, SnippetTabs;
287
+ var maskKey, generateDefaultAlias, shortKeyId, formatDate, emptyForm, keyToEditForm, KeysTable, SnippetTabs;
288
288
  var init_KeysTable = __esm({
289
289
  "src/components/KeysTable.tsx"() {
290
290
  "use strict";
@@ -294,6 +294,11 @@ var init_KeysTable = __esm({
294
294
  if (key.length <= 8) return "***";
295
295
  return `${key.slice(0, 4)}...${key.slice(-4)}`;
296
296
  };
297
+ generateDefaultAlias = (username) => {
298
+ const base = (username || "user").split("@")[0].toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "user";
299
+ const hash = Math.random().toString(36).slice(2, 8);
300
+ return `${base}-${hash}`;
301
+ };
297
302
  shortKeyId = (token) => {
298
303
  if (!token) return "-";
299
304
  if (token.length <= 16) return token;
@@ -327,6 +332,8 @@ var init_KeysTable = __esm({
327
332
  keys,
328
333
  models,
329
334
  teams,
335
+ username,
336
+ keyGenerationSettings,
330
337
  loading,
331
338
  onGenerateKey,
332
339
  onUpdateKey,
@@ -360,12 +367,23 @@ var init_KeysTable = __esm({
360
367
  (k) => (k.key_alias ?? "").toLowerCase().includes(q) || k.models?.some((m) => m.toLowerCase().includes(q))
361
368
  );
362
369
  }, [keys, filterText]);
363
- const selectedModels = models.filter((m) => (formData.models || []).includes(m.model_name));
370
+ const allowUnlimitedBudget = keyGenerationSettings?.allowUnlimitedBudget ?? false;
371
+ const teamRequired = keyGenerationSettings?.teamRequired ?? true;
364
372
  const selectedTeam = teams.find((t) => t.team_id === formData.team_id) ?? null;
373
+ const availableModels = useMemo(() => {
374
+ const teamModels = selectedTeam?.models;
375
+ if (teamModels && teamModels.length > 0) {
376
+ const allowed = new Set(teamModels);
377
+ return models.filter((m) => allowed.has(m.model_name));
378
+ }
379
+ return models;
380
+ }, [models, selectedTeam]);
381
+ const selectedModels = availableModels.filter((m) => (formData.models || []).includes(m.model_name));
365
382
  const editSelectedModels = models.filter((m) => (editForm.models || []).includes(m.model_name));
366
383
  const aliasError = !(formData.alias || "").trim();
384
+ const teamError = teamRequired && !formData.team_id;
367
385
  const budgetInvalid = !unlimitedBudget && (formData.max_budget === void 0 || formData.max_budget === null || formData.max_budget <= 0);
368
- const canGenerate = !aliasError && !budgetInvalid && !submitting;
386
+ const canGenerate = !aliasError && !teamError && !budgetInvalid && !submitting;
369
387
  const budgetEstimate = useMemo(() => {
370
388
  if (unlimitedBudget || budgetInvalid) return null;
371
389
  const inputCosts = selectedModels.map((m) => m.input_cost_per_token).filter((c) => typeof c === "number" && c > 0);
@@ -398,6 +416,10 @@ var init_KeysTable = __esm({
398
416
  setSubmitting(false);
399
417
  }
400
418
  };
419
+ const handleOpenGenerate = () => {
420
+ setFormData({ ...emptyForm(), alias: generateDefaultAlias(username) });
421
+ setGenerateModalOpen(true);
422
+ };
401
423
  const handleCloseModal = () => {
402
424
  setGenerateModalOpen(false);
403
425
  setNewKeyValue(null);
@@ -515,7 +537,7 @@ var init_KeysTable = __esm({
515
537
  variant: "contained",
516
538
  color: "primary",
517
539
  startIcon: /* @__PURE__ */ React2.createElement(Add, null),
518
- onClick: () => setGenerateModalOpen(true)
540
+ onClick: handleOpenGenerate
519
541
  },
520
542
  "Generate New Key"
521
543
  ))), /* @__PURE__ */ React2.createElement(TableContainer, null, /* @__PURE__ */ React2.createElement(Table, null, /* @__PURE__ */ React2.createElement(TableHead, null, /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, null, "Alias"), /* @__PURE__ */ React2.createElement(TableCell, null, "Key ID"), /* @__PURE__ */ React2.createElement(TableCell, null, "Created"), /* @__PURE__ */ React2.createElement(TableCell, null, "Expires"), /* @__PURE__ */ React2.createElement(TableCell, null, "Budget"), /* @__PURE__ */ React2.createElement(TableCell, null, "TPM / RPM"), /* @__PURE__ */ React2.createElement(TableCell, null, "Models"), /* @__PURE__ */ React2.createElement(TableCell, { align: "right" }, "Actions"))), /* @__PURE__ */ React2.createElement(TableBody, null, loading ? /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ React2.createElement(CircularProgress, { size: 24 }))) : filteredKeys.length === 0 ? /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, { colSpan: 8, align: "center", sx: { py: 4 } }, filterText ? /* @__PURE__ */ React2.createElement(Typography2, { color: "text.secondary" }, "No keys match filter") : /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Typography2, { color: "text.secondary", gutterBottom: true }, "No keys yet \u2014 generate your first key to start calling models."), /* @__PURE__ */ React2.createElement(
@@ -525,7 +547,7 @@ var init_KeysTable = __esm({
525
547
  color: "primary",
526
548
  size: "small",
527
549
  startIcon: /* @__PURE__ */ React2.createElement(Add, null),
528
- onClick: () => setGenerateModalOpen(true)
550
+ onClick: handleOpenGenerate
529
551
  },
530
552
  "Generate Your First Key"
531
553
  )))) : filteredKeys.map((key) => {
@@ -615,36 +637,50 @@ var init_KeysTable = __esm({
615
637
  /* @__PURE__ */ React2.createElement(MenuItem, { value: "30d" }, "30 Days"),
616
638
  /* @__PURE__ */ React2.createElement(MenuItem, { value: "90d" }, "90 Days"),
617
639
  /* @__PURE__ */ React2.createElement(MenuItem, { value: "1y" }, "1 Year")
618
- ), teams.length > 0 && /* @__PURE__ */ React2.createElement(
640
+ ), teams.length > 0 ? /* @__PURE__ */ React2.createElement(
619
641
  Autocomplete,
620
642
  {
621
643
  options: teams,
622
644
  getOptionLabel: (t) => t.team_alias || t.team_id,
623
645
  value: selectedTeam,
624
- onChange: (_e, team) => setFormData({ ...formData, team_id: team?.team_id }),
646
+ onChange: (_e, team) => {
647
+ const teamModels = team?.models;
648
+ const restrictedModels = teamModels && teamModels.length > 0 ? (formData.models || []).filter((m) => teamModels.includes(m)) : formData.models;
649
+ setFormData({ ...formData, team_id: team?.team_id, models: restrictedModels });
650
+ },
625
651
  renderInput: (params) => /* @__PURE__ */ React2.createElement(
626
652
  TextField,
627
653
  {
628
654
  ...params,
629
655
  label: "Team",
630
- helperText: "Optional: bind this key to a specific team for scoped access",
656
+ error: teamError,
657
+ 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",
658
+ required: teamRequired,
631
659
  fullWidth: true
632
660
  }
633
661
  )
634
662
  }
635
- ), models.length > 0 && /* @__PURE__ */ React2.createElement(
663
+ ) : teamRequired ? /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "error" }, "Team selection is required, but you don't belong to any team yet \u2014 contact your administrator.") : null, availableModels.length > 0 && /* @__PURE__ */ React2.createElement(
636
664
  Autocomplete,
637
665
  {
638
666
  multiple: true,
639
- options: models,
667
+ options: availableModels,
640
668
  groupBy: (m) => m.mode || "other",
641
669
  getOptionLabel: (m) => m.model_name,
642
670
  value: selectedModels,
643
671
  onChange: (_e, selected) => setFormData({ ...formData, models: selected.map((m) => m.model_name) }),
644
672
  renderOption: (props, m) => /* @__PURE__ */ React2.createElement("li", { ...props }, modelOption(m)),
645
- renderInput: (params) => /* @__PURE__ */ React2.createElement(TextField, { ...params, label: "Models", helperText: "Leave empty to allow all models", fullWidth: true })
673
+ renderInput: (params) => /* @__PURE__ */ React2.createElement(
674
+ TextField,
675
+ {
676
+ ...params,
677
+ label: "Models",
678
+ helperText: selectedTeam ? "Leave empty to allow all models available to this team" : "Leave empty to allow all models",
679
+ fullWidth: true
680
+ }
681
+ )
646
682
  }
647
- ), /* @__PURE__ */ React2.createElement(
683
+ ), allowUnlimitedBudget && /* @__PURE__ */ React2.createElement(
648
684
  FormControlLabel,
649
685
  {
650
686
  control: /* @__PURE__ */ React2.createElement(
@@ -683,6 +719,7 @@ var init_KeysTable = __esm({
683
719
  type: "number",
684
720
  value: formData.tpm_limit ?? "",
685
721
  onChange: (e) => setFormData({ ...formData, tpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
722
+ helperText: "Max tokens per minute this key can consume across all models. Leave blank for no limit.",
686
723
  fullWidth: true
687
724
  }
688
725
  ), /* @__PURE__ */ React2.createElement(
@@ -692,6 +729,7 @@ var init_KeysTable = __esm({
692
729
  type: "number",
693
730
  value: formData.rpm_limit ?? "",
694
731
  onChange: (e) => setFormData({ ...formData, rpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
732
+ helperText: "Max requests per minute this key can make across all models. Leave blank for no limit.",
695
733
  fullWidth: true
696
734
  }
697
735
  ))), /* @__PURE__ */ React2.createElement(DialogActions, null, newKeyValue ? /* @__PURE__ */ React2.createElement(Button, { onClick: handleCloseModal, variant: "contained", color: "success" }, "Done") : /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement(Button, { onClick: handleCloseModal }, "Cancel"), /* @__PURE__ */ React2.createElement(
@@ -739,6 +777,7 @@ var init_KeysTable = __esm({
739
777
  type: "number",
740
778
  value: editForm.tpm_limit ?? "",
741
779
  onChange: (e) => setEditForm({ ...editForm, tpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
780
+ helperText: "Max tokens per minute this key can consume across all models. Leave blank for no limit.",
742
781
  fullWidth: true
743
782
  }
744
783
  ), /* @__PURE__ */ React2.createElement(
@@ -748,6 +787,7 @@ var init_KeysTable = __esm({
748
787
  type: "number",
749
788
  value: editForm.rpm_limit ?? "",
750
789
  onChange: (e) => setEditForm({ ...editForm, rpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
790
+ helperText: "Max requests per minute this key can make across all models. Leave blank for no limit.",
751
791
  fullWidth: true
752
792
  }
753
793
  ), /* @__PURE__ */ React2.createElement(Box2, { mt: 1, pt: 2, borderTop: "1px solid", sx: { borderColor: "divider" } }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "caption", color: "text.secondary", display: "block", mb: 1 }, "Danger Zone"), !resetSpendConfirm ? /* @__PURE__ */ React2.createElement(
@@ -1410,6 +1450,7 @@ var init_LiteLLMPage = __esm({
1410
1450
  () => api.getTeams().catch(() => []),
1411
1451
  [api]
1412
1452
  );
1453
+ const { value: liteLlmConfig } = useAsync2(() => api.getConfig(), [api]);
1413
1454
  const teams = useMemo3(() => {
1414
1455
  if (!allTeams?.length) return [];
1415
1456
  if (!userInfo) return allTeams;
@@ -1588,6 +1629,8 @@ var init_LiteLLMPage = __esm({
1588
1629
  keys: keys ?? [],
1589
1630
  models: allowedModels,
1590
1631
  teams: teams ?? [],
1632
+ username: userInfo.user_id,
1633
+ keyGenerationSettings: liteLlmConfig?.keyGeneration,
1591
1634
  loading: keysLoading || modelsLoading,
1592
1635
  onGenerateKey: handleGenerateKey,
1593
1636
  onUpdateKey: handleUpdateKey,