@acarmisc/backstage-plugin-litellm 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs.js CHANGED
@@ -1051,26 +1051,6 @@ function BudgetCell({ spend, maxBudget }) {
1051
1051
  const pct = Math.min(100, spend / maxBudget * 100);
1052
1052
  return /* @__PURE__ */ import_react3.default.createElement(import_Box3.default, { minWidth: 104 }, /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, { variant: "body2", sx: { fontVariantNumeric: "tabular-nums", mb: 0.5 } }, "$", spend.toFixed(2), /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, { component: "span", variant: "caption", color: "text.secondary", sx: { ml: 0.5 } }, "/ $", maxBudget)), /* @__PURE__ */ import_react3.default.createElement(Meter, { value: pct, tone: budgetTone(pct), height: 4 }));
1053
1053
  }
1054
- function fmtCost(perToken) {
1055
- if (!perToken) return null;
1056
- const per1k = perToken * 1e3;
1057
- return per1k < 0.01 ? `$${(perToken * 1e6).toFixed(2)}/M` : `$${per1k.toFixed(3)}/1K`;
1058
- }
1059
- function formatContextWindow(maxInput, maxOutput) {
1060
- if (!maxInput && !maxOutput) return null;
1061
- const fmt = (n) => {
1062
- if (!n) return null;
1063
- if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
1064
- if (n >= 1e3) return `${Math.round(n / 1e3)}K`;
1065
- return String(n);
1066
- };
1067
- const inPart = fmt(maxInput);
1068
- const outPart = fmt(maxOutput);
1069
- if (inPart && outPart) return `ctx ${inPart} in / ${outPart} out`;
1070
- if (inPart) return `ctx ${inPart}`;
1071
- if (outPart) return `ctx ${outPart} out`;
1072
- return null;
1073
- }
1074
1054
  function compareKeys(a, b, sortKey) {
1075
1055
  switch (sortKey) {
1076
1056
  case "alias":
@@ -1089,7 +1069,7 @@ function compareKeys(a, b, sortKey) {
1089
1069
  return 0;
1090
1070
  }
1091
1071
  }
1092
- var import_react3, import_Table, import_TableBody, import_TableCell, import_TableContainer, import_TableHead, import_TableRow, import_TableSortLabel, import_Button2, import_IconButton, import_Box3, import_Typography3, import_Dialog, import_DialogTitle, import_DialogContent, import_DialogActions, import_TextField, import_CircularProgress, import_Autocomplete, import_Skeleton2, import_InputAdornment, import_styles3, import_icons_material2, maskKey, shortKeyId, formatDate, keyToEditForm, KeysTable;
1072
+ var import_react3, import_Table, import_TableBody, import_TableCell, import_TableContainer, import_TableHead, import_TableRow, import_TableSortLabel, import_Button2, import_IconButton, import_Box3, import_Typography3, import_Dialog, import_DialogTitle, import_DialogContent, import_DialogActions, import_TextField, import_CircularProgress, import_Skeleton2, import_InputAdornment, import_styles3, import_icons_material2, shortKeyId, formatDate, KeysTable;
1093
1073
  var init_KeysTable = __esm({
1094
1074
  "src/components/KeysTable.tsx"() {
1095
1075
  "use strict";
@@ -1111,17 +1091,12 @@ var init_KeysTable = __esm({
1111
1091
  import_DialogActions = __toESM(require("@mui/material/DialogActions"));
1112
1092
  import_TextField = __toESM(require("@mui/material/TextField"));
1113
1093
  import_CircularProgress = __toESM(require("@mui/material/CircularProgress"));
1114
- import_Autocomplete = __toESM(require("@mui/material/Autocomplete"));
1115
1094
  import_Skeleton2 = __toESM(require("@mui/material/Skeleton"));
1116
1095
  import_InputAdornment = __toESM(require("@mui/material/InputAdornment"));
1117
1096
  import_styles3 = require("@mui/material/styles");
1118
1097
  import_icons_material2 = require("@mui/icons-material");
1119
1098
  init_api();
1120
1099
  init_ui();
1121
- maskKey = (key) => {
1122
- if (key.length <= 8) return "***";
1123
- return `${key.slice(0, 4)}...${key.slice(-4)}`;
1124
- };
1125
1100
  shortKeyId = (token) => {
1126
1101
  if (!token) return "-";
1127
1102
  if (token.length <= 16) return token;
@@ -1134,33 +1109,19 @@ var init_KeysTable = __esm({
1134
1109
  return dateStr;
1135
1110
  }
1136
1111
  };
1137
- keyToEditForm = (k) => ({
1138
- key_alias: k.key_alias ?? "",
1139
- models: k.models ?? [],
1140
- max_budget: k.max_budget,
1141
- tpm_limit: k.tpm_limit,
1142
- rpm_limit: k.rpm_limit
1143
- });
1144
1112
  KeysTable = ({
1145
1113
  keys,
1146
- models,
1147
1114
  loading,
1148
1115
  onGenerateKeyClick,
1149
- onUpdateKey,
1116
+ onEditKey,
1150
1117
  onBlockKey,
1151
1118
  onUnblockKey,
1152
- onResetKeySpend,
1153
1119
  onDeleteKey,
1154
1120
  onPruneExpiredKeys
1155
1121
  }) => {
1156
- const [editingKey, setEditingKey] = (0, import_react3.useState)(null);
1157
- const [editForm, setEditForm] = (0, import_react3.useState)({});
1158
- const [editSubmitting, setEditSubmitting] = (0, import_react3.useState)(false);
1159
1122
  const [blockingKeyId, setBlockingKeyId] = (0, import_react3.useState)(null);
1160
1123
  const [deleteConfirmId, setDeleteConfirmId] = (0, import_react3.useState)(null);
1161
1124
  const [deleteSubmitting, setDeleteSubmitting] = (0, import_react3.useState)(false);
1162
- const [resetSpendConfirm, setResetSpendConfirm] = (0, import_react3.useState)(false);
1163
- const [resetSpendSubmitting, setResetSpendSubmitting] = (0, import_react3.useState)(false);
1164
1125
  const [filterText, setFilterText] = (0, import_react3.useState)("");
1165
1126
  const [sortKey, setSortKey] = (0, import_react3.useState)("created");
1166
1127
  const [sortDirection, setSortDirection] = (0, import_react3.useState)("desc");
@@ -1185,27 +1146,8 @@ var init_KeysTable = __esm({
1185
1146
  return sortDirection === "asc" ? comparison : -comparison;
1186
1147
  });
1187
1148
  }, [keys, filterText, sortKey, sortDirection]);
1188
- const editSelectedModels = models.filter((m) => (editForm.models || []).includes(m.model_name));
1189
1149
  const handleOpenEdit = (k) => {
1190
- setEditingKey(k);
1191
- setEditForm(keyToEditForm(k));
1192
- };
1193
- const handleCloseEdit = () => {
1194
- setEditingKey(null);
1195
- setEditForm({});
1196
- setResetSpendConfirm(false);
1197
- };
1198
- const handleUpdate = async () => {
1199
- if (!editingKey) return;
1200
- setEditSubmitting(true);
1201
- try {
1202
- await onUpdateKey(editingKey.token ?? editingKey.key, editForm);
1203
- handleCloseEdit();
1204
- } catch (error) {
1205
- console.error("Failed to update key:", error);
1206
- } finally {
1207
- setEditSubmitting(false);
1208
- }
1150
+ onEditKey(k);
1209
1151
  };
1210
1152
  const handleToggleBlock = async (key) => {
1211
1153
  const keyId = key.token ?? key.key;
@@ -1220,17 +1162,6 @@ var init_KeysTable = __esm({
1220
1162
  setBlockingKeyId(null);
1221
1163
  }
1222
1164
  };
1223
- const handleResetSpend = async () => {
1224
- if (!editingKey) return;
1225
- setResetSpendSubmitting(true);
1226
- try {
1227
- await onResetKeySpend(editingKey.token ?? editingKey.key);
1228
- setResetSpendConfirm(false);
1229
- handleCloseEdit();
1230
- } finally {
1231
- setResetSpendSubmitting(false);
1232
- }
1233
- };
1234
1165
  const handleConfirmDelete = async () => {
1235
1166
  if (!deleteConfirmId) return;
1236
1167
  setDeleteSubmitting(true);
@@ -1261,12 +1192,6 @@ var init_KeysTable = __esm({
1261
1192
  setPruneConfirmCount(null);
1262
1193
  }
1263
1194
  };
1264
- const modelOption = (m) => {
1265
- const inCost = fmtCost(m.input_cost_per_token);
1266
- const outCost = fmtCost(m.output_cost_per_token);
1267
- const ctx = formatContextWindow(m.max_input_tokens, m.max_output_tokens);
1268
- return /* @__PURE__ */ import_react3.default.createElement(import_Box3.default, null, /* @__PURE__ */ import_react3.default.createElement("span", null, m.model_name), m.supports_function_calling && " \u{1F527}", m.supports_vision && " \u{1F441}\uFE0F", ctx && /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, ctx), (inCost || outCost) && /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, inCost, " in \xB7 ", outCost, " out"));
1269
- };
1270
1195
  const renderTableBody = () => {
1271
1196
  if (loading) {
1272
1197
  return /* @__PURE__ */ import_react3.default.createElement(import_TableRow.default, null, /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, { colSpan: 8, sx: { py: 2 } }, /* @__PURE__ */ import_react3.default.createElement(import_Skeleton2.default, { variant: "rounded", height: 140 })));
@@ -1438,75 +1363,7 @@ var init_KeysTable = __esm({
1438
1363
  ))
1439
1364
  },
1440
1365
  /* @__PURE__ */ import_react3.default.createElement(import_TableContainer.default, null, /* @__PURE__ */ import_react3.default.createElement(import_Table.default, { size: "small", sx: dataTableSx }, /* @__PURE__ */ import_react3.default.createElement(import_TableHead.default, null, /* @__PURE__ */ import_react3.default.createElement(import_TableRow.default, null, /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, { sortDirection: sortKey === "alias" ? sortDirection : false }, /* @__PURE__ */ import_react3.default.createElement(import_TableSortLabel.default, { active: sortKey === "alias", direction: sortKey === "alias" ? sortDirection : "asc", onClick: () => handleSort("alias") }, "Alias")), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, null, "Key ID"), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, { sortDirection: sortKey === "created" ? sortDirection : false }, /* @__PURE__ */ import_react3.default.createElement(import_TableSortLabel.default, { active: sortKey === "created", direction: sortKey === "created" ? sortDirection : "asc", onClick: () => handleSort("created") }, "Created")), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, { sortDirection: sortKey === "expires" ? sortDirection : false }, /* @__PURE__ */ import_react3.default.createElement(import_TableSortLabel.default, { active: sortKey === "expires", direction: sortKey === "expires" ? sortDirection : "asc", onClick: () => handleSort("expires") }, "Expires")), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, { sortDirection: sortKey === "budget" ? sortDirection : false }, /* @__PURE__ */ import_react3.default.createElement(import_TableSortLabel.default, { active: sortKey === "budget", direction: sortKey === "budget" ? sortDirection : "asc", onClick: () => handleSort("budget") }, "Budget")), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, { sortDirection: sortKey === "limits" ? sortDirection : false }, /* @__PURE__ */ import_react3.default.createElement(import_TableSortLabel.default, { active: sortKey === "limits", direction: sortKey === "limits" ? sortDirection : "asc", onClick: () => handleSort("limits") }, "TPM / RPM")), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, { sortDirection: sortKey === "models" ? sortDirection : false }, /* @__PURE__ */ import_react3.default.createElement(import_TableSortLabel.default, { active: sortKey === "models", direction: sortKey === "models" ? sortDirection : "asc", onClick: () => handleSort("models") }, "Models")), /* @__PURE__ */ import_react3.default.createElement(import_TableCell.default, { align: "right" }, "Actions"))), /* @__PURE__ */ import_react3.default.createElement(import_TableBody.default, null, renderTableBody())))
1441
- ), /* @__PURE__ */ import_react3.default.createElement(import_Dialog.default, { open: !!editingKey, onClose: handleCloseEdit, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ import_react3.default.createElement(import_DialogTitle.default, null, "Edit Key"), /* @__PURE__ */ import_react3.default.createElement(import_DialogContent.default, null, editingKey && /* @__PURE__ */ import_react3.default.createElement(import_Box3.default, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, { variant: "body2", color: "text.secondary" }, /* @__PURE__ */ import_react3.default.createElement("code", { style: { fontFamily: "monospace", color: "inherit" } }, maskKey(editingKey.key))), /* @__PURE__ */ import_react3.default.createElement(
1442
- import_TextField.default,
1443
- {
1444
- label: "Alias",
1445
- value: editForm.key_alias || "",
1446
- onChange: (e) => setEditForm({ ...editForm, key_alias: e.target.value }),
1447
- fullWidth: true
1448
- }
1449
- ), models.length > 0 && /* @__PURE__ */ import_react3.default.createElement(
1450
- import_Autocomplete.default,
1451
- {
1452
- multiple: true,
1453
- options: models,
1454
- groupBy: (m) => m.mode || "other",
1455
- getOptionLabel: (m) => m.model_name,
1456
- value: editSelectedModels,
1457
- onChange: (_e, selected) => setEditForm({ ...editForm, models: selected.map((m) => m.model_name) }),
1458
- renderOption: (props, m) => /* @__PURE__ */ import_react3.default.createElement("li", { ...props }, modelOption(m)),
1459
- renderInput: (params) => /* @__PURE__ */ import_react3.default.createElement(import_TextField.default, { ...params, label: "Models", fullWidth: true })
1460
- }
1461
- ), /* @__PURE__ */ import_react3.default.createElement(
1462
- import_TextField.default,
1463
- {
1464
- label: "Max Budget (USD)",
1465
- type: "number",
1466
- value: editForm.max_budget ?? "",
1467
- onChange: (e) => setEditForm({ ...editForm, max_budget: e.target.value ? Number(e.target.value) : void 0 }),
1468
- fullWidth: true
1469
- }
1470
- ), /* @__PURE__ */ import_react3.default.createElement(
1471
- import_TextField.default,
1472
- {
1473
- label: "TPM Limit",
1474
- type: "number",
1475
- value: editForm.tpm_limit ?? "",
1476
- onChange: (e) => setEditForm({ ...editForm, tpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
1477
- helperText: "Max tokens per minute this key can consume across all models. Leave blank for no limit.",
1478
- fullWidth: true
1479
- }
1480
- ), /* @__PURE__ */ import_react3.default.createElement(
1481
- import_TextField.default,
1482
- {
1483
- label: "RPM Limit",
1484
- type: "number",
1485
- value: editForm.rpm_limit ?? "",
1486
- onChange: (e) => setEditForm({ ...editForm, rpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
1487
- helperText: "Max requests per minute this key can make across all models. Leave blank for no limit.",
1488
- fullWidth: true
1489
- }
1490
- ), /* @__PURE__ */ import_react3.default.createElement(import_Box3.default, { mt: 1, pt: 2, borderTop: "1px solid", sx: { borderColor: "divider" } }, /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, { variant: "caption", color: "text.secondary", display: "block", mb: 1 }, "Danger Zone"), !resetSpendConfirm ? /* @__PURE__ */ import_react3.default.createElement(
1491
- import_Button2.default,
1492
- {
1493
- size: "small",
1494
- color: "warning",
1495
- variant: "outlined",
1496
- onClick: () => setResetSpendConfirm(true)
1497
- },
1498
- "Reset Spend to $0"
1499
- ) : /* @__PURE__ */ import_react3.default.createElement(import_Box3.default, { display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }, /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, { variant: "body2", color: "warning.main" }, "Zero out spend counter?"), /* @__PURE__ */ import_react3.default.createElement(
1500
- import_Button2.default,
1501
- {
1502
- size: "small",
1503
- color: "warning",
1504
- variant: "contained",
1505
- disabled: resetSpendSubmitting,
1506
- onClick: handleResetSpend
1507
- },
1508
- resetSpendSubmitting ? /* @__PURE__ */ import_react3.default.createElement(import_CircularProgress.default, { size: 16 }) : "Confirm"
1509
- ), /* @__PURE__ */ import_react3.default.createElement(import_Button2.default, { size: "small", onClick: () => setResetSpendConfirm(false) }, "Cancel"))))), /* @__PURE__ */ import_react3.default.createElement(import_DialogActions.default, null, /* @__PURE__ */ import_react3.default.createElement(import_Button2.default, { onClick: handleCloseEdit }, "Cancel"), /* @__PURE__ */ import_react3.default.createElement(import_Button2.default, { onClick: handleUpdate, variant: "contained", color: "primary", disabled: editSubmitting }, editSubmitting ? /* @__PURE__ */ import_react3.default.createElement(import_CircularProgress.default, { size: 24 }) : "Save"))), /* @__PURE__ */ import_react3.default.createElement(import_Dialog.default, { open: !!deleteConfirmId, onClose: () => setDeleteConfirmId(null), maxWidth: "xs", fullWidth: true }, /* @__PURE__ */ import_react3.default.createElement(import_DialogTitle.default, null, "Revoke Key?"), /* @__PURE__ */ import_react3.default.createElement(import_DialogContent.default, null, /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, null, "This will permanently revoke the key. Any integrations using it will stop working immediately.")), /* @__PURE__ */ import_react3.default.createElement(import_DialogActions.default, null, /* @__PURE__ */ import_react3.default.createElement(import_Button2.default, { onClick: () => setDeleteConfirmId(null), disabled: deleteSubmitting }, "Cancel"), /* @__PURE__ */ import_react3.default.createElement(
1366
+ ), /* @__PURE__ */ import_react3.default.createElement(import_Dialog.default, { open: !!deleteConfirmId, onClose: () => setDeleteConfirmId(null), maxWidth: "xs", fullWidth: true }, /* @__PURE__ */ import_react3.default.createElement(import_DialogTitle.default, null, "Revoke Key?"), /* @__PURE__ */ import_react3.default.createElement(import_DialogContent.default, null, /* @__PURE__ */ import_react3.default.createElement(import_Typography3.default, null, "This will permanently revoke the key. Any integrations using it will stop working immediately.")), /* @__PURE__ */ import_react3.default.createElement(import_DialogActions.default, null, /* @__PURE__ */ import_react3.default.createElement(import_Button2.default, { onClick: () => setDeleteConfirmId(null), disabled: deleteSubmitting }, "Cancel"), /* @__PURE__ */ import_react3.default.createElement(
1510
1367
  import_Button2.default,
1511
1368
  {
1512
1369
  onClick: handleConfirmDelete,
@@ -1543,7 +1400,7 @@ var init_format = __esm({
1543
1400
  }
1544
1401
  });
1545
1402
 
1546
- // src/components/GenerateKeyDialog.tsx
1403
+ // src/components/KeyFormDialog.tsx
1547
1404
  function trimSlash(url) {
1548
1405
  return url.replace(/\/+$/, "");
1549
1406
  }
@@ -1636,17 +1493,17 @@ function teamHelperText(teamError, teamRequired) {
1636
1493
  if (teamRequired) return "Bind this key to a team for scoped access";
1637
1494
  return "Optional: bind this key to a specific team for scoped access";
1638
1495
  }
1639
- function budgetHelperText(budgetInvalid, budgetEstimate) {
1640
- if (budgetInvalid) return 'Enter a positive budget or tick "Unlimited"';
1496
+ function budgetHelperText(budgetInvalid, budgetEstimate, unlimited) {
1497
+ if (budgetInvalid && !unlimited) return 'Enter a positive budget or tick "Unlimited"';
1641
1498
  if (budgetEstimate !== null) return `\u2248 ${fmtInt(budgetEstimate)} tokens at the selected model's rate`;
1642
1499
  return void 0;
1643
1500
  }
1644
- function fmtCost2(perToken) {
1501
+ function fmtCost(perToken) {
1645
1502
  if (!perToken) return null;
1646
1503
  const per1k = perToken * 1e3;
1647
1504
  return per1k < 0.01 ? `$${(perToken * 1e6).toFixed(2)}/M` : `$${per1k.toFixed(3)}/1K`;
1648
1505
  }
1649
- function formatContextWindow2(maxInput, maxOutput) {
1506
+ function formatContextWindow(maxInput, maxOutput) {
1650
1507
  if (!maxInput && !maxOutput) return null;
1651
1508
  const fmt = (n) => {
1652
1509
  if (!n) return null;
@@ -1661,9 +1518,9 @@ function formatContextWindow2(maxInput, maxOutput) {
1661
1518
  if (outPart) return `ctx ${outPart} out`;
1662
1519
  return null;
1663
1520
  }
1664
- var import_react4, import_Box4, import_Typography4, import_Dialog2, import_DialogTitle2, import_DialogContent2, import_DialogActions2, import_TextField2, import_MenuItem, import_Button3, import_IconButton2, import_CircularProgress2, import_Autocomplete2, import_Checkbox, import_FormControlLabel, import_Alert, import_Accordion, import_AccordionSummary, import_AccordionDetails, import_Tabs, import_Tab, import_icons_material3, generateDefaultAlias, emptyForm, ALL_PROXY_MODELS, SNIPPET_FILE_HINTS, SnippetTabs, GenerateKeyDialog;
1665
- var init_GenerateKeyDialog = __esm({
1666
- "src/components/GenerateKeyDialog.tsx"() {
1521
+ var import_react4, import_Box4, import_Typography4, import_Dialog2, import_DialogTitle2, import_DialogContent2, import_DialogActions2, import_TextField2, import_MenuItem, import_Button3, import_IconButton2, import_CircularProgress2, import_Autocomplete, import_Checkbox, import_FormControlLabel, import_Alert, import_Accordion, import_AccordionSummary, import_AccordionDetails, import_Tabs, import_Tab, import_icons_material3, generateDefaultAlias, createForm, editForm, ALL_PROXY_MODELS, SNIPPET_FILE_HINTS, SnippetTabs, KeyFormDialog;
1522
+ var init_KeyFormDialog = __esm({
1523
+ "src/components/KeyFormDialog.tsx"() {
1667
1524
  "use strict";
1668
1525
  import_react4 = __toESM(require("react"));
1669
1526
  import_Box4 = __toESM(require("@mui/material/Box"));
@@ -1677,7 +1534,7 @@ var init_GenerateKeyDialog = __esm({
1677
1534
  import_Button3 = __toESM(require("@mui/material/Button"));
1678
1535
  import_IconButton2 = __toESM(require("@mui/material/IconButton"));
1679
1536
  import_CircularProgress2 = __toESM(require("@mui/material/CircularProgress"));
1680
- import_Autocomplete2 = __toESM(require("@mui/material/Autocomplete"));
1537
+ import_Autocomplete = __toESM(require("@mui/material/Autocomplete"));
1681
1538
  import_Checkbox = __toESM(require("@mui/material/Checkbox"));
1682
1539
  import_FormControlLabel = __toESM(require("@mui/material/FormControlLabel"));
1683
1540
  import_Alert = __toESM(require("@mui/material/Alert"));
@@ -1693,8 +1550,8 @@ var init_GenerateKeyDialog = __esm({
1693
1550
  const hash = Math.random().toString(36).slice(2, 8);
1694
1551
  return `${base}-${hash}`;
1695
1552
  };
1696
- emptyForm = () => ({
1697
- alias: "",
1553
+ createForm = (username) => ({
1554
+ alias: generateDefaultAlias(username),
1698
1555
  models: [],
1699
1556
  duration: "30d",
1700
1557
  max_budget: 100,
@@ -1703,6 +1560,13 @@ var init_GenerateKeyDialog = __esm({
1703
1560
  team_id: void 0,
1704
1561
  key_type: "llm_api"
1705
1562
  });
1563
+ editForm = (k) => ({
1564
+ key_alias: k.key_alias ?? "",
1565
+ models: k.models ?? [],
1566
+ max_budget: k.max_budget,
1567
+ tpm_limit: k.tpm_limit,
1568
+ rpm_limit: k.rpm_limit
1569
+ });
1706
1570
  ALL_PROXY_MODELS = "all-proxy-models";
1707
1571
  SNIPPET_FILE_HINTS = {
1708
1572
  opencode: "Add to ~/.config/opencode/opencode.json",
@@ -1749,46 +1613,79 @@ var init_GenerateKeyDialog = __esm({
1749
1613
  model && /* @__PURE__ */ import_react4.default.createElement(import_Typography4.default, { variant: "caption", color: "text.secondary", display: "block", mt: 1 }, "Using model \u201C", model, "\u201D \u2014 swap it for any model you have access to.")
1750
1614
  ));
1751
1615
  };
1752
- GenerateKeyDialog = ({
1616
+ KeyFormDialog = ({
1753
1617
  open,
1754
1618
  onClose,
1619
+ mode,
1620
+ keyToEdit = null,
1755
1621
  keys,
1756
1622
  models,
1757
1623
  teams,
1758
1624
  username,
1759
1625
  keyGenerationSettings,
1760
- onGenerateKey,
1626
+ onCreateKey,
1627
+ onUpdateKey,
1628
+ onResetKeySpend,
1761
1629
  onGetConfig
1762
1630
  }) => {
1763
- const [formData, setFormData] = (0, import_react4.useState)(emptyForm());
1631
+ const isCreate = mode === "create";
1632
+ const [generateForm, setGenerateForm] = (0, import_react4.useState)(createForm(username));
1633
+ const [editFormState, setEditFormState] = (0, import_react4.useState)({});
1764
1634
  const [unlimitedBudget, setUnlimitedBudget] = (0, import_react4.useState)(false);
1765
1635
  const [submitting, setSubmitting] = (0, import_react4.useState)(false);
1766
1636
  const [newKeyValue, setNewKeyValue] = (0, import_react4.useState)(null);
1767
1637
  const [newKeySnippets, setNewKeySnippets] = (0, import_react4.useState)(null);
1768
1638
  const [newKeyModel, setNewKeyModel] = (0, import_react4.useState)("");
1769
1639
  const [generateError, setGenerateError] = (0, import_react4.useState)(null);
1640
+ const [editError, setEditError] = (0, import_react4.useState)(null);
1641
+ const [resetSpendConfirm, setResetSpendConfirm] = (0, import_react4.useState)(false);
1642
+ const [resetSpendSubmitting, setResetSpendSubmitting] = (0, import_react4.useState)(false);
1770
1643
  (0, import_react4.useEffect)(() => {
1771
1644
  if (open) {
1772
- setFormData({ ...emptyForm(), alias: generateDefaultAlias(username) });
1773
- setUnlimitedBudget(false);
1645
+ if (isCreate) {
1646
+ setGenerateForm(createForm(username));
1647
+ } else if (keyToEdit) {
1648
+ setEditFormState(editForm(keyToEdit));
1649
+ setUnlimitedBudget(
1650
+ keyGenerationSettings?.allowUnlimitedBudget === true && (keyToEdit.max_budget === null || keyToEdit.max_budget === void 0)
1651
+ );
1652
+ } else {
1653
+ setEditFormState({});
1654
+ setUnlimitedBudget(false);
1655
+ }
1774
1656
  setNewKeyValue(null);
1775
1657
  setNewKeySnippets(null);
1776
1658
  setNewKeyModel("");
1777
1659
  setGenerateError(null);
1660
+ setEditError(null);
1661
+ setResetSpendConfirm(false);
1778
1662
  }
1779
- }, [open, username]);
1663
+ }, [open, isCreate, keyToEdit, username, keyGenerationSettings?.allowUnlimitedBudget]);
1780
1664
  const allowUnlimitedBudget = keyGenerationSettings?.allowUnlimitedBudget ?? false;
1781
1665
  const teamRequired = keyGenerationSettings?.teamRequired ?? true;
1782
- const selectedTeam = teams.find((t) => t.team_id === formData.team_id) ?? null;
1666
+ const formData = isCreate ? generateForm : editFormState;
1667
+ const setFormData = (patch) => {
1668
+ if (isCreate) {
1669
+ setGenerateForm((prev) => ({ ...prev, ...patch }));
1670
+ } else {
1671
+ setEditFormState((prev) => ({ ...prev, ...patch }));
1672
+ }
1673
+ };
1674
+ const currentAlias = (isCreate ? generateForm.alias : editFormState.key_alias) ?? "";
1675
+ const setAliasField = (value) => {
1676
+ if (isCreate) setFormData({ alias: value });
1677
+ else setFormData({ key_alias: value });
1678
+ };
1679
+ const selectedTeam = teams.find((t) => t.team_id === (isCreate ? generateForm.team_id : void 0)) ?? null;
1783
1680
  const availableModels = (0, import_react4.useMemo)(() => {
1784
1681
  return models.filter((m) => isModelAllowedByTeam(m, selectedTeam?.models));
1785
1682
  }, [models, selectedTeam]);
1786
1683
  const selectedModels = availableModels.filter((m) => (formData.models || []).includes(m.model_name));
1787
- const aliasError = !(formData.alias || "").trim();
1788
- const teamError = teamRequired && !formData.team_id;
1684
+ const aliasError = !currentAlias.trim();
1685
+ const teamError = isCreate && teamRequired && !generateForm.team_id;
1789
1686
  const budgetInvalid = !unlimitedBudget && (formData.max_budget === void 0 || formData.max_budget === null || formData.max_budget <= 0);
1790
- const canGenerate = !aliasError && !teamError && !budgetInvalid && !submitting;
1791
- const aliasDuplicate = !!formData.alias && keys.some((k) => k.key_alias === formData.alias);
1687
+ const canSubmit = !aliasError && !teamError && !budgetInvalid && !submitting;
1688
+ const aliasDuplicate = !!currentAlias && keys.some((k) => k.key_alias === currentAlias && k !== keyToEdit);
1792
1689
  const budgetEstimate = (0, import_react4.useMemo)(() => {
1793
1690
  if (unlimitedBudget || budgetInvalid) return null;
1794
1691
  const inputCosts = selectedModels.map((m) => m.input_cost_per_token).filter((c) => typeof c === "number" && c > 0);
@@ -1801,11 +1698,11 @@ var init_GenerateKeyDialog = __esm({
1801
1698
  setGenerateError(null);
1802
1699
  try {
1803
1700
  const request = {
1804
- ...formData,
1805
- max_budget: unlimitedBudget ? null : formData.max_budget
1701
+ ...generateForm,
1702
+ max_budget: unlimitedBudget ? null : generateForm.max_budget
1806
1703
  };
1807
- const response = await onGenerateKey(request);
1808
- const model = formData.models?.[0] ?? availableModels[0]?.model_name ?? "";
1704
+ const response = await onCreateKey(request);
1705
+ const model = generateForm.models?.[0] ?? availableModels[0]?.model_name ?? "";
1809
1706
  setNewKeyValue(response.key);
1810
1707
  setNewKeyModel(model);
1811
1708
  setNewKeySnippets(null);
@@ -1814,7 +1711,7 @@ var init_GenerateKeyDialog = __esm({
1814
1711
  setNewKeySnippets(buildSnippets(config.baseUrl, response.key, model));
1815
1712
  } catch {
1816
1713
  }
1817
- setFormData(emptyForm());
1714
+ setGenerateForm(createForm(username));
1818
1715
  setUnlimitedBudget(false);
1819
1716
  } catch (error) {
1820
1717
  setGenerateError(error?.message ?? "Failed to generate key");
@@ -1822,27 +1719,58 @@ var init_GenerateKeyDialog = __esm({
1822
1719
  setSubmitting(false);
1823
1720
  }
1824
1721
  };
1722
+ const handleUpdate = async () => {
1723
+ if (!keyToEdit) return;
1724
+ setSubmitting(true);
1725
+ setEditError(null);
1726
+ try {
1727
+ const request = {
1728
+ ...editFormState,
1729
+ max_budget: unlimitedBudget ? null : editFormState.max_budget
1730
+ };
1731
+ await onUpdateKey(keyToEdit.token ?? keyToEdit.key, request);
1732
+ onClose();
1733
+ } catch (error) {
1734
+ setEditError(error?.message ?? "Failed to update key");
1735
+ } finally {
1736
+ setSubmitting(false);
1737
+ }
1738
+ };
1739
+ const handleResetSpend = async () => {
1740
+ if (!keyToEdit || !onResetKeySpend) return;
1741
+ setResetSpendSubmitting(true);
1742
+ try {
1743
+ await onResetKeySpend(keyToEdit.token ?? keyToEdit.key);
1744
+ setResetSpendConfirm(false);
1745
+ onClose();
1746
+ } finally {
1747
+ setResetSpendSubmitting(false);
1748
+ }
1749
+ };
1825
1750
  const handleClose = () => {
1826
1751
  onClose();
1827
1752
  setGenerateError(null);
1753
+ setEditError(null);
1828
1754
  setNewKeyValue(null);
1829
1755
  setNewKeySnippets(null);
1830
- setFormData(emptyForm());
1756
+ setGenerateForm(createForm(username));
1757
+ setEditFormState({});
1831
1758
  setUnlimitedBudget(false);
1759
+ setResetSpendConfirm(false);
1832
1760
  };
1833
1761
  const copyToClipboard = (text) => {
1834
1762
  navigator.clipboard.writeText(text);
1835
1763
  };
1836
1764
  const modelOption = (m) => {
1837
- const inCost = fmtCost2(m.input_cost_per_token);
1838
- const outCost = fmtCost2(m.output_cost_per_token);
1839
- const ctx = formatContextWindow2(m.max_input_tokens, m.max_output_tokens);
1765
+ const inCost = fmtCost(m.input_cost_per_token);
1766
+ const outCost = fmtCost(m.output_cost_per_token);
1767
+ const ctx = formatContextWindow(m.max_input_tokens, m.max_output_tokens);
1840
1768
  return /* @__PURE__ */ import_react4.default.createElement(import_Box4.default, null, /* @__PURE__ */ import_react4.default.createElement("span", null, m.model_name), m.supports_function_calling && " \u{1F527}", m.supports_vision && " \u{1F441}\uFE0F", ctx && /* @__PURE__ */ import_react4.default.createElement(import_Typography4.default, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, ctx), (inCost || outCost) && /* @__PURE__ */ import_react4.default.createElement(import_Typography4.default, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, inCost, " in \xB7 ", outCost, " out"));
1841
1769
  };
1842
1770
  const renderTeamField = () => {
1843
1771
  if (teams.length > 0) {
1844
1772
  return /* @__PURE__ */ import_react4.default.createElement(
1845
- import_Autocomplete2.default,
1773
+ import_Autocomplete.default,
1846
1774
  {
1847
1775
  options: teams,
1848
1776
  getOptionLabel: (t) => t.team_alias || t.team_id,
@@ -1852,28 +1780,61 @@ var init_GenerateKeyDialog = __esm({
1852
1780
  const model = models.find((m) => m.model_name === name);
1853
1781
  return model ? isModelAllowedByTeam(model, team?.models) : false;
1854
1782
  });
1855
- setFormData({ ...formData, team_id: team?.team_id, models: restrictedModels });
1783
+ setFormData({ team_id: team?.team_id, models: restrictedModels });
1856
1784
  },
1785
+ disabled: !isCreate,
1857
1786
  renderInput: (params) => /* @__PURE__ */ import_react4.default.createElement(
1858
1787
  import_TextField2.default,
1859
1788
  {
1860
1789
  ...params,
1861
1790
  label: "Team",
1862
1791
  error: teamError,
1863
- helperText: teamHelperText(teamError, teamRequired),
1864
- required: teamRequired,
1792
+ helperText: isCreate ? teamHelperText(teamError, teamRequired) : "Team binding is fixed after creation \u2014 delete and recreate the key to change it",
1793
+ required: isCreate && teamRequired,
1865
1794
  fullWidth: true
1866
1795
  }
1867
1796
  )
1868
1797
  }
1869
1798
  );
1870
1799
  }
1871
- if (teamRequired) {
1800
+ if (teamRequired && isCreate) {
1872
1801
  return /* @__PURE__ */ import_react4.default.createElement(import_Typography4.default, { variant: "body2", color: "error" }, "Team selection is required, but you don't belong to any team yet \u2014 contact your administrator.");
1873
1802
  }
1874
1803
  return null;
1875
1804
  };
1876
- return /* @__PURE__ */ import_react4.default.createElement(import_Dialog2.default, { open, onClose: handleClose, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ import_react4.default.createElement(import_DialogTitle2.default, null, newKeyValue ? "Key Generated" : "Generate New Key"), /* @__PURE__ */ import_react4.default.createElement(import_DialogContent2.default, null, newKeyValue ? /* @__PURE__ */ import_react4.default.createElement(import_Box4.default, null, /* @__PURE__ */ import_react4.default.createElement(import_Typography4.default, { variant: "body2", color: "text.secondary", gutterBottom: true }, "Copy this key now. You won't be able to see it again."), /* @__PURE__ */ import_react4.default.createElement(
1805
+ const editKeyId = keyToEdit?.token ?? keyToEdit?.key ?? "";
1806
+ const renderDangerZone = () => {
1807
+ if (isCreate || !onResetKeySpend) return null;
1808
+ return /* @__PURE__ */ import_react4.default.createElement(import_Box4.default, { mt: 1, pt: 2, borderTop: "1px solid", sx: { borderColor: "divider" } }, /* @__PURE__ */ import_react4.default.createElement(import_Typography4.default, { variant: "caption", color: "text.secondary", display: "block", mb: 1 }, "Danger Zone"), !resetSpendConfirm ? /* @__PURE__ */ import_react4.default.createElement(
1809
+ import_Button3.default,
1810
+ {
1811
+ size: "small",
1812
+ color: "warning",
1813
+ variant: "outlined",
1814
+ onClick: () => setResetSpendConfirm(true)
1815
+ },
1816
+ "Reset Spend to $0"
1817
+ ) : /* @__PURE__ */ import_react4.default.createElement(import_Box4.default, { display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }, /* @__PURE__ */ import_react4.default.createElement(import_Typography4.default, { variant: "body2", color: "warning.main" }, "Zero out spend counter?"), /* @__PURE__ */ import_react4.default.createElement(
1818
+ import_Button3.default,
1819
+ {
1820
+ size: "small",
1821
+ color: "warning",
1822
+ variant: "contained",
1823
+ disabled: resetSpendSubmitting,
1824
+ onClick: handleResetSpend
1825
+ },
1826
+ resetSpendSubmitting ? /* @__PURE__ */ import_react4.default.createElement(import_CircularProgress2.default, { size: 16 }) : "Confirm"
1827
+ ), /* @__PURE__ */ import_react4.default.createElement(import_Button3.default, { size: "small", onClick: () => setResetSpendConfirm(false) }, "Cancel")));
1828
+ };
1829
+ const maskKey = (key) => {
1830
+ if (key.length <= 8) return "***";
1831
+ return `${key.slice(0, 4)}...${key.slice(-4)}`;
1832
+ };
1833
+ let dialogTitle = "Edit Key";
1834
+ if (newKeyValue) dialogTitle = "Key Generated";
1835
+ else if (isCreate) dialogTitle = "Generate New Key";
1836
+ const submitLabel = isCreate ? "Generate" : "Save";
1837
+ return /* @__PURE__ */ import_react4.default.createElement(import_Dialog2.default, { open, onClose: handleClose, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ import_react4.default.createElement(import_DialogTitle2.default, null, dialogTitle), /* @__PURE__ */ import_react4.default.createElement(import_DialogContent2.default, null, newKeyValue ? /* @__PURE__ */ import_react4.default.createElement(import_Box4.default, null, /* @__PURE__ */ import_react4.default.createElement(import_Typography4.default, { variant: "body2", color: "text.secondary", gutterBottom: true }, "Copy this key now. You won't be able to see it again."), /* @__PURE__ */ import_react4.default.createElement(
1877
1838
  import_Box4.default,
1878
1839
  {
1879
1840
  display: "flex",
@@ -1922,14 +1883,15 @@ var init_GenerateKeyDialog = __esm({
1922
1883
  newKeySnippets.publicEndpoint
1923
1884
  ),
1924
1885
  /* @__PURE__ */ import_react4.default.createElement(import_IconButton2.default, { size: "small", onClick: () => copyToClipboard(newKeySnippets.publicEndpoint) }, /* @__PURE__ */ import_react4.default.createElement(import_icons_material3.ContentCopy, { fontSize: "small" }))
1925
- )), newKeySnippets && /* @__PURE__ */ import_react4.default.createElement(import_Box4.default, { mt: 3 }, /* @__PURE__ */ import_react4.default.createElement(import_Box4.default, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_icons_material3.Code, { fontSize: "small", color: "action" }), /* @__PURE__ */ import_react4.default.createElement(import_Typography4.default, { variant: "subtitle2" }, "Start calling the proxy \u2014 paste and run")), /* @__PURE__ */ import_react4.default.createElement(SnippetTabs, { snippets: newKeySnippets, model: newKeyModel, onCopy: copyToClipboard }))) : /* @__PURE__ */ import_react4.default.createElement(import_Box4.default, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, generateError && /* @__PURE__ */ import_react4.default.createElement(import_Alert.default, { severity: "error", onClose: () => setGenerateError(null) }, generateError), /* @__PURE__ */ import_react4.default.createElement(
1886
+ )), newKeySnippets && /* @__PURE__ */ import_react4.default.createElement(import_Box4.default, { mt: 3 }, /* @__PURE__ */ import_react4.default.createElement(import_Box4.default, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_icons_material3.Code, { fontSize: "small", color: "action" }), /* @__PURE__ */ import_react4.default.createElement(import_Typography4.default, { variant: "subtitle2" }, "Start calling the proxy \u2014 paste and run")), /* @__PURE__ */ import_react4.default.createElement(SnippetTabs, { snippets: newKeySnippets, model: newKeyModel, onCopy: copyToClipboard }))) : /* @__PURE__ */ import_react4.default.createElement(import_Box4.default, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, !isCreate && /* @__PURE__ */ import_react4.default.createElement(import_Typography4.default, { variant: "body2", color: "text.secondary" }, /* @__PURE__ */ import_react4.default.createElement("code", { style: { fontFamily: "monospace", color: "inherit" } }, maskKey(editKeyId))), generateError && /* @__PURE__ */ import_react4.default.createElement(import_Alert.default, { severity: "error", onClose: () => setGenerateError(null) }, generateError), editError && /* @__PURE__ */ import_react4.default.createElement(import_Alert.default, { severity: "error", onClose: () => setEditError(null) }, editError), /* @__PURE__ */ import_react4.default.createElement(
1926
1887
  import_TextField2.default,
1927
1888
  {
1928
1889
  label: "Alias",
1929
- value: formData.alias || "",
1890
+ value: currentAlias,
1930
1891
  onChange: (e) => {
1931
- setFormData({ ...formData, alias: e.target.value });
1892
+ setAliasField(e.target.value);
1932
1893
  setGenerateError(null);
1894
+ setEditError(null);
1933
1895
  },
1934
1896
  error: aliasError,
1935
1897
  color: aliasDuplicate ? "warning" : void 0,
@@ -1937,13 +1899,13 @@ var init_GenerateKeyDialog = __esm({
1937
1899
  required: true,
1938
1900
  fullWidth: true
1939
1901
  }
1940
- ), /* @__PURE__ */ import_react4.default.createElement(
1902
+ ), !isCreate ? null : /* @__PURE__ */ import_react4.default.createElement(
1941
1903
  import_TextField2.default,
1942
1904
  {
1943
1905
  select: true,
1944
1906
  label: "Duration",
1945
- value: formData.duration || "30d",
1946
- onChange: (e) => setFormData({ ...formData, duration: e.target.value }),
1907
+ value: generateForm.duration || "30d",
1908
+ onChange: (e) => setFormData({ duration: e.target.value }),
1947
1909
  fullWidth: true
1948
1910
  },
1949
1911
  /* @__PURE__ */ import_react4.default.createElement(import_MenuItem.default, { value: "1d" }, "1 Day"),
@@ -1952,14 +1914,14 @@ var init_GenerateKeyDialog = __esm({
1952
1914
  /* @__PURE__ */ import_react4.default.createElement(import_MenuItem.default, { value: "90d" }, "90 Days"),
1953
1915
  /* @__PURE__ */ import_react4.default.createElement(import_MenuItem.default, { value: "1y" }, "1 Year")
1954
1916
  ), renderTeamField(), availableModels.length > 0 && /* @__PURE__ */ import_react4.default.createElement(
1955
- import_Autocomplete2.default,
1917
+ import_Autocomplete.default,
1956
1918
  {
1957
1919
  multiple: true,
1958
1920
  options: availableModels,
1959
1921
  groupBy: (m) => m.mode || "other",
1960
1922
  getOptionLabel: (m) => m.model_name,
1961
1923
  value: selectedModels,
1962
- onChange: (_e, selected) => setFormData({ ...formData, models: selected.map((m) => m.model_name) }),
1924
+ onChange: (_e, selected) => setFormData({ models: selected.map((m) => m.model_name) }),
1963
1925
  renderOption: (props, m) => /* @__PURE__ */ import_react4.default.createElement("li", { ...props }, modelOption(m)),
1964
1926
  renderInput: (params) => /* @__PURE__ */ import_react4.default.createElement(
1965
1927
  import_TextField2.default,
@@ -1981,9 +1943,9 @@ var init_GenerateKeyDialog = __esm({
1981
1943
  onChange: (e) => {
1982
1944
  setUnlimitedBudget(e.target.checked);
1983
1945
  if (e.target.checked) {
1984
- setFormData({ ...formData, max_budget: null });
1946
+ setFormData({ max_budget: null });
1985
1947
  } else {
1986
- setFormData({ ...formData, max_budget: 100 });
1948
+ setFormData({ max_budget: editFormState.max_budget ?? generateForm.max_budget ?? 100 });
1987
1949
  }
1988
1950
  }
1989
1951
  }
@@ -1996,9 +1958,9 @@ var init_GenerateKeyDialog = __esm({
1996
1958
  label: "Max Budget (USD)",
1997
1959
  type: "number",
1998
1960
  value: unlimitedBudget ? "" : formData.max_budget ?? "",
1999
- onChange: (e) => setFormData({ ...formData, max_budget: e.target.value ? Number(e.target.value) : void 0 }),
1961
+ onChange: (e) => setFormData({ max_budget: e.target.value ? Number(e.target.value) : void 0 }),
2000
1962
  error: budgetInvalid,
2001
- helperText: budgetHelperText(budgetInvalid, budgetEstimate),
1963
+ helperText: budgetHelperText(budgetInvalid, budgetEstimate, unlimitedBudget),
2002
1964
  disabled: unlimitedBudget,
2003
1965
  required: true,
2004
1966
  fullWidth: true
@@ -2009,7 +1971,7 @@ var init_GenerateKeyDialog = __esm({
2009
1971
  label: "TPM Limit",
2010
1972
  type: "number",
2011
1973
  value: formData.tpm_limit ?? "",
2012
- onChange: (e) => setFormData({ ...formData, tpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
1974
+ onChange: (e) => setFormData({ tpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
2013
1975
  helperText: "Max tokens per minute this key can consume across all models. Leave blank for no limit.",
2014
1976
  fullWidth: true
2015
1977
  }
@@ -2019,26 +1981,26 @@ var init_GenerateKeyDialog = __esm({
2019
1981
  label: "RPM Limit",
2020
1982
  type: "number",
2021
1983
  value: formData.rpm_limit ?? "",
2022
- onChange: (e) => setFormData({ ...formData, rpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
1984
+ onChange: (e) => setFormData({ rpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
2023
1985
  helperText: "Max requests per minute this key can make across all models. Leave blank for no limit.",
2024
1986
  fullWidth: true
2025
1987
  }
2026
- ))))), /* @__PURE__ */ import_react4.default.createElement(import_DialogActions2.default, null, newKeyValue ? /* @__PURE__ */ import_react4.default.createElement(import_Button3.default, { onClick: handleClose, variant: "contained", color: "success" }, "Done") : /* @__PURE__ */ import_react4.default.createElement(import_react4.default.Fragment, null, /* @__PURE__ */ import_react4.default.createElement(import_Button3.default, { onClick: handleClose }, "Cancel"), /* @__PURE__ */ import_react4.default.createElement(
1988
+ ))), renderDangerZone())), /* @__PURE__ */ import_react4.default.createElement(import_DialogActions2.default, null, newKeyValue ? /* @__PURE__ */ import_react4.default.createElement(import_Button3.default, { onClick: handleClose, variant: "contained", color: "success" }, "Done") : /* @__PURE__ */ import_react4.default.createElement(import_react4.default.Fragment, null, /* @__PURE__ */ import_react4.default.createElement(import_Button3.default, { onClick: handleClose }, "Cancel"), /* @__PURE__ */ import_react4.default.createElement(
2027
1989
  import_Button3.default,
2028
1990
  {
2029
- onClick: handleGenerate,
1991
+ onClick: isCreate ? handleGenerate : handleUpdate,
2030
1992
  variant: "contained",
2031
1993
  color: "primary",
2032
- disabled: !canGenerate
1994
+ disabled: !canSubmit
2033
1995
  },
2034
- submitting ? /* @__PURE__ */ import_react4.default.createElement(import_CircularProgress2.default, { size: 24 }) : "Generate"
1996
+ submitting ? /* @__PURE__ */ import_react4.default.createElement(import_CircularProgress2.default, { size: 24 }) : submitLabel
2035
1997
  ))));
2036
1998
  };
2037
1999
  }
2038
2000
  });
2039
2001
 
2040
2002
  // src/components/ManageTeamDialog.tsx
2041
- var import_react5, import_Dialog3, import_DialogTitle3, import_DialogContent3, import_DialogActions3, import_TextField3, import_Button4, import_Alert2, import_Checkbox2, import_FormControlLabel2, import_Autocomplete3, import_CircularProgress3, import_MenuItem2, import_Box5, import_Divider2, import_Typography5, import_IconButton3, import_Paper3, import_Table2, import_TableBody2, import_TableCell2, import_TableContainer2, import_TableHead2, import_TableRow2, import_icons_material4, import_core_plugin_api2, import_plugin_catalog_react, import_catalog_model, import_react_use, BUDGET_DURATION_OPTIONS, DEFAULT_BUDGET_DURATION, FALLBACK_BUDGET_CEILING, ManageTeamDialog;
2003
+ var import_react5, import_Dialog3, import_DialogTitle3, import_DialogContent3, import_DialogActions3, import_TextField3, import_Button4, import_Alert2, import_Checkbox2, import_FormControlLabel2, import_Autocomplete2, import_CircularProgress3, import_MenuItem2, import_Box5, import_Divider2, import_Typography5, import_IconButton3, import_Paper3, import_Table2, import_TableBody2, import_TableCell2, import_TableContainer2, import_TableHead2, import_TableRow2, import_icons_material4, import_core_plugin_api2, import_plugin_catalog_react, import_catalog_model, import_react_use, BUDGET_DURATION_OPTIONS, DEFAULT_BUDGET_DURATION, FALLBACK_BUDGET_CEILING, ManageTeamDialog;
2042
2004
  var init_ManageTeamDialog = __esm({
2043
2005
  "src/components/ManageTeamDialog.tsx"() {
2044
2006
  "use strict";
@@ -2052,7 +2014,7 @@ var init_ManageTeamDialog = __esm({
2052
2014
  import_Alert2 = __toESM(require("@mui/material/Alert"));
2053
2015
  import_Checkbox2 = __toESM(require("@mui/material/Checkbox"));
2054
2016
  import_FormControlLabel2 = __toESM(require("@mui/material/FormControlLabel"));
2055
- import_Autocomplete3 = __toESM(require("@mui/material/Autocomplete"));
2017
+ import_Autocomplete2 = __toESM(require("@mui/material/Autocomplete"));
2056
2018
  import_CircularProgress3 = __toESM(require("@mui/material/CircularProgress"));
2057
2019
  import_MenuItem2 = __toESM(require("@mui/material/MenuItem"));
2058
2020
  import_Box5 = __toESM(require("@mui/material/Box"));
@@ -2317,7 +2279,7 @@ var init_ManageTeamDialog = __esm({
2317
2279
  fullWidth: true
2318
2280
  }
2319
2281
  ), /* @__PURE__ */ import_react5.default.createElement(
2320
- import_Autocomplete3.default,
2282
+ import_Autocomplete2.default,
2321
2283
  {
2322
2284
  multiple: true,
2323
2285
  options: modelNames,
@@ -2390,7 +2352,7 @@ var init_ManageTeamDialog = __esm({
2390
2352
  },
2391
2353
  /* @__PURE__ */ import_react5.default.createElement(import_icons_material4.Delete, { fontSize: "small" })
2392
2354
  ))))))), /* @__PURE__ */ import_react5.default.createElement(import_Box5.default, { sx: { display: "flex", gap: 1, alignItems: "flex-start", flexWrap: "wrap" } }, /* @__PURE__ */ import_react5.default.createElement(
2393
- import_Autocomplete3.default,
2355
+ import_Autocomplete2.default,
2394
2356
  {
2395
2357
  freeSolo: true,
2396
2358
  autoHighlight: true,
@@ -2449,7 +2411,7 @@ var init_ManageTeamDialog = __esm({
2449
2411
  },
2450
2412
  "Add"
2451
2413
  ))), showKnowledgeBases && /* @__PURE__ */ import_react5.default.createElement(import_Box5.default, null, /* @__PURE__ */ import_react5.default.createElement(import_Divider2.default, { sx: { my: 1 } }), /* @__PURE__ */ import_react5.default.createElement(import_Typography5.default, { variant: "subtitle2", sx: { mb: 1 } }, "Knowledge Bases"), /* @__PURE__ */ import_react5.default.createElement(import_Typography5.default, { variant: "body2", color: "text.secondary", sx: { mb: 1 } }, "Attaching a knowledge base exposes its documents to every key in this team."), kbError && /* @__PURE__ */ import_react5.default.createElement(import_Alert2.default, { severity: "error", sx: { mb: 1 }, onClose: () => setKbError(null) }, kbError), /* @__PURE__ */ import_react5.default.createElement(
2452
- import_Autocomplete3.default,
2414
+ import_Autocomplete2.default,
2453
2415
  {
2454
2416
  multiple: true,
2455
2417
  options: kbOptions,
@@ -2468,7 +2430,7 @@ var init_ManageTeamDialog = __esm({
2468
2430
  },
2469
2431
  kbBusy ? "Saving\u2026" : "Save knowledge bases"
2470
2432
  ))), showMcpServers && /* @__PURE__ */ import_react5.default.createElement(import_Box5.default, null, /* @__PURE__ */ import_react5.default.createElement(import_Divider2.default, { sx: { my: 1 } }), /* @__PURE__ */ import_react5.default.createElement(import_Typography5.default, { variant: "subtitle2", sx: { mb: 1 } }, "MCP Servers"), /* @__PURE__ */ import_react5.default.createElement(import_Typography5.default, { variant: "body2", color: "text.secondary", sx: { mb: 1 } }, "Attaching an MCP server lets every key in this team invoke that server's tools through the model \u2014 including any side effects those tools have."), mcpError && /* @__PURE__ */ import_react5.default.createElement(import_Alert2.default, { severity: "error", sx: { mb: 1 }, onClose: () => setMcpError(null) }, mcpError), /* @__PURE__ */ import_react5.default.createElement(
2471
- import_Autocomplete3.default,
2433
+ import_Autocomplete2.default,
2472
2434
  {
2473
2435
  multiple: true,
2474
2436
  options: mcpOptions,
@@ -4120,7 +4082,7 @@ var init_LiteLLMPage = __esm({
4120
4082
  import_plugin_permission_react = require("@backstage/plugin-permission-react");
4121
4083
  init_DashboardHeader();
4122
4084
  init_KeysTable();
4123
- init_GenerateKeyDialog();
4085
+ init_KeyFormDialog();
4124
4086
  init_ManageTeamDialog();
4125
4087
  init_UsageStats();
4126
4088
  init_LiteLLMBudgetWidget();
@@ -4178,6 +4140,8 @@ var init_LiteLLMPage = __esm({
4178
4140
  );
4179
4141
  const [snackbar, setSnackbar] = (0, import_react12.useState)(null);
4180
4142
  const [manageTeam, setManageTeam] = (0, import_react12.useState)(null);
4143
+ const [keyToEdit, setKeyToEdit] = (0, import_react12.useState)(null);
4144
+ const keyFormOpen = generateDialogOpen || !!keyToEdit;
4181
4145
  const [teamUsageCache, setTeamUsageCache] = (0, import_react12.useState)({});
4182
4146
  const [teamUsageLoading, setTeamUsageLoading] = (0, import_react12.useState)({});
4183
4147
  const { value: userInfo, loading: userLoading, error: userError } = (0, import_react_use3.useAsync)(
@@ -4421,13 +4385,11 @@ var init_LiteLLMPage = __esm({
4421
4385
  KeysTable,
4422
4386
  {
4423
4387
  keys: keys ?? [],
4424
- models: allowedModels,
4425
4388
  loading: keysLoading || modelsLoading,
4426
4389
  onGenerateKeyClick: () => setGenerateDialogOpen(true),
4427
- onUpdateKey: handleUpdateKey,
4390
+ onEditKey: setKeyToEdit,
4428
4391
  onBlockKey: handleBlockKey,
4429
4392
  onUnblockKey: handleUnblockKey,
4430
- onResetKeySpend: handleResetKeySpend,
4431
4393
  onDeleteKey: handleDeleteKey,
4432
4394
  onPruneExpiredKeys: handlePruneExpiredKeys
4433
4395
  }
@@ -4460,16 +4422,23 @@ var init_LiteLLMPage = __esm({
4460
4422
  loading: modelsLoading
4461
4423
  }
4462
4424
  ), activeTab === "audit" && userInfo.can_view_audit && /* @__PURE__ */ import_react12.default.createElement(AuditLog, { api }), /* @__PURE__ */ import_react12.default.createElement(
4463
- GenerateKeyDialog,
4425
+ KeyFormDialog,
4464
4426
  {
4465
- open: generateDialogOpen,
4466
- onClose: () => setGenerateDialogOpen(false),
4427
+ open: keyFormOpen,
4428
+ onClose: () => {
4429
+ setGenerateDialogOpen(false);
4430
+ setKeyToEdit(null);
4431
+ },
4432
+ mode: keyToEdit ? "edit" : "create",
4433
+ keyToEdit,
4467
4434
  keys: keys ?? [],
4468
4435
  models: allowedModels,
4469
4436
  teams: teams ?? [],
4470
4437
  username: userInfo.user_id,
4471
4438
  keyGenerationSettings: liteLlmConfig?.keyGeneration,
4472
- onGenerateKey: handleGenerateKey,
4439
+ onCreateKey: handleGenerateKey,
4440
+ onUpdateKey: handleUpdateKey,
4441
+ onResetKeySpend: handleResetKeySpend,
4473
4442
  onGetConfig: () => api.getConfig()
4474
4443
  }
4475
4444
  ), /* @__PURE__ */ import_react12.default.createElement(
@@ -4553,6 +4522,7 @@ var index_exports = {};
4553
4522
  __export(index_exports, {
4554
4523
  DashboardHeader: () => DashboardHeader,
4555
4524
  GenerateKeyDialog: () => GenerateKeyDialog,
4525
+ KeyFormDialog: () => KeyFormDialog,
4556
4526
  KeysTable: () => KeysTable,
4557
4527
  LiteLLMBudgetGauges: () => LiteLLMBudgetGauges,
4558
4528
  LiteLLMBudgetWidget: () => LiteLLMBudgetWidget,
@@ -5010,7 +4980,30 @@ var LiteLLMBudgetGauges = ({
5010
4980
  };
5011
4981
 
5012
4982
  // src/index.ts
5013
- init_GenerateKeyDialog();
4983
+ init_KeyFormDialog();
4984
+
4985
+ // src/components/GenerateKeyDialog.tsx
4986
+ var import_react16 = __toESM(require("react"));
4987
+ init_KeyFormDialog();
4988
+ var GenerateKeyDialog = (props) => /* @__PURE__ */ import_react16.default.createElement(
4989
+ KeyFormDialog,
4990
+ {
4991
+ open: props.open,
4992
+ onClose: props.onClose,
4993
+ mode: "create",
4994
+ keys: props.keys,
4995
+ models: props.models,
4996
+ teams: props.teams,
4997
+ username: props.username,
4998
+ keyGenerationSettings: props.keyGenerationSettings,
4999
+ onCreateKey: props.onGenerateKey,
5000
+ onUpdateKey: async () => {
5001
+ },
5002
+ onGetConfig: props.onGetConfig
5003
+ }
5004
+ );
5005
+
5006
+ // src/index.ts
5014
5007
  init_ManageTeamDialog();
5015
5008
  init_api();
5016
5009
  init_permissions();