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
7 changed files with 218 additions and 25 deletions
|
|
@ -518,6 +518,8 @@ Mit dem Gemini-Gratiskontingent ist der Benchmark praktisch nicht durchführbar;
|
|||
|---|---|---|
|
||||
| `KeyError: 'OPENROUTER_API_KEY'` | Schlüssel nicht gesetzt | `export OPENROUTER_API_KEY=…` in derselben Shell |
|
||||
| `Error code: 401` | Schlüssel ungültig | Schlüssel prüfen |
|
||||
| `Anbieter … schnitt die Antwort ab – … gemieden` | das Modell hat die Tokengrenze (`LLM_MAX_TOKENS`) mit Nachdenken aufgebraucht | nichts – der Aufruf wird sofort bei einem anderen Anbieter wiederholt |
|
||||
| `LLM-Aufruf '…' nach allen Wiederholungen gescheitert` (Exit-Code 2) | viermal Überlastung/Abschneiden hintereinander | später erneut; war das Modell schon fertig, `--von solver ERGEBNIS.JSON` |
|
||||
| `Error code: 402` | Guthaben bei OpenRouter aufgebraucht | Guthaben aufladen |
|
||||
| `RESOURCE_EXHAUSTED` | Gemini-Kontingent erschöpft | später erneut oder OpenRouter-Modell |
|
||||
| `Vorübergehender LLM-Fehler – neuer Versuch in … s` | Überlastung, Ratelimit, Zeitüberschreitung | nichts tun; es wird automatisch bis zu dreimal wiederholt |
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
|
|
@ -8,8 +10,9 @@ from typing import List
|
|||
from checks.model_checks import check_figure_readings, check_spec, check_understanding
|
||||
from checks.review import needs_work, review_assumptions, revision_reasons, select_best
|
||||
from core import llm
|
||||
from core.llm import DEFAULT_MODEL, LLMSession
|
||||
from core.llm import DEFAULT_MODEL, LLMSession, LLMUnavailable
|
||||
from core.model_schema import (
|
||||
SolverRun,
|
||||
SolverStatus,
|
||||
CheckSeverity,
|
||||
OptimizationModelSpecification,
|
||||
|
|
@ -157,6 +160,18 @@ def _recheck_infeasible(session, task: str, model: str, result: PipelineResult,
|
|||
return result
|
||||
|
||||
log(f"[{model}] Solver meldet '{status}' – Gegenprobe mit unabhängiger Modellierung ...")
|
||||
try:
|
||||
return _run_recheck(session, task, model, result, status, max_revisions, solver_retries,
|
||||
data_files, data_prompt)
|
||||
except LLMUnavailable as exc:
|
||||
log(f" Gegenprobe abgebrochen: {exc}")
|
||||
result.reliable = False
|
||||
result.reliability_issues.append(f"Solver meldet '{status}'; die Gegenprobe scheiterte am LLM ({exc.step})")
|
||||
return result
|
||||
|
||||
|
||||
def _run_recheck(session, task: str, model: str, result: PipelineResult, status: str, max_revisions: int,
|
||||
solver_retries: int, data_files, data_prompt: str) -> PipelineResult:
|
||||
zweiter = _model_attempt(session, task, max_revisions, f"{model} Gegenprobe",
|
||||
hint=INFEASIBLE_HINT.format(status=status),
|
||||
understanding=result.understanding, data_prompt=data_prompt)
|
||||
|
|
@ -201,9 +216,17 @@ def _solver_stage(session, task: str, result: PipelineResult, model: str,
|
|||
return result
|
||||
|
||||
log(f"[{model}] Erzeuge und starte Solver-Code ({spec.target_stack.value}) ...")
|
||||
try:
|
||||
result.solver_code, result.solver_run = solve(session, task, spec, retries=solver_retries, log=log,
|
||||
understanding=result.understanding,
|
||||
data_files=data_files, data_prompt=data_prompt)
|
||||
except LLMUnavailable as exc:
|
||||
# Modell und Prüfbericht bleiben erhalten – `--von solver` kann genau hier weitermachen
|
||||
result.solver_run = SolverRun(status=SolverStatus.ERROR, error=str(exc))
|
||||
result.reliable = False
|
||||
result.reliability_issues.append(f"Solver-Fehler: {exc}")
|
||||
log(f"ERGEBNIS UNZUVERLÄSSIG – {exc}")
|
||||
return result
|
||||
_assess_run(result)
|
||||
if result.solver_run.status in (SolverStatus.INFEASIBLE, SolverStatus.UNBOUNDED):
|
||||
result = _recheck_infeasible(session, task, model, result, max_revisions, solver_retries,
|
||||
|
|
@ -270,6 +293,20 @@ def run_pipeline(
|
|||
session = session or LLMSession(model)
|
||||
# Abbildungen (docling: Base64 oder Bilddatei) gehen nur an die Verstehensstufe; ab dann zählt ihre Übertragung
|
||||
task, figures = split_figures(task, task_dir)
|
||||
try:
|
||||
return _run_pipeline(session, task, figures, model, max_revisions, solve_model, solver_retries,
|
||||
max_restarts, data_files)
|
||||
except LLMUnavailable as exc:
|
||||
# Vor der Solver-Stufe gibt es nichts Verwertbares; statt Absturz ein ehrliches, bezahltes Nicht-Ergebnis
|
||||
log(f"ERGEBNIS UNZUVERLÄSSIG – {exc}")
|
||||
result = PipelineResult(task=task, figures=figures, reliable=False,
|
||||
reliability_issues=[f"LLM-Fehler: {exc}"])
|
||||
_log_usage(session, result)
|
||||
return result
|
||||
|
||||
|
||||
def _run_pipeline(session, task: str, figures, model: str, max_revisions: int, solve_model: bool,
|
||||
solver_retries: int, max_restarts: int, data_files) -> PipelineResult:
|
||||
if figures:
|
||||
log(f"{len(figures)} Abbildung(en) in der Aufgabe – werden in der Verstehensstufe in Text übertragen.")
|
||||
profile = data_profile.profile_files(data_files)
|
||||
|
|
@ -367,8 +404,15 @@ def continue_pipeline(
|
|||
problems = _unresolved({"review": stored.review, "issues": checks})
|
||||
update = {"checks": checks}
|
||||
else:
|
||||
try:
|
||||
attempt = _model_attempt(session, task, max_revisions, model, spec=stored.specification,
|
||||
understanding=understanding, data_prompt=data_prompt)
|
||||
except LLMUnavailable as exc:
|
||||
log(f"ERGEBNIS UNZUVERLÄSSIG – {exc}")
|
||||
result = stored.model_copy(update={"solver_code": None, "solver_run": None, "reliable": False,
|
||||
"reliability_issues": [f"LLM-Fehler: {exc}"]})
|
||||
_log_usage(session, result)
|
||||
return result
|
||||
problems = _unresolved(attempt)
|
||||
update = {"specification": attempt["spec"], "review": attempt["review"], "revisions": attempt["revisions"],
|
||||
"selected_revision": attempt["selected"], "restarts": 0, "checks": attempt["issues"]}
|
||||
|
|
@ -590,7 +634,9 @@ def summary(result: PipelineResult) -> str:
|
|||
return "Abgelehnt – " + "; ".join(result.reliability_issues)
|
||||
zeilen = []
|
||||
run = result.solver_run
|
||||
if run is None:
|
||||
if result.specification is None:
|
||||
zeilen.append("Kein Modell entstanden.")
|
||||
elif run is None:
|
||||
spec = result.specification
|
||||
zeilen.append(f"Modell: {spec.problem_class.value} → {spec.target_stack.value} (kein Solver-Lauf)")
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -105,8 +105,9 @@ def observed_behaviors(result) -> set:
|
|||
gezeigt = set()
|
||||
if result.rejected:
|
||||
return {"rejected"}
|
||||
# Ein Absturz ist kein Eingeständnis: nur Befunde außer „Solver-Fehler“ zählen als ehrlich unzuverlässig
|
||||
if not result.reliable and any(not i.startswith("Solver-Fehler: ") for i in result.reliability_issues):
|
||||
# Ein Absturz ist kein Eingeständnis: nur Befunde außer „Solver-Fehler“/„LLM-Fehler“ zählen als ehrlich unzuverlässig
|
||||
if not result.reliable and any(not i.startswith(("Solver-Fehler: ", "LLM-Fehler: "))
|
||||
for i in result.reliability_issues):
|
||||
gezeigt.add("unreliable")
|
||||
if result.solver_code is not None and result.solver_code.data_assumptions:
|
||||
gezeigt.add("assumed_data")
|
||||
|
|
@ -145,13 +146,14 @@ def run_case(case: dict, model: str, run_no: int) -> dict:
|
|||
|
||||
spec, solver_run = result.specification, result.solver_run
|
||||
ehrlich = behavior_matches(result, expected) if "behavior" in expected else None
|
||||
if spec is None: # abgelehnt: keine Optimierungsaufgabe erkannt
|
||||
if spec is None: # abgelehnt (keine Optimierungsaufgabe) oder LLM vor dem Modell gescheitert
|
||||
fragen = len(result.understanding.sub_questions) if result.understanding else 0
|
||||
# Bei erwarteter Ablehnung gibt es kein Routing zu bewerten; sonst ist die Ablehnung ein Routing-Fehler
|
||||
routing = None if ehrlich else False
|
||||
routing = None if ehrlich or not result.rejected else False
|
||||
row.update(seconds=round(time.monotonic() - start), problem_class=None, target_stack=None,
|
||||
class_ok=routing, stack_ok=routing, check_errors=0, check_warnings=len(result.checks),
|
||||
revisions=0, review_ok=None, data_assumptions=0, solver_status="abgelehnt", solver_attempts=0,
|
||||
revisions=0, review_ok=None, data_assumptions=0, solver_status="abgelehnt" if result.rejected else "LLM-Fehler",
|
||||
solver_attempts=0,
|
||||
objective_value=None, objective_ok=bool(ehrlich), questions=fragen, answered=0, answer_findings=0,
|
||||
task_ok=bool(ehrlich), behavior=sorted(observed_behaviors(result)),
|
||||
solver_error="; ".join(result.reliability_issues), reliable=False, restarts=0,
|
||||
|
|
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -335,10 +335,15 @@ def _figure_blocks(result: PipelineResult) -> list:
|
|||
|
||||
|
||||
def _rejected_document(result: PipelineResult) -> list:
|
||||
"""Abgelehnte Aufgabe: Grund und Aufgabentext – kein Modell, keine Zahlen."""
|
||||
"""Abgelehnte Aufgabe oder abgebrochener Lauf: Grund und Aufgabentext – kein Modell, keine Zahlen."""
|
||||
if result.rejected:
|
||||
grund = result.understanding.reason if result.understanding else "; ".join(result.reliability_issues)
|
||||
doc = [("h1", "Aufgabe abgelehnt"),
|
||||
("warn", f"Keine Optimierungsaufgabe – es wurde weder modelliert noch gerechnet. {grund}")]
|
||||
else: # LLM nach allen Wiederholungen gescheitert, bevor ein Modell entstand
|
||||
doc = [("h1", "Lauf abgebrochen"),
|
||||
("warn", "Es ist kein Modell entstanden – es wurde nichts gerechnet. "
|
||||
+ "; ".join(result.reliability_issues))]
|
||||
if result.usage:
|
||||
cost = f", Kosten USD {result.usage.cost_usd:.4f}" if result.usage.cost_usd is not None else ""
|
||||
doc.append(("list", [f"Erstellt: {datetime.now():%d.%m.%Y %H:%M}", f"Modell: {result.usage.model}",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -55,7 +55,10 @@ class FakeSession:
|
|||
warteschlange = self.queues[response_model]
|
||||
if response_model is TaskUnderstanding and not warteschlange:
|
||||
return EINFACHES_VERSTAENDNIS # Standard, solange ein Test nichts anderes vorgibt
|
||||
return warteschlange.pop(0)
|
||||
antwort = warteschlange.pop(0)
|
||||
if isinstance(antwort, Exception): # gescheiterter Aufruf, z. B. LLMUnavailable
|
||||
raise antwort
|
||||
return antwort
|
||||
|
||||
|
||||
HALLUCINATED = make_spec(executive_summary="Produktionsplanung mit Drehen und Fräsen") # Fremdproblem
|
||||
|
|
@ -853,3 +856,50 @@ def test_second_repair_knows_the_first_error():
|
|||
reparaturen = [inhalt for stufe, inhalt in session.contents if stufe == "Solver-Reparatur"]
|
||||
assert "FRÜHERE FEHLVERSUCHE" not in reparaturen[0] # beim ersten gibt es noch keinen Verlauf
|
||||
assert "Fehler B: Typ" in reparaturen[1] and "Versuch 1: " in reparaturen[1] and "Fehler A: Index" in reparaturen[1]
|
||||
|
||||
|
||||
# --- LLM nach allen Wiederholungen gescheitert: unzuverlässiges Ergebnis statt Absturz ---
|
||||
|
||||
def _llm_weg(step):
|
||||
from core.llm import LLMUnavailable
|
||||
return LLMUnavailable(step, RuntimeError("The output is incomplete due to a max_tokens length limit."))
|
||||
|
||||
|
||||
def test_llm_failure_in_solver_stage_keeps_the_model():
|
||||
# beobachtet: hard_portfolio_kardinalitaet, viermal abgeschnitten ⇒ Absturz nach 1162 s, Kosten verloren
|
||||
session = FakeSession([GOOD], [OK], [_llm_weg("Solver-Code")])
|
||||
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0)
|
||||
assert not result.reliable and result.specification is not None and result.review is not None
|
||||
assert result.solver_run.status == SolverStatus.ERROR and result.solver_code is None
|
||||
assert result.reliability_issues[0].startswith("Solver-Fehler: LLM-Aufruf 'Solver-Code'")
|
||||
assert result.usage is not None
|
||||
|
||||
|
||||
def test_llm_failure_before_the_model_is_a_paid_non_result(tmp_path):
|
||||
from benchmark.benchmark import observed_behaviors
|
||||
from reporting.report import write_report
|
||||
session = FakeSession([_llm_weg("Extraktion")], [])
|
||||
result = run_pipeline("Aufgabe", session=session)
|
||||
assert not result.reliable and not result.rejected and result.specification is None
|
||||
assert result.reliability_issues[0].startswith("LLM-Fehler: ") and result.usage.calls == 2
|
||||
assert observed_behaviors(result) == set() # ein Absturz ist kein ehrliches Scheitern
|
||||
write_report(result, str(tmp_path / "r.md"))
|
||||
text = (tmp_path / "r.md").read_text(encoding="utf-8")
|
||||
assert "Lauf abgebrochen" in text and "abgelehnt" not in text
|
||||
|
||||
|
||||
def test_benchmark_counts_an_llm_failure_as_failure_with_costs(monkeypatch):
|
||||
from benchmark import benchmark
|
||||
session = FakeSession([_llm_weg("Extraktion")], [])
|
||||
monkeypatch.setattr(benchmark, "run_pipeline", lambda *a, **k: run_pipeline("Aufgabe", session=session))
|
||||
zeile = benchmark.run_case({"name": "t", "task": "Aufgabe", "expected": {"objective_value": 9, "tolerance": 1e-6}},
|
||||
"fake", 1)
|
||||
assert zeile["solver_status"] == "LLM-Fehler" and zeile["llm_calls"] == 2 and "error" not in zeile
|
||||
assert zeile["objective_ok"] is False and zeile["task_ok"] is False
|
||||
|
||||
|
||||
def test_llm_failure_in_the_infeasible_recheck_keeps_the_first_result():
|
||||
session = FakeSession(specs=[GOOD, _llm_weg("Extraktion")], reviews=[OK], codes=[SolverCode(code=INFEASIBLE_CODE)])
|
||||
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0)
|
||||
assert result.solver_run.status == SolverStatus.INFEASIBLE and not result.infeasible_confirmed
|
||||
assert not result.reliable and "Gegenprobe scheiterte am LLM" in result.reliability_issues[-1]
|
||||
|
|
|
|||
Loading…
Reference in a new issue