feat: stufen-spezifisches Token-Limit für die Solver-Stufe (LLM_MAX_TOKENS_SOLVER)

Die Solver-Stufe erzeugt langen Python-Code plus Reasoning. Bei komplexen
Aufgaben (Portfoliomanagement: 4× je ~26k Reasoning-Tokens am 32k-Limit
abgeschnitten, $0,28 für nichts) reichte das globale Limit nicht. DeepSeek
V4.1 Flash unterstützt bis 393k Output-Tokens (OpenRouter).

Neu: LLM_MAX_TOKENS_SOLVER (Default 65536, 0 = globales Limit gilt auch
für den Solver). Wirkt nur für die Stufen 'Solver-Code' und 'Solver-Reparatur'.
CLI-Option --max-tokens-solver. 438 Tests grün.
This commit is contained in:
Dieter Schlüter 2026-09-18 21:48:18 +02:00
commit e7a2b8de81

View file

@ -47,6 +47,11 @@ LLM_TIMEOUT = int(os.environ.get("LLM_TIMEOUT", "420")) # Sekunden je Aufruf
# Ohne Vorgabe schneiden manche OpenRouter-Anbieter die Antwort ab (beobachtet: abgebrochene
# Modellspezifikation in einer Korrekturrunde). Großzügig setzen; bezahlt werden nur erzeugte Tokens.
LLM_MAX_TOKENS = int(os.environ.get("LLM_MAX_TOKENS", "32000"))
# Die Solver-Stufe erzeugt langen Python-Code plus Reasoning – bei komplexen Aufgaben (beobachtet:
# Portfoliomanagement, 4× je ~26k Reasoning-Tokens am 32k-Limit abgeschnitten) reicht das globale
# Limit nicht. DeepSeek V4.1 Flash unterstützt bis 393k Output-Tokens (OpenRouter), wir setzen die
# Solver-Stufe bewusst höher. 0 = das globale Limit gilt auch für den Solver.
LLM_MAX_TOKENS_SOLVER = int(os.environ.get("LLM_MAX_TOKENS_SOLVER", "65536"))
# Denkaufwand (OpenRouter): "low" | "medium" | "high"; leer = Modellvorgabe.
# Reasoning-Tokens machen den Großteil der Laufzeit aus (gemessen: 45.000 von 73.000 Output-Tokens).
REASONING_EFFORT = os.environ.get("LLM_REASONING_EFFORT", "").strip().lower()
@ -159,10 +164,14 @@ class LLMSession:
"""Stufen mit abweichendem Modell – für die Fortschrittsausgabe."""
return {step: m for step, m in self.step_models.items() if m != self.model}
def _kwargs(self, model: str, response_model, messages) -> dict:
def _kwargs(self, model: str, response_model, messages, step: str = "") -> dict:
kwargs = {"model": model, "response_model": response_model, "messages": messages}
if provider_for(model) == "openrouter":
kwargs["max_tokens"] = LLM_MAX_TOKENS
# Solver-Stufe bekommt ein eigenes, höheres Limit (siehe LLM_MAX_TOKENS_SOLVER).
if step in ("Solver-Code", "Solver-Reparatur") and LLM_MAX_TOKENS_SOLVER > 0:
kwargs["max_tokens"] = LLM_MAX_TOKENS_SOLVER
else:
kwargs["max_tokens"] = LLM_MAX_TOKENS
# nur Anbieter wählen, die strukturierte Ausgabe (response_format) wirklich unterstützen
anbieter = {"require_parameters": True, "preferred_min_throughput": {"p50": OPENROUTER_MIN_THROUGHPUT}}
if self.ignored_providers:
@ -177,7 +186,7 @@ class LLMSession:
provider = provider_for(model)
waits = list(RETRY_WAITS)
while True:
kwargs = self._kwargs(model, response_model, messages)
kwargs = self._kwargs(model, response_model, messages, step)
started = time.monotonic()
try:
result, completion = self._clients[provider].chat.completions.create_with_completion(**kwargs)