Bienvenue dans ce tutoriel pratique. Aujourd'hui, je vais vous expliquer comment tester rigoureusement vos produits Agent IA avant leur mise en production. Si vous lancez une application qui utilise l'intelligence artificielle en Production, vous devez anticiper les pics de trafic, les erreurs temporaires et les limitations de débit. Sans ces protections, votre application risque de planter publiquement — un cauchemar pour n'importe quel développeur.

Personnellement, j'ai vécu ce problème lors du lancement d'un chatbot client pour une PME e-commerce. Notre système tombait en carafe dès que plus de 50 utilisateurs simultanés tentaient d'interagir. Après des nuits blanches à corriger en urgence, j'ai compris l'importance vitale des tests de charge préventifs. Aujourd'hui, je partage avec vous ma méthodologie complète.

Pour qui / Pour qui ce n'est pas fait

✅ Ce tutoriel est fait pour vous si : ❌ Ce tutoriel n'est PAS pour vous si :
  • Vous développez un produit Agent IA (chatbot, assistant, automate)
  • Vous n'avez jamais fait de tests de charge auparavant
  • Vous utilisez une API IA en backend (GPT, Claude, Gemini...)
  • Vous lancez un MVP et voulez éviter les pannes de production
  • Vous cherchez une solution tout-en-un économique
  • Vous gérez déjà des systèmes distribués à l'échelle million d'utilisateurs
  • Vous n'utilisez pas d'API IA dans votre application
  • Vous cherchez uniquement de la théorie sans implémentation
  • Votre budget est illimité et vous n'avez pas besoin d'optimiser les coûts

Comprendre les 3 Piliers de la Résilience IA

Avant de coder, graspons ensemble les trois mécanismes essentiels qui protégeront votre application des pannes :

1. Le Rate Limiting (Limitation de Débit)

Imaginez un restaurant avec 10 places. Si 100 personnes arrivent en même temps, le système doit gérer la file d'attente intelligemment. Le rate limiting fait exactement cela : il limite le nombre de requêtes par seconde envoyées à l'API IA.

Sans rate limiting : Votre système envoie 1000 requêtes en 1 seconde, l'API vous bloque, vos utilisateurs reçoivent des erreurs 429. Horrible pour l'expérience.

Avec rate limiting : Votre système envoie maximum 100 requêtes par seconde, les autres patientent dans une queue, tous vos utilisateurs sont servis progressivement.

2. Le Retry (Nouvelle Tentative)

L'IA peut avoir des micro-coupures de 200 millisecondes. Au lieu de montrer une erreur à l'utilisateur, le retry automatique réessaie silencieusement. C'est commeredémarrer une machine à café quand elle bug — une seconde tentative résout 90% des problèmes.

3. Le Fallback (Solution de Secours)

Si votre API IA principale (ex: GPT-4.1) est saturée, le fallback bascule vers une alternative (ex: DeepSeek V3.2). L'utilisateur ne remarque rien, votre service continue de fonctionner. C'est le plan B intégré.

Architecture de Notre Solution de Test

Voici le schéma conceptuel de notre système de test haute concurrence :

┌─────────────────────────────────────────────────────────────────┐
│                    ARCHITECTURE DE TEST                         │
│                                                                 │
│  ┌─────────┐    ┌─────────────┐    ┌──────────────────────────┐ │
│  │  Load   │───▶│   Queue     │───▶│   Rate Limiter           │ │
│  │  Tester │    │   (Buffer)  │    │   (Token Bucket/Sliding) │ │
│  └─────────┘    └─────────────┘    └──────────────────────────┘ │
│                                              │                   │
│                                              ▼                   │
│  ┌─────────────────────────────────────────────────────────────┐ │
│  │                    API HolySheep AI                        │ │
│  │              base_url: https://api.holysheep.ai/v1         │ │
│  │                   Latence < 50ms                           │ │
│  └─────────────────────────────────────────────────────────────┘ │
│                         │           │           │               │
│              ┌──────────┴───┐ ┌─────┴────┐ ┌────┴───────┐      │
│              │  GPT-4.1     │ │ Claude   │ │ DeepSeek   │      │
│              │  $8/MTok      │ │ Sonnet   │ │ V3.2       │      │
│              │  (Primary)    │ │ $15/MTok │ │ $0.42/MTok │      │
│              └──────────────┘ └──────────┘ └────────────┘      │
│                      │           │           │                  │
│                      └───────────┴───────────┘                  │
│                                  │                               │
│                                  ▼                               │
│                    ┌─────────────────────┐                       │
│                    │   Circuit Breaker  │                       │
│                    │   + Fallback Logic │                       │
│                    └─────────────────────┘                       │
│                                  │                               │
│                                  ▼                               │
│                    ┌─────────────────────┐                       │
│                    │   Metrics Dashboard │                       │
│                    │   Latence / Errors  │                       │
│                    └─────────────────────┘                       │
└─────────────────────────────────────────────────────────────────┘

