From 4875872fe559fb9d30a9efe3766106459dedd914 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:25:39 -0700 Subject: [PATCH] fix(cost): treat a non-mapping off_peak_pricing value as never off-peak A bare string or list under off_peak_pricing in YAML passed the truthy guard and crashed _is_off_peak with AttributeError, breaking cost calculation for that deployment. Malformed pieces of the block are documented to not match rather than error, so guard the block itself the same way and bill standard rates. --- .../litellm_core_utils/llm_cost_calc/utils.py | 2 +- .../llm_cost_calc/test_llm_cost_calc_utils.py | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/litellm/litellm_core_utils/llm_cost_calc/utils.py b/litellm/litellm_core_utils/llm_cost_calc/utils.py index c968fac254e..b34c416cd40 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/utils.py +++ b/litellm/litellm_core_utils/llm_cost_calc/utils.py @@ -443,7 +443,7 @@ def _apply_off_peak_pricing( off_peak_pricing falls back to the standard rate. """ off_peak: Final = model_info.get("off_peak_pricing") - if not off_peak or not _is_off_peak(off_peak, current_time): + if not isinstance(off_peak, Mapping) or not _is_off_peak(off_peak, current_time): return prompt_base_cost, completion_base_cost, cache_read_cost return ( _coerce_off_peak_rate(off_peak.get("input_cost_per_token"), prompt_base_cost), diff --git a/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py b/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py index 501a9314c90..0e1c832ebf5 100644 --- a/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py +++ b/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py @@ -648,6 +648,33 @@ def test_get_token_base_cost_applies_off_peak_pricing(): assert peak[4] == 1e-7 +def test_get_token_base_cost_non_mapping_off_peak_block_bills_standard_rates(): + """A truthy non-mapping off_peak_pricing value (a bare string or a list in + YAML) must bill standard rates rather than raising, matching how every + other malformed piece of the block behaves. + """ + from datetime import datetime, timezone + from typing import cast + + from litellm.types.utils import ModelInfo + + usage = Usage(prompt_tokens=100, completion_tokens=50, total_tokens=150) + when = datetime(2026, 1, 1, 18, 0, tzinfo=timezone.utc) + + for malformed_block in ("16:00-19:00", ["16:00-19:00"], 5e-7, True): + model_info = cast( + ModelInfo, + { + "input_cost_per_token": 1e-6, + "output_cost_per_token": 2e-6, + "off_peak_pricing": malformed_block, + }, + ) + result = _get_token_base_cost(model_info, usage, current_time=when) + assert result[0] == 1e-6 + assert result[1] == 2e-6 + + def test_get_token_base_cost_off_peak_falls_back_to_standard_when_unset(): from datetime import datetime, timezone from typing import cast