# 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://:/?token= ``` - Das **Token ist der Raum** — dasselbe `RVS_TOKEN` wie 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 `requestId` zu. - 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 `requestId` an die Wartenden (siehe Beispiel). Mehrere gleichzeitige `recv()` auf derselben Verbindung gehen nicht. Jede Nachricht hat diese Hülle: ```json { "type": "", "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` ```json { "type": "llm_cluster_models_request", "payload": { "requestId": "a1" } } ``` Antwort: ```json { "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` ```json { "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: ```json { "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. ```python 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: 1. **Modelliste:** `llm_cluster_models_request` statt `llm_models_request`. Statt einer Antwort pro Box kommt **eine** Antwort mit jedem Modell einmal. Kein Einsammeln über Sekunden mehr. 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. 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 Verlauf. Neue Codes siehe Tabelle oben. 6. **Lesen:** ein Leser pro Verbindung, Antworten per `requestId` zuordnen. 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/`](../xtts/openai_rvs_wrapper/): ```bash 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, mit `context_length` - `POST /v1/chat/completions` — auch `stream: true` (kommt derzeit in einem Rutsch) und `tools` - 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-Gpu` sagen, wer geantwortet hat - **Läuft das Programm selbst in Docker**, ist `127.0.0.1` dort der eigene Container. Dann das Docker-Netz `aria-llm` anhängen und `http://openai-rvs-wrapper:8099/v1` nehmen (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://: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.