Installation de l'Environnement de Test

Ouvrez votre terminal et exécutez les commandes suivantes pour installer les dépendances nécessaires :

# Installation des dépendances Python pour les tests de charge
pip install aiohttp asyncio-rate-limitor httpx prometheus-client psutil

Vérification de l'installation

python --version

Devrait afficher Python 3.9+

Le Code Complet du Système de Test

Module 1 : Configuration et Client HolySheep

# config.py - Configuration centralisée du système de test
import os
from typing import Dict, List

class HolySheepConfig:
    """Configuration pour l'API HolySheep AI"""
    
    # ✅ URLs officielles HolySheep - JAMAIS api.openai.com ou api.anthropic.com
    BASE_URL = "https://api.holysheep.ai/v1"
    
    # ⚠️ Remplacez par votre vraie clé depuis https://www.holysheep.ai/register
    API_KEY = os.getenv("HOLYSHEEP_API_KEY", "YOUR_HOLYSHEEP_API_KEY")
    
    # Modèles disponibles avec leurs prix 2026
    MODELS = {
        "gpt-4.1": {
            "name": "GPT-4.1",
            "provider": "OpenAI via HolySheep",
            "price_per_mtok": 8.00,  # USD par million de tokens
            "latency_estimate": "35-45ms",
            "context_window": 128000,
            "use_case": "Réponses complexes, coding advanced"
        },
        "claude-sonnet-4.5": {
            "name": "Claude Sonnet 4.5",
            "provider": "Anthropic via HolySheep",
            "price_per_mtok": 15.00,
            "latency_estimate": "40-50ms",
            "context_window": 200000,
            "use_case": "Analyse nuancée, rédaction longue"
        },
        "gemini-2.5-flash": {
            "name": "Gemini 2.5 Flash",
            "provider": "Google via HolySheep",
            "price_per_mtok": 2.50,
            "latency_estimate": "25-35ms",
            "context_window": 1000000,
            "use_case": "Réponses rapides, volume élevé"
        },
        "deepseek-v3.2": {
            "name": "DeepSeek V3.2",
            "provider": "DeepSeek via HolySheep",
            "price_per_mtok": 0.42,  # ✅ Économie 85%+ vs GPT-4.1
            "latency_estimate": "30-40ms",
            "context_window": 64000,
            "use_case": "Fallback économique, haute volumétrie"
        }
    }
    
    # Configuration du rate limiting
    RATE_LIMITS = {
        "requests_per_second": 50,      # 50 requêtes/sec max
        "tokens_per_minute": 50000,      # 50K tokens/minute
        "concurrent_connections": 100,   # 100 connexions simultanées
        "retry_attempts": 3,             # 3 tentatives en cas d'erreur
        "retry_delay_base": 1.0,         # Délai initial 1 seconde
        "timeout_seconds": 30            # Timeout global 30 sec
    }
    
    # Configuration du test de charge
    LOAD_TEST_CONFIG = {
        "duration_seconds": 60,          # Test de 60 secondes
        "virtual_users": 200,            # 200 utilisateurs virtuels
        "ramp_up_seconds": 10,           # Montée en charge sur 10 sec
        "peak_requests_per_second": 500  # Pic à 500 req/sec
    }
    
    @classmethod
    def get_fallback_chain(cls) -> List[str]:
        """Retourne la chaîne de fallback par priorité"""
        return ["gpt-4.1", "claude-sonnet-4.5", "gemini-2.5-flash", "deepseek-v3.2"]
    
    @classmethod
    def calculate_cost(cls, model: str, tokens: int) -> float:
        """Calcule le coût en USD pour un nombre de tokens"""
        price = cls.MODELS.get(model, {}).get("price_per_mtok", 0)
        return (tokens / 1_000_000) * price
    
    @classmethod
    def calculate_savings(cls, tokens: int) -> Dict[str, float]:
        """Calcule les économies potentielles avec HolySheep"""
        gpt_cost = cls.calculate_cost("gpt-4.1", tokens)
        deepseek_cost = cls.calculate_cost("deepseek-v3.2", tokens)
        return {
            "gpt_cost_usd": gpt_cost,
            "deepseek_cost_usd": deepseek_cost,
            "savings_usd": gpt_cost - deepseek_cost,
            "savings_percentage": ((gpt_cost - deepseek_cost) / gpt_cost * 100) if gpt_cost > 0 else 0
        }

