From 36f3eb3a771d63ba73f9e4fd8757a0b31fa1de17 Mon Sep 17 00:00:00 2001 From: Srivatsa03 Date: Tue, 30 Jun 2026 12:10:48 -0500 Subject: [PATCH 1/7] feat(cost): support time-based off-peak pricing in cost calculation Some providers charge different per-token rates depending on the time of day. DeepSeek, for example, has historically discounted its chat and reasoner models during an off-peak window (16:30-00:30 UTC). LiteLLM's cost map only modeled static per-token pricing, so cost tracking could not stay accurate for these providers. This adds optional off-peak pricing to a model entry: input_cost_per_token_off_peak, output_cost_per_token_off_peak, cache_read_input_token_cost_off_peak, and an off_peak_hours_utc window expressed as "HH:MM-HH:MM" in UTC (the window may wrap past midnight). When the current UTC time falls inside the window, the cost calculator uses the off-peak rates and otherwise falls back to the standard rates, so existing models are unaffected. The fields are also accepted as custom pricing on a deployment, so they can be set from the proxy config or the SDK. The window check is a pure function that takes the current time as an argument, which keeps the regression tests deterministic without patching the clock. --- .../litellm_core_utils/llm_cost_calc/utils.py | 72 +++++++++ litellm/types/utils.py | 14 ++ litellm/utils.py | 1 + .../llm_cost_calc/test_llm_cost_calc_utils.py | 152 ++++++++++++++++++ 4 files changed, 239 insertions(+) diff --git a/litellm/litellm_core_utils/llm_cost_calc/utils.py b/litellm/litellm_core_utils/llm_cost_calc/utils.py index 0a52e1d283e..af609dbc089 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/utils.py +++ b/litellm/litellm_core_utils/llm_cost_calc/utils.py @@ -3,6 +3,7 @@ from collections.abc import Mapping from dataclasses import dataclass +from datetime import datetime, timezone from types import MappingProxyType from typing import Any, Final, Literal, TypedDict, cast @@ -266,10 +267,75 @@ def _get_tiered_base_costs(model_info: ModelInfo, usage: Usage) -> tuple[float, ) +def _is_within_off_peak_window(off_peak_hours_utc: str | list[str], current_time: datetime | None = None) -> bool: + """Return True if current_time (UTC, defaulting to now) falls inside any off-peak window. + + off_peak_hours_utc is a "HH:MM-HH:MM" string in UTC, or a list of such strings for providers + with multiple daily windows (e.g. ["16:30-00:30", "04:00-06:00"]). A window may wrap past + midnight. The start is inclusive and the end is exclusive; malformed windows are ignored. + """ + if current_time is None: + current_time = datetime.now(timezone.utc) + now = current_time.time() + windows = [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").time() + end = datetime.strptime(end_str.strip(), "%H:%M").time() + except (ValueError, AttributeError): + continue + if start <= end: + if start <= now < end: + return True + elif now >= start or now < end: + return True + return False + + +def _coerce_off_peak_rate(value: object, default: float) -> float: + if isinstance(value, bool): + return default + if isinstance(value, (int, float)): + return float(value) + if isinstance(value, str): + try: + return float(value) + except ValueError: + return default + return default + + +def _apply_off_peak_pricing( + model_info: ModelInfo, + current_time: datetime | None, + prompt_base_cost: float, + completion_base_cost: float, + cache_read_cost: float, +) -> tuple[float, float, float]: + """Swap in off-peak per-token rates when the current UTC time is inside one of the model's + off_peak_pricing windows. Applied after threshold pricing so the discount is honored rather + than overwritten when a model combines off-peak and above-threshold rates. Any rate left + unset in off_peak_pricing falls back to the standard rate. + """ + off_peak = model_info.get("off_peak_pricing") + if not off_peak: + return prompt_base_cost, completion_base_cost, cache_read_cost + hours_utc = off_peak.get("hours_utc") + if not hours_utc or not _is_within_off_peak_window(hours_utc, 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), + _coerce_off_peak_rate(off_peak.get("output_cost_per_token"), completion_base_cost), + _coerce_off_peak_rate(off_peak.get("cache_read_input_token_cost"), cache_read_cost), + ) + + def _get_token_base_cost( model_info: ModelInfo, usage: Usage, service_tier: str | None = None, + current_time: datetime | None = None, *, threshold_is_inclusive: bool = False, ) -> tuple[float, float, float, float, float]: @@ -321,6 +387,9 @@ def _get_token_base_cost( k for k in model_info if k.startswith("input_cost_per_token_above_") and not k.endswith(_SERVICE_TIER_SUFFIXES) ] if not threshold_keys: + prompt_base_cost, completion_base_cost, cache_read_cost = _apply_off_peak_pricing( + model_info, current_time, prompt_base_cost, completion_base_cost, cache_read_cost + ) return ( prompt_base_cost, completion_base_cost, @@ -427,6 +496,9 @@ def _get_token_base_cost( except Exception: continue + prompt_base_cost, completion_base_cost, cache_read_cost = _apply_off_peak_pricing( + model_info, current_time, prompt_base_cost, completion_base_cost, cache_read_cost + ) return ( prompt_base_cost, completion_base_cost, diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 67eae2b4f21..d1eba8feb80 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -190,6 +190,19 @@ class AgenticLoopParams(TypedDict, total=False): """The LLM provider name (e.g., 'bedrock', 'anthropic')""" +class OffPeakPricing(TypedDict, total=False): + """Time-windowed off-peak rates for providers that discount by time of day (e.g. DeepSeek). + + hours_utc is a "HH:MM-HH:MM" string in UTC, or a list of them for multiple daily windows; + a window may wrap past midnight. Any rate left unset falls back to the standard rate. + """ + + hours_utc: str | list[str] + input_cost_per_token: float + output_cost_per_token: float + cache_read_input_token_cost: float + + class ModelInfoBase(ProviderSpecificModelInfo, total=False): key: Required[str] # the key in litellm.model_cost which is returned @@ -222,6 +235,7 @@ class ModelInfoBase(ProviderSpecificModelInfo, total=False): # Smallest prefix this model will actually cache, whatever caching mechanism its provider uses. # Absent means the provider-agnostic default applies; see MINIMUM_PROMPT_CACHE_TOKEN_COUNT. prompt_cache_min_tokens: int | None + off_peak_pricing: OffPeakPricing | None # time-windowed off-peak rates input_cost_per_character: float | None # only for vertex ai models input_cost_per_audio_token: float | None input_cost_per_token_above_128k_tokens: float | None # only for vertex ai models diff --git a/litellm/utils.py b/litellm/utils.py index e5ce7157e77..32d83715bf3 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -5658,6 +5658,7 @@ def _get_model_info_helper( cache_creation_input_token_cost_above_1hr=_model_info.get( "cache_creation_input_token_cost_above_1hr", None ), + off_peak_pricing=_model_info.get("off_peak_pricing", None), input_cost_per_character=_model_info.get("input_cost_per_character", None), input_cost_per_token_above_128k_tokens=_model_info.get("input_cost_per_token_above_128k_tokens", None), input_cost_per_token_above_200k_tokens=_model_info.get("input_cost_per_token_above_200k_tokens", None), 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 c8c36032793..ffe090cf2fb 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 @@ -31,6 +31,7 @@ from litellm.litellm_core_utils.llm_cost_calc.utils import ( TokenTypeCostBreakdown, _calculate_input_cost, _get_token_base_cost, + _is_within_off_peak_window, calculate_cache_writing_cost, generic_cost_per_token, get_token_type_cost_breakdown, @@ -3517,3 +3518,154 @@ def test_generic_cost_per_token_grok_46_long_context(_local_model_cost_map): ) assert prompt_cost == pytest.approx(200_000 * 4e-06 + 50_000 * 1e-06) assert completion_cost == pytest.approx(1_000 * 1.2e-05) + + +def test_is_within_off_peak_window_same_day(): + from datetime import datetime, timezone + + window = "09:00-17:00" + assert _is_within_off_peak_window(window, datetime(2026, 1, 1, 12, 0, tzinfo=timezone.utc)) is True + assert _is_within_off_peak_window(window, datetime(2026, 1, 1, 8, 59, tzinfo=timezone.utc)) is False + assert _is_within_off_peak_window(window, datetime(2026, 1, 1, 9, 0, tzinfo=timezone.utc)) is True + assert _is_within_off_peak_window(window, datetime(2026, 1, 1, 17, 0, tzinfo=timezone.utc)) is False + + +def test_is_within_off_peak_window_wraps_midnight(): + from datetime import datetime, timezone + + window = "16:30-00:30" + assert _is_within_off_peak_window(window, datetime(2026, 1, 1, 18, 0, tzinfo=timezone.utc)) is True + assert _is_within_off_peak_window(window, datetime(2026, 1, 1, 0, 15, tzinfo=timezone.utc)) is True + assert _is_within_off_peak_window(window, datetime(2026, 1, 1, 16, 30, tzinfo=timezone.utc)) is True + assert _is_within_off_peak_window(window, datetime(2026, 1, 1, 0, 30, tzinfo=timezone.utc)) is False + assert _is_within_off_peak_window(window, datetime(2026, 1, 1, 12, 0, tzinfo=timezone.utc)) is False + + +def test_is_within_off_peak_window_multiple_windows(): + from datetime import datetime, timezone + + # Providers like DeepSeek V4 have more than one daily peak/off-peak window. + windows = ["01:00-05:00", "13:00-16:00"] + assert _is_within_off_peak_window(windows, datetime(2026, 1, 1, 3, 0, tzinfo=timezone.utc)) is True + assert _is_within_off_peak_window(windows, datetime(2026, 1, 1, 14, 30, tzinfo=timezone.utc)) is True + assert _is_within_off_peak_window(windows, datetime(2026, 1, 1, 9, 0, tzinfo=timezone.utc)) is False + # a malformed entry in the list is ignored, valid entries still match + assert _is_within_off_peak_window(["bad", "13:00-16:00"], datetime(2026, 1, 1, 14, 0, tzinfo=timezone.utc)) is True + assert _is_within_off_peak_window([], datetime(2026, 1, 1, 14, 0, tzinfo=timezone.utc)) is False + + +def test_is_within_off_peak_window_malformed_returns_false(): + from datetime import datetime, timezone + + now = datetime(2026, 1, 1, 18, 0, tzinfo=timezone.utc) + assert _is_within_off_peak_window("not-a-window", now) is False + assert _is_within_off_peak_window("16:30", now) is False + assert _is_within_off_peak_window("25:00-26:00", now) is False + + +def test_get_token_base_cost_applies_off_peak_pricing(): + from datetime import datetime, timezone + from typing import cast + + from litellm.types.utils import ModelInfo + + model_info = cast( + ModelInfo, + { + "input_cost_per_token": 1e-6, + "output_cost_per_token": 2e-6, + "cache_read_input_token_cost": 1e-7, + "off_peak_pricing": { + "hours_utc": "16:30-00:30", + "input_cost_per_token": 5e-7, + "output_cost_per_token": 1e-6, + "cache_read_input_token_cost": 5e-8, + }, + }, + ) + usage = Usage(prompt_tokens=100, completion_tokens=50, total_tokens=150) + + off_peak = _get_token_base_cost(model_info, usage, current_time=datetime(2026, 1, 1, 18, 0, tzinfo=timezone.utc)) + assert off_peak[0] == 5e-7 + assert off_peak[1] == 1e-6 + assert off_peak[4] == 5e-8 + + peak = _get_token_base_cost(model_info, usage, current_time=datetime(2026, 1, 1, 12, 0, tzinfo=timezone.utc)) + assert peak[0] == 1e-6 + assert peak[1] == 2e-6 + assert peak[4] == 1e-7 + + +def test_get_token_base_cost_off_peak_falls_back_to_standard_when_unset(): + from datetime import datetime, timezone + from typing import cast + + from litellm.types.utils import ModelInfo + + model_info = cast( + ModelInfo, + { + "input_cost_per_token": 1e-6, + "output_cost_per_token": 2e-6, + "off_peak_pricing": {"hours_utc": "16:30-00:30", "input_cost_per_token": 5e-7}, + }, + ) + usage = Usage(prompt_tokens=100, completion_tokens=50, total_tokens=150) + + result = _get_token_base_cost(model_info, usage, current_time=datetime(2026, 1, 1, 18, 0, tzinfo=timezone.utc)) + assert result[0] == 5e-7 + assert result[1] == 2e-6 + + +def test_get_token_base_cost_off_peak_wins_over_threshold(): + from datetime import datetime, timezone + from typing import cast + + from litellm.types.utils import ModelInfo + + model_info = cast( + ModelInfo, + { + "input_cost_per_token": 1e-6, + "output_cost_per_token": 2e-6, + "input_cost_per_token_above_200k_tokens": 3e-6, + "output_cost_per_token_above_200k_tokens": 4e-6, + "off_peak_pricing": { + "hours_utc": "16:30-00:30", + "input_cost_per_token": 5e-7, + "output_cost_per_token": 1e-6, + }, + }, + ) + usage = Usage(prompt_tokens=250000, completion_tokens=250000, total_tokens=500000) + + off_peak = _get_token_base_cost(model_info, usage, current_time=datetime(2026, 1, 1, 18, 0, tzinfo=timezone.utc)) + assert off_peak[0] == 5e-7 + assert off_peak[1] == 1e-6 + + peak = _get_token_base_cost(model_info, usage, current_time=datetime(2026, 1, 1, 12, 0, tzinfo=timezone.utc)) + assert peak[0] == 3e-6 + assert peak[1] == 4e-6 + + +def test_get_model_info_propagates_off_peak_fields(): + model_name = "test-off-peak-model" + off_peak_pricing = { + "hours_utc": "16:30-00:30", + "input_cost_per_token": 5e-7, + "output_cost_per_token": 1e-6, + "cache_read_input_token_cost": 5e-8, + } + litellm.register_model( + { + model_name: { + "litellm_provider": "openai", + "mode": "chat", + "input_cost_per_token": 1e-6, + "output_cost_per_token": 2e-6, + "off_peak_pricing": off_peak_pricing, + } + } + ) + info = litellm.get_model_info(model=model_name) + assert info["off_peak_pricing"] == off_peak_pricing From 03e0f119b362c1ea67b326616ed03c93e34974f9 Mon Sep 17 00:00:00 2001 From: Srivatsa03 Date: Sun, 2 Aug 2026 21:34:09 -0500 Subject: [PATCH 2/7] fix(cost): evaluate off-peak windows in UTC for timezone-aware inputs _is_within_off_peak_window used current_time.time(), which drops tzinfo, so a caller passing a non-UTC aware datetime had the window compared against local wall-clock instead of UTC. That silently mispriced off-peak requests. Normalize aware datetimes to UTC before comparing; naive datetimes stay as-is per the documented UTC contract. Added a regression test with a UTC+8 datetime that fails without the fix --- litellm/litellm_core_utils/llm_cost_calc/utils.py | 2 ++ .../llm_cost_calc/test_llm_cost_calc_utils.py | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/litellm/litellm_core_utils/llm_cost_calc/utils.py b/litellm/litellm_core_utils/llm_cost_calc/utils.py index af609dbc089..49826618fec 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/utils.py +++ b/litellm/litellm_core_utils/llm_cost_calc/utils.py @@ -276,6 +276,8 @@ def _is_within_off_peak_window(off_peak_hours_utc: str | list[str], current_time """ if current_time is None: current_time = datetime.now(timezone.utc) + elif current_time.tzinfo is not None: + current_time = current_time.astimezone(timezone.utc) now = current_time.time() windows = [off_peak_hours_utc] if isinstance(off_peak_hours_utc, str) else off_peak_hours_utc for window in windows: 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 ffe090cf2fb..4e95759b311 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 @@ -3554,6 +3554,19 @@ def test_is_within_off_peak_window_multiple_windows(): assert _is_within_off_peak_window([], datetime(2026, 1, 1, 14, 0, tzinfo=timezone.utc)) is False +def test_is_within_off_peak_window_normalizes_timezone_aware_input(): + from datetime import datetime, timedelta, timezone + + # A caller may pass a non-UTC aware datetime; the window is UTC and must be + # evaluated in UTC, not against the caller's wall-clock. 09:00 at UTC+8 is + # 01:00 UTC, inside the 01:00-05:00 window. + tz_plus_8 = timezone(timedelta(hours=8)) + assert _is_within_off_peak_window("01:00-05:00", datetime(2026, 1, 1, 9, 0, tzinfo=tz_plus_8)) is True + assert _is_within_off_peak_window("01:00-05:00", datetime(2026, 1, 1, 12, 0, tzinfo=tz_plus_8)) is True + # 06:00 at UTC+8 is 22:00 UTC the previous day, outside the window + assert _is_within_off_peak_window("01:00-05:00", datetime(2026, 1, 1, 6, 0, tzinfo=tz_plus_8)) is False + + def test_is_within_off_peak_window_malformed_returns_false(): from datetime import datetime, timezone From 191f207ddaafb9573f15a2e0ec8c9fcb5663bed3 Mon Sep 17 00:00:00 2001 From: Srivatsa03 Date: Mon, 10 Aug 2026 21:47:08 -0500 Subject: [PATCH 3/7] refactor(cost): conform off-peak pricing to current lint budgets Rebasing onto litellm_internal_staging picked up stricter ceilings than this branch was written against. Bind the off-peak results to fresh names instead of reassigning the base costs, mark the new locals Final, avoid rebinding the current_time parameter, and make the window parse explicit about UTC so DTZ007, LIT010 and LIT011 all stay within budget --- .../litellm_core_utils/llm_cost_calc/utils.py | 37 +++++++++---------- litellm/types/utils.py | 10 ++--- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/litellm/litellm_core_utils/llm_cost_calc/utils.py b/litellm/litellm_core_utils/llm_cost_calc/utils.py index 49826618fec..673d5b5b8bd 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/utils.py +++ b/litellm/litellm_core_utils/llm_cost_calc/utils.py @@ -1,7 +1,7 @@ # What is this? ## Helper utilities for cost_per_token() -from collections.abc import Mapping +from collections.abc import Mapping, Sequence from dataclasses import dataclass from datetime import datetime, timezone from types import MappingProxyType @@ -267,24 +267,21 @@ def _get_tiered_base_costs(model_info: ModelInfo, usage: Usage) -> tuple[float, ) -def _is_within_off_peak_window(off_peak_hours_utc: str | list[str], current_time: datetime | None = None) -> bool: +def _is_within_off_peak_window(off_peak_hours_utc: str | Sequence[str], current_time: datetime | None = None) -> bool: """Return True if current_time (UTC, defaulting to now) falls inside any off-peak window. off_peak_hours_utc is a "HH:MM-HH:MM" string in UTC, or a list of such strings for providers with multiple daily windows (e.g. ["16:30-00:30", "04:00-06:00"]). A window may wrap past midnight. The start is inclusive and the end is exclusive; malformed windows are ignored. """ - if current_time is None: - current_time = datetime.now(timezone.utc) - elif current_time.tzinfo is not None: - current_time = current_time.astimezone(timezone.utc) - now = current_time.time() - windows = [off_peak_hours_utc] if isinstance(off_peak_hours_utc, str) else off_peak_hours_utc + reference: Final = current_time if current_time is not None else datetime.now(timezone.utc) + 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").time() - end = datetime.strptime(end_str.strip(), "%H:%M").time() + 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 if start <= end: @@ -320,10 +317,10 @@ def _apply_off_peak_pricing( than overwritten when a model combines off-peak and above-threshold rates. Any rate left unset in off_peak_pricing falls back to the standard rate. """ - off_peak = model_info.get("off_peak_pricing") + off_peak: Final = model_info.get("off_peak_pricing") if not off_peak: return prompt_base_cost, completion_base_cost, cache_read_cost - hours_utc = off_peak.get("hours_utc") + hours_utc: Final = off_peak.get("hours_utc") if not hours_utc or not _is_within_off_peak_window(hours_utc, current_time): return prompt_base_cost, completion_base_cost, cache_read_cost return ( @@ -389,15 +386,15 @@ def _get_token_base_cost( k for k in model_info if k.startswith("input_cost_per_token_above_") and not k.endswith(_SERVICE_TIER_SUFFIXES) ] if not threshold_keys: - prompt_base_cost, completion_base_cost, cache_read_cost = _apply_off_peak_pricing( + off_peak_prompt_cost, off_peak_completion_cost, off_peak_cache_read_cost = _apply_off_peak_pricing( model_info, current_time, prompt_base_cost, completion_base_cost, cache_read_cost ) return ( - prompt_base_cost, - completion_base_cost, + off_peak_prompt_cost, + off_peak_completion_cost, cache_creation_cost, cache_creation_cost_above_1hr, - cache_read_cost, + off_peak_cache_read_cost, ) # Only sort the threshold keys (typically 1-2 keys instead of 66+) @@ -498,15 +495,15 @@ def _get_token_base_cost( except Exception: continue - prompt_base_cost, completion_base_cost, cache_read_cost = _apply_off_peak_pricing( + discounted_prompt_cost, discounted_completion_cost, discounted_cache_read_cost = _apply_off_peak_pricing( model_info, current_time, prompt_base_cost, completion_base_cost, cache_read_cost ) return ( - prompt_base_cost, - completion_base_cost, + discounted_prompt_cost, + discounted_completion_cost, cache_creation_cost, cache_creation_cost_above_1hr, - cache_read_cost, + discounted_cache_read_cost, ) diff --git a/litellm/types/utils.py b/litellm/types/utils.py index d1eba8feb80..c7280f9e049 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -197,10 +197,10 @@ class OffPeakPricing(TypedDict, total=False): a window may wrap past midnight. Any rate left unset falls back to the standard rate. """ - hours_utc: str | list[str] - input_cost_per_token: float - output_cost_per_token: float - cache_read_input_token_cost: float + hours_utc: ReadOnly[str | Sequence[str]] + input_cost_per_token: ReadOnly[float] + output_cost_per_token: ReadOnly[float] + cache_read_input_token_cost: ReadOnly[float] class ModelInfoBase(ProviderSpecificModelInfo, total=False): @@ -235,7 +235,7 @@ class ModelInfoBase(ProviderSpecificModelInfo, total=False): # Smallest prefix this model will actually cache, whatever caching mechanism its provider uses. # Absent means the provider-agnostic default applies; see MINIMUM_PROMPT_CACHE_TOKEN_COUNT. prompt_cache_min_tokens: int | None - off_peak_pricing: OffPeakPricing | None # time-windowed off-peak rates + off_peak_pricing: ReadOnly[OffPeakPricing | None] # time-windowed off-peak rates input_cost_per_character: float | None # only for vertex ai models input_cost_per_audio_token: float | None input_cost_per_token_above_128k_tokens: float | None # only for vertex ai models From f7eb99db30f578850a2c8d4eefb510b74708eac7 Mon Sep 17 00:00:00 2001 From: Srivatsa03 Date: Thu, 13 Aug 2026 17:28:00 -0500 Subject: [PATCH 4/7] test(cost): move off-peak tests beside the related cost tests They sat at the end of the file, which is where everyone else appends too, so this branch picked up a conflict there on nearly every rebase. Grouping them with the other _get_token_base_cost test keeps them clear of that churn and next to the code they cover. Pure move, no test changes --- .../llm_cost_calc/test_llm_cost_calc_utils.py | 328 +++++++++--------- 1 file changed, 164 insertions(+), 164 deletions(-) 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 4e95759b311..4071adfcd1a 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 @@ -409,6 +409,170 @@ def test_get_token_base_cost_picks_highest_crossed_tier(): assert prompt_base_cost == 9e-6 +def test_is_within_off_peak_window_same_day(): + from datetime import datetime, timezone + + window = "09:00-17:00" + assert _is_within_off_peak_window(window, datetime(2026, 1, 1, 12, 0, tzinfo=timezone.utc)) is True + assert _is_within_off_peak_window(window, datetime(2026, 1, 1, 8, 59, tzinfo=timezone.utc)) is False + assert _is_within_off_peak_window(window, datetime(2026, 1, 1, 9, 0, tzinfo=timezone.utc)) is True + assert _is_within_off_peak_window(window, datetime(2026, 1, 1, 17, 0, tzinfo=timezone.utc)) is False + + +def test_is_within_off_peak_window_wraps_midnight(): + from datetime import datetime, timezone + + window = "16:30-00:30" + assert _is_within_off_peak_window(window, datetime(2026, 1, 1, 18, 0, tzinfo=timezone.utc)) is True + assert _is_within_off_peak_window(window, datetime(2026, 1, 1, 0, 15, tzinfo=timezone.utc)) is True + assert _is_within_off_peak_window(window, datetime(2026, 1, 1, 16, 30, tzinfo=timezone.utc)) is True + assert _is_within_off_peak_window(window, datetime(2026, 1, 1, 0, 30, tzinfo=timezone.utc)) is False + assert _is_within_off_peak_window(window, datetime(2026, 1, 1, 12, 0, tzinfo=timezone.utc)) is False + + +def test_is_within_off_peak_window_multiple_windows(): + from datetime import datetime, timezone + + # Providers like DeepSeek V4 have more than one daily peak/off-peak window. + windows = ["01:00-05:00", "13:00-16:00"] + assert _is_within_off_peak_window(windows, datetime(2026, 1, 1, 3, 0, tzinfo=timezone.utc)) is True + assert _is_within_off_peak_window(windows, datetime(2026, 1, 1, 14, 30, tzinfo=timezone.utc)) is True + assert _is_within_off_peak_window(windows, datetime(2026, 1, 1, 9, 0, tzinfo=timezone.utc)) is False + # a malformed entry in the list is ignored, valid entries still match + assert _is_within_off_peak_window(["bad", "13:00-16:00"], datetime(2026, 1, 1, 14, 0, tzinfo=timezone.utc)) is True + assert _is_within_off_peak_window([], datetime(2026, 1, 1, 14, 0, tzinfo=timezone.utc)) is False + + +def test_is_within_off_peak_window_normalizes_timezone_aware_input(): + from datetime import datetime, timedelta, timezone + + # A caller may pass a non-UTC aware datetime; the window is UTC and must be + # evaluated in UTC, not against the caller's wall-clock. 09:00 at UTC+8 is + # 01:00 UTC, inside the 01:00-05:00 window. + tz_plus_8 = timezone(timedelta(hours=8)) + assert _is_within_off_peak_window("01:00-05:00", datetime(2026, 1, 1, 9, 0, tzinfo=tz_plus_8)) is True + assert _is_within_off_peak_window("01:00-05:00", datetime(2026, 1, 1, 12, 0, tzinfo=tz_plus_8)) is True + # 06:00 at UTC+8 is 22:00 UTC the previous day, outside the window + assert _is_within_off_peak_window("01:00-05:00", datetime(2026, 1, 1, 6, 0, tzinfo=tz_plus_8)) is False + + +def test_is_within_off_peak_window_malformed_returns_false(): + from datetime import datetime, timezone + + now = datetime(2026, 1, 1, 18, 0, tzinfo=timezone.utc) + assert _is_within_off_peak_window("not-a-window", now) is False + assert _is_within_off_peak_window("16:30", now) is False + assert _is_within_off_peak_window("25:00-26:00", now) is False + + +def test_get_token_base_cost_applies_off_peak_pricing(): + from datetime import datetime, timezone + from typing import cast + + from litellm.types.utils import ModelInfo + + model_info = cast( + ModelInfo, + { + "input_cost_per_token": 1e-6, + "output_cost_per_token": 2e-6, + "cache_read_input_token_cost": 1e-7, + "off_peak_pricing": { + "hours_utc": "16:30-00:30", + "input_cost_per_token": 5e-7, + "output_cost_per_token": 1e-6, + "cache_read_input_token_cost": 5e-8, + }, + }, + ) + usage = Usage(prompt_tokens=100, completion_tokens=50, total_tokens=150) + + off_peak = _get_token_base_cost(model_info, usage, current_time=datetime(2026, 1, 1, 18, 0, tzinfo=timezone.utc)) + assert off_peak[0] == 5e-7 + assert off_peak[1] == 1e-6 + assert off_peak[4] == 5e-8 + + peak = _get_token_base_cost(model_info, usage, current_time=datetime(2026, 1, 1, 12, 0, tzinfo=timezone.utc)) + assert peak[0] == 1e-6 + assert peak[1] == 2e-6 + assert peak[4] == 1e-7 + + +def test_get_token_base_cost_off_peak_falls_back_to_standard_when_unset(): + from datetime import datetime, timezone + from typing import cast + + from litellm.types.utils import ModelInfo + + model_info = cast( + ModelInfo, + { + "input_cost_per_token": 1e-6, + "output_cost_per_token": 2e-6, + "off_peak_pricing": {"hours_utc": "16:30-00:30", "input_cost_per_token": 5e-7}, + }, + ) + usage = Usage(prompt_tokens=100, completion_tokens=50, total_tokens=150) + + result = _get_token_base_cost(model_info, usage, current_time=datetime(2026, 1, 1, 18, 0, tzinfo=timezone.utc)) + assert result[0] == 5e-7 + assert result[1] == 2e-6 + + +def test_get_token_base_cost_off_peak_wins_over_threshold(): + from datetime import datetime, timezone + from typing import cast + + from litellm.types.utils import ModelInfo + + model_info = cast( + ModelInfo, + { + "input_cost_per_token": 1e-6, + "output_cost_per_token": 2e-6, + "input_cost_per_token_above_200k_tokens": 3e-6, + "output_cost_per_token_above_200k_tokens": 4e-6, + "off_peak_pricing": { + "hours_utc": "16:30-00:30", + "input_cost_per_token": 5e-7, + "output_cost_per_token": 1e-6, + }, + }, + ) + usage = Usage(prompt_tokens=250000, completion_tokens=250000, total_tokens=500000) + + off_peak = _get_token_base_cost(model_info, usage, current_time=datetime(2026, 1, 1, 18, 0, tzinfo=timezone.utc)) + assert off_peak[0] == 5e-7 + assert off_peak[1] == 1e-6 + + peak = _get_token_base_cost(model_info, usage, current_time=datetime(2026, 1, 1, 12, 0, tzinfo=timezone.utc)) + assert peak[0] == 3e-6 + assert peak[1] == 4e-6 + + +def test_get_model_info_propagates_off_peak_fields(): + model_name = "test-off-peak-model" + off_peak_pricing = { + "hours_utc": "16:30-00:30", + "input_cost_per_token": 5e-7, + "output_cost_per_token": 1e-6, + "cache_read_input_token_cost": 5e-8, + } + litellm.register_model( + { + model_name: { + "litellm_provider": "openai", + "mode": "chat", + "input_cost_per_token": 1e-6, + "output_cost_per_token": 2e-6, + "off_peak_pricing": off_peak_pricing, + } + } + ) + info = litellm.get_model_info(model=model_name) + assert info["off_peak_pricing"] == off_peak_pricing + + def test_generic_cost_per_token_gpt54_above_272k_tokens(_local_model_cost_map): """GPT-5.4/5.4-pro: prompts >272K input tokens priced at 2x input, 1.5x output.""" model = "gpt-5.4" @@ -3518,167 +3682,3 @@ def test_generic_cost_per_token_grok_46_long_context(_local_model_cost_map): ) assert prompt_cost == pytest.approx(200_000 * 4e-06 + 50_000 * 1e-06) assert completion_cost == pytest.approx(1_000 * 1.2e-05) - - -def test_is_within_off_peak_window_same_day(): - from datetime import datetime, timezone - - window = "09:00-17:00" - assert _is_within_off_peak_window(window, datetime(2026, 1, 1, 12, 0, tzinfo=timezone.utc)) is True - assert _is_within_off_peak_window(window, datetime(2026, 1, 1, 8, 59, tzinfo=timezone.utc)) is False - assert _is_within_off_peak_window(window, datetime(2026, 1, 1, 9, 0, tzinfo=timezone.utc)) is True - assert _is_within_off_peak_window(window, datetime(2026, 1, 1, 17, 0, tzinfo=timezone.utc)) is False - - -def test_is_within_off_peak_window_wraps_midnight(): - from datetime import datetime, timezone - - window = "16:30-00:30" - assert _is_within_off_peak_window(window, datetime(2026, 1, 1, 18, 0, tzinfo=timezone.utc)) is True - assert _is_within_off_peak_window(window, datetime(2026, 1, 1, 0, 15, tzinfo=timezone.utc)) is True - assert _is_within_off_peak_window(window, datetime(2026, 1, 1, 16, 30, tzinfo=timezone.utc)) is True - assert _is_within_off_peak_window(window, datetime(2026, 1, 1, 0, 30, tzinfo=timezone.utc)) is False - assert _is_within_off_peak_window(window, datetime(2026, 1, 1, 12, 0, tzinfo=timezone.utc)) is False - - -def test_is_within_off_peak_window_multiple_windows(): - from datetime import datetime, timezone - - # Providers like DeepSeek V4 have more than one daily peak/off-peak window. - windows = ["01:00-05:00", "13:00-16:00"] - assert _is_within_off_peak_window(windows, datetime(2026, 1, 1, 3, 0, tzinfo=timezone.utc)) is True - assert _is_within_off_peak_window(windows, datetime(2026, 1, 1, 14, 30, tzinfo=timezone.utc)) is True - assert _is_within_off_peak_window(windows, datetime(2026, 1, 1, 9, 0, tzinfo=timezone.utc)) is False - # a malformed entry in the list is ignored, valid entries still match - assert _is_within_off_peak_window(["bad", "13:00-16:00"], datetime(2026, 1, 1, 14, 0, tzinfo=timezone.utc)) is True - assert _is_within_off_peak_window([], datetime(2026, 1, 1, 14, 0, tzinfo=timezone.utc)) is False - - -def test_is_within_off_peak_window_normalizes_timezone_aware_input(): - from datetime import datetime, timedelta, timezone - - # A caller may pass a non-UTC aware datetime; the window is UTC and must be - # evaluated in UTC, not against the caller's wall-clock. 09:00 at UTC+8 is - # 01:00 UTC, inside the 01:00-05:00 window. - tz_plus_8 = timezone(timedelta(hours=8)) - assert _is_within_off_peak_window("01:00-05:00", datetime(2026, 1, 1, 9, 0, tzinfo=tz_plus_8)) is True - assert _is_within_off_peak_window("01:00-05:00", datetime(2026, 1, 1, 12, 0, tzinfo=tz_plus_8)) is True - # 06:00 at UTC+8 is 22:00 UTC the previous day, outside the window - assert _is_within_off_peak_window("01:00-05:00", datetime(2026, 1, 1, 6, 0, tzinfo=tz_plus_8)) is False - - -def test_is_within_off_peak_window_malformed_returns_false(): - from datetime import datetime, timezone - - now = datetime(2026, 1, 1, 18, 0, tzinfo=timezone.utc) - assert _is_within_off_peak_window("not-a-window", now) is False - assert _is_within_off_peak_window("16:30", now) is False - assert _is_within_off_peak_window("25:00-26:00", now) is False - - -def test_get_token_base_cost_applies_off_peak_pricing(): - from datetime import datetime, timezone - from typing import cast - - from litellm.types.utils import ModelInfo - - model_info = cast( - ModelInfo, - { - "input_cost_per_token": 1e-6, - "output_cost_per_token": 2e-6, - "cache_read_input_token_cost": 1e-7, - "off_peak_pricing": { - "hours_utc": "16:30-00:30", - "input_cost_per_token": 5e-7, - "output_cost_per_token": 1e-6, - "cache_read_input_token_cost": 5e-8, - }, - }, - ) - usage = Usage(prompt_tokens=100, completion_tokens=50, total_tokens=150) - - off_peak = _get_token_base_cost(model_info, usage, current_time=datetime(2026, 1, 1, 18, 0, tzinfo=timezone.utc)) - assert off_peak[0] == 5e-7 - assert off_peak[1] == 1e-6 - assert off_peak[4] == 5e-8 - - peak = _get_token_base_cost(model_info, usage, current_time=datetime(2026, 1, 1, 12, 0, tzinfo=timezone.utc)) - assert peak[0] == 1e-6 - assert peak[1] == 2e-6 - assert peak[4] == 1e-7 - - -def test_get_token_base_cost_off_peak_falls_back_to_standard_when_unset(): - from datetime import datetime, timezone - from typing import cast - - from litellm.types.utils import ModelInfo - - model_info = cast( - ModelInfo, - { - "input_cost_per_token": 1e-6, - "output_cost_per_token": 2e-6, - "off_peak_pricing": {"hours_utc": "16:30-00:30", "input_cost_per_token": 5e-7}, - }, - ) - usage = Usage(prompt_tokens=100, completion_tokens=50, total_tokens=150) - - result = _get_token_base_cost(model_info, usage, current_time=datetime(2026, 1, 1, 18, 0, tzinfo=timezone.utc)) - assert result[0] == 5e-7 - assert result[1] == 2e-6 - - -def test_get_token_base_cost_off_peak_wins_over_threshold(): - from datetime import datetime, timezone - from typing import cast - - from litellm.types.utils import ModelInfo - - model_info = cast( - ModelInfo, - { - "input_cost_per_token": 1e-6, - "output_cost_per_token": 2e-6, - "input_cost_per_token_above_200k_tokens": 3e-6, - "output_cost_per_token_above_200k_tokens": 4e-6, - "off_peak_pricing": { - "hours_utc": "16:30-00:30", - "input_cost_per_token": 5e-7, - "output_cost_per_token": 1e-6, - }, - }, - ) - usage = Usage(prompt_tokens=250000, completion_tokens=250000, total_tokens=500000) - - off_peak = _get_token_base_cost(model_info, usage, current_time=datetime(2026, 1, 1, 18, 0, tzinfo=timezone.utc)) - assert off_peak[0] == 5e-7 - assert off_peak[1] == 1e-6 - - peak = _get_token_base_cost(model_info, usage, current_time=datetime(2026, 1, 1, 12, 0, tzinfo=timezone.utc)) - assert peak[0] == 3e-6 - assert peak[1] == 4e-6 - - -def test_get_model_info_propagates_off_peak_fields(): - model_name = "test-off-peak-model" - off_peak_pricing = { - "hours_utc": "16:30-00:30", - "input_cost_per_token": 5e-7, - "output_cost_per_token": 1e-6, - "cache_read_input_token_cost": 5e-8, - } - litellm.register_model( - { - model_name: { - "litellm_provider": "openai", - "mode": "chat", - "input_cost_per_token": 1e-6, - "output_cost_per_token": 2e-6, - "off_peak_pricing": off_peak_pricing, - } - } - ) - info = litellm.get_model_info(model=model_name) - assert info["off_peak_pricing"] == off_peak_pricing From 9b98ce9d0f764a4ec39784dea59767e9e94db347 Mon Sep 17 00:00:00 2001 From: Srivatsa03 Date: Sat, 15 Aug 2026 10:49:51 -0500 Subject: [PATCH 5/7] fix(cost): apply off-peak rates on the tiered-pricing path Tiered pricing resolves its own base rates and returns early, before the off-peak swap ran, so a model carrying both tiered_pricing and off_peak_pricing billed the tier rate around the clock. Route every base-cost path through one helper so the window applies wherever the rates came from, and say plainly in the docstring that an off-peak rate replaces the rate it lands on rather than discounting it --- .../litellm_core_utils/llm_cost_calc/utils.py | 63 ++++++++++++------- .../llm_cost_calc/test_llm_cost_calc_utils.py | 32 ++++++++++ 2 files changed, 73 insertions(+), 22 deletions(-) diff --git a/litellm/litellm_core_utils/llm_cost_calc/utils.py b/litellm/litellm_core_utils/llm_cost_calc/utils.py index 673d5b5b8bd..b763d609e04 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/utils.py +++ b/litellm/litellm_core_utils/llm_cost_calc/utils.py @@ -313,9 +313,10 @@ def _apply_off_peak_pricing( cache_read_cost: float, ) -> tuple[float, float, float]: """Swap in off-peak per-token rates when the current UTC time is inside one of the model's - off_peak_pricing windows. Applied after threshold pricing so the discount is honored rather - than overwritten when a model combines off-peak and above-threshold rates. Any rate left - unset in off_peak_pricing falls back to the standard rate. + off_peak_pricing windows. An off-peak rate replaces the rate that would otherwise apply + rather than discounting it, so a model that also has tiered or above-threshold pricing bills + the flat off-peak rate for the whole request while the window is open. Any rate left unset in + off_peak_pricing falls back to the standard rate. """ off_peak: Final = model_info.get("off_peak_pricing") if not off_peak: @@ -330,6 +331,22 @@ def _apply_off_peak_pricing( ) +def _apply_off_peak_to_base_costs( + model_info: ModelInfo, + current_time: datetime | None, + base_costs: tuple[float, float, float, float, float], +) -> tuple[float, float, float, float, float]: + """Apply off-peak rates to an already-resolved set of base costs, whichever pricing path + produced them. Cache-creation rates are passed through untouched, since off_peak_pricing + has no field for them. + """ + prompt, completion, cache_creation, cache_creation_above_1hr, cache_read = base_costs + off_peak_prompt, off_peak_completion, off_peak_cache_read = _apply_off_peak_pricing( + model_info, current_time, prompt, completion, cache_read + ) + return (off_peak_prompt, off_peak_completion, cache_creation, cache_creation_above_1hr, off_peak_cache_read) + + def _get_token_base_cost( model_info: ModelInfo, usage: Usage, @@ -352,7 +369,7 @@ def _get_token_base_cost( """ tiered_base_costs: Final = _get_tiered_base_costs(model_info=model_info, usage=usage) if tiered_base_costs is not None: - return tiered_base_costs + return _apply_off_peak_to_base_costs(model_info, current_time, tiered_base_costs) # Get service tier aware cost keys input_cost_key: Final = _get_service_tier_cost_key("input_cost_per_token", service_tier) @@ -386,15 +403,16 @@ def _get_token_base_cost( k for k in model_info if k.startswith("input_cost_per_token_above_") and not k.endswith(_SERVICE_TIER_SUFFIXES) ] if not threshold_keys: - off_peak_prompt_cost, off_peak_completion_cost, off_peak_cache_read_cost = _apply_off_peak_pricing( - model_info, current_time, prompt_base_cost, completion_base_cost, cache_read_cost - ) - return ( - off_peak_prompt_cost, - off_peak_completion_cost, - cache_creation_cost, - cache_creation_cost_above_1hr, - off_peak_cache_read_cost, + return _apply_off_peak_to_base_costs( + model_info, + current_time, + ( + prompt_base_cost, + completion_base_cost, + cache_creation_cost, + cache_creation_cost_above_1hr, + cache_read_cost, + ), ) # Only sort the threshold keys (typically 1-2 keys instead of 66+) @@ -495,15 +513,16 @@ def _get_token_base_cost( except Exception: continue - discounted_prompt_cost, discounted_completion_cost, discounted_cache_read_cost = _apply_off_peak_pricing( - model_info, current_time, prompt_base_cost, completion_base_cost, cache_read_cost - ) - return ( - discounted_prompt_cost, - discounted_completion_cost, - cache_creation_cost, - cache_creation_cost_above_1hr, - discounted_cache_read_cost, + return _apply_off_peak_to_base_costs( + model_info, + current_time, + ( + prompt_base_cost, + completion_base_cost, + cache_creation_cost, + cache_creation_cost_above_1hr, + cache_read_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 4071adfcd1a..d8c22e34e1a 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 @@ -573,6 +573,38 @@ def test_get_model_info_propagates_off_peak_fields(): assert info["off_peak_pricing"] == off_peak_pricing +def test_get_token_base_cost_off_peak_wins_over_tiered_pricing(): + """Tiered pricing resolves base rates on its own path and returns early, so off-peak has to + be applied there too or a model carrying both would silently bill the tier rate all day.""" + from datetime import datetime, timezone + + model_name = "litellm-test-off-peak-tiered" + litellm.register_model( + { + model_name: { + "litellm_provider": "openai", + "mode": "chat", + "tiered_pricing": [ + {"range": [0, 128000], "input_cost_per_token": 3e-6, "output_cost_per_token": 6e-6}, + ], + "off_peak_pricing": { + "hours_utc": "16:30-00:30", + "input_cost_per_token": 5e-7, + "output_cost_per_token": 1e-6, + }, + } + } + ) + info = litellm.get_model_info(model=model_name) + usage = Usage(prompt_tokens=1_000, completion_tokens=100, total_tokens=1_100) + + inside = _get_token_base_cost(info, usage, current_time=datetime(2026, 1, 1, 18, 0, tzinfo=timezone.utc)) + assert inside[:2] == (5e-7, 1e-6) + + outside = _get_token_base_cost(info, usage, current_time=datetime(2026, 1, 1, 12, 0, tzinfo=timezone.utc)) + assert outside[:2] == (3e-6, 6e-6) + + def test_generic_cost_per_token_gpt54_above_272k_tokens(_local_model_cost_map): """GPT-5.4/5.4-pro: prompts >272K input tokens priced at 2x input, 1.5x output.""" model = "gpt-5.4" From 09276db6d423767713e258ac6a24ae78baece6eb Mon Sep 17 00:00:00 2001 From: Srivatsa03 Date: Sat, 22 Aug 2026 23:11:57 -0500 Subject: [PATCH 6/7] fix(cost): treat an equal-ended off-peak window as the whole day A window whose start equals its end is the natural way to spell off-peak all day, and the docstring's promise that a window may wrap past midnight invites it. It took the non-wrap branch instead, where start <= now < end can never hold, so it matched nothing. It parses cleanly, so it never reached the branch that ignores malformed windows: no exception, no log, and the model billed at standard rates around the clock while the config said otherwise. Let equality fall through to the wrap branch, which covers every instant, and say so in the docstring. Reported by @xyzs996 in review. --- litellm/litellm_core_utils/llm_cost_calc/utils.py | 5 +++-- .../llm_cost_calc/test_llm_cost_calc_utils.py | 13 +++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/litellm/litellm_core_utils/llm_cost_calc/utils.py b/litellm/litellm_core_utils/llm_cost_calc/utils.py index b763d609e04..09b71d3d7fb 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/utils.py +++ b/litellm/litellm_core_utils/llm_cost_calc/utils.py @@ -272,7 +272,8 @@ def _is_within_off_peak_window(off_peak_hours_utc: str | Sequence[str], current_ off_peak_hours_utc is a "HH:MM-HH:MM" string in UTC, or a list of such strings for providers with multiple daily windows (e.g. ["16:30-00:30", "04:00-06:00"]). A window may wrap past - midnight. The start is inclusive and the end is exclusive; malformed windows are ignored. + midnight, and a window whose start equals its end covers the whole day. The start is + inclusive and the end is exclusive; malformed windows are ignored. """ reference: Final = current_time if current_time is not None else datetime.now(timezone.utc) now: Final = (reference.astimezone(timezone.utc) if reference.tzinfo is not None else reference).time() @@ -284,7 +285,7 @@ def _is_within_off_peak_window(off_peak_hours_utc: str | Sequence[str], current_ end = datetime.strptime(end_str.strip(), "%H:%M").replace(tzinfo=timezone.utc).time() except (ValueError, AttributeError): continue - if start <= end: + if start < end: if start <= now < end: return True elif now >= start or now < end: 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 d8c22e34e1a..b801042767a 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 @@ -430,6 +430,19 @@ def test_is_within_off_peak_window_wraps_midnight(): assert _is_within_off_peak_window(window, datetime(2026, 1, 1, 12, 0, tzinfo=timezone.utc)) is False +def test_is_within_off_peak_window_equal_start_and_end_covers_whole_day(): + """An equal start and end is the natural way to spell off-peak all day. It used to take the + non-wrap branch, where start <= now < end can never hold, so it matched nothing and billed at + standard rates around the clock without raising or logging anything.""" + from datetime import datetime, timezone + + for window in ("00:00-00:00", "10:00-10:00"): + for hour in range(24): + assert ( + _is_within_off_peak_window(window, datetime(2026, 1, 1, hour, 0, tzinfo=timezone.utc)) is True + ), f"{window} should cover {hour:02d}:00" + + def test_is_within_off_peak_window_multiple_windows(): from datetime import datetime, timezone From d2a649afc4a90b7ae1205dee4060287690921b7a Mon Sep 17 00:00:00 2001 From: Srivatsa03 Date: Sat, 22 Aug 2026 23:40:03 -0500 Subject: [PATCH 7/7] docs(cost): state that a naive off-peak current_time is read as UTC An aware value is converted, a naive one is taken to already be UTC rather than localised. Nothing signals the difference, so a caller passing datetime.now() instead of datetime.now(timezone.utc) shifts every window by the host's offset and bills silently wrong. Say so where a caller will read it. Reported by @xyzs996 in review. --- litellm/litellm_core_utils/llm_cost_calc/utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/litellm/litellm_core_utils/llm_cost_calc/utils.py b/litellm/litellm_core_utils/llm_cost_calc/utils.py index 09b71d3d7fb..4d9496ae6a9 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/utils.py +++ b/litellm/litellm_core_utils/llm_cost_calc/utils.py @@ -274,6 +274,10 @@ def _is_within_off_peak_window(off_peak_hours_utc: str | Sequence[str], current_ with multiple daily windows (e.g. ["16:30-00:30", "04:00-06:00"]). A window may wrap past midnight, and a window whose start equals its end covers the whole day. The start is inclusive and the end is exclusive; malformed windows are ignored. + + An aware current_time is converted to UTC. A naive one is taken to already be UTC rather + than being localised, so callers must pass datetime.now(timezone.utc), never datetime.now(), + or every window shifts by the host's offset. """ reference: Final = current_time if current_time is not None else datetime.now(timezone.utc) now: Final = (reference.astimezone(timezone.utc) if reference.tzinfo is not None else reference).time()