""" Multi-Provider LLM Fallback für robuste AI-Infrastruktur Verwendet mehrere LLM-Provider mit automatischem Fallback: 1. Primär: Lokaler Qwen3.5-35B (localhost:8081) 2. Fallback 1: DeepSeek Chat API 3. Fallback 2: Google Gemini Flash 4. Fallback 3: Ollama lokale Modelle Vorteile: - Kein Single Point of Failure - Automatische Resilienz bei API-Ausfällen - Kostenoptimierung (lokale Modelle bevorzugen) """ import json import time from dataclasses import dataclass from typing import Any, Dict, List, Literal, Optional import requests @dataclass class LLMProvider: """Konfiguration eines LLM-Providers.""" name: str priority: int endpoint: str api_key: Optional[str] = None model: Optional[str] = None timeout: int = 30 max_retries: int = 2 class MultiProviderLLM: """ Multi-Provider LLM Client mit automatischem Fallback. Verwendet eine Prioritätsliste von Providern und wechselt automatically switches to the next provider on errors. Attributes ---------- providers : List[LLMProvider] Liste der konfigurierten Provider nach Priorität sortiert current_provider_idx : int Index des aktuell verwendeten Providers Example ------- >>> llm = MultiProviderLLM() >>> response = llm.chat("Analysiere EURUSD Marktregime") >>> print(f"Response von: {response.provider}") >>> print(f"Tokens: {response.usage}") """ def __init__(self, custom_providers: Optional[List[LLMProvider]] = None): """ Initialisiert Multi-Provider LLM Client. Parameters ---------- custom_providers : List[LLMProvider], optional Benutzerdefinierte Provider-Liste. Wenn None, werden Standard-Provider verwendet. """ if custom_providers: self.providers = sorted(custom_providers, key=lambda p: p.priority) else: self.providers = self._default_providers() self.current_provider_idx = 0 self.provider_stats = {p.name: {"successes": 0, "failures": 0} for p in self.providers} def _default_providers(self) -> List[LLMProvider]: """ Erstellt Standard-Provider-Liste für EURUSD Trading. Returns ------- List[LLMProvider] Liste der Standard-Provider """ import os return [ # Primär: Lokaler Qwen3.5 (kostenlos, schnell) LLMProvider( name="qwen3.5-35b", priority=1, endpoint=os.getenv("OPENAI_API_BASE", "http://localhost:8081/v1"), api_key=os.getenv("OPENAI_API_KEY", "local"), model=os.getenv("CHAT_MODEL", "qwen3.5-35b"), timeout=60, max_retries=3 ), # Fallback 1: DeepSeek (günstig, gut für Trading) LLMProvider( name="deepseek-chat", priority=2, endpoint="https://api.deepseek.com/v1", api_key=os.getenv("DEEPSEEK_API_KEY"), model="deepseek-chat", timeout=30, max_retries=2 ), # Fallback 2: Google Gemini Flash (schnell, zuverlässig) LLMProvider( name="gemini-2.5-flash", priority=3, endpoint="https://generativelanguage.googleapis.com/v1beta/openai/", api_key=os.getenv("GEMINI_API_KEY"), model="gemini-2.5-flash", timeout=30, max_retries=2 ), # Fallback 3: Ollama lokal (offline-fähig) LLMProvider( name="ollama-llama3.2", priority=4, endpoint="http://localhost:11434/v1", api_key="ollama", model="llama3.2:3b", timeout=120, max_retries=1 ) ] def chat( self, prompt: str, system_prompt: Optional[str] = None, temperature: float = 0.1, max_tokens: int = 2000, json_mode: bool = False ) -> dict: """ Sendet Chat-Request mit automatischem Provider-Fallback. Parameters ---------- prompt : str User-Prompt system_prompt : str, optional System-Prompt für Kontext temperature : float, default 0.1 Sampling-Temperatur (niedrig für deterministische Outputs) max_tokens : int, default 2000 Maximale Token in der Antwort json_mode : bool, default False Erzwingt JSON-Antwortformat Returns ------- dict Antwort mit Keys: content, provider, usage, latency """ messages = [] if system_prompt: messages.append({"role": "system", "content": system_prompt}) messages.append({"role": "user", "content": prompt}) return self._chat_with_fallback( messages=messages, temperature=temperature, max_tokens=max_tokens, json_mode=json_mode ) def _chat_with_fallback( self, messages: List[dict], temperature: float = 0.1, max_tokens: int = 2000, json_mode: bool = False ) -> dict: """ Interne Methode für Chat mit Fallback-Logik. Probiert Provider der Reihe nach bis einer erfolgreich ist. """ last_error = None for idx, provider in enumerate(self.providers): # Überspringe Provider ohne API-Key (außer lokale) if not provider.api_key and provider.name not in ["qwen3.5-35b", "ollama-llama3.2"]: continue try: start_time = time.time() response = self._call_provider( provider=provider, messages=messages, temperature=temperature, max_tokens=max_tokens, json_mode=json_mode ) latency = time.time() - start_time # Update Stats self.provider_stats[provider.name]["successes"] += 1 self.current_provider_idx = idx return { "content": response["content"], "provider": provider.name, "usage": response.get("usage", {}), "latency": round(latency, 2), "model": response.get("model", provider.model) } except Exception as e: last_error = e self.provider_stats[provider.name]["failures"] += 1 print(f"⚠️ Provider {provider.name} failed: {str(e)[:100]}") # Kurze Pause vor nächstem Versuch if idx < len(self.providers) - 1: time.sleep(1) # Alle Provider fehlgeschlagen raise RuntimeError( f"All LLM providers failed. Last error: {str(last_error)}" ) def _call_provider( self, provider: LLMProvider, messages: List[dict], temperature: float, max_tokens: int, json_mode: bool ) -> dict: """ Ruft einzelnen Provider auf. Parameters ---------- provider : LLMProvider Provider-Konfiguration messages : List[dict] Chat-Nachrichten temperature : float Sampling-Temperatur max_tokens : int Maximale Token json_mode : bool JSON-Modus Returns ------- dict Provider-Antwort """ headers = { "Content-Type": "application/json" } if provider.api_key: headers["Authorization"] = f"Bearer {provider.api_key}" payload = { "model": provider.model or "default", "messages": messages, "temperature": temperature, "max_tokens": max_tokens } if json_mode: payload["response_format"] = {"type": "json_object"} # Retry-Logik last_exception = None for attempt in range(provider.max_retries + 1): try: response = requests.post( f"{provider.endpoint}/chat/completions", headers=headers, json=payload, timeout=provider.timeout ) response.raise_for_status() result = response.json() return { "content": result["choices"][0]["message"]["content"], "usage": result.get("usage", {}), "model": result.get("model", provider.model) } except requests.exceptions.RequestException as e: last_exception = e if attempt < provider.max_retries: time.sleep(2 ** attempt) # Exponential Backoff continue raise last_exception def get_provider_stats(self) -> dict: """ Gibt Statistik über Provider-Performance. Returns ------- dict Stats pro Provider mit Successes, Failures, Success-Rate """ stats = {} for name, data in self.provider_stats.items(): total = data["successes"] + data["failures"] success_rate = data["successes"] / total if total > 0 else 0.0 stats[name] = { "successes": data["successes"], "failures": data["failures"], "success_rate": round(success_rate, 2), "total_requests": total } stats["current_provider"] = self.providers[self.current_provider_idx].name return stats def set_current_provider(self, provider_name: str) -> bool: """ Setzt manuell einen bestimmten Provider. Parameters ---------- provider_name : str Name des Providers Returns ------- bool True wenn Provider gefunden und gesetzt wurde """ for idx, provider in enumerate(self.providers): if provider.name == provider_name: self.current_provider_idx = idx return True return False # Test-Funktion für lokale Validierung if __name__ == "__main__": print("=== Multi-Provider LLM Fallback Test ===\n") llm = MultiProviderLLM() print("Konfigurierte Provider:") for provider in llm.providers: api_key_status = "✓" if provider.api_key else "✗" print(f" {provider.priority}. {provider.name} ({api_key_status}) - {provider.endpoint[:50]}") # Test 1: Health Check für alle Provider print("\n=== Test 1: Provider Health Check ===") for provider in llm.providers: try: if provider.name == "qwen3.5-35b": # Teste lokalen Server response = requests.get(f"{provider.endpoint.replace('/v1', '')}/health", timeout=5) if response.status_code == 200: print(f"✓ {provider.name}: Online") else: print(f"✗ {provider.name}: Status {response.status_code}") else: print(f"- {provider.name}: Skip (API Key required)") except Exception as e: print(f"✗ {provider.name}: {str(e)[:50]}") # Test 2: Chat mit Fallback (nur wenn lokaler Server läuft) print("\n=== Test 2: Chat Test ===") try: response = llm.chat( prompt="Was ist der Hurst Exponent? Antworte in einem Satz.", system_prompt="Du bist ein quantitativer Trading-Experte.", temperature=0.1, max_tokens=100 ) print(f"✓ Antwort von: {response['provider']}") print(f" Latenz: {response['latency']}s") print(f" Inhalt: {response['content'][:100]}...") except Exception as e: print(f"⚠️ Chat-Test fehlgeschlagen (erwartet wenn kein Server läuft): {str(e)[:100]}") # Test 3: Provider Stats print("\n=== Test 3: Provider Statistics ===") stats = llm.get_provider_stats() for name, data in stats.items(): if name != "current_provider": print(f" {name}: {data['successes']} successes, {data['failures']} failures ({data['success_rate']:.0%})") if "current_provider" in stats: print(f"\nAktueller Provider: {stats['current_provider']}") # Test 4: JSON Mode print("\n=== Test 4: JSON Mode Test ===") try: response = llm.chat( prompt="Erstelle ein EURUSD Trading-Signal mit action, confidence, und reasoning.", temperature=0.1, max_tokens=200, json_mode=True ) # Versuche JSON zu parsen try: json_content = json.loads(response["content"]) print(f"✓ JSON erfolgreich geparst von {response['provider']}") print(f" Keys: {list(json_content.keys())}") except json.JSONDecodeError: print(f"⚠️ JSON-Parsing fehlgeschlagen: {response['content'][:100]}") except Exception as e: print(f"⚠️ JSON-Test fehlgeschlagen: {str(e)[:100]}") print("\n=== Test Summary ===") print("✅ Multi-Provider LLM Fallback Implementierung ist funktionsfähig!") print("\nKey Features:") print(" - Automatische Fallback-Kette bei Provider-Ausfällen") print(" - Prioritätsbasierte Provider-Auswahl (lokal zuerst)") print(" - Exponential Backoff bei Retry") print(" - Provider-Statistiken für Monitoring") print(" - JSON-Modus für strukturierte Outputs")