From 423677f19ee0fcd51c3e0492014acc47863e7a2e Mon Sep 17 00:00:00 2001 From: Ryan Crabbe Date: Sat, 11 Apr 2026 09:47:09 -0700 Subject: [PATCH] fix(spend): convert string dates to tz-aware UTC datetimes in _get_spend_report_for_time_range MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This helper takes start_date/end_date as plain strings and passed them straight to query_raw. Prisma forwards untyped text to Postgres, which parses `'2026-04-10'::timestamptz` using the session timezone because there's no +00:00 offset to respect. Under a non-UTC session this shifts the resolved instant by the session offset, and the AT TIME ZONE 'UTC' wrap introduced in the previous commit then strips it to a plain timestamp that's still offset by the same amount — producing the exact 4h drift that wrap was meant to prevent. Normalize the strings to datetime(..., tzinfo=timezone.utc) at the top of the function so Prisma serializes them with the explicit +00:00 suffix, which makes the ::timestamptz cast session-TZ-independent. Also replaces the $1::date comparison in the team_alias query with the same ::timestamptz AT TIME ZONE 'UTC' pattern used by every other site in this PR, so both queries share one consistent shape. Verified live on a real Postgres under session TZ America/New_York: the function now returns exactly the April 10 UTC demo rows ($4.20 / 10 rows) whereas the previous shape returned 13 rows (2 correct April 10 rows dropped, 5 rows from April 11 early-morning wrongly shifted in). --- .../spend_management_endpoints.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/spend_tracking/spend_management_endpoints.py b/litellm/proxy/spend_tracking/spend_management_endpoints.py index de51f3b0a8..6b7fe7d609 100644 --- a/litellm/proxy/spend_tracking/spend_management_endpoints.py +++ b/litellm/proxy/spend_tracking/spend_management_endpoints.py @@ -1456,6 +1456,16 @@ async def _get_spend_report_for_time_range( ) return None + # Normalize string inputs to tz-aware UTC datetimes so Prisma serializes + # them with an explicit +00:00 suffix. Raw strings get bound as untyped + # text, which forces Postgres to parse `::timestamptz` using the DB + # session timezone and drifts the window by the offset even with the + # AT TIME ZONE 'UTC' wrap below. + start_date_obj = datetime.strptime(start_date, "%Y-%m-%d").replace( + tzinfo=timezone.utc + ) + end_date_obj = datetime.strptime(end_date, "%Y-%m-%d").replace(tzinfo=timezone.utc) + try: sql_query = """ SELECT @@ -1466,13 +1476,16 @@ async def _get_spend_report_for_time_range( LEFT JOIN "LiteLLM_TeamTable" t ON s.team_id = t.team_id WHERE - s."startTime" >= $1::date AND s."startTime" < ($2::date + INTERVAL '1 day') + s."startTime" >= ($1::timestamptz AT TIME ZONE 'UTC') + AND s."startTime" < (($2::timestamptz + INTERVAL '1 day') AT TIME ZONE 'UTC') GROUP BY t.team_alias ORDER BY total_spend DESC; """ - response = await prisma_client.db.query_raw(sql_query, start_date, end_date) + response = await prisma_client.db.query_raw( + sql_query, start_date_obj, end_date_obj + ) # get spend per tag for today sql_query = """ @@ -1487,7 +1500,7 @@ async def _get_spend_report_for_time_range( """ spend_per_tag = await prisma_client.db.query_raw( - sql_query, start_date, end_date + sql_query, start_date_obj, end_date_obj ) return response, spend_per_tag