diff --git a/litellm/types/router.py b/litellm/types/router.py index d8b2e457706..5c693354629 100644 --- a/litellm/types/router.py +++ b/litellm/types/router.py @@ -172,6 +172,11 @@ class TagRateLimitEntry(BaseModel): def _validate_key_ttl_seconds(self) -> "TagRateLimitEntry": if self.key_ttl_seconds is not None and self.key_ttl_seconds <= 0: raise ValueError("key_ttl_seconds must be a positive integer when set") + if self.key_ttl_seconds is not None and self.key_ttl_seconds < self.period_seconds: + raise ValueError( + "key_ttl_seconds must be at least period_seconds when set -- a shorter TTL expires the " + "counter before its window rolls over, letting tagged traffic reset to zero and exceed the limit" + ) return self @model_validator(mode="after") diff --git a/tests/test_litellm/proxy/hooks/test_tag_rate_limiter.py b/tests/test_litellm/proxy/hooks/test_tag_rate_limiter.py index 6617e0f9c45..dd547d88a4b 100644 --- a/tests/test_litellm/proxy/hooks/test_tag_rate_limiter.py +++ b/tests/test_litellm/proxy/hooks/test_tag_rate_limiter.py @@ -2461,7 +2461,7 @@ def test_ttl_for_concurrency_never_drops_below_the_safety_floor_even_with_a_lowe """ below_floor: Final = 10 assert ( - _PROXY_TagRateLimiter._ttl_for(_concurrency_limit(period_seconds=60, key_ttl_seconds=below_floor)) + _PROXY_TagRateLimiter._ttl_for(_concurrency_limit(period_seconds=5, key_ttl_seconds=below_floor)) == _CONCURRENCY_MIN_SAFETY_TTL_SECONDS ) @@ -2471,6 +2471,17 @@ def test_tag_rate_limit_entry_rejects_non_positive_key_ttl_seconds(): TagRateLimitEntry(name="per_minute", limit=1, period_seconds=60, key_ttl_seconds=0) +def test_tag_rate_limit_entry_rejects_key_ttl_seconds_shorter_than_period_seconds(): + """ + Regression test for a real bug: a key_ttl_seconds shorter than + period_seconds expires the bucket key before its window rolls over, + resetting the counter to zero mid-window and letting tagged traffic + exceed the configured limit. + """ + with pytest.raises(ValueError): + TagRateLimitEntry(name="per_minute", limit=1, period_seconds=60, key_ttl_seconds=59) + + # --------------------------------------------------------------------------- # per-tag max_in_memory_cache_size override -- dedicated cache partitions # ---------------------------------------------------------------------------