From 3d39d3e82971a1c5c8fea055c95dfa5705a8ead4 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 14 May 2026 04:15:23 +0000 Subject: [PATCH] =?UTF-8?q?fix(ollama):=20Streaming=20statt=20Single-Reque?= =?UTF-8?q?st=20=E2=80=94=20beendet=20ReadTimeout=20bei=20langen=20Antwort?= =?UTF-8?q?en?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vorher: stream=False → der gesamte Generierungs-Lauf musste innerhalb des Read-Timeout (default 600 s) abgeschlossen sein. Bei langen Transkripten oder größeren Modellen reicht das nicht und der Mac-Worker wirft httpx.ReadTimeout. Jetzt: stream=True + httpx.Timeout(read=...) pro Chunk. Solange Ollama kontinuierlich Tokens liefert, läuft kein Timeout. Zusätzlich keep_alive 30m, damit das Modell zwischen Calls im RAM bleibt (kein erneuter Load-Overhead). Logs jetzt mit Chunk-Anzahl + Gesamtlänge — leichter zu debuggen. --- mac-worker/app/core/ollama_client.py | 46 +++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/mac-worker/app/core/ollama_client.py b/mac-worker/app/core/ollama_client.py index 056b512..b066122 100644 --- a/mac-worker/app/core/ollama_client.py +++ b/mac-worker/app/core/ollama_client.py @@ -40,19 +40,51 @@ def _truncate(text: str, max_chars: int = 60000) -> str: async def _chat(prompt: str) -> str: + """Ruft Ollama im Streaming-Modus auf. + + Streaming hat zwei wichtige Vorteile: + 1. Read-Timeout gilt pro Chunk statt für die gesamte Antwort — + lange Generierungen laufen nicht mehr in den Gesamt-Timeout. + 2. Wir können in den Logs sehen, dass das Modell tatsächlich arbeitet + (Anzahl Chunks). + + `keep_alive`: das Modell bleibt 30 min im RAM zwischen Calls, + damit nachfolgende Anfragen nicht erneut die Ladezeit zahlen. + """ url = f"{settings.ollama_url.rstrip('/')}/api/generate" payload = { "model": settings.ollama_model, "prompt": prompt, - "stream": False, + "stream": True, + "keep_alive": "30m", "options": {"temperature": 0.2}, } - log.info("Ollama generate model=%s prompt_len=%d", settings.ollama_model, len(prompt)) - async with httpx.AsyncClient(timeout=settings.ollama_timeout) as c: - r = await c.post(url, json=payload) - r.raise_for_status() - data = r.json() - return data.get("response", "") + log.info("Ollama generate model=%s prompt_len=%d (streaming)", settings.ollama_model, len(prompt)) + timeout = httpx.Timeout( + connect=15.0, + read=settings.ollama_timeout, # pro Chunk — großzügig wegen initial Modell-Load + write=30.0, + pool=10.0, + ) + chunks: list[str] = [] + chunk_count = 0 + async with httpx.AsyncClient(timeout=timeout) as c: + async with c.stream("POST", url, json=payload) as r: + r.raise_for_status() + async for line in r.aiter_lines(): + if not line.strip(): + continue + try: + obj = json.loads(line) + except json.JSONDecodeError: + log.warning("Ollama: ungültige JSON-Zeile übersprungen: %r", line[:100]) + continue + chunks.append(obj.get("response", "")) + chunk_count += 1 + if obj.get("done"): + break + log.info("Ollama: %d Chunks empfangen, Gesamtlänge %d Zeichen", chunk_count, sum(map(len, chunks))) + return "".join(chunks) async def summarize(transcript: str, title: str = "", profile_name: str | None = None) -> dict[str, Any]: