From e696ab1850c6703c4e77e91b4ac7d48a0ac9a5f8 Mon Sep 17 00:00:00 2001 From: LancyZhao <207653224+LancyZhao@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:58:34 +0800 Subject: [PATCH] test(router): lock deployment-level cache-read price of 0 as a real price a168bab rewrote the deployment-level lookup to drop two mutable-dict literals and, in doing so, replaced a truthiness check with an explicit `is not None`. That silently fixed a behavior bug the commit message never claimed: a deployment configuring `cache_read_input_token_cost: 0` was read as unset, fell through to the cost map and then to the input-cost fallback, and lost the tie to a deployment whose cache reads are actually billed. Providers that do not charge for cache reads make 0 a real configuration, not an edge case. No test covered it, so nothing stops the next rewrite from going back to `or` / truthiness. This adds the missing case: verified failing on bdfb64c's implementation (both list orders) and passing on the current one. --- .../router_strategy/test_lowest_cost.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/test_litellm/router_strategy/test_lowest_cost.py b/tests/test_litellm/router_strategy/test_lowest_cost.py index 309780f217d..c95b16daeba 100644 --- a/tests/test_litellm/router_strategy/test_lowest_cost.py +++ b/tests/test_litellm/router_strategy/test_lowest_cost.py @@ -45,3 +45,44 @@ async def test_cost_routing_breaks_input_output_tie_on_cache_read_cost(cheaper_c ) assert selected["model_info"]["id"] == "cheaper-cache" + + +@pytest.mark.parametrize("free_cache_first", [True, False]) +@pytest.mark.asyncio +async def test_cost_routing_honors_zero_deployment_cache_read_cost(free_cache_first): + """ + A deployment-level cache_read_input_token_cost of 0 is a price, not a missing value. + + Truthiness checks treat it as unset and fall through to the cost map / input-cost + fallback, which ranks a deployment whose cache reads are free as the priciest one + in a tie. Providers that do not charge for cache reads make this a real config. + """ + free = { + "model_name": "cache-zero-test", + "litellm_params": { + "model": "openai/zero-model-not-in-cost-map", + "input_cost_per_token": 1e-06, + "output_cost_per_token": 2e-06, + "cache_read_input_token_cost": 0.0, + }, + "model_info": {"id": "free-cache"}, + } + paid = { + "model_name": "cache-zero-test", + "litellm_params": { + "model": "openai/zero-model-not-in-cost-map", + "input_cost_per_token": 1e-06, + "output_cost_per_token": 2e-06, + "cache_read_input_token_cost": 1e-08, + }, + "model_info": {"id": "paid-cache"}, + } + model_list = [free, paid] if free_cache_first else [paid, free] + + logger = LowestCostLoggingHandler(router_cache=DualCache()) + + selected = await logger.async_get_available_deployments( + model_group="cache-zero-test", healthy_deployments=model_list + ) + + assert selected["model_info"]["id"] == "free-cache"