mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-02 02:11:58 +00:00
Co-authored-by: yuneng <yuneng@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
177 lines
5.8 KiB
Python
177 lines
5.8 KiB
Python
from datetime import datetime, time, timezone
|
|
from zoneinfo import ZoneInfo
|
|
|
|
import pytest
|
|
|
|
|
|
import litellm
|
|
from litellm.proxy.common_utils.timezone_utils import (
|
|
BudgetResetSettings,
|
|
compute_budget_reset_at,
|
|
get_budget_reset_settings,
|
|
get_budget_reset_time,
|
|
get_budget_reset_timezone,
|
|
parse_budget_reset_time,
|
|
)
|
|
|
|
|
|
def _restore_attr(obj, name, original):
|
|
if original is None:
|
|
if hasattr(obj, name):
|
|
delattr(obj, name)
|
|
else:
|
|
setattr(obj, name, original)
|
|
|
|
|
|
def test_get_budget_reset_time():
|
|
"""
|
|
Test that the budget reset time is set to the first of the next month
|
|
"""
|
|
# Get the current date
|
|
now = datetime.now(timezone.utc)
|
|
|
|
# Calculate expected reset date (first of next month)
|
|
if now.month == 12:
|
|
expected_month = 1
|
|
expected_year = now.year + 1
|
|
else:
|
|
expected_month = now.month + 1
|
|
expected_year = now.year
|
|
expected_reset_at = datetime(expected_year, expected_month, 1, tzinfo=timezone.utc)
|
|
|
|
# Verify budget_reset_at is set to first of next month
|
|
assert get_budget_reset_time(budget_duration="1mo") == expected_reset_at
|
|
|
|
|
|
def test_get_budget_reset_timezone_reads_litellm_attr():
|
|
"""
|
|
Test that get_budget_reset_timezone reads from litellm.timezone attribute.
|
|
"""
|
|
original = getattr(litellm, "timezone", None)
|
|
try:
|
|
litellm.timezone = "Asia/Tokyo"
|
|
assert get_budget_reset_timezone() == "Asia/Tokyo"
|
|
finally:
|
|
if original is None:
|
|
if hasattr(litellm, "timezone"):
|
|
delattr(litellm, "timezone")
|
|
else:
|
|
litellm.timezone = original
|
|
|
|
|
|
def test_get_budget_reset_timezone_fallback_utc():
|
|
"""
|
|
Test that get_budget_reset_timezone falls back to UTC when litellm.timezone is not set.
|
|
"""
|
|
original = getattr(litellm, "timezone", None)
|
|
try:
|
|
if hasattr(litellm, "timezone"):
|
|
delattr(litellm, "timezone")
|
|
assert get_budget_reset_timezone() == "UTC"
|
|
finally:
|
|
if original is not None:
|
|
litellm.timezone = original
|
|
|
|
|
|
def test_get_budget_reset_timezone_fallback_on_none():
|
|
"""
|
|
Test that get_budget_reset_timezone falls back to UTC when litellm.timezone is None.
|
|
"""
|
|
original = getattr(litellm, "timezone", None)
|
|
try:
|
|
litellm.timezone = None
|
|
assert get_budget_reset_timezone() == "UTC"
|
|
finally:
|
|
if original is None:
|
|
if hasattr(litellm, "timezone"):
|
|
delattr(litellm, "timezone")
|
|
else:
|
|
litellm.timezone = original
|
|
|
|
|
|
def test_get_budget_reset_time_respects_timezone():
|
|
"""
|
|
Test that get_budget_reset_time uses the configured timezone for reset calculation.
|
|
A daily reset should align to midnight in the configured timezone.
|
|
"""
|
|
original = getattr(litellm, "timezone", None)
|
|
try:
|
|
litellm.timezone = "Asia/Tokyo"
|
|
reset_at = get_budget_reset_time(budget_duration="1d")
|
|
# The reset time should be midnight in Asia/Tokyo
|
|
tokyo_reset = reset_at.astimezone(ZoneInfo("Asia/Tokyo"))
|
|
assert tokyo_reset.hour == 0
|
|
assert tokyo_reset.minute == 0
|
|
assert tokyo_reset.second == 0
|
|
finally:
|
|
if original is None:
|
|
if hasattr(litellm, "timezone"):
|
|
delattr(litellm, "timezone")
|
|
else:
|
|
litellm.timezone = original
|
|
|
|
|
|
def test_parse_budget_reset_time_hh_mm():
|
|
assert parse_budget_reset_time("12:00") == time(12, 0)
|
|
|
|
|
|
def test_parse_budget_reset_time_hh_mm_ss():
|
|
assert parse_budget_reset_time("09:30:15") == time(9, 30, 15)
|
|
|
|
|
|
def test_parse_budget_reset_time_unset_defaults_to_midnight():
|
|
assert parse_budget_reset_time(None) == time(0, 0)
|
|
assert parse_budget_reset_time("") == time(0, 0)
|
|
|
|
|
|
def test_parse_budget_reset_time_invalid_string_raises():
|
|
with pytest.raises(ValueError, match="hour 'HH:MM' or 'HH:MM:SS' string, e\\.g\\."):
|
|
parse_budget_reset_time("25:00")
|
|
with pytest.raises(ValueError, match="Invalid budget_reset_time 'noon'; expected a"):
|
|
parse_budget_reset_time("noon")
|
|
|
|
|
|
def test_parse_budget_reset_time_non_string_raises():
|
|
# Unquoted "12:00" in YAML parses to the int 720; it must fail loudly,
|
|
# not silently fall back to midnight.
|
|
with pytest.raises(ValueError, match="hour 'HH:MM' string, e\\.g\\."):
|
|
parse_budget_reset_time(720)
|
|
|
|
|
|
def test_get_budget_reset_settings_reads_globals():
|
|
orig_tz = getattr(litellm, "timezone", None)
|
|
orig_rt = getattr(litellm, "budget_reset_time", None)
|
|
try:
|
|
litellm.timezone = "Asia/Jerusalem"
|
|
litellm.budget_reset_time = "12:00"
|
|
settings = get_budget_reset_settings()
|
|
assert settings.timezone == "Asia/Jerusalem"
|
|
assert settings.reset_time_of_day == time(12, 0)
|
|
finally:
|
|
_restore_attr(litellm, "timezone", orig_tz)
|
|
_restore_attr(litellm, "budget_reset_time", orig_rt)
|
|
|
|
|
|
def test_compute_budget_reset_at_applies_offset():
|
|
settings = BudgetResetSettings(
|
|
timezone="Asia/Jerusalem", reset_time_of_day=time(12, 0)
|
|
)
|
|
reset_at = compute_budget_reset_at("1d", settings)
|
|
jerusalem = reset_at.astimezone(ZoneInfo("Asia/Jerusalem"))
|
|
assert jerusalem.hour == 12
|
|
assert jerusalem.minute == 0
|
|
assert reset_at > datetime.now(timezone.utc)
|
|
|
|
|
|
def test_get_budget_reset_time_honors_global_budget_reset_time():
|
|
orig_tz = getattr(litellm, "timezone", None)
|
|
orig_rt = getattr(litellm, "budget_reset_time", None)
|
|
try:
|
|
litellm.timezone = "UTC"
|
|
litellm.budget_reset_time = "12:00"
|
|
reset_at = get_budget_reset_time(budget_duration="1d")
|
|
assert reset_at.astimezone(timezone.utc).hour == 12
|
|
assert reset_at.astimezone(timezone.utc).minute == 0
|
|
finally:
|
|
_restore_attr(litellm, "timezone", orig_tz)
|
|
_restore_attr(litellm, "budget_reset_time", orig_rt)
|