@acarmisc/backstage-plugin-litellm 0.31.0 → 0.32.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.
@@ -26,6 +26,12 @@ interface ManageTeamDialogProps {
26
26
  mcpServers?: McpServerInfo[];
27
27
  /** Called to replace the team's attached MCP servers; parent refreshes `team`. */
28
28
  onSaveMcpServers?: (mcpServerIds: string[]) => Promise<void>;
29
+ /** When true, only members section is shown (not budget/models/KB/MCP); title changes; Save becomes Done. */
30
+ membersOnly?: boolean;
31
+ /** Current user's LiteLLM ID; used in membersOnly mode to prevent self-removal and role-restricted removal. */
32
+ currentUserId?: string;
33
+ /** Roles that can manage members in membersOnly mode; used to disable removal of role holders. */
34
+ memberManagerRoles?: string[];
29
35
  }
30
36
  export declare const ManageTeamDialog: FC<ManageTeamDialogProps>;
31
37
  export {};
@@ -12,6 +12,11 @@ interface TeamUsageProps {
12
12
  onCreateTeam?: () => void;
13
13
  /** Called when a team card is expanded to trigger data loading. */
14
14
  onTeamExpand?: (team: TeamInfo) => void;
15
+ /** Per-team: whether the caller may manage this team's members only. */
16
+ canManageMembers?: (team: TeamInfo) => boolean;
17
+ onManageMembers?: (team: TeamInfo) => void;
18
+ /** Teams are synced from an identity provider: show a badge, no write controls. */
19
+ readOnly?: boolean;
15
20
  }
16
21
  export declare const TeamUsage: FC<TeamUsageProps>;
17
22
  export {};
package/dist/index.cjs.js CHANGED
@@ -2575,9 +2575,22 @@ var init_KeyFormDialog = __esm({
2575
2575
  setCloseWithoutCopyConfirm(false);
2576
2576
  setTouched({});
2577
2577
  setSubmitAttempted(false);
2578
- onGetConfig().then(setConfig).catch(() => setConfig(null));
2579
2578
  }
2580
- }, [open, isCreate, keyToEdit, username, keyGenerationSettings?.allowUnlimitedBudget, onGetConfig]);
2579
+ }, [open, isCreate, keyToEdit, username, keyGenerationSettings?.allowUnlimitedBudget]);
2580
+ const onGetConfigRef = (0, import_react6.useRef)(onGetConfig);
2581
+ onGetConfigRef.current = onGetConfig;
2582
+ (0, import_react6.useEffect)(() => {
2583
+ if (!open) return void 0;
2584
+ let cancelled = false;
2585
+ onGetConfigRef.current().then((c) => {
2586
+ if (!cancelled) setConfig(c);
2587
+ }).catch(() => {
2588
+ if (!cancelled) setConfig(null);
2589
+ });
2590
+ return () => {
2591
+ cancelled = true;
2592
+ };
2593
+ }, [open]);
2581
2594
  const allowUnlimitedBudget = keyGenerationSettings?.allowUnlimitedBudget ?? false;
2582
2595
  const teamRequired = keyGenerationSettings?.teamRequired ?? true;
2583
2596
  const formData = isCreate ? generateForm : editFormState;
@@ -3114,7 +3127,7 @@ var init_KeyFormDialog = __esm({
3114
3127
  });
3115
3128
 
3116
3129
  // src/components/ManageTeamDialog.tsx
