Lorsque vous implémentez le streaming en temps réel avec les modèles GPT-5, les interruptions de connexion représentent un défi technique majeur. Cet article détaille les stratégies professionnelles pour maintenir l'intégrité des données et gérer efficacement les déconnexions réseau.

Tableau Comparatif des Solutions API

Critère HolySheep AI API Officielle Services Relais
Prix GPT-4.1 ~¥0.42/MTok (économie 85%+) $8/MTok $3-5/MTok
Latence moyenne <50ms 150-300ms 80-200ms
Paiements WeChat/Alipay/USD Carte internationale Variable
Crédits gratuits ✅ Inclus ❌ Aucun ⚠️ Limité
Fiabilité streaming 99.95% 98.5% 95-97%

Comprendre les Interruptions de Streaming

En tant qu'ingénieur ayant déployé des solutions de streaming à grande échelle, j'ai rencontré de nombreux scénarios où une connexion instable ou un timeout réseau corrompait le flux de données. HolySheep AI offre une latence inférieure à 50ms qui réduit significativement les risques d'interruption sur les réseaux à latence variable.

Architecture de Reconnection Robuste

Pour garantir la continuité du streaming même lors d'interruptions réseau, implémentez un système de reconnexion intelligent avec gestion d'état côté client.

import asyncio
import json
from typing import AsyncIterator, Optional
import aiohttp
from dataclasses import dataclass
from enum import Enum

class StreamState(Enum):
    CONNECTED = "connected"
    RECONNECTING = "reconnecting"
    COMPLETED = "completed"
    ERROR = "error"

@dataclass
class StreamConfig:
    base_url: str = "https://api.holysheep.ai/v1"
    api_key: str = "YOUR_HOLYSHEEP_API_KEY"
    max_retries: int = 5
    retry_delay: float = 1.0
    timeout: int = 120

class StreamingManager:
    """Gestionnaire de streaming resilient avec reconnect automatique"""
    
    def __init__(self, config: StreamConfig):
        self.config = config
        self.state = StreamState.DISCONNECTED
        self.last_content_id: Optional[str] = None
        self.received_chunks: list[str] = []
    
    async def stream_completion(
        self, 
        prompt: str, 
        model: str = "gpt-5-turbo"
    ) -> AsyncIterator[str]:
        """Stream avec gestion d'interruption et reprise"""
        
        headers = {
            "Authorization": f"Bearer {self.config.api_key}",
            "Content-Type": "application/json"
        }
        
        payload = {
            "model": model,
            "messages": [{"role": "user", "content": prompt}],
            "stream": True,
            "temperature": 0.7
        }
        
        async with aiohttp.ClientSession() as session:
            for attempt in range(self.config.max_retries):
                try:
                    self.state = StreamState.CONNECTED
                    
                    async with session.post(
                        f"{self.config.base_url}/chat/completions",
                        headers=headers,
                        json=payload,
                        timeout=aiohttp.ClientTimeout(total=self.config.timeout)
                    ) as response:
                        
                        if response.status != 200:
                            error_body = await response.text()
                            raise ConnectionError(f"HTTP {response.status}: {error_body}")
                        
                        async for line in response.content:
                            line = line.decode('utf-8').strip()
                            
                            if not line or not line.startswith('data: '):
                                continue
                            
                            if line == 'data: [DONE]':
                                self.state = StreamState.COMPLETED
                                break
                            
                            data = json.loads(line[6:])
                            
                            if 'choices' in data and len(data['choices']) > 0:
                                delta = data['choices'][0].get('delta', {})
                                content = delta.get('content', '')
                                
                                if content:
                                    self.received_chunks.append(content)
                                    yield content
                                    
                except (aiohttp.ClientError, asyncio.TimeoutError) as e:
                    self.state = StreamState.RECONNECTING
                    delay = self.config.retry_delay * (2 ** attempt)
                    
                    print(f"Interruption détectée (tentative {attempt + 1}): {e}")
                    print(f"Reconnection dans {delay}s...")
                    
                    await asyncio.sleep(delay)
                    
                    # Préparation pour reprise avec continuation
                    if self.received_chunks:
                        context_so_far = "".join(self.received_chunks)
                        payload["messages"].append({
                            "role": "assistant", 
                            "content": context_so_far
                        })
                        payload["messages"].append({
                            "role": "user",
                            "content": "[Continuer la réponse précédente si applicable]"
                        })
            
            self.state = StreamState.ERROR
            raise RuntimeError(f"Échec après {self.config.max_retries} tentatives")

Validation et Intégrité des Données

Une interruption pendant le streaming peut créer des réponses incomplètes. Voici une stratégie de validation qui vérifie l'intégrité des données reçues et implémente une logique de reprise contextuelle sophistiquée.

import hashlib
import time
from typing import Dict, List, Optional
from dataclasses import dataclass, field

@dataclass
class StreamChunk:
    index: int
    content: str
    timestamp: float
    checksum: str
    
    @classmethod
    def create(cls, index: int, content: str) -> "StreamChunk":
        return cls(
            index=index,
            content=content,
            timestamp=time.time(),
            checksum=hashlib.sha256(content.encode()).hexdigest()[:16]
        )

class DataIntegrityValidator:
    """Valide l'intégrité des données de streaming"""
    
    def __init__(self):
        self.chunks: List[StreamChunk] = []
        self.expected_continuation: Optional[str] = None
        self.received_bytes: int = 0
    
    def add_chunk(self, content: str, force_index: Optional[int] = None) -> bool:
        """Ajoute un chunk avec validation de séquence"""
        
        index = force_index if force_index is not None else len(self.chunks)
        
        chunk = StreamChunk.create(index, content)
        self.chunks.append(chunk)
        self.received_bytes += len(content.encode('utf-8'))
        
        # Validation de l'ordre séquentiel
        if index > 0 and self.chunks:
            prev_chunk = self.chunks[index - 1]
            time_gap = chunk.timestamp - prev_chunk.timestamp
            
            # Alerte si gap temporel anormal (>5s)
            if time_gap > 5.0:
                print(f"⚠️ Gap temporel détecté: {time_gap}s entre chunks {index-1} et {index}")
                return False
        
        return True
    
    def get_complete_response(self) -> str:
        """Reconstruit la réponse complète"""
        return "".join(chunk.content for chunk in self.chunks)
    
    def detect_incomplete_response(self) -> Dict[str, any]:
        """Détecte si la réponse est potentiellement incomplète"""
        
        response = self.get_complete_response()
        
        # Marqueurs de fin de phrase incomplets
        incomplete_markers = ['et', 'ou', 'mais', 'car', 'donc', 'car']
        
        for marker in incomplete_markers:
            if response.endswith(marker + ' '):
                return {
                    "is_complete": False,
                    "reason": f"Fin avec marqueur incomplet: '{marker}'",
                    "last_chars": response[-50:]
                }
        
        # Vérifier les parenthèses/non fermé
        open_parens = response.count('(')
        close_parens = response.count(')')
        
        if open_parens > close_parens:
            return {
                "is_complete": False,
                "reason": f"Parentheses non fermees: {open_parens - close_parens}",
                "last_chars": response[-50:]
            }
        
        return {
            "is_complete": True,
            "total_chunks": len(self.chunks),
            "total_bytes": self.received_bytes
        }
    
    async def request_completion_resume(
        self, 
        api_manager: 'StreamingManager',
        original_prompt: str
    ) -> str:
        """Demande la completion de la réponse interrompue"""
        
        validation = self.detect_incomplete_response()
        
        if validation["is_complete"]:
            return self.get_complete_response()
        
        print(f"Résumption nécessaire: {validation['reason']}")
        
        # Construire le contexte pour continuation
        context = self.get_complete_response()
        
        resume_payload = {
            "model": "gpt-5-turbo",
            "messages": [
                {"role": "user", "content": original_prompt},
                {"role": "assistant", "content": context},
                {"role": "user", "content": "Veuillez compléter votre réponse précédente de manière naturelle."}
            ],
            "stream": True,
            "temperature": 0.7
        }
        
        async for chunk in api_manager.stream_completion(
            prompt="[CONTINUATION]", 
            model="gpt-5-turbo"
        ):
            self.add_chunk(chunk)
        
        return self.get_complete_response()

Exemple d'utilisation intégrée

