Add rate limiting (slowapi) to control-plane endpoints: admin, broker, webhook, /v1/models

This commit is contained in:
inference-bot committed 2026-09-14 20:58:13 -06:00
1 parent 505f3d9f48
commit 1312df4131
2 files changed
+32 -1

No files matched your search

+31 -1
View File
@@ -27,12 +27,31 @@ import httpx
import jwt
from fastapi import FastAPI, Request, Response, HTTPException
from fastapi.responses import JSONResponse, StreamingResponse
from slowapi import Limiter
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("member-portal")
app = FastAPI(title="Inference Cooperative Member Portal")
# --- Rate limiting (control plane only, not the member inference path) ---
# The data plane (chat completions via /v1/*) is capped by each member's
# LiteLLM budget, NOT by rate — a member streaming a long conversation must
# never be throttled. Rate limiting here covers the control-plane surfaces:
# admin endpoints, the broker, the webhook, and the unauthenticated model list.
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
@app.exception_handler(RateLimitExceeded)
async def _rate_limit_handler(request: Request, exc: RateLimitExceeded):
return JSONResponse(
status_code=429,
content={"detail": "Too many requests. Slow down."},
)
# --- Configuration (from environment) ---
CLOUDRON_API = os.environ.get("CLOUDRON_API_ORIGIN", "https://my.inference.coop")
CLOUDRON_TOKEN = os.environ.get("CLOUDRON_TOKEN", "")
@@ -780,6 +799,7 @@ async def broker_revoke_api_key(email: str, name: str) -> None:
# --- A. Open Collective webhook ---
@app.post("/webhook/opencollective/{token}")
@limiter.limit("30/minute")
async def opencollective_webhook(request: Request, token: str):
"""Handle Open Collective membership events.
@@ -854,7 +874,8 @@ async def opencollective_webhook(request: Request, token: str):
# --- B. Key injector (proxy) ---
@app.get("/v1/models")
async def list_models():
@limiter.limit("60/minute")
async def list_models(request: Request):
"""List models (same for everyone — no per-user auth needed).
Annotates each model with OpenWebUI metadata so web search is enabled by
@@ -971,6 +992,7 @@ async def health():
@app.post("/admin/sync-loomio")
@limiter.limit("20/minute")
async def admin_sync_loomio(request: Request):
"""Manually trigger a Loomio membership sync.
@@ -985,6 +1007,7 @@ async def admin_sync_loomio(request: Request):
@app.post("/admin/provision")
@limiter.limit("20/minute")
async def admin_provision(request: Request):
"""Manually provision a member by email (full pipeline).
@@ -1005,6 +1028,7 @@ async def admin_provision(request: Request):
@app.post("/admin/set-balance")
@limiter.limit("20/minute")
async def admin_set_balance(request: Request):
"""Set (or add to) a member's credit balance and sync it to LiteLLM.
@@ -1039,6 +1063,7 @@ async def admin_set_balance(request: Request):
@app.get("/admin/overview")
@limiter.limit("30/minute")
async def admin_overview(request: Request):
"""Return a co-op-wide overview for the admin dashboard.
@@ -1184,12 +1209,14 @@ def resolve_member_email(identity: str) -> str:
@app.get("/broker/keys")
@limiter.limit("30/minute")
async def broker_list(request: Request):
email = _verify_broker_secret(request)
return {"keys": await broker_list_api_keys(email)}
@app.post("/broker/keys")
@limiter.limit("30/minute")
async def broker_create(request: Request):
email = _verify_broker_secret(request)
body = await request.json()
@@ -1199,6 +1226,7 @@ async def broker_create(request: Request):
@app.delete("/broker/keys/{name}")
@limiter.limit("30/minute")
async def broker_revoke(name: str, request: Request):
email = _verify_broker_secret(request)
await broker_revoke_api_key(email, name)
@@ -1276,6 +1304,7 @@ async def broker_get_usage(email: str) -> dict:
@app.get("/broker/usage")
@limiter.limit("60/minute")
async def broker_usage(request: Request):
email = _verify_broker_secret(request)
return await broker_get_usage(email)
@@ -1346,6 +1375,7 @@ async def reconcile_memberships() -> dict:
@app.post("/admin/sweep")
@limiter.limit("20/minute")
async def admin_sweep(request: Request):
"""Manually trigger a full membership reconciliation.
+1
View File
@@ -3,3 +3,4 @@ uvicorn[standard]==0.30.6
httpx==0.27.2
pydantic==2.9.2
PyJWT==2.9.0
slowapi==0.1.9