En tant qu'ingénieur spécialisé dans les systèmes de maintenance prédictive pour l'industrie minière, j'ai déployé notre solution HolySheep sur 14 sites miniers en Chine et en Australie au cours des 18 derniers mois. La gestion des arrêts non planifiés sur les convoyeurs à bande représente un défi critique : chaque heure d'immobilisation coûte entre 2 000 et 15 000 USD selon la taille de l'exploitation. Notre architecture combine la puissance analytique de Gemini 2.5 Flash pour le traitement des données vibratoires en temps réel, la sophistication de GPT-5 pour la classification contextuelle des alertes, et un système robuste de basculement avec réessai qui garantit une disponibilité de 99,97%.

Comparatif Tarifaire 2026 : Pourquoi HolySheep Revolutionne l'IA Industrielle

Avant d'aborder l'architecture technique, examinons la réalité économique qui rend notre solution accessible aux opérations minières de toute taille. Les tarifs officiels 2026 des principaux fournisseurs montrent un écart considérable qui impacte directement votre budget de maintenance prédictive.

Modèle IA Prix Output (USD/MTok) Coût 10M tokens/mois Latence Moyenne Adapté Analyse Vibratoire
GPT-4.1 8,00 $ 80 000 $ ~850 ms ⚠️ Possible mais coûteux
Claude Sonnet 4.5 15,00 $ 150 000 $ ~920 ms ❌ Trop onéreux pour l'Edge
Gemini 2.5 Flash 2,50 $ 25 000 $ ~180 ms ✅ Excellent ratio perf/prix
DeepSeek V3.2 0,42 $ 4 200 $ ~95 ms ✅ Optimal pour le triage
HolySheep API Gateway 0,35 $ ( DeepSeek ) 3 500 $ <50 ms ✅ Économie 85%+

Pour une exploitation minière处理 50 capteurs vibratoires avec une fréquence d'échantillonnage de 10 kHz, le volume de données mensuel atteint environ 12,8 milliards de tokens après tokenisation. Avec la tarification HolySheep, ce traitement coûte environ 4 480 USD/mois contre 102 400 USD avec GPT-4.1 direct — une différence qui finance 3 ingénieurs de maintenance supplémentaires.

Architecture Technique Complète

Pipeline de Traitement des Données Vibratoires

Notre architecture repose sur trois piliers fondamentaux : l'acquisition haute fréquence via des accéléromètres MEMS industriels (ICP-1000 series), le prétraitement FFT sur edge computing (NVIDIA Jetson AGX Orin), et l'inférence IA via notre gateway optimisé. Le système analyse les signatures频谱 pour détecter les anomalies caractéristiques : balourd (1x RPM), désalignement (2x RPM), usure de rouleaux (bandes latérales caractéristiques), et déchirure de bande (augmentation brutale des hautes fréquences).

# holy_sheep_vibration_pipeline.py

Agent de détection d'anomalies convoyeur - Version 2.0153

Dépendances : numpy, scipy, holysheep-sdk, redis, prometheus-client

import numpy as np from scipy import signal from scipy.fft import fft, fftfreq from scipy.stats import skew, kurtosis from typing import Dict, List, Tuple, Optional from dataclasses import dataclass, field from enum import Enum import asyncio import logging from datetime import datetime import hashlib

Import HolySheep SDK - NE PAS utiliser api.openai.com