async def main(): config = StreamConfig( base_url="https://api.holysheep.ai/v1", api_key="YOUR_HOLYSHEEP_API_KEY" ) manager = StreamingManager(config) validator = DataIntegrityValidator() original_prompt = "Expliquez le fonctionnement des websockets en détail." try: async for chunk in manager.stream_completion(original_prompt): if not validator.add_chunk(chunk): print("⚠️ Avertissement: Détection d'interruption potentielle") print(chunk, end='', flush=True) # Validation finale result = validator.detect_incomplete_response() if not result["is_complete"]: print("\n\n🔄 Complétion automatique en cours...") final_response = await validator.request_completion_resume( manager, original_prompt ) print(final_response) else: print(f"\n\n✅ Réponse complète validée ({result['total_chunks']} chunks)") except Exception as e: print(f"❌ Erreur fatale: {e}") if __name__ == "__main__": asyncio.run(main())

Stratégie de Résilience Multi-Niveau

Pour les applications de production critiques, implémentez une stratégie de résilience à plusieurs niveaux qui combine retry local, buffering intelligent, et fallback vers des endpoints alternatifs.

import logging
from typing import Callable, Any, Optional
from functools import wraps
import backoff

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class ResilientStreamingClient:
    """
    Client streaming avec résilience multi-niveau.
    Inclut circuit breaker, rate limiting, et fallback intelligent.
    """
    
    def __init__(
        self,
        primary_url: str = "https://api.holysheep.ai/v1",
        fallback_url: Optional[str] = None,
        api_key: str = "YOUR_HOLYSHEEP_API_KEY"
    ):
        self.primary_url = primary_url
        self.fallback_url = fallback_url
        self.api_key = api_key
        self.consecutive_failures = 0
        self.circuit_open = False
        self.circuit_open_time: Optional[float] = None
        
        # Configuration circuit breaker
        self.failure_threshold = 5
        self.circuit_reset_timeout = 60.0
        self.recovery_timeout = 30.0
    
    def _check_circuit_breaker(self) -> bool:
        """Vérifie l'état du circuit breaker"""
        
        if not self.circuit_open:
            return True
        
        if self.circuit_open_time is None:
            return False
        
        elapsed = time.time() - self.circuit_open_time
        
        if elapsed >= self.circuit_reset_timeout:
            logger.info("🔄 Circuit breaker: Tentative de recovery")
            self.circuit_open = False
            self.circuit_open_time = None
            return True
        
        return False
    
    def _trip_circuit_breaker(self):
        """Active le circuit breaker après échecs consécutifs"""
        
        self.consecutive_failures += 1
        
        if self.consecutive_failures >= self.failure_threshold:
            self.circuit_open = True
            self.circuit_open_time = time.time()
            logger.warning(f"⚠️ Circuit breaker ACTIVÉ après {self.consecutive_failures} échecs")
    
    def _record_success(self):
        """Enregistre un succès pour le circuit breaker"""
        
        self.consecutive_failures = 0
        if self.circuit_open:
            logger.info("✅ Circuit breaker: Recovery confirmé")
            self.circuit_open = False
            self.circuit_open_time = None
    
    @backoff.on_exception(
        backoff.expo,
        (aiohttp.ClientError, asyncio.TimeoutError),
        max_value=30,
        max_tries=4,
        jitter=backoff.random_jitter
    )
    async def _make_request(
        self,
        url: str,
        session: aiohttp.ClientSession,
        **kwargs
    ) -> aiohttp.ClientResponse:
        """Requête avec retry exponentiel et jitter"""
        
        async with session.request(**kwargs, url=url) as response:
            response.raise_for_status()
            return response
    
    async def stream_with_resilience(
        self,
        prompt: str,
        model: str = "gpt-5-turbo"
    ) -> AsyncIterator[Dict[str, Any]]:
        """
        Stream avec résilience complète: circuit breaker + fallback + retry
        """
        
        if not self._check_circuit_breaker():
            if self.fallback_url:
                logger.info("🔀 Utilisation du fallback URL")
                urls_to_try = [self.fallback_url, self.primary_url]
            else:
                raise ConnectionError("Circuit breaker ouvert et pas de fallback disponible")
        else:
            urls_to_try = [self.primary_url]
            if self.fallback_url:
                urls_to_try.append(self.fallback_url)
        
        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json"
        }
        
        payload = {
            "model": model,
            "messages": [{"role": "user", "content": prompt}],
            "stream": True
        }
        
        last_error = None
        
        for url in urls_to_try:
            try:
                async with aiohttp.ClientSession() as session:
                    async with session.post(
                        f"{url}/chat/completions",
                        headers=headers,
                        json=payload,
                        timeout=aiohttp.ClientTimeout(total=120)
                    ) as response:
                        
                        if response.status == 429:
                            logger.warning("⏳ Rate limit atteint, attente...")
                            await asyncio.sleep(5)
                            continue
                        
                        response.raise_for_status()
                        self._record_success()
                        
                        async for line in response.content:
                            line = line.decode('utf-8').strip()
                            
                            if line and line.startswith('data: '):
                                if line == 'data: [DONE]':
                                    break
                                
                                try:
                                    data = json.loads(line[6:])
                                    yield data
                                except json.JSONDecodeError:
                                    continue
                        
                        return
            
            except Exception as e:
                last_error = e
                logger.error(f"❌ Erreur avec {url}: {e}")
                self._trip_circuit_breaker()
                continue
        
        raise last_error or RuntimeError("Toutes les tentatives ont échoué")

