fix(stt-server): retry brain + timeout par tentative + log timing effectif (#21)

Suite au déploiement 0.2.0, deux points observés en pilotant l'app en direct :

- Le log de timing (`ask … recall/gen/total`) n'émettait pas : uvicorn ne
  configure que ses propres loggers → on attache un handler INFO sur `stt_server`.
- La chaîne LiteLLM→llama-server (ROCm gfx1031) se fige par à-coups (~1-2 min,
  502 pour tous les appels, cf. incidents-llm-gpu.md). Résilience côté STT :
  brain tente 2× (fermeture du client httpx entre les essais → connexion neuve si
  le pool est vicié) et le timeout passe à 30s PAR tentative (≤ ~2× au pire).
  Un blip transitoire devient une requête récupérée au lieu d'un 502.

bump serveur 0.2.0 → 0.2.1

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ALI YESILKAYA 2026-06-19 15:46:35 +02:00 committed by GitHub
parent ced79658cf
commit 681b94e268
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 32 additions and 5 deletions

View file

@ -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"

View file

@ -1,3 +1,3 @@
"""STT-server — orchestrateur AI in-cluster pour les clients STT."""
__version__ = "0.2.0"
__version__ = "0.2.1"

View file

@ -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

View file

@ -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 "".

View file

@ -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"