fix(proxy/auth): handle tz-aware temp_budget_expiry

/key/update accepts ISO 8601 with timezone (e.g. "2026-01-20T00:00:00Z").
Pydantic parses it to a tz-aware datetime; prepare_metadata_fields stores
v.isoformat() which preserves the +00:00 offset. On the next request
_get_temp_budget_increase compared the parsed value against a naive
datetime.now() and raised TypeError, bricking the key.

Normalize a naive expiry to UTC and compare against datetime.now(timezone.utc).
This commit is contained in:
Akhilesh Arora 2026-05-21 18:00:01 +02:00
parent 697a90ea77
commit ca140b60d3
No known key found for this signature in database
GPG key ID: 564A6143F12D4670
2 changed files with 35 additions and 1 deletions

View file

@ -2318,7 +2318,9 @@ def _get_temp_budget_increase(valid_token: UserAPIKeyAuth):
and "temp_budget_expiry" in valid_token_metadata
):
expiry = datetime.fromisoformat(valid_token_metadata["temp_budget_expiry"])
if expiry > datetime.now():
if expiry.tzinfo is None:
expiry = expiry.replace(tzinfo=timezone.utc)
if expiry > datetime.now(timezone.utc):
return valid_token_metadata["temp_budget_increase"]
return None

View file

@ -3457,3 +3457,35 @@ async def test_user_api_key_auth_does_not_overwrite_end_user_id_set_by_builder()
finally:
for k, v in originals.items():
setattr(_proxy_server_mod, k, v)
def test_get_temp_budget_increase_tz_aware_expiry():
"""
/key/update accepts ISO 8601 with timezone (e.g. "2026-01-20T00:00:00Z");
Pydantic parses it to a tz-aware datetime and prepare_metadata_fields
stores .isoformat() -> "2026-01-20T00:00:00+00:00". The comparison must
not raise TypeError on the next request. Also covers the naive-input
path (legacy metadata written without timezone).
"""
from datetime import datetime, timedelta, timezone
from litellm.proxy.auth.user_api_key_auth import _get_temp_budget_increase
future_aware = (datetime.now(timezone.utc) + timedelta(days=1)).isoformat()
past_aware = (datetime.now(timezone.utc) - timedelta(days=1)).isoformat()
future_naive = (datetime.now() + timedelta(days=1)).isoformat()
for expiry, expected in [
(future_aware, 100), # tz-aware future
(past_aware, None), # tz-aware past
(future_naive, 100), # naive future, exercises the `tzinfo is None` branch
]:
token = UserAPIKeyAuth(
max_budget=100,
spend=0,
metadata={
"temp_budget_increase": 100,
"temp_budget_expiry": expiry,
},
)
assert _get_temp_budget_increase(token) == expected, expiry