Module 2 : Rate Limiter Avancé avec Token Bucket

# rate_limiter.py - Implémentation du rate limiting avancé
import time
import asyncio
from collections import deque
from typing import Optional
from threading import Lock

class TokenBucketRateLimiter:
    """
    Rate Limiter basé sur l'algorithme Token Bucket.
    Permet de lisser les pics de trafic tout en maximisant le throughput.
    """
    
    def __init__(self, rate: int, capacity: int):
        """
        Args:
            rate: Nombre de tokens ajoutés par seconde
            capacity: Capacité maximale du bucket
        """
        self.rate = rate
        self.capacity = capacity
        self.tokens = capacity
        self.last_update = time.time()
        self.lock = Lock()
        self.total_requests = 0
        self.total_wait_time = 0.0
        self.rejected_requests = 0
    
    def _refill(self):
        """Rajoute des tokens selon le temps écoulé"""
        now = time.time()
        elapsed = now - self.last_update
        tokens_to_add = elapsed * self.rate
        self.tokens = min(self.capacity, self.tokens + tokens_to_add)
        self.last_update = now
    
    def acquire(self, tokens: int = 1, blocking: bool = True) -> bool:
        """
        Acquiert des tokens pour traiter une requête.
        
        Args:
            tokens: Nombre de tokens nécessaires
            blocking: Si True, attend si nécessaire ; si False, échoue immédiatement
        
        Returns:
            True si les tokens ont été acquis
        """
        with self.lock:
            self._refill()
            
            if self.tokens >= tokens:
                self.tokens -= tokens
                self.total_requests += 1
                return True
            
            if not blocking:
                self.rejected_requests += 1
                return False
            
            # Calcul du temps d'attente
            tokens_needed = tokens - self.tokens
            wait_time = tokens_needed / self.rate
            self.total_wait_time += wait_time
            
            # Simulation du wait (en réalité, on libère le lock)
        
        # Attente passive
        time.sleep(wait_time)
        
        with self.lock:
            self.tokens -= tokens
            self.total_requests += 1
            return True
    
    async def acquire_async(self, tokens: int = 1) -> bool:
        """Version async pour les environnements asynchrones"""
        await asyncio.sleep(0)  # Yield to event loop
        
        with self.lock:
            self._refill()
            
            if self.tokens >= tokens:
                self.tokens -= tokens
                self.total_requests += 1
                return True
        
        # Wait and retry
        await asyncio.sleep(0.1)
        return await self.acquire_async(tokens)
    
    def get_stats(self) -> dict:
        """Retourne les statistiques du rate limiter"""
        return {
            "total_requests": self.total_requests,
            "rejected_requests": self.rejected_requests,
            "total_wait_time": self.total_wait_time,
            "avg_wait_time": self.total_wait_time / self.total_requests if self.total_requests > 0 else 0,
            "current_tokens": self.tokens
        }


class SlidingWindowRateLimiter:
    """
    Rate Limiter basé sur une fenêtre glissante.
    Plus précis que Token Bucket pour respecter des limites strictes.
    """
    
    def __init__(self, max_requests: int, window_seconds: int):
        self.max_requests = max_requests
        self.window_seconds = window_seconds
        self.requests = deque()
        self.lock = Lock()
    
    def _clean_old_requests(self):
        """Supprime les requêtes hors de la fenêtre"""
        now = time.time()
        cutoff = now - self.window_seconds
        
        while self.requests and self.requests[0] < cutoff:
            self.requests.popleft()
    
    def is_allowed(self) -> bool:
        """Vérifie si une nouvelle requête est autorisée"""
        with self.lock:
            self._clean_old_requests()
            
            if len(self.requests) < self.max_requests:
                self.requests.append(time.time())
                return True
            
            return False
    
    def time_until_allowed(self) -> float:
        """Retourne le temps d'attente avant la prochaine requête"""
        with self.lock:
            self._clean_old_requests()
            
            if len(self.requests) < self.max_requests:
                return 0.0
            
            oldest = self.requests[0]
            return max(0.0, oldest + self.window_seconds - time.time())


Instanciation des rate limiters selon la config

def create_rate_limiters(config): """Factory pour créer les rate limiters selon la configuration""" return { "requests_per_second": TokenBucketRateLimiter( rate=config.RATE_LIMITS["requests_per_second"], capacity=config.RATE_LIMITS["concurrent_connections"] ), "tokens_per_minute": TokenBucketRateLimiter( rate=config.RATE_LIMITS["tokens_per_minute"] / 60, capacity=config.RATE_LIMITS["tokens_per_minute"] / 2 ), "concurrent": SlidingWindowRateLimiter( max_requests=config.RATE_LIMITS["concurrent_connections"], window_seconds=1 ) }

Module 3 : Client IA avec Retry et Fallback Intelligent

# holy_sheep_client.py - Client IA complet avec retry et fallback
import httpx
import asyncio
import time
from typing import Optional, Dict, Any, List
from dataclasses import dataclass
from enum import Enum

class CircuitState(Enum):
    CLOSED = "closed"      # Normal, requests pass through
    OPEN = "open"          # Bloqué, on bypass vers fallback
    HALF_OPEN = "half_open"  # Test de récupération


@dataclass
class RequestMetrics:
    latency_ms: float
    status_code: int
    model_used: str
    success: bool
    error_type: Optional[str] = None
    tokens_used: int = 0


class CircuitBreaker:
    """
    Circuit Breaker pattern pour protéger contre les cascades d'erreurs.
    Surveille les échecs et ouvre le circuit quand le seuil est dépassé.
    """
    
    def __init__(self, failure_threshold: int = 5, timeout_seconds: int = 30):
        self.failure_threshold = failure_threshold
        self.timeout_seconds = timeout_seconds
        self.failure_count = 0
        self.last_failure_time: Optional[float] = None
        self.state = CircuitState.CLOSED
    
    def record_success(self):
        self.failure_count = 0
        self.state = CircuitState.CLOSED
    
    def record_failure(self):
        self.failure_count += 1
        self.last_failure_time = time.time()
        
        if self.failure_count >= self.failure_threshold:
            self.state = CircuitState.OPEN
    
    def can_execute(self) -> bool:
        if self.state == CircuitState.CLOSED:
            return True
        
        if self.state == CircuitState.OPEN:
            if time.time() - self.last_failure_time >= self.timeout_seconds:
                self.state = CircuitState.HALF_OPEN
                return True
            return False
        
        # HALF_OPEN : on autorise quelques requêtes test
        return True


class HolySheepAIClient:
    """
    Client complet pour l'API HolySheep AI avec :
    - Rate Limiting automatique
    - Retry exponentiel
    - Fallback multi-modèle
    - Circuit Breaker
    """
    
    def __init__(self, api_key: str, config):
        self.api_key = api_key
        self.config = config
        self.base_url = config.BASE_URL
        self.rate_limiters = create_rate_limiters(config)
        self.circuit_breakers: Dict[str, CircuitBreaker] = {}
        self.metrics: List[RequestMetrics] = []
        
        # Initialiser un circuit breaker par modèle
        for model_id in config.MODELS.keys():
            self.circuit_breakers[model_id] = CircuitBreaker()
    
    def _get_headers(self) -> Dict[str, str]:
        return {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json"
        }
    
    async def chat_completion(
        self,
        messages: List[Dict[str, str]],
        model: str = "gpt-4.1",
        temperature: float = 0.7,
        max_tokens: int = 1000
    ) -> Dict[str, Any]:
        """
        Envoie une requête de chat completion avec gestion complète des erreurs.
        """
        # Étape 1 : Vérifier le circuit breaker
        cb = self.circuit_breakers.get(model)
        if cb and not cb.can_execute():
            return await self._fallback_to_alternative(model, messages, temperature, max_tokens)
        
        # Étape 2 : Acquérir le rate limiter
        await self.rate_limiters["requests_per_second"].acquire_async()
        
        # Étape 3 : Envoyer la requête avec retry
        return await self._request_with_retry(
            model=model,
            messages=messages,
            temperature=temperature,
            max_tokens=max_tokens
        )
    
    async def _request_with_retry(
        self,
        model: str,
        messages: List[Dict[str, str]],
        temperature: float,
        max_tokens: int,
        attempt: int = 0
    ) -> Dict[str, Any]:
        """Requête avec retry exponentiel"""
        
        max_attempts = self.config.RATE_LIMITS["retry_attempts"]
        base_delay = self.config.RATE_LIMITS["retry_delay_base"]
        
        start_time = time.time()
        
        try:
            async with httpx.AsyncClient(timeout=self.config.RATE_LIMITS["timeout_seconds"]) as client:
                response = await client.post(
                    f"{self.base_url}/chat/completions",
                    headers=self._get_headers(),
                    json={
                        "model": model,
                        "messages": messages,
                        "temperature": temperature,
                        "max_tokens": max_tokens
                    }
                )
                
                latency = (time.time() - start_time) * 1000
                
                if response.status_code == 200:
                    # Succès !
                    cb = self.circuit_breakers.get(model)
                    if cb:
                        cb.record_success()
                    
                    data = response.json()
                    tokens_used = data.get("usage", {}).get("total_tokens", 0)
                    
                    self.metrics.append(RequestMetrics(
                        latency_ms=latency,
                        status_code=200,
                        model_used=model,
                        success=True,
                        tokens_used=tokens_used
                    ))
                    
                    return {"success": True, "data": data, "model": model, "latency_ms": latency}
                
                elif response.status_code == 429:
                    # Rate Limited par HolySheep - on retry
                    self.metrics.append(RequestMetrics(
                        latency_ms=latency,
                        status_code=429,
                        model_used=model,
                        success=False,
                        error_type="rate_limited"
                    ))
                    raise httpx.HTTPStatusError("Rate limited", request=response.request, response=response)
                
                elif response.status_code >= 500:
                    # Erreur serveur - on retry
                    self.metrics.append(RequestMetrics(
                        latency_ms=latency,
                        status_code=response.status_code,
                        model_used=model,
                        success=False,
                        error_type="server_error"
                    ))
                    raise httpx.HTTPStatusError("Server error", request=response.request, response=response)
                
                else:
                    # Erreur client (4xx) - pas de retry
                    return {
                        "success": False,
                        "error": f"HTTP {response.status_code}",
                        "model": model
                    }
        
        except (httpx.HTTPStatusError, httpx.RequestError, asyncio.TimeoutError) as e:
            # Gérer le retry
            if attempt < max_attempts - 1:
                delay = base_delay * (2 ** attempt)  # Exponentiel : 1s, 2s, 4s
                await asyncio.sleep(delay)
                return await self._request_with_retry(
                    model, messages, temperature, max_tokens, attempt + 1
                )
            
            # Plus de retry disponibles - fallback
            cb = self.circuit_breakers.get(model)
            if cb:
                cb.record_failure()
            
            return await self._fallback_to_alternative(model, messages, temperature, max_tokens)
    
    async def _fallback_to_alternative(
        self,
        failed_model: str,
        messages: List[Dict[str, str]],
        temperature: float,
        max_tokens: int
    ) -> Dict[str, Any]:
        """Bascule vers le modèle suivant dans la chaîne de fallback"""
        
        fallback_chain = self.config.get_fallback_chain()
        failed_index = fallback_chain.index(failed_model) if failed_model in fallback_chain else 0
        
        for i in range(failed_index + 1, len(fallback_chain)):
            alternative_model = fallback_chain[i]
            cb = self.circuit_breakers.get(alternative_model)
            
            # Ne pas tester un modèle dont le circuit est ouvert
            if cb and not cb.can_execute():
                continue
            
            try:
                result = await self._request_with_retry(
                    alternative_model, messages, temperature, max_tokens
                )
                
                if result.get("success"):
                    result["fallback_from"] = failed_model
                    result["fallback_to"] = alternative_model
                    return result
            
            except Exception:
                continue
        
        # Aucun modèle disponible
        return {
            "success": False,
            "error": "All models unavailable",
            "fallback_from": failed_model
        }
    
    def get_metrics_summary(self) -> Dict[str, Any]:
        """Retourne un résumé des métriques de performance"""
        if not self.metrics:
            return {"error": "No metrics available"}
        
        successful = [m for m in self.metrics if m.success]
        failed = [m for m in self.metrics if not m.success]
        
        latencies = [m.latency_ms for m in successful]
        
        return {
            "total_requests": len(self.metrics),
            "successful_requests": len(successful),
            "failed_requests": len(failed),
            "success_rate": len(successful) / len(self.metrics) * 100,
            "avg_latency_ms": sum(latencies) / len(latencies) if latencies else 0,
            "p50_latency_ms": sorted(latencies)[len(latencies) // 2] if latencies else 0,
            "p95_latency_ms": sorted(latencies)[int(len(latencies) * 0.95)] if latencies else 0,
            "p99_latency_ms": sorted(latencies)[int(len(latencies) * 0.99)] if latencies else 0,
            "total_tokens": sum(m.tokens_used for m in successful),
            "fallback_count": len([m for m in self.metrics if hasattr(m, 'error_type')])
        }

Module 4 : Simulateur de Charge Haute Concurrence

# load_tester.py - Simulateur de charge pour tester la résilience
import asyncio
import random
import time
from typing import List, Dict, Any
from datetime import datetime
from collections import defaultdict

class LoadSimulator:
    """
    Simule une charge réaliste sur votre système Agent IA.
    Crée des utilisateurs virtuels qui génèrent des requêtes simultanées.
    """
    
    def __init__(self, client, config):
        self.client = client
        self.config = config
        self.results: List[Dict[str, Any]] = []
        self.errors: List[Dict[str, Any]] = []
        self.start_time = None
        self.end_time = None
    
    def _generate_realistic_prompt(self) -> List[Dict[str, str]]:
        """Génère des prompts réalistes pour le test"""
        prompts = [
            [{"role": "user", "content": "Explique-moi la différence entre l'apprentissage supervisé et non supervisé en 3 phrases."}],
            [{"role": "user", "content": "Écris un email professionnel pour demander un report de délai de livraison."}],
            [{"role": "user", "content": "Donne-moi 5 idées de contenu pour Instagram sur le thème de la cuisine healthy."}],
            [{"role": "user", "content": "Rédige un résumé des dernières actualités en intelligence artificielle."}],
            [{"role": "user", "content": "Comment optimiser mes chances de recrutement sur LinkedIn ?"}],
        ]
        return random.choice(prompts)
    
    async def _virtual_user_simulation(self, user_id: int, duration: int):
        """Simule le comportement d'un utilisateur virtuel"""
        
        end_time = time.time() + duration
        request_count = 0
        
        while time.time() < end_time:
            try:
                # Générer une requête
                messages = self._generate_realistic_prompt()
                model = random.choice(["gpt-4.1", "claude-sonnet-4.5", "deepseek-v3.2"])
                
                start = time.time()
                result = await self.client.chat_completion(
                    messages=messages,
                    model=model,
                    temperature=0.7,
                    max_tokens=500
                )
                latency = (time.time() - start) * 1000
                
                self.results.append({
                    "user_id": user_id,
                    "timestamp": datetime.now().isoformat(),
                    "model": result.get("model", model),
                    "latency_ms": latency,
                    "success": result.get("success", False),
                    "fallback_from": result.get("fallback_from"),
                    "error": result.get("error")
                })
                
                request_count += 1
                
                # Pause aléatoire entre requêtes (simule le temps de réflexion humain)
                await asyncio.sleep(random.uniform(0.5, 2.0))
            
            except Exception as e:
                self.errors.append({
                    "user_id": user_id,
                    "timestamp": datetime.now().isoformat(),
                    "error_type": type(e).__name__,
                    "error_message": str(e)
                })
    
    async def run_load_test(
        self,
        virtual_users: int = 100,
        duration_seconds: int = 60,
        ramp_up_seconds: int = 10
    ):
        """
        Lance le test de charge complet.
        
        Args:
            virtual_users: Nombre d'utilisateurs virtuels simultanés
            duration_seconds: Durée totale du test
            ramp_up_seconds: Temps de montée en charge progressive
        """
        print(f"🚀 Démarrage du test de charge...")
        print(f"   Utilisateurs virtuels : {virtual_users}")
        print(f"   Durée : {duration_seconds} secondes")
        print(f"   Rampe-up : {ramp_up_seconds} secondes")
        print("-" * 50)
        
        self.start_time = time.time()
        self.results = []
        self.errors = []
        
        # Créer les tâches utilisateurs avec ramp-up progressif
        tasks = []
        for i in range(virtual_users):
            # Delay progressif pour le ramp-up
            delay = (i / virtual_users) * ramp_up_seconds if ramp_up_seconds > 0 else 0
            task = asyncio.create_task(self._delayed_user_start(i, delay, duration_seconds))
            tasks.append(task)
        
        # Attendre la fin de tous les utilisateurs
        await asyncio.gather(*tasks)
        
        self.end_time = time.time()
        return self.generate_report()
    
    async def _delayed_user_start(self, user_id: int, delay: float, duration: int):
        """Démarre un utilisateur avec un délai"""
        if delay > 0:
            await asyncio.sleep(delay)
        await self._virtual_user_simulation(user_id, duration)
    
    def generate_report(self) -> Dict[str, Any]:
        """Génère un rapport détaillé des résultats du test"""
        
        if not self.results:
            return {"error": "No results available"}
        
        total_duration = self.end_time - self.start_time if self.end_time else 0
        
        # Calculer les statistiques
        successful = [r for r in self.results if r["success"]]
        failed = [r for r in self.results if not r["success"]]
        latencies = [r["latency_ms"] for r in successful]
        
        # Grouper par modèle
        by_model = defaultdict(list)
        for r in successful:
            by_model[r["model"]].append(r["latency_ms"])
        
        # Grouper par minute
        by_minute = defaultdict(int)
        for r in self.results:
            minute = r["timestamp"][:16]  # YYYY-MM-DDTHH:MM
            by_minute[minute] += 1
        
        report = {
            "test_info": {
                "start_time": datetime.fromtimestamp(self.start_time).isoformat() if self.start_time else None,
                "end_time": datetime.fromtimestamp(self.end_time).isoformat() if self.end_time else None,
                "duration_seconds": total_duration,
                "total_requests": len(self.results),
                "virtual_users": len(set(r["user_id"] for r in self.results))
            },
            "success_metrics": {
                "successful": len(successful),
                "failed": len(failed),
                "success_rate": len(successful) / len(self.results) * 100 if self.results else 0,
                "requests_per_second": len(self.results) / total_duration if total_duration > 0 else 0
            },
            "latency_metrics": {
                "avg_ms": sum(latencies) / len(latencies) if latencies else 0,
                "min_ms": min(latencies) if latencies else 0,
                "max_ms": max(latencies) if latencies else 0,
                "p50_ms": sorted(latencies)[len(latencies) // 2] if latencies else 0,
                "p95_ms": sorted(latencies)[int(len(latencies) * 0.95)] if latencies else 0,
                "p99_ms": sorted(latencies)[int(len(latencies) * 0.99)] if latencies else 0
            },
            "model_breakdown": {
                model: {
                    "requests": len(latencies_list),
                    "avg_latency_ms": sum(latencies_list) / len(latencies_list) if latencies_list else 0,
                    "percentage": len(latencies_list) / len(successful) * 100 if successful else 0
                }
                for model, latencies_list in by_model.items()
            },
            "fallback_stats": {
                "total_fallbacks": len([r for r in self.results if r.get("fallback_from")]),
                "fallback_details": [
                    {"from": r.get("fallback_from"), "to": r.get("model")}
                    for r in self.results if r.get("fallback_from")
                ]
            },
            "error_summary": {
                "total_errors": len(self.errors),
                "by_type": {
                    error_type: len([e for e in self.errors if e["error_type"] == error_type])
                    for error_type in set(e["error_type"] for e in self.errors)
                }
            }
        }
        
        return report


Exemple d'utilisation complète

async def main(): """Exemple d'exécution du test de charge""" # Initialisation config = HolySheepConfig() client = HolySheepAIClient( api_key=config.API_KEY, config=config ) # Créer et lancer le test simulator = LoadSimulator(client, config) report = await simulator.run_load_test( virtual_users=100, duration_seconds=60, ramp_up_seconds=10 ) # Afficher le rapport print("\n" + "=" * 60) print("📊 RAPPORT DE TEST DE CHARGE") print("=" * 60) print(f"\n✅ Requêtes réussies : {report['success_metrics']['successful']}") print(f"❌ Requêtes échouées : {