Skip to content

Commit 478aedf

Browse files
authored
Add rate limits to assistant chat messages (#3514)
1 parent b9a6e99 commit 478aedf

6 files changed

Lines changed: 78 additions & 10 deletions

File tree

inference/server/main.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
import asyncio
2+
import math
23
import signal
34
import sys
45

56
import fastapi
7+
import redis.asyncio as redis
68
import sqlmodel
79
from fastapi.middleware.cors import CORSMiddleware
10+
from fastapi_limiter import FastAPILimiter
811
from loguru import logger
912
from oasst_inference_server import database, deps, models, plugins
1013
from oasst_inference_server.routes import account, admin, auth, chats, configs, workers
1114
from oasst_inference_server.settings import settings
1215
from oasst_shared.schemas import inference
1316
from prometheus_fastapi_instrumentator import Instrumentator
1417
from starlette.middleware.sessions import SessionMiddleware
18+
from starlette.status import HTTP_429_TOO_MANY_REQUESTS
1519

1620
app = fastapi.FastAPI(title=settings.PROJECT_NAME)
1721

@@ -81,6 +85,27 @@ async def alembic_upgrade():
8185
signal.signal(signal.SIGINT, signal.SIG_DFL)
8286

8387

88+
@app.on_event("startup")
89+
async def setup_rate_limiter():
90+
if not settings.rate_limit:
91+
logger.warning("Skipping rate limiter setup on startup (rate_limit is False)")
92+
return
93+
94+
async def http_callback(request: fastapi.Request, response: fastapi.Response, pexpire: int):
95+
"""Error callback function when too many requests"""
96+
expire = math.ceil(pexpire / 1000)
97+
raise fastapi.HTTPException(f"Too Many Requests. Retry After {expire} seconds.", HTTP_429_TOO_MANY_REQUESTS)
98+
99+
try:
100+
client = redis.Redis(
101+
host=settings.redis_host, port=settings.redis_port, db=settings.redis_ratelim_db, decode_responses=True
102+
)
103+
logger.info(f"Connected to {client=}")
104+
await FastAPILimiter.init(client, http_callback=http_callback)
105+
except Exception:
106+
logger.exception("Failed to establish Redis connection")
107+
108+
84109
@app.on_event("startup")
85110
async def maybe_add_debug_api_keys():
86111
debug_api_keys = settings.debug_api_keys_list

inference/server/oasst_inference_server/auth.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,14 @@
2323
trusted_client_scheme = APIKeyHeader(name="TrustedClient", auto_error=False, scheme_name="TrustedClient")
2424

2525

26-
def get_current_user_id(
27-
token: str = Security(authorization_scheme), trusted_client_token: str = Security(trusted_client_scheme)
28-
) -> str:
29-
"""Get the current user ID by decoding the JWT token."""
30-
if trusted_client_token is not None:
31-
info: auth.TrustedClient = auth.TrustedClientToken(content=trusted_client_token).content
32-
if info.api_key not in settings.trusted_api_keys_list:
33-
raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Unauthorized client")
34-
return info.user_id
26+
def get_user_id_from_trusted_client_token(trusted_client_token: str) -> str:
27+
info: auth.TrustedClient = auth.TrustedClientToken(content=trusted_client_token).content
28+
if info.api_key not in settings.trusted_api_keys_list:
29+
raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Unauthorized client")
30+
return info.user_id
31+
3532

33+
def get_user_id_from_auth_token(token: str) -> str:
3634
if token is None or not token.startswith("Bearer "):
3735
logger.warning(f"Invalid token: {token}")
3836
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Not authenticated")
@@ -56,6 +54,16 @@ def get_current_user_id(
5654
return user_id
5755

5856

57+
def get_current_user_id(
58+
token: str = Security(authorization_scheme), trusted_client_token: str = Security(trusted_client_scheme)
59+
) -> str:
60+
"""Get the current user ID."""
61+
if trusted_client_token is not None:
62+
return get_user_id_from_trusted_client_token(trusted_client_token)
63+
64+
return get_user_id_from_auth_token(token)
65+
66+
5967
def create_access_token(user_id: str) -> str:
6068
"""Create encoded JSON Web Token (JWT) for the given user ID."""
6169
payload: bytes = build_payload(user_id, "access", settings.auth_access_token_expire_minutes)

inference/server/oasst_inference_server/deps.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import contextlib
22

3+
import fastapi
34
import redis.asyncio as redis
45
from fastapi import Depends
6+
from fastapi_limiter.depends import RateLimiter
57
from oasst_inference_server import auth
68
from oasst_inference_server.chat_repository import ChatRepository
79
from oasst_inference_server.database import AsyncSession, get_async_session
@@ -54,3 +56,20 @@ async def manual_chat_repository():
5456
async def manual_user_chat_repository(user_id: str):
5557
async with manual_create_session() as session:
5658
yield await create_user_chat_repository(session, user_id)
59+
60+
61+
async def user_identifier(request: fastapi.Request) -> str:
62+
"""Identify a request by user based on auth header"""
63+
trusted_client_token = request.headers.get("TrustedClient")
64+
if trusted_client_token is not None:
65+
return auth.get_user_id_from_trusted_client_token(trusted_client_token)
66+
67+
token = request.headers.get("Authorization")
68+
return auth.get_user_id_from_auth_token(token)
69+
70+
71+
class UserRateLimiter(RateLimiter):
72+
def __init__(
73+
self, times: int = 100, milliseconds: int = 0, seconds: int = 0, minutes: int = 1, hours: int = 0
74+
) -> None:
75+
super().__init__(times, milliseconds, seconds, minutes, hours, user_identifier)

inference/server/oasst_inference_server/routes/chats.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,17 @@ async def create_prompter_message(
117117
return fastapi.Response(status_code=500)
118118

119119

120-
@router.post("/{chat_id}/assistant_message")
120+
@router.post(
121+
"/{chat_id}/assistant_message",
122+
dependencies=[
123+
Depends(
124+
deps.UserRateLimiter(
125+
times=settings.rate_limit_messages_user_times,
126+
seconds=settings.rate_limit_messages_user_seconds,
127+
)
128+
),
129+
],
130+
)
121131
async def create_assistant_message(
122132
chat_id: str,
123133
request: chat_schema.CreateAssistantMessageRequest,

inference/server/oasst_inference_server/settings.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,18 @@ class Settings(pydantic.BaseSettings):
1414
redis_host: str = "localhost"
1515
redis_port: int = 6379
1616
redis_db: int = 0
17+
redis_ratelim_db: int = 1
1718

1819
message_queue_expire: int = 60
1920
work_queue_max_size: int | None = None
2021

2122
chat_max_messages: int | None = None
2223
message_max_length: int | None = None
2324

25+
rate_limit: bool = True
26+
rate_limit_messages_user_times: int = 20
27+
rate_limit_messages_user_seconds: int = 600
28+
2429
allowed_worker_compat_hashes: str = "*"
2530

2631
@property

inference/server/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ asyncpg
44
authlib
55
beautifulsoup4 # web_retriever plugin
66
cryptography==39.0.0
7+
fastapi-limiter
78
fastapi[all]==0.88.0
89
google-api-python-client
910
google-auth-httplib2

0 commit comments

Comments
 (0)