3117
- var import_react7, 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_Box10, import_Divider2, import_Typography8, import_IconButton4, import_Paper4, import_Table2, import_TableBody2, import_TableCell2, import_TableContainer2, import_TableHead2, import_TableRow2, import_icons_material5, import_core_plugin_api4, import_plugin_catalog_react, import_catalog_model, import_jsx_runtime11, BUDGET_DURATION_OPTIONS, DEFAULT_BUDGET_DURATION, FALLBACK_BUDGET_CEILING, ManageTeamDialog;
3130
+ var import_react7, 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_Box10, import_Divider2, import_Typography8, import_IconButton4, import_Tooltip4, import_Paper4, import_Table2, import_TableBody2, import_TableCell2, import_TableContainer2, import_TableHead2, import_TableRow2, import_icons_material5, import_core_plugin_api4, import_plugin_catalog_react, import_catalog_model, import_jsx_runtime11, BUDGET_DURATION_OPTIONS, DEFAULT_BUDGET_DURATION, FALLBACK_BUDGET_CEILING, ManageTeamDialog;
3118
3131
  var init_ManageTeamDialog = __esm({
3119
3132
  "src/components/ManageTeamDialog.tsx"() {
3120
3133
  "use strict";
@@ -3135,6 +3148,7 @@ var init_ManageTeamDialog = __esm({
3135
3148
  import_Divider2 = __toESM(require("@mui/material/Divider"));
3136
3149
  import_Typography8 = __toESM(require("@mui/material/Typography"));
3137
3150
  import_IconButton4 = __toESM(require("@mui/material/IconButton"));
3151
+ import_Tooltip4 = __toESM(require("@mui/material/Tooltip"));
3138
3152
  import_Paper4 = __toESM(require("@mui/material/Paper"));
3139
3153
  import_Table2 = __toESM(require("@mui/material/Table"));
3140
3154
  import_TableBody2 = __toESM(require("@mui/material/TableBody"));
@@ -3173,7 +3187,10 @@ var init_ManageTeamDialog = __esm({
3173
3187
  onSaveKnowledgeBases,
3174
3188
  canManageMcpServers,
3175
3189
  mcpServers,
3176
- onSaveMcpServers
3190
+ onSaveMcpServers,
3191
+ membersOnly,
3192
+ currentUserId,
3193
+ memberManagerRoles
3177
3194
  }) => {
3178
3195
  const [alias, setAlias] = (0, import_react7.useState)("");
3179
3196
  const [models, setModels] = (0, import_react7.useState)([]);
@@ -3198,7 +3215,7 @@ var init_ManageTeamDialog = __esm({
3198
3215
  const hideBudgetForManagers = config?.display?.hideTeamBudgetForManagers ?? false;
3199
3216
  const budgetWriteOnly = hideBudgetForManagers && mode === "edit";
3200
3217
  const members = (0, import_react7.useMemo)(() => team?.members_with_roles ?? [], [team]);
3201
- const showMembers = mode === "edit" && !!canManageMembers;
3218
+ const showMembers = mode === "edit" && !!canManageMembers || membersOnly;
3202
3219
  const catalogApi = (0, import_core_plugin_api4.useApi)(import_plugin_catalog_react.catalogApiRef);
3203
3220
  const [searchUsers, setSearchUsers] = (0, import_react7.useState)([]);
3204
3221
  const [usersLoading, setUsersLoading] = (0, import_react7.useState)(false);
@@ -3347,7 +3364,7 @@ var init_ManageTeamDialog = __esm({
3347
3364
  const handleAddMember = async () => {
3348
3365
  if (!onAddMember || !memberRef.trim()) return;
3349
3366
  setMemberError(null);
3350
- const budget = memberBudget ? parseFloat(memberBudget) : void 0;
3367
+ const budget = !membersOnly && memberBudget ? parseFloat(memberBudget) : void 0;
3351
3368
  if (budget !== void 0 && (isNaN(budget) || budget <= 0)) {
3352
3369
  setMemberError("Max budget in team must be a positive number");
3353
3370
  return;
@@ -3408,72 +3425,95 @@ var init_ManageTeamDialog = __esm({
3408
3425
  const showMcpServers = mode === "edit" && !!canManageMcpServers;
3409
3426
  const mcpOptions = (mcpServers ?? []).map((s) => s.id);
3410
3427
  const mcpLabel = (id) => (mcpServers ?? []).find((s) => s.id === id)?.name ?? id;
3428
+ const isRemovalDisabled = (memberUserId, memberRole) => {
3429
+ if (!membersOnly) return { disabled: false };
3430
+ if (memberUserId === currentUserId) {
3431
+ return { disabled: true, reason: "You can't remove yourself" };
3432
+ }
3433
+ if (memberManagerRoles?.includes(memberRole)) {
3434
+ return { disabled: true, reason: `Only platform admins can remove a team ${memberRole}` };
3435
+ }
3436
+ return { disabled: false };
3437
+ };
3438
+ const getDialogTitle = () => {
3439
+ if (membersOnly) {
3440
+ return `Manage members \u2014 ${team?.team_alias || "Untitled team"}`;
3441
+ }
3442
+ return mode === "create" ? "Create Team" : "Edit Team";
3443
+ };
3411
3444
  return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_Dialog3.default, { open, onClose, maxWidth: "sm", fullWidth: true, children: [
3412
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_DialogTitle3.default, { children: mode === "create" ? "Create Team" : "Edit Team" }),
3445
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_DialogTitle3.default, { children: getDialogTitle() }),
3413
3446
  /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_DialogContent3.default, { sx: { pt: 2, display: "flex", flexDirection: "column", gap: 2 }, children: [
3414
3447
  error && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Alert2.default, { severity: "error", children: error }),
3415
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3416
- import_TextField3.default,
3417
- {
3418
- label: "Team Alias",
3419
- value: alias,
3420
- onChange: (e) => setAlias(e.target.value),
3421
- disabled: submitting,
3422
- fullWidth: true
3423
- }
3424
- ),
3425
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3426
- import_Autocomplete2.default,
3427
- {
3428
- multiple: true,
3429
- options: modelNames,
3430
- value: models,
3431
- onChange: (_, newValue) => setModels(newValue),
3432
- disabled: submitting,
3433
- renderInput: (params) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3434
- import_TextField3.default,
3435
- {
3436
- ...params,
3437
- label: "Models",
3438
- required: true,
3439
- helperText: "At least one model is required here \u2014 this delegated flow only grants access to models your admin has allow-listed for team creation, not every proxy model. (Teams with no model restriction shown elsewhere were configured directly in LiteLLM.)"
3440
- }
3441
- )
3442
- }
3443
- ),
3444
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3445
- import_TextField3.default,
3446
- {
3447
- label: budgetWriteOnly ? "New Max Budget ($)" : "Max Budget ($)",
3448
- type: "number",
3449
- value: maxBudget,
3450
- onChange: (e) => setMaxBudget(e.target.value),
3451
- disabled: submitting || unlimited,
3452
- inputProps: { min: 0, max: maxBudgetCeiling },
3453
- helperText: budgetWriteOnly ? "Current budget is hidden \u2014 leave blank to keep it, or enter a new value" : `Maximum: $${maxBudgetCeiling}`,
3454
- fullWidth: true
3455
- }
3456
- ),
3457
- allowUnlimitedBudget && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3458
- import_FormControlLabel2.default,
3459
- {
3460
- control: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Checkbox2.default, { checked: unlimited, onChange: (e) => setUnlimited(e.target.checked), disabled: submitting }),
3461
- label: "Unlimited Budget"
3462
- }
3463
- ),
3464
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3465
- import_TextField3.default,
3466
- {
3467
- select: true,
3468
- label: "Budget Duration",
3469
- value: budgetDuration,
3470
- onChange: (e) => setBudgetDuration(e.target.value),
3471
- disabled: submitting || unlimited,
3472
- helperText: "Spend-reset period for the team budget",
3473
- fullWidth: true,
3474
- children: durationOptions.map((o) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_MenuItem2.default, { value: o.value, children: o.label }, o.value))
3475
- }
3476
- ),
3448
+ membersOnly && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_Alert2.default, { severity: "info", children: [
3449
+ "As a team ",
3450
+ team?.members_with_roles?.find((m) => m.user_id === currentUserId)?.role || "member",
3451
+ " you can add or remove members. Budgets and models are managed by your platform admins."
3452
+ ] }),
3453
+ !membersOnly && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
3454
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3455
+ import_TextField3.default,
3456
+ {
3457
+ label: "Team Alias",
3458
+ value: alias,
3459
+ onChange: (e) => setAlias(e.target.value),
3460
+ disabled: submitting,
3461
+ fullWidth: true
3462
+ }
3463
+ ),
3464
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3465
+ import_Autocomplete2.default,
3466
+ {
3467
+ multiple: true,
3468
+ options: modelNames,
3469
+ value: models,
3470
+ onChange: (_, newValue) => setModels(newValue),
3471
+ disabled: submitting,
3472
+ renderInput: (params) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3473
+ import_TextField3.default,
3474
+ {
3475
+ ...params,
3476
+ label: "Models",
3477
+ required: true,
3478
+ helperText: "At least one model is required here \u2014 this delegated flow only grants access to models your admin has allow-listed for team creation, not every proxy model. (Teams with no model restriction shown elsewhere were configured directly in LiteLLM.)"
3479
+ }
3480
+ )
3481
+ }
3482
+ ),
3483
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3484
+ import_TextField3.default,
3485
+ {
3486
+ label: budgetWriteOnly ? "New Max Budget ($)" : "Max Budget ($)",
3487
+ type: "number",
3488
+ value: maxBudget,
3489
+ onChange: (e) => setMaxBudget(e.target.value),
3490
+ disabled: submitting || unlimited,
3491
+ inputProps: { min: 0, max: maxBudgetCeiling },
3492
+ helperText: budgetWriteOnly ? "Current budget is hidden \u2014 leave blank to keep it, or enter a new value" : `Maximum: $${maxBudgetCeiling}`,
3493
+ fullWidth: true
3494
+ }
3495
+ ),
3496
+ allowUnlimitedBudget && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3497
+ import_FormControlLabel2.default,
3498
+ {
3499
+ control: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Checkbox2.default, { checked: unlimited, onChange: (e) => setUnlimited(e.target.checked), disabled: submitting }),
3500
+ label: "Unlimited Budget"
3501
+ }
3502
+ ),
3503
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3504
+ import_TextField3.default,
3505
+ {
3506
+ select: true,
3507
+ label: "Budget Duration",
3508
+ value: budgetDuration,
3509
+ onChange: (e) => setBudgetDuration(e.target.value),
3510
+ disabled: submitting || unlimited,
3511
+ helperText: "Spend-reset period for the team budget",
3512
+ fullWidth: true,
3513
+ children: durationOptions.map((o) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_MenuItem2.default, { value: o.value, children: o.label }, o.value))
3514
+ }
3515
+ )
3516
+ ] }),
3477
3517
  showMembers && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_Box10.default, { children: [
3478
3518
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Divider2.default, { sx: { my: 1 } }),
3479
3519
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Typography8.default, { variant: "subtitle2", sx: { mb: 1 }, children: "Members" }),
@@ -3505,17 +3545,20 @@ var init_ManageTeamDialog = __esm({
3505
3545
  }
3506
3546
  ) }),
3507
3547
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_TableCell2.default, { align: "right", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(StatusPill, { label: m.role, tone: m.role === "admin" ? "accent" : "neutral", dot: false }) }),
3508
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_TableCell2.default, { align: "right", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3509
- import_IconButton4.default,
3510
- {
3511
- edge: "end",
3512
- size: "small",
3513
- "aria-label": `remove ${m.user_id}`,
3514
- disabled: memberBusy,
3515
- onClick: () => handleRemoveMember(m.user_id),
3516
- children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_icons_material5.Delete, { fontSize: "small" })
3517
- }
3518
- ) })
3548
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_TableCell2.default, { align: "right", children: (() => {
3549
+ const { disabled, reason } = isRemovalDisabled(m.user_id, m.role);
3550
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Tooltip4.default, { title: reason ?? "", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3551
+ import_IconButton4.default,
3552
+ {
3553
+ edge: "end",
3554
+ size: "small",
3555
+ "aria-label": `remove ${m.user_id}`,
3556
+ disabled: memberBusy || disabled,
3557
+ onClick: () => handleRemoveMember(m.user_id),
3558
+ children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_icons_material5.Delete, { fontSize: "small" })
3559
+ }
3560
+ ) }) });
3561
+ })() })
3519
3562
  ] }, m.user_id)) })
