Kontext-Limit sichtbar machen: Gateway-Warnung bei finish_reason=length + Etappen-Regel

User-Wunsch 15.07. ('es sollte eine Warnung geben - Kontext ist nicht
unendlich'): 4 P3-Worker starben still an finish_reason=length.
- Gateway erkennt abgerissene Antworten (Stream-Chunk + Non-Stream) und
  meldet je Modell max. alle 10 min in den Briefkasten (silent -> Chronik),
  Zustellung per MC_ANNOUNCE_HTTP ans Steuerpult (Store bleibt Ein-Schreiber).
- Steckbrief-Hook: KONTEXT-BUDGET & ETAPPEN-REGEL - gezielt lesen, grosse
  Aufgaben in Etappen schneiden (kanban_create/complete mit Plan) statt
  am Limit zu sterben.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-15 14:43:36 +02:00
parent 3468bc9c80
commit 3cf111d973
4 changed files with 39 additions and 2 deletions
+29
View File
@@ -7,11 +7,38 @@ verstreute, still scheiternde String-Suche durch einen testbaren SSE-Zeilenparse
import json
import logging
import time
from services.token_stats import increment_tokens
log = logging.getLogger(__name__)
# Kontext-Limit-Warnung (User-Wunsch 15.07.: „es sollte eine Warnung geben — Kontext ist
# nicht unendlich"): finish_reason=length heißt, eine Antwort ist am Token-/Kontext-Budget
# ABGERISSEN — bei Nacht-Workern stirbt damit still die halbe Arbeit (4 Worker am 15.07.).
# Statt still: Eintrag in den Briefkasten (silent → Chronik/Panel, kein Sprach-Spam),
# je Modell höchstens alle 10 min. Läuft im mc2-gateway-Prozess → announce liefert per
# MC_ANNOUNCE_HTTP beim Steuerpult ab (Unit-Env), nie direkt in die Store-Datei.
_trunc_last: dict[str, float] = {}
_TRUNC_EVERY = 600.0 # s
def warn_truncation(model: str) -> None:
now = time.time()
if now - _trunc_last.get(model, 0.0) < _TRUNC_EVERY:
return
_trunc_last[model] = now
log.warning("gateway: finish_reason=length bei %s — Antwort am Kontext-/Token-Limit abgerissen", model)
try:
from services import announce
announce.add(
f"Eine Antwort von „{model}“ ist am Token-/Kontext-Limit abgerissen "
f"(finish_reason=length). War das ein Nacht-Worker, ist seine Aufgabe zu groß "
f"geschnitten — besser in Etappen teilen (eine Etappe = ein Worker-Lauf).",
"[Kontext-Limit]", "gateway", "silent")
except Exception:
log.warning("gateway: Kontext-Limit-Warnung nicht zustellbar", exc_info=True)
def record_usage(usage: dict | None, model: str) -> None:
"""Ein usage-Objekt verbuchen (no-op bei None/leer)."""
@@ -26,6 +53,8 @@ def record_usage(usage: dict | None, model: str) -> None:
def record_stream_chunk(chunk: bytes, model: str) -> None:
"""Rohen SSE-Chunk auf `usage` prüfen und Tokens verbuchen. Fehler werden
geloggt (debug) statt verschluckt — ein defekter Chunk bricht den Stream nicht."""
if b'"finish_reason":"length"' in chunk or b'"finish_reason": "length"' in chunk:
warn_truncation(model)
if b'"usage"' not in chunk:
return
text = chunk.decode("utf-8", errors="ignore")