From 9783ef4e797d39e9f3b02859c8ff7ff1a4b60c25 Mon Sep 17 00:00:00 2001 From: michelligabriele Date: Mon, 11 May 2026 21:27:36 +0200 Subject: [PATCH] fix(usage): remove timezone date-window expansion in aggregated dashboard --- .../common_daily_activity.py | 42 ++++++------------- .../test_common_daily_activity.py | 28 +++++++++++++ 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/litellm/proxy/management_endpoints/common_daily_activity.py b/litellm/proxy/management_endpoints/common_daily_activity.py index d173cd745ba..7cd0134bf38 100644 --- a/litellm/proxy/management_endpoints/common_daily_activity.py +++ b/litellm/proxy/management_endpoints/common_daily_activity.py @@ -1,5 +1,5 @@ import asyncio -from datetime import datetime, timedelta +from datetime import datetime from types import SimpleNamespace from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union @@ -386,38 +386,20 @@ def _adjust_dates_for_timezone( timezone_offset_minutes: Optional[int], ) -> Tuple[str, str]: """ - Adjust date range to account for timezone differences. + Return the date range unchanged. - The database stores dates in UTC. When a user in a different timezone - selects a local date range, we need to expand the UTC query range to - capture all records that fall within their local date range. + ``LiteLLM_DailyUserSpend.date`` is stored as a ``YYYY-MM-DD`` UTC string + at day granularity, and the UI formats ``start_date``/``end_date`` from + the browser's local year/month/day — so the strings already line up with + how rows are keyed. The previous ±1 day expansion pulled an adjacent UTC + day into the result set, inflating single-day totals for any non-UTC + user (e.g. IST users saw two days summed together). - Args: - start_date: Start date in YYYY-MM-DD format (user's local date) - end_date: End date in YYYY-MM-DD format (user's local date) - timezone_offset_minutes: Minutes behind UTC (positive = west of UTC) - This matches JavaScript's Date.getTimezoneOffset() convention. - For example: PST = +480 (8 hours * 60 = 480 minutes behind UTC) - - Returns: - Tuple of (adjusted_start_date, adjusted_end_date) in YYYY-MM-DD format + ``timezone_offset_minutes`` is kept in the signature for compatibility + with callers; it is intentionally ignored. """ - if timezone_offset_minutes is None or timezone_offset_minutes == 0: - return start_date, end_date - - start = datetime.strptime(start_date, "%Y-%m-%d") - end = datetime.strptime(end_date, "%Y-%m-%d") - - if timezone_offset_minutes > 0: - # West of UTC (Americas): local evening extends into next UTC day - # e.g., Feb 4 23:59 PST = Feb 5 07:59 UTC - end = end + timedelta(days=1) - else: - # East of UTC (Asia/Europe): local morning starts in previous UTC day - # e.g., Feb 4 00:00 IST = Feb 3 18:30 UTC - start = start - timedelta(days=1) - - return start.strftime("%Y-%m-%d"), end.strftime("%Y-%m-%d") + del timezone_offset_minutes + return start_date, end_date def _build_where_conditions( diff --git a/tests/test_litellm/proxy/management_endpoints/test_common_daily_activity.py b/tests/test_litellm/proxy/management_endpoints/test_common_daily_activity.py index dc983aa26fd..1c8d15d9632 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_common_daily_activity.py +++ b/tests/test_litellm/proxy/management_endpoints/test_common_daily_activity.py @@ -9,6 +9,7 @@ sys.path.insert( ) # Adds the parent directory to the system path from litellm.proxy.management_endpoints.common_daily_activity import ( + _adjust_dates_for_timezone, _is_user_agent_tag, get_api_key_metadata, get_daily_activity, @@ -57,6 +58,33 @@ async def test_get_daily_activity_empty_entity_id_list(): assert where_conditions["team_id"] == {"in": []} +@pytest.mark.parametrize( + "timezone_offset_minutes", + [ + pytest.param(480, id="pst_west_of_utc"), + pytest.param(-330, id="ist_east_of_utc"), + pytest.param(0, id="utc"), + pytest.param(None, id="no_offset"), + ], +) +def test_adjust_dates_for_timezone_single_day_is_unchanged(timezone_offset_minutes): + # Regression: selecting a single local day in any timezone must keep the + # query window pinned to that one day. Previously, ±1 day was added based + # on the JS timezone offset, which inflated single-day totals on the + # aggregated usage dashboard for non-UTC users. + start, end = _adjust_dates_for_timezone( + "2026-04-15", "2026-04-15", timezone_offset_minutes + ) + assert start == "2026-04-15" + assert end == "2026-04-15" + + +def test_adjust_dates_for_timezone_multi_day_range_is_unchanged(): + start, end = _adjust_dates_for_timezone("2026-04-10", "2026-04-15", -330) + assert start == "2026-04-10" + assert end == "2026-04-15" + + def test_is_user_agent_tag(): """Test _is_user_agent_tag function.""" # Test None and empty string