diff --git a/litellm/litellm_core_utils/llm_cost_calc/utils.py b/litellm/litellm_core_utils/llm_cost_calc/utils.py index 9f28fe2b6fb..2527013e118 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 date, datetime, timezone, tzinfo +from datetime import date, datetime, time, timezone, tzinfo from types import MappingProxyType from typing import Final, Literal, TypedDict, cast from zoneinfo import ZoneInfo, ZoneInfoNotFoundError @@ -408,14 +408,7 @@ def _is_within_off_peak_window(off_peak_hours_utc: str | Sequence[str], current_ """ reference: Final = current_time if current_time is not None else current_billing_time() now: Final = (reference.astimezone(timezone.utc) if reference.tzinfo is not None else reference).time() - windows: Final = (off_peak_hours_utc,) if isinstance(off_peak_hours_utc, str) else off_peak_hours_utc - for window in windows: - try: - start_str, end_str = window.split("-") - start = datetime.strptime(start_str.strip(), "%H:%M").replace(tzinfo=timezone.utc).time() - end = datetime.strptime(end_str.strip(), "%H:%M").replace(tzinfo=timezone.utc).time() - except (ValueError, AttributeError): - continue + for start, end in _parse_windows(off_peak_hours_utc): if start < end: if start <= now < end: return True @@ -447,6 +440,21 @@ _WEEKDAY_NUMBERS: Final = MappingProxyType( ) +def _parse_window(window: object) -> tuple[time, time] | None: + try: + start_str, end_str = window.split("-") + start: Final = datetime.strptime(start_str.strip(), "%H:%M").replace(tzinfo=timezone.utc).time() + end: Final = datetime.strptime(end_str.strip(), "%H:%M").replace(tzinfo=timezone.utc).time() + except (ValueError, AttributeError): + return None + return (start, end) + + +def _parse_windows(off_peak_hours_utc: str | Sequence[str]) -> tuple[tuple[time, time], ...]: + windows: Final = (off_peak_hours_utc,) if isinstance(off_peak_hours_utc, str) else off_peak_hours_utc + return tuple(parsed for window in windows if (parsed := _parse_window(window)) is not None) + + def _normalize_weekday(value: object) -> int | None: if isinstance(value, bool): return None @@ -522,8 +530,8 @@ def _is_off_peak(off_peak: Mapping[str, object], current_time: datetime | None = hours apply only on its weekdays. A rule carrying override_dates applies only on those dates, read on the weekday_timezone calendar: on a listed date the matching rules alone decide (their weekdays are ignored, and the flat hours_utc and every other rule are - skipped), and on any other date the rule never applies (a malformed override_dates or - hours_utc disables the rule entirely). + skipped), and on any other date the rule never applies (a malformed override_dates, or an + hours_utc none of whose windows parse, disables the rule entirely). """ reference: Final = current_time if current_time is not None else current_billing_time() reference_utc: Final = ( @@ -540,7 +548,7 @@ def _is_off_peak(off_peak: Mapping[str, object], current_time: datetime | None = override_rules: Final = tuple( rule for rule in rules - if _as_window_strings(rule.get("hours_utc")) and today in _as_dates(rule.get("override_dates")) + if _parse_windows(_as_window_strings(rule.get("hours_utc"))) and today in _as_dates(rule.get("override_dates")) ) if override_rules: for rule in override_rules: 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 385167e24fa..39a6ea684df 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 @@ -723,6 +723,9 @@ def test_is_off_peak_ignores_malformed_override_dates(): ["2026-1-1"], ["2026-W01-4"], ["2026-01-1 "], + ["2026-02-30"], + ["2026-13-01"], + ["2026-01-011"], ): block = {"windows": peak_windows + [{"hours_utc": "00:00-00:00", "override_dates": bad}]} assert _is_off_peak(block, peak_instant) is False, ( @@ -740,11 +743,47 @@ def test_is_off_peak_override_rule_without_valid_hours_is_disabled(): "windows": [ {"override_dates": ["2026-03-03"], "hours_utc": 5}, {"override_dates": ["2026-03-03"]}, + {"override_dates": ["2026-03-03"], "hours_utc": "garbage"}, + {"override_dates": ["2026-03-03"], "hours_utc": "25:00-26:00"}, + {"override_dates": ["2026-03-03"], "hours_utc": ["4pm-midnight"]}, ], } assert _is_off_peak(block, datetime(2026, 3, 3, 12, 0, tzinfo=timezone.utc)) is True +def test_is_off_peak_override_rule_ignores_its_weekdays(): + """On a date an override rule lists, its own weekdays field is ignored: a weekend-only + holiday rule still applies on the Thursday holiday.""" + from datetime import datetime, timezone + + block = { + "windows": [ + {"hours_utc": ["00:00-01:00", "04:00-06:00", "10:00-00:00"], "weekdays": [1, 2, 3, 4, 5]}, + {"hours_utc": "00:00-00:00", "weekdays": [6, 7], "override_dates": ["2026-01-01"]}, + ], + } + holiday_thursday = datetime(2026, 1, 1, 2, 0, tzinfo=timezone.utc) + assert _is_off_peak(block, holiday_thursday) is True, ( + f"{holiday_thursday.isoformat()} is a Thursday holiday billed off-peak all day" + ) + + +def test_is_off_peak_matching_override_rules_are_a_union(): + """Every rule listing today's date applies: the date is off-peak inside any of their + windows, peak outside all of them.""" + from datetime import datetime, timezone + + block = { + "windows": [ + {"override_dates": ["2026-03-03"], "hours_utc": "01:00-02:00"}, + {"override_dates": ["2026-03-03"], "hours_utc": "12:00-13:00"}, + ], + } + assert _is_off_peak(block, datetime(2026, 3, 3, 1, 30, tzinfo=timezone.utc)) is True + assert _is_off_peak(block, datetime(2026, 3, 3, 12, 30, tzinfo=timezone.utc)) is True + assert _is_off_peak(block, datetime(2026, 3, 3, 5, 0, tzinfo=timezone.utc)) is False + + 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.""" @@ -803,6 +842,35 @@ def test_override_dates_use_weekday_timezone_calendar_on_shipped_deepseek_rows() ] assert not drifted, f"off_peak schedule drift on {drifted}" + from datetime import date, timedelta + + windows: Final = block["windows"] + assert windows[:2] == [ + {"hours_utc": ["00:00-01:00", "04:00-06:00", "10:00-00:00"], "weekdays": [1, 2, 3, 4, 5]}, + {"hours_utc": "00:00-00:00", "weekdays": [6, 7]}, + ] + holidays: Final = { + (start + timedelta(days=offset)).isoformat() + for start, days in ( + (date(2026, 1, 1), 3), + (date(2026, 2, 15), 9), + (date(2026, 4, 4), 3), + (date(2026, 5, 1), 5), + (date(2026, 6, 19), 3), + (date(2026, 9, 25), 3), + (date(2026, 10, 1), 7), + ) + for offset in range(days) + } + workdays: Final = {"2026-01-04", "2026-02-14", "2026-02-28", "2026-05-09", "2026-09-20", "2026-10-10"} + holiday_rule, workday_rule = windows[2], windows[3] + assert holiday_rule["hours_utc"] == "00:00-00:00" + assert set(holiday_rule["override_dates"]) == holidays + assert workday_rule["hours_utc"] == windows[0]["hours_utc"] + assert set(workday_rule["override_dates"]) == workdays + assert holidays.isdisjoint(workdays) + assert all(date.fromisoformat(day).isoweekday() in (6, 7) for day in workdays) + shanghai_make_up_sunday = datetime(2026, 1, 3, 17, 0, tzinfo=timezone.utc) make_up_sunday_peak = datetime(2026, 1, 4, 2, 0, tzinfo=timezone.utc) plain_sunday = datetime(2026, 1, 11, 2, 0, tzinfo=timezone.utc) diff --git a/tests/test_litellm/test_model_prices_schema.py b/tests/test_litellm/test_model_prices_schema.py index 2437a8b6700..329d62b4efe 100644 --- a/tests/test_litellm/test_model_prices_schema.py +++ b/tests/test_litellm/test_model_prices_schema.py @@ -392,7 +392,11 @@ def deepseek_off_peak_drift(entry: Mapping[str, object]) -> str | None: windows: Final = tuple(block.get("windows", ())) if windows[: len(DEEPSEEK_OFF_PEAK_WINDOWS)] != DEEPSEEK_OFF_PEAK_WINDOWS: return f"windows={block.get('windows')}" - if not all("override_dates" in rule for rule in windows[len(DEEPSEEK_OFF_PEAK_WINDOWS) :]): + if not all( + "override_dates" in rule + and rule.get("hours_utc") in ("00:00-00:00", DEEPSEEK_OFF_PEAK_WINDOWS[0]["hours_utc"]) + for rule in windows[len(DEEPSEEK_OFF_PEAK_WINDOWS) :] + ): return f"windows={block.get('windows')}" halved: Final = {rate: block.get(rate) for rate in DEEPSEEK_HALVED_RATES} expected: Final = {rate: float(str(entry[rate])) / 2 for rate in DEEPSEEK_HALVED_RATES} diff --git a/tests/test_litellm/test_utils.py b/tests/test_litellm/test_utils.py index 46ae7fd1e79..e3a5820396b 100644 --- a/tests/test_litellm/test_utils.py +++ b/tests/test_litellm/test_utils.py @@ -1057,7 +1057,11 @@ 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"}}, + "override_dates": { + "type": "array", + "minItems": 1, + "items": {"type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$"}, + }, }, "required": ["hours_utc"], "additionalProperties": False,