feat: rate limiting WS + cache risposte LLM
test / test (push) Has been cancelled

- Rate limiting: 1.5s tra azioni dello stesso client WS
- Cache LLM: SHA256 key, TTL 5 min, max 100 entry
- Cache riduce token spesi su azioni identiche ripetute
This commit is contained in:
truetype committed 2026-07-01 11:29:05 +02:00
1 parent 893823fa1d
commit 9b2fd0ee92
2 files changed
+56 -1

No files matched your search

+35 -1
View File
@@ -1,14 +1,43 @@
from __future__ import annotations
import contextvars
import hashlib
import json
import re
import time
from src.engine.providers import LLMProvider, create_provider
_provider_var: contextvars.ContextVar[LLMProvider | None] = contextvars.ContextVar("provider", default=None)
# Cache risposte LLM: {cache_key: (timestamp, response)}
_cache: dict[str, tuple[float, str]] = {}
_CACHE_TTL = 300 # 5 minuti
def _cache_key(prompt: str, system: str) -> str:
return hashlib.sha256(f"{system}||{prompt}".encode()).hexdigest()
def _cache_get(prompt: str, system: str) -> str | None:
key = _cache_key(prompt, system)
entry = _cache.get(key)
if entry and time.monotonic() - entry[0] < _CACHE_TTL:
return entry[1]
if entry:
del _cache[key]
return None
def _cache_set(prompt: str, system: str, response: str) -> None:
key = _cache_key(prompt, system)
_cache[key] = (time.monotonic(), response)
# Mantieni cache sotto 100 entry
if len(_cache) > 100:
oldest = min(_cache.keys(), key=lambda k: _cache[k][0])
del _cache[oldest]
def get_provider() -> LLMProvider:
provider = _provider_var.get()
@@ -24,7 +53,12 @@ def set_provider(provider: LLMProvider) -> None:
def ask(prompt: str, system: str = "") -> str:
return get_provider().ask(prompt, system=system)
cached = _cache_get(prompt, system)
if cached is not None:
return cached
response = get_provider().ask(prompt, system=system)
_cache_set(prompt, system, response)
return response
def _try_repair_json(text: str) -> str | None:
+21
View File
@@ -4,6 +4,7 @@ import json
import asyncio
import logging
import os
import time
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
@@ -140,6 +141,19 @@ def add_character(
_executor = ThreadPoolExecutor(max_workers=4)
# Rate limiting: almeno 1.5s tra azioni dello stesso client
_last_action: dict[str, float] = {}
_RATE_LIMIT = 1.5
def _check_rate(client_id: str) -> str | None:
now = time.monotonic()
last = _last_action.get(client_id, 0)
if now - last < _RATE_LIMIT:
return f"Troppo veloce! Aspetta {_RATE_LIMIT - (now - last):.1f}s"
_last_action[client_id] = now
return None
@app.websocket("/ws/{campaign_id}")
async def game_ws(ws: WebSocket, campaign_id: int):
@@ -171,6 +185,13 @@ async def game_ws(ws: WebSocket, campaign_id: int):
if not player_action:
continue
# Rate limiting
if ws.client:
err = _check_rate(str(ws.client))
if err:
await ws.send_json({"type": "error", "message": err})
continue
try:
# dm_cycle è bloccante (chiamata HTTP al LLM) —
# la eseguiamo in un thread worker per non bloccare