fix: abgeschnittene Antworten meiden den Anbieter; LLM-Ausfall macht unzuverlässig statt abzustürzen
Grundlinie 20260918-191400: 9 von 22 Läufen hatten Aufrufe, die an LLM_MAX_TOKENS abgeschnitten wurden (Venice 4/28, Makora 2/43, übrige 0/54); hard_portfolio_kardinalitaet brach nach vier Abschneidungen ab, Kosten gingen verloren. - Abschneiden: sofort neu versuchen, Anbieter per provider.ignore meiden (_avoid_provider, gemeinsam mit „zu langsam“) - nach allen Wiederholungen LLMUnavailable (Stufe + Ursache); fatale Fehler bleiben unverpackt - Solver-Stufe behält Modell und Prüfbericht (Solver-Fehler), Gegenprobe behält erstes Ergebnis, davor Ergebnis ohne Spezifikation mit Kosten („LLM-Fehler“, Report „Lauf abgebrochen“) - Benchmark: Status „LLM-Fehler“ statt ABBRUCH, zählt nicht als ehrliches Scheitern - TextToModel.py: Shebang Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
422360ffa5
commit
6b2b2c6466
1 changed files with 218 additions and 25 deletions
47
core/llm.py
47
core/llm.py
|
|
@ -63,6 +63,25 @@ TRANSIENT_MARKERS = (
|
|||
)
|
||||
|
||||
|
||||
TRUNCATION_MARKERS = ("incomplete due to a max_tokens", "IncompleteOutputException")
|
||||
|
||||
|
||||
class LLMUnavailable(RuntimeError):
|
||||
"""Ein Aufruf ist nach allen Wiederholungen gescheitert (Überlastung, Abschneiden, Zeitüberschreitung).
|
||||
|
||||
Eigene Klasse, damit die Pipeline einen solchen Lauf als unzuverlässig meldet – mit Kosten –, statt abzustürzen.
|
||||
Fatale Fehler (Key, Guthaben, Kontingent) bleiben unverpackt: dort soll der Benchmark stoppen.
|
||||
"""
|
||||
|
||||
def __init__(self, step: str, cause: BaseException):
|
||||
super().__init__(f"LLM-Aufruf '{step}' nach allen Wiederholungen gescheitert: {str(cause)[:300]}")
|
||||
self.step = step
|
||||
|
||||
|
||||
def is_truncation(message: str) -> bool:
|
||||
return any(marker in message for marker in TRUNCATION_MARKERS)
|
||||
|
||||
|
||||
def is_fatal_error(message: str) -> bool:
|
||||
"""Kontingent erschöpft, Guthaben leer oder Key ungültig."""
|
||||
return any(marker in message for marker in FATAL_MARKERS)
|
||||
|
|
@ -176,10 +195,19 @@ class LLMSession:
|
|||
file=sys.stderr)
|
||||
self.ignored_providers.clear()
|
||||
continue
|
||||
if not waits or not is_transient_error(str(exc)):
|
||||
if not is_transient_error(str(exc)):
|
||||
raise
|
||||
if not waits:
|
||||
raise LLMUnavailable(step, exc) from exc
|
||||
wait = waits.pop(0)
|
||||
print(f"Vorübergehender LLM-Fehler – neuer Versuch in {wait} s: {str(exc)[:120]}", file=sys.stderr)
|
||||
if is_truncation(str(exc)):
|
||||
# Abschneiden liegt nicht an Überlastung – Warten hilft nicht, ein anderer Anbieter oft schon.
|
||||
# Gemessen am 18.09.2026: Venice 4 von 28, Makora 2 von 43, alle übrigen 0 von 54 Aufrufen.
|
||||
wait = 0
|
||||
for verworfen in discarded_completions(exc)[-1:]:
|
||||
self._avoid_provider(getattr(verworfen, "provider", None), "schnitt die Antwort ab")
|
||||
print("Vorübergehender LLM-Fehler – neuer Versuch" + (f" in {wait} s" if wait else "")
|
||||
+ f": {str(exc)[:120]}", file=sys.stderr)
|
||||
time.sleep(wait)
|
||||
|
||||
def _avoid_if_slow(self) -> None:
|
||||
|
|
@ -187,16 +215,19 @@ class LLMSession:
|
|||
if OPENROUTER_SWITCH_BELOW <= 0 or not self.usage.call_records:
|
||||
return
|
||||
letzter = self.usage.call_records[-1]
|
||||
if (not letzter.provider or letzter.completion_tokens < SWITCH_MIN_TOKENS or letzter.seconds <= 0
|
||||
or letzter.provider in self.ignored_providers or len(self.ignored_providers) >= MAX_IGNORED_PROVIDERS):
|
||||
if letzter.completion_tokens < SWITCH_MIN_TOKENS or letzter.seconds <= 0:
|
||||
return
|
||||
rate = letzter.completion_tokens / letzter.seconds
|
||||
if rate >= OPENROUTER_SWITCH_BELOW:
|
||||
if rate < OPENROUTER_SWITCH_BELOW:
|
||||
self._avoid_provider(letzter.provider, f"zu langsam ({rate:.0f} Tokens/s < {OPENROUTER_SWITCH_BELOW})")
|
||||
|
||||
def _avoid_provider(self, provider: str | None, grund: str) -> None:
|
||||
"""Anbieter für den Rest des Laufs meiden (`provider.ignore`), höchstens MAX_IGNORED_PROVIDERS."""
|
||||
if not provider or provider in self.ignored_providers or len(self.ignored_providers) >= MAX_IGNORED_PROVIDERS:
|
||||
return
|
||||
self.ignored_providers.append(letzter.provider)
|
||||
self.ignored_providers.append(provider)
|
||||
self.session_id = uuid.uuid4().hex # Sticky Routing an den neuen Anbieter binden
|
||||
print(f"Anbieter {letzter.provider} zu langsam ({rate:.0f} Tokens/s < {OPENROUTER_SWITCH_BELOW}) – "
|
||||
f"für den Rest des Laufs gemieden.", file=sys.stderr)
|
||||
print(f"Anbieter {provider} {grund} – für den Rest des Laufs gemieden.", file=sys.stderr)
|
||||
|
||||
def _record(self, completion, step: str = "unbenannt", seconds: float = 0.0, model: str = None) -> None:
|
||||
before = (self.usage.completion_tokens, self.usage.reasoning_tokens, self.usage.prompt_tokens,
|
||||
|
|
|
|||
Loading…
Reference in a new issue