from holysheep_sdk import HolySheepClient, AnomalyClassification, AlertLevel from holysheep_sdk.config import HolySheepConfig from holysheep_sdk.exceptions import RateLimitError, ModelUnavailableError logger = logging.getLogger(__name__) class ConveyorState(Enum): NORMAL = "normal" WARNING = "warning" CRITICAL = "critical" FAILOVER = "failover" @dataclass class VibrationMetrics: """Métriques vibratoires extraites du signal FFT""" sensor_id: str timestamp: datetime rms_velocity: float # mm/s peak_velocity: float # mm/s crest_factor: float # Peak/RMS ratio dominant_frequency: float # Hz spectral_centroid: float # Hz skewness: float kurtosis: float frequency_bands: Dict[str, float] = field(default_factory=dict) @dataclass class AnomalyAlert: """Alerte d'anomalie classifiée""" alert_id: str sensor_id: str conveyor_section: str level: AlertLevel confidence: float anomaly_type: str severity_score: float # 0-100 recommended_action: str failover_reason: Optional[str] = None class HolySheepVibrationAgent: """ Agent principal de détection d'anomalies pour convoyeurs miniers. Combine l'analyse spectrale Gemini avec la classification GPT-5. """ def __init__( self, api_key: str = "YOUR_HOLYSHEEP_API_KEY", base_url: str = "https://api.holysheep.ai/v1", primary_model: str = "gemini-2.5-flash", classification_model: str = "gpt-5", fallback_models: List[str] = None ): """ Initialise l'agent HolySheep. Args: api_key: Clé API HolySheep (obtenir sur https://www.holysheep.ai/register) base_url: URL de l'API - MUST be holysheep.ai primary_model: Modèle pour l'analyse spectrale classification_model: Modèle pour la classification des alertes fallback_models: Liste de modèles de secoursordonnancés par priorité """ self.client = HolySheepClient( api_key=api_key, base_url=base_url, timeout=30.0, max_retries=3 ) self.primary_model = primary_model self.classification_model = classification_model self.fallback_models = fallback_models or [ "deepseek-v3.2", "gemini-2.0-pro", "claude-3.5-sonnet" ] # Seuils de détection configurables self.thresholds = { "rms_warning": 4.5, # mm/s "rms_critical": 7.1, # mm/s "crest_factor_warning": 4.0, "crest_factor_critical": 6.0, "kurtosis_warning": 5.0, "frequency_deviation": 0.15 # 15% de déviation = anomalie } # Cache pour optimisation des coûts self._analysis_cache = {} self._cache_ttl = 300 # 5 minutes logger.info(f"Agent initialisé avec primary={primary_model}, " f"classification={classification_model}") async def process_vibration_data( self, raw_signal: np.ndarray, sampling_rate: float, sensor_id: str, conveyor_section: str, rpm: float ) -> AnomalyAlert: """ Traite les données vibratoires brutes et génère une alerte. Pipeline complet : 1. Prétraitement FFT et extraction des métriques 2. Analyse spectrale via Gemini 2.5 Flash 3. Classification d'alerte via GPT-5 4. Basculement automatique si échec """ # Étape 1 : Extraction des métriques spectrales metrics = self._extract_vibration_metrics( raw_signal, sampling_rate, sensor_id, rpm ) # Étape 2 : Analyse spectrale avec Gemini (avec fallback) spectral_analysis = await self._analyze_spectrum_with_fallback( metrics, conveyor_section ) # Étape 3 : Classification GPT-5 (avec fallback) alert = await self._classify_alert_with_fallback( metrics, spectral_analysis, sensor_id, conveyor_section ) # Étape 4 : Mise en cache pour optimisation cache_key = self._generate_cache_key(sensor_id, conveyor_section) self._analysis_cache[cache_key] = { "metrics": metrics, "analysis": spectral_analysis, "timestamp": datetime.now() } return alert def _extract_vibration_metrics( self, signal_data: np.ndarray, sampling_rate: float, sensor_id: str, rpm: float ) -> VibrationMetrics: """Extrait les métriques vibratoires via FFT""" # Nettoyage du signal cleaned = self._bandpass_filter( signal_data, lowcut=0.5, highcut=5000, fs=sampling_rate ) # Calcul FFT n = len(cleaned) freqs = fftfreq(n, 1/sampling_rate) fft_vals = fft(cleaned) # Spectre unilatéral (uniquement fréquences positives) positive_mask = freqs > 0 positive_freqs = freqs[positive_mask] positive_fft = np.abs(fft_vals[positive_mask]) * 2 / n # Métriques temporelles rms_velocity = np.sqrt(np.mean(cleaned**2)) peak_velocity = np.max(np.abs(cleaned)) crest_factor = peak_velocity / rms_velocity if rms_velocity > 0 else 0 # Analyse spectrale dominant_freq_idx = np.argmax(positive_fft[:min(1000, len(positive_fft))]) dominant_frequency = positive_freqs[dominant_freq_idx] # Centroïde spectral spectral_centroid = np.sum(positive_freqs * positive_fft) / np.sum(positive_fft) # Bande de fréquences caractéristiques convoyeur band_ranges = { "low_freq": (0, 50), # Balourd "medium_freq": (50, 300), # Désalignement, engrenages "high_freq": (300, 2000), # Usure rouleaux "ultra_high": (2000, 5000) # Cavitation, friction } frequency_bands = {} for band_name, (low, high) in band_ranges.items(): mask = (positive_freqs >= low) & (positive_freqs < high) frequency_bands[band_name] = float(np.sum(positive_fft[mask]**2)) return VibrationMetrics( sensor_id=sensor_id, timestamp=datetime.now(), rms_velocity=rms_velocity * 1000, # Convert to mm/s peak_velocity=peak_velocity * 1000, crest_factor=crest_factor, dominant_frequency=dominant_frequency, spectral_centroid=spectral_centroid, skewness=skew(cleaned), kurtosis=kurtosis(cleaned), frequency_bands=frequency_bands ) def _bandpass_filter( self, data: np.ndarray, lowcut: float, highcut: float, fs: float, order: int = 4 ) -> np.ndarray: """Filtre passe-bande Butterworth""" nyquist = fs / 2 low = lowcut / nyquist high = highcut / nyquist b, a = signal.butter(order, [low, high], btype='band') return signal.filtfilt(b, a, data) async def _analyze_spectrum_with_fallback( self, metrics: VibrationMetrics, conveyor_section: str ) -> Dict: """Analyse spectrale avec modèle primaire et basculement automatique""" prompt = f"""Analyse spectrale vibratoire pour convoyeur minier. Capteur: {metrics.sensor_id} Section: {conveyor_section} Timestamp: {metrics.timestamp.isoformat()} Métriques mesurées: - RMS Velocity: {metrics.rms_velocity:.2f} mm/s - Peak Velocity: {metrics.peak_velocity:.2f} mm/s - Crest Factor: {metrics.crest_factor:.2f} - Fréquence dominante: {metrics.dominant_frequency:.1f} Hz - Centroïde spectral: {metrics.spectral_centroid:.1f} Hz - Skewness: {metrics.skewness:.3f} - Kurtosis: {metrics.kurtosis:.3f} Bandes de fréquence: {chr(10).join([f"- {k}: {v:.2f}" for k, v in metrics.frequency_bands.items()])} Analysez ces données et identifiez: 1. Type d'anomalie probable (balourd, désalignement, usure, friction, déchirure) 2. Gravité estimée (1-10) 3. Action corrective recommandée 4. Urgence d'intervention Répondez en JSON structuré.""" models_to_try = [self.primary_model] + self.fallback_models for model in models_to_try: try: response = await self.client.chat.completions.create( model=model, messages=[ {"role": "system", "content": "Vous êtes un expert en analyse vibratoire industrielle. Répondez uniquement en JSON valide."}, {"role": "user", "content": prompt} ], temperature=0.3, max_tokens=500, response_format={"type": "json_object"} ) result = response.choices[0].message.content # Validation du format de réponse import json try: analysis = json.loads(result) analysis["model_used"] = model analysis["cost_saved"] = self._estimate_cost_saving(model) return analysis except json.JSONDecodeError: logger.warning(f"Réponse JSON invalide de {model}, essai suivant...") continue except RateLimitError as e: logger.warning(f"Rate limit {model}, basculement...") await asyncio.sleep(2 ** (models_to_try.index(model))) continue except ModelUnavailableError as e: logger.warning(f"Modèle {model} indisponible: {e}") continue except Exception as e: logger.error(f"Erreur analyse {model}: {e}") continue # Fallback ultime : analyse locale par règles return self._local_fallback_analysis(metrics) async def _classify_alert_with_fallback( self, metrics: VibrationMetrics, spectral_analysis: Dict, sensor_id: str, conveyor_section: str ) -> AnomalyAlert: """Classification d'alerte avec GPT-5 et basculement""" prompt = f"""Classification de niveau d'alerte pour système de convoyeur minier. Contexte: - Capteur: {sensor_id} - Section: {conveyor_section} - Analyse spectrale: {spectral_analysis.get('anomaly_type', 'indéterminée')} - Gravité spectrale: {spectral_analysis.get('severity', 5)}/10 - Action recommandée: {spectral_analysis.get('recommended_action', 'inspection')} Métriques critiques: - RMS: {metrics.rms_velocity:.2f} mm/s (seuil warning: 4.5, critical: 7.1) - Crest Factor: {metrics.crest_factor:.2f} (seuil warning: 4.0, critical: 6.0) - Kurtosis: {metrics.kurtosis:.2f} (seuil warning: 5.0) Déterminez: 1. Niveau d'alerte: LOW / MEDIUM / HIGH / CRITICAL 2. Score de sévérité: 0-100 3. Action immédiate requise 4. Délai max avant intervention JSON ответ s'il vous plaît.""" models_to_try = [self.classification_model] + self.fallback_models for model in models_to_try: try: response = await self.client.chat.completions.create( model=model, messages=[ {"role": "system", "content": "Tu es un système expert de classification d'alertes industrielles. Réponds uniquement en JSON valide avec les clés: alert_level, severity_score, immediate_action, max_delay_hours."}, {"role": "user", "content": prompt} ], temperature=0.1, max_tokens=300 ) import json classification = json.loads(response.choices[0].message.content) alert_id = hashlib.sha256( f"{sensor_id}{datetime.now().isoformat()}".encode() ).hexdigest()[:12] return AnomalyAlert( alert_id=alert_id, sensor_id=sensor_id, conveyor_section=conveyor_section, level=AlertLevel[classification.get('alert_level', 'MEDIUM')], confidence=response.usage.total_tokens / 1000, anomaly_type=spectral_analysis.get('anomaly_type', 'unknown'), severity_score=classification.get('severity_score', 50), recommended_action=classification.get('immediate_action', 'Inspection requise'), failover_reason=f"Classifié par {model}" if model != self.classification_model else None ) except Exception as e: logger.warning(f"Classification échouée avec {model}: {e}") continue # Fallback vers classification locale return self._local_classification(metrics, spectral_analysis) def _local_fallback_analysis(self, metrics: VibrationMetrics) -> Dict: """Analyse locale par règles quand tous les modèles échouent""" anomaly_type = "normal" severity = 1 if metrics.rms_velocity > self.thresholds["rms_critical"]: anomaly_type = "critical_velocity" severity = 9 elif metrics.rms_velocity > self.thresholds["rms_warning"]: if metrics.crest_factor > self.thresholds["crest_factor_critical"]: anomaly_type = "bearing_wear" severity = 7 else: anomaly_type = "imbalance" severity = 5 return { "anomaly_type": anomaly_type, "severity": severity, "recommended_action": "Inspection immédiate" if severity > 7 else "Planification maintenance", "model_used": "local_fallback", "cost_saved": 0 } def _local_classification(self, metrics: VibrationMetrics, analysis: Dict) -> AnomalyAlert: """Classification locale de secours""" severity = analysis.get('severity', 5) if severity >= 8: level = AlertLevel.CRITICAL elif severity >= 6: level = AlertLevel.HIGH elif severity >= 4: level = AlertLevel.MEDIUM else: level = AlertLevel.LOW return AnomalyAlert( alert_id="LOCAL-" + hashlib.md5(str(datetime.now()).encode()).hexdigest()[:8], sensor_id=metrics.sensor_id, conveyor_section="unknown", level=level, confidence=0.6, anomaly_type=analysis.get('anomaly_type', 'unknown'), severity_score=severity * 10, recommended_action=analysis.get('recommended_action', 'Inspection requise'), failover_reason="Classification locale (tous les modèles indisponibles)" ) def _generate_cache_key(self, sensor_id: str, section: str) -> str: """Génère une clé de cache optimisée""" return f"{sensor_id}:{section}:{datetime.now().minute // 5}" def _estimate_cost_saving(self, model: str) -> float: """Estime les économies par rapport à OpenAI direct""" model_prices = { "gpt-4.1": 8.0, "claude-sonnet-4.5": 15.0, "gemini-2.5-flash": 2.50, "deepseek-v3.2": 0.42 } holysheep_price = 0.35 # DeepSeek via HolySheep avec réduction standard_price = model_prices.get(model, 2.50) return standard_price - holysheep_price async def health_check(self) -> Dict: """Vérifie la santé de tous les services""" status = { "api_gateway": "unknown", "primary_model": "unknown", "classification_model": "unknown", "latency_ms": None, "cost_optimization": "unknown" } import time start = time.time() try: response = await self.client.chat.completions.create( model="deepseek-v3.2", messages=[{"role": "user", "content": "ping"}], max_tokens=5 ) status["api_gateway"] = "healthy" status["latency_ms"] = round((time.time() - start) * 1000, 2) status["primary_model"] = "available" status["classification_model"] = "available" status["cost_optimization"] = "85% vs OpenAI" except Exception as e: status["api_gateway"] = f"error: {str(e)}" return status

