@acarmisc/backstage-plugin-litellm 0.31.1 → 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
@@ -3127,7 +3127,7 @@ var init_KeyFormDialog = __esm({
3127
3127
  });
3128
3128
 
3129
3129
  // src/components/ManageTeamDialog.tsx
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_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;
3131
3131
  var init_ManageTeamDialog = __esm({
3132
3132
  "src/components/ManageTeamDialog.tsx"() {
3133
3133
  "use strict";
@@ -3148,6 +3148,7 @@ var init_ManageTeamDialog = __esm({
3148
3148
  import_Divider2 = __toESM(require("@mui/material/Divider"));
3149
3149
  import_Typography8 = __toESM(require("@mui/material/Typography"));
3150
3150
  import_IconButton4 = __toESM(require("@mui/material/IconButton"));
3151
+ import_Tooltip4 = __toESM(require("@mui/material/Tooltip"));
3151
3152
  import_Paper4 = __toESM(require("@mui/material/Paper"));
3152
3153
  import_Table2 = __toESM(require("@mui/material/Table"));
3153
3154
  import_TableBody2 = __toESM(require("@mui/material/TableBody"));
@@ -3186,7 +3187,10 @@ var init_ManageTeamDialog = __esm({
3186
3187
  onSaveKnowledgeBases,
3187
3188
  canManageMcpServers,
3188
3189
  mcpServers,
3189
- onSaveMcpServers
3190
+ onSaveMcpServers,
3191
+ membersOnly,
3192
+ currentUserId,
3193
+ memberManagerRoles
3190
3194
  }) => {
3191
3195
  const [alias, setAlias] = (0, import_react7.useState)("");
3192
3196
  const [models, setModels] = (0, import_react7.useState)([]);
@@ -3211,7 +3215,7 @@ var init_ManageTeamDialog = __esm({
3211
3215
  const hideBudgetForManagers = config?.display?.hideTeamBudgetForManagers ?? false;
3212
3216
  const budgetWriteOnly = hideBudgetForManagers && mode === "edit";
3213
3217
  const members = (0, import_react7.useMemo)(() => team?.members_with_roles ?? [], [team]);
3214
- const showMembers = mode === "edit" && !!canManageMembers;
3218
+ const showMembers = mode === "edit" && !!canManageMembers || membersOnly;
3215
3219
  const catalogApi = (0, import_core_plugin_api4.useApi)(import_plugin_catalog_react.catalogApiRef);
3216
3220
  const [searchUsers, setSearchUsers] = (0, import_react7.useState)([]);
3217
3221
  const [usersLoading, setUsersLoading] = (0, import_react7.useState)(false);
@@ -3360,7 +3364,7 @@ var init_ManageTeamDialog = __esm({
3360
3364
  const handleAddMember = async () => {
3361
3365
  if (!onAddMember || !memberRef.trim()) return;
3362
3366
  setMemberError(null);
3363
- const budget = memberBudget ? parseFloat(memberBudget) : void 0;
3367
+ const budget = !membersOnly && memberBudget ? parseFloat(memberBudget) : void 0;
3364
3368
  if (budget !== void 0 && (isNaN(budget) || budget <= 0)) {
3365
3369
  setMemberError("Max budget in team must be a positive number");
3366
3370
  return;
@@ -3421,72 +3425,95 @@ var init_ManageTeamDialog = __esm({
3421
3425
  const showMcpServers = mode === "edit" && !!canManageMcpServers;
3422
3426
  const mcpOptions = (mcpServers ?? []).map((s) => s.id);
3423
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
+ };
3424
3444
  return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_Dialog3.default, { open, onClose, maxWidth: "sm", fullWidth: true, children: [
3425
- /* @__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() }),
3426
3446
  /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_DialogContent3.default, { sx: { pt: 2, display: "flex", flexDirection: "column", gap: 2 }, children: [
3427
3447
  error && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Alert2.default, { severity: "error", children: error }),
3428
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3429
- import_TextField3.default,
3430
- {
3431
- label: "Team Alias",
3432
- value: alias,
3433
- onChange: (e) => setAlias(e.target.value),
3434
- disabled: submitting,
3435
- fullWidth: true
3436
- }
3437
- ),
3438
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3439
- import_Autocomplete2.default,
3440
- {
3441
- multiple: true,
3442
- options: modelNames,
3443
- value: models,
3444
- onChange: (_, newValue) => setModels(newValue),
3445
- disabled: submitting,
3446
- renderInput: (params) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3447
- import_TextField3.default,
3448
- {
3449
- ...params,
3450
- label: "Models",
3451
- required: true,
3452
- 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.)"
3453
- }
3454
- )
3455
- }
3456
- ),
3457
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3458
- import_TextField3.default,
3459
- {
3460
- label: budgetWriteOnly ? "New Max Budget ($)" : "Max Budget ($)",
3461
- type: "number",
3462
- value: maxBudget,
3463
- onChange: (e) => setMaxBudget(e.target.value),
3464
- disabled: submitting || unlimited,
3465
- inputProps: { min: 0, max: maxBudgetCeiling },
3466
- helperText: budgetWriteOnly ? "Current budget is hidden \u2014 leave blank to keep it, or enter a new value" : `Maximum: $${maxBudgetCeiling}`,
3467
- fullWidth: true
3468
- }
3469
- ),
3470
- allowUnlimitedBudget && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3471
- import_FormControlLabel2.default,
3472
- {
3473
- control: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Checkbox2.default, { checked: unlimited, onChange: (e) => setUnlimited(e.target.checked), disabled: submitting }),
3474
- label: "Unlimited Budget"
3475
- }
3476
- ),
3477
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3478
- import_TextField3.default,
3479
- {
3480
- select: true,
3481
- label: "Budget Duration",
3482
- value: budgetDuration,
3483
- onChange: (e) => setBudgetDuration(e.target.value),
3484
- disabled: submitting || unlimited,
3485
- helperText: "Spend-reset period for the team budget",
3486
- fullWidth: true,
3487
- children: durationOptions.map((o) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_MenuItem2.default, { value: o.value, children: o.label }, o.value))
3488
- }
3489
- ),
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
+ ] }),
3490
3517
  showMembers && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_Box10.default, { children: [
3491
3518
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Divider2.default, { sx: { my: 1 } }),
3492
3519
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Typography8.default, { variant: "subtitle2", sx: { mb: 1 }, children: "Members" }),
@@ -3518,17 +3545,20 @@ var init_ManageTeamDialog = __esm({
3518
3545
  }
3519
3546
  ) }),
3520
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 }) }),
3521
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_TableCell2.default, { align: "right", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3522
- import_IconButton4.default,
3523
- {
3524
- edge: "end",
3525
- size: "small",
3526
- "aria-label": `remove ${m.user_id}`,
3527
- disabled: memberBusy,
3528
- onClick: () => handleRemoveMember(m.user_id),
3529
- children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_icons_material5.Delete, { fontSize: "small" })
3530
- }
3531
- ) })
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
+ })() })
3532
3562
  ] }, m.user_id)) })
3533
3563
  ] }) }),
3534
3564
  /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_Box10.default, { sx: { display: "flex", gap: 1, alignItems: "flex-start", flexWrap: "wrap" }, children: [
@@ -3576,7 +3606,7 @@ var init_ManageTeamDialog = __esm({
3576
3606
  )
3577
3607
  }
3578
3608
  ),
3579
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3609
+ !membersOnly && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3580
3610
  import_TextField3.default,
3581
3611
  {
3582
3612
  label: "Max budget in team ($)",
@@ -3600,7 +3630,7 @@ var init_ManageTeamDialog = __esm({
3600
3630
  )
3601
3631
  ] })
3602
3632
  ] }),
3603
- showKnowledgeBases && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_Box10.default, { children: [
3633
+ !membersOnly && showKnowledgeBases && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_Box10.default, { children: [
3604
3634
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Divider2.default, { sx: { my: 1 } }),
3605
3635
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Typography8.default, { variant: "subtitle2", sx: { mb: 1 }, children: "Knowledge Bases" }),
3606
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." }),
@@ -3627,7 +3657,7 @@ var init_ManageTeamDialog = __esm({
3627
3657
  }
3628
3658
  ) })
3629
3659
  ] }),
3630
- showMcpServers && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_Box10.default, { children: [
3660
+ !membersOnly && showMcpServers && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_Box10.default, { children: [
3631
3661
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Divider2.default, { sx: { my: 1 } }),
3632
3662
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Typography8.default, { variant: "subtitle2", sx: { mb: 1 }, children: "MCP Servers" }),
3633
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." }),
@@ -3656,8 +3686,8 @@ var init_ManageTeamDialog = __esm({
3656
3686
  ] })
3657
3687
  ] }),
3658
3688
  /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_DialogActions3.default, { children: [
3659
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Button4.default, { onClick: onClose, disabled: submitting, children: "Cancel" }),
3660
- /* @__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" })
3661
3691
  ] })
3662
3692
  ] });
3663
3693
  };
@@ -4893,7 +4923,7 @@ function budgetStatValue(hidden, budget) {
4893
4923
  if (budget > 0) return fmtUsd2(budget);
4894
4924
  return "Unlimited";
4895
4925
  }
4896
- 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;
4897
4927
  var init_TeamUsage = __esm({
4898
4928
  "src/components/TeamUsage.tsx"() {
4899
4929
  "use strict";
@@ -4914,12 +4944,13 @@ var init_TeamUsage = __esm({
4914
4944
  import_Button6 = __toESM(require("@mui/material/Button"));
4915
4945
  import_CircularProgress4 = __toESM(require("@mui/material/CircularProgress"));
4916
4946
  import_styles10 = require("@mui/material/styles");
4947
+ import_Tooltip5 = __toESM(require("@mui/material/Tooltip"));
4917
4948
  import_icons_material7 = require("@mui/icons-material");
4918
4949
  import_recharts2 = require("recharts");
4919
4950
  init_ui();
4920
4951
  import_jsx_runtime15 = require("react/jsx-runtime");
4921
4952
  fmtUsd2 = (n) => `$${(n ?? 0).toFixed(2)}`;
4922
- TeamCard = ({ team, usage, usageLoading, canManage, onEditTeam, onExpand }) => {
4953
+ TeamCard = ({ team, usage, usageLoading, canManage, onEditTeam, canManageMembers, onManageMembers, onExpand }) => {
4923
4954
  const [expanded, setExpanded] = (0, import_react10.useState)(false);
4924
4955
  const chart = useChartTheme();
4925
4956
  const handleExpandChange = (newExpanded) => {
@@ -5029,6 +5060,17 @@ var init_TeamUsage = __esm({
5029
5060
  children: "Edit"
5030
5061
  }
5031
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
+ ) }),
5032
5074
  /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
5033
5075
  import_IconButton6.default,
5034
5076
  {
@@ -5159,9 +5201,23 @@ var init_TeamUsage = __esm({
5159
5201
  onEditTeam,
5160
5202
  canCreate,
5161
5203
  onCreateTeam,
5162
- onTeamExpand
5204
+ onTeamExpand,
5205
+ canManageMembers,
5206
+ onManageMembers,
5207
+ readOnly
5163
5208
  }) => {
5164
- 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)(
5165
5221
  import_Button6.default,
5166
5222
  {
5167
5223
  variant: "contained",
@@ -5172,6 +5228,7 @@ var init_TeamUsage = __esm({
5172
5228
  children: "Create Team"
5173
5229
  }
5174
5230
  ) : void 0;
5231
+ const createAction = readOnly ? readOnlyBadge : createButton;
5175
5232
  if (loading) {
5176
5233
  return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(SectionCard, { title: "Teams", actions: createAction, children: [
5177
5234
  /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_Skeleton5.default, { variant: "rounded", height: 120, sx: { mb: 2 } }),
@@ -5201,6 +5258,8 @@ var init_TeamUsage = __esm({
5201
5258
  usageLoading: getTeamUsageLoading(team.team_id),
5202
5259
  canManage,
5203
5260
  onEditTeam,
5261
+ canManageMembers: canManageMembers?.(team),
5262
+ onManageMembers,
5204
5263
  onExpand: onTeamExpand
5205
5264
  },
5206
5265
  team.team_id
@@ -5229,7 +5288,7 @@ function fmtTokens(n) {
5229
5288
  if (n >= 1e3) return `${(n / 1e3).toFixed(0)}K`;
5230
5289
  return String(n);
5231
5290
  }
5232
- 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;
5233
5292
  var init_ModelsTable = __esm({
5234
5293
  "src/components/ModelsTable.tsx"() {
5235
5294
  "use strict";
@@ -5251,7 +5310,7 @@ var init_ModelsTable = __esm({
5251
5310
  import_InputAdornment3 = __toESM(require("@mui/material/InputAdornment"));
5252
5311
  import_icons_material8 = require("@mui/icons-material");
5253
5312
  import_ContentCopy = __toESM(require("@mui/icons-material/ContentCopy"));
5254
- import_Tooltip4 = __toESM(require("@mui/material/Tooltip"));
5313
+ import_Tooltip6 = __toESM(require("@mui/material/Tooltip"));
5255
5314
  import_Snackbar = __toESM(require("@mui/material/Snackbar"));
5256
5315
  import_Alert4 = __toESM(require("@mui/material/Alert"));
5257
5316
  init_ui();
@@ -5414,7 +5473,7 @@ var init_ModelsTable = __esm({
5414
5473
  ] }) }),
5415
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: [
5416
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: [
5417
- /* @__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)(
5418
5477
  "span",
5419
5478
  {
5420
5479
  role: "button",
@@ -5426,7 +5485,7 @@ var init_ModelsTable = __esm({
5426
5485
  children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(TagChip, { label: m.model_name, title: "Copy model ID" })
5427
5486
  }
5428
5487
  ) }),
5429
- /* @__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)(
5430
5489
  import_Box15.default,
5431
5490
  {
5432
5491
  component: "span",
@@ -5836,6 +5895,19 @@ var init_feedback = __esm({
5836
5895
  }
5837
5896
  });
5838
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
+
5839
5911
  // src/unprovisionedCopy.ts
5840
5912
  function getUnprovisionedTitle() {
5841
5913
  return "Your LiteLLM account isn't set up yet";
@@ -5912,6 +5984,7 @@ var init_LiteLLMPage = __esm({
5912
5984
  init_dates();
5913
5985
  init_permissions();
5914
5986
  init_feedback();
5987
+ init_teamMemberManager();
5915
5988
  init_unprovisionedCopy();
5916
5989
  import_jsx_runtime18 = require("react/jsx-runtime");
5917
5990
  PERIOD_LS_KEY2 = "litellm_usage_period";
@@ -6358,6 +6431,8 @@ var init_LiteLLMPage = __esm({
6358
6431
  for (const t of managedTeams ?? []) teamsById.set(t.team_id, t);
6359
6432
  for (const t of teams ?? []) teamsById.set(t.team_id, t);
6360
6433
  const visibleTeams = Array.from(teamsById.values());
6434
+ const readOnly = liteLlmConfig?.teamManagement?.readOnly ?? false;
6435
+ const memberManagerRoles = liteLlmConfig?.teamManagement?.memberManagerRoles ?? [];
6361
6436
  return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
6362
6437
  TeamUsage,
6363
6438
  {
@@ -6365,10 +6440,13 @@ var init_LiteLLMPage = __esm({
6365
6440
  loading: teamsLoading,
6366
6441
  getTeamUsage: (teamId) => teamUsageCache[teamId] ?? null,
6367
6442
  getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false,
6368
- canManage: teamMgmtEnabled && canManageTeam,
6443
+ canManage: teamMgmtEnabled && canManageTeam && !readOnly,
6369
6444
  onEditTeam: (t) => setManageTeam({ mode: "edit", team: t }),
6370
- canCreate: teamMgmtEnabled && canCreateTeam,
6445
+ canCreate: teamMgmtEnabled && canCreateTeam && !readOnly,
6371
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,
6372
6450
  onTeamExpand: (team) => loadTeamUsage(team.team_id)
6373
6451
  }
6374
6452
  );
@@ -6420,10 +6498,13 @@ var init_LiteLLMPage = __esm({
6420
6498
  {
6421
6499
  open: !!manageTeam,
6422
6500
  onClose: () => setManageTeam(null),
6423
- mode: manageTeam?.mode ?? "create",
6501
+ mode: manageTeam?.mode === "members" ? "edit" : manageTeam?.mode ?? "create",
6424
6502
  team: manageTeam?.team,
6425
6503
  allModels: allModels ?? [],
6426
6504
  config: liteLlmConfig,
6505
+ membersOnly: manageTeam?.mode === "members",
6506
+ currentUserId: userInfo?.user_id,
6507
+ memberManagerRoles: liteLlmConfig?.teamManagement?.memberManagerRoles,
6427
6508
  onSubmit: async (payload) => {
6428
6509
  try {
6429
6510
  if (manageTeam?.mode === "edit" && manageTeam.team) {
@@ -6444,7 +6525,7 @@ var init_LiteLLMPage = __esm({
6444
6525
  if (alert) alertApi.post(alert);
6445
6526
  }
6446
6527
  },
6447
- canManageMembers: teamMgmtEnabled && canManageMembers,
6528
+ canManageMembers: teamMgmtEnabled && canManageMembers && !liteLlmConfig?.teamManagement?.readOnly,
6448
6529
  onAddMember: async (userEntityRef, maxBudgetInTeam) => {
6449
6530
  if (!manageTeam?.team) return;
6450
6531
  const updated = await api.addTeamMember(manageTeam.team.team_id, {