From 918fb506a619e88f07013c8253e24c7737d5e007 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 08:54:05 +0000 Subject: [PATCH] fix(ui): let admins change a model's team from the model edit page Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/components/ModelInfoEditForm.tsx | 37 ++++++++++++++++++- .../src/components/model_info_view.test.tsx | 32 ++++++++++++++++ .../src/components/model_info_view.tsx | 7 ++++ 3 files changed, 75 insertions(+), 1 deletion(-) diff --git a/ui/litellm-dashboard/src/components/ModelInfoEditForm.tsx b/ui/litellm-dashboard/src/components/ModelInfoEditForm.tsx index d56e65237eb..9889741afa1 100644 --- a/ui/litellm-dashboard/src/components/ModelInfoEditForm.tsx +++ b/ui/litellm-dashboard/src/components/ModelInfoEditForm.tsx @@ -24,6 +24,7 @@ import CacheControlInjectionPoints, { CACHE_CONTROL_TOOLTIP, type CacheControlInjectionPoint, } from "./add_model/cache_control_settings"; +import type { Team } from "./key_team_helpers/key_list"; import type { CredentialItem } from "./networking"; import NumericalInput from "./shared/numerical_input"; import type { Tag } from "./tag_management/types"; @@ -103,6 +104,7 @@ export interface ModelEditFormValues { litellm_credential_name?: string; litellm_extra_params?: string; model_info?: string; + team_id?: string; } type ModelEditFieldName = keyof ModelEditFormValues; @@ -139,6 +141,7 @@ const modelEditShape = { litellm_credential_name: textish, litellm_extra_params: textish, model_info: textish, + team_id: textish, }; const isJson = (value: string): boolean => { @@ -260,6 +263,7 @@ export const toModelEditFormValues = (localModelData: any, isWildcardModel: bool null, 2, ), + team_id: localModelData.model_info?.team_id ?? undefined, }); const displayCost = (localModelData: any, field: TouchedPricingField): string => { @@ -285,6 +289,7 @@ interface ModelInfoEditFormProps { tagsList: Record; credentialsList: CredentialItem[]; healthCheckModelOptions: { value: string; label: string }[]; + teams: Team[] | null; } const Display: React.FC<{ children: React.ReactNode }> = ({ children }) => ( @@ -355,6 +360,7 @@ const ModelInfoEditForm: React.FC = ({ tagsList, credentialsList, healthCheckModelOptions, + teams, }) => { // Neither RHF's blur-based touchedFields nor its resettable dirtyFields matches antd's touched-on-change. const touchedRef = React.useRef>(new Set()); @@ -800,7 +806,36 @@ const ModelInfoEditForm: React.FC = ({
Team ID - {modelData.model_info.team_id || "Not Set"} + {isEditing ? ( + + {({ id, value, onChange, onBlur }) => { + const items = (teams ?? []).map((team) => ({ + value: team.team_id, + label: team.team_alias ? `${team.team_alias} (${team.team_id})` : team.team_id, + })); + return ( + + ); + }} + + ) : ( + {modelData.model_info.team_id || "Not Set"} + )}
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 3db9418dfb9..9c7896b51ee 100644 --- a/ui/litellm-dashboard/src/components/model_info_view.test.tsx +++ b/ui/litellm-dashboard/src/components/model_info_view.test.tsx @@ -42,6 +42,11 @@ vi.mock("@/app/(dashboard)/hooks/models/useModelCostMap", () => ({ useModelCostMap: (...args: any[]) => mockUseModelCostMap(...args), })); +const mockUseTeams = vi.fn(); +vi.mock("@/app/(dashboard)/hooks/teams/useTeams", () => ({ + useTeams: () => mockUseTeams(), +})); + const mockUsePtuCostAttributionEnabled = vi.fn(); vi.mock("@/app/(dashboard)/hooks/uiSettings/usePtuCostAttributionEnabled", () => ({ usePtuCostAttributionEnabled: () => mockUsePtuCostAttributionEnabled(), @@ -102,6 +107,7 @@ describe("ModelInfoView", () => { }); vi.clearAllMocks(); mockUsePtuCostAttributionEnabled.mockReturnValue(false); + mockUseTeams.mockReturnValue({ data: undefined, isLoading: false, error: null }); mockUseModelsInfo.mockReturnValue({ data: { @@ -1564,6 +1570,32 @@ describe("ModelInfoView", () => { expect(payload.model_info).toMatchObject({ team_id: "team-7" }); }); + it("sends the team picked in the Team ID selector", async () => { + mockUseTeams.mockReturnValue({ + data: [ + { team_id: "team-1", team_alias: "alpha" }, + { team_id: "team-2", team_alias: "beta" }, + ], + 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(); + await enterEditMode(user); + + await user.click(screen.getByText("alpha (team-1)")); + await user.click(await screen.findByText("beta (team-2)")); + + const payload = await save(user); + + expect(payload.model_info.team_id).toBe("team-2"); + }); + 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 35afcdb2985..2a86d068328 100644 --- a/ui/litellm-dashboard/src/components/model_info_view.tsx +++ b/ui/litellm-dashboard/src/components/model_info_view.tsx @@ -420,6 +420,12 @@ export default function ModelInfoView({ health_check_model: values.health_check_model, }; } + 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"); @@ -779,6 +785,7 @@ export default function ModelInfoView({ tagsList={tagsList} credentialsList={credentialsList} healthCheckModelOptions={healthCheckModelOptions} + teams={teams ?? null} /> ) : (

Loading...