Test d'intégration

async def test_resilient_stream(): client = ResilientStreamingClient( primary_url="https://api.holysheep.ai/v1", fallback_url="https://api.holysheep.ai/v1/backup", api_key="YOUR_HOLYSHEEP_API_KEY" ) try: async for chunk in client.stream_with_resilience( "Générez une liste de 10 concepts de programmation." ): if 'choices' in chunk: delta = chunk['choices'][0].get('delta', {}) if content := delta.get('content'): print(content, end='', flush=True) print("\n\n✅ Streaming terminé avec succès") except Exception as e: print(f"\n\n💥 Échec total: {e}") if __name__ == "__main__": asyncio.run(test_resilient_stream())

Gestion Avancée du Buffer et des Timeouts

Un aspect crucial souvent négligé est la gestion du buffer de réception. Configureez des timeouts adaptatifs basés sur le volume de données attendu et l'état du réseau.

from collections import deque
import threading

class AdaptiveBuffer:
    """
    Buffer circulaire avec timeout adaptatif basé sur le flux de données.
    """
    
    def __init__(
        self, 
        max_size: int = 10000,
        base_timeout: float = 30.0,
        activity_threshold: float = 0.1
    ):
        self.buffer = deque(maxlen=max_size)
        self.max_size = max_size
        self.base_timeout = base_timeout
        self.activity_threshold = activity_threshold
        
        self.last_activity_time = time.time()
        self.bytes_per_second = 0.0
        self.sample_window: deque[float] = deque(maxlen=10)
        
        self._lock = threading.Lock()
    
    def add(self, data: str) -> float:
        """Ajoute des données et retourne le timeout adaptatif actuel"""
        
        current_time = time.time()
        
        with self._lock:
            self.buffer.append(data)
            
            # Calcul du débit
            time_delta = current_time - self.last_activity_time
            
            if time_delta > 0:
                data_size = len(data.encode('utf-8'))
                current_rate = data_size / time_delta
                self.sample_window.append(current_rate)
                self.bytes_per_second = sum(self.sample_window) / len(self.sample_window)
            
            self.last_activity_time = current_time
        
        return self.calculate_adaptive_timeout()
    
    def calculate_adaptive_timeout(self) -> float:
        """
        Calcule un timeout adaptatif basé sur:
        - Le débit actuel de données
        - La taille du buffer
        - L'historique d'activité
        """
        
        with self._lock:
            buffer_size = len(self.buffer)
            time_since_activity = time.time() - self.last_activity_time
            
            # Timeout de base
            timeout = self.base_timeout
            
            # Réduction si fort débit attendu
            if self.bytes_per_second > 1000:  # >1KB/s
                timeout = min(timeout, 15.0)
            elif self.bytes_per_second > 100:  # >100B/s
                timeout = min(timeout, 20.0)
            
            # Augmentation si tampon plein (données accumulées)
            if buffer_size > self.max_size * 0.8:
                timeout = max(timeout, 45.0)
            
            # Augmentation si inactivité prolongée
            if time_since_activity > 10:
                timeout += time_since_activity * 0.5
            
            # Timeout maximum absolu
            return min(timeout, 60.0)
    
    def is_empty(self) -> bool:
        with self._lock:
            return len(self.buffer) == 0
    
    def get_all(self) -> str:
        with self._lock:
            return ''.join(self.buffer)
    
    def check_timeout_expired(self) -> bool:
        """Vérifie si le timeout est expiré pour le dernier chunk"""
        
        time_elapsed = time.time() - self.last_activity_time
        return time_elapsed > self.calculate_adaptive_timeout()

