Files
ARIA-AGENT/docs/LLM-SCHNITTSTELLE.md
duffyduckandClaude Opus 5.5 48fc88d430 fix(openai-wrapper): Kontext-Ueberlauf als OpenAI-400 context_length_exceeded
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>
2026-10-25 15:04:09 +01:00

16 KiB
Raw Permalink Blame History

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_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:

{ "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:

  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 — 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 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/:

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, Verlauf zu lang → 400 context_length_exceeded mit 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-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://<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.