fix(llm): kaputte Werkzeug-Argumente im Verlauf lassen nicht mehr die ganze Anfrage scheitern
Zwischendurch kam bei Programmen (Software-Schmiede) "model_error: Server
error '500 …'" — nach "nochmal versuchen" lief es weiter. Ursache, auf der
Box nachgestellt: steht im Verlauf ein frueherer Werkzeugaufruf, dessen
arguments kein gueltiges JSON sind (typisch bei Code: Dateiinhalt mit
Anfuehrungszeichen/Umbruechen), lehnt llama.cpp (Stand der Box, Aug. 2026)
die GANZE Anfrage mit 500 ab — und jede weitere mit demselben Verlauf.
Neuere llama.cpp-Versionen nehmen es an.
llm-adapter:
- _sanitize_messages: solche arguments werden als {"_raw": "…"} verpackt,
dict-arguments zu JSON, fehlende zu "{}". Nur Kopien; gueltige bleiben
unangetastet. Auf der Box: kaputt → 500, eingepackt → 200.
- _llama_error_text: bei HTTP-Fehlern kommt der GRUND von llama.cpp mit
(z.B. "request (9017 tokens) exceeds the available context size (8192)")
statt nur "Server error '500'". Volle Antwort ins Box-Log — falls es noch
einen anderen Ausloeser gibt, steht er dort.
Hinweis: ob das der einzige Ausloeser ist, ist offen — die urspruengliche
Fehlerantwort war 4,6 KB gross, diese Ablehnung nur 265 Byte. Mit dem
durchgereichten Grund ist der naechste Fall sofort zu sehen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -317,6 +317,62 @@ async def _send(ws, mtype: str, payload: dict) -> None:
|
||||
logger.warning("Send fehlgeschlagen (%s): %s", mtype, e)
|
||||
|
||||
|
||||
def _sanitize_messages(messages: list) -> list:
|
||||
"""Verlauf so aufbereiten, dass llama.cpp ihn nicht als Ganzes ablehnt.
|
||||
|
||||
Steht im Verlauf ein frueherer Werkzeugaufruf, dessen arguments KEIN
|
||||
gueltiges JSON sind, antwortet llama.cpp auf die GANZE Anfrage mit 500
|
||||
("Failed to parse tool call arguments as JSON") — und jede weitere Anfrage
|
||||
mit demselben Verlauf auch. Bei Programmen, die Code schreiben, passiert
|
||||
das leicht (Dateiinhalt mit Anfuehrungszeichen/Umbruechen). Statt den
|
||||
ganzen Auftrag scheitern zu lassen: solche Argumente als {"_raw": "…"}
|
||||
verpacken. Ein dict als arguments (statt String) wird zu JSON.
|
||||
Nur Kopien — die Nachrichten des Aufrufers bleiben unveraendert."""
|
||||
out = []
|
||||
for m in messages or []:
|
||||
tcs = m.get("tool_calls") if isinstance(m, dict) else None
|
||||
if not tcs:
|
||||
out.append(m)
|
||||
continue
|
||||
fixed = []
|
||||
for tc in tcs:
|
||||
fn = dict((tc or {}).get("function") or {})
|
||||
args = fn.get("arguments")
|
||||
if isinstance(args, (dict, list)):
|
||||
fn["arguments"] = json.dumps(args, ensure_ascii=False)
|
||||
elif isinstance(args, str):
|
||||
try:
|
||||
json.loads(args or "{}")
|
||||
if not args.strip():
|
||||
fn["arguments"] = "{}"
|
||||
except Exception:
|
||||
logger.warning("Werkzeugaufruf %r im Verlauf hat kein gueltiges JSON als "
|
||||
"arguments (%d Zeichen) — eingepackt", fn.get("name"), len(args))
|
||||
fn["arguments"] = json.dumps({"_raw": args}, ensure_ascii=False)
|
||||
elif args is None:
|
||||
fn["arguments"] = "{}"
|
||||
fixed.append(dict(tc, function=fn))
|
||||
out.append(dict(m, tool_calls=fixed))
|
||||
return out
|
||||
|
||||
|
||||
def _llama_error_text(exc: Exception) -> str:
|
||||
"""Bei HTTP-Fehlern den GRUND von llama.cpp mitnehmen. Bisher kam nur
|
||||
"Server error '500 …'" an — die eigentliche Erklaerung steht im Body."""
|
||||
resp = getattr(exc, "response", None)
|
||||
if resp is None:
|
||||
return re.sub(r"\s*For more information check:\s*\S+", "", str(exc)).strip()
|
||||
detail = ""
|
||||
try:
|
||||
j = resp.json()
|
||||
e = j.get("error") if isinstance(j, dict) else None
|
||||
detail = (e.get("message") if isinstance(e, dict) else e) or json.dumps(j)[:600]
|
||||
except Exception:
|
||||
detail = (resp.text or "")[:600]
|
||||
logger.warning("llama.cpp HTTP %s — Antwort: %s", resp.status_code, (resp.text or "")[:2000])
|
||||
return f"llama.cpp HTTP {resp.status_code}: {str(detail).strip()[:600]}"
|
||||
|
||||
|
||||
async def _call_llama(messages: list, *, max_tokens: int, temperature: float,
|
||||
stop, tools=None, model=None, timeout: float | None = None) -> dict:
|
||||
"""Ruft llama.cpp/llama-swap /v1/chat/completions (OpenAI-Format). Gibt
|
||||
@@ -328,7 +384,7 @@ async def _call_llama(messages: list, *, max_tokens: int, temperature: float,
|
||||
natives Tool-Calling und liefert dann message.tool_calls."""
|
||||
body = {
|
||||
"model": model or LLM_MODEL,
|
||||
"messages": messages,
|
||||
"messages": _sanitize_messages(messages),
|
||||
"max_tokens": max_tokens,
|
||||
"temperature": temperature,
|
||||
"stream": False,
|
||||
@@ -361,10 +417,9 @@ async def _call_llama(messages: list, *, max_tokens: int, temperature: float,
|
||||
"usage": usage,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning("llama.cpp-Call fehlgeschlagen: %s", e)
|
||||
# httpx haengt "For more information check: <Link>" an — fuer niemanden hilfreich.
|
||||
msg = re.sub(r"\s*For more information check:\s*\S+", "", str(e)).strip()
|
||||
return {"ok": False, "content": "", "error": msg[:300]}
|
||||
msg = _llama_error_text(e)
|
||||
logger.warning("llama.cpp-Call fehlgeschlagen: %s", msg[:300])
|
||||
return {"ok": False, "content": "", "error": msg[:700]}
|
||||
|
||||
|
||||
# B0.5-2: Lade-Status ans Diagnostic (service_status, service="llm"). Wir kennen
|
||||
|
||||
Reference in New Issue
Block a user