@acarmisc/backstage-plugin-litellm 0.31.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -2555,9 +2555,22 @@ var init_KeyFormDialog = __esm({
2555
2555
  setCloseWithoutCopyConfirm(false);
2556
2556
  setTouched({});
2557
2557
  setSubmitAttempted(false);
2558
- onGetConfig().then(setConfig).catch(() => setConfig(null));
2559
2558
  }
2560
- }, [open, isCreate, keyToEdit, username, keyGenerationSettings?.allowUnlimitedBudget, onGetConfig]);
2559
+ }, [open, isCreate, keyToEdit, username, keyGenerationSettings?.allowUnlimitedBudget]);
2560
+ const onGetConfigRef = useRef3(onGetConfig);
2561
+ onGetConfigRef.current = onGetConfig;
2562
+ useEffect3(() => {
2563
+ if (!open) return void 0;
2564
+ let cancelled = false;
2565
+ onGetConfigRef.current().then((c) => {
2566
+ if (!cancelled) setConfig(c);
2567
+ }).catch(() => {
2568
+ if (!cancelled) setConfig(null);
2569
+ });
2570
+ return () => {
2571
+ cancelled = true;
2572
+ };
2573
+ }, [open]);
2561
2574
  const allowUnlimitedBudget = keyGenerationSettings?.allowUnlimitedBudget ?? false;
2562
2575
  const teamRequired = keyGenerationSettings?.teamRequired ?? true;
2563
2576
  const formData = isCreate ? generateForm : editFormState;
@@ -3111,6 +3124,7 @@ import Box10 from "@mui/material/Box";
3111
3124
  import Divider2 from "@mui/material/Divider";
3112
3125
  import Typography8 from "@mui/material/Typography";
3113
3126
  import IconButton4 from "@mui/material/IconButton";
3127
+ import Tooltip4 from "@mui/material/Tooltip";
3114
3128
  import Paper4 from "@mui/material/Paper";
3115
3129
  import Table2 from "@mui/material/Table";
3116
3130
  import TableBody2 from "@mui/material/TableBody";
