@acarmisc/backstage-plugin-litellm 0.31.1 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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([]);
@@ -3191,7 +3195,7 @@ var init_ManageTeamDialog = __esm({
3191
3195
  const hideBudgetForManagers = config?.display?.hideTeamBudgetForManagers ?? false;
3192
3196
  const budgetWriteOnly = hideBudgetForManagers && mode === "edit";
3193
3197
  const members = useMemo4(() => team?.members_with_roles ?? [], [team]);
3194
- const showMembers = mode === "edit" && !!canManageMembers;
3198
+ const showMembers = mode === "edit" && !!canManageMembers || membersOnly;
3195
3199
  const catalogApi = useApi3(catalogApiRef);
3196
3200
  const [searchUsers, setSearchUsers] = useState6([]);
3197
3201
  const [usersLoading, setUsersLoading] = useState6(false);
@@ -3340,7 +3344,7 @@ var init_ManageTeamDialog = __esm({
3340
3344
  const handleAddMember = async () => {
3341
3345
  if (!onAddMember || !memberRef.trim()) return;
3342
3346
  setMemberError(null);
3343
- const budget = memberBudget ? parseFloat(memberBudget) : void 0;
3347
+ const budget = !membersOnly && memberBudget ? parseFloat(memberBudget) : void 0;
3344
3348
  if (budget !== void 0 && (isNaN(budget) || budget <= 0)) {
3345
3349
  setMemberError("Max budget in team must be a positive number");
3346
3350
  return;
@@ -3401,72 +3405,95 @@ var init_ManageTeamDialog = __esm({
3401
3405
  const showMcpServers = mode === "edit" && !!canManageMcpServers;
3402
3406
  const mcpOptions = (mcpServers ?? []).map((s) => s.id);
3403
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
+ };
3404
3424
  return /* @__PURE__ */ jsxs10(Dialog3, { open, onClose, maxWidth: "sm", fullWidth: true, children: [
3405
- /* @__PURE__ */ jsx11(DialogTitle3, { children: mode === "create" ? "Create Team" : "Edit Team" }),
3425
+ /* @__PURE__ */ jsx11(DialogTitle3, { children: getDialogTitle() }),
3406
3426
  /* @__PURE__ */ jsxs10(DialogContent3, { sx: { pt: 2, display: "flex", flexDirection: "column", gap: 2 }, children: [
3407
3427
  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,
3459
- {
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
- ),
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
+ ] }),
3470
3497
  showMembers && /* @__PURE__ */ jsxs10(Box10, { children: [
3471
3498
  /* @__PURE__ */ jsx11(Divider2, { sx: { my: 1 } }),
3472
3499
  /* @__PURE__ */ jsx11(Typography8, { variant: "subtitle2", sx: { mb: 1 }, children: "Members" }),
@@ -3498,17 +3525,20 @@ var init_ManageTeamDialog = __esm({
3498
3525
  }
3499
3526
  ) }),
3500
3527
  /* @__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
- ) })
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
+ })() })
3512
3542
  ] }, m.user_id)) })
3513
3543
  ] }) }),
3514
3544
  /* @__PURE__ */ jsxs10(Box10, { sx: { display: "flex", gap: 1, alignItems: "flex-start", flexWrap: "wrap" }, children: [
@@ -3556,7 +3586,7 @@ var init_ManageTeamDialog = __esm({
3556
3586
  )
3557
3587
  }
3558
3588
  ),
3559
- /* @__PURE__ */ jsx11(
3589
+ !membersOnly && /* @__PURE__ */ jsx11(
3560
3590
  TextField3,
3561
3591
  {
3562
3592
  label: "Max budget in team ($)",
@@ -3580,7 +3610,7 @@ var init_ManageTeamDialog = __esm({
3580
3610
  )
3581
3611
  ] })
3582
3612
  ] }),
3583
- showKnowledgeBases && /* @__PURE__ */ jsxs10(Box10, { children: [
3613
+ !membersOnly && showKnowledgeBases && /* @__PURE__ */ jsxs10(Box10, { children: [
3584
3614
  /* @__PURE__ */ jsx11(Divider2, { sx: { my: 1 } }),
3585
3615
  /* @__PURE__ */ jsx11(Typography8, { variant: "subtitle2", sx: { mb: 1 }, children: "Knowledge Bases" }),
3586
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." }),
@@ -3607,7 +3637,7 @@ var init_ManageTeamDialog = __esm({
3607
3637
  }
3608
3638
  ) })
3609
3639
  ] }),
3610
- showMcpServers && /* @__PURE__ */ jsxs10(Box10, { children: [
3640
+ !membersOnly && showMcpServers && /* @__PURE__ */ jsxs10(Box10, { children: [
3611
3641
  /* @__PURE__ */ jsx11(Divider2, { sx: { my: 1 } }),
3612
3642
  /* @__PURE__ */ jsx11(Typography8, { variant: "subtitle2", sx: { mb: 1 }, children: "MCP Servers" }),
3613
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." }),
@@ -3636,8 +3666,8 @@ var init_ManageTeamDialog = __esm({
3636
3666
  ] })
3637
3667
  ] }),
3638
3668
  /* @__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" })
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" })
3641
3671
  ] })
3642
3672
  ] });
3643
3673
  };
@@ -3848,7 +3878,7 @@ import {
3848
3878
  XAxis,
3849
3879
  YAxis,
3850
3880
  CartesianGrid,
3851
- Tooltip as Tooltip4,
3881
+ Tooltip as Tooltip5,
3852
3882
  ResponsiveContainer,
3853
3883
  Legend,
3854
3884
  ReferenceLine,
@@ -4185,7 +4215,7 @@ var init_UsageStats = __esm({
4185
4215
  /* @__PURE__ */ jsx12(XAxis, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }),
4186
4216
  /* @__PURE__ */ jsx12(YAxis, { tickFormatter: fmtUsdCompact, width: 56, ...chart.axis }),
4187
4217
  /* @__PURE__ */ jsx12(
4188
- Tooltip4,
4218
+ Tooltip5,
4189
4219
  {
4190
4220
  cursor: chart.cursor,
4191
4221
  content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: fmtUsd, hideZero: true })
@@ -4211,7 +4241,7 @@ var init_UsageStats = __esm({
4211
4241
  /* @__PURE__ */ jsx12(CartesianGrid, { ...chart.grid }),
4212
4242
  /* @__PURE__ */ jsx12(XAxis, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }),
4213
4243
  /* @__PURE__ */ jsx12(YAxis, { tickFormatter: fmtCompact, width: 48, ...chart.axis }),
4214
- /* @__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 }) }),
4215
4245
  /* @__PURE__ */ jsx12(Legend, { ...chart.legend }),
4216
4246
  /* @__PURE__ */ jsx12(Area, { type: "monotone", dataKey: "promptTokens", name: "Input", stackId: "tok", stroke: SERIES.input, strokeWidth: 1.5, fill: SERIES.input, fillOpacity: 0.25 }),
4217
4247
  /* @__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 +4250,7 @@ var init_UsageStats = __esm({
4220
4250
  /* @__PURE__ */ jsx12(CartesianGrid, { ...chart.grid }),
4221
4251
  /* @__PURE__ */ jsx12(XAxis, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }),
4222
4252
  /* @__PURE__ */ jsx12(YAxis, { tickFormatter: fmtCompact, width: 48, ...chart.axis }),
4223
- /* @__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 }) }),
4224
4254
  /* @__PURE__ */ jsx12(Legend, { ...chart.legend }),
4225
4255
  /* @__PURE__ */ jsx12(Bar, { dataKey: "successfulRequests", name: "Successful", fill: SERIES.success, stackId: "r" }),
4226
4256
  /* @__PURE__ */ jsx12(Bar, { dataKey: "failedRequests", name: "Failed", fill: SERIES.failure, stackId: "r", radius: [3, 3, 0, 0] })
@@ -4230,7 +4260,7 @@ var init_UsageStats = __esm({
4230
4260
  /* @__PURE__ */ jsx12(XAxis, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }),
4231
4261
  /* @__PURE__ */ jsx12(YAxis, { domain: [0, 100], tickFormatter: (v) => `${v}%`, width: 44, ...chart.axis }),
4232
4262
  /* @__PURE__ */ jsx12(
4233
- Tooltip4,
4263
+ Tooltip5,
4234
4264
  {
4235
4265
  cursor: chart.cursor,
4236
4266
  content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: (v) => `${v}%` })
@@ -4267,7 +4297,7 @@ var init_UsageStats = __esm({
4267
4297
  }
4268
4298
  ),
4269
4299
  /* @__PURE__ */ jsx12(
4270
- Tooltip4,
4300
+ Tooltip5,
4271
4301
  {
4272
4302
  cursor: chart.cursor,
4273
4303
  content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: fmtUsd })
@@ -4309,7 +4339,7 @@ var init_UsageStats = __esm({
4309
4339
  /* @__PURE__ */ jsx12(CartesianGrid, { ...chart.grid, vertical: true, horizontal: false }),
4310
4340
  /* @__PURE__ */ jsx12(XAxis, { type: "number", tickFormatter: fmtUsdCompact, ...chart.axis }),
4311
4341
  /* @__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 }) }),
4342
+ /* @__PURE__ */ jsx12(Tooltip5, { cursor: chart.cursor, content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: fmtUsd, labelFormatter: (l) => l }) }),
4313
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)) })
4314
4344
  ] }) })
4315
4345
  }
@@ -4324,7 +4354,7 @@ var init_UsageStats = __esm({
4324
4354
  /* @__PURE__ */ jsx12(CartesianGrid, { ...chart.grid, vertical: true, horizontal: false }),
4325
4355
  /* @__PURE__ */ jsx12(XAxis, { type: "number", tickFormatter: fmtCompact, ...chart.axis }),
4326
4356
  /* @__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 }) }),
4357
+ /* @__PURE__ */ jsx12(Tooltip5, { cursor: chart.cursor, content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: fmtInt, labelFormatter: (l) => l }) }),
4328
4358
  /* @__PURE__ */ jsx12(Legend, { ...chart.legend }),
4329
4359
  /* @__PURE__ */ jsx12(Bar, { dataKey: "promptTokens", name: "Input", fill: SERIES.input, stackId: "t", barSize: 14 }),
4330
4360
  /* @__PURE__ */ jsx12(Bar, { dataKey: "completionTokens", name: "Output", fill: SERIES.output, stackId: "t", barSize: 14, radius: [0, 3, 3, 0] })
@@ -4356,7 +4386,7 @@ var init_UsageStats = __esm({
4356
4386
  /* @__PURE__ */ jsx12(CartesianGrid, { ...chart.grid, vertical: true, horizontal: false }),
4357
4387
  /* @__PURE__ */ jsx12(XAxis, { type: "number", tickFormatter: fmtUsdCompact, ...chart.axis }),
4358
4388
  /* @__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 }) }),
4389
+ /* @__PURE__ */ jsx12(Tooltip5, { cursor: chart.cursor, content: /* @__PURE__ */ jsx12(ChartTooltip, { valueFormatter: fmtUsd, labelFormatter: (l) => l }) }),
4360
4390
  /* @__PURE__ */ jsx12(Bar, { dataKey: "spend", name: "Spend", fill: SERIES.input, radius: [0, 3, 3, 0], barSize: 14 })
4361
4391
  ] }) })
4362
4392
  }
