From 6ac1f318d01053253b83e8581d40a0d816e707f2 Mon Sep 17 00:00:00 2001 From: Lars Date: Fri, 26 Dec 2025 10:33:40 +0100 Subject: [PATCH] =?UTF-8?q?max=20retries=20eingef=C3=BChrt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/llm_service.py | 43 +++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/app/services/llm_service.py b/app/services/llm_service.py index 9de2d89..17ecea6 100644 --- a/app/services/llm_service.py +++ b/app/services/llm_service.py @@ -6,9 +6,12 @@ DESCRIPTION: Hybrid-Client für Ollama, Google GenAI (Gemini) und OpenRouter. WP-20 Fix: Bulletproof Prompt-Auflösung für format() Aufrufe. WP-22/JSON: Optionales JSON-Schema + strict (für OpenRouter structured outputs). FIX: Intelligente Rate-Limit Erkennung (429 Handling), v1-API Sync & Timeouts. -VERSION: 3.3.6 +VERSION: 3.3.7 STATUS: Active -DEPENDENCIES: httpx, yaml, logging, asyncio, json, google-genai, openai, app.config +FIX: +- Implementiert striktes max_retries Handling für alle Provider (v.a. für Chat-Stabilität). +- Synchronisiert Rate-Limit Retries mit dem max_retries Parameter. +- Optimiert Logging für sofortige Fehlererkennung. """ import httpx import yaml @@ -84,8 +87,6 @@ class LLMService: Hole provider-spezifisches Template mit intelligenter Text-Kaskade. HINWEIS: Dies ist nur ein Text-Lookup und verbraucht kein API-Kontingent. Kaskade: Gewählter Provider -> Gemini (Cloud-Stil) -> Ollama (Basis-Stil). - - WP-20 Fix: Garantiert die Rückgabe eines Strings, um AttributeError zu vermeiden. """ active_provider = provider or self.settings.MINDNET_LLM_PROVIDER data = self.prompts.get(key, "") @@ -118,11 +119,6 @@ class LLMService: ) -> str: """ Haupteinstiegspunkt für LLM-Anfragen mit Priorisierung. - - force_json: - - Ollama: nutzt payload["format"]="json" - - Gemini: nutzt response_mime_type="application/json" - - OpenRouter: nutzt response_format=json_object (Fallback) oder json_schema """ target_provider = provider or self.settings.MINDNET_LLM_PROVIDER @@ -154,11 +150,12 @@ class LLMService: strict_json_schema: bool ) -> str: """ - Routet die Anfrage mit intelligenter Rate-Limit Erkennung (WP-20 + WP-76). - Schleife läuft über MINDNET_LLM_RATE_LIMIT_RETRIES. + Routet die Anfrage mit intelligenter Rate-Limit Erkennung. + Nutzt max_retries um die Rate-Limit Schleife zu begrenzen. """ rate_limit_attempts = 0 - max_rate_retries = getattr(self.settings, "LLM_RATE_LIMIT_RETRIES", 3) + # FIX: Wir nutzen max_retries als Limit für Rate-Limit Versuche, wenn explizit klein gewählt (z.B. Chat) + max_rate_retries = min(max_retries, getattr(self.settings, "LLM_RATE_LIMIT_RETRIES", 3)) wait_time = getattr(self.settings, "LLM_RATE_LIMIT_WAIT", 60.0) while rate_limit_attempts <= max_rate_retries: @@ -182,18 +179,17 @@ class LLMService: except Exception as e: err_str = str(e) - # Intelligente 429 Erkennung für alle Cloud-Provider + # Intelligente 429 Erkennung is_rate_limit = any(x in err_str for x in ["429", "RESOURCE_EXHAUSTED", "rate_limited", "Too Many Requests"]) if is_rate_limit and rate_limit_attempts < max_rate_retries: rate_limit_attempts += 1 logger.warning( - f"⏳ [LLMService] Rate Limit (429) detected from {provider}. " - f"Attempt {rate_limit_attempts}/{max_rate_retries}. " - f"Waiting {wait_time}s before cloud retry..." + f"⏳ [LLMService] Rate Limit detected from {provider}. " + f"Attempt {rate_limit_attempts}/{max_rate_retries}. Waiting {wait_time}s..." ) await asyncio.sleep(wait_time) - continue # Nächster Versuch in der Cloud-Schleife + continue # Wenn kein Rate-Limit oder Retries erschöpft -> Fallback zu Ollama (falls aktiviert) if self.settings.LLM_FALLBACK_ENABLED and provider != "ollama": @@ -206,14 +202,12 @@ class LLMService: async def _execute_google(self, prompt, system, force_json, model_override): """Native Google SDK Integration (Gemini) mit v1 Fix.""" model = model_override or self.settings.GEMINI_MODEL - # Fix: Bereinige Modellnamen (Entfernung von 'models/' Präfix) clean_model = model.replace("models/", "") config = types.GenerateContentConfig( system_instruction=system, response_mime_type="application/json" if force_json else "text/plain" ) - # Thread-Offloading mit striktem Timeout gegen "Hangs" response = await asyncio.wait_for( asyncio.to_thread( self.google_client.models.generate_content, @@ -233,7 +227,7 @@ class LLMService: json_schema_name: str = "mindnet_json", strict_json_schema: bool = True ) -> str: - """OpenRouter API Integration (OpenAI-kompatibel) mit Schema-Support.""" + """OpenRouter API Integration (OpenAI-kompatibel).""" model = model_override or self.settings.OPENROUTER_MODEL messages = [] if system: @@ -262,14 +256,14 @@ class LLMService: return response.choices[0].message.content.strip() async def _execute_ollama(self, prompt, system, force_json, max_retries, base_delay): - """Lokaler Ollama Call mit exponentiellem Backoff.""" + """Lokaler Ollama Call mit striktem Retry-Limit.""" payload = { "model": self.settings.LLM_MODEL, "prompt": prompt, "stream": False, "options": { "temperature": 0.1 if force_json else 0.7, - "num_ctx": 8192 + "num_ctx": 8192 # Begrenzung für Stabilität (WP-20) } } if force_json: @@ -285,9 +279,11 @@ class LLMService: return res.json().get("response", "").strip() except Exception as e: attempt += 1 + # WICHTIG: Wenn max_retries=0 (Chat), bricht dies nach dem 1. Versuch (attempt=1) sofort ab. if attempt > max_retries: - logger.error(f"❌ Ollama Error after {attempt} retries: {e}") + logger.error(f"❌ Ollama request failed after {attempt} attempt(s): {e}") raise e + wait_time = base_delay * (2 ** (attempt - 1)) logger.warning(f"⚠️ Ollama attempt {attempt} failed. Retrying in {wait_time}s...") await asyncio.sleep(wait_time) @@ -300,6 +296,7 @@ class LLMService: final_prompt = rag_template.format(context_str=context_str, query=query) + # RAG Aufrufe im Chat nutzen nun standardmäßig max_retries=2 (überschreibbar) return await self.generate_raw_response( final_prompt, system=system_prompt,