fix(spend): convert string dates to tz-aware UTC datetimes in _get_spend_report_for_time_range

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).
This commit is contained in:
Ryan Crabbe 2026-04-11 09:47:09 -07:00
parent c7934c460d
commit 423677f19e
No known key found for this signature in database

View file

@ -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