mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-21 00:21:49 +00:00
Merge pull request #41911 from BerriAI/litellm_rate_limit_reset_time_utc
fix(rate_limiter): render the 429 reset time in UTC as labelled
This commit is contained in:
commit
15f63c33bf
4 changed files with 101 additions and 8 deletions
|
|
@ -19,7 +19,7 @@ Quick summary:
|
|||
|
||||
import json
|
||||
from collections.abc import Callable, Iterable, Mapping, Sequence
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal, NoReturn, TypeAlias
|
||||
|
||||
|
|
@ -661,7 +661,9 @@ class _PROXY_BatchRateLimiter(CustomLogger):
|
|||
) or self.parallel_request_limiter.window_size
|
||||
reset_time: Final = now + window_size if window_start is None else window_start + window_size
|
||||
retry_after: Final = max(0, int(reset_time - now))
|
||||
reset_time_formatted: Final = datetime.fromtimestamp(reset_time).strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||
reset_time_formatted: Final = datetime.fromtimestamp(reset_time, tz=timezone.utc).strftime(
|
||||
"%Y-%m-%d %H:%M:%S UTC"
|
||||
)
|
||||
|
||||
remaining_display: Final = max(0, status["limit_remaining"])
|
||||
current_limit: Final = status["current_limit"]
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from collections.abc import AsyncGenerator, Awaitable, Callable, Mapping, Sequen
|
|||
from contextlib import asynccontextmanager
|
||||
from contextvars import ContextVar
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from types import MappingProxyType
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
|
|
@ -3124,7 +3124,9 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
|
|||
|
||||
now = self._get_current_time().timestamp()
|
||||
reset_time = now + self.window_size
|
||||
reset_time_formatted = datetime.fromtimestamp(reset_time).strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||
reset_time_formatted = datetime.fromtimestamp(reset_time, tz=timezone.utc).strftime(
|
||||
"%Y-%m-%d %H:%M:%S UTC"
|
||||
)
|
||||
|
||||
remaining_display = max(0, status["limit_remaining"])
|
||||
rate_limit_type = status["rate_limit_type"]
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ batch under a per-minute RPM/TPM budget. Scopes that configure `tpd_limit`
|
|||
are charged against a 24h token window instead of their minute counters.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
import time
|
||||
from collections.abc import Iterator
|
||||
from datetime import datetime, timezone
|
||||
from typing import Final
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
|
@ -257,3 +260,39 @@ def test_online_descriptors_ignore_tpd_limit():
|
|||
model_has_failures=False,
|
||||
)
|
||||
assert [(d["key"], d["rate_limit"]["window_size"]) for d in descriptors] == [("api_key", rate_limiter.window_size)]
|
||||
|
||||
|
||||
@pytest.fixture(params=["Europe/Paris", "Asia/Kolkata", "America/Los_Angeles"])
|
||||
def process_timezone(request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch) -> Iterator[str]:
|
||||
monkeypatch.setenv("TZ", request.param)
|
||||
time.tzset()
|
||||
yield request.param
|
||||
monkeypatch.undo()
|
||||
time.tzset()
|
||||
|
||||
|
||||
@pytest.mark.skipif(not hasattr(time, "tzset"), reason="switching the process timezone needs time.tzset()")
|
||||
@pytest.mark.asyncio
|
||||
async def test_batch_rate_limit_error_reports_reset_time_in_utc_on_a_non_utc_proxy(process_timezone: str) -> None:
|
||||
window_start: Final = datetime(2026, 9, 13, 8, 0, 0, tzinfo=timezone.utc)
|
||||
clock: Final = _Clock(window_start)
|
||||
_internal_usage_cache, _rate_limiter, batch_limiter = _make_limiters(clock)
|
||||
user_api_key_dict: Final = UserAPIKeyAuth(api_key=hash_token("tpd-key-utc"), rpm_limit=1, tpd_limit=1000)
|
||||
|
||||
await batch_limiter._check_and_increment_batch_counters(
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
data={},
|
||||
batch_usage=BatchFileUsage(total_tokens=600, request_count=6),
|
||||
)
|
||||
clock.now = datetime(2026, 9, 13, 11, 0, 0, tzinfo=timezone.utc)
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await batch_limiter._check_and_increment_batch_counters(
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
data={},
|
||||
batch_usage=BatchFileUsage(total_tokens=600, request_count=6),
|
||||
)
|
||||
|
||||
assert exc.value.status_code == 429
|
||||
assert exc.value.headers["retry-after"] == str(BATCH_TPD_WINDOW_SECONDS - 3 * 3600)
|
||||
assert exc.value.headers["reset_at"] == "2026-09-14 08:00:00 UTC"
|
||||
assert str(exc.value.detail).endswith("Limit resets at: 2026-09-14 08:00:00 UTC")
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import logging
|
|||
import os
|
||||
import sys
|
||||
import time
|
||||
from collections.abc import Sequence
|
||||
from collections.abc import Iterator, Sequence
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict, List, Optional
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Dict, Final, List, Optional
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
|
@ -21,10 +21,12 @@ from litellm.caching.caching import DualCache
|
|||
from litellm.caching.in_memory_cache import InMemoryCache
|
||||
from litellm.constants import INTERNAL_CALL_ORIGIN_METADATA_KEY
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.proxy.common_utils.proxy_rate_limit_error import ProxyRateLimitError
|
||||
from litellm.proxy.hooks.parallel_request_limiter_v3 import (
|
||||
PARALLEL_REQUEST_SLOT_TTL_SECONDS,
|
||||
ParallelSlotAcquisition,
|
||||
RateLimitDescriptor,
|
||||
RateLimitResponse,
|
||||
RequestRateLimiterStash,
|
||||
_request_stash,
|
||||
get_or_create_request_stash,
|
||||
|
|
@ -6911,3 +6913,51 @@ def test_success_tpm_accounting_skips_team_model_pool_when_key_owns_model_tpm_li
|
|||
assert handler.create_rate_limit_keys("model_per_key", f"{hash_token('sk-pool')}:test-model", "tokens") in charged_keys
|
||||
team_pool_key = handler.create_rate_limit_keys("model_per_team", "t:test-model", "tokens")
|
||||
assert (team_pool_key in charged_keys) is charges_team_model_pool
|
||||
|
||||
|
||||
@pytest.fixture(params=["Europe/Paris", "Asia/Kolkata", "America/Los_Angeles"])
|
||||
def process_timezone(request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch) -> Iterator[str]:
|
||||
monkeypatch.setenv("TZ", request.param)
|
||||
time.tzset()
|
||||
yield request.param
|
||||
monkeypatch.undo()
|
||||
time.tzset()
|
||||
|
||||
|
||||
@pytest.mark.skipif(not hasattr(time, "tzset"), reason="switching the process timezone needs time.tzset()")
|
||||
def test_rate_limit_error_reports_reset_time_in_utc_on_a_non_utc_proxy(process_timezone: str) -> None:
|
||||
now: Final = datetime(2026, 9, 4, 21, 53, 21, tzinfo=timezone.utc)
|
||||
handler: Final = _PROXY_MaxParallelRequestsHandler(
|
||||
internal_usage_cache=InternalUsageCache(DualCache()), time_provider=lambda: now
|
||||
)
|
||||
expected_reset: Final = (now + timedelta(seconds=handler.window_size)).strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||
over_limit: Final[RateLimitResponse] = {
|
||||
"overall_code": "OVER_LIMIT",
|
||||
"statuses": [
|
||||
{
|
||||
"code": "OVER_LIMIT",
|
||||
"descriptor_key": "api_key",
|
||||
"limit_remaining": 0,
|
||||
"rate_limit_type": "requests",
|
||||
"current_limit": 2,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
with pytest.raises(ProxyRateLimitError) as exc_info:
|
||||
handler._handle_rate_limit_error(
|
||||
response=over_limit,
|
||||
descriptors=[{"key": "api_key", "value": "sk-test", "rate_limit": None}],
|
||||
requested_model="gpt-4o-mini",
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 429
|
||||
assert exc_info.value.headers == {
|
||||
"retry-after": str(handler.window_size),
|
||||
"rate_limit_type": "requests",
|
||||
"reset_at": expected_reset,
|
||||
}
|
||||
assert exc_info.value.detail == (
|
||||
"Rate limit exceeded for api_key: sk-test. Limit type: requests. "
|
||||
f"Current limit: 2, Remaining: 0. Limit resets at: {expected_reset}"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue