Add per-second tier to per-provider client rate limit (v0.99.96)

Extends the per-provider limit with a per-second cap (client_rate_limit_rps)
alongside the existing per-minute/per-hour. A per-second bound is the most
effective defense against sub-second retry storms (the shape of the estate
app's hammering). Enforcement now checks 1s / 60s / 3600s windows; dashboard
provider forms gain a "per second" input. 0 = unlimited on every tier.

kilo-spora configured to rps=2, rph=200 to cap its load on kilo's per-IP free
quota (the cascade that was 429-ing kilo-stefy / breaking the lisa rotation).
Co-Authored-By: 's avatarClaude Opus 4.8 (1M context) <noreply@anthropic.com>
parent 979a1320
......@@ -55,7 +55,7 @@ from .auth.qwen import QwenOAuth2
from .handlers import RequestHandler, RotationHandler, AutoselectHandler
from .utils import count_messages_tokens, split_messages_into_chunks, get_max_request_tokens_for_model, get_max_completion_tokens_for_model
__version__ = "0.99.95"
__version__ = "0.99.96"
__all__ = [
# Config
"config",
......
......@@ -112,6 +112,7 @@ class ProviderConfig(BaseModel):
# Per-provider client rate limit: a global cap (all clients combined) on
# requests forwarded to this provider, protecting the upstream from hammering
# (e.g. exhausting a per-IP free-model quota). 0 = unlimited.
client_rate_limit_rps: int = 0 # max client requests per second to this provider
client_rate_limit_rpm: int = 0 # max client requests per minute to this provider
client_rate_limit_rph: int = 0 # max client requests per hour to this provider
api_key: Optional[str] = None # Optional API key in provider config
......
......@@ -196,24 +196,20 @@ def _provider_rl_check(bucket: str, window_seconds: int, max_requests: int) -> t
def _provider_rl_limits(provider_config) -> tuple:
"""Extract (rpm, rph) client rate limits from a provider config (object or dict)."""
"""Extract (rps, rpm, rph) client rate limits from a provider config (object or dict)."""
if provider_config is None:
return 0, 0
return 0, 0, 0
if isinstance(provider_config, dict):
rpm = provider_config.get('client_rate_limit_rpm', 0)
rph = provider_config.get('client_rate_limit_rph', 0)
get = provider_config.get
else:
rpm = getattr(provider_config, 'client_rate_limit_rpm', 0)
rph = getattr(provider_config, 'client_rate_limit_rph', 0)
try:
rpm = int(rpm or 0)
except (TypeError, ValueError):
rpm = 0
try:
rph = int(rph or 0)
except (TypeError, ValueError):
rph = 0
return rpm, rph
get = lambda k, d=0: getattr(provider_config, k, d)
out = []
for key in ('client_rate_limit_rps', 'client_rate_limit_rpm', 'client_rate_limit_rph'):
try:
out.append(int(get(key, 0) or 0))
except (TypeError, ValueError):
out.append(0)
return tuple(out)
def _enforce_provider_client_rate_limit(provider_id: str, provider_config) -> None:
......@@ -224,8 +220,8 @@ def _enforce_provider_client_rate_limit(provider_id: str, provider_config) -> No
the provider's upstream quota. 0 = unlimited (the default), so providers
without a configured limit are unaffected.
"""
rpm, rph = _provider_rl_limits(provider_config)
for window, limit, label in ((60, rpm, "per-minute"), (3600, rph, "per-hour")):
rps, rpm, rph = _provider_rl_limits(provider_config)
for window, limit, label in ((1, rps, "per-second"), (60, rpm, "per-minute"), (3600, rph, "per-hour")):
allowed, retry_after = _provider_rl_check(f"provider:{provider_id}:{window}", window, limit)
if not allowed:
logging.getLogger(__name__).warning(
......
......@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "aisbf"
version = "0.99.95"
version = "0.99.96"
description = "AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations"
readme = "README.md"
license = "GPL-3.0-or-later"
......
......@@ -106,7 +106,7 @@ class InstallCommand(_install):
setup(
name="aisbf",
version="0.99.95",
version="0.99.96",
author="AISBF Contributors",
author_email="stefy@nexlab.net",
description="AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations",
......
......@@ -1810,6 +1810,10 @@ function renderProviderDetails(key) {
${subPanel(`providers:${key}:ratelimit`, window.i18n.t('providers.rate_limit_section') || 'Rate Limiting', `
<div style="border-left: 3px solid #4a9eff; padding-left: 12px;">
<div class="form-group">
<label>${window.i18n.t('providers.client_rate_limit_rps') || 'Max client requests per second'} (0 = unlimited)</label>
<input type="number" min="0" value="${provider.client_rate_limit_rps || 0}" onchange="updateProvider('${key}', 'client_rate_limit_rps', this.value ? parseInt(this.value) : 0)">
</div>
<div class="form-group">
<label>${window.i18n.t('providers.client_rate_limit_rpm') || 'Max client requests per minute'} (0 = unlimited)</label>
<input type="number" min="0" value="${provider.client_rate_limit_rpm || 0}" onchange="updateProvider('${key}', 'client_rate_limit_rpm', this.value ? parseInt(this.value) : 0)">
......
......@@ -1492,6 +1492,10 @@ function renderProviderDetails(key) {
<div style="background: var(--bg-panel); padding: 15px; border-radius: 5px; margin: 15px 0; border-left: 3px solid #4a9eff;">
<h4 style="margin: 0 0 15px 0; color: var(--color-link);">${window.i18n.t('providers.rate_limit_section') || 'Rate Limiting'}</h4>
<div class="form-group">
<label>${window.i18n.t('providers.client_rate_limit_rps') || 'Max client requests per second'} (0 = unlimited)</label>
<input type="number" min="0" value="${provider.client_rate_limit_rps || 0}" onchange="updateProvider('${key}', 'client_rate_limit_rps', this.value ? parseInt(this.value) : 0)">
</div>
<div class="form-group">
<label>${window.i18n.t('providers.client_rate_limit_rpm') || 'Max client requests per minute'} (0 = unlimited)</label>
<input type="number" min="0" value="${provider.client_rate_limit_rpm || 0}" onchange="updateProvider('${key}', 'client_rate_limit_rpm', this.value ? parseInt(this.value) : 0)">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment