diff --git a/litellm/litellm_core_utils/llm_cost_calc/utils.py b/litellm/litellm_core_utils/llm_cost_calc/utils.py index 2f0e2a61086..27ef9176c13 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/utils.py +++ b/litellm/litellm_core_utils/llm_cost_calc/utils.py @@ -4,7 +4,7 @@ import re from collections.abc import Mapping, Sequence from dataclasses import dataclass -from datetime import datetime, timezone, tzinfo +from datetime import date, datetime, timezone, tzinfo from types import MappingProxyType from typing import Final, Literal, TypedDict, cast from zoneinfo import ZoneInfo, ZoneInfoNotFoundError @@ -492,10 +492,23 @@ def _as_window_strings(value: object) -> tuple[str, ...]: return () +def _is_iso_date(value: object) -> bool: + if not isinstance(value, str) or len(value) != 10: + return False + try: + date.fromisoformat(value) + except ValueError: + return False + return True + + def _as_date_strings(value: object) -> tuple[str, ...]: if isinstance(value, str) or not isinstance(value, Sequence): return () - return tuple(entry for entry in value if isinstance(entry, str)) + entries: Final = tuple(value) + if not entries or not all(_is_iso_date(entry) for entry in entries): + return () + return entries def _is_off_peak(off_peak: Mapping[str, object], current_time: datetime | None = None) -> bool: 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 0f61bbdf674..4818f378b1d 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 @@ -724,6 +724,22 @@ def test_is_off_peak_ignores_malformed_override_dates(): ) +def test_is_off_peak_disables_rules_with_partially_malformed_override_dates(): + """One bad entry disables the whole override_dates list: a rule whose dates do not all + parse never applies on any date, so the flat hours decide alone.""" + from datetime import datetime, timezone + + peak_instant = datetime(2026, 3, 4, 12, 0, tzinfo=timezone.utc) + for bad in (["2026-03-04", 20260304], ["2026-03-04", "2026-3-5"]): + block = { + "hours_utc": "00:30-01:00", + "windows": [{"hours_utc": "00:00-00:00", "override_dates": bad}], + } + assert _is_off_peak(block, peak_instant) is False, ( + f"override_dates={bad!r} must disable its rule, leaving only the closed flat window" + ) + + def test_is_off_peak_without_override_dates_is_unchanged(): """The plain DeepSeek windows with no override_dates bill a holiday peak hour at standard rates.""" diff --git a/tests/test_litellm/test_utils.py b/tests/test_litellm/test_utils.py index 3fca6935d57..46ae7fd1e79 100644 --- a/tests/test_litellm/test_utils.py +++ b/tests/test_litellm/test_utils.py @@ -1057,6 +1057,7 @@ def test_aaamodel_prices_and_context_window_json_is_valid(): "type": "array", "items": {"oneOf": [{"type": "integer"}, {"type": "string"}]}, }, + "override_dates": {"type": "array", "items": {"type": "string"}}, }, "required": ["hours_utc"], "additionalProperties": False,