diff --git a/xtts/llm-adapter/adapter.py b/xtts/llm-adapter/adapter.py index 6fb9719..be26974 100644 --- a/xtts/llm-adapter/adapter.py +++ b/xtts/llm-adapter/adapter.py @@ -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: " 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