From 7145768d4e965112897b70153d37bdb3727f891d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 09:08:39 +0000 Subject: [PATCH] fix(ui): only offer team admins the teams they administer in the Team ID selector Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/components/model_info_view.test.tsx | 26 +++++++++++++++++++ .../src/components/model_info_view.tsx | 11 +++----- ui/litellm-dashboard/src/utils/roles.ts | 11 ++++++++ 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/ui/litellm-dashboard/src/components/model_info_view.test.tsx b/ui/litellm-dashboard/src/components/model_info_view.test.tsx index 9c7896b51ee..ae66ff2f429 100644 --- a/ui/litellm-dashboard/src/components/model_info_view.test.tsx +++ b/ui/litellm-dashboard/src/components/model_info_view.test.tsx @@ -1596,6 +1596,32 @@ describe("ModelInfoView", () => { expect(payload.model_info.team_id).toBe("team-2"); }); + it("only offers a team admin the teams they administer", async () => { + mockUseTeams.mockReturnValue({ + data: [ + { team_id: "team-1", team_alias: "alpha", members_with_roles: [{ user_id: "123", role: "admin" }] }, + { team_id: "team-2", team_alias: "beta", members_with_roles: [{ user_id: "123", role: "user" }] }, + { team_id: "team-3", team_alias: "gamma", members_with_roles: [{ user_id: "123", role: "admin" }] }, + ], + isLoading: false, + error: null, + }); + const teamModel = { + ...defaultModelData, + model_info: { ...defaultModelData.model_info, team_id: "team-1" }, + }; + mockUseModelsInfo.mockReturnValue({ data: { data: [teamModel] }, isLoading: false, error: null }); + mockModelInfoV1Call.mockResolvedValue({ data: [teamModel] }); + const user = userEvent.setup(); + render(, { wrapper }); + await user.click(await screen.findByRole("button", { name: /edit settings/i })); + + await user.click(await screen.findByText("alpha (team-1)")); + + expect(await screen.findByRole("option", { name: "gamma (team-3)" })).toBeInTheDocument(); + expect(screen.queryByRole("option", { name: "beta (team-2)" })).not.toBeInTheDocument(); + }); + it("sends the edited LiteLLM extra params", async () => { const user = userEvent.setup(); await enterEditMode(user); diff --git a/ui/litellm-dashboard/src/components/model_info_view.tsx b/ui/litellm-dashboard/src/components/model_info_view.tsx index 2a86d068328..bf7ab669ec0 100644 --- a/ui/litellm-dashboard/src/components/model_info_view.tsx +++ b/ui/litellm-dashboard/src/components/model_info_view.tsx @@ -23,6 +23,7 @@ import { isComplexityRouter as isComplexityRouterParams, } from "./add_model/auto_router_strategies"; import { canModifyModel } from "@/utils/modelPermissions"; +import { teamsUserCanAssign } from "@/utils/roles"; import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams"; import DeleteResourceModal from "./common_components/DeleteResourceModal"; import EditAutoRouterModal from "./edit_auto_router/edit_auto_router_modal"; @@ -174,6 +175,7 @@ export default function ModelInfoView({ isDbModel: modelData?.model_info?.db_model === true, }); const isAdmin = userRole === "Admin"; + const assignableTeams = useMemo(() => teamsUserCanAssign(teams ?? null, userRole, userID), [teams, userRole, userID]); // Editor-aware on purpose: an adaptive or quality router must not offer Edit Auto Router. const isAutoRouterModel = hasAutoRouterEditor(modelData?.litellm_params); // Broader than the editor check: adaptive and quality routers equally have no upstream @@ -420,12 +422,7 @@ export default function ModelInfoView({ health_check_model: values.health_check_model, }; } - if (values.team_id) { - updatedModelInfo = { - ...updatedModelInfo, - team_id: values.team_id, - }; - } + if (values.team_id) updatedModelInfo = { ...updatedModelInfo, team_id: values.team_id }; updatedModelInfo = applyPtuModelInfo(updatedModelInfo, values, ptuCostAttributionEnabled); } catch (e) { toast.fromError("Invalid JSON in Model Info"); @@ -785,7 +782,7 @@ export default function ModelInfoView({ tagsList={tagsList} credentialsList={credentialsList} healthCheckModelOptions={healthCheckModelOptions} - teams={teams ?? null} + teams={assignableTeams} /> ) : (

Loading...

diff --git a/ui/litellm-dashboard/src/utils/roles.ts b/ui/litellm-dashboard/src/utils/roles.ts index 62a5f02cc39..30b53e9b089 100644 --- a/ui/litellm-dashboard/src/utils/roles.ts +++ b/ui/litellm-dashboard/src/utils/roles.ts @@ -46,6 +46,17 @@ export const isUserTeamAdminForSingleTeam = (teamMemberWithRoles: Member[] | nul return teamMemberWithRoles.some((member) => member.user_id === userID && member.role === "admin"); }; +export const teamsUserCanAssign = ( + teams: Team[] | null, + userRole: string | null, + userID: string | null, +): Team[] | null => { + if (teams == null || all_admin_roles.includes(userRole ?? "")) { + return teams; + } + return teams.filter((team) => isUserTeamAdminForSingleTeam(team.members_with_roles, userID ?? "")); +}; + export const isOrgAdminForAnyOrg = ( organizations: Organization[] | null | undefined, userID: string | null | undefined,