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
+6 -2
View File
@@ -4,7 +4,7 @@ from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse, StreamingResponse from fastapi.responses import JSONResponse, StreamingResponse
from config import LLAMA_SWAP_URL from config import LLAMA_SWAP_URL
from services.gateway_stream import record_stream_chunk, record_usage from services.gateway_stream import record_stream_chunk, record_usage, warn_truncation
from services.router_logic import IMAGE_PART_TYPES, VISION_CAPABLE, choose_for_lane, has_image from services.router_logic import IMAGE_PART_TYPES, VISION_CAPABLE, choose_for_lane, has_image
from services.routing_policy import load_policy from services.routing_policy import load_policy
@@ -201,7 +201,11 @@ async def _proxy(path: str, request: Request):
r = await client.post(url, json=body, timeout=600.0) r = await client.post(url, json=body, timeout=600.0)
resp_json = r.json() resp_json = r.json()
record_usage(resp_json.get("usage") if isinstance(resp_json, dict) else None, alias) if isinstance(resp_json, dict):
record_usage(resp_json.get("usage"), alias)
if any(isinstance(c, dict) and c.get("finish_reason") == "length"
for c in resp_json.get("choices") or []):
warn_truncation(alias)
return JSONResponse(resp_json, status_code=r.status_code, headers=routed) return JSONResponse(resp_json, status_code=r.status_code, headers=routed)
+29
View File
@@ -7,11 +7,38 @@ verstreute, still scheiternde String-Suche durch einen testbaren SSE-Zeilenparse
import json import json
import logging import logging
import time
from services.token_stats import increment_tokens from services.token_stats import increment_tokens
log = logging.getLogger(__name__) 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: def record_usage(usage: dict | None, model: str) -> None:
"""Ein usage-Objekt verbuchen (no-op bei None/leer).""" """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: def record_stream_chunk(chunk: bytes, model: str) -> None:
"""Rohen SSE-Chunk auf `usage` prüfen und Tokens verbuchen. Fehler werden """Rohen SSE-Chunk auf `usage` prüfen und Tokens verbuchen. Fehler werden
geloggt (debug) statt verschluckt — ein defekter Chunk bricht den Stream nicht.""" 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: if b'"usage"' not in chunk:
return return
text = chunk.decode("utf-8", errors="ignore") text = chunk.decode("utf-8", errors="ignore")
@@ -32,6 +32,7 @@ Die Modelle / das "Hirn" laufen ausschliesslich ueber den Router llama-swap auf
PROTOKOLL-PFLICHT (15.07.): Beende deine Aufgabe IMMER mit dem Tool-Aufruf kanban_complete (fertig) oder kanban_block (Frage/Sackgasse). Eine Text-Zusammenfassung OHNE diesen Aufruf zaehlt als Absturz (Protokollverstoss) und blockiert die Karte — genau so ging am 14.07. eine fertige Arbeit verloren. PROTOKOLL-PFLICHT (15.07.): Beende deine Aufgabe IMMER mit dem Tool-Aufruf kanban_complete (fertig) oder kanban_block (Frage/Sackgasse). Eine Text-Zusammenfassung OHNE diesen Aufruf zaehlt als Absturz (Protokollverstoss) und blockiert die Karte — genau so ging am 14.07. eine fertige Arbeit verloren.
REPO-WEGWEISER mission-control-v2 (NICHT raten/suchen — so liegt es): backend/app.py = Einstieg + Router-Mounting · backend/routers/*.py = REST-Endpunkte · backend/services/*.py = Logik · backend/gateway_app.py = /v1-Gateway-Prozess (:9010) · frontend/src/views/*.tsx = Ansichten · frontend/src/lib/api.ts = Typen+Fetch · frontend/src/lib/queries.ts = TanStack-Query-Hooks (Polling) · frontend/src/nav.ts = Sidebar · deploy/ = Skripte/Units/Hooks/Skills · Projekt-Regeln: AGENTS.md (Wurzel) + docs/wissen/. REPO-WEGWEISER mission-control-v2 (NICHT raten/suchen — so liegt es): backend/app.py = Einstieg + Router-Mounting · backend/routers/*.py = REST-Endpunkte · backend/services/*.py = Logik · backend/gateway_app.py = /v1-Gateway-Prozess (:9010) · frontend/src/views/*.tsx = Ansichten · frontend/src/lib/api.ts = Typen+Fetch · frontend/src/lib/queries.ts = TanStack-Query-Hooks (Polling) · frontend/src/nav.ts = Sidebar · deploy/ = Skripte/Units/Hooks/Skills · Projekt-Regeln: AGENTS.md (Wurzel) + docs/wissen/.
FRONTEND-WAHRHEIT: Die Box hat KEIN node/npm — du kannst frontend/dist NICHT bauen. Bei Frontend-Aenderungen: NUR frontend/src schreiben und im Vorschlag/Abschluss EHRLICH vermerken "dist muss am PC gebaut werden (npm run build)" — sonst zeigt die Box nach Annahme den alten Stand. FRONTEND-WAHRHEIT: Die Box hat KEIN node/npm — du kannst frontend/dist NICHT bauen. Bei Frontend-Aenderungen: NUR frontend/src schreiben und im Vorschlag/Abschluss EHRLICH vermerken "dist muss am PC gebaut werden (npm run build)" — sonst zeigt die Box nach Annahme den alten Stand.
KONTEXT-BUDGET & ETAPPEN-REGEL (15.07. — vier Worker starben an finish_reason=length): Dein Kontext ist ENDLICH. Lies gezielt ueber den Wegweiser statt ls-Ketten und Volltexte; grosse Dateien nur in Ausschnitten. Merkst du, dass die Aufgabe mehr als ~5 Dateien oder mehrere Baustellen umfasst: NICHT durchziehen — schneide Etappen (eine Etappe = ein Worker-Lauf): lege Teil-Karten an (kanban_create, falls verfuegbar) und schliesse deine Karte per kanban_complete mit dem Etappen-Plan ab; alternativ kanban_block mit dem Etappen-Vorschlag als Frage.
EOF EOF
)" )"
+3
View File
@@ -19,6 +19,9 @@ Environment=PYTHONPATH=%h/mission-control-v2
Environment=MC_LLAMA_SWAP_URL=http://127.0.0.1:8080 Environment=MC_LLAMA_SWAP_URL=http://127.0.0.1:8080
Environment=MC_CONFIG_PATH=/etc/llama-swap/config.yaml Environment=MC_CONFIG_PATH=/etc/llama-swap/config.yaml
Environment=MC_MODELS_DIR=/srv/models Environment=MC_MODELS_DIR=/srv/models
# Briefkasten-Meldungen (z. B. Kontext-Limit-Warnung) NIE direkt in die Store-Datei —
# die gehört exklusiv dem Steuerpult; per HTTP abliefern (gleiches Muster wie mc2-steward).
Environment=MC_ANNOUNCE_HTTP=http://127.0.0.1:9001
Restart=always Restart=always
RestartSec=2 RestartSec=2