⚠ 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)