3520
3563
  ] }) }),
3521
3564
  /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_Box10.default, { sx: { display: "flex", gap: 1, alignItems: "flex-start", flexWrap: "wrap" }, children: [
@@ -3563,7 +3606,7 @@ var init_ManageTeamDialog = __esm({
3563
3606
  )
3564
3607
  }
3565
3608
  ),
3566
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3609
+ !membersOnly && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3567
3610
  import_TextField3.default,
3568
3611
  {
3569
3612
  label: "Max budget in team ($)",
@@ -3587,7 +3630,7 @@ var init_ManageTeamDialog = __esm({
3587
3630
  )
3588
3631
  ] })
3589
3632
  ] }),
3590
- showKnowledgeBases && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_Box10.default, { children: [
3633
+ !membersOnly && showKnowledgeBases && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_Box10.default, { children: [
3591
3634
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Divider2.default, { sx: { my: 1 } }),
3592
3635
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Typography8.default, { variant: "subtitle2", sx: { mb: 1 }, children: "Knowledge Bases" }),
3593
3636
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Typography8.default, { variant: "body2", color: "text.secondary", sx: { mb: 1 }, children: "Attaching a knowledge base exposes its documents to every key in this team." }),
@@ -3614,7 +3657,7 @@ var init_ManageTeamDialog = __esm({
3614
3657
  }
3615
3658
  ) })
3616
3659
  ] }),
3617
- showMcpServers && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_Box10.default, { children: [
3660
+ !membersOnly && showMcpServers && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_Box10.default, { children: [
3618
3661
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Divider2.default, { sx: { my: 1 } }),
3619
3662
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Typography8.default, { variant: "subtitle2", sx: { mb: 1 }, children: "MCP Servers" }),
3620
3663
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Typography8.default, { variant: "body2", color: "text.secondary", sx: { mb: 1 }, children: "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." }),
@@ -3643,8 +3686,8 @@ var init_ManageTeamDialog = __esm({
3643
3686
  ] })
3644
3687
  ] }),
3645
3688
  /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_DialogActions3.default, { children: [
3646
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Button4.default, { onClick: onClose, disabled: submitting, children: "Cancel" }),
3647
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Button4.default, { onClick: handleSubmit, disabled: submitting, variant: "contained", children: submitting ? "Saving..." : "Save" })
3689
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Button4.default, { onClick: onClose, disabled: submitting, children: membersOnly ? "Done" : "Cancel" }),
3690
+ !membersOnly && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Button4.default, { onClick: handleSubmit, disabled: submitting, variant: "contained", children: submitting ? "Saving..." : "Save" })
3648
3691
  ] })
3649
3692
  ] });
3650
3693
  };
@@ -4880,7 +4923,7 @@ function budgetStatValue(hidden, budget) {
4880
4923
  if (budget > 0) return fmtUsd2(budget);
4881
4924
  return "Unlimited";
4882
4925
  }
