From d4d29d237c7c539e85ce01dbdccc762f6b20ebe5 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Sun, 6 Sep 2026 10:55:34 +0000 Subject: [PATCH] fix(router): release a deployment's cost-map key when it is deleted The ownership ledger only grew. A deleted deployment kept its claim, so if a later catalog refresh started publishing a model under that same name, the next registration would treat the catalog entry as the deployment's own and evict it. Deleting a deployment now gives the key back, which also stops the ledger growing for the life of the process. --- litellm/router.py | 4 ++- .../test_router_model_cost_isolation.py | 33 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/litellm/router.py b/litellm/router.py index 0dd4af038b9..95c95f86f87 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -615,7 +615,8 @@ RETRY_BREADCRUMB_LIMIT: Final = 4 # Cost-map keys created by _register_deployment_in_model_cost, which shares one flat # namespace with the built-in model catalog. Only a key it created may be evicted, or a # deployment whose id names a real model would strip that model's pricing and -# capabilities for every other deployment of it. +# capabilities for every other deployment of it. delete_deployment gives a key back, so a +# later catalog refresh that starts serving that name is not treated as a deployment's own. _DEPLOYMENT_COST_MAP_KEYS: Final[set[str]] = set() # mutable-ok: ownership of shared cost-map keys @@ -9874,6 +9875,7 @@ class Router: _budget_limiter: Final = self._get_router_deployment_budget_limiter() if _budget_limiter is not None: _budget_limiter.unregister_deployment_budget(model_id=id) + _DEPLOYMENT_COST_MAP_KEYS.discard(id) try: self._unregister_pre_routing_strategy_for_deployment( deployment=item if isinstance(item, Deployment) else Deployment(**item) diff --git a/tests/test_litellm/test_router_model_cost_isolation.py b/tests/test_litellm/test_router_model_cost_isolation.py index b9cbcbdfb08..749af1da7c0 100644 --- a/tests/test_litellm/test_router_model_cost_isolation.py +++ b/tests/test_litellm/test_router_model_cost_isolation.py @@ -315,6 +315,39 @@ def test_should_drop_a_stale_price_even_when_the_deployment_declares_a_provider( _restore_model_cost_entries(original) +def test_should_give_a_cost_map_key_back_when_the_deployment_is_deleted(): + """Deleting a deployment releases its claim on the shared cost-map key. + + Held forever, a later catalog refresh that starts publishing a model under that same + name would be treated as the deleted deployment's own entry and evicted. + """ + from litellm.router import _DEPLOYMENT_COST_MAP_KEYS + + model_id = "deployment-to-delete" + original = {model_id: litellm.model_cost.get(model_id)} + router = Router( + model_list=[ + { + "model_name": "to-delete", + "litellm_params": {"model": "gpt-4o-mini", "mock_response": "ok"}, + "model_info": {"id": model_id, "input_cost_per_token": 0.005}, + } + ] + ) + + try: + assert model_id in _DEPLOYMENT_COST_MAP_KEYS + + assert router.delete_deployment(id=model_id) is not None + + assert model_id not in _DEPLOYMENT_COST_MAP_KEYS, ( + "a deleted deployment kept its claim on the shared cost-map key" + ) + finally: + _DEPLOYMENT_COST_MAP_KEYS.discard(model_id) + _restore_model_cost_entries(original) + + def test_should_preserve_builtin_pricing_regardless_of_deployment_order(): """ The built-in pricing should be preserved no matter which deployment