@acarmisc/backstage-plugin-litellm 0.31.1 → 0.33.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
@@ -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)([]);
@@ -3200,6 +3204,7 @@ var init_ManageTeamDialog = __esm({
3200
3204
  const [memberBudget, setMemberBudget] = (0, import_react7.useState)("");
3201
3205
  const [memberBusy, setMemberBusy] = (0, import_react7.useState)(false);
3202
3206
  const [memberError, setMemberError] = (0, import_react7.useState)(null);
3207
+ const [pendingMembers, setPendingMembers] = (0, import_react7.useState)([]);
3203
3208
  const [kbIds, setKbIds] = (0, import_react7.useState)([]);
3204
3209
  const [kbBusy, setKbBusy] = (0, import_react7.useState)(false);
3205
3210
  const [kbError, setKbError] = (0, import_react7.useState)(null);
@@ -3210,8 +3215,17 @@ var init_ManageTeamDialog = __esm({
3210
3215
  const maxBudgetCeiling = config?.teamManagement?.maxBudgetCeiling ?? FALLBACK_BUDGET_CEILING;
3211
3216
  const hideBudgetForManagers = config?.display?.hideTeamBudgetForManagers ?? false;
3212
3217
  const budgetWriteOnly = hideBudgetForManagers && mode === "edit";
3213
- const members = (0, import_react7.useMemo)(() => team?.members_with_roles ?? [], [team]);
3214
- const showMembers = mode === "edit" && !!canManageMembers;
3218
+ const creating = mode === "create";
3219
+ const members = (0, import_react7.useMemo)(
3220
+ () => creating ? pendingMembers.map((p) => ({
3221
+ user_id: p.userEntityRef,
3222
+ user_email: p.label,
3223
+ role: "user"
3224
+ })) : team?.members_with_roles ?? [],
3225
+ [creating, pendingMembers, team]
3226
+ );
3227
+ const showMembers = !!canManageMembers || !!membersOnly;
3228
+ const wide = showMembers && !membersOnly;
3215
3229
  const catalogApi = (0, import_core_plugin_api4.useApi)(import_plugin_catalog_react.catalogApiRef);
3216
3230
  const [searchUsers, setSearchUsers] = (0, import_react7.useState)([]);
3217
3231
  const [usersLoading, setUsersLoading] = (0, import_react7.useState)(false);
@@ -3256,8 +3270,8 @@ var init_ManageTeamDialog = __esm({
3256
3270
  email,
3257
3271
  label: email ? `${displayName} (${email})` : displayName
3258
3272
  };
3259
- }).filter((o) => !(o.email && existingEmails.has(o.email.toLowerCase()))).sort((a, b) => a.label.localeCompare(b.label));
3260
- }, [searchUsers, members]);
3273
+ }).filter((o) => !(o.email && existingEmails.has(o.email.toLowerCase()))).filter((o) => !pendingMembers.some((p) => p.userEntityRef === o.ref)).sort((a, b) => a.label.localeCompare(b.label));
3274
+ }, [searchUsers, members, pendingMembers]);
3261
3275
  (0, import_react7.useEffect)(() => {
3262
3276
  return () => {
3263
3277
  if (debounceTimerRef.current) {
@@ -3291,6 +3305,7 @@ var init_ManageTeamDialog = __esm({
3291
3305
  setMemberRef("");
3292
3306
  setMemberBudget("");
3293
3307
  setMemberError(null);
3308
+ setPendingMembers([]);
3294
3309
  setKbIds(team?.object_permission?.vector_stores ?? []);
3295
3310
  setKbError(null);
3296
3311
  setMcpIds(team?.object_permission?.mcp_servers ?? []);
@@ -3349,7 +3364,13 @@ var init_ManageTeamDialog = __esm({
3349
3364
  ...budgetDuration && { budget_duration: budgetDuration }
3350
3365
  };
3351
3366
  }
3352
- await onSubmit(payload);
3367
+ await onSubmit(
3368
+ payload,
3369
+ creating ? pendingMembers.map(({ userEntityRef, maxBudgetInTeam }) => ({
3370
+ userEntityRef,
3371
+ ...maxBudgetInTeam !== void 0 && { maxBudgetInTeam }
3372
+ })) : void 0
3373
+ );
3353
3374
  onClose();
3354
3375
  } catch (err) {
3355
3376
  setError(err instanceof Error ? err.message : "An error occurred");
@@ -3358,13 +3379,26 @@ var init_ManageTeamDialog = __esm({
3358
3379
  }
3359
3380
  };
3360
3381
  const handleAddMember = async () => {
3361
- if (!onAddMember || !memberRef.trim()) return;
3382
+ if (!memberRef.trim() || !creating && !onAddMember) return;
3362
3383
  setMemberError(null);
3363
- const budget = memberBudget ? parseFloat(memberBudget) : void 0;
3384
+ const budget = !membersOnly && memberBudget ? parseFloat(memberBudget) : void 0;
3364
3385
  if (budget !== void 0 && (isNaN(budget) || budget <= 0)) {
3365
3386
  setMemberError("Max budget in team must be a positive number");
3366
3387
  return;
3367
3388
  }
3389
+ if (creating) {
3390
+ const ref = memberRef.trim();
3391
+ if (!pendingMembers.some((p) => p.userEntityRef === ref)) {
3392
+ setPendingMembers((list) => [
3393
+ ...list,
3394
+ { userEntityRef: ref, label: memberQuery.trim() || ref, maxBudgetInTeam: budget }
3395
+ ]);
3396
+ }
3397
+ setMemberQuery("");
3398
+ setMemberRef("");
3399
+ setMemberBudget("");
3400
+ return;
3401
+ }
3368
3402
  try {
3369
3403
  setMemberBusy(true);
3370
3404
  await onAddMember(memberRef.trim(), budget);
@@ -3378,6 +3412,10 @@ var init_ManageTeamDialog = __esm({
3378
3412
  }
3379
3413
  };
3380
3414
  const handleRemoveMember = async (userId) => {
3415
+ if (creating) {
3416
+ setPendingMembers((list) => list.filter((p) => p.userEntityRef !== userId));
3417
+ return;
3418
+ }
3381
3419
  if (!onRemoveMember) return;
3382
3420
  setMemberError(null);
3383
3421
  try {
@@ -3421,186 +3459,242 @@ var init_ManageTeamDialog = __esm({
3421
3459
  const showMcpServers = mode === "edit" && !!canManageMcpServers;
3422
3460
  const mcpOptions = (mcpServers ?? []).map((s) => s.id);
3423
3461
  const mcpLabel = (id) => (mcpServers ?? []).find((s) => s.id === id)?.name ?? id;
3424
- 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" }),
3462
+ const isRemovalDisabled = (memberUserId, memberRole) => {
3463
+ if (!membersOnly) return { disabled: false };
3464
+ if (memberUserId === currentUserId) {
3465
+ return { disabled: true, reason: "You can't remove yourself" };
3466
+ }
3467
+ if (memberManagerRoles?.includes(memberRole)) {
3468
+ return { disabled: true, reason: `Only platform admins can remove a team ${memberRole}` };
3469
+ }
3470
+ return { disabled: false };
3471
+ };
3472
+ const submitLabel = creating ? "Create team" : "Save";
3473
+ const getDialogTitle = () => {
3474
+ if (membersOnly) {
3475
+ return `Manage members \u2014 ${team?.team_alias || "Untitled team"}`;
3476
+ }
3477
+ return mode === "create" ? "Create Team" : "Edit Team";
3478
+ };
3479
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_Dialog3.default, { open, onClose, maxWidth: wide ? "md" : "sm", fullWidth: true, children: [
3480
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_DialogTitle3.default, { children: getDialogTitle() }),
3426
3481
  /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_DialogContent3.default, { sx: { pt: 2, display: "flex", flexDirection: "column", gap: 2 }, children: [
3427
3482
  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,
3483
+ membersOnly && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_Alert2.default, { severity: "info", children: [
3484
+ "As a team ",
3485
+ team?.members_with_roles?.find((m) => m.user_id === currentUserId)?.role || "member",
3486
+ " you can add or remove members. Budgets and models are managed by your platform admins."
3487
+ ] }),
3488
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
3489
+ import_Box10.default,
3479
3490
  {
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
- ),
3490
- showMembers && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_Box10.default, { children: [
3491
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Divider2.default, { sx: { my: 1 } }),
3492
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Typography8.default, { variant: "subtitle2", sx: { mb: 1 }, children: "Members" }),
3493
- memberError && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Alert2.default, { severity: "error", sx: { mb: 1 }, onClose: () => setMemberError(null), children: memberError }),
3494
- members.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Typography8.default, { variant: "body2", color: "text.secondary", sx: { mb: 1 }, children: "No members yet." }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_TableContainer2.default, { component: import_Paper4.default, variant: "outlined", sx: { borderRadius: 1.5, mb: 1.5 }, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_Table2.default, { size: "small", sx: dataTableSx, children: [
3495
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_TableHead2.default, { children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_TableRow2.default, { children: [
3496
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_TableCell2.default, { children: "User" }),
3497
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_TableCell2.default, { align: "right", children: "Role" }),
3498
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_TableCell2.default, { align: "right", sx: { width: 40 } })
3499
- ] }) }),
3500
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_TableBody2.default, { children: members.map((m) => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_TableRow2.default, { children: [
3501
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_TableCell2.default, { children: m.user_email ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
3502
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Typography8.default, { variant: "body2", children: m.user_email }),
3491
+ sx: {
3492
+ display: "grid",
3493
+ gridTemplateColumns: {
3494
+ xs: "1fr",
3495
+ md: wide ? "minmax(0, 1fr) minmax(0, 1fr)" : "1fr"
3496
+ },
3497
+ gap: 3,
3498
+ alignItems: "start"
3499
+ },
3500
+ children: [
3501
+ !membersOnly && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_Box10.default, { sx: { display: "flex", flexDirection: "column", gap: 2 }, children: [
3502
+ wide && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Typography8.default, { variant: "subtitle2", children: "Settings" }),
3503
3503
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3504
- import_Typography8.default,
3504
+ import_TextField3.default,
3505
3505
  {
3506
- variant: "caption",
3507
- color: "text.secondary",
3508
- sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 11 },
3509
- children: m.user_id
3506
+ label: "Team Alias",
3507
+ value: alias,
3508
+ onChange: (e) => setAlias(e.target.value),
3509
+ disabled: submitting,
3510
+ fullWidth: true
3510
3511
  }
3511
- )
3512
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3513
- import_Typography8.default,
3514
- {
3515
- variant: "body2",
3516
- sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace" },
3517
- children: m.user_id
3518
- }
3519
- ) }),
3520
- /* @__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
- ) })
3532
- ] }, m.user_id)) })
3533
- ] }) }),
3534
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_Box10.default, { sx: { display: "flex", gap: 1, alignItems: "flex-start", flexWrap: "wrap" }, children: [
3535
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3536
- import_Autocomplete2.default,
3537
- {
3538
- freeSolo: true,
3539
- autoHighlight: true,
3540
- options: userOptions,
3541
- loading: usersLoading,
3542
- inputValue: memberQuery,
3543
- onInputChange: (_, newInputValue, reason) => {
3544
- setMemberQuery(newInputValue);
3545
- if (reason === "input" || reason === "clear") {
3546
- setMemberRef(newInputValue);
3547
- handleUserSearch(newInputValue);
3512
+ ),
3513
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3514
+ import_Autocomplete2.default,
3515
+ {
3516
+ multiple: true,
3517
+ options: modelNames,
3518
+ value: models,
3519
+ onChange: (_, newValue) => setModels(newValue),
3520
+ disabled: submitting,
3521
+ renderInput: (params) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3522
+ import_TextField3.default,
3523
+ {
3524
+ ...params,
3525
+ label: "Models",
3526
+ required: true,
3527
+ helperText: "Only models your admin allow-listed for teams. At least one is required."
3528
+ }
3529
+ )
3548
3530
  }
3549
- },
3550
- onChange: (_, newValue) => {
3551
- if (newValue && typeof newValue !== "string") {
3552
- setMemberQuery(newValue.label);
3553
- setMemberRef(newValue.ref);
3531
+ ),
3532
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
3533
+ import_Box10.default,
3534
+ {
3535
+ sx: {
3536
+ display: "grid",
3537
+ gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
3538
+ gap: 2
3539
+ },
3540
+ children: [
3541
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3542
+ import_TextField3.default,
3543
+ {
3544
+ label: budgetWriteOnly ? "New Max Budget ($)" : "Max Budget ($)",
3545
+ type: "number",
3546
+ value: maxBudget,
3547
+ onChange: (e) => setMaxBudget(e.target.value),
3548
+ disabled: submitting || unlimited,
3549
+ inputProps: { min: 0, max: maxBudgetCeiling },
3550
+ helperText: budgetWriteOnly ? "Current budget is hidden \u2014 leave blank to keep it, or enter a new value" : `Maximum: $${maxBudgetCeiling}`,
3551
+ fullWidth: true
3552
+ }
3553
+ ),
3554
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3555
+ import_TextField3.default,
3556
+ {
3557
+ select: true,
3558
+ label: "Budget Duration",
3559
+ value: budgetDuration,
3560
+ onChange: (e) => setBudgetDuration(e.target.value),
3561
+ disabled: submitting || unlimited,
3562
+ helperText: "Spend-reset period for the team budget",
3563
+ fullWidth: true,
3564
+ children: durationOptions.map((o) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_MenuItem2.default, { value: o.value, children: o.label }, o.value))
3565
+ }
3566
+ )
3567
+ ]
3554
3568
  }
3555
- },
3556
- getOptionLabel: (o) => typeof o === "string" ? o : o.label,
3557
- isOptionEqualToValue: (o, v) => o.ref === (typeof v === "string" ? v : v.ref),
3558
- disabled: memberBusy,
3559
- sx: { flex: 1, minWidth: 260 },
3560
- renderInput: (params) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3561
- import_TextField3.default,
3569
+ ),
3570
+ allowUnlimitedBudget && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3571
+ import_FormControlLabel2.default,
3562
3572
  {
3563
- ...params,
3564
- label: "Add member",
3565
- placeholder: "Search by name or email\u2026",
3566
- size: "small",
3567
- helperText: "Pick a catalog user, or paste a user entity ref",
3568
- InputProps: {
3569
- ...params.InputProps,
3570
- endAdornment: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
3571
- usersLoading ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_CircularProgress3.default, { color: "inherit", size: 14 }) : null,
3572
- params.InputProps.endAdornment
3573
- ] })
3574
- }
3573
+ control: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Checkbox2.default, { checked: unlimited, onChange: (e) => setUnlimited(e.target.checked), disabled: submitting }),
3574
+ label: "Unlimited Budget"
3575
3575
  }
3576
3576
  )
3577
- }
3578
- ),
3579
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3580
- import_TextField3.default,
3581
- {
3582
- label: "Max budget in team ($)",
3583
- type: "number",
3584
- value: memberBudget,
3585
- onChange: (e) => setMemberBudget(e.target.value),
3586
- disabled: memberBusy,
3587
- size: "small",
3588
- sx: { width: 160 }
3589
- }
3590
- ),
3591
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3592
- import_Button4.default,
3593
- {
3594
- onClick: handleAddMember,
3595
- disabled: memberBusy || !memberRef.trim(),
3596
- variant: "outlined",
3597
- sx: { mt: 0.5 },
3598
- children: "Add"
3599
- }
3600
- )
3601
- ] })
3602
- ] }),
3603
- showKnowledgeBases && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_Box10.default, { children: [
3577
+ ] }),
3578
+ showMembers && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_Box10.default, { children: [
3579
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Typography8.default, { variant: "subtitle2", sx: { mb: creating ? 0.25 : 1 }, children: creating ? "Initial members" : "Members" }),
3580
+ creating && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Typography8.default, { variant: "body2", color: "text.secondary", sx: { mb: 1 }, children: "Added as team users right after the team is created." }),
3581
+ memberError && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Alert2.default, { severity: "error", sx: { mb: 1 }, onClose: () => setMemberError(null), children: memberError }),
3582
+ members.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Typography8.default, { variant: "body2", color: "text.secondary", sx: { mb: 1 }, children: creating ? "No members picked yet \u2014 you can also add them later." : "No members yet." }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_TableContainer2.default, { component: import_Paper4.default, variant: "outlined", sx: { borderRadius: 1.5, mb: 1.5 }, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_Table2.default, { size: "small", sx: dataTableSx, children: [
3583
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_TableHead2.default, { children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_TableRow2.default, { children: [
3584
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_TableCell2.default, { children: "User" }),
3585
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_TableCell2.default, { align: "right", children: "Role" }),
3586
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_TableCell2.default, { align: "right", sx: { width: 40 } })
3587
+ ] }) }),
3588
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_TableBody2.default, { children: members.map((m) => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_TableRow2.default, { children: [
3589
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_TableCell2.default, { children: m.user_email ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
3590
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Typography8.default, { variant: "body2", children: m.user_email }),
3591
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3592
+ import_Typography8.default,
3593
+ {
3594
+ variant: "caption",
3595
+ color: "text.secondary",
3596
+ sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 11 },
3597
+ children: m.user_id
3598
+ }
3599
+ )
3600
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3601
+ import_Typography8.default,
3602
+ {
3603
+ variant: "body2",
3604
+ sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace" },
3605
+ children: m.user_id
3606
+ }
3607
+ ) }),
3608
+ /* @__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 }) }),
3609
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_TableCell2.default, { align: "right", children: (() => {
3610
+ const { disabled, reason } = isRemovalDisabled(m.user_id, m.role);
3611
+ 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)(
3612
+ import_IconButton4.default,
3613
+ {
3614
+ edge: "end",
3615
+ size: "small",
3616
+ "aria-label": `remove ${m.user_id}`,
3617
+ disabled: memberBusy || disabled,
3618
+ onClick: () => handleRemoveMember(m.user_id),
3619
+ children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_icons_material5.Delete, { fontSize: "small" })
3620
+ }
3621
+ ) }) });
3622
+ })() })
3623
+ ] }, m.user_id)) })
3624
+ ] }) }),
3625
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_Box10.default, { sx: { display: "flex", gap: 1, alignItems: "flex-start", flexWrap: "wrap" }, children: [
3626
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3627
+ import_Autocomplete2.default,
3628
+ {
3629
+ freeSolo: true,
3630
+ autoHighlight: true,
3631
+ options: userOptions,
3632
+ loading: usersLoading,
3633
+ inputValue: memberQuery,
3634
+ onInputChange: (_, newInputValue, reason) => {
3635
+ setMemberQuery(newInputValue);
3636
+ if (reason === "input" || reason === "clear") {
3637
+ setMemberRef(newInputValue);
3638
+ handleUserSearch(newInputValue);
3639
+ }
3640
+ },
3641
+ onChange: (_, newValue) => {
3642
+ if (newValue && typeof newValue !== "string") {
3643
+ setMemberQuery(newValue.label);
3644
+ setMemberRef(newValue.ref);
3645
+ }
3646
+ },
3647
+ getOptionLabel: (o) => typeof o === "string" ? o : o.label,
3648
+ isOptionEqualToValue: (o, v) => o.ref === (typeof v === "string" ? v : v.ref),
3649
+ disabled: memberBusy,
3650
+ sx: { flex: "1 1 240px" },
3651
+ renderInput: (params) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3652
+ import_TextField3.default,
3653
+ {
3654
+ ...params,
3655
+ label: "Add member",
3656
+ placeholder: "Search by name or email\u2026",
3657
+ size: "small",
3658
+ helperText: "Pick a catalog user, or paste a user entity ref",
3659
+ InputProps: {
3660
+ ...params.InputProps,
3661
+ endAdornment: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
3662
+ usersLoading ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_CircularProgress3.default, { color: "inherit", size: 14 }) : null,
3663
+ params.InputProps.endAdornment
3664
+ ] })
3665
+ }
3666
+ }
3667
+ )
3668
+ }
3669
+ ),
3670
+ !membersOnly && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3671
+ import_TextField3.default,
3672
+ {
3673
+ label: "Max budget in team ($)",
3674
+ type: "number",
3675
+ value: memberBudget,
3676
+ onChange: (e) => setMemberBudget(e.target.value),
3677
+ disabled: memberBusy,
3678
+ size: "small",
3679
+ sx: { width: 150 }
3680
+ }
3681
+ ),
3682
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3683
+ import_Button4.default,
3684
+ {
3685
+ onClick: handleAddMember,
3686
+ disabled: memberBusy || !memberRef.trim(),
3687
+ variant: "outlined",
3688
+ sx: { mt: 0.5 },
3689
+ children: "Add"
3690
+ }
3691
+ )
3692
+ ] })
3693
+ ] })
3694
+ ]
3695
+ }
3696
+ ),
3697
+ !membersOnly && showKnowledgeBases && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_Box10.default, { children: [
3604
3698
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Divider2.default, { sx: { my: 1 } }),
3605
3699
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Typography8.default, { variant: "subtitle2", sx: { mb: 1 }, children: "Knowledge Bases" }),
3606
3700
  /* @__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 +3721,7 @@ var init_ManageTeamDialog = __esm({
3627
3721
  }
3628
3722
  ) })
3629
3723
  ] }),
3630
- showMcpServers && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_Box10.default, { children: [
3724
+ !membersOnly && showMcpServers && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_Box10.default, { children: [
3631
3725
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Divider2.default, { sx: { my: 1 } }),
3632
3726
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Typography8.default, { variant: "subtitle2", sx: { mb: 1 }, children: "MCP Servers" }),
3633
3727
  /* @__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 +3750,8 @@ var init_ManageTeamDialog = __esm({
3656
3750
  ] })
3657
3751
  ] }),
3658
3752
  /* @__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" })
3753
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Button4.default, { onClick: onClose, disabled: submitting, children: membersOnly ? "Done" : "Cancel" }),
3754
+ !membersOnly && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_Button4.default, { onClick: handleSubmit, disabled: submitting, variant: "contained", children: submitting ? "Saving..." : submitLabel })
3661
3755
  ] })
3662
3756
  ] });
3663
3757
  };
@@ -4893,7 +4987,7 @@ function budgetStatValue(hidden, budget) {
4893
4987
  if (budget > 0) return fmtUsd2(budget);
4894
4988
  return "Unlimited";
4895
4989
  }
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;
4990
+ 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
4991
  var init_TeamUsage = __esm({
4898
4992
  "src/components/TeamUsage.tsx"() {
4899
4993
  "use strict";
@@ -4914,12 +5008,13 @@ var init_TeamUsage = __esm({
4914
5008
  import_Button6 = __toESM(require("@mui/material/Button"));
4915
5009
  import_CircularProgress4 = __toESM(require("@mui/material/CircularProgress"));
4916
5010
  import_styles10 = require("@mui/material/styles");
5011
+ import_Tooltip5 = __toESM(require("@mui/material/Tooltip"));
4917
5012
  import_icons_material7 = require("@mui/icons-material");
4918
5013
  import_recharts2 = require("recharts");
4919
5014
  init_ui();
4920
5015
  import_jsx_runtime15 = require("react/jsx-runtime");
4921
5016
  fmtUsd2 = (n) => `$${(n ?? 0).toFixed(2)}`;
4922
- TeamCard = ({ team, usage, usageLoading, canManage, onEditTeam, onExpand }) => {
5017
+ TeamCard = ({ team, usage, usageLoading, canManage, onEditTeam, canManageMembers, onManageMembers, onExpand }) => {
4923
5018
  const [expanded, setExpanded] = (0, import_react10.useState)(false);
4924
5019
  const chart = useChartTheme();
4925
5020
  const handleExpandChange = (newExpanded) => {
@@ -5029,6 +5124,17 @@ var init_TeamUsage = __esm({
5029
5124
  children: "Edit"
5030
5125
  }
5031
5126
  ),
5127
+ 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)(
5128
+ import_Button6.default,
5129
+ {
5130
+ size: "small",
5131
+ variant: "text",
5132
+ startIcon: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_icons_material7.GroupAddOutlined, {}),
5133
+ onClick: () => onManageMembers(team),
5134
+ sx: { textTransform: "none" },
5135
+ children: "Manage members"
5136
+ }
5137
+ ) }),
5032
5138
  /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
5033
5139
  import_IconButton6.default,
5034
5140
  {
@@ -5159,9 +5265,23 @@ var init_TeamUsage = __esm({
5159
5265
  onEditTeam,
5160
5266
  canCreate,
5161
5267
  onCreateTeam,
5162
- onTeamExpand
5268
+ onTeamExpand,
5269
+ canManageMembers,
5270
+ onManageMembers,
5271
+ readOnly
5163
5272
  }) => {
5164
- const createAction = canCreate && onCreateTeam ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
5273
+ 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)(
5274
+ import_Box14.default,
5275
+ {
5276
+ component: "span",
5277
+ sx: { display: "inline-flex", alignItems: "center", gap: 0.5, color: "text.secondary", fontSize: 12.5 },
5278
+ children: [
5279
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_icons_material7.LockOutlined, { sx: { fontSize: 15 } }),
5280
+ "Synced from identity provider"
5281
+ ]
5282
+ }
5283
+ ) });
5284
+ const createButton = canCreate && onCreateTeam ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
5165
5285
  import_Button6.default,
5166
5286
  {
5167
5287
  variant: "contained",
@@ -5172,6 +5292,7 @@ var init_TeamUsage = __esm({
5172
5292
  children: "Create Team"
5173
5293
  }
5174
5294
  ) : void 0;
5295
+ const createAction = readOnly ? readOnlyBadge : createButton;
5175
5296
  if (loading) {
5176
5297
  return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(SectionCard, { title: "Teams", actions: createAction, children: [
5177
5298
  /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_Skeleton5.default, { variant: "rounded", height: 120, sx: { mb: 2 } }),
@@ -5201,6 +5322,8 @@ var init_TeamUsage = __esm({
5201
5322
  usageLoading: getTeamUsageLoading(team.team_id),
5202
5323
  canManage,
5203
5324
  onEditTeam,
5325
+ canManageMembers: canManageMembers?.(team),
5326
+ onManageMembers,
5204
5327
  onExpand: onTeamExpand
5205
5328
  },
5206
5329
  team.team_id
@@ -5229,7 +5352,7 @@ function fmtTokens(n) {
5229
5352
  if (n >= 1e3) return `${(n / 1e3).toFixed(0)}K`;
5230
5353
  return String(n);
5231
5354
  }
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;
5355
+ 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
5356
  var init_ModelsTable = __esm({
5234
5357
  "src/components/ModelsTable.tsx"() {
5235
5358
  "use strict";
@@ -5251,7 +5374,7 @@ var init_ModelsTable = __esm({
5251
5374
  import_InputAdornment3 = __toESM(require("@mui/material/InputAdornment"));
5252
5375
  import_icons_material8 = require("@mui/icons-material");
5253
5376
  import_ContentCopy = __toESM(require("@mui/icons-material/ContentCopy"));
5254
- import_Tooltip4 = __toESM(require("@mui/material/Tooltip"));
5377
+ import_Tooltip6 = __toESM(require("@mui/material/Tooltip"));
5255
5378
  import_Snackbar = __toESM(require("@mui/material/Snackbar"));
5256
5379
  import_Alert4 = __toESM(require("@mui/material/Alert"));
5257
5380
  init_ui();
@@ -5414,7 +5537,7 @@ var init_ModelsTable = __esm({
5414
5537
  ] }) }),
5415
5538
  /* @__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
5539
  /* @__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)(
5540
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_Tooltip6.default, { title: m.model_name, placement: "top", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
5418
5541
  "span",
5419
5542
  {
5420
5543
  role: "button",
@@ -5426,7 +5549,7 @@ var init_ModelsTable = __esm({
5426
5549
  children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(TagChip, { label: m.model_name, title: "Copy model ID" })
5427
5550
  }
5428
5551
  ) }),
5429
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_Tooltip4.default, { title: "Copy model ID", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
5552
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_Tooltip6.default, { title: "Copy model ID", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
5430
5553
  import_Box15.default,
5431
5554
  {
5432
5555
  component: "span",
@@ -5836,6 +5959,19 @@ var init_feedback = __esm({
5836
5959
  }
5837
5960
  });
5838
5961
 
5962
+ // src/teamMemberManager.ts
5963
+ function isTeamMemberManager(team, userId, memberManagerRoles, readOnly) {
5964
+ if (!team || !userId || !memberManagerRoles || memberManagerRoles.length === 0 || readOnly) {
5965
+ return false;
5966
+ }
5967
+ return team.members_with_roles?.some((m) => m.user_id === userId && memberManagerRoles.includes(m.role)) ?? false;
5968
+ }
5969
+ var init_teamMemberManager = __esm({
5970
+ "src/teamMemberManager.ts"() {
5971
+ "use strict";
5972
+ }
5973
+ });
5974
+
5839
5975
  // src/unprovisionedCopy.ts
5840
5976
  function getUnprovisionedTitle() {
5841
5977
  return "Your LiteLLM account isn't set up yet";
@@ -5912,6 +6048,7 @@ var init_LiteLLMPage = __esm({
5912
6048
  init_dates();
5913
6049
  init_permissions();
5914
6050
  init_feedback();
6051
+ init_teamMemberManager();
5915
6052
  init_unprovisionedCopy();
5916
6053
  import_jsx_runtime18 = require("react/jsx-runtime");
5917
6054
  PERIOD_LS_KEY2 = "litellm_usage_period";
@@ -6358,6 +6495,8 @@ var init_LiteLLMPage = __esm({
6358
6495
  for (const t of managedTeams ?? []) teamsById.set(t.team_id, t);
6359
6496
  for (const t of teams ?? []) teamsById.set(t.team_id, t);
6360
6497
  const visibleTeams = Array.from(teamsById.values());
6498
+ const readOnly = liteLlmConfig?.teamManagement?.readOnly ?? false;
6499
+ const memberManagerRoles = liteLlmConfig?.teamManagement?.memberManagerRoles ?? [];
6361
6500
  return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
6362
6501
  TeamUsage,
6363
6502
  {
@@ -6365,10 +6504,13 @@ var init_LiteLLMPage = __esm({
6365
6504
  loading: teamsLoading,
6366
6505
  getTeamUsage: (teamId) => teamUsageCache[teamId] ?? null,
6367
6506
  getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false,
6368
- canManage: teamMgmtEnabled && canManageTeam,
6507
+ canManage: teamMgmtEnabled && canManageTeam && !readOnly,
6369
6508
  onEditTeam: (t) => setManageTeam({ mode: "edit", team: t }),
6370
- canCreate: teamMgmtEnabled && canCreateTeam,
6509
+ canCreate: teamMgmtEnabled && canCreateTeam && !readOnly,
6371
6510
  onCreateTeam: () => setManageTeam({ mode: "create" }),
6511
+ canManageMembers: (t) => teamMgmtEnabled && canManageMembers && isTeamMemberManager(t, userInfo?.user_id, memberManagerRoles, readOnly),
6512
+ onManageMembers: (t) => setManageTeam({ mode: "members", team: t }),
6513
+ readOnly,
6372
6514
  onTeamExpand: (team) => loadTeamUsage(team.team_id)
6373
6515
  }
6374
6516
  );
@@ -6420,11 +6562,14 @@ var init_LiteLLMPage = __esm({
6420
6562
  {
6421
6563
  open: !!manageTeam,
6422
6564
  onClose: () => setManageTeam(null),
6423
- mode: manageTeam?.mode ?? "create",
6565
+ mode: manageTeam?.mode === "members" ? "edit" : manageTeam?.mode ?? "create",
6424
6566
  team: manageTeam?.team,
6425
6567
  allModels: allModels ?? [],
6426
6568
  config: liteLlmConfig,
6427
- onSubmit: async (payload) => {
6569
+ membersOnly: manageTeam?.mode === "members",
6570
+ currentUserId: userInfo?.user_id,
6571
+ memberManagerRoles: liteLlmConfig?.teamManagement?.memberManagerRoles,
6572
+ onSubmit: async (payload, initialMembers) => {
6428
6573
  try {
6429
6574
  if (manageTeam?.mode === "edit" && manageTeam.team) {
6430
6575
  const loadedAt = manageTeam.team.metadata?.updated_at_iso;
@@ -6434,7 +6579,23 @@ var init_LiteLLMPage = __esm({
6434
6579
  ...typeof loadedAt === "string" ? { expectedUpdatedAtIso: loadedAt } : {}
6435
6580
  });
6436
6581
  } else {
6437
- await api.createTeam(payload);
6582
+ const created = await api.createTeam(payload);
6583
+ const failed = [];
6584
+ for (const m of initialMembers ?? []) {
6585
+ try {
6586
+ await api.addTeamMember(created.team_id, m);
6587
+ } catch (e) {
6588
+ failed.push(`${m.userEntityRef} (${e.message})`);
6589
+ }
6590
+ }
6591
+ if (failed.length) {
6592
+ alertApi.post({
6593
+ message: `Team created, but some members could not be added: ${failed.join(", ")}`,
6594
+ severity: "warning"
6595
+ });
6596
+ refreshTeams();
6597
+ return;
6598
+ }
6438
6599
  }
6439
6600
  const alert = toastFor("teamSaveSuccess");
6440
6601
  if (alert) alertApi.post(alert);
@@ -6444,7 +6605,7 @@ var init_LiteLLMPage = __esm({
6444
6605
  if (alert) alertApi.post(alert);
6445
6606
  }
6446
6607
  },
6447
- canManageMembers: teamMgmtEnabled && canManageMembers,
6608
+ canManageMembers: teamMgmtEnabled && canManageMembers && !liteLlmConfig?.teamManagement?.readOnly,
6448
6609
  onAddMember: async (userEntityRef, maxBudgetInTeam) => {
6449
6610
  if (!manageTeam?.team) return;
6450
6611
  const updated = await api.addTeamMember(manageTeam.team.team_id, {