diff --git a/ci_cd/generate_model_prices_schema.py b/ci_cd/generate_model_prices_schema.py index 15ed0a15e01..e63b576bf29 100644 --- a/ci_cd/generate_model_prices_schema.py +++ b/ci_cd/generate_model_prices_schema.py @@ -60,6 +60,15 @@ OFF_PEAK_WINDOW: Final[JsonSchema] = { }, "minItems": 1, }, + "override_dates": { + "type": "array", + "items": ISO_DATE, + "minItems": 1, + "description": ( + "YYYY-MM-DD dates, read on weekday_timezone, on which this rule alone decides: " + "weekdays and every other window are ignored. On any other date the rule does not apply." + ), + }, }, "required": ["hours_utc"], "additionalProperties": False, @@ -76,21 +85,6 @@ OBJECT_KEYS: dict[str, JsonSchema] = { "type": "string", "description": "IANA zone the weekdays of each window are read on; defaults to UTC.", }, - "off_peak_dates": { - "type": "array", - "items": ISO_DATE, - "minItems": 1, - "description": "Calendar dates, read on weekday_timezone, that are off-peak all day (public holidays).", - }, - "weekday_dates": { - "type": "array", - "items": ISO_DATE, - "minItems": 1, - "description": ( - "Calendar dates, read on weekday_timezone, that follow the Monday-to-Friday rules " - "even when they fall on a weekend (make-up workdays)." - ), - }, "input_cost_per_token": NONNEG_NUMBER, "output_cost_per_token": NONNEG_NUMBER, "output_cost_per_reasoning_token": NONNEG_NUMBER, diff --git a/litellm-rust/crates/model-catalog/src/model_info.rs b/litellm-rust/crates/model-catalog/src/model_info.rs index 33266643ba8..d0db8b31131 100644 --- a/litellm-rust/crates/model-catalog/src/model_info.rs +++ b/litellm-rust/crates/model-catalog/src/model_info.rs @@ -116,6 +116,9 @@ pub struct OffPeakWindow { pub hours_utc: UtcHours, #[serde(default, skip_serializing_if = "Option::is_none")] pub weekdays: Option>, + /// YYYY-MM-DD dates on which this rule alone decides, ignoring weekdays and other windows. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub override_dates: Option>, } /// Rates that replace the same-named base fields inside the stated UTC windows. @@ -129,12 +132,6 @@ pub struct OffPeakPricing { pub windows: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub weekday_timezone: Option, - /// YYYY-MM-DD dates, read on weekday_timezone, that are off-peak all day. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub off_peak_dates: Option>, - /// YYYY-MM-DD dates, read on weekday_timezone, that follow the Monday-to-Friday rules. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub weekday_dates: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub input_cost_per_token: Option, #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/litellm/litellm_core_utils/llm_cost_calc/utils.py b/litellm/litellm_core_utils/llm_cost_calc/utils.py index 8a5deb293b0..faa4c31adb1 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/utils.py +++ b/litellm/litellm_core_utils/llm_cost_calc/utils.py @@ -466,32 +466,22 @@ def _weekday_calendar(weekday_timezone: object) -> tzinfo: return timezone.utc -_MONDAY_TO_FRIDAY: Final = frozenset({1, 2, 3, 4, 5}) - - def _calendar_date(reference_utc: datetime, weekday_timezone: object) -> str: return reference_utc.astimezone(_weekday_calendar(weekday_timezone)).date().isoformat() -def _matches_weekdays( - reference_utc: datetime, weekdays: object, weekday_timezone: object, weekday_dates: frozenset[str] -) -> bool: +def _matches_weekdays(reference_utc: datetime, weekdays: object, weekday_timezone: object) -> bool: """Return True when reference_utc falls on one of the rule's weekdays, read on the calendar named by weekday_timezone (default UTC). An absent weekdays means every day. The calendar matters even when UTC and vendor-local weekdays agree at every currently priced hour: a - window past 16:00 UTC is where an Asia/Shanghai weekday diverges from the UTC one. A - calendar date listed in weekday_dates is a make-up workday, so it follows the - Monday-to-Friday rules whatever its real isoweekday is. + window past 16:00 UTC is where an Asia/Shanghai weekday diverges from the UTC one. """ if weekdays is None: return True if isinstance(weekdays, str) or not isinstance(weekdays, Sequence): return False allowed: Final = frozenset(day for day in map(_normalize_weekday, weekdays) if day is not None) - local: Final = reference_utc.astimezone(_weekday_calendar(weekday_timezone)) - if local.date().isoformat() in weekday_dates: - return not allowed.isdisjoint(_MONDAY_TO_FRIDAY) - return local.isoweekday() in allowed + return reference_utc.astimezone(_weekday_calendar(weekday_timezone)).isoweekday() in allowed def _as_window_strings(value: object) -> tuple[str, ...]: @@ -502,40 +492,48 @@ def _as_window_strings(value: object) -> tuple[str, ...]: return () -def _as_date_strings(value: object) -> frozenset[str]: +def _as_date_strings(value: object) -> tuple[str, ...]: if isinstance(value, str) or not isinstance(value, Sequence): - return frozenset() - return frozenset(entry for entry in value if isinstance(entry, str)) + return () + return tuple(entry for entry in value if isinstance(entry, str)) def _is_off_peak(off_peak: Mapping[str, object], current_time: datetime | None = None) -> bool: """Return True when current_time (UTC, defaulting to now) is off-peak under the block's rules: the flat hours_utc windows, which apply every day, or any entry in windows, whose - hours apply only on its weekdays. off_peak_dates lists calendar dates that are off-peak - all day, and weekday_dates lists dates that follow the Monday-to-Friday rules even on a - weekend; both are read on the weekday_timezone calendar. + 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. """ reference: Final = current_time if current_time is not None else current_billing_time() reference_utc: Final = ( reference.astimezone(timezone.utc) if reference.tzinfo is not None else reference.replace(tzinfo=timezone.utc) ) weekday_timezone: Final = off_peak.get("weekday_timezone") - if _calendar_date(reference_utc, weekday_timezone) in _as_date_strings(off_peak.get("off_peak_dates")): - return True + today: Final = _calendar_date(reference_utc, weekday_timezone) + windows: Final = off_peak.get("windows") + rules: Final = ( + () + if isinstance(windows, str) or not isinstance(windows, Sequence) + else tuple(rule for rule in windows if isinstance(rule, Mapping)) + ) + override_rules: Final = tuple(rule for rule in rules if today in _as_date_strings(rule.get("override_dates"))) + if override_rules: + for rule in override_rules: + if _is_within_off_peak_window(_as_window_strings(rule.get("hours_utc")), reference_utc): + return True + return False flat_windows: Final = _as_window_strings(off_peak.get("hours_utc")) if flat_windows and _is_within_off_peak_window(flat_windows, reference_utc): return True - windows: Final = off_peak.get("windows") - if isinstance(windows, str) or not isinstance(windows, Sequence): - return False - weekday_dates: Final = _as_date_strings(off_peak.get("weekday_dates")) - for rule in windows: - if not isinstance(rule, Mapping): + for rule in rules: + if _as_date_strings(rule.get("override_dates")): continue rule_windows = _as_window_strings(rule.get("hours_utc")) if not rule_windows: continue - if not _matches_weekdays(reference_utc, rule.get("weekdays"), weekday_timezone, weekday_dates): + if not _matches_weekdays(reference_utc, rule.get("weekdays"), weekday_timezone): continue if _is_within_off_peak_window(rule_windows, reference_utc): return True diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 0f2486ec0fc..93f3576ac3d 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -58222,51 +58222,7 @@ "off_peak_pricing": { "cache_read_input_token_cost": 3e-09, "input_cost_per_token": 1.5e-07, - "off_peak_dates": [ - "2026-01-01", - "2026-01-02", - "2026-01-03", - "2026-02-15", - "2026-02-16", - "2026-02-17", - "2026-02-18", - "2026-02-19", - "2026-02-20", - "2026-02-21", - "2026-02-22", - "2026-02-23", - "2026-04-04", - "2026-04-05", - "2026-04-06", - "2026-05-01", - "2026-05-02", - "2026-05-03", - "2026-05-04", - "2026-05-05", - "2026-06-19", - "2026-06-20", - "2026-06-21", - "2026-09-25", - "2026-09-26", - "2026-09-27", - "2026-10-01", - "2026-10-02", - "2026-10-03", - "2026-10-04", - "2026-10-05", - "2026-10-06", - "2026-10-07" - ], "output_cost_per_token": 6e-07, - "weekday_dates": [ - "2026-01-04", - "2026-02-14", - "2026-02-28", - "2026-05-09", - "2026-09-20", - "2026-10-10" - ], - "weekday_timezone": "Asia/Shanghai", "windows": [ { "hours_utc": [ @@ -58288,6 +58244,59 @@ 6, 7 ] + }, + { + "hours_utc": "00:00-00:00", + "override_dates": [ + "2026-01-01", + "2026-01-02", + "2026-01-03", + "2026-02-15", + "2026-02-16", + "2026-02-17", + "2026-02-18", + "2026-02-19", + "2026-02-20", + "2026-02-21", + "2026-02-22", + "2026-02-23", + "2026-04-04", + "2026-04-05", + "2026-04-06", + "2026-05-01", + "2026-05-02", + "2026-05-03", + "2026-05-04", + "2026-05-05", + "2026-06-19", + "2026-06-20", + "2026-06-21", + "2026-09-25", + "2026-09-26", + "2026-09-27", + "2026-10-01", + "2026-10-02", + "2026-10-03", + "2026-10-04", + "2026-10-05", + "2026-10-06", + "2026-10-07" + ] + }, + { + "hours_utc": [ + "00:00-01:00", + "04:00-06:00", + "10:00-00:00" + ], + "override_dates": [ + "2026-01-04", + "2026-02-14", + "2026-02-28", + "2026-05-09", + "2026-09-20", + "2026-10-10" + ] } ] }, @@ -58428,51 +58437,7 @@ "off_peak_pricing": { "cache_read_input_token_cost": 2.2e-08, "input_cost_per_token": 6.6e-07, - "off_peak_dates": [ - "2026-01-01", - "2026-01-02", - "2026-01-03", - "2026-02-15", - "2026-02-16", - "2026-02-17", - "2026-02-18", - "2026-02-19", - "2026-02-20", - "2026-02-21", - "2026-02-22", - "2026-02-23", - "2026-04-04", - "2026-04-05", - "2026-04-06", - "2026-05-01", - "2026-05-02", - "2026-05-03", - "2026-05-04", - "2026-05-05", - "2026-06-19", - "2026-06-20", - "2026-06-21", - "2026-09-25", - "2026-09-26", - "2026-09-27", - "2026-10-01", - "2026-10-02", - "2026-10-03", - "2026-10-04", - "2026-10-05", - "2026-10-06", - "2026-10-07" - ], "output_cost_per_token": 1.98e-06, - "weekday_dates": [ - "2026-01-04", - "2026-02-14", - "2026-02-28", - "2026-05-09", - "2026-09-20", - "2026-10-10" - ], - "weekday_timezone": "Asia/Shanghai", "windows": [ { "hours_utc": [ @@ -58494,6 +58459,59 @@ 6, 7 ] + }, + { + "hours_utc": "00:00-00:00", + "override_dates": [ + "2026-01-01", + "2026-01-02", + "2026-01-03", + "2026-02-15", + "2026-02-16", + "2026-02-17", + "2026-02-18", + "2026-02-19", + "2026-02-20", + "2026-02-21", + "2026-02-22", + "2026-02-23", + "2026-04-04", + "2026-04-05", + "2026-04-06", + "2026-05-01", + "2026-05-02", + "2026-05-03", + "2026-05-04", + "2026-05-05", + "2026-06-19", + "2026-06-20", + "2026-06-21", + "2026-09-25", + "2026-09-26", + "2026-09-27", + "2026-10-01", + "2026-10-02", + "2026-10-03", + "2026-10-04", + "2026-10-05", + "2026-10-06", + "2026-10-07" + ] + }, + { + "hours_utc": [ + "00:00-01:00", + "04:00-06:00", + "10:00-00:00" + ], + "override_dates": [ + "2026-01-04", + "2026-02-14", + "2026-02-28", + "2026-05-09", + "2026-09-20", + "2026-10-10" + ] } ] }, diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 0b8dc965427..a7d9731ac2d 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -233,11 +233,14 @@ class OffPeakWindow(TypedDict, total=False): midnight and an equal-ended window covers the whole day. weekdays is a list of days the rule applies on, as ISO-8601 numbers (1 = Monday .. 7 = Sunday) or English day names; omitted means every day. The weekday is read on the calendar named by the block's - weekday_timezone. + weekday_timezone. override_dates lists YYYY-MM-DD dates on that calendar on which this + rule alone decides, ignoring weekdays and every other window; on any other date the + rule does not apply. """ hours_utc: ReadOnly[str | Sequence[str]] weekdays: ReadOnly[Sequence[int | str]] + override_dates: ReadOnly[Sequence[str]] class OffPeakPricing(TypedDict, total=False): @@ -247,17 +250,12 @@ class OffPeakPricing(TypedDict, total=False): applying on every day of the week; a window may wrap past midnight. windows adds day-of-week-qualified rules (e.g. weekend-only whole-day off-peak), matched as a union with hours_utc. weekday_timezone names the IANA calendar weekdays are read on, defaulting - to UTC. off_peak_dates lists YYYY-MM-DD dates, read on that calendar, that are off-peak - all day (public holidays), and weekday_dates lists dates that follow the Monday-to-Friday - rules even on a weekend (make-up workdays). Any rate left unset falls back to the - standard rate. + to UTC. Any rate left unset falls back to the standard rate. """ hours_utc: ReadOnly[str | Sequence[str]] windows: ReadOnly[Sequence[OffPeakWindow]] weekday_timezone: ReadOnly[str] - off_peak_dates: ReadOnly[Sequence[str]] - weekday_dates: ReadOnly[Sequence[str]] input_cost_per_token: ReadOnly[float] output_cost_per_token: ReadOnly[float] output_cost_per_reasoning_token: ReadOnly[float] diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 0f2486ec0fc..93f3576ac3d 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -58222,51 +58222,7 @@ "off_peak_pricing": { "cache_read_input_token_cost": 3e-09, "input_cost_per_token": 1.5e-07, - "off_peak_dates": [ - "2026-01-01", - "2026-01-02", - "2026-01-03", - "2026-02-15", - "2026-02-16", - "2026-02-17", - "2026-02-18", - "2026-02-19", - "2026-02-20", - "2026-02-21", - "2026-02-22", - "2026-02-23", - "2026-04-04", - "2026-04-05", - "2026-04-06", - "2026-05-01", - "2026-05-02", - "2026-05-03", - "2026-05-04", - "2026-05-05", - "2026-06-19", - "2026-06-20", - "2026-06-21", - "2026-09-25", - "2026-09-26", - "2026-09-27", - "2026-10-01", - "2026-10-02", - "2026-10-03", - "2026-10-04", - "2026-10-05", - "2026-10-06", - "2026-10-07" - ], "output_cost_per_token": 6e-07, - "weekday_dates": [ - "2026-01-04", - "2026-02-14", - "2026-02-28", - "2026-05-09", - "2026-09-20", - "2026-10-10" - ], - "weekday_timezone": "Asia/Shanghai", "windows": [ { "hours_utc": [ @@ -58288,6 +58244,59 @@ 6, 7 ] + }, + { + "hours_utc": "00:00-00:00", + "override_dates": [ + "2026-01-01", + "2026-01-02", + "2026-01-03", + "2026-02-15", + "2026-02-16", + "2026-02-17", + "2026-02-18", + "2026-02-19", + "2026-02-20", + "2026-02-21", + "2026-02-22", + "2026-02-23", + "2026-04-04", + "2026-04-05", + "2026-04-06", + "2026-05-01", + "2026-05-02", + "2026-05-03", + "2026-05-04", + "2026-05-05", + "2026-06-19", + "2026-06-20", + "2026-06-21", + "2026-09-25", + "2026-09-26", + "2026-09-27", + "2026-10-01", + "2026-10-02", + "2026-10-03", + "2026-10-04", + "2026-10-05", + "2026-10-06", + "2026-10-07" + ] + }, + { + "hours_utc": [ + "00:00-01:00", + "04:00-06:00", + "10:00-00:00" + ], + "override_dates": [ + "2026-01-04", + "2026-02-14", + "2026-02-28", + "2026-05-09", + "2026-09-20", + "2026-10-10" + ] } ] }, @@ -58428,51 +58437,7 @@ "off_peak_pricing": { "cache_read_input_token_cost": 2.2e-08, "input_cost_per_token": 6.6e-07, - "off_peak_dates": [ - "2026-01-01", - "2026-01-02", - "2026-01-03", - "2026-02-15", - "2026-02-16", - "2026-02-17", - "2026-02-18", - "2026-02-19", - "2026-02-20", - "2026-02-21", - "2026-02-22", - "2026-02-23", - "2026-04-04", - "2026-04-05", - "2026-04-06", - "2026-05-01", - "2026-05-02", - "2026-05-03", - "2026-05-04", - "2026-05-05", - "2026-06-19", - "2026-06-20", - "2026-06-21", - "2026-09-25", - "2026-09-26", - "2026-09-27", - "2026-10-01", - "2026-10-02", - "2026-10-03", - "2026-10-04", - "2026-10-05", - "2026-10-06", - "2026-10-07" - ], "output_cost_per_token": 1.98e-06, - "weekday_dates": [ - "2026-01-04", - "2026-02-14", - "2026-02-28", - "2026-05-09", - "2026-09-20", - "2026-10-10" - ], - "weekday_timezone": "Asia/Shanghai", "windows": [ { "hours_utc": [ @@ -58494,6 +58459,59 @@ 6, 7 ] + }, + { + "hours_utc": "00:00-00:00", + "override_dates": [ + "2026-01-01", + "2026-01-02", + "2026-01-03", + "2026-02-15", + "2026-02-16", + "2026-02-17", + "2026-02-18", + "2026-02-19", + "2026-02-20", + "2026-02-21", + "2026-02-22", + "2026-02-23", + "2026-04-04", + "2026-04-05", + "2026-04-06", + "2026-05-01", + "2026-05-02", + "2026-05-03", + "2026-05-04", + "2026-05-05", + "2026-06-19", + "2026-06-20", + "2026-06-21", + "2026-09-25", + "2026-09-26", + "2026-09-27", + "2026-10-01", + "2026-10-02", + "2026-10-03", + "2026-10-04", + "2026-10-05", + "2026-10-06", + "2026-10-07" + ] + }, + { + "hours_utc": [ + "00:00-01:00", + "04:00-06:00", + "10:00-00:00" + ], + "override_dates": [ + "2026-01-04", + "2026-02-14", + "2026-02-28", + "2026-05-09", + "2026-09-20", + "2026-10-10" + ] } ] }, diff --git a/model_prices_and_context_window.schema.json b/model_prices_and_context_window.schema.json index 2de7b6ac524..3ebc95f9c9e 100644 --- a/model_prices_and_context_window.schema.json +++ b/model_prices_and_context_window.schema.json @@ -559,6 +559,16 @@ ] }, "minItems": 1 + }, + "override_dates": { + "type": "array", + "items": { + "type": "string", + "format": "date", + "pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$" + }, + "minItems": 1, + "description": "YYYY-MM-DD dates, read on weekday_timezone, on which this rule alone decides: weekdays and every other window are ignored. On any other date the rule does not apply." } }, "required": [ @@ -572,26 +582,6 @@ "type": "string", "description": "IANA zone the weekdays of each window are read on; defaults to UTC." }, - "off_peak_dates": { - "type": "array", - "items": { - "type": "string", - "format": "date", - "pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$" - }, - "minItems": 1, - "description": "Calendar dates, read on weekday_timezone, that are off-peak all day (public holidays)." - }, - "weekday_dates": { - "type": "array", - "items": { - "type": "string", - "format": "date", - "pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$" - }, - "minItems": 1, - "description": "Calendar dates, read on weekday_timezone, that follow the Monday-to-Friday rules even when they fall on a weekend (make-up workdays)." - }, "input_cost_per_token": { "type": "number", "minimum": 0 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 1b2d1f7f781..fa9cd5ddd1d 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 @@ -601,156 +601,145 @@ def test_is_off_peak_weekday_qualified_windows_deepseek_schedule(): def test_is_off_peak_holiday_weekday_is_off_peak_all_day(): - """A date in off_peak_dates is off-peak around the clock, including inside the peak - windows a plain weekday would bill at standard rates.""" + """A rule whose override_dates names a public holiday applies its off-peak window all + day, including inside the peak hours a plain weekday would bill at standard rates.""" from datetime import datetime, timezone deepseek = { - "weekday_timezone": "Asia/Shanghai", - "off_peak_dates": ["2026-01-01", "2026-10-01"], - "weekday_dates": ["2026-01-04", "2026-02-14"], "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]}, + {"hours_utc": "00:00-00:00", "override_dates": ["2026-01-01", "2026-10-01"]}, + { + "hours_utc": ["00:00-01:00", "04:00-06:00", "10:00-00:00"], + "override_dates": ["2026-01-04", "2026-02-14"], + }, ], } holiday = datetime(2026, 1, 1, 2, 0, tzinfo=timezone.utc) - holiday_second_window = datetime(2026, 1, 1, 7, 30, tzinfo=timezone.utc) normal_thursday = datetime(2026, 1, 8, 2, 0, tzinfo=timezone.utc) - normal_thursday_second_window = datetime(2026, 1, 8, 7, 30, tzinfo=timezone.utc) assert _is_off_peak(deepseek, holiday) is True, f"{holiday.isoformat()} is a public holiday" - assert _is_off_peak(deepseek, holiday_second_window) is True, ( - f"{holiday_second_window.isoformat()} is a public holiday" - ) assert _is_off_peak(deepseek, normal_thursday) is False, ( f"{normal_thursday.isoformat()} is a normal Thursday peak hour" ) - assert _is_off_peak(deepseek, normal_thursday_second_window) is False, ( - f"{normal_thursday_second_window.isoformat()} is a normal Thursday peak hour" - ) -def test_is_off_peak_make_up_weekend_day_follows_weekday_rules(): - """A Sunday listed in weekday_dates is a make-up workday: the weekday windows apply, so - its peak hours bill standard and its weekday off-peak hours bill off-peak.""" +def test_is_off_peak_make_up_weekend_day_uses_override_hours(): + """On a make-up workday only the rule listing the date decides: the weekend whole-day + rule does not fire, and the make-up rule's weekday off-peak hours apply instead.""" from datetime import datetime, timezone deepseek = { - "weekday_timezone": "Asia/Shanghai", - "off_peak_dates": ["2026-01-01", "2026-10-01"], - "weekday_dates": ["2026-01-04", "2026-02-14"], "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]}, + {"hours_utc": "00:00-00:00", "override_dates": ["2026-01-01", "2026-10-01"]}, + { + "hours_utc": ["00:00-01:00", "04:00-06:00", "10:00-00:00"], + "override_dates": ["2026-01-04", "2026-02-14"], + }, ], } make_up_sunday_peak = datetime(2026, 1, 4, 2, 0, tzinfo=timezone.utc) - make_up_sunday_peak_second = datetime(2026, 1, 4, 7, 30, tzinfo=timezone.utc) make_up_sunday_off_peak = datetime(2026, 1, 4, 5, 0, tzinfo=timezone.utc) - make_up_sunday_off_peak_second = datetime(2026, 1, 4, 12, 0, tzinfo=timezone.utc) plain_sunday = datetime(2026, 1, 11, 2, 0, tzinfo=timezone.utc) assert _is_off_peak(deepseek, make_up_sunday_peak) is False, ( f"{make_up_sunday_peak.isoformat()} is a make-up workday peak hour" ) - assert _is_off_peak(deepseek, make_up_sunday_peak_second) is False, ( - f"{make_up_sunday_peak_second.isoformat()} is a make-up workday peak hour" - ) assert _is_off_peak(deepseek, make_up_sunday_off_peak) is True, ( - f"{make_up_sunday_off_peak.isoformat()} is a make-up workday off-peak hour" - ) - assert _is_off_peak(deepseek, make_up_sunday_off_peak_second) is True, ( - f"{make_up_sunday_off_peak_second.isoformat()} is a make-up workday off-peak hour" + f"{make_up_sunday_off_peak.isoformat()} is inside the make-up rule's off-peak hours" ) assert _is_off_peak(deepseek, plain_sunday) is True, ( f"{plain_sunday.isoformat()} is a normal Sunday, off-peak all day" ) -def test_is_off_peak_date_exceptions_read_on_weekday_timezone_calendar(): - """Both date lists are read on the weekday_timezone calendar, so the Shanghai date can +def test_is_off_peak_override_skips_flat_hours_and_other_rules(): + """On a date an override rule lists, the flat hours_utc and every other rule are + skipped: only the matching rules' windows decide.""" + from datetime import datetime, timezone + + block = { + "hours_utc": "00:00-00:00", + "windows": [ + {"hours_utc": "00:00-00:00", "weekdays": [1, 2, 3, 4, 5, 6, 7]}, + {"override_dates": ["2026-03-03"], "hours_utc": "12:00-13:00"}, + ], + } + outside_window = datetime(2026, 3, 3, 2, 0, tzinfo=timezone.utc) + inside_window = datetime(2026, 3, 3, 12, 30, tzinfo=timezone.utc) + next_day = datetime(2026, 3, 4, 2, 0, tzinfo=timezone.utc) + assert _is_off_peak(block, outside_window) is False, ( + "the override rule's window is closed and nothing else applies on its date" + ) + assert _is_off_peak(block, inside_window) is True, "the override rule's window is open" + assert _is_off_peak(block, next_day) is True, ( + "the next day falls back to the flat hours_utc and weekday rules" + ) + + +def test_is_off_peak_override_rule_does_not_leak_onto_other_dates(): + """A rule carrying override_dates never participates on a date it does not list.""" + from datetime import datetime, timezone + + block = {"windows": [{"override_dates": ["2026-03-03"], "hours_utc": "00:00-00:00"}]} + assert _is_off_peak(block, datetime(2026, 3, 3, 2, 0, tzinfo=timezone.utc)) is True + assert _is_off_peak(block, datetime(2026, 3, 4, 2, 0, tzinfo=timezone.utc)) is False + + +def test_is_off_peak_override_dates_read_on_weekday_timezone_calendar(): + """Override dates are read on the weekday_timezone calendar, so the Shanghai date can diverge from the UTC date over 16:00-24:00 UTC.""" from datetime import datetime, timezone - holiday_saturday_rule = { + shanghai = { "weekday_timezone": "Asia/Shanghai", - "windows": [{"hours_utc": "16:00-17:00", "weekdays": [6, 7]}], - "off_peak_dates": ["2026-01-01"], + "windows": [{"override_dates": ["2026-01-01"], "hours_utc": "00:00-00:00"}], } - shanghai_new_year = datetime(2025, 12, 31, 16, 30, tzinfo=timezone.utc) - utc_new_year = datetime(2026, 1, 1, 16, 30, tzinfo=timezone.utc) - assert _is_off_peak(holiday_saturday_rule, shanghai_new_year) is True, ( - f"{shanghai_new_year.isoformat()} is already 2026-01-01 in Shanghai, a holiday" + utc_block = {"windows": [{"override_dates": ["2026-01-01"], "hours_utc": "00:00-00:00"}]} + in_shanghai = datetime(2025, 12, 31, 16, 30, tzinfo=timezone.utc) + before_shanghai = datetime(2025, 12, 31, 15, 30, tzinfo=timezone.utc) + assert _is_off_peak(shanghai, in_shanghai) is True, ( + f"{in_shanghai.isoformat()} is already 2026-01-01 in Shanghai" ) - assert _is_off_peak(holiday_saturday_rule, utc_new_year) is False, ( - f"{utc_new_year.isoformat()} is 2026-01-02 in Shanghai, no longer the holiday" + assert _is_off_peak(shanghai, before_shanghai) is False, ( + f"{before_shanghai.isoformat()} is still 2025-12-31 in Shanghai" ) - - utc_holiday_saturday_rule = { - "windows": [{"hours_utc": "16:00-17:00", "weekdays": [6, 7]}], - "off_peak_dates": ["2026-01-01"], - } - assert _is_off_peak(utc_holiday_saturday_rule, shanghai_new_year) is False, ( - f"{shanghai_new_year.isoformat()} is still 2025-12-31 on the UTC calendar" - ) - assert _is_off_peak(utc_holiday_saturday_rule, utc_new_year) is True, ( - f"{utc_new_year.isoformat()} is 2026-01-01 on the UTC calendar" - ) - - make_up_weekday_rule = { - "weekday_timezone": "Asia/Shanghai", - "windows": [{"hours_utc": "16:00-17:00", "weekdays": [1, 2, 3, 4, 5]}], - "weekday_dates": ["2026-01-04"], - } - shanghai_make_up_sunday = datetime(2026, 1, 3, 16, 30, tzinfo=timezone.utc) - shanghai_saturday = datetime(2026, 1, 2, 16, 30, tzinfo=timezone.utc) - assert _is_off_peak(make_up_weekday_rule, shanghai_make_up_sunday) is True, ( - f"{shanghai_make_up_sunday.isoformat()} is Sunday 2026-01-04 in Shanghai, a make-up workday" - ) - assert _is_off_peak(make_up_weekday_rule, shanghai_saturday) is False, ( - f"{shanghai_saturday.isoformat()} is Saturday 2026-01-03 in Shanghai" + assert _is_off_peak(utc_block, in_shanghai) is False, ( + f"{in_shanghai.isoformat()} is still 2025-12-31 on the UTC calendar" ) -def test_is_off_peak_make_up_day_does_not_match_weekend_only_rules(): - """A make-up workday follows the Monday-to-Friday rules, so a weekend-only rule must not - match it even though its real isoweekday is Sunday.""" +def test_is_off_peak_ignores_malformed_override_dates(): + """A bare string, non-string entries, or an unparseable date are ignored, so malformed + override_dates can never silently widen the off-peak hours.""" from datetime import datetime, timezone - weekend_only = { - "windows": [{"hours_utc": "00:00-00:00", "weekdays": [6, 7]}], - "weekday_dates": ["2026-01-04"], - } - make_up_sunday = datetime(2026, 1, 4, 12, 0, tzinfo=timezone.utc) - plain_saturday = datetime(2026, 1, 3, 12, 0, tzinfo=timezone.utc) - assert _is_off_peak(weekend_only, make_up_sunday) is False, ( - f"{make_up_sunday.isoformat()} is a make-up workday, not a weekend day" - ) - assert _is_off_peak(weekend_only, plain_saturday) is True, ( - f"{plain_saturday.isoformat()} is a normal Saturday" - ) + peak_instant = datetime(2026, 1, 1, 12, 0, tzinfo=timezone.utc) + weekday_windows = [{"hours_utc": "00:30-01:00", "weekdays": [1, 2, 3, 4, 5]}] + for bad in ("2026-01-01", [20260101, None], ["2026-1-1"]): + block = {"windows": [{**weekday_windows[0], "override_dates": bad}]} + assert _is_off_peak(block, peak_instant) is False, ( + f"override_dates={bad!r} is malformed and must be ignored" + ) -def test_is_off_peak_ignores_malformed_date_exceptions(): - """A bare string or non-string entries are ignored, matching how a bare weekdays string - is treated: malformed config can never silently widen the off-peak hours.""" +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.""" from datetime import datetime, timezone - holiday = datetime(2026, 1, 1, 12, 0, tzinfo=timezone.utc) - weekend_whole_day = [{"hours_utc": "00:00-00:00", "weekdays": [6, 7]}] - assert _is_off_peak({"off_peak_dates": "2026-01-01", "windows": weekend_whole_day}, holiday) is False, ( - "a bare off_peak_dates string is malformed and must be ignored" - ) - block = {"off_peak_dates": ["01/01/2026", 20260101, None], "windows": weekend_whole_day} - assert _is_off_peak(block, holiday) is False, "unparseable entries are simply never equal to a date" - make_up_sunday = datetime(2026, 1, 4, 12, 0, tzinfo=timezone.utc) - block = {"weekday_dates": "2026-01-04", "windows": weekend_whole_day} - assert _is_off_peak(block, make_up_sunday) is True, ( - f"{make_up_sunday.isoformat()} is a plain Sunday once the bare weekday_dates string is ignored" - ) + deepseek = { + "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]}, + ], + } + assert _is_off_peak(deepseek, datetime(2026, 1, 1, 2, 0, tzinfo=timezone.utc)) is False -def test_get_token_base_cost_off_peak_date_bills_holiday_rates(): - """End to end through _get_token_base_cost: a holiday listed in off_peak_dates bills the +def test_get_token_base_cost_override_dates_bill_holiday_rates(): + """End to end through _get_token_base_cost: a date listed on an override rule bills the off-peak input rate during what would be a weekday peak hour.""" from datetime import datetime, timezone from typing import cast @@ -763,10 +752,10 @@ def test_get_token_base_cost_off_peak_date_bills_holiday_rates(): "input_cost_per_token": 1e-6, "output_cost_per_token": 2e-6, "off_peak_pricing": { - "off_peak_dates": ["2026-01-01"], "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]}, + {"hours_utc": "00:00-00:00", "override_dates": ["2026-01-01"]}, ], "input_cost_per_token": 5e-7, "output_cost_per_token": 1e-6, diff --git a/tests/test_litellm/test_model_prices_schema.py b/tests/test_litellm/test_model_prices_schema.py index 4d92ecaed8c..2437a8b6700 100644 --- a/tests/test_litellm/test_model_prices_schema.py +++ b/tests/test_litellm/test_model_prices_schema.py @@ -135,10 +135,11 @@ OFF_PEAK_ENTRY: Final = MappingProxyType( "output_cost_per_token": 8e-6, "off_peak_pricing": { "hours_utc": "16:30-00:30", - "windows": [{"hours_utc": ["00:30-02:00"], "weekdays": [6, "Sunday", "mon", "THURS"]}], + "windows": [ + {"hours_utc": ["00:30-02:00"], "weekdays": [6, "Sunday", "mon", "THURS"]}, + {"hours_utc": "00:00-00:00", "override_dates": ["2026-01-01"]}, + ], "weekday_timezone": "Asia/Shanghai", - "off_peak_dates": ["2026-01-01"], - "weekday_dates": ["2026-01-04"], "input_cost_per_token": 1e-6, "output_cost_per_token": 4e-6, "cache_read_input_token_cost": 1e-7, @@ -169,9 +170,9 @@ def test_generator_classifies_off_peak_pricing_as_a_windowed_rate_block(): {"hours_utc": "25:00-01:00", "input_cost_per_token": 1e-6}, {"hours_utc": ["16:30-00:30", "4pm-midnight"], "input_cost_per_token": 1e-6}, {"windows": [{"hours_utc": "00:30-02:00", "weekdays": ["Funday"]}], "input_cost_per_token": 1e-6}, - {"hours_utc": "16:30-00:30", "off_peak_dates": ["2026-1-1"], "input_cost_per_token": 1e-6}, - {"hours_utc": "16:30-00:30", "off_peak_dates": "2026-01-01", "input_cost_per_token": 1e-6}, - {"hours_utc": "16:30-00:30", "weekday_dates": [], "input_cost_per_token": 1e-6}, + {"windows": [{"hours_utc": "00:30-02:00", "override_dates": ["2026-1-1"]}], "input_cost_per_token": 1e-6}, + {"windows": [{"hours_utc": "00:30-02:00", "override_dates": "2026-01-01"}], "input_cost_per_token": 1e-6}, + {"windows": [{"hours_utc": "00:30-02:00", "override_dates": []}], "input_cost_per_token": 1e-6}, ], ) def test_generated_off_peak_schema_rejects_malformed_blocks(block: dict): @@ -388,7 +389,10 @@ def deepseek_off_peak_drift(entry: Mapping[str, object]) -> str | None: block: Final = entry.get("off_peak_pricing") if not isinstance(block, dict): return "no off_peak_pricing block" - if tuple(block.get("windows", ())) != DEEPSEEK_OFF_PEAK_WINDOWS: + 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) :]): 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}