@@ -3153,7 +3167,10 @@ var init_ManageTeamDialog = __esm({
3153
3167
  onSaveKnowledgeBases,
3154
3168
  canManageMcpServers,
3155
3169
  mcpServers,
3156
- onSaveMcpServers
3170
+ onSaveMcpServers,
3171
+ membersOnly,
3172
+ currentUserId,
3173
+ memberManagerRoles
3157
3174
  }) => {
3158
3175
  const [alias, setAlias] = useState6("");
3159
3176
  const [models, setModels] = useState6([]);
@@ -3178,7 +3195,7 @@ var init_ManageTeamDialog = __esm({
3178
3195
  const hideBudgetForManagers = config?.display?.hideTeamBudgetForManagers ?? false;
3179
3196
  const budgetWriteOnly = hideBudgetForManagers && mode === "edit";
3180
3197
  const members = useMemo4(() => team?.members_with_roles ?? [], [team]);
3181
- const showMembers = mode === "edit" && !!canManageMembers;
3198
+ const showMembers = mode === "edit" && !!canManageMembers || membersOnly;
3182
3199
  const catalogApi = useApi3(catalogApiRef);
3183
3200
  const [searchUsers, setSearchUsers] = useState6([]);
3184
3201
  const [usersLoading, setUsersLoading] = useState6(false);
@@ -3327,7 +3344,7 @@ var init_ManageTeamDialog = __esm({
3327
3344
  const handleAddMember = async () => {
3328
3345
  if (!onAddMember || !memberRef.trim()) return;
3329
3346
  setMemberError(null);
3330
- const budget = memberBudget ? parseFloat(memberBudget) : void 0;
3347
+ const budget = !membersOnly && memberBudget ? parseFloat(memberBudget) : void 0;
3331
3348
  if (budget !== void 0 && (isNaN(budget) || budget <= 0)) {
3332
3349
  setMemberError("Max budget in team must be a positive number");
3333
3350
  return;
@@ -3388,72 +3405,95 @@ var init_ManageTeamDialog = __esm({
3388
3405
  const showMcpServers = mode === "edit" && !!canManageMcpServers;
3389
3406
  const mcpOptions = (mcpServers ?? []).map((s) => s.id);
3390
3407
  const mcpLabel = (id) => (mcpServers ?? []).find((s) => s.id === id)?.name ?? id;
3408
+ const isRemovalDisabled = (memberUserId, memberRole) => {
3409
+ if (!membersOnly) return { disabled: false };
3410
+ if (memberUserId === currentUserId) {
3411
+ return { disabled: true, reason: "You can't remove yourself" };
3412
+ }
3413
+ if (memberManagerRoles?.includes(memberRole)) {
3414
+ return { disabled: true, reason: `Only platform admins can remove a team ${memberRole}` };
3415
+ }
3416
+ return { disabled: false };
3417
+ };
3418
+ const getDialogTitle = () => {
3419
+ if (membersOnly) {
3420
+ return `Manage members \u2014 ${team?.team_alias || "Untitled team"}`;
3421
+ }
3422
+ return mode === "create" ? "Create Team" : "Edit Team";
3423
+ };
3391
3424
  return /* @__PURE__ */ jsxs10(Dialog3, { open, onClose, maxWidth: "sm", fullWidth: true, children: [
3392
- /* @__PURE__ */ jsx11(DialogTitle3, { children: mode === "create" ? "Create Team" : "Edit Team" }),
3425
+ /* @__PURE__ */ jsx11(DialogTitle3, { children: getDialogTitle() }),
3393
3426
  /* @__PURE__ */ jsxs10(DialogContent3, { sx: { pt: 2, display: "flex", flexDirection: "column", gap: 2 }, children: [
3394
3427
  error && /* @__PURE__ */ jsx11(Alert2, { severity: "error", children: error }),
3395
- /* @__PURE__ */ jsx11(
3396
- TextField3,
3397
- {
3398
- label: "Team Alias",
3399
- value: alias,
3400
- onChange: (e) => setAlias(e.target.value),
3401
- disabled: submitting,
3402
- fullWidth: true
3403
- }
3404
- ),
3405
- /* @__PURE__ */ jsx11(
3406
- Autocomplete2,
3407
- {
3408
- multiple: true,
3409
- options: modelNames,
3410
- value: models,
3411
- onChange: (_, newValue) => setModels(newValue),
3412
- disabled: submitting,
3413
- renderInput: (params) => /* @__PURE__ */ jsx11(
3414
- TextField3,
3415
- {
3416
- ...params,
3417
- label: "Models",
3418
- required: true,
3419
- 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.)"
3420
- }
3421
- )
3422
- }
3423
- ),
3424
- /* @__PURE__ */ jsx11(
3425
- TextField3,
3426
- {
3427
- label: budgetWriteOnly ? "New Max Budget ($)" : "Max Budget ($)",
3428
- type: "number",
3429
- value: maxBudget,
3430
- onChange: (e) => setMaxBudget(e.target.value),
3431
- disabled: submitting || unlimited,
3432
- inputProps: { min: 0, max: maxBudgetCeiling },
3433
- helperText: budgetWriteOnly ? "Current budget is hidden \u2014 leave blank to keep it, or enter a new value" : `Maximum: $${maxBudgetCeiling}`,
3434
- fullWidth: true
3435
- }
3436
- ),
3437
- allowUnlimitedBudget && /* @__PURE__ */ jsx11(
3438
- FormControlLabel2,
3439
- {
3440
- control: /* @__PURE__ */ jsx11(Checkbox2, { checked: unlimited, onChange: (e) => setUnlimited(e.target.checked), disabled: submitting }),
3441
- label: "Unlimited Budget"
3442
- }
3443
- ),
3444
- /* @__PURE__ */ jsx11(
3445
- TextField3,
3446
- {
3447
- select: true,
3448
- label: "Budget Duration",
3449
- value: budgetDuration,
3450
- onChange: (e) => setBudgetDuration(e.target.value),
3451
- disabled: submitting || unlimited,
3452
- helperText: "Spend-reset period for the team budget",
3453
- fullWidth: true,
3454
- children: durationOptions.map((o) => /* @__PURE__ */ jsx11(MenuItem2, { value: o.value, children: o.label }, o.value))
3455
- }
3456
- ),
3428
+ membersOnly && /* @__PURE__ */ jsxs10(Alert2, { severity: "info", children: [
3429
+ "As a team ",
3430
+ team?.members_with_roles?.find((m) => m.user_id === currentUserId)?.role || "member",
3431
+ " you can add or remove members. Budgets and models are managed by your platform admins."
3432
+ ] }),
3433
+ !membersOnly && /* @__PURE__ */ jsxs10(Fragment4, { children: [
3434
+ /* @__PURE__ */ jsx11(
3435
+ TextField3,
3436
+ {
3437
+ label: "Team Alias",
3438
+ value: alias,
3439
+ onChange: (e) => setAlias(e.target.value),
3440
+ disabled: submitting,
3441
+ fullWidth: true
3442
+ }
3443
+ ),
3444
+ /* @__PURE__ */ jsx11(
3445
+ Autocomplete2,
3446
+ {
3447
+ multiple: true,
3448
+ options: modelNames,
3449
+ value: models,
3450
+ onChange: (_, newValue) => setModels(newValue),
3451
+ disabled: submitting,
3452
+ renderInput: (params) => /* @__PURE__ */ jsx11(
3453
+ TextField3,
3454
+ {
3455
+ ...params,
3456
+ label: "Models",
3457
+ required: true,
3458
+ 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.)"
3459
+ }
3460
+ )
3461
+ }
3462
+ ),
3463
+ /* @__PURE__ */ jsx11(
3464
+ TextField3,
3465
+ {
3466
+ label: budgetWriteOnly ? "New Max Budget ($)" : "Max Budget ($)",
3467
+ type: "number",
3468
+ value: maxBudget,
3469
+ onChange: (e) => setMaxBudget(e.target.value),
3470
+ disabled: submitting || unlimited,
3471
+ inputProps: { min: 0, max: maxBudgetCeiling },
3472
+ helperText: budgetWriteOnly ? "Current budget is hidden \u2014 leave blank to keep it, or enter a new value" : `Maximum: $${maxBudgetCeiling}`,
3473
+ fullWidth: true
3474
+ }
3475
+ ),
3476
+ allowUnlimitedBudget && /* @__PURE__ */ jsx11(
3477
+ FormControlLabel2,
3478
+ {
3479
+ control: /* @__PURE__ */ jsx11(Checkbox2, { checked: unlimited, onChange: (e) => setUnlimited(e.target.checked), disabled: submitting }),
3480
+ label: "Unlimited Budget"
3481
+ }
3482
+ ),
3483
+ /* @__PURE__ */ jsx11(
3484
+ TextField3,
3485
+ {
3486
+ select: true,
3487
+ label: "Budget Duration",
3488
+ value: budgetDuration,
3489
+ onChange: (e) => setBudgetDuration(e.target.value),
3490
+ disabled: submitting || unlimited,
3491
+ helperText: "Spend-reset period for the team budget",
3492
+ fullWidth: true,
3493
+ children: durationOptions.map((o) => /* @__PURE__ */ jsx11(MenuItem2, { value: o.value, children: o.label }, o.value))
3494
+ }
3495
+ )
3496
+ ] }),
3457
3497
  showMembers && /* @__PURE__ */ jsxs10(Box10, { children: [
3458
3498
  /* @__PURE__ */ jsx11(Divider2, { sx: { my: 1 } }),
3459
3499
  /* @__PURE__ */ jsx11(Typography8, { variant: "subtitle2", sx: { mb: 1 }, children: "Members" }),
@@ -3485,17 +3525,20 @@ var init_ManageTeamDialog = __esm({
3485
3525
  }
3486
3526
  ) }),
3487
3527
  /* @__PURE__ */ jsx11(TableCell2, { align: "right", children: /* @__PURE__ */ jsx11(StatusPill, { label: m.role, tone: m.role === "admin" ? "accent" : "neutral", dot: false }) }),
3488
- /* @__PURE__ */ jsx11(TableCell2, { align: "right", children: /* @__PURE__ */ jsx11(
3489
- IconButton4,
3490
- {
3491
- edge: "end",
3492
- size: "small",
3493
- "aria-label": `remove ${m.user_id}`,
3494
- disabled: memberBusy,
3495
- onClick: () => handleRemoveMember(m.user_id),
3496
- children: /* @__PURE__ */ jsx11(DeleteIcon, { fontSize: "small" })
3497
- }
3498
- ) })
3528
+ /* @__PURE__ */ jsx11(TableCell2, { align: "right", children: (() => {
3529
+ const { disabled, reason } = isRemovalDisabled(m.user_id, m.role);
3530
+ return /* @__PURE__ */ jsx11(Tooltip4, { title: reason ?? "", children: /* @__PURE__ */ jsx11("span", { children: /* @__PURE__ */ jsx11(
3531
+ IconButton4,
3532
+ {
3533
+ edge: "end",
3534
+ size: "small",
3535
+ "aria-label": `remove ${m.user_id}`,
3536
+ disabled: memberBusy || disabled,
3537
+ onClick: () => handleRemoveMember(m.user_id),
3538
+ children: /* @__PURE__ */ jsx11(DeleteIcon, { fontSize: "small" })
3539
+ }
3540
+ ) }) });
3541
+ })() })
3499
3542
  ] }, m.user_id)) })
3500
3543
  ] }) }),
3501
3544
  /* @__PURE__ */ jsxs10(Box10, { sx: { display: "flex", gap: 1, alignItems: "flex-start", flexWrap: "wrap" }, children: [
@@ -3543,7 +3586,7 @@ var init_ManageTeamDialog = __esm({
3543
3586
  )
3544
3587
  }
3545
3588
  ),
3546
- /* @__PURE__ */ jsx11(
3589
+ !membersOnly && /* @__PURE__ */ jsx11(
3547
3590
  TextField3,
3548
3591
  {
3549
3592
  label: "Max budget in team ($)",
@@ -3567,7 +3610,7 @@ var init_ManageTeamDialog = __esm({
3567
3610
  )
3568
3611
  ] })
3569
3612
  ] }),
3570
- showKnowledgeBases && /* @__PURE__ */ jsxs10(Box10, { children: [
3613
+ !membersOnly && showKnowledgeBases && /* @__PURE__ */ jsxs10(Box10, { children: [
3571
3614
  /* @__PURE__ */ jsx11(Divider2, { sx: { my: 1 } }),
3572
3615
  /* @__PURE__ */ jsx11(Typography8, { variant: "subtitle2", sx: { mb: 1 }, children: "Knowledge Bases" }),
3573
3616
  /* @__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." }),
@@ -3594,7 +3637,7 @@ var init_ManageTeamDialog = __esm({
3594
3637
  }
3595
3638
  ) })
3596
3639
  ] }),
3597
- showMcpServers && /* @__PURE__ */ jsxs10(Box10, { children: [
3640
+ !membersOnly && showMcpServers && /* @__PURE__ */ jsxs10(Box10, { children: [
3598
3641
  /* @__PURE__ */ jsx11(Divider2, { sx: { my: 1 } }),
3599
3642
  /* @__PURE__ */ jsx11(Typography8, { variant: "subtitle2", sx: { mb: 1 }, children: "MCP Servers" }),
3600
3643
  /* @__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." }),
@@ -3623,8 +3666,8 @@ var init_ManageTeamDialog = __esm({
3623
3666
  ] })
3624
3667
  ] }),
3625
3668
  /* @__PURE__ */ jsxs10(DialogActions3, { children: [
3626
- /* @__PURE__ */ jsx11(Button4, { onClick: onClose, disabled: submitting, children: "Cancel" }),
3627
- /* @__PURE__ */ jsx11(Button4, { onClick: handleSubmit, disabled: submitting, variant: "contained", children: submitting ? "Saving..." : "Save" })
3669
+ /* @__PURE__ */ jsx11(Button4, { onClick: onClose, disabled: submitting, children: membersOnly ? "Done" : "Cancel" }),
3670
+ !membersOnly && /* @__PURE__ */ jsx11(Button4, { onClick: handleSubmit, disabled: submitting, variant: "contained", children: submitting ? "Saving..." : "Save" })
3628
3671
  ] })
3629
3672
  ] });
3630
3673
  };
@@ -3835,7 +3878,7 @@ import {
3835
3878
  XAxis,
3836
3879
  YAxis,
3837
3880
  CartesianGrid,
3838
- Tooltip as Tooltip4,
3881
+ Tooltip as Tooltip5,
3839
3882
  ResponsiveContainer,
3840
3883
  Legend,
3841
3884
  ReferenceLine,
@@ -4172,7 +4215,7 @@ var init_UsageStats = __esm({
4172
4215
  /* @__PURE__ */ jsx12(XAxis, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }),
4173
4216
  /* @__PURE__ */ jsx12(YAxis, { tickFormatter: fmtUsdCompact, width: 56, ...chart.axis }),
4174
4217
  /* @__PURE__ */ jsx12(
4175
- Tooltip4,
4218
+ Tooltip5,
4176
4219
  {
4177
4220
  cursor: chart.cursor,
4178
4221
  content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: fmtUsd, hideZero: true })
@@ -4198,7 +4241,7 @@ var init_UsageStats = __esm({
4198
4241
  /* @__PURE__ */ jsx12(CartesianGrid, { ...chart.grid }),
4199
4242
  /* @__PURE__ */ jsx12(XAxis, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }),
4200
4243
  /* @__PURE__ */ jsx12(YAxis, { tickFormatter: fmtCompact, width: 48, ...chart.axis }),
4201
- /* @__PURE__ */ jsx12(Tooltip4, { cursor: chart.cursor, content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: fmtInt }) }),
4244
+ /* @__PURE__ */ jsx12(Tooltip5, { cursor: chart.cursor, content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: fmtInt }) }),
4202
4245
  /* @__PURE__ */ jsx12(Legend, { ...chart.legend }),
4203
4246
  /* @__PURE__ */ jsx12(Area, { type: "monotone", dataKey: "promptTokens", name: "Input", stackId: "tok", stroke: SERIES.input, strokeWidth: 1.5, fill: SERIES.input, fillOpacity: 0.25 }),
4204
4247
  /* @__PURE__ */ jsx12(Area, { type: "monotone", dataKey: "completionTokens", name: "Output", stackId: "tok", stroke: SERIES.output, strokeWidth: 1.5, fill: SERIES.output, fillOpacity: 0.25 })
@@ -4207,7 +4250,7 @@ var init_UsageStats = __esm({
4207
4250
  /* @__PURE__ */ jsx12(CartesianGrid, { ...chart.grid }),
4208
4251
  /* @__PURE__ */ jsx12(XAxis, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }),
4209
4252
  /* @__PURE__ */ jsx12(YAxis, { tickFormatter: fmtCompact, width: 48, ...chart.axis }),
4210
- /* @__PURE__ */ jsx12(Tooltip4, { cursor: chart.cursor, content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: fmtInt }) }),
4253
+ /* @__PURE__ */ jsx12(Tooltip5, { cursor: chart.cursor, content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: fmtInt }) }),
4211
4254
  /* @__PURE__ */ jsx12(Legend, { ...chart.legend }),
4212
4255
  /* @__PURE__ */ jsx12(Bar, { dataKey: "successfulRequests", name: "Successful", fill: SERIES.success, stackId: "r" }),
4213
4256
  /* @__PURE__ */ jsx12(Bar, { dataKey: "failedRequests", name: "Failed", fill: SERIES.failure, stackId: "r", radius: [3, 3, 0, 0] })
@@ -4217,7 +4260,7 @@ var init_UsageStats = __esm({
4217
4260
  /* @__PURE__ */ jsx12(XAxis, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }),
4218
4261
  /* @__PURE__ */ jsx12(YAxis, { domain: [0, 100], tickFormatter: (v) => `${v}%`, width: 44, ...chart.axis }),
4219
4262
  /* @__PURE__ */ jsx12(
4220
- Tooltip4,
4263
+ Tooltip5,
4221
4264
  {
4222
4265
  cursor: chart.cursor,
4223
4266
  content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: (v) => `${v}%` })
@@ -4254,7 +4297,7 @@ var init_UsageStats = __esm({
4254
4297
  }
4255
4298
  ),
4256
4299
  /* @__PURE__ */ jsx12(
4257
- Tooltip4,
4300
+ Tooltip5,
4258
4301
  {
4259
4302
  cursor: chart.cursor,
4260
4303
  content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: fmtUsd })
@@ -4296,7 +4339,7 @@ var init_UsageStats = __esm({
4296
4339
  /* @__PURE__ */ jsx12(CartesianGrid, { ...chart.grid, vertical: true, horizontal: false }),
4297
4340
  /* @__PURE__ */ jsx12(XAxis, { type: "number", tickFormatter: fmtUsdCompact, ...chart.axis }),
4298
4341
  /* @__PURE__ */ jsx12(YAxis, { type: "category", dataKey: "model", width: 170, ...chart.axis, tick: { fontSize: 10.5, fill: chart.axis.tick.fill } }),
4299
- /* @__PURE__ */ jsx12(Tooltip4, { cursor: chart.cursor, content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: fmtUsd, labelFormatter: (l) => l }) }),
4342
+ /* @__PURE__ */ jsx12(Tooltip5, { cursor: chart.cursor, content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: fmtUsd, labelFormatter: (l) => l }) }),
4300
4343
  /* @__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)) })
4301
4344
  ] }) })
4302
4345
  }
@@ -4311,7 +4354,7 @@ var init_UsageStats = __esm({
4311
4354
  /* @__PURE__ */ jsx12(CartesianGrid, { ...chart.grid, vertical: true, horizontal: false }),
4312
4355
  /* @__PURE__ */ jsx12(XAxis, { type: "number", tickFormatter: fmtCompact, ...chart.axis }),
4313
4356
  /* @__PURE__ */ jsx12(YAxis, { type: "category", dataKey: "model", width: 170, ...chart.axis, tick: { fontSize: 10.5, fill: chart.axis.tick.fill } }),
4314
- /* @__PURE__ */ jsx12(Tooltip4, { cursor: chart.cursor, content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: fmtInt, labelFormatter: (l) => l }) }),
4357
+ /* @__PURE__ */ jsx12(Tooltip5, { cursor: chart.cursor, content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: fmtInt, labelFormatter: (l) => l }) }),
4315
4358
  /* @__PURE__ */ jsx12(Legend, { ...chart.legend }),
4316
4359
  /* @__PURE__ */ jsx12(Bar, { dataKey: "promptTokens", name: "Input", fill: SERIES.input, stackId: "t", barSize: 14 }),
4317
4360
  /* @__PURE__ */ jsx12(Bar, { dataKey: "completionTokens", name: "Output", fill: SERIES.output, stackId: "t", barSize: 14, radius: [0, 3, 3, 0] })
@@ -4343,7 +4386,7 @@ var init_UsageStats = __esm({
4343
4386
  /* @__PURE__ */ jsx12(CartesianGrid, { ...chart.grid, vertical: true, horizontal: false }),
4344
4387
  /* @__PURE__ */ jsx12(XAxis, { type: "number", tickFormatter: fmtUsdCompact, ...chart.axis }),
4345
4388
  /* @__PURE__ */ jsx12(YAxis, { type: "category", dataKey: "keyAlias", width: 160, ...chart.axis, tick: { fontSize: 10.5, fill: chart.axis.tick.fill } }),
4346
- /* @__PURE__ */ jsx12(Tooltip4, { cursor: chart.cursor, content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: fmtUsd, labelFormatter: (l) => l }) }),
4389
+ /* @__PURE__ */ jsx12(Tooltip5, { cursor: chart.cursor, content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: fmtUsd, labelFormatter: (l) => l }) }),
4347
4390
  /* @__PURE__ */ jsx12(Bar, { dataKey: "spend", name: "Spend", fill: SERIES.input, radius: [0, 3, 3, 0], barSize: 14 })
