---
title: 'Quick wins pipeline LLM : prompt, connexions, cache, event loop'
type: 'bugfix'
created: '2026-08-26'
status: 'draft'
context: []
---

<frozen-after-approval reason="human-owned intent — do not modify unless human renegotiates">

## Intent

**Problem:** Le pipeline de traduction LLM souffre de 4 défauts vérifiés : (1) dans les 3 providers LLM, un prompt personnalisé/glossaire **remplace** le prompt par défaut — la paire de langues et les règles métier disparaissent de la requête ; (2) chaque segment traduit ouvre une nouvelle connexion TCP+TLS (`requests.post` nu) ; (3) le cache de traduction Redis/LRU (`services/translation_cache.py`) est écrit et testé mais **jamais branché** — re-traduire un fichier re-facture 100 % des appels ; (4) 7 appels bloquants (SHA-256 de fichiers, DB, disque, sonde réseau Google) s'exécutent directement dans l'event loop et peuvent geler tout le serveur.

**Approach:** 4 correctifs chirurgicaux dans la couche providers (openai/minimax/deepseek) et le runner de jobs de `translate_routes.py`, sans changement d'interface publique ni des translators.

## Boundaries & Constraints

**Always:** L'interface `TranslationProvider` / `TranslationRequest` / `TranslationResponse` reste inchangée ; la paire de langues doit **toujours** figurer dans le system prompt ; le cache reste piloté par env (`REDIS_CACHE_TTL`, `LRU_CACHE_MAXSIZE`, `REDIS_URL`) ; tout échec du cache est silencieux (fallback LRU, puis poursuite sans cache) ; `from_cache=True` sur les réponses servies du cache.

**Ask First:** modifier `build_full_prompt` (services/glossary_service.py) ; modifier les schémas providers ; introduire un nouveau backend de cache.

**Never:** Pas de batch multi-segments LLM (chantier suivant) ; pas de refonte de la double couche providers legacy/nouvelle ; pas de modification des translators/ ; pas de nouvel endpoint ; pas de persistance des jobs.

## I/O & Edge-Case Matrix

| Scenario | Input / State | Expected Output / Behavior | Error Handling |
|----------|--------------|---------------------------|----------------|
| Prompt custom actif | glossaire et/ou prompt utilisateur | System prompt = prompt par défaut formaté (avec paire de langues) **+** contexte custom en complément | N/A |
| Segment déjà en cache | même texte + langues + provider + hash prompt | Réponse `from_cache=True`, 0 requête HTTP | N/A |
| Segment répété dans un même document | 2e occurrence après succès de la 1ʳᵉ | Servie par le cache (le batch est séquentiel) | N/A |
| Redis indisponible | `get_cache()` init ou `set()` en échec | Fallback LRU RAM puis non-bloquant ; traduction réussie quand même | Log warning, jamais d'exception |
| Provider en erreur | échec API après retries | Rien n'est écrit dans le cache | Erreur existante propagée |
| Fichier 50 Mo + glossaire configuré | job lancé pendant que d'autres requêtes arrivent | SHA-256, zip-safety, DB, settings et sonde Google hors event loop (`asyncio.to_thread`) | Erreurs existantes propagées |

</frozen-after-approval>

## Code Map

- `services/providers/openai_provider.py` -- `_build_system_prompt` :120-128 (bug du remplacement), `requests.post` :263, `translate_text` :426-455 (point d'entrée cache)
- `services/providers/minimax_provider.py` -- prompt inline :181-183, `requests.post` :117, `translate_text` :168+
- `services/providers/deepseek_provider.py` -- prompt inline :164-166, `requests.post` :111, `translate_text` :151+
- `services/translation_cache.py` -- API prête et testée : `make_cache_key` :45, `hash_prompt` :77, `get_cache()` :402 (Redis auto + fallback LRU) — aucun appelant hors tests
- `routes/translate_routes.py` -- appels bloquants dans coroutines : `calculate_sha256` :858/:871, `validate_zip_safety` :884, `_load_admin_settings` :1174, `get_glossary_terms` :1191, `get_prompt_content` :1206, `_google_cloud_key_valid` :1242 (fait une traduction HTTP de test synchrone)
- `tests/test_providers/` -- 8 fichiers existants, dont `test_minimax_provider.py` (récent) : patterns de mock à réutiliser

## Tasks & Acceptance

**Execution:**
- [ ] `services/providers/openai_provider.py` -- (1) `_build_system_prompt` : concaténer le prompt par défaut formaté + le custom au lieu de `return custom_prompt` ; (2) `requests.Session` d'instance réutilisée dans `_make_api_request` ; (3) `translate_text` : lookup `get_cache()` + `make_cache_key(..., custom_prompt_hash=hash_prompt(custom_prompt))` avant l'appel, `cache.set` après succès -- corrige le bug qualité, la latence et le coût
- [ ] `services/providers/minimax_provider.py` -- mêmes 3 changements (prompt construit inline :181-183)
- [ ] `services/providers/deepseek_provider.py` -- mêmes 3 changements (prompt construit inline :164-166)
- [ ] `routes/translate_routes.py` -- envelopper les 7 appels bloquants listés dans la Code Map dans `asyncio.to_thread` -- l'event loop reste réactif pendant les jobs
- [ ] `tests/test_providers/` -- nouveaux tests couvrant la matrice I/O : le system prompt contient la paire de langues **et** le custom (×3 providers) ; cache hit → 0 appel HTTP et `from_cache=True` ; erreur provider → rien mis en cache ; Redis down → traduction quand même

**Acceptance Criteria:**
- Given un custom_prompt sans mention de langue, when traduction via openai/minimax/deepseek, then le payload contient « from {source} to {target} » et le contenu custom.
- Given un segment déjà en cache (même clé), when re-traduit, then aucune requête HTTP n'est émise vers l'API LLM.
- Given Redis down, when traduction, then réponse normale via LRU ou sans cache, sans exception remontée au job.
- Given la suite de tests existante, when `pytest -x`, then 0 régression.

## Spec Change Log

## Design Notes

Concaténation du prompt (les 3 providers) :

```python
system_prompt = DEFAULT_TRANSLATION_PROMPT.format(
    source_lang=source_lang_name, target_lang=target_lang_name
)
if custom_prompt:
    system_prompt += (
        "\n\nAdditional context and instructions from the user "
        f"(comply without overriding the language pair above):\n{custom_prompt}"
    )
```

La `requests.Session` est créée dans `__init__` du provider (le pooling urllib3 est thread-safe ; les translators appellent `translate_text` depuis 6 threads). Clé de cache : réutiliser `make_cache_key(text, target_language, source_language, self._provider_name, custom_prompt_hash=hash_prompt(custom_prompt))` — le hash du prompt isole déjà les glossaires différents ; `user_id` absent des metadata aujourd'hui → valeur par défaut « anon » (partage inter-utilisateurs acceptable : mémoire de traduction standard, TTL 24 h).

## Verification

**Commands:**
- `pytest tests/test_providers/ -x` -- expected: succès, nouveaux tests inclus
- `pytest -x` -- expected: succès complet, 0 régression
