max retries eingeführt
All checks were successful
Deploy mindnet to llm-node / deploy (push) Successful in 4s

This commit is contained in:
Lars 2025-12-26 10:33:40 +01:00
parent cbaf664123
commit 6ac1f318d0

View File

@ -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-20 Fix: Bulletproof Prompt-Auflösung für format() Aufrufe.
WP-22/JSON: Optionales JSON-Schema + strict (für OpenRouter structured outputs). WP-22/JSON: Optionales JSON-Schema + strict (für OpenRouter structured outputs).
FIX: Intelligente Rate-Limit Erkennung (429 Handling), v1-API Sync & Timeouts. FIX: Intelligente Rate-Limit Erkennung (429 Handling), v1-API Sync & Timeouts.
VERSION: 3.3.6 VERSION: 3.3.7
STATUS: Active 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 httpx
import yaml import yaml
@ -84,8 +87,6 @@ class LLMService:
Hole provider-spezifisches Template mit intelligenter Text-Kaskade. Hole provider-spezifisches Template mit intelligenter Text-Kaskade.
HINWEIS: Dies ist nur ein Text-Lookup und verbraucht kein API-Kontingent. HINWEIS: Dies ist nur ein Text-Lookup und verbraucht kein API-Kontingent.
Kaskade: Gewählter Provider -> Gemini (Cloud-Stil) -> Ollama (Basis-Stil). 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 active_provider = provider or self.settings.MINDNET_LLM_PROVIDER
data = self.prompts.get(key, "") data = self.prompts.get(key, "")
@ -118,11 +119,6 @@ class LLMService:
) -> str: ) -> str:
""" """
Haupteinstiegspunkt für LLM-Anfragen mit Priorisierung. 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 target_provider = provider or self.settings.MINDNET_LLM_PROVIDER
@ -154,11 +150,12 @@ class LLMService:
strict_json_schema: bool strict_json_schema: bool
) -> str: ) -> str:
""" """
Routet die Anfrage mit intelligenter Rate-Limit Erkennung (WP-20 + WP-76). Routet die Anfrage mit intelligenter Rate-Limit Erkennung.
Schleife läuft über MINDNET_LLM_RATE_LIMIT_RETRIES. Nutzt max_retries um die Rate-Limit Schleife zu begrenzen.
""" """
rate_limit_attempts = 0 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) wait_time = getattr(self.settings, "LLM_RATE_LIMIT_WAIT", 60.0)
while rate_limit_attempts <= max_rate_retries: while rate_limit_attempts <= max_rate_retries:
@ -182,18 +179,17 @@ class LLMService:
except Exception as e: except Exception as e:
err_str = str(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"]) 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: if is_rate_limit and rate_limit_attempts < max_rate_retries:
rate_limit_attempts += 1 rate_limit_attempts += 1
logger.warning( logger.warning(
f"⏳ [LLMService] Rate Limit (429) detected from {provider}. " f"⏳ [LLMService] Rate Limit detected from {provider}. "
f"Attempt {rate_limit_attempts}/{max_rate_retries}. " f"Attempt {rate_limit_attempts}/{max_rate_retries}. Waiting {wait_time}s..."
f"Waiting {wait_time}s before cloud retry..."
) )
await asyncio.sleep(wait_time) 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) # Wenn kein Rate-Limit oder Retries erschöpft -> Fallback zu Ollama (falls aktiviert)
if self.settings.LLM_FALLBACK_ENABLED and provider != "ollama": 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): async def _execute_google(self, prompt, system, force_json, model_override):
"""Native Google SDK Integration (Gemini) mit v1 Fix.""" """Native Google SDK Integration (Gemini) mit v1 Fix."""
model = model_override or self.settings.GEMINI_MODEL model = model_override or self.settings.GEMINI_MODEL
# Fix: Bereinige Modellnamen (Entfernung von 'models/' Präfix)
clean_model = model.replace("models/", "") clean_model = model.replace("models/", "")
config = types.GenerateContentConfig( config = types.GenerateContentConfig(
system_instruction=system, system_instruction=system,
response_mime_type="application/json" if force_json else "text/plain" response_mime_type="application/json" if force_json else "text/plain"
) )
# Thread-Offloading mit striktem Timeout gegen "Hangs"
response = await asyncio.wait_for( response = await asyncio.wait_for(
asyncio.to_thread( asyncio.to_thread(
self.google_client.models.generate_content, self.google_client.models.generate_content,
@ -233,7 +227,7 @@ class LLMService:
json_schema_name: str = "mindnet_json", json_schema_name: str = "mindnet_json",
strict_json_schema: bool = True strict_json_schema: bool = True
) -> str: ) -> str:
"""OpenRouter API Integration (OpenAI-kompatibel) mit Schema-Support.""" """OpenRouter API Integration (OpenAI-kompatibel)."""
model = model_override or self.settings.OPENROUTER_MODEL model = model_override or self.settings.OPENROUTER_MODEL
messages = [] messages = []
if system: if system:
@ -262,14 +256,14 @@ class LLMService:
return response.choices[0].message.content.strip() return response.choices[0].message.content.strip()
async def _execute_ollama(self, prompt, system, force_json, max_retries, base_delay): 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 = { payload = {
"model": self.settings.LLM_MODEL, "model": self.settings.LLM_MODEL,
"prompt": prompt, "prompt": prompt,
"stream": False, "stream": False,
"options": { "options": {
"temperature": 0.1 if force_json else 0.7, "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: if force_json:
@ -285,9 +279,11 @@ class LLMService:
return res.json().get("response", "").strip() return res.json().get("response", "").strip()
except Exception as e: except Exception as e:
attempt += 1 attempt += 1
# WICHTIG: Wenn max_retries=0 (Chat), bricht dies nach dem 1. Versuch (attempt=1) sofort ab.
if attempt > max_retries: 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 raise e
wait_time = base_delay * (2 ** (attempt - 1)) wait_time = base_delay * (2 ** (attempt - 1))
logger.warning(f"⚠️ Ollama attempt {attempt} failed. Retrying in {wait_time}s...") logger.warning(f"⚠️ Ollama attempt {attempt} failed. Retrying in {wait_time}s...")
await asyncio.sleep(wait_time) await asyncio.sleep(wait_time)
@ -300,6 +296,7 @@ class LLMService:
final_prompt = rag_template.format(context_str=context_str, query=query) 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( return await self.generate_raw_response(
final_prompt, final_prompt,
system=system_prompt, system=system_prompt,