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:
Dieter Schlüter 2026-09-18 19:24:23 +02:00
commit 6b2b2c6466

View file

@ -298,3 +298,60 @@ def test_no_endpoints_left_lifts_the_ignore_list():
completions["openrouter"].create_with_completion = create_with_completion
assert session.create(str, [{"role": "user", "content": "x"}], step="Prüfung") == "ok"
assert gesendet == [["AtlasCloud", "Parasail"], None] and session.ignored_providers == []
# --- Abschneiden am Tokenlimit: anderer Anbieter statt Warten, am Ende LLMUnavailable statt Absturz ---
def _abgeschnitten(anbieter):
from instructor.core.exceptions import IncompleteOutputException
antwort = _antwort_mit_verbrauch(32000)
antwort.provider = anbieter
return IncompleteOutputException(last_completion=antwort)
def test_truncation_avoids_the_provider_and_retries_without_waiting(monkeypatch):
# gemessen am 18.09.2026: Venice 4 von 28 Aufrufen abgeschnitten, alle übrigen Anbieter fast nie
gewartet = []
monkeypatch.setattr(llm.time, "sleep", gewartet.append)
session, completions = fake_session()
gesendet, versuche = [], iter([_abgeschnitten("Venice"), ("ok", _antwort_mit_verbrauch(9000))])
def create_with_completion(**kwargs):
gesendet.append((kwargs["extra_body"]["provider"].get("ignore"), kwargs["extra_body"]["session_id"]))
naechster = next(versuche)
if isinstance(naechster, Exception):
raise naechster
return naechster
completions["openrouter"].create_with_completion = create_with_completion
assert session.create(str, [{"role": "user", "content": "x"}], step="Solver-Code") == "ok"
assert gewartet == [0]
assert gesendet[0] == (None, "lauf-1") and gesendet[1][0] == ["Venice"] and gesendet[1][1] != "lauf-1"
def test_exhausted_retries_raise_llm_unavailable(monkeypatch):
monkeypatch.setattr(llm.time, "sleep", lambda s: None)
session, completions = fake_session()
anbieter = iter(["Venice", "Makora", "Parasail", "Together"])
def create_with_completion(**kwargs):
raise _abgeschnitten(next(anbieter))
completions["openrouter"].create_with_completion = create_with_completion
with pytest.raises(llm.LLMUnavailable) as exc:
session.create(str, [{"role": "user", "content": "x"}], step="Extraktion")
assert exc.value.step == "Extraktion"
assert session.usage.calls == 4 # alle vier bezahlten Antworten gezählt
assert session.ignored_providers == ["Venice", "Makora", "Parasail"] # höchstens MAX_IGNORED_PROVIDERS
def test_fatal_errors_stay_unwrapped():
session, completions = fake_session()
def create_with_completion(**kwargs):
raise RuntimeError("Error code: 402 - {'error': {'message': 'Insufficient credits'}}")
completions["openrouter"].create_with_completion = create_with_completion
with pytest.raises(RuntimeError) as exc:
session.create(str, [{"role": "user", "content": "x"}])
assert not isinstance(exc.value, llm.LLMUnavailable)