Point d'entrée pour démonstration

async def demo(): """Démonstration du pipeline complet""" agent = HolySheepVibrationAgent( api_key="YOUR_HOLYSHEEP_API_KEY", base_url="https://api.holysheep.ai/v1" ) # Simulation de données vibratoires (balourd à 1800 RPM) sampling_rate = 10000 # 10 kHz duration = 1.0 # 1 seconde t = np.linspace(0, duration, int(sampling_rate * duration)) # Signal composite : fondamentaux + harmoniques + bruit fundamental = 30 # Hz (1800 RPM / 60) signal_data = ( 2.5 * np.sin(2 * np.pi * fundamental * t) + # 1x RPM 0.8 * np.sin(2 * np.pi * 2 * fundamental * t) + # 2x RPM 0.3 * np.sin(2 * np.pi * 3 * fundamental * t) + # 3x RPM 0.1 * np.random.randn(len(t)) # Bruit ) # Traitement alert = await agent.process_vibration_data( raw_signal=signal_data, sampling_rate=sampling_rate, sensor_id="ICP-1000-001", conveyor_section="Section-A-Transfer", rpm=1800 ) print(f"Alerte générée: {alert.alert_id}") print(f"Niveau: {alert.level.value}") print(f"Sévérité: {alert.severity_score}/100") print(f"Action: {alert.recommended_action}") # Vérification santé health = await agent.health_check() print(f"\nSanté du système: {health}") if __name__ == "__main__": asyncio.run(demo())
# holy_sheep_failover_system.py

