From 988f73ef6e985ef78af294702d1deffc476e9ccb Mon Sep 17 00:00:00 2001 From: Aarkin7 Date: Thu, 4 Jun 2026 22:05:15 +0530 Subject: [PATCH] fix(router): make delete_deployment wildcard cleanup symmetric with upsert After the previous commit moved _remove_deployment_from_wildcard_state out of the inner index-map guard in upsert_deployment, delete_deployment was still calling it only inside `if deployment_idx is not None`. Greptile flagged the asymmetry: under a desynced index_map, delete would silently leave the stale wildcard credential in pattern_router. Moves the cleanup call to the top of the try block, mirroring the upsert path. Cleanup is idempotent so the change is a no-op on the happy path. Adds a regression test that simulates the desync by removing the entry from model_id_to_deployment_index_map and asserts delete still clears pattern_router. --- litellm/router.py | 4 +++- tests/test_litellm/test_router.py | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/litellm/router.py b/litellm/router.py index c3c4172d1fd..3028bb8aeed 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -8715,6 +8715,9 @@ class Router: deployment_idx = self.model_id_to_deployment_index_map[id] try: + # Idempotent and symmetric with upsert_deployment, so a desynced + # index_map cannot leave stale wildcard credentials behind. + self._remove_deployment_from_wildcard_state(model_id=id) if deployment_idx is not None: # Pop the item from the list first item = self.model_list.pop(deployment_idx) @@ -8723,7 +8726,6 @@ class Router: self._update_deployment_indices_after_removal( model_id=id, removal_idx=deployment_idx ) - self._remove_deployment_from_wildcard_state(model_id=id) _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/test_litellm/test_router.py b/tests/test_litellm/test_router.py index dad81ef2edf..561c4a4a5e1 100644 --- a/tests/test_litellm/test_router.py +++ b/tests/test_litellm/test_router.py @@ -4448,3 +4448,24 @@ def test_remove_deployment_from_wildcard_state_is_noop_for_empty_id(): assert router.pattern_router.patterns == snapshot_patterns assert router.provider_default_deployment_ids == snapshot_ids + + +def test_delete_deployment_cleans_wildcard_state_even_when_index_is_desynced(): + """ + Symmetric with the upsert path: delete_deployment must clean wildcard + state from the model_id regardless of whether the fast-mapping index + still knows about it. Simulates index corruption / partial-failure by + removing the entry from model_id_to_deployment_index_map while leaving + pattern_router intact, then calls delete and asserts pattern_router is + cleaned. + """ + router, _ = _build_wildcard_router(api_key="key-A") + model_id = "wildcard-deployment-1" + + assert router.pattern_router.patterns + del router.model_id_to_deployment_index_map[model_id] + + router.delete_deployment(id=model_id) + + assert router.pattern_router.patterns == {} + assert model_id not in router.provider_default_deployment_ids