@@ -4891,14 +4921,15 @@ import IconButton6 from "@mui/material/IconButton";
4891
4921
  import Button6 from "@mui/material/Button";
4892
4922
  import CircularProgress4 from "@mui/material/CircularProgress";
4893
4923
  import { alpha as alpha10 } from "@mui/material/styles";
4894
- 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";
4895
4926
  import {
4896
4927
  AreaChart as AreaChart2,
4897
4928
  Area as Area2,
4898
4929
  XAxis as XAxis2,
4899
4930
  YAxis as YAxis2,
4900
4931
  CartesianGrid as CartesianGrid2,
4901
- Tooltip as Tooltip5,
4932
+ Tooltip as Tooltip6,
4902
4933
  ResponsiveContainer as ResponsiveContainer2
4903
4934
  } from "recharts";
4904
4935
  import { Fragment as Fragment8, jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
@@ -4922,7 +4953,7 @@ var init_TeamUsage = __esm({
4922
4953
  "use strict";
4923
4954
  init_ui();
4924
4955
  fmtUsd2 = (n) => `$${(n ?? 0).toFixed(2)}`;
4925
- TeamCard = ({ team, usage, usageLoading, canManage, onEditTeam, onExpand }) => {
4956
+ TeamCard = ({ team, usage, usageLoading, canManage, onEditTeam, canManageMembers, onManageMembers, onExpand }) => {
4926
4957
  const [expanded, setExpanded] = useState9(false);
4927
4958
  const chart = useChartTheme();
4928
4959
  const handleExpandChange = (newExpanded) => {
@@ -4959,7 +4990,7 @@ var init_TeamUsage = __esm({
4959
4990
  /* @__PURE__ */ jsx15(XAxis2, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }),
4960
4991
  /* @__PURE__ */ jsx15(YAxis2, { tickFormatter: fmtUsdCompact, width: 52, ...chart.axis }),
4961
4992
  /* @__PURE__ */ jsx15(
4962
- Tooltip5,
4993
+ Tooltip6,
4963
4994
  {
4964
4995
  cursor: chart.cursor,
4965
4996
  content: /* @__PURE__ */ jsx15(ChartTooltip, { valueFormatter: (v) => `$${v.toFixed(4)}` })
@@ -5032,6 +5063,17 @@ var init_TeamUsage = __esm({
5032
5063
  children: "Edit"
5033
5064
  }
5034
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
+ ) }),
5035
5077
  /* @__PURE__ */ jsx15(
5036
5078
  IconButton6,
5037
5079
  {
@@ -5162,9 +5204,23 @@ var init_TeamUsage = __esm({
5162
5204
  onEditTeam,
5163
5205
  canCreate,
5164
5206
  onCreateTeam,
5165
- onTeamExpand
5207
+ onTeamExpand,
5208
+ canManageMembers,
5209
+ onManageMembers,
5210
+ readOnly
5166
5211
  }) => {
5167
- 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(
5168
5224
  Button6,
5169
5225
  {
5170
5226
  variant: "contained",
@@ -5175,6 +5231,7 @@ var init_TeamUsage = __esm({
5175
5231
  children: "Create Team"
5176
5232
  }
5177
5233
  ) : void 0;
5234
+ const createAction = readOnly ? readOnlyBadge : createButton;
5178
5235
  if (loading) {
5179
5236
  return /* @__PURE__ */ jsxs14(SectionCard, { title: "Teams", actions: createAction, children: [
5180
5237
  /* @__PURE__ */ jsx15(Skeleton5, { variant: "rounded", height: 120, sx: { mb: 2 } }),
@@ -5204,6 +5261,8 @@ var init_TeamUsage = __esm({
5204
5261
  usageLoading: getTeamUsageLoading(team.team_id),
5205
5262
  canManage,
5206
5263
  onEditTeam,
5264
+ canManageMembers: canManageMembers?.(team),
5265
+ onManageMembers,
5207
5266
  onExpand: onTeamExpand
5208
5267
  },
5209
5268
  team.team_id
@@ -5233,7 +5292,7 @@ import TextField5 from "@mui/material/TextField";
5233
5292
  import InputAdornment3 from "@mui/material/InputAdornment";
5234
5293
  import { Search as Search2 } from "@mui/icons-material";
5235
5294
  import ContentCopyIcon from "@mui/icons-material/ContentCopy";
5236
- import Tooltip6 from "@mui/material/Tooltip";
5295
+ import Tooltip7 from "@mui/material/Tooltip";
5237
5296
  import Snackbar from "@mui/material/Snackbar";
5238
5297
  import Alert4 from "@mui/material/Alert";
5239
5298
  import { alpha as alpha11, useTheme as useTheme3 } from "@mui/material/styles";
@@ -5417,7 +5476,7 @@ var init_ModelsTable = __esm({
5417
5476
  ] }) }),
5418
5477
  /* @__PURE__ */ jsx16(TableBody5, { children: filteredModels.map((m) => /* @__PURE__ */ jsxs15(TableRow5, { hover: true, children: [
5419
5478
  /* @__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(
5479
+ /* @__PURE__ */ jsx16(Tooltip7, { title: m.model_name, placement: "top", children: /* @__PURE__ */ jsx16(
5421
5480
  "span",
5422
5481
  {
5423
5482
  role: "button",
@@ -5429,7 +5488,7 @@ var init_ModelsTable = __esm({
5429
5488
  children: /* @__PURE__ */ jsx16(TagChip, { label: m.model_name, title: "Copy model ID" })
5430
5489
  }
5431
5490
  ) }),
5432
- /* @__PURE__ */ jsx16(Tooltip6, { title: "Copy model ID", children: /* @__PURE__ */ jsx16(
5491
+ /* @__PURE__ */ jsx16(Tooltip7, { title: "Copy model ID", children: /* @__PURE__ */ jsx16(
5433
5492
  Box15,
5434
5493
  {
5435
5494
  component: "span",
@@ -5852,6 +5911,19 @@ var init_feedback = __esm({
5852
5911
  }
5853
5912
  });
5854
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
+
5855
5927
  // src/unprovisionedCopy.ts
5856
5928
  function getUnprovisionedTitle() {
5857
5929
  return "Your LiteLLM account isn't set up yet";
@@ -5929,6 +6001,7 @@ var init_LiteLLMPage = __esm({
5929
6001
  init_dates();
5930
6002
  init_permissions();
5931
6003
  init_feedback();
6004
+ init_teamMemberManager();
5932
6005
  init_unprovisionedCopy();
5933
6006
  PERIOD_LS_KEY2 = "litellm_usage_period";
5934
6007
  PAGE_TABS = ["overview", "keys", "teams", "models", "audit"];
@@ -6374,6 +6447,8 @@ var init_LiteLLMPage = __esm({
6374
6447
  for (const t of managedTeams ?? []) teamsById.set(t.team_id, t);
6375
6448
  for (const t of teams ?? []) teamsById.set(t.team_id, t);
6376
6449
  const visibleTeams = Array.from(teamsById.values());
6450
+ const readOnly = liteLlmConfig?.teamManagement?.readOnly ?? false;
6451
+ const memberManagerRoles = liteLlmConfig?.teamManagement?.memberManagerRoles ?? [];
6377
6452
  return /* @__PURE__ */ jsx18(
6378
6453
  TeamUsage,
6379
6454
  {
@@ -6381,10 +6456,13 @@ var init_LiteLLMPage = __esm({
6381
6456
  loading: teamsLoading,
6382
6457
  getTeamUsage: (teamId) => teamUsageCache[teamId] ?? null,
6383
6458
  getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false,
6384
- canManage: teamMgmtEnabled && canManageTeam,
6459
+ canManage: teamMgmtEnabled && canManageTeam && !readOnly,
6385
6460
  onEditTeam: (t) => setManageTeam({ mode: "edit", team: t }),
6386
- canCreate: teamMgmtEnabled && canCreateTeam,
6461
+ canCreate: teamMgmtEnabled && canCreateTeam && !readOnly,
6387
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,
6388
6466
  onTeamExpand: (team) => loadTeamUsage(team.team_id)
6389
6467
  }
6390
6468
  );
@@ -6436,10 +6514,13 @@ var init_LiteLLMPage = __esm({
6436
6514
  {
6437
6515
  open: !!manageTeam,
6438
6516
  onClose: () => setManageTeam(null),
6439
- mode: manageTeam?.mode ?? "create",
6517
+ mode: manageTeam?.mode === "members" ? "edit" : manageTeam?.mode ?? "create",
6440
6518
  team: manageTeam?.team,
6441
6519
  allModels: allModels ?? [],
6442
6520
  config: liteLlmConfig,
6521
+ membersOnly: manageTeam?.mode === "members",
6522
+ currentUserId: userInfo?.user_id,
6523
+ memberManagerRoles: liteLlmConfig?.teamManagement?.memberManagerRoles,
6443
6524
  onSubmit: async (payload) => {
6444
6525
  try {
6445
6526
  if (manageTeam?.mode === "edit" && manageTeam.team) {
@@ -6460,7 +6541,7 @@ var init_LiteLLMPage = __esm({
6460
6541
  if (alert) alertApi.post(alert);
6461
6542
  }
6462
6543
  },
6463
- canManageMembers: teamMgmtEnabled && canManageMembers,
6544
+ canManageMembers: teamMgmtEnabled && canManageMembers && !liteLlmConfig?.teamManagement?.readOnly,
6464
6545
  onAddMember: async (userEntityRef, maxBudgetInTeam) => {
6465
6546
  if (!manageTeam?.team) return;
6466
6547
  const updated = await api.addTeamMember(manageTeam.team.team_id, {
@@ -6584,7 +6665,7 @@ import FormControl2 from "@mui/material/FormControl";
6584
6665
  import Select2 from "@mui/material/Select";
6585
6666
  import MenuItem6 from "@mui/material/MenuItem";
6586
6667
  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";
6668
+ import { AreaChart as AreaChart3, Area as Area3, XAxis as XAxis3, Tooltip as Tooltip8, ResponsiveContainer as ResponsiveContainer3 } from "recharts";
6588
6669
  import { useApi as useApi5 } from "@backstage/core-plugin-api";
6589
6670
  import { useRouteRef as useRouteRef2 } from "@backstage/frontend-plugin-api";
6590
6671
  import { Link as Link3 } from "@backstage/core-components";
@@ -6749,7 +6830,7 @@ var LiteLLMHomeWidget = ({
6749
6830
  sx: { mb: 0.5 },
6750
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: [
6751
6832
  /* @__PURE__ */ jsx20(XAxis3, { dataKey: "date", hide: true }),
6752
- /* @__PURE__ */ jsx20(Tooltip7, { content: /* @__PURE__ */ jsx20(ChartTooltip, { valueFormatter: fmtUsd }) }),
6833
+ /* @__PURE__ */ jsx20(Tooltip8, { content: /* @__PURE__ */ jsx20(ChartTooltip, { valueFormatter: fmtUsd }) }),
6753
6834
  /* @__PURE__ */ jsx20(
6754
6835
  Area3,
6755
6836
  {
@@ -6796,7 +6877,7 @@ import Collapse4 from "@mui/material/Collapse";
6796
6877
  import { useTheme as useTheme4 } from "@mui/material/styles";
6797
6878
  import useMediaQuery from "@mui/material/useMediaQuery";
6798
6879
  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";
6880
+ import { AreaChart as AreaChart4, Area as Area4, ResponsiveContainer as ResponsiveContainer4, Tooltip as Tooltip9, ReferenceLine as ReferenceLine2, XAxis as XAxis4 } from "recharts";
6800
6881
  import { useApi as useApi6 } from "@backstage/core-plugin-api";
6801
6882
  import { Link as Link4 } from "@backstage/core-components";
6802
6883
 
@@ -7098,7 +7179,7 @@ var SpendStrip = ({ data, mtdSpend, loading, height, usageUnavailable = false, m
7098
7179
  }
7099
7180
  return /* @__PURE__ */ jsx21(ResponsiveContainer4, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs19(AreaChart4, { data, margin: { top: 4, right: 0, bottom: 0, left: 0 }, children: [
7100
7181
  /* @__PURE__ */ jsx21(XAxis4, { dataKey: "date", hide: true }),
7101
- /* @__PURE__ */ jsx21(Tooltip8, { content: /* @__PURE__ */ jsx21(ChartTooltip, { valueFormatter: fmtUsd }) }),
7182
+ /* @__PURE__ */ jsx21(Tooltip9, { content: /* @__PURE__ */ jsx21(ChartTooltip, { valueFormatter: fmtUsd }) }),
7102
7183
  maxBudget !== void 0 && /* @__PURE__ */ jsx21(
7103
7184
  ReferenceLine2,
7104
7185
  {