From 79a901089b7ec66228483f6508efe45d824bd9ba Mon Sep 17 00:00:00 2001 From: duffyduck Date: Mon, 19 Oct 2026 13:32:37 +0200 Subject: [PATCH] feat(llm): Kontextfenster pro Karte einstellbar + Fehlerart context_exceeded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Anlass: die Software-Schmiede bekam "request (8274 tokens) exceeds the available context size (8192 tokens)". Sie holt ctx vom Cluster zwar ab, kuerzt aber nach einem eigenen, von Hand eingetragenen Wert (32768). ARIA-Seite: - Neue Fehlerart context_exceeded (statt model_error) mit promptTokens und ctx — Programme koennen automatisch kuerzen und nochmal senden, statt Text auszuwerten. llm-adapter liest es aus llama.cpps exceed_context_size_error, der Cluster-Router reicht es durch und wiederholt es nicht anderswo. - Diagnostic → LLM-Karten: Kontextfenster pro Karte (4k … 128k). Laedt das Modell mit neuem -c; Programme sehen den neuen Wert ueber ctx. Passt es nicht in den Grafikspeicher, setzt die Box zurueck — jetzt inkl. Registry (vorher blieb bei einem Fehlschlag der zu grosse ctx in aria_models.json stehen und waere beim naechsten Start wieder gescheitert). - Doku: context_exceeded, ctx kann sich aendern, "danach kuerzen, nicht nur anzeigen". Getestet gegen llama-swap: Kontext 512 → 1024 (gleiche Karte), Fehlschlag bei 65536 → Registry + Config wieder 1024, zu langer Verlauf → context_exceeded mit promptTokens=6030, ctx=1024. Co-Authored-By: Claude Opus 5.5 --- bridge/llm_cluster.py | 3 ++- diagnostic/index.html | 32 +++++++++++++++++++++++++++++++- docs/LLM-SCHNITTSTELLE.md | 13 +++++++++---- xtts/llm-adapter/adapter.py | 30 +++++++++++++++++++++++++++++- 4 files changed, 71 insertions(+), 7 deletions(-) diff --git a/bridge/llm_cluster.py b/bridge/llm_cluster.py index aab42b7..d840775 100644 --- a/bridge/llm_cluster.py +++ b/bridge/llm_cluster.py @@ -211,7 +211,8 @@ class LlmCluster: res = await self.route(payload) out = {"requestId": rid, "ok": bool(res.get("ok"))} for k in ("content", "tool_calls", "model", "node", "gpu", "instanceId", "usage", - "elapsedMs", "attempts", "error", "errorCode", "available"): + "elapsedMs", "attempts", "error", "errorCode", "available", + "promptTokens", "ctx"): if res.get(k) is not None: out[k] = res[k] await self.b._send_to_rvs({"type": "llm_cluster_response", "payload": out, diff --git a/diagnostic/index.html b/diagnostic/index.html index b591ea3..66423d8 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -9175,13 +9175,25 @@ const job = Object.values(_llmJobs || {}).find(j => j.instanceId === b.instanceId && String(j.gpu) === String(sl.gpu) && !['ready', 'error'].includes(j.phase)); const jobTxt = job ? (job.phase === 'download' && job.percent != null ? `lädt ${job.percent.toFixed(0)} %` : 'lädt …') : ''; - rows.push(`
+ // Kontextfenster dieser Karte. Mehr Kontext = mehr VRAM; passt es + // nicht, setzt die Box die Karte von selbst zurueck. + const CTX = [4096, 8192, 16384, 32768, 65536, 131072]; + const curCtx = sl.ctx || 0; + const ctxOpts = (curCtx && !CTX.includes(curCtx) ? [curCtx, ...CTX] : CTX) + .map(n => ``).join(''); + const ctxSel = sl.model + ? `` + : ''; + rows.push(`
${escapeHtml(b.node)} · Karte ${escapeHtml(sl.gpu)}
${escapeHtml(sl.gpuName || '')}${vramGB ? ' · ' + vramGB.toFixed(0) + ' GB' : ''}${sl.vramUsedMB >= 0 ? ' · belegt ' + (sl.vramUsedMB / 1024).toFixed(1) + ' GB' : ''}${sl.ctx ? ' · Kontext ' + sl.ctx : ''}
${sl.onCpu ? '
⚠ Modell läuft auf der CPU, Karte leer — Treiber zu alt? (bootstrap.sh --upgrade-driver)
' : ''}
+ ${ctxSel} ${jobTxt || st}
`); } @@ -9189,6 +9201,24 @@ box.innerHTML = rows.join(''); } + // Kontextfenster einer Karte aendern = dasselbe Modell mit anderem -c neu + // laden. Programme bekommen den neuen Wert automatisch (ctx in + // llm_cluster_models). Passt es nicht in den Grafikspeicher, setzt die Box + // die Karte auf den alten Stand zurueck. + function onLlmCardCtx(sel) { + const target = sel.dataset.box, gpu = sel.dataset.gpu, model = sel.dataset.model; + const cur = Number(sel.dataset.current || 0), next = Number(sel.value); + if (next === cur) return; + const m = _llmCatalog.find(x => x.id === model); + if (!m || !m.hfRepo) { alert(`${model} steht nicht im Modell-Katalog — Kontext so nicht änderbar.`); sel.value = cur; return; } + const more = next > cur; + if (!confirm(`${model} auf ${_boxName(target)} · Karte ${gpu} mit ${next} statt ${cur || '?'} Tokens Kontext neu laden?\n\n` + + (more ? 'Mehr Kontext braucht mehr Grafikspeicher. Passt es nicht, setzt die Box die Karte von selbst auf den alten Stand zurück.\n\n' : '') + + 'Die Box startet ihre Modelle dabei kurz neu. Programme bekommen den neuen Wert automatisch.')) { sel.value = cur; return; } + send({ action: 'llm_provision_model', targetInstance: target, gpu, key: m.id, hfRepo: m.hfRepo, quant: m.quant, ctx: next }); + openLlmDownloads(); + } + function onLlmCardSelect(sel) { const target = sel.dataset.box, gpu = sel.dataset.gpu, current = sel.dataset.current, next = sel.value; const node = _boxName(target); diff --git a/docs/LLM-SCHNITTSTELLE.md b/docs/LLM-SCHNITTSTELLE.md index 750125f..5f1454b 100644 --- a/docs/LLM-SCHNITTSTELLE.md +++ b/docs/LLM-SCHNITTSTELLE.md @@ -100,7 +100,7 @@ Antwort: | Feld | Bedeutung | |---|---| | `model` | Name — genau so in der Anfrage angeben. **Das** zeigst du dem Nutzer zur Auswahl. | -| `ctx` | **Kontextfenster in Tokens** (Eingabe + Ausgabe zusammen). Das kleinste unter allen Karten mit diesem Modell — darauf kannst du dich verlassen, egal wohin verteilt wird. Nicht mehr annehmen. `0` = unbekannt. | +| `ctx` | **Kontextfenster in Tokens** (Eingabe + Ausgabe zusammen). Das kleinste unter allen Karten mit diesem Modell — darauf kannst du dich verlassen, egal wohin verteilt wird. Nicht mehr annehmen. `0` = unbekannt. **Kann sich ändern** (der Betreiber stellt es pro Karte ein) — vor längeren Aufträgen neu holen. | | `available` | auf wie vielen Karten es gerade liegt | | `cards` | nur zur Info (Anzeige, Fehlersuche) — für die Anfrage nicht nötig | @@ -165,7 +165,8 @@ Fehler — `ok: false`, dazu `error` (Text für Menschen) und `errorCode`: | `bad_request` | Anfrage unvollständig, z.B. `messages` leer oder `model` fehlt bei mehreren Modellen | Anfrage korrigieren | | `timeout` | Keine Antwort innerhalb der Frist (Standard 600 s) — auch nach Ersatzversuch | später nochmal | | `box_offline` | Box ist während der Anfrage weggefallen, und es gab keine Ersatz-Box | später nochmal | -| `model_error` | Das Modell selbst hat einen Fehler gemeldet, z.B. Kontext zu lang. Wird nicht anderswo wiederholt — wäre dort genauso. | `error` lesen | +| `context_exceeded` | Der Verlauf ist länger als das Kontextfenster. Dazu `promptTokens` (so lang war die Anfrage) und `ctx` (so viel geht). | Verlauf kürzen und nochmal senden — z.B. auf ~80 % von `ctx` abzüglich der gewünschten Antwortlänge. | +| `model_error` | Das Modell selbst hat einen anderen Fehler gemeldet. Wird nicht anderswo wiederholt — wäre dort genauso. | `error` lesen | **Fehler nie als Antwort der KI behandeln.** Zeig `error` dem Nutzer als Fehlermeldung an, aber schreib ihn nicht als Assistant-Nachricht in den @@ -247,8 +248,12 @@ Wer schon gegen Abschnitt C gebaut hat (`llm_models_request` / 2. **Auswahl:** Dem Nutzer nur den **Modellnamen** zeigen — nicht mehr „Modell · Karte · Box". Wo es läuft, entscheidet der Cluster. 3. **Kontextfenster:** `ctx` aus der Liste übernehmen statt einen eigenen Wert - einzustellen. Die Box gibt nur so viel her, wie dort steht — rechnet ein - Programm mit mehr, scheitern lange Aufträge oder werden falsch gekürzt. + einzustellen — und **danach kürzen**, nicht nur anzeigen. Die Box gibt nur + so viel her, wie dort steht; rechnet ein Programm mit mehr, kommt + `context_exceeded`. Den Fehler zusätzlich abfangen: kürzen und nochmal + senden (`promptTokens`/`ctx` stehen dabei). Tokens grob schätzen reicht + nicht immer — Code und Deutsch brauchen mehr Tokens pro Zeichen als + englischer Fließtext. 4. **Anfrage:** `llm_cluster_request` statt `llm_request`; **kein** `targetInstance` mehr. Die Antwort kommt als `llm_cluster_response`. 5. **Fehler:** `ok: false` als Fehler anzeigen, nicht als Antwort in den diff --git a/xtts/llm-adapter/adapter.py b/xtts/llm-adapter/adapter.py index be26974..5ad45ff 100644 --- a/xtts/llm-adapter/adapter.py +++ b/xtts/llm-adapter/adapter.py @@ -356,6 +356,24 @@ def _sanitize_messages(messages: list) -> list: return out +def _llama_error_info(exc: Exception) -> dict: + """Maschinenlesbare Fehlerart. Wichtigster Fall: der Verlauf ist laenger + als das Kontextfenster — dann soll ein Programm automatisch kuerzen und + nochmal fragen koennen, statt einen Text auszuwerten.""" + resp = getattr(exc, "response", None) + if resp is None: + return {} + try: + e = (resp.json() or {}).get("error") or {} + except Exception: + return {} + if isinstance(e, dict) and e.get("type") == "exceed_context_size_error": + return {"errorCode": "context_exceeded", + "promptTokens": int(e.get("n_prompt_tokens") or 0), + "ctx": int(e.get("n_ctx") or 0)} + return {} + + 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.""" @@ -419,7 +437,7 @@ async def _call_llama(messages: list, *, max_tokens: int, temperature: float, except Exception as e: msg = _llama_error_text(e) logger.warning("llama.cpp-Call fehlgeschlagen: %s", msg[:300]) - return {"ok": False, "content": "", "error": msg[:700]} + return {"ok": False, "content": "", "error": msg[:700], **_llama_error_info(e)} # B0.5-2: Lade-Status ans Diagnostic (service_status, service="llm"). Wir kennen @@ -678,6 +696,8 @@ async def _do_llm_request(ws, payload: dict) -> None: "tool_calls": tc, "error": res.get("error"), "model": model, + # Fehlerart fuer Programme (z.B. context_exceeded + promptTokens + ctx) + **{k: res[k] for k in ("errorCode", "promptTokens", "ctx") if res.get(k)}, # Wer hat geantwortet: Box + Karte. Hilft Clients mit mehreren Boxen. "instanceId": INSTANCE_ID, "node": NODE_NAME, "gpu": _card_of(model), "usage": res.get("usage") or {}, @@ -927,6 +947,10 @@ async def _handle_provision(ws, payload: dict) -> None: error=f"{key} liegt schon auf Karte {elsewhere} dieser Box — ein Modell pro Box nur einmal.") quant_note = "" repo_files = None + # Fuer ein Zuruecksetzen: der bisherige Registry-Eintrag dieses Modells. + # Noetig, wenn nur das Kontextfenster geaendert wird (gleiches Modell, gleiche + # Karte) — sonst bliebe nach einem Fehlschlag der zu grosse Wert stehen. + old_entry = next((dict(e) for e in _load_registry() if e.get("key") == key), None) if repo: wanted = (payload.get("quant") or "Q4_K_M").strip() quant = wanted @@ -1012,6 +1036,10 @@ async def _handle_provision(ws, payload: dict) -> None: else: slots.pop(card, None) _save_slots(slots) + reg = [e for e in _load_registry() if e.get("key") != key] + if old_entry: + reg.append(old_entry) + _save_registry(reg) _generate_config() await _reload_llama() await _announce(ws)