From cf5082d8077212669b8cb91f2cc3c814007f02e8 Mon Sep 17 00:00:00 2001 From: Tanishq <74068132+tanishqbaweja@users.noreply.github.com> Date: Mon, 26 Oct 2026 01:34:04 +0530 Subject: [PATCH] fix(router): tolerate null deployment metadata in cost callbacks --- litellm/router_strategy/lowest_cost.py | 4 +-- .../unit/router_strategy/test_lowest_cost.py | 33 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/litellm/router_strategy/lowest_cost.py b/litellm/router_strategy/lowest_cost.py index 5894da9cb4f..4862e52f5c6 100644 --- a/litellm/router_strategy/lowest_cost.py +++ b/litellm/router_strategy/lowest_cost.py @@ -58,7 +58,7 @@ class LowestCostLoggingHandler(CustomLogger): else: model_group: Final = kwargs["litellm_params"]["metadata"].get("model_group", None) - id = kwargs["litellm_params"].get("model_info", {}).get("id", None) + id = (kwargs["litellm_params"].get("model_info") or {}).get("id", None) if model_group is None or id is None: return elif isinstance(id, int): @@ -134,7 +134,7 @@ class LowestCostLoggingHandler(CustomLogger): else: model_group: Final = kwargs["litellm_params"]["metadata"].get("model_group", None) - id = kwargs["litellm_params"].get("model_info", {}).get("id", None) + id = (kwargs["litellm_params"].get("model_info") or {}).get("id", None) if model_group is None or id is None: return elif isinstance(id, int): diff --git a/tests/unit/router_strategy/test_lowest_cost.py b/tests/unit/router_strategy/test_lowest_cost.py index e3fdcd69499..c44d318501c 100644 --- a/tests/unit/router_strategy/test_lowest_cost.py +++ b/tests/unit/router_strategy/test_lowest_cost.py @@ -1,5 +1,7 @@ import asyncio, copy, importlib, os, time +import logging from datetime import datetime +from typing import Final import pytest @@ -36,6 +38,37 @@ def _recorded_minute_counters(cache: DualCache) -> dict[str, int]: return next(iter(minute_buckets.values())) +@pytest.mark.asyncio +@pytest.mark.parametrize("use_async", [False, True], ids=["sync", "async"]) +@pytest.mark.parametrize( + "deployment_params", + [{}, {"model_info": None}, {"model_info": {}}, {"model_info": {"id": None}}], + ids=["omitted", "null", "empty", "null-id"], +) +async def test_success_without_a_deployment_id_skips_cost_accounting_without_errors( + caplog: pytest.LogCaptureFixture, + use_async: bool, + deployment_params: dict[str, dict[str, str | None] | None], +) -> None: + cache: Final = DualCache() + handler: Final = LowestCostLoggingHandler(router_cache=cache) + call_args: Final = { + "kwargs": {"litellm_params": {"metadata": {"model_group": "gpt-5.5-pool"}, **deployment_params}}, + "response_obj": _chat_response_with_no_completion_tokens(), + "start_time": datetime(2026, 1, 1, 12, 0, 0), + "end_time": datetime(2026, 1, 1, 12, 0, 2), + } + + with caplog.at_level(logging.ERROR, logger="LiteLLM"): + if use_async: + await handler.async_log_success_event(**call_args) + else: + handler.log_success_event(**call_args) + + assert cache.get_cache(key=COST_KEY) is None + assert caplog.record_tuples == [] + + def test_log_success_event_counts_a_response_with_no_completion_tokens(): cache = DualCache() handler = LowestCostLoggingHandler(router_cache=cache)