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 ...@@ -55,7 +55,7 @@ from .auth.qwen import QwenOAuth2
from .handlers import RequestHandler, RotationHandler, AutoselectHandler 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 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__ = [ __all__ = [
# Config # Config
"config", "config",
......
...@@ -112,6 +112,7 @@ class ProviderConfig(BaseModel): ...@@ -112,6 +112,7 @@ class ProviderConfig(BaseModel):
# Per-provider client rate limit: a global cap (all clients combined) on # Per-provider client rate limit: a global cap (all clients combined) on
# requests forwarded to this provider, protecting the upstream from hammering # requests forwarded to this provider, protecting the upstream from hammering
# (e.g. exhausting a per-IP free-model quota). 0 = unlimited. # (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_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 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 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 ...@@ -196,24 +196,20 @@ def _provider_rl_check(bucket: str, window_seconds: int, max_requests: int) -> t
def _provider_rl_limits(provider_config) -> tuple: 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: if provider_config is None:
return 0, 0 return 0, 0, 0
if isinstance(provider_config, dict): if isinstance(provider_config, dict):
rpm = provider_config.get('client_rate_limit_rpm', 0) get = provider_config.get
rph = provider_config.get('client_rate_limit_rph', 0)
else: else:
rpm = getattr(provider_config, 'client_rate_limit_rpm', 0) get = lambda k, d=0: getattr(provider_config, k, d)
rph = getattr(provider_config, 'client_rate_limit_rph', 0) out = []
try: for key in ('client_rate_limit_rps', 'client_rate_limit_rpm', 'client_rate_limit_rph'):
rpm = int(rpm or 0) try:
except (TypeError, ValueError): out.append(int(get(key, 0) or 0))
rpm = 0 except (TypeError, ValueError):
try: out.append(0)
rph = int(rph or 0) return tuple(out)
except (TypeError, ValueError):
rph = 0
return rpm, rph
def _enforce_provider_client_rate_limit(provider_id: str, provider_config) -> None: 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 ...@@ -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 the provider's upstream quota. 0 = unlimited (the default), so providers
without a configured limit are unaffected. without a configured limit are unaffected.
""" """
rpm, rph = _provider_rl_limits(provider_config) rps, rpm, rph = _provider_rl_limits(provider_config)
for window, limit, label in ((60, rpm, "per-minute"), (3600, rph, "per-hour")): 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) allowed, retry_after = _provider_rl_check(f"provider:{provider_id}:{window}", window, limit)
if not allowed: if not allowed:
logging.getLogger(__name__).warning( logging.getLogger(__name__).warning(
......
...@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" ...@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "aisbf" 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" description = "AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations"
readme = "README.md" readme = "README.md"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
......
...@@ -106,7 +106,7 @@ class InstallCommand(_install): ...@@ -106,7 +106,7 @@ class InstallCommand(_install):
setup( setup(
name="aisbf", name="aisbf",
version="0.99.95", version="0.99.96",
author="AISBF Contributors", author="AISBF Contributors",
author_email="stefy@nexlab.net", 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", 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) { ...@@ -1810,6 +1810,10 @@ function renderProviderDetails(key) {
${subPanel(`providers:${key}:ratelimit`, window.i18n.t('providers.rate_limit_section') || 'Rate Limiting', ` ${subPanel(`providers:${key}:ratelimit`, window.i18n.t('providers.rate_limit_section') || 'Rate Limiting', `
<div style="border-left: 3px solid #4a9eff; padding-left: 12px;"> <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"> <div class="form-group">
<label>${window.i18n.t('providers.client_rate_limit_rpm') || 'Max client requests per minute'} (0 = unlimited)</label> <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)"> <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) { ...@@ -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;"> <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> <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"> <div class="form-group">
<label>${window.i18n.t('providers.client_rate_limit_rpm') || 'Max client requests per minute'} (0 = unlimited)</label> <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)"> <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