diff --git a/litellm/router.py b/litellm/router.py index 5ffe60c2da0..61af13a8745 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -8212,6 +8212,7 @@ class Router: self._invalidate_model_group_info_cache() self._invalidate_access_groups_cache() self._update_deployment_indices_after_removal(model_id=deployment_id, removal_idx=removal_idx) + self._deregister_strategy_router(model_name=deployment.model_name) # if the model_id is not in router self.add_deployment(deployment=deployment) @@ -8225,6 +8226,18 @@ class Router: else: raise e + def _deregister_strategy_router(self, model_name: Optional[str]) -> None: + """Drop any per-strategy router (auto/complexity/quality/adaptive) registered under + model_name. Called whenever a deployment is removed so the same model_name can be + cleanly re-added; otherwise re-adding raises " already exists" (silently + swallowed when ignore_invalid_deployments=True), dropping it from model_list.""" + if model_name is None: + return + self.auto_routers.pop(model_name, None) + self.complexity_routers.pop(model_name, None) + self.quality_routers.pop(model_name, None) + self.adaptive_routers.pop(model_name, None) + def delete_deployment(self, id: str) -> Optional[Deployment]: """ Parameters: @@ -8245,6 +8258,7 @@ class Router: self._invalidate_model_group_info_cache() self._invalidate_access_groups_cache() self._update_deployment_indices_after_removal(model_id=id, removal_idx=deployment_idx) + self._deregister_strategy_router(model_name=item.get("model_name")) _budget_limiter = self._get_router_deployment_budget_limiter() if _budget_limiter is not None: _budget_limiter.unregister_deployment_budget(model_id=id) diff --git a/tests/router_unit_tests/test_router_helper_utils.py b/tests/router_unit_tests/test_router_helper_utils.py index 848a6c28a57..e1a3978b6f6 100644 --- a/tests/router_unit_tests/test_router_helper_utils.py +++ b/tests/router_unit_tests/test_router_helper_utils.py @@ -1854,6 +1854,57 @@ def test_init_auto_router_deployment_duplicate_model_name(mock_auto_router, mode router.init_auto_router_deployment(deployment) +def _auto_router_deployment(config="/path/to/config", model_id="ar-id"): + return Deployment( + model_name="my-auto-router", + litellm_params=LiteLLM_Params( + model="auto_router/my-auto-router", + auto_router_config_path=config, + auto_router_default_model="gpt-5-mini", + auto_router_embedding_model="text-embedding-3-small", + ), + model_info=ModelInfo(id=model_id, db_model=True), + ) + + +@patch("litellm.router_strategy.auto_router.auto_router.AutoRouter") +def test_delete_deployment_deregisters_auto_router(mock_auto_router, model_list): + """Regression: deleting an auto-router deployment must drop it from ``auto_routers`` so the + same model_name can be re-added. Previously the stale entry made the re-add raise + 'already exists', which (with ignore_invalid_deployments) silently dropped the deployment.""" + mock_auto_router.return_value = MagicMock() + router = Router(model_list=model_list, ignore_invalid_deployments=True) + + router.add_deployment(deployment=_auto_router_deployment()) + assert "my-auto-router" in router.auto_routers + assert router.has_model_id("ar-id") + + router.delete_deployment(id="ar-id") + assert "my-auto-router" not in router.auto_routers + + router.add_deployment(deployment=_auto_router_deployment()) + assert router.has_model_id("ar-id") + assert "my-auto-router" in router.auto_routers + assert any(m.get("model_name") == "my-auto-router" for m in router.model_list) + + +@patch("litellm.router_strategy.auto_router.auto_router.AutoRouter") +def test_upsert_auto_router_deployment_update_keeps_it_registered(mock_auto_router, model_list): + """Regression for the semantic router 'keeps disappearing' bug: upserting an auto-router + deployment with changed params (as the periodic DB sync does) must keep it in ``model_list`` + and ``auto_routers`` instead of silently dropping it via an 'already exists' error.""" + mock_auto_router.return_value = MagicMock() + router = Router(model_list=model_list, ignore_invalid_deployments=True) + + router.upsert_deployment(deployment=_auto_router_deployment(config="/path/v1")) + assert router.has_model_id("ar-id") + + router.upsert_deployment(deployment=_auto_router_deployment(config="/path/v2")) + assert router.has_model_id("ar-id") + assert "my-auto-router" in router.auto_routers + assert any(m.get("model_name") == "my-auto-router" for m in router.model_list) + + def test_generate_model_id_with_deployment_model_name(model_list): """Test that _generate_model_id works correctly with deployment model_name and handles None values properly""" router = Router(model_list=model_list) diff --git a/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.tsx b/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.tsx index 1714cfb3907..2d65b46aad3 100644 --- a/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.tsx +++ b/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.tsx @@ -147,6 +147,11 @@ const AddAutoRouterTab: React.FC = ({ form, handleOk, acc return; } + if (!currentFormValues.auto_router_embedding_model && !currentFormValues.custom_embedding_model) { + NotificationManager.fromBackend("Please select an Embedding Model"); + return; + } + form.setFieldsValue({ custom_llm_provider: "auto_router", model: currentFormValues.auto_router_name, @@ -330,15 +335,16 @@ const AddAutoRouterTab: React.FC = ({ form, handleOk, acc {/* Auto Router Embedding Model */} { setShowCustomEmbeddingModel(value === "custom"); form.setFieldValue("auto_router_embedding_model", value); diff --git a/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx b/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx index caa061147d8..cba4d3b8897 100644 --- a/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx +++ b/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx @@ -106,7 +106,7 @@ const EditAutoRouterModal: React.FC = ({ ...modelData.litellm_params, auto_router_config: JSON.stringify(routerConfig), auto_router_default_model: values.auto_router_default_model, - auto_router_embedding_model: values.auto_router_embedding_model || undefined, + auto_router_embedding_model: values.auto_router_embedding_model, }; // Prepare updated model_info @@ -205,15 +205,18 @@ const EditAutoRouterModal: React.FC = ({ {/* Embedding Model */} - + { setShowCustomEmbeddingModel(value === "custom"); }} options={[...modelOptions, { value: "custom", label: "Enter custom model name" }]} showSearch={true} - allowClear />