From 549e6d4f465ec9cf4ca80d3d7e2541ff61ea29ef Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2026 17:17:52 -0700 Subject: [PATCH] fix(router): keep DB-only deployments priced across price data reloads (#48486) * fix(router): track every router for price data reloads so DB-only deployments keep custom pricing Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(router): register the router for reload replay only once model_list exists, and type the regression test Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: joshua Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/router.py | 7 +++-- .../unit/test_router_model_cost_isolation.py | 30 +++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/litellm/router.py b/litellm/router.py index ddb4b9bc075..8d95e3e91d4 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -1483,15 +1483,16 @@ class Router: if model_list is not None: # set_model_list will build indices automatically self.set_model_list(model_list) - # Track this router so a price data reload can rebuild its deployments' - # cost-map entries from the list it is serving at that moment. - _live_routers.add(self) self.healthy_deployments: list = self.model_list for m in model_list: if "model" in m["litellm_params"]: self.deployment_latency_map[m["litellm_params"]["model"]] = 0 else: self.model_list: list = [] # initialize an empty list - to allow _add_deployment and delete_deployment to work + # Track this router so a price data reload can rebuild its deployments' + # cost-map entries from the list it is serving at that moment, whether + # those deployments came from model_list or were added later from the DB. + _live_routers.add(self) if allowed_fails is not None: self.allowed_fails = allowed_fails diff --git a/tests/unit/test_router_model_cost_isolation.py b/tests/unit/test_router_model_cost_isolation.py index f001008ee2c..20b0055f729 100644 --- a/tests/unit/test_router_model_cost_isolation.py +++ b/tests/unit/test_router_model_cost_isolation.py @@ -3081,6 +3081,36 @@ def test_reload_keeps_custom_pricing_configured_on_litellm_params_for_a_db_model _invalidate_model_cost_lowercase_map() +def test_reload_keeps_custom_pricing_of_a_db_model_on_a_router_built_without_a_model_list( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """ + A proxy whose config.yaml has no model_list builds its router without one and + takes every deployment from the DB afterwards. A price data reload must restore + that deployment's custom pricing exactly as it does for a config router. + """ + fetched_catalog: Final = copy.deepcopy(litellm.model_cost) + router: Final = Router() + router.add_deployment( + deployment=Deployment( + model_name="db-only-priced", + litellm_params=LiteLLM_Params( + model="openai/gpt-4o", + api_key="sk-fake", + input_cost_per_token=0.000123, + output_cost_per_token=0.000456, + ), + model_info=ModelInfo(id="db-only-priced-id"), + ) + ) + assert litellm.model_cost["db-only-priced-id"]["input_cost_per_token"] == 0.000123 + + _simulate_price_data_reload_with_provider_sets(monkeypatch, fetched_catalog) + + assert litellm.model_cost["db-only-priced-id"]["input_cost_per_token"] == 0.000123 + assert litellm.model_cost["db-only-priced-id"]["output_cost_per_token"] == 0.000456 + + def test_replay_live_router_model_cost_rebuilds_every_live_router(): """ A process can hold more than one Router, so the rebuild has to fan out across