diff --git a/ci_cd/generate_model_prices_schema.py b/ci_cd/generate_model_prices_schema.py index 8eec07dadda..8e4302d0775 100644 --- a/ci_cd/generate_model_prices_schema.py +++ b/ci_cd/generate_model_prices_schema.py @@ -20,6 +20,11 @@ NONNEG_INTEGER: JsonSchema = {"type": "integer", "minimum": 0} BOOLEAN: JsonSchema = {"type": "boolean"} STRING: JsonSchema = {"type": "string"} TIME_WINDOW: Final[JsonSchema] = {"type": "string", "pattern": r"^([01]\d|2[0-3]):[0-5]\d-([01]\d|2[0-3]):[0-5]\d$"} +ISO_DATE: Final[JsonSchema] = { + "type": "string", + "format": "date", + "pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$", +} WEEKDAY_PATTERN: Final = ( r"(?i)^(mon|monday|tue|tues|tuesday|wed|wednesday|thu|thur|thurs|thursday|fri|friday|sat|saturday|sun|sunday)$" ) @@ -55,6 +60,16 @@ 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, and malformed entries disable it." + ), + }, }, "required": ["hours_utc"], "additionalProperties": False, @@ -258,10 +273,8 @@ def string_key_schemas(modes: tuple) -> dict[str, JsonSchema]: "description": "URL of the provider pricing/model page this entry was taken from.", }, "deprecation_date": { - "type": "string", + **ISO_DATE, "description": "Date the provider deprecates the model, YYYY-MM-DD.", - "format": "date", - "pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$", }, "web_search_billing_unit": { "type": "string", diff --git a/litellm-rust/crates/model-catalog/src/model_info.rs b/litellm-rust/crates/model-catalog/src/model_info.rs index 4a56e1112d1..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. diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 064e3040054..43f0b5ec256 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, and malformed entries disable it entirely. """ hours_utc: ReadOnly[str | Sequence[str]] weekdays: ReadOnly[Sequence[int | str]] + override_dates: ReadOnly[Sequence[str]] class OffPeakPricing(TypedDict, total=False): diff --git a/model_prices_and_context_window.schema.json b/model_prices_and_context_window.schema.json index 395b2db1137..f7924f5c966 100644 --- a/model_prices_and_context_window.schema.json +++ b/model_prices_and_context_window.schema.json @@ -249,9 +249,9 @@ }, "deprecation_date": { "type": "string", - "description": "Date the provider deprecates the model, YYYY-MM-DD.", "format": "date", - "pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$" + "pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$", + "description": "Date the provider deprecates the model, YYYY-MM-DD." }, "gemini_audio_only_live": { "type": "boolean" @@ -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, and malformed entries disable it." } }, "required": [ diff --git a/tests/test_litellm/test_model_prices_schema.py b/tests/test_litellm/test_model_prices_schema.py index 052278631e2..2437a8b6700 100644 --- a/tests/test_litellm/test_model_prices_schema.py +++ b/tests/test_litellm/test_model_prices_schema.py @@ -135,7 +135,10 @@ 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", "input_cost_per_token": 1e-6, "output_cost_per_token": 4e-6, @@ -167,6 +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}, + {"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): @@ -383,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} diff --git a/tests/test_litellm/test_utils.py b/tests/test_litellm/test_utils.py index 5dc09db4535..1805c738cd8 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,