@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.esm.js CHANGED
@@ -3124,6 +3124,7 @@ import Box10 from "@mui/material/Box";
3124
3124
  import Divider2 from "@mui/material/Divider";
3125
3125
  import Typography8 from "@mui/material/Typography";
3126
3126
  import IconButton4 from "@mui/material/IconButton";
3127
+ import Tooltip4 from "@mui/material/Tooltip";
3127
3128
  import Paper4 from "@mui/material/Paper";
3128
3129
  import Table2 from "@mui/material/Table";
3129
3130
  import TableBody2 from "@mui/material/TableBody";
@@ -3166,7 +3167,10 @@ var init_ManageTeamDialog = __esm({
3166
3167
  onSaveKnowledgeBases,
3167
3168
  canManageMcpServers,
3168
3169
  mcpServers,
3169
- onSaveMcpServers
3170
+ onSaveMcpServers,
3171
+ membersOnly,
3172
+ currentUserId,
3173
+ memberManagerRoles
3170
3174
  }) => {
3171
3175
  const [alias, setAlias] = useState6("");
3172
3176
  const [models, setModels] = useState6([]);
@@ -3180,6 +3184,7 @@ var init_ManageTeamDialog = __esm({
3180
3184
  const [memberBudget, setMemberBudget] = useState6("");
3181
3185
  const [memberBusy, setMemberBusy] = useState6(false);
3182
3186
  const [memberError, setMemberError] = useState6(null);
3187
+ const [pendingMembers, setPendingMembers] = useState6([]);
3183
3188
  const [kbIds, setKbIds] = useState6([]);
3184
3189
  const [kbBusy, setKbBusy] = useState6(false);
3185
3190
  const [kbError, setKbError] = useState6(null);
@@ -3190,8 +3195,17 @@ var init_ManageTeamDialog = __esm({
3190
3195
  const maxBudgetCeiling = config?.teamManagement?.maxBudgetCeiling ?? FALLBACK_BUDGET_CEILING;
3191
3196
  const hideBudgetForManagers = config?.display?.hideTeamBudgetForManagers ?? false;
3192
3197
  const budgetWriteOnly = hideBudgetForManagers && mode === "edit";
3193
- const members = useMemo4(() => team?.members_with_roles ?? [], [team]);
3194
- const showMembers = mode === "edit" && !!canManageMembers;
3198
+ const creating = mode === "create";
3199
+ const members = useMemo4(
3200
+ () => creating ? pendingMembers.map((p) => ({
3201
+ user_id: p.userEntityRef,
3202
+ user_email: p.label,
3203
+ role: "user"
3204
+ })) : team?.members_with_roles ?? [],
3205
+ [creating, pendingMembers, team]
3206
+ );
3207
+ const showMembers = !!canManageMembers || !!membersOnly;
3208
+ const wide = showMembers && !membersOnly;
3195
3209
  const catalogApi = useApi3(catalogApiRef);
3196
3210
  const [searchUsers, setSearchUsers] = useState6([]);
3197
3211
  const [usersLoading, setUsersLoading] = useState6(false);
@@ -3236,8 +3250,8 @@ var init_ManageTeamDialog = __esm({
3236
3250
  email,
3237
3251
  label: email ? `${displayName} (${email})` : displayName
3238
3252
  };
3239
- }).filter((o) => !(o.email && existingEmails.has(o.email.toLowerCase()))).sort((a, b) => a.label.localeCompare(b.label));
3240
- }, [searchUsers, members]);
3253
+ }).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));
3254
+ }, [searchUsers, members, pendingMembers]);
3241
3255
  useEffect4(() => {
3242
3256
  return () => {
3243
3257
  if (debounceTimerRef.current) {
@@ -3271,6 +3285,7 @@ var init_ManageTeamDialog = __esm({
3271
3285
  setMemberRef("");
3272
3286
  setMemberBudget("");
3273
3287
  setMemberError(null);
3288
+ setPendingMembers([]);
3274
3289
  setKbIds(team?.object_permission?.vector_stores ?? []);
3275
3290
  setKbError(null);
3276
3291
  setMcpIds(team?.object_permission?.mcp_servers ?? []);
@@ -3329,7 +3344,13 @@ var init_ManageTeamDialog = __esm({
3329
3344
  ...budgetDuration && { budget_duration: budgetDuration }
3330
3345
  };
3331
3346
  }
3332
- await onSubmit(payload);
3347
+ await onSubmit(
3348
+ payload,
3349
+ creating ? pendingMembers.map(({ userEntityRef, maxBudgetInTeam }) => ({
3350
+ userEntityRef,
3351
+ ...maxBudgetInTeam !== void 0 && { maxBudgetInTeam }
3352
+ })) : void 0
3353
+ );
3333
3354
  onClose();
3334
3355
  } catch (err) {
3335
3356
  setError(err instanceof Error ? err.message : "An error occurred");
@@ -3338,13 +3359,26 @@ var init_ManageTeamDialog = __esm({
3338
3359
  }
3339
3360
  };
3340
3361
  const handleAddMember = async () => {
3341
- if (!onAddMember || !memberRef.trim()) return;
3362
+ if (!memberRef.trim() || !creating && !onAddMember) return;
3342
3363
  setMemberError(null);
3343
- const budget = memberBudget ? parseFloat(memberBudget) : void 0;
3364
+ const budget = !membersOnly && memberBudget ? parseFloat(memberBudget) : void 0;
3344
3365
  if (budget !== void 0 && (isNaN(budget) || budget <= 0)) {
3345
3366
  setMemberError("Max budget in team must be a positive number");
3346
3367
  return;
3347
3368
  }
3369
+ if (creating) {
3370
+ const ref = memberRef.trim();
3371
+ if (!pendingMembers.some((p) => p.userEntityRef === ref)) {
3372
+ setPendingMembers((list) => [
3373
+ ...list,
3374
+ { userEntityRef: ref, label: memberQuery.trim() || ref, maxBudgetInTeam: budget }
3375
+ ]);
3376
+ }
3377
+ setMemberQuery("");
3378
+ setMemberRef("");
3379
+ setMemberBudget("");
3380
+ return;
3381
+ }
3348
3382
  try {
3349
3383
  setMemberBusy(true);
3350
3384
  await onAddMember(memberRef.trim(), budget);
@@ -3358,6 +3392,10 @@ var init_ManageTeamDialog = __esm({
3358
3392
  }
3359
3393
  };
3360
3394
  const handleRemoveMember = async (userId) => {
3395
+ if (creating) {
3396
+ setPendingMembers((list) => list.filter((p) => p.userEntityRef !== userId));
3397
+ return;
3398
+ }
3361
3399
  if (!onRemoveMember) return;
3362
3400
  setMemberError(null);
3363
3401
  try {
@@ -3401,186 +3439,242 @@ var init_ManageTeamDialog = __esm({
3401
3439
  const showMcpServers = mode === "edit" && !!canManageMcpServers;
3402
3440
  const mcpOptions = (mcpServers ?? []).map((s) => s.id);
3403
3441
  const mcpLabel = (id) => (mcpServers ?? []).find((s) => s.id === id)?.name ?? id;
3404
- return /* @__PURE__ */ jsxs10(Dialog3, { open, onClose, maxWidth: "sm", fullWidth: true, children: [
3405
- /* @__PURE__ */ jsx11(DialogTitle3, { children: mode === "create" ? "Create Team" : "Edit Team" }),
3442
+ const isRemovalDisabled = (memberUserId, memberRole) => {
3443
+ if (!membersOnly) return { disabled: false };
3444
+ if (memberUserId === currentUserId) {
3445
+ return { disabled: true, reason: "You can't remove yourself" };
3446
+ }
3447
+ if (memberManagerRoles?.includes(memberRole)) {
3448
+ return { disabled: true, reason: `Only platform admins can remove a team ${memberRole}` };
3449
+ }
3450
+ return { disabled: false };
3451
+ };
3452
+ const submitLabel = creating ? "Create team" : "Save";
3453
+ const getDialogTitle = () => {
3454
+ if (membersOnly) {
3455
+ return `Manage members \u2014 ${team?.team_alias || "Untitled team"}`;
3456
+ }
3457
+ return mode === "create" ? "Create Team" : "Edit Team";
3458
+ };
3459
+ return /* @__PURE__ */ jsxs10(Dialog3, { open, onClose, maxWidth: wide ? "md" : "sm", fullWidth: true, children: [
3460
+ /* @__PURE__ */ jsx11(DialogTitle3, { children: getDialogTitle() }),
3406
3461
  /* @__PURE__ */ jsxs10(DialogContent3, { sx: { pt: 2, display: "flex", flexDirection: "column", gap: 2 }, children: [
3407
3462
  error && /* @__PURE__ */ jsx11(Alert2, { severity: "error", children: error }),
3408
- /* @__PURE__ */ jsx11(
3409
- TextField3,
3410
- {
3411
- label: "Team Alias",
3412
- value: alias,
3413
- onChange: (e) => setAlias(e.target.value),
3414
- disabled: submitting,
3415
- fullWidth: true
3416
- }
3417
- ),
3418
- /* @__PURE__ */ jsx11(
3419
- Autocomplete2,
3420
- {
3421
- multiple: true,
3422
- options: modelNames,
3423
- value: models,
3424
- onChange: (_, newValue) => setModels(newValue),
3425
- disabled: submitting,
3426
- renderInput: (params) => /* @__PURE__ */ jsx11(
3427
- TextField3,
3428
- {
3429
- ...params,
3430
- label: "Models",
3431
- required: true,
3432
- 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.)"
3433
- }
3434
- )
3435
- }
3436
- ),
3437
- /* @__PURE__ */ jsx11(
3438
- TextField3,
3439
- {
3440
- label: budgetWriteOnly ? "New Max Budget ($)" : "Max Budget ($)",
3441
- type: "number",
3442
- value: maxBudget,
3443
- onChange: (e) => setMaxBudget(e.target.value),
3444
- disabled: submitting || unlimited,
3445
- inputProps: { min: 0, max: maxBudgetCeiling },
3446
- helperText: budgetWriteOnly ? "Current budget is hidden \u2014 leave blank to keep it, or enter a new value" : `Maximum: $${maxBudgetCeiling}`,
3447
- fullWidth: true
3448
- }
3449
- ),
3450
- allowUnlimitedBudget && /* @__PURE__ */ jsx11(
3451
- FormControlLabel2,
3452
- {
3453
- control: /* @__PURE__ */ jsx11(Checkbox2, { checked: unlimited, onChange: (e) => setUnlimited(e.target.checked), disabled: submitting }),
3454
- label: "Unlimited Budget"
3455
- }
3456
- ),
3457
- /* @__PURE__ */ jsx11(
3458
- TextField3,
3463
+ membersOnly && /* @__PURE__ */ jsxs10(Alert2, { severity: "info", children: [
3464
+ "As a team ",
3465
+ team?.members_with_roles?.find((m) => m.user_id === currentUserId)?.role || "member",
3466
+ " you can add or remove members. Budgets and models are managed by your platform admins."
3467
+ ] }),
3468
+ /* @__PURE__ */ jsxs10(
3469
+ Box10,
3459
3470
  {
3460
- select: true,
3461
- label: "Budget Duration",
3462
- value: budgetDuration,
3463
- onChange: (e) => setBudgetDuration(e.target.value),
3464
- disabled: submitting || unlimited,
3465
- helperText: "Spend-reset period for the team budget",
3466
- fullWidth: true,
3467
- children: durationOptions.map((o) => /* @__PURE__ */ jsx11(MenuItem2, { value: o.value, children: o.label }, o.value))
3468
- }
3469
- ),
3470
- showMembers && /* @__PURE__ */ jsxs10(Box10, { children: [
3471
- /* @__PURE__ */ jsx11(Divider2, { sx: { my: 1 } }),
3472
- /* @__PURE__ */ jsx11(Typography8, { variant: "subtitle2", sx: { mb: 1 }, children: "Members" }),
3473
- memberError && /* @__PURE__ */ jsx11(Alert2, { severity: "error", sx: { mb: 1 }, onClose: () => setMemberError(null), children: memberError }),
3474
- members.length === 0 ? /* @__PURE__ */ jsx11(Typography8, { variant: "body2", color: "text.secondary", sx: { mb: 1 }, children: "No members yet." }) : /* @__PURE__ */ jsx11(TableContainer2, { component: Paper4, variant: "outlined", sx: { borderRadius: 1.5, mb: 1.5 }, children: /* @__PURE__ */ jsxs10(Table2, { size: "small", sx: dataTableSx, children: [
3475
- /* @__PURE__ */ jsx11(TableHead2, { children: /* @__PURE__ */ jsxs10(TableRow2, { children: [
3476
- /* @__PURE__ */ jsx11(TableCell2, { children: "User" }),
3477
- /* @__PURE__ */ jsx11(TableCell2, { align: "right", children: "Role" }),
3478
- /* @__PURE__ */ jsx11(TableCell2, { align: "right", sx: { width: 40 } })
3479
- ] }) }),
3480
- /* @__PURE__ */ jsx11(TableBody2, { children: members.map((m) => /* @__PURE__ */ jsxs10(TableRow2, { children: [
3481
- /* @__PURE__ */ jsx11(TableCell2, { children: m.user_email ? /* @__PURE__ */ jsxs10(Fragment4, { children: [
3482
- /* @__PURE__ */ jsx11(Typography8, { variant: "body2", children: m.user_email }),
3471
+ sx: {
3472
+ display: "grid",
3473
+ gridTemplateColumns: {
3474
+ xs: "1fr",
3475
+ md: wide ? "minmax(0, 1fr) minmax(0, 1fr)" : "1fr"
3476
+ },
3477
+ gap: 3,
3478
+ alignItems: "start"
3479
+ },
3480
+ children: [
3481
+ !membersOnly && /* @__PURE__ */ jsxs10(Box10, { sx: { display: "flex", flexDirection: "column", gap: 2 }, children: [
3482
+ wide && /* @__PURE__ */ jsx11(Typography8, { variant: "subtitle2", children: "Settings" }),
3483
3483
  /* @__PURE__ */ jsx11(
3484
- Typography8,
3484
+ TextField3,
3485
3485
  {
3486
- variant: "caption",
3487
- color: "text.secondary",
3488
- sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 11 },
3489
- children: m.user_id
3486
+ label: "Team Alias",
3487
+ value: alias,
3488
+ onChange: (e) => setAlias(e.target.value),
3489
+ disabled: submitting,
3490
+ fullWidth: true
3490
3491
  }
3491
- )
3492
- ] }) : /* @__PURE__ */ jsx11(
3493
- Typography8,
3494
- {
3495
- variant: "body2",
3496
- sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace" },
3497
- children: m.user_id
3498
- }
3499
- ) }),
3500
- /* @__PURE__ */ jsx11(TableCell2, { align: "right", children: /* @__PURE__ */ jsx11(StatusPill, { label: m.role, tone: m.role === "admin" ? "accent" : "neutral", dot: false }) }),
3501
- /* @__PURE__ */ jsx11(TableCell2, { align: "right", children: /* @__PURE__ */ jsx11(
3502
- IconButton4,
3503
- {
3504
- edge: "end",
3505
- size: "small",
3506
- "aria-label": `remove ${m.user_id}`,
3507
- disabled: memberBusy,
3508
- onClick: () => handleRemoveMember(m.user_id),
3509
- children: /* @__PURE__ */ jsx11(DeleteIcon, { fontSize: "small" })
3510
- }
3511
- ) })
3512
- ] }, m.user_id)) })
3513
- ] }) }),
3514
- /* @__PURE__ */ jsxs10(Box10, { sx: { display: "flex", gap: 1, alignItems: "flex-start", flexWrap: "wrap" }, children: [
3515
- /* @__PURE__ */ jsx11(
3516
- Autocomplete2,
3517
- {
3518
- freeSolo: true,
3519
- autoHighlight: true,
3520
- options: userOptions,
3521
- loading: usersLoading,
3522
- inputValue: memberQuery,
3523
- onInputChange: (_, newInputValue, reason) => {
3524
- setMemberQuery(newInputValue);
3525
- if (reason === "input" || reason === "clear") {
3526
- setMemberRef(newInputValue);
3527
- handleUserSearch(newInputValue);
3492
+ ),
3493
+ /* @__PURE__ */ jsx11(
3494
+ Autocomplete2,
3495
+ {
3496
+ multiple: true,
3497
+ options: modelNames,
3498
+ value: models,
3499
+ onChange: (_, newValue) => setModels(newValue),
3500
+ disabled: submitting,
3501
+ renderInput: (params) => /* @__PURE__ */ jsx11(
3502
+ TextField3,
3503
+ {
3504
+ ...params,
3505
+ label: "Models",
3506
+ required: true,
3507
+ helperText: "Only models your admin allow-listed for teams. At least one is required."
3508
+ }
3509
+ )
3528
3510
  }
3529
- },
3530
- onChange: (_, newValue) => {
3531
- if (newValue && typeof newValue !== "string") {
3532
- setMemberQuery(newValue.label);
3533
- setMemberRef(newValue.ref);
3511
+ ),
3512
+ /* @__PURE__ */ jsxs10(
3513
+ Box10,
3514
+ {
3515
+ sx: {
3516
+ display: "grid",
3517
+ gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
3518
+ gap: 2
3519
+ },
3520
+ children: [
3521
+ /* @__PURE__ */ jsx11(
3522
+ TextField3,
3523
+ {
3524
+ label: budgetWriteOnly ? "New Max Budget ($)" : "Max Budget ($)",
3525
+ type: "number",
3526
+ value: maxBudget,
3527
+ onChange: (e) => setMaxBudget(e.target.value),
3528
+ disabled: submitting || unlimited,
3529
+ inputProps: { min: 0, max: maxBudgetCeiling },
3530
+ helperText: budgetWriteOnly ? "Current budget is hidden \u2014 leave blank to keep it, or enter a new value" : `Maximum: $${maxBudgetCeiling}`,
3531
+ fullWidth: true
3532
+ }
3533
+ ),
3534
+ /* @__PURE__ */ jsx11(
3535
+ TextField3,
3536
+ {
3537
+ select: true,
3538
+ label: "Budget Duration",
3539
+ value: budgetDuration,
3540
+ onChange: (e) => setBudgetDuration(e.target.value),
3541
+ disabled: submitting || unlimited,
3542
+ helperText: "Spend-reset period for the team budget",
3543
+ fullWidth: true,
3544
+ children: durationOptions.map((o) => /* @__PURE__ */ jsx11(MenuItem2, { value: o.value, children: o.label }, o.value))
3545
+ }
3546
+ )
3547
+ ]
3534
3548
  }
3535
- },
3536
- getOptionLabel: (o) => typeof o === "string" ? o : o.label,
3537
- isOptionEqualToValue: (o, v) => o.ref === (typeof v === "string" ? v : v.ref),
3538
- disabled: memberBusy,
3539
- sx: { flex: 1, minWidth: 260 },
3540
- renderInput: (params) => /* @__PURE__ */ jsx11(
3541
- TextField3,
3549
+ ),
3550
+ allowUnlimitedBudget && /* @__PURE__ */ jsx11(
3551
+ FormControlLabel2,
3542
3552
  {
3543
- ...params,
3544
- label: "Add member",
3545
- placeholder: "Search by name or email\u2026",
3546
- size: "small",
3547
- helperText: "Pick a catalog user, or paste a user entity ref",
3548
- InputProps: {
3549
- ...params.InputProps,
3550
- endAdornment: /* @__PURE__ */ jsxs10(Fragment4, { children: [
3551
- usersLoading ? /* @__PURE__ */ jsx11(CircularProgress3, { color: "inherit", size: 14 }) : null,
3552
- params.InputProps.endAdornment
3553
- ] })
3554
- }
3553
+ control: /* @__PURE__ */ jsx11(Checkbox2, { checked: unlimited, onChange: (e) => setUnlimited(e.target.checked), disabled: submitting }),
3554
+ label: "Unlimited Budget"
3555
3555
  }
3556
3556
  )
3557
- }
3558
- ),
3559
- /* @__PURE__ */ jsx11(
3560
- TextField3,
3561
- {
3562
- label: "Max budget in team ($)",
3563
- type: "number",
3564
- value: memberBudget,
3565
- onChange: (e) => setMemberBudget(e.target.value),
3566
- disabled: memberBusy,
3567
- size: "small",
3568
- sx: { width: 160 }
3569
- }
3570
- ),
3571
- /* @__PURE__ */ jsx11(
3572
- Button4,
3573
- {
3574
- onClick: handleAddMember,
3575
- disabled: memberBusy || !memberRef.trim(),
3576
- variant: "outlined",
3577
- sx: { mt: 0.5 },
3578
- children: "Add"
3579
- }
3580
- )
3581
- ] })
3582
- ] }),
3583
- showKnowledgeBases && /* @__PURE__ */ jsxs10(Box10, { children: [
3557
+ ] }),
3558
+ showMembers && /* @__PURE__ */ jsxs10(Box10, { children: [
3559
+ /* @__PURE__ */ jsx11(Typography8, { variant: "subtitle2", sx: { mb: creating ? 0.25 : 1 }, children: creating ? "Initial members" : "Members" }),
3560
+ creating && /* @__PURE__ */ jsx11(Typography8, { variant: "body2", color: "text.secondary", sx: { mb: 1 }, children: "Added as team users right after the team is created." }),
3561
+ memberError && /* @__PURE__ */ jsx11(Alert2, { severity: "error", sx: { mb: 1 }, onClose: () => setMemberError(null), children: memberError }),
3562
+ members.length === 0 ? /* @__PURE__ */ jsx11(Typography8, { 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__ */ jsx11(TableContainer2, { component: Paper4, variant: "outlined", sx: { borderRadius: 1.5, mb: 1.5 }, children: /* @__PURE__ */ jsxs10(Table2, { size: "small", sx: dataTableSx, children: [
3563
+ /* @__PURE__ */ jsx11(TableHead2, { children: /* @__PURE__ */ jsxs10(TableRow2, { children: [
3564
+ /* @__PURE__ */ jsx11(TableCell2, { children: "User" }),
3565
+ /* @__PURE__ */ jsx11(TableCell2, { align: "right", children: "Role" }),
3566
+ /* @__PURE__ */ jsx11(TableCell2, { align: "right", sx: { width: 40 } })
3567
+ ] }) }),
3568
+ /* @__PURE__ */ jsx11(TableBody2, { children: members.map((m) => /* @__PURE__ */ jsxs10(TableRow2, { children: [
3569
+ /* @__PURE__ */ jsx11(TableCell2, { children: m.user_email ? /* @__PURE__ */ jsxs10(Fragment4, { children: [
3570
+ /* @__PURE__ */ jsx11(Typography8, { variant: "body2", children: m.user_email }),
3571
+ /* @__PURE__ */ jsx11(
3572
+ Typography8,
3573
+ {
3574
+ variant: "caption",
3575
+ color: "text.secondary",
3576
+ sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 11 },
3577
+ children: m.user_id
3578
+ }
3579
+ )
3580
+ ] }) : /* @__PURE__ */ jsx11(
3581
+ Typography8,
3582
+ {
3583
+ variant: "body2",
3584
+ sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace" },
3585
+ children: m.user_id
3586
+ }
3587
+ ) }),
3588
+ /* @__PURE__ */ jsx11(TableCell2, { align: "right", children: /* @__PURE__ */ jsx11(StatusPill, { label: m.role, tone: m.role === "admin" ? "accent" : "neutral", dot: false }) }),
3589
+ /* @__PURE__ */ jsx11(TableCell2, { align: "right", children: (() => {
3590
+ const { disabled, reason } = isRemovalDisabled(m.user_id, m.role);
3591
+ return /* @__PURE__ */ jsx11(Tooltip4, { title: reason ?? "", children: /* @__PURE__ */ jsx11("span", { children: /* @__PURE__ */ jsx11(
3592
+ IconButton4,
3593
+ {
3594
+ edge: "end",
3595
+ size: "small",
3596
+ "aria-label": `remove ${m.user_id}`,
3597
+ disabled: memberBusy || disabled,
3598
+ onClick: () => handleRemoveMember(m.user_id),
3599
+ children: /* @__PURE__ */ jsx11(DeleteIcon, { fontSize: "small" })
3600
+ }
3601
+ ) }) });
3602
+ })() })
3603
+ ] }, m.user_id)) })
3604
+ ] }) }),
3605
+ /* @__PURE__ */ jsxs10(Box10, { sx: { display: "flex", gap: 1, alignItems: "flex-start", flexWrap: "wrap" }, children: [
3606
+ /* @__PURE__ */ jsx11(
3607
+ Autocomplete2,
3608
+ {
3609
+ freeSolo: true,
3610
+ autoHighlight: true,
3611
+ options: userOptions,
3612
+ loading: usersLoading,
3613
+ inputValue: memberQuery,
3614
+ onInputChange: (_, newInputValue, reason) => {
3615
+ setMemberQuery(newInputValue);
3616
+ if (reason === "input" || reason === "clear") {
3617
+ setMemberRef(newInputValue);
3618
+ handleUserSearch(newInputValue);
3619
+ }
3620
+ },
3621
+ onChange: (_, newValue) => {
3622
+ if (newValue && typeof newValue !== "string") {
3623
+ setMemberQuery(newValue.label);
3624
+ setMemberRef(newValue.ref);
3625
+ }
3626
+ },
3627
+ getOptionLabel: (o) => typeof o === "string" ? o : o.label,
3628
+ isOptionEqualToValue: (o, v) => o.ref === (typeof v === "string" ? v : v.ref),
3629
+ disabled: memberBusy,
3630
+ sx: { flex: "1 1 240px" },
3631
+ renderInput: (params) => /* @__PURE__ */ jsx11(
3632
+ TextField3,
3633
+ {
3634
+ ...params,
3635
+ label: "Add member",
3636
+ placeholder: "Search by name or email\u2026",
3637
+ size: "small",
3638
+ helperText: "Pick a catalog user, or paste a user entity ref",
3639
+ InputProps: {
3640
+ ...params.InputProps,
3641
+ endAdornment: /* @__PURE__ */ jsxs10(Fragment4, { children: [
3642
+ usersLoading ? /* @__PURE__ */ jsx11(CircularProgress3, { color: "inherit", size: 14 }) : null,
3643
+ params.InputProps.endAdornment
3644
+ ] })
3645
+ }
3646
+ }
3647
+ )
3648
+ }
3649
+ ),
3650
+ !membersOnly && /* @__PURE__ */ jsx11(
3651
+ TextField3,
3652
+ {
3653
+ label: "Max budget in team ($)",
3654
+ type: "number",
3655
+ value: memberBudget,
3656
+ onChange: (e) => setMemberBudget(e.target.value),
3657
+ disabled: memberBusy,
3658
+ size: "small",
3659
+ sx: { width: 150 }
3660
+ }
3661
+ ),
3662
+ /* @__PURE__ */ jsx11(
3663
+ Button4,
3664
+ {
3665
+ onClick: handleAddMember,
3666
+ disabled: memberBusy || !memberRef.trim(),
3667
+ variant: "outlined",
3668
+ sx: { mt: 0.5 },
3669
+ children: "Add"
3670
+ }
3671
+ )
3672
+ ] })
3673
+ ] })
3674
+ ]
3675
+ }
3676
+ ),
3677
+ !membersOnly && showKnowledgeBases && /* @__PURE__ */ jsxs10(Box10, { children: [
3584
3678
  /* @__PURE__ */ jsx11(Divider2, { sx: { my: 1 } }),
3585
3679
  /* @__PURE__ */ jsx11(Typography8, { variant: "subtitle2", sx: { mb: 1 }, children: "Knowledge Bases" }),
3586
3680
  /* @__PURE__ */ jsx11(Typography8, { variant: "body2", color: "text.secondary", sx: { mb: 1 }, children: "Attaching a knowledge base exposes its documents to every key in this team." }),
@@ -3607,7 +3701,7 @@ var init_ManageTeamDialog = __esm({
3607
3701
  }
3608
3702
  ) })
3609
3703
  ] }),
3610
- showMcpServers && /* @__PURE__ */ jsxs10(Box10, { children: [
3704
+ !membersOnly && showMcpServers && /* @__PURE__ */ jsxs10(Box10, { children: [
3611
3705
  /* @__PURE__ */ jsx11(Divider2, { sx: { my: 1 } }),
3612
3706
  /* @__PURE__ */ jsx11(Typography8, { variant: "subtitle2", sx: { mb: 1 }, children: "MCP Servers" }),
3613
3707
  /* @__PURE__ */ jsx11(Typography8, { 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." }),
@@ -3636,8 +3730,8 @@ var init_ManageTeamDialog = __esm({
3636
3730
  ] })
3637
3731
  ] }),
3638
3732
  /* @__PURE__ */ jsxs10(DialogActions3, { children: [
3639
- /* @__PURE__ */ jsx11(Button4, { onClick: onClose, disabled: submitting, children: "Cancel" }),
3640
- /* @__PURE__ */ jsx11(Button4, { onClick: handleSubmit, disabled: submitting, variant: "contained", children: submitting ? "Saving..." : "Save" })
3733
+ /* @__PURE__ */ jsx11(Button4, { onClick: onClose, disabled: submitting, children: membersOnly ? "Done" : "Cancel" }),
3734
+ !membersOnly && /* @__PURE__ */ jsx11(Button4, { onClick: handleSubmit, disabled: submitting, variant: "contained", children: submitting ? "Saving..." : submitLabel })
3641
3735
  ] })
3642
3736
  ] });
3643
3737
  };
@@ -3848,7 +3942,7 @@ import {
3848
3942
  XAxis,
3849
3943
  YAxis,
3850
3944
  CartesianGrid,
3851
- Tooltip as Tooltip4,
3945
+ Tooltip as Tooltip5,
3852
3946
  ResponsiveContainer,
3853
3947
  Legend,
3854
3948
  ReferenceLine,
@@ -4185,7 +4279,7 @@ var init_UsageStats = __esm({
4185
4279
  /* @__PURE__ */ jsx12(XAxis, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }),
4186
4280
  /* @__PURE__ */ jsx12(YAxis, { tickFormatter: fmtUsdCompact, width: 56, ...chart.axis }),
4187
4281
  /* @__PURE__ */ jsx12(
4188
- Tooltip4,
4282
+ Tooltip5,
4189
4283
  {
4190
4284
  cursor: chart.cursor,
4191
4285
  content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: fmtUsd, hideZero: true })
@@ -4211,7 +4305,7 @@ var init_UsageStats = __esm({
4211
4305
  /* @__PURE__ */ jsx12(CartesianGrid, { ...chart.grid }),
4212
4306
  /* @__PURE__ */ jsx12(XAxis, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }),
4213
4307
  /* @__PURE__ */ jsx12(YAxis, { tickFormatter: fmtCompact, width: 48, ...chart.axis }),
4214
- /* @__PURE__ */ jsx12(Tooltip4, { cursor: chart.cursor, content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: fmtInt }) }),
4308
+ /* @__PURE__ */ jsx12(Tooltip5, { cursor: chart.cursor, content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: fmtInt }) }),
4215
4309
  /* @__PURE__ */ jsx12(Legend, { ...chart.legend }),
4216
4310
  /* @__PURE__ */ jsx12(Area, { type: "monotone", dataKey: "promptTokens", name: "Input", stackId: "tok", stroke: SERIES.input, strokeWidth: 1.5, fill: SERIES.input, fillOpacity: 0.25 }),
4217
4311
  /* @__PURE__ */ jsx12(Area, { type: "monotone", dataKey: "completionTokens", name: "Output", stackId: "tok", stroke: SERIES.output, strokeWidth: 1.5, fill: SERIES.output, fillOpacity: 0.25 })
@@ -4220,7 +4314,7 @@ var init_UsageStats = __esm({
4220
4314
  /* @__PURE__ */ jsx12(CartesianGrid, { ...chart.grid }),
4221
4315
  /* @__PURE__ */ jsx12(XAxis, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }),
4222
4316
  /* @__PURE__ */ jsx12(YAxis, { tickFormatter: fmtCompact, width: 48, ...chart.axis }),
4223
- /* @__PURE__ */ jsx12(Tooltip4, { cursor: chart.cursor, content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: fmtInt }) }),
4317
+ /* @__PURE__ */ jsx12(Tooltip5, { cursor: chart.cursor, content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: fmtInt }) }),
4224
4318
  /* @__PURE__ */ jsx12(Legend, { ...chart.legend }),
4225
4319
  /* @__PURE__ */ jsx12(Bar, { dataKey: "successfulRequests", name: "Successful", fill: SERIES.success, stackId: "r" }),
4226
4320
  /* @__PURE__ */ jsx12(Bar, { dataKey: "failedRequests", name: "Failed", fill: SERIES.failure, stackId: "r", radius: [3, 3, 0, 0] })
@@ -4230,7 +4324,7 @@ var init_UsageStats = __esm({
4230
4324
  /* @__PURE__ */ jsx12(XAxis, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }),
4231
4325
  /* @__PURE__ */ jsx12(YAxis, { domain: [0, 100], tickFormatter: (v) => `${v}%`, width: 44, ...chart.axis }),
4232
4326
  /* @__PURE__ */ jsx12(
4233
- Tooltip4,
4327
+ Tooltip5,
4234
4328
  {
4235
4329
  cursor: chart.cursor,
4236
4330
  content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: (v) => `${v}%` })
@@ -4267,7 +4361,7 @@ var init_UsageStats = __esm({
4267
4361
  }
4268
4362
  ),
4269
4363
  /* @__PURE__ */ jsx12(
4270
- Tooltip4,
4364
+ Tooltip5,
4271
4365
  {
4272
4366
  cursor: chart.cursor,
4273
4367
  content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: fmtUsd })
@@ -4309,7 +4403,7 @@ var init_UsageStats = __esm({
4309
4403
  /* @__PURE__ */ jsx12(CartesianGrid, { ...chart.grid, vertical: true, horizontal: false }),
4310
4404
  /* @__PURE__ */ jsx12(XAxis, { type: "number", tickFormatter: fmtUsdCompact, ...chart.axis }),
4311
4405
  /* @__PURE__ */ jsx12(YAxis, { type: "category", dataKey: "model", width: 170, ...chart.axis, tick: { fontSize: 10.5, fill: chart.axis.tick.fill } }),
4312
- /* @__PURE__ */ jsx12(Tooltip4, { cursor: chart.cursor, content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: fmtUsd, labelFormatter: (l) => l }) }),
4406
+ /* @__PURE__ */ jsx12(Tooltip5, { cursor: chart.cursor, content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: fmtUsd, labelFormatter: (l) => l }) }),
4313
4407
  /* @__PURE__ */ jsx12(Bar, { dataKey: "spend", name: "Spend", radius: [0, 3, 3, 0], barSize: 14, children: topModelSpendBars.map((r) => /* @__PURE__ */ jsx12(Cell, { fill: chartColor(r.model) }, r.model)) })
4314
4408
  ] }) })
4315
4409
  }
@@ -4324,7 +4418,7 @@ var init_UsageStats = __esm({
4324
4418
  /* @__PURE__ */ jsx12(CartesianGrid, { ...chart.grid, vertical: true, horizontal: false }),
4325
4419
  /* @__PURE__ */ jsx12(XAxis, { type: "number", tickFormatter: fmtCompact, ...chart.axis }),
4326
4420
  /* @__PURE__ */ jsx12(YAxis, { type: "category", dataKey: "model", width: 170, ...chart.axis, tick: { fontSize: 10.5, fill: chart.axis.tick.fill } }),
4327
- /* @__PURE__ */ jsx12(Tooltip4, { cursor: chart.cursor, content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: fmtInt, labelFormatter: (l) => l }) }),
4421
+ /* @__PURE__ */ jsx12(Tooltip5, { cursor: chart.cursor, content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: fmtInt, labelFormatter: (l) => l }) }),
4328
4422
  /* @__PURE__ */ jsx12(Legend, { ...chart.legend }),
4329
4423
  /* @__PURE__ */ jsx12(Bar, { dataKey: "promptTokens", name: "Input", fill: SERIES.input, stackId: "t", barSize: 14 }),
4330
4424
  /* @__PURE__ */ jsx12(Bar, { dataKey: "completionTokens", name: "Output", fill: SERIES.output, stackId: "t", barSize: 14, radius: [0, 3, 3, 0] })
@@ -4356,7 +4450,7 @@ var init_UsageStats = __esm({
4356
4450
  /* @__PURE__ */ jsx12(CartesianGrid, { ...chart.grid, vertical: true, horizontal: false }),
4357
4451
  /* @__PURE__ */ jsx12(XAxis, { type: "number", tickFormatter: fmtUsdCompact, ...chart.axis }),
4358
4452
  /* @__PURE__ */ jsx12(YAxis, { type: "category", dataKey: "keyAlias", width: 160, ...chart.axis, tick: { fontSize: 10.5, fill: chart.axis.tick.fill } }),
4359
- /* @__PURE__ */ jsx12(Tooltip4, { cursor: chart.cursor, content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: fmtUsd, labelFormatter: (l) => l }) }),
4453
+ /* @__PURE__ */ jsx12(Tooltip5, { cursor: chart.cursor, content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: fmtUsd, labelFormatter: (l) => l }) }),
4360
4454
  /* @__PURE__ */ jsx12(Bar, { dataKey: "spend", name: "Spend", fill: SERIES.input, radius: [0, 3, 3, 0], barSize: 14 })
4361
4455
  ] }) })
4362
4456
  }
@@ -4891,14 +4985,15 @@ import IconButton6 from "@mui/material/IconButton";
4891
4985
  import Button6 from "@mui/material/Button";
4892
4986
  import CircularProgress4 from "@mui/material/CircularProgress";
4893
4987
  import { alpha as alpha10 } from "@mui/material/styles";
4894
- import { ExpandMore as ExpandMore3, Group, Add as Add2 } from "@mui/icons-material";
4988
+ import MuiTooltip from "@mui/material/Tooltip";
4989
+ import { ExpandMore as ExpandMore3, Group, Add as Add2, LockOutlined, GroupAddOutlined } from "@mui/icons-material";
4895
4990
  import {
4896
4991
  AreaChart as AreaChart2,
4897
4992
  Area as Area2,
4898
4993
  XAxis as XAxis2,
4899
4994
  YAxis as YAxis2,
4900
4995
  CartesianGrid as CartesianGrid2,
4901
- Tooltip as Tooltip5,
4996
+ Tooltip as Tooltip6,
4902
4997
  ResponsiveContainer as ResponsiveContainer2
4903
4998
  } from "recharts";
4904
4999
  import { Fragment as Fragment8, jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
@@ -4922,7 +5017,7 @@ var init_TeamUsage = __esm({
4922
5017
  "use strict";
4923
5018
  init_ui();
4924
5019
  fmtUsd2 = (n) => `$${(n ?? 0).toFixed(2)}`;
4925
- TeamCard = ({ team, usage, usageLoading, canManage, onEditTeam, onExpand }) => {
5020
+ TeamCard = ({ team, usage, usageLoading, canManage, onEditTeam, canManageMembers, onManageMembers, onExpand }) => {
4926
5021
  const [expanded, setExpanded] = useState9(false);
4927
5022
  const chart = useChartTheme();
4928
5023
  const handleExpandChange = (newExpanded) => {
@@ -4959,7 +5054,7 @@ var init_TeamUsage = __esm({
4959
5054
  /* @__PURE__ */ jsx15(XAxis2, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }),
4960
5055
  /* @__PURE__ */ jsx15(YAxis2, { tickFormatter: fmtUsdCompact, width: 52, ...chart.axis }),
4961
5056
  /* @__PURE__ */ jsx15(
4962
- Tooltip5,
5057
+ Tooltip6,
4963
5058
  {
4964
5059
  cursor: chart.cursor,
4965
5060
  content: /* @__PURE__ */ jsx15(ChartTooltip, { valueFormatter: (v) => `$${v.toFixed(4)}` })
@@ -5032,6 +5127,17 @@ var init_TeamUsage = __esm({
5032
5127
  children: "Edit"
5033
5128
  }
5034
5129
  ),
5130
+ canManageMembers && onManageMembers && !canManage && /* @__PURE__ */ jsx15(MuiTooltip, { title: "Add or remove members of this team", children: /* @__PURE__ */ jsx15(
5131
+ Button6,
5132
+ {
5133
+ size: "small",
5134
+ variant: "text",
5135
+ startIcon: /* @__PURE__ */ jsx15(GroupAddOutlined, {}),
5136
+ onClick: () => onManageMembers(team),
5137
+ sx: { textTransform: "none" },
5138
+ children: "Manage members"
5139
+ }
5140
+ ) }),
5035
5141
  /* @__PURE__ */ jsx15(
5036
5142
  IconButton6,
5037
5143
  {
@@ -5162,9 +5268,23 @@ var init_TeamUsage = __esm({
5162
5268
  onEditTeam,
5163
5269
  canCreate,
5164
5270
  onCreateTeam,
5165
- onTeamExpand
5271
+ onTeamExpand,
5272
+ canManageMembers,
5273
+ onManageMembers,
5274
+ readOnly
5166
5275
  }) => {
5167
- const createAction = canCreate && onCreateTeam ? /* @__PURE__ */ jsx15(
5276
+ const readOnlyBadge = /* @__PURE__ */ jsx15(MuiTooltip, { title: "Teams and memberships are managed in your identity provider. Changes made here would be overwritten, so editing is disabled.", children: /* @__PURE__ */ jsxs14(
5277
+ Box14,
5278
+ {
5279
+ component: "span",
5280
+ sx: { display: "inline-flex", alignItems: "center", gap: 0.5, color: "text.secondary", fontSize: 12.5 },
5281
+ children: [
5282
+ /* @__PURE__ */ jsx15(LockOutlined, { sx: { fontSize: 15 } }),
5283
+ "Synced from identity provider"
5284
+ ]
5285
+ }
5286
+ ) });
5287
+ const createButton = canCreate && onCreateTeam ? /* @__PURE__ */ jsx15(
5168
5288
  Button6,
5169
5289
  {
5170
5290
  variant: "contained",
@@ -5175,6 +5295,7 @@ var init_TeamUsage = __esm({
5175
5295
  children: "Create Team"
5176
5296
  }
5177
5297
  ) : void 0;
5298
+ const createAction = readOnly ? readOnlyBadge : createButton;
5178
5299
  if (loading) {
5179
5300
  return /* @__PURE__ */ jsxs14(SectionCard, { title: "Teams", actions: createAction, children: [
5180
5301
  /* @__PURE__ */ jsx15(Skeleton5, { variant: "rounded", height: 120, sx: { mb: 2 } }),
@@ -5204,6 +5325,8 @@ var init_TeamUsage = __esm({
5204
5325
  usageLoading: getTeamUsageLoading(team.team_id),
5205
5326
  canManage,
5206
5327
  onEditTeam,
5328
+ canManageMembers: canManageMembers?.(team),
5329
+ onManageMembers,
5207
5330
  onExpand: onTeamExpand
5208
5331
  },
5209
5332
  team.team_id
@@ -5233,7 +5356,7 @@ import TextField5 from "@mui/material/TextField";
5233
5356
  import InputAdornment3 from "@mui/material/InputAdornment";
5234
5357
  import { Search as Search2 } from "@mui/icons-material";
5235
5358
  import ContentCopyIcon from "@mui/icons-material/ContentCopy";
5236
- import Tooltip6 from "@mui/material/Tooltip";
5359
+ import Tooltip7 from "@mui/material/Tooltip";
5237
5360
  import Snackbar from "@mui/material/Snackbar";
5238
5361
  import Alert4 from "@mui/material/Alert";
5239
5362
  import { alpha as alpha11, useTheme as useTheme3 } from "@mui/material/styles";
@@ -5417,7 +5540,7 @@ var init_ModelsTable = __esm({
5417
5540
  ] }) }),
5418
5541
  /* @__PURE__ */ jsx16(TableBody5, { children: filteredModels.map((m) => /* @__PURE__ */ jsxs15(TableRow5, { hover: true, children: [
5419
5542
  /* @__PURE__ */ jsx16(TableCell5, { children: /* @__PURE__ */ jsxs15(Box15, { sx: { display: "flex", alignItems: "center", gap: 0.5 }, children: [
5420
- /* @__PURE__ */ jsx16(Tooltip6, { title: m.model_name, placement: "top", children: /* @__PURE__ */ jsx16(
5543
+ /* @__PURE__ */ jsx16(Tooltip7, { title: m.model_name, placement: "top", children: /* @__PURE__ */ jsx16(
5421
5544
  "span",
5422
5545
  {
5423
5546
  role: "button",
@@ -5429,7 +5552,7 @@ var init_ModelsTable = __esm({
5429
5552
  children: /* @__PURE__ */ jsx16(TagChip, { label: m.model_name, title: "Copy model ID" })
5430
5553
  }
5431
5554
  ) }),
5432
- /* @__PURE__ */ jsx16(Tooltip6, { title: "Copy model ID", children: /* @__PURE__ */ jsx16(
5555
+ /* @__PURE__ */ jsx16(Tooltip7, { title: "Copy model ID", children: /* @__PURE__ */ jsx16(
5433
5556
  Box15,
5434
5557
  {
5435
5558
  component: "span",
@@ -5852,6 +5975,19 @@ var init_feedback = __esm({
5852
5975
  }
5853
5976
  });
5854
5977
 
5978
+ // src/teamMemberManager.ts
5979
+ function isTeamMemberManager(team, userId, memberManagerRoles, readOnly) {
5980
+ if (!team || !userId || !memberManagerRoles || memberManagerRoles.length === 0 || readOnly) {
5981
+ return false;
5982
+ }
5983
+ return team.members_with_roles?.some((m) => m.user_id === userId && memberManagerRoles.includes(m.role)) ?? false;
5984
+ }
5985
+ var init_teamMemberManager = __esm({
5986
+ "src/teamMemberManager.ts"() {
5987
+ "use strict";
5988
+ }
5989
+ });
5990
+
5855
5991
  // src/unprovisionedCopy.ts
5856
5992
  function getUnprovisionedTitle() {
5857
5993
  return "Your LiteLLM account isn't set up yet";
@@ -5929,6 +6065,7 @@ var init_LiteLLMPage = __esm({
5929
6065
  init_dates();
5930
6066
  init_permissions();
5931
6067
  init_feedback();
6068
+ init_teamMemberManager();
5932
6069
  init_unprovisionedCopy();
5933
6070
  PERIOD_LS_KEY2 = "litellm_usage_period";
5934
6071
  PAGE_TABS = ["overview", "keys", "teams", "models", "audit"];
@@ -6374,6 +6511,8 @@ var init_LiteLLMPage = __esm({
6374
6511
  for (const t of managedTeams ?? []) teamsById.set(t.team_id, t);
6375
6512
  for (const t of teams ?? []) teamsById.set(t.team_id, t);
6376
6513
  const visibleTeams = Array.from(teamsById.values());
6514
+ const readOnly = liteLlmConfig?.teamManagement?.readOnly ?? false;
6515
+ const memberManagerRoles = liteLlmConfig?.teamManagement?.memberManagerRoles ?? [];
6377
6516
  return /* @__PURE__ */ jsx18(
6378
6517
  TeamUsage,
6379
6518
  {
@@ -6381,10 +6520,13 @@ var init_LiteLLMPage = __esm({
6381
6520
  loading: teamsLoading,
6382
6521
  getTeamUsage: (teamId) => teamUsageCache[teamId] ?? null,
6383
6522
  getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false,
6384
- canManage: teamMgmtEnabled && canManageTeam,
6523
+ canManage: teamMgmtEnabled && canManageTeam && !readOnly,
6385
6524
  onEditTeam: (t) => setManageTeam({ mode: "edit", team: t }),
6386
- canCreate: teamMgmtEnabled && canCreateTeam,
6525
+ canCreate: teamMgmtEnabled && canCreateTeam && !readOnly,
6387
6526
  onCreateTeam: () => setManageTeam({ mode: "create" }),
6527
+ canManageMembers: (t) => teamMgmtEnabled && canManageMembers && isTeamMemberManager(t, userInfo?.user_id, memberManagerRoles, readOnly),
6528
+ onManageMembers: (t) => setManageTeam({ mode: "members", team: t }),
6529
+ readOnly,
6388
6530
  onTeamExpand: (team) => loadTeamUsage(team.team_id)
6389
6531
  }
6390
6532
  );
@@ -6436,11 +6578,14 @@ var init_LiteLLMPage = __esm({
6436
6578
  {
6437
6579
  open: !!manageTeam,
6438
6580
  onClose: () => setManageTeam(null),
6439
- mode: manageTeam?.mode ?? "create",
6581
+ mode: manageTeam?.mode === "members" ? "edit" : manageTeam?.mode ?? "create",
6440
6582
  team: manageTeam?.team,
6441
6583
  allModels: allModels ?? [],
6442
6584
  config: liteLlmConfig,
6443
- onSubmit: async (payload) => {
6585
+ membersOnly: manageTeam?.mode === "members",
6586
+ currentUserId: userInfo?.user_id,
6587
+ memberManagerRoles: liteLlmConfig?.teamManagement?.memberManagerRoles,
6588
+ onSubmit: async (payload, initialMembers) => {
6444
6589
  try {
6445
6590
  if (manageTeam?.mode === "edit" && manageTeam.team) {
6446
6591
  const loadedAt = manageTeam.team.metadata?.updated_at_iso;
@@ -6450,7 +6595,23 @@ var init_LiteLLMPage = __esm({
6450
6595
  ...typeof loadedAt === "string" ? { expectedUpdatedAtIso: loadedAt } : {}
6451
6596
  });
6452
6597
  } else {
6453
- await api.createTeam(payload);
6598
+ const created = await api.createTeam(payload);
6599
+ const failed = [];
6600
+ for (const m of initialMembers ?? []) {
6601
+ try {
6602
+ await api.addTeamMember(created.team_id, m);
6603
+ } catch (e) {
6604
+ failed.push(`${m.userEntityRef} (${e.message})`);
6605
+ }
6606
+ }
6607
+ if (failed.length) {
6608
+ alertApi.post({
6609
+ message: `Team created, but some members could not be added: ${failed.join(", ")}`,
6610
+ severity: "warning"
6611
+ });
6612
+ refreshTeams();
6613
+ return;
6614
+ }
6454
6615
  }
6455
6616
  const alert = toastFor("teamSaveSuccess");
6456
6617
  if (alert) alertApi.post(alert);
@@ -6460,7 +6621,7 @@ var init_LiteLLMPage = __esm({
6460
6621
  if (alert) alertApi.post(alert);
6461
6622
  }
6462
6623
  },
6463
- canManageMembers: teamMgmtEnabled && canManageMembers,
6624
+ canManageMembers: teamMgmtEnabled && canManageMembers && !liteLlmConfig?.teamManagement?.readOnly,
6464
6625
  onAddMember: async (userEntityRef, maxBudgetInTeam) => {
6465
6626
  if (!manageTeam?.team) return;
6466
6627
  const updated = await api.addTeamMember(manageTeam.team.team_id, {
@@ -6584,7 +6745,7 @@ import FormControl2 from "@mui/material/FormControl";
6584
6745
  import Select2 from "@mui/material/Select";
6585
6746
  import MenuItem6 from "@mui/material/MenuItem";
6586
6747
  import Skeleton7 from "@mui/material/Skeleton";
6587
- import { AreaChart as AreaChart3, Area as Area3, XAxis as XAxis3, Tooltip as Tooltip7, ResponsiveContainer as ResponsiveContainer3 } from "recharts";
6748
+ import { AreaChart as AreaChart3, Area as Area3, XAxis as XAxis3, Tooltip as Tooltip8, ResponsiveContainer as ResponsiveContainer3 } from "recharts";
6588
6749
  import { useApi as useApi5 } from "@backstage/core-plugin-api";
6589
6750
  import { useRouteRef as useRouteRef2 } from "@backstage/frontend-plugin-api";
6590
6751
  import { Link as Link3 } from "@backstage/core-components";
@@ -6749,7 +6910,7 @@ var LiteLLMHomeWidget = ({
6749
6910
  sx: { mb: 0.5 },
6750
6911
  children: /* @__PURE__ */ jsx20(ResponsiveContainer3, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs18(AreaChart3, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 }, children: [
6751
6912
  /* @__PURE__ */ jsx20(XAxis3, { dataKey: "date", hide: true }),
6752
- /* @__PURE__ */ jsx20(Tooltip7, { content: /* @__PURE__ */ jsx20(ChartTooltip, { valueFormatter: fmtUsd }) }),
6913
+ /* @__PURE__ */ jsx20(Tooltip8, { content: /* @__PURE__ */ jsx20(ChartTooltip, { valueFormatter: fmtUsd }) }),
6753
6914
  /* @__PURE__ */ jsx20(
6754
6915
  Area3,
6755
6916
  {
@@ -6796,7 +6957,7 @@ import Collapse4 from "@mui/material/Collapse";
6796
6957
  import { useTheme as useTheme4 } from "@mui/material/styles";
6797
6958
  import useMediaQuery from "@mui/material/useMediaQuery";
6798
6959
  import { ExpandMore as ExpandMore4 } from "@mui/icons-material";
6799
- import { AreaChart as AreaChart4, Area as Area4, ResponsiveContainer as ResponsiveContainer4, Tooltip as Tooltip8, ReferenceLine as ReferenceLine2, XAxis as XAxis4 } from "recharts";
6960
+ import { AreaChart as AreaChart4, Area as Area4, ResponsiveContainer as ResponsiveContainer4, Tooltip as Tooltip9, ReferenceLine as ReferenceLine2, XAxis as XAxis4 } from "recharts";
6800
6961
  import { useApi as useApi6 } from "@backstage/core-plugin-api";
6801
6962
  import { Link as Link4 } from "@backstage/core-components";
6802
6963
 
@@ -7098,7 +7259,7 @@ var SpendStrip = ({ data, mtdSpend, loading, height, usageUnavailable = false, m
7098
7259
  }
7099
7260
  return /* @__PURE__ */ jsx21(ResponsiveContainer4, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs19(AreaChart4, { data, margin: { top: 4, right: 0, bottom: 0, left: 0 }, children: [
7100
7261
  /* @__PURE__ */ jsx21(XAxis4, { dataKey: "date", hide: true }),
7101
- /* @__PURE__ */ jsx21(Tooltip8, { content: /* @__PURE__ */ jsx21(ChartTooltip, { valueFormatter: fmtUsd }) }),
7262
+ /* @__PURE__ */ jsx21(Tooltip9, { content: /* @__PURE__ */ jsx21(ChartTooltip, { valueFormatter: fmtUsd }) }),
7102
7263
  maxBudget !== void 0 && /* @__PURE__ */ jsx21(
7103
7264
  ReferenceLine2,
7104
7265
  {