Programme sollen nicht Box und Karte aussuchen muessen. Die ARIA-Bridge kennt
alle Boxen samt Karten-Belegung und verteilt (bridge/llm_cluster.py):
- Auswahl: unter allen online Boxen mit dem Modell die mit den wenigsten
laufenden Anfragen, bei Gleichstand reihum. Eine Karte / alles frei →
alles dorthin ("verteilen, wenn noetig").
- Ausfall: geht eine Box waehrend der Anfrage offline (kein Ping mehr), wird
sie EINMAL an eine andere Box mit demselben Modell gegeben. Modell-Fehler
(z.B. Kontext zu lang) werden nicht wiederholt — waeren ueberall gleich.
- Fehler kommen als Fehler (ok=false + errorCode), nie als Antwort-Text: ein
Programm wuerde ihn sonst als Aussage der KI in seinen Verlauf schreiben —
so ist bei ARIA schon mal ein ganzer Chat kaputtgegangen.
RVS: llm_cluster_models_request → llm_cluster_models (jedes Modell EINMAL,
ctx = kleinstes Kontextfenster unter seinen Karten), llm_cluster_request →
llm_cluster_response. In der RVS-Allow-List freigegeben.
ARIA nutzt denselben Router (mit ihrer 30-s-Frist fuer den lokalen
Schnellweg) — vorher Round-Robin ohne Ausfall-Ersatz, und errorCode ging
verloren.
xtts/openai_rvs_wrapper: fuer Programme, die nur "OpenAI-kompatibel" koennen
(OpenHands & Co.). Kleiner Container NEBEN dem Programm, fuer das der Cluster
dann http://127.0.0.1:8099/v1 ist; braucht nur eine ausgehende Verbindung zum
RVS. /v1/models (jedes Modell einmal, context_length), /v1/chat/completions
(auch stream und tools), Fehler als HTTP mit OpenAI-Fehlerobjekt. Hilfsseite
auf / zeigt Status, Base URL und Modelle. Fuer Programme in Docker: Netz
aria-llm, Name openai-rvs-wrapper. .env.example mit RVS_HOST/PORT/TOKEN.
(Ein OpenAI-Endpunkt direkt auf der VM war kurz drin und ist wieder raus —
der Wrapper braucht keinen offenen Port auf der VM.)
docs/LLM-SCHNITTSTELLE.md neu gegliedert: A) Cluster ueber RVS (empfohlen),
B) Wrapper, C) direkt an eine Box (Sonderfall), plus "Umstellen" fuer
bestehende Clients (Software-Schmiede).
Tests: mit echtem RVS und zwei echten llm-adaptern als zwei Boxen —
Modell einmal gelistet, 6 parallele Anfragen 3/3 verteilt, Box friert
mitten in der Anfrage ein → nach 15 s von der anderen beantwortet
(attempts=2), danach direkt die gesunde, nicht geladenes Modell →
model_unavailable ohne content. Wrapper als Docker-Container gegen das
offizielle OpenAI-SDK: models.list, create mit/ohne stream, NotFoundError,
AuthenticationError. Das Doku-Beispiel lief unveraendert (nur Adresse).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
14 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. |
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 |
model_error |
Das Modell selbst hat einen Fehler gemeldet, z.B. Kontext zu lang. 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. Die Box gibt nur so viel her, wie dort steht — rechnet ein Programm mit mehr, scheitern lange Aufträge oder werden falsch gekürzt. - 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 - 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.
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.