Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions ci_cd/cost_map_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
map. A pull request that leaves all three untouched skips them, since merging it keeps the base branch's copies
and its head tree only carries whatever state the branch was cut from. Pull requests from the cost map sync bot
(branches named litellm_cost_map_sync_*) always get the file checks and additionally may only touch those three
files and may only add or update models.
files and may only add or update models. When the diff also changes the schema generator the sync and
validation checks are skipped, since this check runs the base branch's generator and the unit tests cover the
head's.
"""

from __future__ import annotations
Expand All @@ -23,6 +25,7 @@
COST_MAP_PATH: Final = "model_prices_and_context_window.json"
BACKUP_PATH: Final = "litellm/model_prices_and_context_window_backup.json"
SCHEMA_PATH: Final = "model_prices_and_context_window.schema.json"
GENERATOR_PATH: Final = "ci_cd/generate_model_prices_schema.py"
GUARDED_PATHS: Final = (COST_MAP_PATH, BACKUP_PATH, SCHEMA_PATH)
BOT_BRANCH_PREFIX: Final = "litellm_cost_map_sync_"

Expand Down Expand Up @@ -51,15 +54,17 @@ def _rendered_schema(cost_map: CostMap) -> str:
return str(error)


def _file_failures(head: Snapshot, head_map: CostMap) -> tuple[str, ...]:
schema_text: Final = _rendered_schema(head_map)
if not schema_text.startswith("{"):
return (schema_text,)
def _file_failures(head: Snapshot, head_map: CostMap, generator_changed: bool) -> tuple[str, ...]:
backup_failure: Final = (
()
if head.backup == head.cost_map
else (f"{BACKUP_PATH} differs from {COST_MAP_PATH}; copy the root file over it",)
)
if generator_changed:
return backup_failure
schema_text: Final = _rendered_schema(head_map)
if not schema_text.startswith("{"):
return (schema_text,)
schema_failure: Final = (
()
if head.schema == schema_text
Expand Down Expand Up @@ -127,7 +132,11 @@ def guard_failures(base: Snapshot, head: Snapshot, changed_files: Sequence[str],
head_map: Final = _parse_object(head.cost_map, COST_MAP_PATH)
if isinstance(head_map, str):
return (head_map,)
return (*_file_failures(head, head_map), *(_bot_failures(base, head_map, changed_files) if bot else ()))
generator_changed: Final = GENERATOR_PATH in changed_files
return (
*_file_failures(head, head_map, generator_changed),
*(_bot_failures(base, head_map, changed_files) if bot else ()),
)


def _git(*args: str) -> str | None:
Expand Down
19 changes: 16 additions & 3 deletions ci_cd/generate_model_prices_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
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)$"
)
Expand Down Expand Up @@ -55,6 +60,16 @@
},
"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,
Expand Down Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions litellm-rust/crates/model-catalog/src/model_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ pub struct OffPeakWindow {
pub hours_utc: UtcHours,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub weekdays: Option<Vec<Weekday>>,
/// 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<Vec<String>>,
}

/// Rates that replace the same-named base fields inside the stated UTC windows.
Expand Down
86 changes: 70 additions & 16 deletions litellm/litellm_core_utils/llm_cost_calc/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import re
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from datetime import datetime, timezone, tzinfo
from datetime import date, datetime, time, timezone, tzinfo
from types import MappingProxyType
from typing import Final, Literal, TypedDict, cast
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -447,6 +440,21 @@ def _is_within_off_peak_window(off_peak_hours_utc: str | Sequence[str], current_
)


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
Expand All @@ -466,6 +474,10 @@ def _weekday_calendar(weekday_timezone: object) -> tzinfo:
return timezone.utc


def _calendar_date(reference_utc: datetime, weekday_timezone: object) -> date:
return reference_utc.astimezone(_weekday_calendar(weekday_timezone)).date()


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
Expand All @@ -488,24 +500,66 @@ def _as_window_strings(value: object) -> tuple[str, ...]:
return ()


def _parse_iso_date(value: object) -> date | None:
if (
not isinstance(value, str)
or len(value) != 10
or value[4] != "-"
or value[7] != "-"
or not (value[:4] + value[5:7] + value[8:10]).isdigit()
):
return None
try:
return date(int(value[:4]), int(value[5:7]), int(value[8:10]))
except ValueError:
return None


def _as_dates(value: object) -> frozenset[date]:
if isinstance(value, str) or not isinstance(value, Sequence):
return frozenset()
parsed: Final = tuple(_parse_iso_date(entry) for entry in value)
if not parsed or any(entry is None for entry in parsed):
return frozenset()
return frozenset(entry for entry in parsed if entry is not None)


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.
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 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 = (
reference.astimezone(timezone.utc) if reference.tzinfo is not None else reference.replace(tzinfo=timezone.utc)
)
weekday_timezone: Final = off_peak.get("weekday_timezone")
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 _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:
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_timezone: Final = off_peak.get("weekday_timezone")
for rule in windows:
if not isinstance(rule, Mapping):
for rule in rules:
if "override_dates" in rule:
continue
rule_windows = _as_window_strings(rule.get("hours_utc"))
if not rule_windows:
Expand Down
Loading
Loading