class StreamTimeoutManager:
    """Gestionnaire de timeout pour flux de streaming"""
    
    def __init__(self):
        self.buffers: Dict[str, AdaptiveBuffer] = {}
        self._cleanup_task: Optional[asyncio.Task] = None
    
    async def start_monitoring(self, stream_id: str) -> asyncio.Task:
        """Démarre la surveillance d'un flux"""
        
        self.buffers[stream_id] = AdaptiveBuffer()
        
        async def monitor_loop():
            while stream_id in self.buffers:
                buffer = self.buffers[stream_id]
                
                if buffer.check_timeout_expired() and not buffer.is_empty():
                    logger.warning(
                        f"⏱️ Timeout détecté pour stream {stream_id}: "
                        f"{buffer.get_all()[-100:]}"
                    )
                    # Émettre événement de timeout
                    yield {"type": "timeout", "stream_id": stream_id}
                
                await asyncio.sleep(1.0)
        
        return asyncio.create_task(monitor_loop())
    
    def add_data(self, stream_id: str, data: str) -> float:
        """Ajoute des données au buffer et retourne le timeout actuel"""
        
        if stream_id not in self.buffers:
            self.buffers[stream_id] = AdaptiveBuffer()
        
        return self.buffers[stream_id].add(data)
    
    async def stop_monitoring(self, stream_id: str):
        """Arrête la surveillance d'un flux"""
        
        if stream_id in self.buffers:
            del self.buffers[stream_id]

Monitoring et Observabilité

Pour une gestion proactive des interruptions, implémentez un système de monitoring qui suit les métriques clés de santé du streaming.

from dataclasses import dataclass, field
from typing import Dict, List
import statistics

@dataclass
class StreamMetrics:
    stream_id: str
    start_time: float = field(default_factory=time.time)
    total_chunks: int = 0
    total_bytes: int = 0
    interruptions: List[Dict] = field(default_factory=list)
    retry_count: int = 0
    latencies: List[float] = field(default_factory=list)
    last_chunk_time: float = field(default_factory=time.time)
    
    def record_chunk(self, chunk_size: int, latency: float):
        self.total_chunks += 1
        self.total_bytes += chunk_size
        self.last_chunk_time = time.time()
        self.latencies.append(latency)
    
    def record_interruption(self, reason: str):
        self.interruptions.append({
            "time": time.time(),
            "reason": reason,
            "chunks_before": self.total_chunks
        })
    
    def record_retry(self):
        self.retry_count += 1
    
    def get_health_score(self) -> float:
        """Score de santé du stream (0-100)"""
        
        if self.total_chunks == 0:
            return 0.0
        
        # Pénalité pour interruptions
        interruption_penalty = len(self.interruptions) * 10
        
        # Pénalité pour retries
        retry_penalty = self.retry_count * 5
        
        # Bonus pour latence stable
        if len(self.latencies) > 1:
            latency_cv = statistics.stdev(self.latencies) / statistics.mean(self.latencies)
            stability_bonus = max(0, 20 - latency_cv * 100)
        else:
            stability_bonus = 10
        
        score = 100 - interruption_penalty - retry_penalty + stability_bonus
        return max(0.0, min(100.0, score))
    
    def to_dict(self) -> Dict:
        return {
            "stream_id": self.stream_id,
            "duration": time.time() - self.start_time,
            "total_chunks": self.total_chunks,
            "total_bytes": self.total_bytes,
            "interruptions": len(self.interruptions),
            "retry_count": self.retry_count,
            "health_score": self.get_health_score(),
            "avg_latency": statistics.mean(self.latencies) if self.latencies else 0,
            "p95_latency": (
                sorted(self.latencies)[int(len(self.latencies) * 0.95)]
                if len(self.latencies) > 1 else 0
            )
        }

