fix(router): break cost-based-routing input+output ties on cache-read price

This commit is contained in:
LancyZhao 2026-08-24 17:16:04 +08:00
parent f005afa146
commit 8dd25b9e66
No known key found for this signature in database
2 changed files with 56 additions and 2 deletions

View file

@ -262,6 +262,11 @@ class LowestCostLoggingHandler(CustomLogger):
if item_output_cost is None:
item_output_cost = item_litellm_model_cost_map.get("output_cost_per_token", 5.0)
# Secondary ranking signal used to break ties on total input+output price.
# Fall back to the input cost when a model has no cache-read price, so models
# missing that field are not ranked as if their cache reads were free.
item_cache_read_cost = item_litellm_model_cost_map.get("cache_read_input_token_cost", item_input_cost)
# if litellm["model"] is not in model_cost map -> use item_cost = $10
item_cost = item_input_cost + item_output_cost
@ -294,12 +299,12 @@ class LowestCostLoggingHandler(CustomLogger):
): # if user passed in tpm / rpm in the model_list
continue
else:
potential_deployments.append((_deployment, item_cost))
potential_deployments.append((_deployment, item_cost, item_cache_read_cost))
if len(potential_deployments) == 0:
return None
potential_deployments = sorted(potential_deployments, key=lambda x: x[1])
potential_deployments = sorted(potential_deployments, key=lambda x: (x[1], x[2]))
selected_deployment: Final = potential_deployments[0][0]
return selected_deployment

View file

@ -199,3 +199,52 @@ async def test_get_available_endpoints_tpm_rpm_check_async(ans_rpm):
assert (d_ans and d_ans["model_info"]["id"]) == ans
print("selected deployment:", d_ans)
@pytest.mark.parametrize("cheaper_cache_first", [True, False])
@pytest.mark.asyncio
async def test_lowest_cost_routing_breaks_input_output_tie_on_cache_read_cost(
cheaper_cache_first,
):
"""
Regression test for https://github.com/BerriAI/litellm/issues/38064
When two deployments tie on input + output price, cost-based routing must
fall back to the cache-read price instead of returning whichever deployment
happens to be listed first in the model_list.
"""
import litellm
cheaper_cache = "tencent/deepseek-v4-flash" # lower cache_read_input_token_cost
pricier_cache = "fireworks_ai/deepseek-v4-flash"
# The assertion is only meaningful while the shipped price map still ties these
# two models on input+output and separates them on cache-read price.
cheap = litellm.model_cost[cheaper_cache]
pricey = litellm.model_cost[pricier_cache]
assert (
cheap["input_cost_per_token"] + cheap["output_cost_per_token"]
== pricey["input_cost_per_token"] + pricey["output_cost_per_token"]
), "precondition: the two deployments must tie on input+output price"
assert (
cheap["cache_read_input_token_cost"] < pricey["cache_read_input_token_cost"]
), "precondition: cheaper_cache must have the lower cache-read price"
ordered = [cheaper_cache, pricier_cache] if cheaper_cache_first else [pricier_cache, cheaper_cache]
model_list = [
{
"model_name": "deepseek-v4-flash",
"litellm_params": {"model": model},
"model_info": {"id": model},
}
for model in ordered
]
lowest_cost_logger = LowestCostLoggingHandler(router_cache=DualCache())
selected = await lowest_cost_logger.async_get_available_deployments(
model_group="deepseek-v4-flash", healthy_deployments=model_list
)
# Cheaper cache-read deployment must win regardless of model_list ordering.
assert selected["model_info"]["id"] == cheaper_cache