Bisher kam context_exceeded als 502 an — OpenAI-Clients (litellm/OpenHands) hielten das fuer einen Serverfehler statt zu kuerzen/verdichten. Jetzt 400 mit code context_length_exceeded und dem OpenAI-ueblichen Text inkl. Zahlen. Doku: Abschnitt "Den Verlauf verwaltet das Programm" (Kuerzen, Verdichten, Gedaechtnis) fuer Drittprogramme. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
16 KiB
Lokale Sprachmodelle nutzen — Schnittstelle für Programme
Wie ein Programm die lokalen Sprachmodelle auf den KI-Boxen von ARIA nutzt.
Kurz: Du fragst ein Modell, nicht eine Box oder Karte. Der Cluster sucht sich aus, wo es läuft. Eigene Programme sprechen dafür über den RVS (Abschnitt A). Programme, die nur „OpenAI-kompatibel" können, bekommen den OpenAI-RVS-Wrapper neben sich gestellt (Abschnitt B).
Die Grundregel
Programme wählen ein Modell aus — laden aber nie.
- Jede KI-Box hat eine oder mehrere Grafikkarten fürs LLM. Jede dieser Karten hält genau ein Modell, dauerhaft geladen.
- Welches Modell wo liegt, legt nur der Betreiber in der ARIA-Diagnostic fest (Einstellungen → LLM-Karten).
- Ein Programm sieht die geladenen Modelle des ganzen Clusters — jedes einmal, egal auf wie vielen Karten es liegt — und wählt eins.
- Liegt das Modell auf mehreren Karten, verteilt der Cluster bei Last. Fällt eine Box mitten in einer Anfrage aus, übernimmt eine andere mit demselben Modell. Davon merkt das Programm nichts.
- Ist das Modell nirgends geladen, gibt es einen Fehler — nie eine normale Antwort mit Fehlertext. Ein Programm würde so einen Text sonst als Aussage der KI in seinen Verlauf schreiben.
- Mehrere Programme dürfen gleichzeitig dasselbe Modell nutzen. Die Karte bedient sie parallel; jede einzelne Antwort wird dabei etwas langsamer.
Welcher Zugang?
| A) Cluster über RVS | B) OpenAI-RVS-Wrapper | C) Direkt an eine Box | |
|---|---|---|---|
| Für | eigene Programme | OpenHands, Continue, Open WebUI … | Sonderfälle |
| Sieht | alle Modelle, jedes einmal | alle Modelle, jedes einmal | nur diese Box |
| Verteilung + Ausfall-Ersatz | ja | ja | nein |
| Braucht | ausgehende Verbindung zum RVS | Wrapper-Container auf demselben Rechner | ausgehende Verbindung zum RVS |
A) Cluster über RVS — empfohlen
Verbinden
wss://<RVS_HOST>:<RVS_PORT>/?token=<RVS_TOKEN>
- Das Token ist der Raum — dasselbe
RVS_TOKENwie bei ARIA und den KI-Boxen. Wer es hat, ist im Raum. - Der Programm-Rechner braucht nur eine ausgehende Verbindung zum RVS. Keine Portfreigabe, kein VPN — er kann in jedem Netz stehen.
- Der RVS ist ein Verteiler: Jede Nachricht geht an alle anderen im Raum.
Du siehst also auch Antworten für andere — ordne sie immer über die
requestIdzu. - Unbekannte Nachrichtentypen verwirft der RVS ohne Fehlermeldung. Die hier beschriebenen sind freigegeben.
- Nur ein Leser pro Verbindung. Schickst du mehrere Anfragen gleichzeitig,
liest eine Schleife alle Nachrichten und verteilt sie per
requestIdan die Wartenden (siehe Beispiel). Mehrere gleichzeitigerecv()auf derselben Verbindung gehen nicht.
Jede Nachricht hat diese Hülle:
{ "type": "<typ>", "payload": { … }, "timestamp": 1760000000000 }
Beantwortet werden llm_cluster_*-Nachrichten von der ARIA-Bridge — sie
kennt alle Boxen und entscheidet, wohin eine Anfrage geht. Läuft ARIA nicht,
kommt keine Antwort (dein Programm sollte einen Timeout haben).
1. Welche Modelle gibt es? — llm_cluster_models_request
{ "type": "llm_cluster_models_request", "payload": { "requestId": "a1" } }
Antwort:
{
"type": "llm_cluster_models",
"payload": {
"requestId": "a1",
"models": [
{
"model": "qwen3-8b",
"ctx": 8192,
"available": 2,
"cards": [
{ "node": "ki-box", "instanceId": "llm@ki-box", "gpu": "1", "gpuName": "NVIDIA GeForce RTX 3060", "state": "ready" },
{ "node": "werkstatt", "instanceId": "llm@werkstatt", "gpu": "0", "gpuName": "NVIDIA RTX A4000", "state": "ready" }
]
}
]
}
}
| 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. 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 |
2. Fragen — llm_cluster_request
{
"type": "llm_cluster_request",
"payload": {
"requestId": "a2",
"model": "qwen3-8b",
"messages": [
{ "role": "system", "content": "Du antwortest kurz." },
{ "role": "user", "content": "Wie hoch ist die Zugspitze?" }
],
"max_tokens": 256,
"temperature": 0.7
}
}
| Feld | Pflicht | Bedeutung |
|---|---|---|
requestId |
ja | eindeutig pro Anfrage (z.B. UUID) |
model |
ja* | aus llm_cluster_models. *Darf fehlen, wenn im Cluster genau ein Modell geladen ist. |
messages |
ja | Verlauf im OpenAI-Format (system / user / assistant / tool) |
max_tokens |
nein | Standard 512 |
temperature |
nein | Standard 0.7 |
stop |
nein | Liste von Stop-Strings |
tools |
nein | Tool-Definitionen im OpenAI-Format; die Antwort enthält dann ggf. tool_calls |
3. Antwort — llm_cluster_response
Erfolg:
{
"type": "llm_cluster_response",
"payload": {
"requestId": "a2",
"ok": true,
"content": "Die Zugspitze ist 2962 Meter hoch.",
"model": "qwen3-8b",
"node": "werkstatt",
"gpu": "0",
"usage": { "prompt_tokens": 31, "completion_tokens": 12, "total_tokens": 43 },
"elapsedMs": 640,
"attempts": 1
}
}
tool_calls steht drin, wenn das Modell ein Werkzeug aufrufen will (OpenAI-Format).
node/gpu sagen, wer geantwortet hat; attempts: 2 heißt, die erste Box ist
ausgefallen und eine andere hat übernommen.
Fehler — ok: false, dazu error (Text für Menschen) und errorCode:
errorCode |
Bedeutung | Was tun |
|---|---|---|
model_unavailable |
Das Modell ist gerade auf keiner Karte im Cluster geladen. available nennt, was es gibt. |
Dem Nutzer anzeigen; Liste neu holen. |
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 |
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
Verlauf.
Beispiel (Python)
Braucht Python 3.11+ und websockets (pip install websockets). Ein Leser
pro Verbindung, mehrere Anfragen gleichzeitig möglich.
import asyncio, json, uuid
import websockets
RVS = "wss://rvs.example.org:444/?token=DEIN_RVS_TOKEN"
class AriaCluster:
"""Eine RVS-Verbindung, ein Leser. Antworten gehen per requestId an die
wartende Anfrage — der Raum ist ein Verteiler, Fremdes wird ignoriert."""
def __init__(self, ws):
self.ws, self.wait = ws, {}
self.reader = asyncio.create_task(self._read())
async def _read(self):
async for raw in self.ws:
msg = json.loads(raw)
p = msg.get("payload") or {}
fut = self.wait.get((msg.get("type"), p.get("requestId")))
if fut and not fut.done():
fut.set_result(p)
async def _call(self, mtype, answer, payload, timeout):
rid = str(uuid.uuid4())
fut = asyncio.get_running_loop().create_future()
self.wait[(answer, rid)] = fut
try:
await self.ws.send(json.dumps({"type": mtype, "payload": dict(payload, requestId=rid)}))
return await asyncio.wait_for(fut, timeout)
finally:
self.wait.pop((answer, rid), None)
async def models(self):
res = await self._call("llm_cluster_models_request", "llm_cluster_models", {}, 6)
return res["models"]
async def chat(self, model, messages, timeout=660, **opts):
res = await self._call("llm_cluster_request", "llm_cluster_response",
dict(opts, model=model, messages=messages), timeout)
if not res["ok"]:
raise RuntimeError(f"{res.get('errorCode')}: {res.get('error')}")
return res
async def main():
async with websockets.connect(RVS, max_size=2**24) as ws:
cluster = AriaCluster(ws)
models = await cluster.models()
for m in models:
print(f"{m['model']} (Kontext {m['ctx']}, auf {m['available']} Karte(n))")
if models:
res = await cluster.chat(models[0]["model"],
[{"role": "user", "content": "Sag Hallo in einem Satz."}],
max_tokens=64)
print(res["content"], f"— von {res['node']} / Karte {res['gpu']}")
asyncio.run(main())
Umstellen von der direkten Box-Ansprache
Wer schon gegen Abschnitt C gebaut hat (llm_models_request /
llm_request mit targetInstance), stellt so um:
- Modelliste:
llm_cluster_models_requeststattllm_models_request. Statt einer Antwort pro Box kommt eine Antwort mit jedem Modell einmal. Kein Einsammeln über Sekunden mehr. - Auswahl: Dem Nutzer nur den Modellnamen zeigen — nicht mehr „Modell · Karte · Box". Wo es läuft, entscheidet der Cluster.
- Kontextfenster:
ctxaus der Liste übernehmen statt einen eigenen Wert einzustellen — und danach kürzen, nicht nur anzeigen. Die Box gibt nur so viel her, wie dort steht; rechnet ein Programm mit mehr, kommtcontext_exceeded. Den Fehler zusätzlich abfangen: kürzen und nochmal senden (promptTokens/ctxstehen dabei). Tokens grob schätzen reicht nicht immer — Code und Deutsch brauchen mehr Tokens pro Zeichen als englischer Fließtext. - Anfrage:
llm_cluster_requeststattllm_request; keintargetInstancemehr. Die Antwort kommt alsllm_cluster_response. - Fehler:
ok: falseals Fehler anzeigen, nicht als Antwort in den Verlauf. Neue Codes siehe Tabelle oben. - Lesen: ein Leser pro Verbindung, Antworten per
requestIdzuordnen.
Felder und Format von messages, tools, tool_calls und usage bleiben
gleich.
B) OpenAI-RVS-Wrapper — für Programme, die nur OpenAI können
Ein kleiner Container, der auf demselben Rechner wie das Programm läuft. Für das Programm ist der Cluster dann ein ganz normaler OpenAI-Endpunkt:
Programm ──HTTP──▶ Wrapper (127.0.0.1:8099) ──RVS──▶ ARIA-Bridge ──▶ KI-Box / Karte
Liegt im Repo unter xtts/openai_rvs_wrapper/:
cd xtts/openai_rvs_wrapper
cp .env.example .env # RVS_HOST, RVS_PORT, RVS_TOKEN eintragen
docker compose up -d --build
Im Browser zeigt http://127.0.0.1:8099/ eine Hilfsseite: ob die Verbindung steht und welche Modelle es gibt. Im Programm eintragen:
| Feld | Wert |
|---|---|
| Base URL | http://127.0.0.1:8099/v1 |
| API-Key | beliebig — oder WRAPPER_API_KEY, falls gesetzt |
| Modell | ein Name von der Hilfsseite |
| Kontextfenster | der Wert von der Hilfsseite |
GET /v1/models— jedes Modell einmal, mitcontext_lengthPOST /v1/chat/completions— auchstream: true(kommt derzeit in einem Rutsch) undtools- Fehler als HTTP-Fehler mit OpenAI-Fehlerobjekt: nicht geladenes Modell →
404
model_not_found, Cluster nicht erreichbar → 503, Zeitüberschreitung → 504, Verlauf zu lang → 400context_length_exceededmit dem OpenAI-üblichen Text („maximum context length is N tokens …") — daran erkennen OpenAI-Clients (litellm, OpenHands …) den Überlauf und kürzen/verdichten selbst - Antwort-Header
X-Aria-Node/X-Aria-Gpusagen, wer geantwortet hat - Läuft das Programm selbst in Docker, ist
127.0.0.1dort der eigene Container. Dann das Docker-Netzaria-llmanhängen undhttp://openai-rvs-wrapper:8099/v1nehmen (Details in der README des Wrappers).
Mit dem offiziellen OpenAI-Python-SDK getestet (models.list,
chat.completions.create mit und ohne Stream, Fehlerklassen).
C) Direkt an eine Box — Sonderfall
Ohne den Cluster, eine bestimmte Box gezielt. Keine Verteilung, kein Ausfall-Ersatz. Nur nötig, wenn ein Programm bewusst eine bestimmte Box will oder ARIA nicht läuft.
Modelle der Boxen: llm_models_request {requestId} → jede Box
antwortet einzeln mit llm_models {requestId, instanceId, node, models: [{model, gpu, gpuName, ctx, state}]}. Ein paar Sekunden einsammeln.
Anfrage: llm_request {requestId, targetInstance, model, messages, …}
→ llm_response {requestId, ok, content, tool_calls, model, instanceId, node, gpu, usage, elapsedMs}. Bei Fehler errorCode:
model_not_loaded (mit loadedModels) oder no_model_loaded.
Ohne targetInstance antworten nur Boxen, die das Modell geladen haben — die
anderen schweigen. Hat keine es, kommt keine Antwort. Darum hier immer
targetInstance setzen.
OpenAI-Port einer Box: Jede Box hat zusätzlich einen eigenen
OpenAI-Endpunkt (http://<IP der Box>:8081/v1, Standard nur localhost der
Box). Er sieht nur diese Box; nicht geladene Modelle → HTTP 404. Für mehrere
Boxen den Wrapper (B) nehmen.
Den Verlauf verwaltet das Programm
Die Box merkt sich nichts zwischen zwei Anfragen. Jede Anfrage muss alles
mitbringen, was das Modell sehen soll (Systemprompt, Werkzeuge, Verlauf). Das
Kontextfenster (ctx) ist die Obergrenze für eine Anfrage inklusive
Antwort. Wächst der Verlauf darüber hinaus, muss das Programm ihn kleiner
machen — drei übliche Wege, auch kombiniert:
| Weg | Was passiert | Gut für |
|---|---|---|
| Kürzen | älteste Nachrichten weglassen (Systemprompt behalten) | einfache Chats, Werkzeug-Schleifen |
| Verdichten („Compacting") | alte Teile vom Modell zusammenfassen lassen, Zusammenfassung statt Original mitsenden | lange Arbeitssitzungen |
| Gedächtnis (RAG) | Wichtiges als Fakten in einer Datenbank ablegen, pro Anfrage nur Passendes dazuholen | dauerhaftes Wissen über Sitzungen hinweg |
Wichtig dabei: Systemprompt und Werkzeug-Definitionen zählen mit, Platz
für die Antwort (max_tokens) einplanen, Token grob schätzen (Deutsch/Code:
eher 2,5–3 Zeichen pro Token) — und context_exceeded trotzdem abfangen:
kürzen, einmal neu senden.
Was Programme nicht senden
llm_provision_model, llm_unload_model und llm_remove_model sind der
ARIA-Diagnostic vorbehalten. Technisch könnte jeder im Raum sie senden — der
RVS unterscheidet die Teilnehmer nicht. Ein Programm, das sich nicht daran
hält, tauscht anderen das Modell unter den Füßen weg.
Gleichzeitige Nutzung und Geschwindigkeit
- Mehrere Anfragen an dasselbe Modell laufen auf einer Karte parallel. Jede einzelne wird etwas langsamer, in Summe schafft die Karte mehr.
- Liegt ein Modell auf mehreren Karten, schickt der Cluster neue Anfragen an die Karte mit den wenigsten laufenden.
- Der Kontext (
ctx) ist bei aktuellen llama.cpp-Versionen ein gemeinsamer Vorrat aller gleichzeitigen Anfragen auf einer Karte. Schicken mehrere Programme gleichzeitig sehr lange Verläufe, wird es dort eng.
Wenn sich etwas ändert
- Lädt der Betreiber ein anderes Modell in eine Karte, ist das alte dort weg.
Liegt es noch auf einer anderen Karte, merkt ein Programm nichts; sonst kommt
model_unavailable— dann die Liste neu holen. - Beim Laden oder Entladen startet die betroffene Box alle ihre Modelle kurz neu (wenige Sekunden). Anfragen in diesem Moment warten oder gehen an eine andere Box.
- Diese Schnittstelle wird erweitert, nicht umgebaut: Neue Felder können dazukommen. Unbekannte Felder ignorieren.