4348
4391
  ] }) })
4349
4392
  }
@@ -4878,14 +4921,15 @@ import IconButton6 from "@mui/material/IconButton";
4878
4921
  import Button6 from "@mui/material/Button";
4879
4922
  import CircularProgress4 from "@mui/material/CircularProgress";
4880
4923
  import { alpha as alpha10 } from "@mui/material/styles";
4881
- import { ExpandMore as ExpandMore3, Group, Add as Add2 } from "@mui/icons-material";
4924
+ import MuiTooltip from "@mui/material/Tooltip";
4925
+ import { ExpandMore as ExpandMore3, Group, Add as Add2, LockOutlined, GroupAddOutlined } from "@mui/icons-material";
4882
4926
  import {
4883
4927
  AreaChart as AreaChart2,
4884
4928
  Area as Area2,
4885
4929
  XAxis as XAxis2,
4886
4930
  YAxis as YAxis2,
4887
4931
  CartesianGrid as CartesianGrid2,
4888
- Tooltip as Tooltip5,
4932
+ Tooltip as Tooltip6,
4889
4933
  ResponsiveContainer as ResponsiveContainer2
4890
4934
  } from "recharts";
4891
4935
  import { Fragment as Fragment8, jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
@@ -4909,7 +4953,7 @@ var init_TeamUsage = __esm({
4909
4953
  "use strict";
4910
4954
  init_ui();
4911
4955
  fmtUsd2 = (n) => `$${(n ?? 0).toFixed(2)}`;
4912
- TeamCard = ({ team, usage, usageLoading, canManage, onEditTeam, onExpand }) => {
4956
+ TeamCard = ({ team, usage, usageLoading, canManage, onEditTeam, canManageMembers, onManageMembers, onExpand }) => {
4913
4957
  const [expanded, setExpanded] = useState9(false);
4914
4958
  const chart = useChartTheme();
4915
4959
  const handleExpandChange = (newExpanded) => {
@@ -4946,7 +4990,7 @@ var init_TeamUsage = __esm({
4946
4990
  /* @__PURE__ */ jsx15(XAxis2, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }),
4947
4991
  /* @__PURE__ */ jsx15(YAxis2, { tickFormatter: fmtUsdCompact, width: 52, ...chart.axis }),
4948
4992
  /* @__PURE__ */ jsx15(
4949
- Tooltip5,
4993
+ Tooltip6,
4950
4994
  {
4951
4995
  cursor: chart.cursor,
4952
4996
  content: /* @__PURE__ */ jsx15(ChartTooltip, { valueFormatter: (v) => `$${v.toFixed(4)}` })
@@ -5019,6 +5063,17 @@ var init_TeamUsage = __esm({
5019
5063
  children: "Edit"
5020
5064
  }
5021
5065
  ),
5066
+ canManageMembers && onManageMembers && !canManage && /* @__PURE__ */ jsx15(MuiTooltip, { title: "Add or remove members of this team", children: /* @__PURE__ */ jsx15(
5067
+ Button6,
5068
+ {
5069
+ size: "small",
5070
+ variant: "text",
5071
+ startIcon: /* @__PURE__ */ jsx15(GroupAddOutlined, {}),
5072
+ onClick: () => onManageMembers(team),
5073
+ sx: { textTransform: "none" },
5074
+ children: "Manage members"
5075
+ }
5076
+ ) }),
5022
5077
  /* @__PURE__ */ jsx15(
5023
5078
  IconButton6,
5024
5079
  {
@@ -5149,9 +5204,23 @@ var init_TeamUsage = __esm({
5149
5204
  onEditTeam,
5150
5205
  canCreate,
5151
5206
  onCreateTeam,
5152
- onTeamExpand
5207
+ onTeamExpand,
5208
+ canManageMembers,
5209
+ onManageMembers,
5210
+ readOnly
5153
5211
  }) => {
5154
- const createAction = canCreate && onCreateTeam ? /* @__PURE__ */ jsx15(
5212
+ 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(
5213
+ Box14,
5214
+ {
5215
+ component: "span",
5216
+ sx: { display: "inline-flex", alignItems: "center", gap: 0.5, color: "text.secondary", fontSize: 12.5 },
5217
+ children: [
5218
+ /* @__PURE__ */ jsx15(LockOutlined, { sx: { fontSize: 15 } }),
5219
+ "Synced from identity provider"
5220
+ ]
5221
+ }
5222
+ ) });
5223
+ const createButton = canCreate && onCreateTeam ? /* @__PURE__ */ jsx15(
5155
5224
  Button6,
5156
5225
  {
5157
5226
  variant: "contained",
@@ -5162,6 +5231,7 @@ var init_TeamUsage = __esm({
5162
5231
  children: "Create Team"
5163
5232
  }
5164
5233
  ) : void 0;
5234
+ const createAction = readOnly ? readOnlyBadge : createButton;
5165
5235
  if (loading) {
5166
5236
  return /* @__PURE__ */ jsxs14(SectionCard, { title: "Teams", actions: createAction, children: [
5167
5237
  /* @__PURE__ */ jsx15(Skeleton5, { variant: "rounded", height: 120, sx: { mb: 2 } }),
@@ -5191,6 +5261,8 @@ var init_TeamUsage = __esm({
5191
5261
  usageLoading: getTeamUsageLoading(team.team_id),
5192
5262
  canManage,
5193
5263
  onEditTeam,
5264
+ canManageMembers: canManageMembers?.(team),
5265
+ onManageMembers,
5194
5266
  onExpand: onTeamExpand
5195
5267
  },
5196
5268
  team.team_id
@@ -5220,7 +5292,7 @@ import TextField5 from "@mui/material/TextField";
5220
5292
  import InputAdornment3 from "@mui/material/InputAdornment";
5221
5293
  import { Search as Search2 } from "@mui/icons-material";
5222
5294
  import ContentCopyIcon from "@mui/icons-material/ContentCopy";
5223
- import Tooltip6 from "@mui/material/Tooltip";
5295
+ import Tooltip7 from "@mui/material/Tooltip";
5224
5296
  import Snackbar from "@mui/material/Snackbar";
5225
5297
  import Alert4 from "@mui/material/Alert";
5226
5298
  import { alpha as alpha11, useTheme as useTheme3 } from "@mui/material/styles";
@@ -5404,7 +5476,7 @@ var init_ModelsTable = __esm({
5404
5476
  ] }) }),
5405
5477
  /* @__PURE__ */ jsx16(TableBody5, { children: filteredModels.map((m) => /* @__PURE__ */ jsxs15(TableRow5, { hover: true, children: [
5406
5478
  /* @__PURE__ */ jsx16(TableCell5, { children: /* @__PURE__ */ jsxs15(Box15, { sx: { display: "flex", alignItems: "center", gap: 0.5 }, children: [
5407
- /* @__PURE__ */ jsx16(Tooltip6, { title: m.model_name, placement: "top", children: /* @__PURE__ */ jsx16(
5479
+ /* @__PURE__ */ jsx16(Tooltip7, { title: m.model_name, placement: "top", children: /* @__PURE__ */ jsx16(
5408
5480
  "span",
5409
5481
  {
5410
5482
  role: "button",
@@ -5416,7 +5488,7 @@ var init_ModelsTable = __esm({
5416
5488
  children: /* @__PURE__ */ jsx16(TagChip, { label: m.model_name, title: "Copy model ID" })
5417
5489
  }
5418
5490
  ) }),
5419
- /* @__PURE__ */ jsx16(Tooltip6, { title: "Copy model ID", children: /* @__PURE__ */ jsx16(
5491
+ /* @__PURE__ */ jsx16(Tooltip7, { title: "Copy model ID", children: /* @__PURE__ */ jsx16(
5420
5492
  Box15,
5421
5493
  {
5422
5494
  component: "span",
@@ -5839,6 +5911,19 @@ var init_feedback = __esm({
5839
5911
  }
5840
5912
  });
5841
5913
 
5914
+ // src/teamMemberManager.ts
5915
+ function isTeamMemberManager(team, userId, memberManagerRoles, readOnly) {
5916
+ if (!team || !userId || !memberManagerRoles || memberManagerRoles.length === 0 || readOnly) {
5917
+ return false;
5918
+ }
5919
+ return team.members_with_roles?.some((m) => m.user_id === userId && memberManagerRoles.includes(m.role)) ?? false;
5920
+ }
5921
+ var init_teamMemberManager = __esm({
5922
+ "src/teamMemberManager.ts"() {
5923
+ "use strict";
5924
+ }
5925
+ });
5926
+
5842
5927
  // src/unprovisionedCopy.ts
5843
5928
  function getUnprovisionedTitle() {
5844
5929
  return "Your LiteLLM account isn't set up yet";
@@ -5916,6 +6001,7 @@ var init_LiteLLMPage = __esm({
5916
6001
  init_dates();
5917
6002
  init_permissions();
5918
6003
  init_feedback();
6004
+ init_teamMemberManager();
5919
6005
  init_unprovisionedCopy();
5920
6006
  PERIOD_LS_KEY2 = "litellm_usage_period";
5921
6007
  PAGE_TABS = ["overview", "keys", "teams", "models", "audit"];
@@ -6089,6 +6175,7 @@ var init_LiteLLMPage = __esm({
6089
6175
  ]);
6090
6176
  return allModels.filter((m) => allowed.has(m.model_name));
6091
6177
  }, [allModels, userInfo, teams]);
6178
+ const getConfig = useCallback4(() => api.getConfig(), [api]);
6092
6179
  const handleGenerateKey = useCallback4(
6093
6180
  async (request) => {
6094
6181
  try {
@@ -6360,6 +6447,8 @@ var init_LiteLLMPage = __esm({
6360
6447
  for (const t of managedTeams ?? []) teamsById.set(t.team_id, t);
6361
6448
  for (const t of teams ?? []) teamsById.set(t.team_id, t);
6362
6449
  const visibleTeams = Array.from(teamsById.values());
6450
+ const readOnly = liteLlmConfig?.teamManagement?.readOnly ?? false;
6451
+ const memberManagerRoles = liteLlmConfig?.teamManagement?.memberManagerRoles ?? [];
6363
6452
  return /* @__PURE__ */ jsx18(
6364
6453
  TeamUsage,
6365
6454
  {
@@ -6367,10 +6456,13 @@ var init_LiteLLMPage = __esm({
6367
6456
  loading: teamsLoading,
6368
6457
  getTeamUsage: (teamId) => teamUsageCache[teamId] ?? null,
6369
6458
  getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false,
6370
- canManage: teamMgmtEnabled && canManageTeam,
6459
+ canManage: teamMgmtEnabled && canManageTeam && !readOnly,
6371
6460
  onEditTeam: (t) => setManageTeam({ mode: "edit", team: t }),
6372
- canCreate: teamMgmtEnabled && canCreateTeam,
6461
+ canCreate: teamMgmtEnabled && canCreateTeam && !readOnly,
6373
6462
  onCreateTeam: () => setManageTeam({ mode: "create" }),
6463
+ canManageMembers: (t) => teamMgmtEnabled && canManageMembers && isTeamMemberManager(t, userInfo?.user_id, memberManagerRoles, readOnly),
6464
+ onManageMembers: (t) => setManageTeam({ mode: "members", team: t }),
6465
+ readOnly,
6374
6466
  onTeamExpand: (team) => loadTeamUsage(team.team_id)
6375
6467
  }
6376
6468
  );
@@ -6414,7 +6506,7 @@ var init_LiteLLMPage = __esm({
6414
6506
  onCreateKey: handleGenerateKey,
6415
6507
  onUpdateKey: handleUpdateKey,
6416
6508
  onResetKeySpend: handleResetKeySpend,
6417
- onGetConfig: () => api.getConfig()
6509
+ onGetConfig: getConfig
6418
6510
  }
6419
6511
  ),
6420
6512
  /* @__PURE__ */ jsx18(
@@ -6422,10 +6514,13 @@ var init_LiteLLMPage = __esm({
6422
6514
  {
6423
6515
  open: !!manageTeam,
6424
6516
  onClose: () => setManageTeam(null),
6425
- mode: manageTeam?.mode ?? "create",
6517
+ mode: manageTeam?.mode === "members" ? "edit" : manageTeam?.mode ?? "create",
6426
6518
  team: manageTeam?.team,
6427
6519
  allModels: allModels ?? [],
6428
6520
  config: liteLlmConfig,
6521
+ membersOnly: manageTeam?.mode === "members",
6522
+ currentUserId: userInfo?.user_id,
6523
+ memberManagerRoles: liteLlmConfig?.teamManagement?.memberManagerRoles,
6429
6524
  onSubmit: async (payload) => {
6430
6525
  try {
6431
6526
  if (manageTeam?.mode === "edit" && manageTeam.team) {
@@ -6446,7 +6541,7 @@ var init_LiteLLMPage = __esm({
6446
6541
  if (alert) alertApi.post(alert);
6447
6542
  }
6448
6543
  },
6449
- canManageMembers: teamMgmtEnabled && canManageMembers,
6544
+ canManageMembers: teamMgmtEnabled && canManageMembers && !liteLlmConfig?.teamManagement?.readOnly,
6450
6545
  onAddMember: async (userEntityRef, maxBudgetInTeam) => {
6451
6546
  if (!manageTeam?.team) return;
6452
6547
  const updated = await api.addTeamMember(manageTeam.team.team_id, {
@@ -6570,7 +6665,7 @@ import FormControl2 from "@mui/material/FormControl";
6570
6665
  import Select2 from "@mui/material/Select";
6571
6666
  import MenuItem6 from "@mui/material/MenuItem";
6572
6667
  import Skeleton7 from "@mui/material/Skeleton";
6573
- import { AreaChart as AreaChart3, Area as Area3, XAxis as XAxis3, Tooltip as Tooltip7, ResponsiveContainer as ResponsiveContainer3 } from "recharts";
6668
+ import { AreaChart as AreaChart3, Area as Area3, XAxis as XAxis3, Tooltip as Tooltip8, ResponsiveContainer as ResponsiveContainer3 } from "recharts";
6574
6669
  import { useApi as useApi5 } from "@backstage/core-plugin-api";
6575
6670
  import { useRouteRef as useRouteRef2 } from "@backstage/frontend-plugin-api";
6576
6671
  import { Link as Link3 } from "@backstage/core-components";
@@ -6735,7 +6830,7 @@ var LiteLLMHomeWidget = ({
6735
6830
  sx: { mb: 0.5 },
6736
6831
  children: /* @__PURE__ */ jsx20(ResponsiveContainer3, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs18(AreaChart3, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 }, children: [
6737
6832
  /* @__PURE__ */ jsx20(XAxis3, { dataKey: "date", hide: true }),
6738
- /* @__PURE__ */ jsx20(Tooltip7, { content: /* @__PURE__ */ jsx20(ChartTooltip, { valueFormatter: fmtUsd }) }),
6833
+ /* @__PURE__ */ jsx20(Tooltip8, { content: /* @__PURE__ */ jsx20(ChartTooltip, { valueFormatter: fmtUsd }) }),
6739
6834
  /* @__PURE__ */ jsx20(
6740
6835
  Area3,
6741
6836
  {
@@ -6782,7 +6877,7 @@ import Collapse4 from "@mui/material/Collapse";
6782
6877
  import { useTheme as useTheme4 } from "@mui/material/styles";
6783
6878
  import useMediaQuery from "@mui/material/useMediaQuery";
6784
6879
  import { ExpandMore as ExpandMore4 } from "@mui/icons-material";
6785
- import { AreaChart as AreaChart4, Area as Area4, ResponsiveContainer as ResponsiveContainer4, Tooltip as Tooltip8, ReferenceLine as ReferenceLine2, XAxis as XAxis4 } from "recharts";
6880
+ import { AreaChart as AreaChart4, Area as Area4, ResponsiveContainer as ResponsiveContainer4, Tooltip as Tooltip9, ReferenceLine as ReferenceLine2, XAxis as XAxis4 } from "recharts";
6786
6881
  import { useApi as useApi6 } from "@backstage/core-plugin-api";
6787
6882
  import { Link as Link4 } from "@backstage/core-components";
6788
6883
 
@@ -7084,7 +7179,7 @@ var SpendStrip = ({ data, mtdSpend, loading, height, usageUnavailable = false, m
7084
7179
  }
7085
7180
  return /* @__PURE__ */ jsx21(ResponsiveContainer4, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs19(AreaChart4, { data, margin: { top: 4, right: 0, bottom: 0, left: 0 }, children: [
7086
7181
  /* @__PURE__ */ jsx21(XAxis4, { dataKey: "date", hide: true }),
7087
- /* @__PURE__ */ jsx21(Tooltip8, { content: /* @__PURE__ */ jsx21(ChartTooltip, { valueFormatter: fmtUsd }) }),
7182
+ /* @__PURE__ */ jsx21(Tooltip9, { content: /* @__PURE__ */ jsx21(ChartTooltip, { valueFormatter: fmtUsd }) }),
7088
7183
  maxBudget !== void 0 && /* @__PURE__ */ jsx21(
7089
7184
  ReferenceLine2,
7090
7185
  {