class StreamMonitor:
    """Moniteur centralisé pour tous les streams"""
    
    def __init__(self):
        self.active_streams: Dict[str, StreamMetrics] = {}
        self.completed_streams: List[StreamMetrics] = []
    
    def start_stream(self, stream_id: str) -> StreamMetrics:
        metrics = StreamMetrics(stream_id=stream_id)
        self.active_streams[stream_id] = metrics
        return metrics
    
    def get_stream(self, stream_id: str) -> Optional[StreamMetrics]:
        return self.active_streams.get(stream_id)
    
    def end_stream(self, stream_id: str):
        if stream_id in self.active_streams:
            self.completed_streams.append(self.active_streams[stream_id])
            del self.active_streams[stream_id]
    
    def get_global_stats(self) -> Dict:
        if not self.completed_streams:
            return {"message": "Aucune donnée historique"}
        
        all_scores = [s.get_health_score() for s in self.completed_streams]
        total_retries = sum(s.retry_count for s in self.completed_streams)
        total_interruptions = sum(len(s.interruptions) for s in self.completed_streams)
        
        return {
            "total_streams": len(self.completed_streams),
            "avg_health_score": statistics.mean(all_scores),
            "p95_health_score": sorted(all_scores)[int(len(all_scores) * 0.95)],
            "total_retries": total_retries,
            "total_interruptions": total_interruptions,
            "success_rate": len([s for s in all_scores if s >= 80]) / len(all_scores) * 100
        }

Singleton pour le monitoring global

global_monitor = StreamMonitor()

Erreurs courantes et solutions

Erreur 1 : Interruption avec perte de données partielles

# ❌ PROBLÈME : Réception de chunks incomplets après timeout

Erreur: aiohttp.ClientTimeout: Total timeout exceeded

async def bad_streaming_handler(): async with aiohttp.ClientSession() as session: async with session.post(url, json=payload) as resp: chunks = [] async for line in resp.content: chunks.append(line) # Perte si interruption ici! return b"".join(chunks)

✅ SOLUTION : Bufferisation avec validation et reprise

async def resilient_streaming_handler(): buffer = AdaptiveBuffer(max_size=5000, base_timeout=45.0) validator = DataIntegrityValidator() async with aiohttp.ClientSession() as session: retry_count = 0 while retry_count < 3: try: async with session.post(url, json=payload) as resp: async for line in resp.content: buffer.add_data(line) if not validator.validate_chunk(line): raise ValueError("Chunk invalide") # Vérification finale if not validator.is_complete(): raise IncompleteResponseError("Réponse tronquée") return validator.get_full_response() except (aiohttp.ClientError, asyncio.TimeoutError): retry_count += 1 await asyncio.sleep(2 ** retry_count) raise MaxRetriesExceededError("Échec après 3 tentatives")

Erreur 2 : Circuit breaker trop agressif

# ❌ PROBLÈME : Circuit breaker s'active trop vite sur réseau instable

Comportement: Déconnexions fréquentes même avec retry réussi

class TooAggressiveBreaker: def __init__(self): self.failure_threshold = 2 # Trop bas! self.circuit_timeout = 60 # Trop long! # Résultat: Circuit ouvert après 2 échecs temporaires

✅ SOLUTION : Configuration équilibrée avec jitter

class BalancedCircuitBreaker: def __init__(self): # Seuils adaptatifs self.failure_threshold = 5 # 5 échecs avant ouverture self.success_threshold = 3 # 3 succès pour fermeture self.circuit_timeout = 30.0 # Retry après 30s self.half_open_timeout = 10.0 # Test en half-open # Compteurs self.failures = 0 self.successes = 0 self.state = "closed" def record_failure(self): self.failures += 1 self.successes = 0 # Reset if self.failures >= self.failure_threshold: self.state = "open" self.open_time = time.time() def record_success(self): self.successes += 1 if self.state == "half-open" and self.successes >= self.success_threshold: self.state = "closed" self.failures = 0 elif self.state == "closed": self.failures = max(0, self.failures - 1) def can_attempt(self) -> bool: if self.state == "closed": return True if self.state == "open": elapsed = time.time() - self.open_time if elapsed >= self.circuit_timeout: self.state = "half-open" return True return False return self.state == "half-open"

Erreur 3 : Incohérence des chunks lors de reconnexion

# ❌ PROBLÈME : Duplication ou perte après reconnexion

Symptôme: Réponse contient des parties dupliquées ou manquantes

async def naive_reconnect(): # Problème: Pas de tracking de position for attempt in range(3): try: async for chunk in stream_from_api(): yield chunk # Risque de doublons après reconnect! except: await asyncio.sleep(1)

✅ SOLUTION : Checkpointing avec identifiants de chunk

class ChunkCheckpointManager: def __init__(self, storage_path: str = "./checkpoints"): self.storage_path = storage_path self.checkpoints: Dict[str, int] = {} def save_checkpoint(self, stream_id: str, chunk_index: int, content: str): """Sauvegarde incrémentale du checkpoint""" checkpoint_file = f"{self.storage_path}/{stream_id}.json" data = { "last_index": chunk_index, "content": content, "timestamp": time.time() } with open(checkpoint_file, 'w') as f: json.dump(data, f) def load_checkpoint(self, stream_id: str) -> Optional[Dict]: """Charge le dernier checkpoint valide""" checkpoint_file = f"{self.storage_path}/{stream_id}.json" if not os.path.exists(checkpoint_file): return None try: with open(checkpoint_file, 'r') as f: data = json.load(f) # Expiration après 1 heure if time.time() - data.get("timestamp", 0) > 3600: return None return data except (json.JSONDecodeError, IOError): return None def get_start_index(self, stream_id: str) -> int: checkpoint = self.load_checkpoint(stream_id) return checkpoint.get("last_index", -1) + 1 if checkpoint else 0 async def smart_reconnect_stream(stream_id: str, api_manager): checkpoint_mgr = ChunkCheckpointManager() start_index = checkpoint_mgr.get_start_index(stream_id) previous_content = "" if start_index > 0: checkpoint = checkpoint_mgr.load_checkpoint(stream_id) previous_content = checkpoint.get("content", "") if checkpoint else "" async for index, chunk in api_manager.stream(): if index < start_index: continue # Skip déjà reçus # Vérification anti-duplication if chunk.startswith(previous_content[-50:]): chunk = chunk[len(previous_content[-50:]):] yield chunk previous_content += chunk # Sauvegarde périodique (tous les 10 chunks) if (index + 1) % 10 == 0: checkpoint_mgr.save_checkpoint(stream_id, index, previous_content) # Nettoyage après succès os.remove(f"{checkpoint_mgr.storage_path}/{stream_id}.json")

Erreur 4 : Rate limiting mal géré

# ❌ PROBLÈME : Ignorer les 429 Too Many Requests

Résultat: Ban temporaire ou permanent de l'API

async def ignoring_rate_limit(): async with session.post(url) as resp: if resp.status == 429: continue # IGNORÉ! - Mauvaise pratique return await resp.json()

✅ SOLUTION : Backoff exponentiel avec respect du Retry-After

async def proper_rate_limit_handling(): headers = { "Authorization": f"Bearer YOUR_HOLYSHEep_API_KEY", "Content-Type": "application/json" } async with aiohttp.ClientSession() as session: async with session.post( f"https://api.holysheep.ai/v1/chat/completions", headers=headers, json=payload ) as resp: if resp.status == 429: # Extraire le Retry-After si disponible retry_after = resp.headers.get('Retry-After') if retry_after: wait_time = int(retry_after) else: # Backoff exponentiel par défaut wait_time = 60 # Commencer à 1 minute logger.warning(f"⏳ Rate limit atteint. Attente: {wait_time}s") await asyncio.sleep(wait_time) # Retry automatique async with session.post( f"https://api.holysheep.ai/v1/chat/completions", headers=headers, json=payload ) as retry_resp: return await retry_resp.json() resp.raise_for_status() return await resp.json()

Recommandations de Production

Après des années de gestion de flux de streaming à grande échelle, mes recommandations clés pour la production sont :

Conclusion

La gestion des interruptions de streaming avec