From 5dd3fdbc3dce6cac86b6a5bde930b0fd59d8d301 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 17:31:05 -0700 Subject: [PATCH] fix(router): evict stale global pattern_router entries on upsert/delete (#39664) * fix(router): evict stale global pattern_router entries on upsert/delete upsert_deployment and delete_deployment cleaned team_pattern_routers but left the outgoing deployment in the global pattern_router, so wildcard requests kept round-robining onto the stale entry after a PATCH /model/{id}/update. Fixes #29064 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(router): dedupe test_get_configured_mode_reads_deployment_model_info name Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(router): restore global pattern_router eviction dropped by previous commit Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: yassin Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/router.py | 1 + tests/test_litellm/test_router.py | 34 +++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/litellm/router.py b/litellm/router.py index dea9aa62729..6da201725b6 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -9546,6 +9546,7 @@ class Router: public_model_name for _, public_model_name in self.team_model_to_deployment_indices ) + self.pattern_router.remove_deployment(model_id) for team_id in list(self.team_pattern_routers.keys()): team_pattern_router = self.team_pattern_routers[team_id] team_pattern_router.remove_deployment(model_id) diff --git a/tests/test_litellm/test_router.py b/tests/test_litellm/test_router.py index f7f0d79b4fd..5fc96bcfbb1 100644 --- a/tests/test_litellm/test_router.py +++ b/tests/test_litellm/test_router.py @@ -5101,6 +5101,40 @@ def test_team_wildcard_credentials_not_usable_after_delete_deployment(): ) +def test_global_wildcard_pattern_router_evicts_stale_entry_on_upsert_and_delete(): + """ + Regression for #29064: upsert_deployment removed the old deployment from + model_list but left it in the global pattern_router, so wildcard requests + round-robined between the stale and the corrected deployment. + """ + from litellm.types.router import Deployment, LiteLLM_Params + + router = litellm.Router( + model_list=[ + { + "model_name": "openai/*", + "litellm_params": {"model": "openai/openai/*", "api_key": "sk-old"}, + "model_info": {"id": "global-wildcard"}, + } + ] + ) + + router.upsert_deployment( + Deployment( + model_name="openai/*", + litellm_params=LiteLLM_Params(model="openai/*", api_key="sk-new"), + model_info={"id": "global-wildcard"}, + ) + ) + + matches = router.pattern_router.route("openai/gpt-5.2") + assert matches is not None + assert [m["litellm_params"]["api_key"] for m in matches] == ["sk-new"] + + router.delete_deployment(id="global-wildcard") + assert router.pattern_router.patterns == {} + + def test_pattern_match_router_remove_deployment(): """ remove_deployment must drop only the deployment with the given model id and