diff --git a/stt/server/pyproject.toml b/stt/server/pyproject.toml index 86202f7..2e46234 100644 --- a/stt/server/pyproject.toml +++ b/stt/server/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "stt-server" -version = "0.2.0" +version = "0.2.1" description = "STT-server — orchestrateur AI du homelab Funk (API pour les clients STT)" requires-python = ">=3.11" diff --git a/stt/server/stt_server/__init__.py b/stt/server/stt_server/__init__.py index fe833f3..c2022bd 100644 --- a/stt/server/stt_server/__init__.py +++ b/stt/server/stt_server/__init__.py @@ -1,3 +1,3 @@ """STT-server — orchestrateur AI in-cluster pour les clients STT.""" -__version__ = "0.2.0" +__version__ = "0.2.1" diff --git a/stt/server/stt_server/app.py b/stt/server/stt_server/app.py index f3fa7e9..aa81777 100644 --- a/stt/server/stt_server/app.py +++ b/stt/server/stt_server/app.py @@ -23,6 +23,14 @@ from stt_server.longterm import LongTermMemory from stt_server.memory import SessionStore log = logging.getLogger("stt_server") +# uvicorn ne configure que ses propres loggers : on attache notre handler en INFO +# pour que les lignes de timing (`ask … recall/gen/total`) sortent dans les logs du pod. +if not log.handlers: + log.setLevel(logging.INFO) + _h = logging.StreamHandler() + _h.setFormatter(logging.Formatter("%(levelname)s: %(name)s: %(message)s")) + log.addHandler(_h) + log.propagate = False sessions = SessionStore() longterm = LongTermMemory() if settings.memory_longterm else None diff --git a/stt/server/stt_server/brain.py b/stt/server/stt_server/brain.py index 8fdfe6f..570abf0 100644 --- a/stt/server/stt_server/brain.py +++ b/stt/server/stt_server/brain.py @@ -59,8 +59,25 @@ async def ask( "temperature": settings.temperature, } headers = {"Authorization": f"Bearer {settings.litellm_key}"} - r = await _get_client().post(settings.litellm_url, json=payload, headers=headers) - r.raise_for_status() + # Résilience : la chaîne LiteLLM→llama-server (ROCm) se fige par à-coups. Une 2ᵉ tentative + # récupère un blip transitoire (ex. connexion keep-alive périmée) au lieu de renvoyer 502. + # Timeout PAR tentative (`request_timeout`) → on ne dépasse pas ~2× ce budget au pire. + last_exc: Exception | None = None + for attempt in range(2): + try: + r = await _get_client().post( + settings.litellm_url, + json=payload, + headers=headers, + timeout=settings.request_timeout, + ) + r.raise_for_status() + break + except httpx.HTTPError as e: + last_exc = e + await aclose() # repart sur une connexion neuve (pool potentiellement vicié) + else: + raise last_exc # type: ignore[misc] msg = r.json()["choices"][0]["message"] # Filet de sécurité : si un modèle « thinking » renvoie un content vide (tout parti # en reasoning_content), on récupère le raisonnement plutôt que de renvoyer "". diff --git a/stt/server/stt_server/config.py b/stt/server/stt_server/config.py index e6ed3b6..7ebee70 100644 --- a/stt/server/stt_server/config.py +++ b/stt/server/stt_server/config.py @@ -30,7 +30,9 @@ class Settings: ) max_tokens: int = int(os.getenv("STT_MAX_TOKENS", "200")) temperature: float = float(os.getenv("STT_TEMPERATURE", "0.7")) - request_timeout: float = float(os.getenv("STT_REQUEST_TIMEOUT", "60")) + # Timeout PAR tentative vers LiteLLM (brain fait jusqu'à 2 tentatives). Une réponse vocale + # avec /no_think tient en quelques secondes → 30s suffit, et borne l'attente d'un blip backend. + request_timeout: float = float(os.getenv("STT_REQUEST_TIMEOUT", "30")) # Qwen3 est un modèle « thinking » → ajoute `/no_think` pour désactiver le raisonnement # (sinon content vide / timeout). Inoffensif pour les modèles non-Qwen. disable_thinking: bool = os.getenv("STT_DISABLE_THINKING", "true").lower() == "true"