Add rate limiting (slowapi) to control-plane endpoints: admin, broker, webhook, /v1/models
This commit is contained in:
1 parent
505f3d9f48
commit
1312df4131
2 files changed
+32
-1
No files matched your search
+31
-1
@@ -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.
|
||||
|
||||
|
||||
Reference in new issue
Block a user