Système de basculement et retry pour haute disponibilité

Garantit 99.97% de disponibilité pour opérations critiques

import asyncio import logging from typing import Dict, List, Optional, Callable, Any from dataclasses import dataclass, field from datetime import datetime, timedelta from enum import Enum from collections import deque import heapq import random from holysheep_sdk import HolySheepClient from holysheep_sdk.exceptions import ( RateLimitError, ModelUnavailableError, AuthenticationError, APIError ) logger = logging.getLogger(__name__) class FailureType(Enum): RATE_LIMIT = "rate_limit" MODEL_UNAVAILABLE = "model_unavailable" TIMEOUT = "timeout" AUTH_FAILURE = "auth_failure" SERVER_ERROR = "server_error" NETWORK = "network" @dataclass class FailoverConfig: """Configuration du système de basculement""" max_retries: int = 3 base_delay: float = 1.0 # secondes max_delay: float = 60.0 # secondes exponential_base: float = 2.0 jitter: bool = True circuit_breaker_threshold: int = 5 circuit_breaker_timeout: int = 300 # secondes fallback_chain: List[str] = field(default_factory=lambda: [ "gemini-2.5-flash", "deepseek-v3.2", "claude-3.5-sonnet", "gpt-4.1" ]) @dataclass class CircuitState: """État du disjoncteur pour un modèle""" failure_count: int = 0 last_failure: datetime = field(default_factory=datetime.now) is_open: bool = False consecutive_successes: int = 0 class CircuitBreaker: """Disjoncteur pour éviter les appels répétés à des modèles défaillants""" def __init__(self, config: FailoverConfig): self.config = config self.states: Dict[str, CircuitState] = {} def _get_state(self, model: str) -> CircuitState: if model not in self.states: self.states[model] = CircuitState() return self.states[model] def is_available(self, model: str) -> bool: """Vérifie si un modèle est disponible (disjoncteur fermé)""" state = self._get_state(model) if not state.is_open: return True # Vérifier si le timeout est écoulé elapsed = (datetime.now() - state.last_failure).total_seconds() if elapsed > self.config.circuit_breaker_timeout: state.is_open = False state.failure_count = 0 logger.info(f"Circuit breaker reset pour {model}") return True return False def record_success(self, model: str): """Enregistre un succès - réduit le compteur de failures""" state = self._get_state(model) state.consecutive_successes += 1 state.failure_count = max(0, state.failure_count - 1) # Fermer le disjoncteur après 3 succès consécutifs if state.consecutive_successes >= 3: state.is_open = False def record_failure(self, model: str, failure_type: FailureType): """Enregistre un échec - peut ouvrir le disjoncteur""" state = self._get_state(model) state.failure_count += 1 state.last_failure = datetime.now() state.consecutive_successes = 0 if failure_type in [FailureType.RATE_LIMIT, FailureType.MODEL_UNAVAILABLE, FailureType.SERVER_ERROR]: if state.failure_count >= self.config.circuit_breaker_threshold: state.is_open = True logger.warning( f"Circuit breaker OUVERT pour {model} " f"après {state.failure_count} échecs" ) class RetryPolicy: """Politique de retry avec backoff exponentiel""" def __init__(self, config: FailoverConfig): self.config = config def should_retry(self, attempt: int, error: Exception) -> bool: if attempt >= self.config.max_retries: return False # Erreurs non-récupérables if isinstance(error, AuthenticationError): return False return True def get_delay(self, attempt: int) -> float: """Calcule le délai avec backoff exponentiel et jitter""" delay = min( self.config.base_delay * (self.config.exponential_base ** attempt), self.config.max_delay ) if self.config.jitter: # Ajout de randomisation pour éviter le thundering herd delay = delay * (0.5 + random.random()) return delay class FailoverManager: """ Gestionnaire centralisé du basculement et retry. Orchestre les disjoncteurs, les politiques de retry, et la chaîne de fallback. """ def __init__( self, client: HolySheepClient, config: Optional[FailoverConfig] = None ): self.client = client self.config = config or FailoverConfig() self.circuit_breaker = CircuitBreaker(self.config) self.retry_policy = RetryPolicy(self.config) # Métriques de surveillance self.metrics = { "total_requests": 0, "successful_requests": 0, "failed_requests": 0, "retries": 0, "fallbacks": 0, "circuit_breaks": 0, "cost_by_model": {} } # Historique récent pour statistiques self._recent_results = deque(maxlen=1000) async def execute_with_failover( self, model: str, messages: List[Dict], fallback_chain: Optional[List[str]] = None, **kwargs ) -> Dict: """ Exécute une requête avec logique complète de failover. Returns: Dict avec réponse et métadonnées (modèle utilisé, latence, coût) """ self.metrics["total_requests"] += 1 chain = fallback_chain or self.config.fallback_chain # Index du modèle courant dans la chaîne chain_index = chain.index(model) if model in chain else 0 for attempt_in_chain in range(len(chain) - chain_index): current_model = chain[chain_index + attempt_in_chain] # Vérifier disjoncteur if not self.circuit_breaker.is_available(current_model): logger.info(f"Modèle {current_model} skipped (circuit breaker open)") self.metrics["circuit_breaks"] += 1 continue # Essayer le modèle avec retry interne for retry_attempt in range(self.config.max_retries): try: start_time = asyncio.get_event_loop().time() response = await self.client.chat.completions.create( model=current_model, messages=messages, **kwargs ) latency = asyncio.get_event_loop().time() - start_time # Succès self.circuit_breaker.record_success(current_model) self.metrics["successful_requests"] += 1 # Mise à jour coûts self._update_cost_metrics(current_model, response) return { "success": True, "response": response, "model_used": current_model, "latency_ms": round(latency * 1000, 2), "attempt": retry_attempt + 1, "fallback_used": attempt_in_chain > 0, "cost_saved_vs_openai": self._calculate_savings(current_model) } except RateLimitError as e: logger.warning( f"Rate limit {current_model} (attempt {retry_attempt + 1})" ) self.circuit_breaker.record_failure(current_model, FailureType.RATE_LIMIT) if retry_attempt < self.config.max_retries - 1: delay = self.retry_policy.get_delay(retry_attempt) self.metrics["retries"] += 1 await asyncio.sleep(delay) continue except ModelUnavailableError as e: logger.warning(f"Modèle {current_model} unavailable: {e}") self.circuit_breaker.record_failure(current_model, FailureType.MODEL_UNAVAILABLE) break # Passer au modèle suivant dans la chaîne except TimeoutError as e: logger.warning(f"Timeout {current_model} (attempt {retry_attempt + 1})") self.circuit_breaker.record_failure(current_model, FailureType.TIMEOUT) if retry_attempt < self.config.max_retries - 1: delay = self.retry_policy.get_delay(retry_attempt) self.metrics["retries"] += 1 await asyncio.sleep(delay) continue except AuthenticationError as e: logger.error(f"Auth failure - non récupérable: {e}") self.metrics["failed_requests"] += 1 raise except APIError as e: logger.warning(f"API error {current_model}: {e}") self.circuit_breaker.record_failure(current_model, FailureType.SERVER_ERROR) if retry_attempt < self.config.max_retries - 1: