@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/components/KeysTable.d.ts +7 -0
- package/dist/index.cjs.js +55 -12
- package/dist/index.cjs.js.map +2 -2
- package/dist/index.esm.js +55 -12
- package/dist/index.esm.js.map +2 -2
- package/dist/types.d.ts +7 -0
- package/package.json +1 -1
|
@@ -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
|
@@ -276,7 +276,7 @@ response = client.chat.completions.create(
|
|
|
276
276
|
print(response.choices[0].message.content)`
|
|
277
277
|
};
|
|
278
278
|
}
|
|
279
|
-
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;
|
|
280
280
|
var init_KeysTable = __esm({
|
|
281
281
|
"src/components/KeysTable.tsx"() {
|
|
282
282
|
"use strict";
|
|
@@ -289,6 +289,11 @@ var init_KeysTable = __esm({
|
|
|
289
289
|
if (key.length <= 8) return "***";
|
|
290
290
|
return `${key.slice(0, 4)}...${key.slice(-4)}`;
|
|
291
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
|
+
};
|
|
292
297
|
shortKeyId = (token) => {
|
|
293
298
|
if (!token) return "-";
|
|
294
299
|
if (token.length <= 16) return token;
|
|
@@ -322,6 +327,8 @@ var init_KeysTable = __esm({
|
|
|
322
327
|
keys,
|
|
323
328
|
models,
|
|
324
329
|
teams,
|
|
330
|
+
username,
|
|
331
|
+
keyGenerationSettings,
|
|
325
332
|
loading,
|
|
326
333
|
onGenerateKey,
|
|
327
334
|
onUpdateKey,
|
|
@@ -355,12 +362,23 @@ var init_KeysTable = __esm({
|
|
|
355
362
|
(k) => (k.key_alias ?? "").toLowerCase().includes(q) || k.models?.some((m) => m.toLowerCase().includes(q))
|
|
356
363
|
);
|
|
357
364
|
}, [keys, filterText]);
|
|
358
|
-
const
|
|
365
|
+
const allowUnlimitedBudget = keyGenerationSettings?.allowUnlimitedBudget ?? false;
|
|
366
|
+
const teamRequired = keyGenerationSettings?.teamRequired ?? true;
|
|
359
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));
|
|
360
377
|
const editSelectedModels = models.filter((m) => (editForm.models || []).includes(m.model_name));
|
|
361
378
|
const aliasError = !(formData.alias || "").trim();
|
|
379
|
+
const teamError = teamRequired && !formData.team_id;
|
|
362
380
|
const budgetInvalid = !unlimitedBudget && (formData.max_budget === void 0 || formData.max_budget === null || formData.max_budget <= 0);
|
|
363
|
-
const canGenerate = !aliasError && !budgetInvalid && !submitting;
|
|
381
|
+
const canGenerate = !aliasError && !teamError && !budgetInvalid && !submitting;
|
|
364
382
|
const budgetEstimate = (0, import_react2.useMemo)(() => {
|
|
365
383
|
if (unlimitedBudget || budgetInvalid) return null;
|
|
366
384
|
const inputCosts = selectedModels.map((m) => m.input_cost_per_token).filter((c) => typeof c === "number" && c > 0);
|
|
@@ -393,6 +411,10 @@ var init_KeysTable = __esm({
|
|
|
393
411
|
setSubmitting(false);
|
|
394
412
|
}
|
|
395
413
|
};
|
|
414
|
+
const handleOpenGenerate = () => {
|
|
415
|
+
setFormData({ ...emptyForm(), alias: generateDefaultAlias(username) });
|
|
416
|
+
setGenerateModalOpen(true);
|
|
417
|
+
};
|
|
396
418
|
const handleCloseModal = () => {
|
|
397
419
|
setGenerateModalOpen(false);
|
|
398
420
|
setNewKeyValue(null);
|
|
@@ -510,7 +532,7 @@ var init_KeysTable = __esm({
|
|
|
510
532
|
variant: "contained",
|
|
511
533
|
color: "primary",
|
|
512
534
|
startIcon: /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Add, null),
|
|
513
|
-
onClick:
|
|
535
|
+
onClick: handleOpenGenerate
|
|
514
536
|
},
|
|
515
537
|
"Generate New Key"
|
|
516
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(
|
|
@@ -520,7 +542,7 @@ var init_KeysTable = __esm({
|
|
|
520
542
|
color: "primary",
|
|
521
543
|
size: "small",
|
|
522
544
|
startIcon: /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Add, null),
|
|
523
|
-
onClick:
|
|
545
|
+
onClick: handleOpenGenerate
|
|
524
546
|
},
|
|
525
547
|
"Generate Your First Key"
|
|
526
548
|
)))) : filteredKeys.map((key) => {
|
|
@@ -610,36 +632,50 @@ var init_KeysTable = __esm({
|
|
|
610
632
|
/* @__PURE__ */ import_react2.default.createElement(import_material2.MenuItem, { value: "30d" }, "30 Days"),
|
|
611
633
|
/* @__PURE__ */ import_react2.default.createElement(import_material2.MenuItem, { value: "90d" }, "90 Days"),
|
|
612
634
|
/* @__PURE__ */ import_react2.default.createElement(import_material2.MenuItem, { value: "1y" }, "1 Year")
|
|
613
|
-
), teams.length > 0
|
|
635
|
+
), teams.length > 0 ? /* @__PURE__ */ import_react2.default.createElement(
|
|
614
636
|
import_material2.Autocomplete,
|
|
615
637
|
{
|
|
616
638
|
options: teams,
|
|
617
639
|
getOptionLabel: (t) => t.team_alias || t.team_id,
|
|
618
640
|
value: selectedTeam,
|
|
619
|
-
onChange: (_e, team) =>
|
|
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
|
+
},
|
|
620
646
|
renderInput: (params) => /* @__PURE__ */ import_react2.default.createElement(
|
|
621
647
|
import_material2.TextField,
|
|
622
648
|
{
|
|
623
649
|
...params,
|
|
624
650
|
label: "Team",
|
|
625
|
-
|
|
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,
|
|
626
654
|
fullWidth: true
|
|
627
655
|
}
|
|
628
656
|
)
|
|
629
657
|
}
|
|
630
|
-
),
|
|
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(
|
|
631
659
|
import_material2.Autocomplete,
|
|
632
660
|
{
|
|
633
661
|
multiple: true,
|
|
634
|
-
options:
|
|
662
|
+
options: availableModels,
|
|
635
663
|
groupBy: (m) => m.mode || "other",
|
|
636
664
|
getOptionLabel: (m) => m.model_name,
|
|
637
665
|
value: selectedModels,
|
|
638
666
|
onChange: (_e, selected) => setFormData({ ...formData, models: selected.map((m) => m.model_name) }),
|
|
639
667
|
renderOption: (props, m) => /* @__PURE__ */ import_react2.default.createElement("li", { ...props }, modelOption(m)),
|
|
640
|
-
renderInput: (params) => /* @__PURE__ */ import_react2.default.createElement(
|
|
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
|
+
)
|
|
641
677
|
}
|
|
642
|
-
), /* @__PURE__ */ import_react2.default.createElement(
|
|
678
|
+
), allowUnlimitedBudget && /* @__PURE__ */ import_react2.default.createElement(
|
|
643
679
|
import_material2.FormControlLabel,
|
|
644
680
|
{
|
|
645
681
|
control: /* @__PURE__ */ import_react2.default.createElement(
|
|
@@ -678,6 +714,7 @@ var init_KeysTable = __esm({
|
|
|
678
714
|
type: "number",
|
|
679
715
|
value: formData.tpm_limit ?? "",
|
|
680
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.",
|
|
681
718
|
fullWidth: true
|
|
682
719
|
}
|
|
683
720
|
), /* @__PURE__ */ import_react2.default.createElement(
|
|
@@ -687,6 +724,7 @@ var init_KeysTable = __esm({
|
|
|
687
724
|
type: "number",
|
|
688
725
|
value: formData.rpm_limit ?? "",
|
|
689
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.",
|
|
690
728
|
fullWidth: true
|
|
691
729
|
}
|
|
692
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(
|
|
@@ -734,6 +772,7 @@ var init_KeysTable = __esm({
|
|
|
734
772
|
type: "number",
|
|
735
773
|
value: editForm.tpm_limit ?? "",
|
|
736
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.",
|
|
737
776
|
fullWidth: true
|
|
738
777
|
}
|
|
739
778
|
), /* @__PURE__ */ import_react2.default.createElement(
|
|
@@ -743,6 +782,7 @@ var init_KeysTable = __esm({
|
|
|
743
782
|
type: "number",
|
|
744
783
|
value: editForm.rpm_limit ?? "",
|
|
745
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.",
|
|
746
786
|
fullWidth: true
|
|
747
787
|
}
|
|
748
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(
|
|
@@ -1326,6 +1366,7 @@ var init_LiteLLMPage = __esm({
|
|
|
1326
1366
|
() => api.getTeams().catch(() => []),
|
|
1327
1367
|
[api]
|
|
1328
1368
|
);
|
|
1369
|
+
const { value: liteLlmConfig } = (0, import_react_use2.useAsync)(() => api.getConfig(), [api]);
|
|
1329
1370
|
const teams = (0, import_react6.useMemo)(() => {
|
|
1330
1371
|
if (!allTeams?.length) return [];
|
|
1331
1372
|
if (!userInfo) return allTeams;
|
|
@@ -1504,6 +1545,8 @@ var init_LiteLLMPage = __esm({
|
|
|
1504
1545
|
keys: keys ?? [],
|
|
1505
1546
|
models: allowedModels,
|
|
1506
1547
|
teams: teams ?? [],
|
|
1548
|
+
username: userInfo.user_id,
|
|
1549
|
+
keyGenerationSettings: liteLlmConfig?.keyGeneration,
|
|
1507
1550
|
loading: keysLoading || modelsLoading,
|
|
1508
1551
|
onGenerateKey: handleGenerateKey,
|
|
1509
1552
|
onUpdateKey: handleUpdateKey,
|