4883
- var import_react10, import_Paper8, import_Box14, import_Typography12, import_Skeleton5, import_Divider3, import_Table4, import_TableBody4, import_TableCell4, import_TableHead4, import_TableRow4, import_TableContainer4, import_Collapse2, import_IconButton6, import_Button6, import_CircularProgress4, import_styles10, import_icons_material7, import_recharts2, import_jsx_runtime15, fmtUsd2, TeamCard, TeamUsage;
4926
+ var import_react10, import_Paper8, import_Box14, import_Typography12, import_Skeleton5, import_Divider3, import_Table4, import_TableBody4, import_TableCell4, import_TableHead4, import_TableRow4, import_TableContainer4, import_Collapse2, import_IconButton6, import_Button6, import_CircularProgress4, import_styles10, import_Tooltip5, import_icons_material7, import_recharts2, import_jsx_runtime15, fmtUsd2, TeamCard, TeamUsage;
4884
4927
  var init_TeamUsage = __esm({
4885
4928
  "src/components/TeamUsage.tsx"() {
4886
4929
  "use strict";
@@ -4901,12 +4944,13 @@ var init_TeamUsage = __esm({
4901
4944
  import_Button6 = __toESM(require("@mui/material/Button"));
4902
4945
  import_CircularProgress4 = __toESM(require("@mui/material/CircularProgress"));
4903
4946
  import_styles10 = require("@mui/material/styles");
4947
+ import_Tooltip5 = __toESM(require("@mui/material/Tooltip"));
4904
4948
  import_icons_material7 = require("@mui/icons-material");
4905
4949
  import_recharts2 = require("recharts");
4906
4950
  init_ui();
4907
4951
  import_jsx_runtime15 = require("react/jsx-runtime");
4908
4952
  fmtUsd2 = (n) => `$${(n ?? 0).toFixed(2)}`;
4909
- TeamCard = ({ team, usage, usageLoading, canManage, onEditTeam, onExpand }) => {
4953
+ TeamCard = ({ team, usage, usageLoading, canManage, onEditTeam, canManageMembers, onManageMembers, onExpand }) => {
4910
4954
  const [expanded, setExpanded] = (0, import_react10.useState)(false);
4911
4955
  const chart = useChartTheme();
4912
4956
  const handleExpandChange = (newExpanded) => {
@@ -5016,6 +5060,17 @@ var init_TeamUsage = __esm({
5016
5060
  children: "Edit"
5017
5061
  }
5018
5062
  ),
5063
+ canManageMembers && onManageMembers && !canManage && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_Tooltip5.default, { title: "Add or remove members of this team", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
5064
+ import_Button6.default,
5065
+ {
5066
+ size: "small",
5067
+ variant: "text",
5068
+ startIcon: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_icons_material7.GroupAddOutlined, {}),
5069
+ onClick: () => onManageMembers(team),
5070
+ sx: { textTransform: "none" },
5071
+ children: "Manage members"
5072
+ }
5073
+ ) }),
5019
5074
  /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
5020
5075
  import_IconButton6.default,
5021
5076
  {
@@ -5146,9 +5201,23 @@ var init_TeamUsage = __esm({
5146
5201
  onEditTeam,
5147
5202
  canCreate,
5148
5203
  onCreateTeam,
5149
- onTeamExpand
5204
+ onTeamExpand,
5205
+ canManageMembers,
5206
+ onManageMembers,
5207
+ readOnly
5150
5208
  }) => {
5151
- const createAction = canCreate && onCreateTeam ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
5209
+ const readOnlyBadge = /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_Tooltip5.default, { title: "Teams and memberships are managed in your identity provider. Changes made here would be overwritten, so editing is disabled.", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
5210
+ import_Box14.default,
5211
+ {
5212
+ component: "span",
5213
+ sx: { display: "inline-flex", alignItems: "center", gap: 0.5, color: "text.secondary", fontSize: 12.5 },
5214
+ children: [
5215
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_icons_material7.LockOutlined, { sx: { fontSize: 15 } }),
5216
+ "Synced from identity provider"
5217
+ ]
5218
+ }
5219
+ ) });
5220
+ const createButton = canCreate && onCreateTeam ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
5152
5221
  import_Button6.default,
5153
5222
  {
5154
5223
  variant: "contained",
@@ -5159,6 +5228,7 @@ var init_TeamUsage = __esm({
5159
5228
  children: "Create Team"
5160
5229
  }
5161
5230
  ) : void 0;
5231
+ const createAction = readOnly ? readOnlyBadge : createButton;
5162
5232
  if (loading) {
5163
5233
  return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(SectionCard, { title: "Teams", actions: createAction, children: [
5164
5234
  /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_Skeleton5.default, { variant: "rounded", height: 120, sx: { mb: 2 } }),
@@ -5188,6 +5258,8 @@ var init_TeamUsage = __esm({
5188
5258
  usageLoading: getTeamUsageLoading(team.team_id),
5189
5259
  canManage,
5190
5260
  onEditTeam,
5261
+ canManageMembers: canManageMembers?.(team),
5262
+ onManageMembers,
5191
5263
  onExpand: onTeamExpand
5192
5264
  },
5193
5265
  team.team_id
@@ -5216,7 +5288,7 @@ function fmtTokens(n) {
5216
5288
  if (n >= 1e3) return `${(n / 1e3).toFixed(0)}K`;
5217
5289
  return String(n);
5218
5290
  }
5219
- var import_react11, import_Box15, import_Table5, import_TableBody5, import_TableCell5, import_TableContainer5, import_TableHead5, import_TableRow5, import_TableSortLabel2, import_Typography13, import_MenuItem4, import_Select, import_FormControl, import_InputLabel, import_TextField5, import_InputAdornment3, import_icons_material8, import_ContentCopy, import_Tooltip4, import_Snackbar, import_Alert4, import_styles11, import_jsx_runtime16, ALL_PROXY_MODELS, ModelsTable;
5291
+ var import_react11, import_Box15, import_Table5, import_TableBody5, import_TableCell5, import_TableContainer5, import_TableHead5, import_TableRow5, import_TableSortLabel2, import_Typography13, import_MenuItem4, import_Select, import_FormControl, import_InputLabel, import_TextField5, import_InputAdornment3, import_icons_material8, import_ContentCopy, import_Tooltip6, import_Snackbar, import_Alert4, import_styles11, import_jsx_runtime16, ALL_PROXY_MODELS, ModelsTable;
5220
5292
  var init_ModelsTable = __esm({
5221
5293
  "src/components/ModelsTable.tsx"() {
5222
5294
  "use strict";
@@ -5238,7 +5310,7 @@ var init_ModelsTable = __esm({
5238
5310
  import_InputAdornment3 = __toESM(require("@mui/material/InputAdornment"));
5239
5311
  import_icons_material8 = require("@mui/icons-material");
5240
5312
  import_ContentCopy = __toESM(require("@mui/icons-material/ContentCopy"));
5241
- import_Tooltip4 = __toESM(require("@mui/material/Tooltip"));
5313
+ import_Tooltip6 = __toESM(require("@mui/material/Tooltip"));
5242
5314
  import_Snackbar = __toESM(require("@mui/material/Snackbar"));
5243
5315
  import_Alert4 = __toESM(require("@mui/material/Alert"));
5244
5316
  init_ui();
@@ -5401,7 +5473,7 @@ var init_ModelsTable = __esm({
5401
5473
  ] }) }),
5402
5474
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_TableBody5.default, { children: filteredModels.map((m) => /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_TableRow5.default, { hover: true, children: [
5403
5475
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_TableCell5.default, { children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_Box15.default, { sx: { display: "flex", alignItems: "center", gap: 0.5 }, children: [
5404
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_Tooltip4.default, { title: m.model_name, placement: "top", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
5476
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_Tooltip6.default, { title: m.model_name, placement: "top", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
5405
5477
  "span",
5406
5478
  {
5407
5479
  role: "button",
@@ -5413,7 +5485,7 @@ var init_ModelsTable = __esm({
5413
5485
  children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(TagChip, { label: m.model_name, title: "Copy model ID" })
5414
5486
  }
5415
5487
  ) }),
5416
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_Tooltip4.default, { title: "Copy model ID", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
5488
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_Tooltip6.default, { title: "Copy model ID", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
5417
5489
  import_Box15.default,
5418
5490
  {
5419
5491
  component: "span",
@@ -5823,6 +5895,19 @@ var init_feedback = __esm({
5823
5895
  }
5824
5896
  });
5825
5897
 
5898
+ // src/teamMemberManager.ts
5899
+ function isTeamMemberManager(team, userId, memberManagerRoles, readOnly) {
5900
+ if (!team || !userId || !memberManagerRoles || memberManagerRoles.length === 0 || readOnly) {
5901
+ return false;
5902
+ }
5903
+ return team.members_with_roles?.some((m) => m.user_id === userId && memberManagerRoles.includes(m.role)) ?? false;
5904
+ }
5905
+ var init_teamMemberManager = __esm({
5906
+ "src/teamMemberManager.ts"() {
5907
+ "use strict";
5908
+ }
5909
+ });
5910
+
5826
5911
  // src/unprovisionedCopy.ts
5827
5912
  function getUnprovisionedTitle() {
5828
5913
  return "Your LiteLLM account isn't set up yet";
@@ -5899,6 +5984,7 @@ var init_LiteLLMPage = __esm({
5899
5984
  init_dates();
5900
5985
  init_permissions();
5901
5986
  init_feedback();
5987
+ init_teamMemberManager();
5902
5988
  init_unprovisionedCopy();
5903
5989
  import_jsx_runtime18 = require("react/jsx-runtime");
5904
5990
  PERIOD_LS_KEY2 = "litellm_usage_period";
@@ -6073,6 +6159,7 @@ var init_LiteLLMPage = __esm({
6073
6159
  ]);
6074
6160
  return allModels.filter((m) => allowed.has(m.model_name));
6075
6161
  }, [allModels, userInfo, teams]);
6162
+ const getConfig = (0, import_react13.useCallback)(() => api.getConfig(), [api]);
6076
6163
  const handleGenerateKey = (0, import_react13.useCallback)(
6077
6164
  async (request) => {
6078
6165
  try {
@@ -6344,6 +6431,8 @@ var init_LiteLLMPage = __esm({
6344
6431
  for (const t of managedTeams ?? []) teamsById.set(t.team_id, t);
6345
6432
  for (const t of teams ?? []) teamsById.set(t.team_id, t);
6346
6433
  const visibleTeams = Array.from(teamsById.values());
6434
+ const readOnly = liteLlmConfig?.teamManagement?.readOnly ?? false;
6435
+ const memberManagerRoles = liteLlmConfig?.teamManagement?.memberManagerRoles ?? [];
6347
6436
  return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
6348
6437
  TeamUsage,
6349
6438
  {
@@ -6351,10 +6440,13 @@ var init_LiteLLMPage = __esm({
6351
6440
  loading: teamsLoading,
6352
6441
  getTeamUsage: (teamId) => teamUsageCache[teamId] ?? null,
6353
6442
  getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false,
6354
- canManage: teamMgmtEnabled && canManageTeam,
6443
+ canManage: teamMgmtEnabled && canManageTeam && !readOnly,
6355
6444
  onEditTeam: (t) => setManageTeam({ mode: "edit", team: t }),
6356
- canCreate: teamMgmtEnabled && canCreateTeam,
6445
+ canCreate: teamMgmtEnabled && canCreateTeam && !readOnly,
6357
6446
  onCreateTeam: () => setManageTeam({ mode: "create" }),
6447
+ canManageMembers: (t) => teamMgmtEnabled && canManageMembers && isTeamMemberManager(t, userInfo?.user_id, memberManagerRoles, readOnly),
6448
+ onManageMembers: (t) => setManageTeam({ mode: "members", team: t }),
6449
+ readOnly,
6358
6450
  onTeamExpand: (team) => loadTeamUsage(team.team_id)
6359
6451
  }
6360
6452
  );
@@ -6398,7 +6490,7 @@ var init_LiteLLMPage = __esm({
6398
6490
  onCreateKey: handleGenerateKey,
6399
6491
  onUpdateKey: handleUpdateKey,
6400
6492
  onResetKeySpend: handleResetKeySpend,
6401
- onGetConfig: () => api.getConfig()
6493
+ onGetConfig: getConfig
6402
6494
  }
6403
6495
  ),
6404
6496
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
@@ -6406,10 +6498,13 @@ var init_LiteLLMPage = __esm({
6406
6498
  {
6407
6499
  open: !!manageTeam,
6408
6500
  onClose: () => setManageTeam(null),
6409
- mode: manageTeam?.mode ?? "create",
6501
+ mode: manageTeam?.mode === "members" ? "edit" : manageTeam?.mode ?? "create",
6410
6502
  team: manageTeam?.team,
6411
6503
  allModels: allModels ?? [],
6412
6504
  config: liteLlmConfig,
6505
+ membersOnly: manageTeam?.mode === "members",
6506
+ currentUserId: userInfo?.user_id,
6507
+ memberManagerRoles: liteLlmConfig?.teamManagement?.memberManagerRoles,
6413
6508
  onSubmit: async (payload) => {
6414
6509
  try {
6415
6510
  if (manageTeam?.mode === "edit" && manageTeam.team) {
@@ -6430,7 +6525,7 @@ var init_LiteLLMPage = __esm({
6430
6525
  if (alert) alertApi.post(alert);
6431
6526
  }
6432
6527
  },
6433
- canManageMembers: teamMgmtEnabled && canManageMembers,
6528
+ canManageMembers: teamMgmtEnabled && canManageMembers && !liteLlmConfig?.teamManagement?.readOnly,
6434
6529
  onAddMember: async (userEntityRef, maxBudgetInTeam) => {
6435
6530
  if (!manageTeam?.team) return;
6436
6531
  const updated = await api.addTeamMember(manageTeam.team.team_id, {