Einstellungen → Modelle & KI → "LLM-Karten": fuer jede Karte, die auf einer Box fuers LLM vorgesehen ist, ein Dropdown mit den Katalog-Modellen und "— leer —" (entlaedt die Karte). Nur echte LLM-Karten erscheinen. Modelle, die auf derselben Box schon auf einer anderen Karte liegen, werden nicht angeboten; zu grosse (Katalog-Groesse > VRAM der Karte) sind als "passt nicht" markiert. Waehrend eines Ladevorgangs ist das Dropdown gesperrt und zeigt den Fortschritt. Box meldet dafuer pro Karte den VRAM (nvidia-smi memory.total) und das Kontextfenster, mit dem llama-server das Modell startet (-c). Kontext an Clients: llm_models enthaelt jetzt "ctx". Anlass: die Software- Schmiede rechnete mit 32768 Tokens, die Box startet die Modelle mit 8192 — lange Auftraege waeren gescheitert oder falsch gekuerzt worden, und der Client konnte es nicht wissen. Doku entsprechend ergaenzt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
11 KiB
Lokale Sprachmodelle nutzen — Schnittstelle für Drittprogramme
Diese Seite beschreibt, wie ein Programm die lokalen Sprachmodelle auf den KI-Boxen von ARIA nutzt: über den RVS (WebSocket) oder direkt über den OpenAI-kompatiblen Port einer Box.
Die Grundregel
Clients wählen 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 auf welcher Karte liegt, legt ausschließlich der Betreiber in der ARIA-Diagnostic fest (Modell-Katalog → Laden → Karte).
- Ein Client — ARIA selbst, ein Drittprogramm, OpenHands — sieht nur die geladenen Modelle aller Boxen und schickt seine Anfrage an eines davon.
- Eine Anfrage für ein Modell, das nicht geladen ist, wird abgelehnt. Sie lädt nichts nach und verdrängt nichts. Kein Client kann einem anderen das Modell wegnehmen.
- Mehrere Clients dürfen gleichzeitig dasselbe Modell nutzen. Die Karte bedient sie parallel; jede einzelne Antwort wird dabei etwas langsamer, die Karte schafft in Summe aber mehr.
Welche Karten einer Box dem LLM gehören, steht in deren .env:
# Beispiel: Karte 0 für Spracherkennung + Sprachausgabe, Karten 1 und 2 fürs LLM
WHISPER_GPU=0
F5TTS_GPU=0
LLM_GPUS=1,2
Zwei Zugänge
| RVS (WebSocket) | OpenAI-Port der Box | |
|---|---|---|
| Erreichbar | überall, wo der RVS erreichbar ist | nur im Netz der Box (Standard: nur localhost) |
| Sieht | alle Boxen und ihre Karten | nur diese eine Box |
| Format | eigene Nachrichten (unten) | OpenAI /v1/chat/completions |
| Typische Nutzer | eigene Programme, ARIA | OpenHands, Continue, Open WebUI |
A) Über den RVS
Verbinden
wss://<RVS_HOST>:<RVS_PORT>/?token=<RVS_TOKEN>
- Das Token ist der Raum. Es ist dasselbe Token, das ARIA, die Diagnostic
und die KI-Boxen nutzen (
RVS_TOKEN). Wer das Token hat, ist im Raum. - Der RVS ist ein Verteiler: Jede Nachricht geht an alle anderen
Teilnehmer des Raums. Du siehst also auch Antworten, die für andere Clients
bestimmt sind — ordne Antworten immer über die
requestIdzu. - Der RVS lässt nur bekannte Nachrichtentypen durch. Unbekannte Typen verschwinden ohne Fehlermeldung. Die hier beschriebenen sind freigegeben.
Jede Nachricht hat diese Hülle:
{ "type": "<typ>", "payload": { … }, "timestamp": 1760000000000 }
1. Welche Modelle gibt es? — llm_models_request
{ "type": "llm_models_request", "payload": { "requestId": "abc-1" } }
Jede Box im Raum antwortet einzeln mit llm_models. Sammle die Antworten
ein paar Sekunden lang ein (2–3 s reichen):
{
"type": "llm_models",
"payload": {
"requestId": "abc-1",
"instanceId": "llm@ki-box",
"node": "ki-box",
"models": [
{ "model": "qwen3-8b", "gpu": "1", "gpuName": "NVIDIA GeForce RTX 3060", "ctx": 8192, "state": "ready" },
{ "model": "qwen3-4b", "gpu": "2", "gpuName": "NVIDIA GeForce RTX 3060", "ctx": 8192, "state": "ready" }
]
}
}
| Feld | Bedeutung |
|---|---|
instanceId |
Adresse der Box — brauchst du gleich als targetInstance |
node |
lesbarer Name der Box |
models[].model |
Modellname, so in der Anfrage angeben |
models[].gpu |
Karte, auf der es läuft |
models[].ctx |
Kontextfenster in Tokens, mit dem das Modell gestartet ist — Eingabe und Ausgabe zusammen. Diesen Wert verwenden, nicht den, den das Modell theoretisch kann: Die Box gibt nur so viel her, wie hier steht. 0 = unbekannt. |
models[].state |
ready = bereit, starting = startet gerade neu (Anfragen warten dann kurz), unknown = Box konnte es nicht prüfen |
Es stehen nur geladene Modelle in der Liste. Was dort nicht steht, kannst du nicht nutzen.
Mitlesen statt fragen: Die Boxen melden sich außerdem regelmäßig selbst
(worker_hello etwa alle 30 s, worker_ping alle 10 s, jeweils mit
service: "llm", models und slots). Ein Client, der länger verbunden
bleibt, kann die Liste daraus aktuell halten.
2. Fragen — llm_request
{
"type": "llm_request",
"payload": {
"requestId": "abc-2",
"targetInstance": "llm@ki-box",
"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) — damit findest du deine Antwort |
messages |
ja | Verlauf im OpenAI-Format (system / user / assistant / tool) |
model |
dringend empfohlen | eines aus llm_models. Ohne Angabe nimmt die Box das Modell ihrer ersten LLM-Karte. |
targetInstance |
dringend empfohlen | die instanceId aus llm_models. Siehe „Ohne targetInstance". |
max_tokens |
nein | Standard 512 |
temperature |
nein | Standard 0.7 |
stop |
nein | Liste von Stop-Strings |
tools |
nein | Tool-Definitionen im OpenAI-Format; Antwort enthält dann ggf. tool_calls |
3. Antwort — llm_response
{
"type": "llm_response",
"payload": {
"requestId": "abc-2",
"ok": true,
"content": "Die Zugspitze ist 2962 Meter hoch.",
"tool_calls": null,
"model": "qwen3-8b",
"instanceId": "llm@ki-box",
"node": "ki-box",
"gpu": "1",
"usage": { "prompt_tokens": 31, "completion_tokens": 12, "total_tokens": 43 },
"elapsedMs": 640
}
}
Im Fehlerfall ist ok false, dazu kommen error (Text für Menschen) und
errorCode (für Programme):
errorCode |
Bedeutung | Was tun |
|---|---|---|
model_not_loaded |
Das Modell liegt auf dieser Box nicht. loadedModels nennt, was dort geladen ist. |
Ein geladenes Modell wählen. Die Liste neu holen (llm_models_request). |
no_model_loaded |
Auf dieser Box ist keine LLM-Karte belegt. | Andere Box wählen. |
| (fehlt) | Fehler des Modells selbst, z.B. Kontext zu lang | error lesen |
Ohne targetInstance
Ohne targetInstance geht die Anfrage an alle Boxen. Antworten werden nur die,
die das Modell geladen haben — die anderen schweigen, damit du nicht von
jeder Box einen Fehler bekommst. Hat keine Box das Modell, kommt also gar
keine Antwort und dein Client läuft in seinen Timeout. Darum: immer
targetInstance setzen. Dann bekommst du bei einem Problem sofort eine klare
Absage.
Haben mehrere Boxen dasselbe Modell geladen und du lässt targetInstance
weg, antworten alle. Auch das spricht für targetInstance.
Was Clients nicht senden
llm_provision_model, llm_unload_model und llm_remove_model sind der
Diagnostic vorbehalten. Technisch könnte jeder im Raum sie senden — der RVS
unterscheidet die Teilnehmer nicht. Ein Programm, das sich daran nicht hält,
tauscht anderen Clients das Modell unter den Füßen weg; genau das soll diese
Schnittstelle verhindern.
Beispiel (Python)
Braucht Python 3.11+ und websockets (pip install websockets). Genau so gegen einen
echten RVS, llm-adapter und llama-swap getestet — nur die Adresse war eine andere.
import asyncio, json, uuid
import websockets
RVS = "wss://rvs.example.org:443/?token=DEIN_RVS_TOKEN"
async def models(ws, wait=2.5):
"""Alle geladenen Modelle aller Boxen: [(instanceId, node, model, gpu)]."""
rid = str(uuid.uuid4())
await ws.send(json.dumps({"type": "llm_models_request", "payload": {"requestId": rid}}))
found = []
loop = asyncio.get_running_loop()
end = loop.time() + wait
while (left := end - loop.time()) > 0:
try:
msg = json.loads(await asyncio.wait_for(ws.recv(), left))
except asyncio.TimeoutError:
break
p = msg.get("payload") or {}
if msg.get("type") == "llm_models" and p.get("requestId") == rid:
found += [(p["instanceId"], p["node"], m["model"], m["gpu"]) for m in p["models"]]
return found
async def ask(ws, instance_id, model, prompt, timeout=120):
rid = str(uuid.uuid4())
await ws.send(json.dumps({"type": "llm_request", "payload": {
"requestId": rid, "targetInstance": instance_id, "model": model,
"messages": [{"role": "user", "content": prompt}], "max_tokens": 256}}))
async with asyncio.timeout(timeout):
while True:
msg = json.loads(await ws.recv())
p = msg.get("payload") or {}
# Der Raum ist ein Verteiler: nur UNSERE Antwort nehmen.
if msg.get("type") == "llm_response" and p.get("requestId") == rid:
if not p["ok"]:
raise RuntimeError(f"{p.get('errorCode')}: {p.get('error')}")
return p["content"]
async def main():
async with websockets.connect(RVS, max_size=2**24) as ws:
available = await models(ws)
for iid, node, model, gpu in available:
print(f"{node} / Karte {gpu}: {model} ({iid})")
if available:
iid, node, model, gpu = available[0]
print(await ask(ws, iid, model, "Sag Hallo in einem Satz."))
asyncio.run(main())
B) Über den OpenAI-Port einer Box
Jede KI-Box bietet einen OpenAI-kompatiblen Endpunkt — für Programme, die nur „OpenAI-kompatibel" können (OpenHands, Continue, Open WebUI …).
http://<IP der Box>:<LLM_OPENAI_PORT>/v1 (Standard-Port 8081)
GET /v1/modelslistet nur die geladenen Modelle dieser Box.POST /v1/chat/completionsmit"model": "<Name>"wie gewohnt.- Ein nicht geladenes Modell wird mit HTTP 404 abgelehnt
(
"no router for requested model") — es wird nichts nachgeladen. - Ist in der Diagnostic ein API-Key gesetzt, mit
Authorization: Bearer <KEY>schicken. - Standardmäßig hört der Port nur auf
localhostder Box. Für Zugriff aus dem NetzLLM_OPENAI_BIND=0.0.0.0in der.envder Box setzen — dann unbedingt einen API-Key vergeben. - Dieser Zugang sieht nur seine eigene Box. Wer mehrere Boxen nutzen will, nimmt den RVS.
curl http://192.168.177.153:8081/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model": "qwen3-8b", "messages": [{"role": "user", "content": "Hallo"}]}'
Gleichzeitige Nutzung und Geschwindigkeit
- Mehrere Clients können dasselbe Modell gleichzeitig nutzen. llama.cpp bedient sie parallel (Continuous Batching); jede Antwort wird etwas langsamer, insgesamt schafft die Karte mehr.
- Der Kontext (
-c, Standard 8192 Tokens) ist bei aktuellen llama.cpp-Versionen ein gemeinsamer Vorrat aller gleichzeitigen Anfragen. Schicken mehrere Clients gleichzeitig sehr lange Verläufe, wird es dort eng. - Ältere llama.cpp-Versionen arbeiten Anfragen an dasselbe Modell nacheinander
ab statt parallel. Welche Version eine Box fährt, hängt am Image von
llama-swap (
docker compose pull llama-swapholt die aktuelle).
Wenn sich etwas ändert
- Lädt der Betreiber ein anderes Modell in eine Karte, ist das alte sofort
weg; laufende Clients bekommen dann
model_not_loadedund sollten die Liste neu holen. - Beim Laden oder Entladen startet die Box alle ihre Modelle kurz neu
(wenige Sekunden). Anfragen in diesem Moment warten, bis das Modell wieder
bereit ist (
state: "starting"). - Diese Schnittstelle wird erweitert, nicht umgebaut: Neue Felder können dazukommen. Ein Client sollte unbekannte Felder ignorieren.