From 42086bdbd45e65d06a9a368251ea254ec19663c7 Mon Sep 17 00:00:00 2001 From: Jonathan Wrede Date: Sun, 10 May 2026 18:13:21 +0000 Subject: [PATCH] fix(proxy): remove deleted model from all teams' model lists When a model was deleted, it was only removed from a team's models array if the model itself was a team-specific model (team_id set in model_info). Regular proxy models added to teams were never cleaned up on deletion, leaving stale references in the team's models list. After the existing team-model alias cleanup, scan all teams that reference the deleted model's name and remove it from their models arrays. Fixes #26785 --- .../model_management_endpoints.py | 20 +++++ .../test_model_management_endpoints.py | 77 +++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/litellm/proxy/management_endpoints/model_management_endpoints.py b/litellm/proxy/management_endpoints/model_management_endpoints.py index af84bc123ff..f86a3ebc423 100644 --- a/litellm/proxy/management_endpoints/model_management_endpoints.py +++ b/litellm/proxy/management_endpoints/model_management_endpoints.py @@ -808,6 +808,26 @@ async def delete_model( data={"models": existing_team_row.models}, ) + # Remove deleted model from all teams that reference it by name + try: + affected_teams = await prisma_client.db.litellm_teamtable.find_many( + where={"models": {"has": model_params.model_name}} + ) + for team in affected_teams: + updated_models = [ + m for m in team.models if m != model_params.model_name + ] + await prisma_client.db.litellm_teamtable.update( + where={"team_id": team.team_id}, + data={"models": updated_models}, + ) + except Exception as e: + verbose_proxy_logger.warning( + "Failed to remove model %s from teams: %s", + model_params.model_name, + e, + ) + # update DB if store_model_in_db is True: """ diff --git a/tests/test_litellm/proxy/management_endpoints/test_model_management_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_model_management_endpoints.py index f0bf4578636..d89b1578c2f 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_model_management_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_model_management_endpoints.py @@ -1363,6 +1363,83 @@ class TestAddAndDeleteModelLifecycle: assert str(exc_info.value.code) == "400" +class TestDeleteModelCascadeTeams: + """Deleting a model should remove it from all teams' models arrays.""" + + @pytest.mark.asyncio + async def test_delete_model_removes_from_teams(self): + from litellm.proxy.management_endpoints.model_management_endpoints import ( + delete_model as delete_model_endpoint, + ModelInfoDelete, + ) + + model_id = "cascade-test-model" + admin_user = UserAPIKeyAuth( + user_id="test-admin", user_role=LitellmUserRoles.PROXY_ADMIN + ) + + db_row = LiteLLM_ProxyModelTable( + model_id=model_id, + model_name="gpt-4", + litellm_params={"model": "openai/gpt-4"}, + model_info={"id": model_id}, + created_by="test-admin", + updated_by="test-admin", + ) + + team_a = MagicMock() + team_a.team_id = "team-a" + team_a.models = ["gpt-4", "claude-sonnet"] + + team_b = MagicMock() + team_b.team_id = "team-b" + team_b.models = ["gpt-4"] + + mock_prisma = MagicMock() + mock_prisma.db = MagicMock() + mock_prisma.db.litellm_proxymodeltable = AsyncMock() + mock_prisma.db.litellm_proxymodeltable.find_unique = AsyncMock( + return_value=db_row + ) + mock_prisma.db.litellm_proxymodeltable.delete = AsyncMock(return_value=db_row) + mock_prisma.db.litellm_teamtable = AsyncMock() + mock_prisma.db.litellm_teamtable.find_many = AsyncMock( + return_value=[team_a, team_b] + ) + mock_prisma.db.litellm_teamtable.update = AsyncMock() + + _PS = "litellm.proxy.proxy_server" + with ( + patch(f"{_PS}.prisma_client", mock_prisma), + patch(f"{_PS}.store_model_in_db", True), + patch(f"{_PS}.proxy_logging_obj", MagicMock()), + patch(f"{_PS}.general_settings", {}), + patch(f"{_PS}.premium_user", True), + patch(f"{_PS}.llm_router", MagicMock()), + ): + result = await delete_model_endpoint( + model_info=ModelInfoDelete(id=model_id), + user_api_key_dict=admin_user, + ) + assert "deleted successfully" in result["message"] + + mock_prisma.db.litellm_teamtable.find_many.assert_called_once_with( + where={"models": {"has": "gpt-4"}} + ) + assert mock_prisma.db.litellm_teamtable.update.call_count == 2 + + calls = mock_prisma.db.litellm_teamtable.update.call_args_list + team_a_call = next( + c for c in calls if c.kwargs["where"]["team_id"] == "team-a" + ) + assert team_a_call.kwargs["data"]["models"] == ["claude-sonnet"] + + team_b_call = next( + c for c in calls if c.kwargs["where"]["team_id"] == "team-b" + ) + assert team_b_call.kwargs["data"]["models"] == [] + + class TestGetTeamDeployments: """Tests for _get_team_deployments which filters by model_name prefix + Python-side team_id check."""