From c602c9a94aed6a3dfe3faf9ba092ffdba5ae45d8 Mon Sep 17 00:00:00 2001 From: Loi Nguyen <1948922+lntutor@users.noreply.github.com> Date: Sun, 19 Jul 2026 19:56:01 +0700 Subject: [PATCH] fix(budget): correct multi-day rollover in standardized reset-time handlers _handle_hour_reset, _handle_minute_reset and _handle_second_reset reduced the overflowing next_hour with % 24 but always advanced the date by a single timedelta(days=1). Any duration spanning >= 48h (e.g. 48h, 72h, 3000m, 180000s) therefore reset up to days too early -- 48h from 10:00 landed the reset only 14h out, and 72h advanced a single day instead of three. Capture days_ahead = next_hour // 24 before the modulo so the full whole-day overflow is applied. Fixes #17993. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01N6RtoHuxrDqTUo9Mw9h4Cv --- litellm/litellm_core_utils/duration_parser.py | 9 ++++--- .../test_duration_parser.py | 25 +++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/litellm/litellm_core_utils/duration_parser.py b/litellm/litellm_core_utils/duration_parser.py index 438ff5600ba..75c2232c4f4 100644 --- a/litellm/litellm_core_utils/duration_parser.py +++ b/litellm/litellm_core_utils/duration_parser.py @@ -236,8 +236,9 @@ def _handle_hour_reset(current_time: datetime, base_midnight: datetime, value: i # Handle overnight case if next_hour >= 24: + days_ahead = next_hour // 24 next_hour = next_hour % 24 - next_day = base_midnight + timedelta(days=1) + next_day = base_midnight + timedelta(days=days_ahead) return next_day.replace(hour=next_hour) return current_time.replace(hour=next_hour, minute=0, second=0, microsecond=0) @@ -270,8 +271,9 @@ def _handle_minute_reset(current_time: datetime, base_midnight: datetime, value: # Handle overnight case if next_hour >= 24: + days_ahead = next_hour // 24 next_hour = next_hour % 24 - next_day = base_midnight + timedelta(days=1) + next_day = base_midnight + timedelta(days=days_ahead) return next_day.replace(hour=next_hour, minute=next_minute, second=0, microsecond=0) return current_time.replace(hour=next_hour, minute=next_minute, second=0, microsecond=0) @@ -309,8 +311,9 @@ def _handle_second_reset(current_time: datetime, base_midnight: datetime, value: # Handle overnight case if next_hour >= 24: + days_ahead = next_hour // 24 next_hour = next_hour % 24 - next_day = base_midnight + timedelta(days=1) + next_day = base_midnight + timedelta(days=days_ahead) return next_day.replace(hour=next_hour, minute=next_minute, second=next_second, microsecond=0) return current_time.replace(hour=next_hour, minute=next_minute, second=next_second, microsecond=0) diff --git a/tests/test_litellm/litellm_core_utils/test_duration_parser.py b/tests/test_litellm/litellm_core_utils/test_duration_parser.py index 3e4446c6672..f92005150f7 100644 --- a/tests/test_litellm/litellm_core_utils/test_duration_parser.py +++ b/tests/test_litellm/litellm_core_utils/test_duration_parser.py @@ -71,6 +71,31 @@ class TestStandardizedResetTime(unittest.TestCase): second_result = get_next_standardized_reset_time("15s", base_time, "UTC") self.assertEqual(second_result, second_expected) + def test_multi_day_rollover_resets(self): + """Durations spanning more than 24h must advance the full number of days.""" + # Base time: 2023-05-15 10:00 UTC + base_time = datetime(2023, 5, 15, 10, 0, 0, tzinfo=timezone.utc) + + # 48h aligns to midnight + 48h; first boundary after 10:00 is the 17th. + self.assertEqual( + get_next_standardized_reset_time("48h", base_time, "UTC"), + datetime(2023, 5, 17, 0, 0, 0, tzinfo=timezone.utc), + ) + # 72h -> the 18th (previously advanced only a single day). + self.assertEqual( + get_next_standardized_reset_time("72h", base_time, "UTC"), + datetime(2023, 5, 18, 0, 0, 0, tzinfo=timezone.utc), + ) + # 3000m == 50h and 180000s == 50h -> 10:00 + 50h == the 17th at 12:00. + self.assertEqual( + get_next_standardized_reset_time("3000m", base_time, "UTC"), + datetime(2023, 5, 17, 12, 0, 0, tzinfo=timezone.utc), + ) + self.assertEqual( + get_next_standardized_reset_time("180000s", base_time, "UTC"), + datetime(2023, 5, 17, 12, 0, 0, tzinfo=timezone.utc), + ) + def test_timezone_handling(self): """Test timezone handling with different regions""" # Base time: 2023-05-15 22:30:00 UTC (late in UTC day)