Mikei386 fdaaef98cc router: Metadaten pro Bild speichern (Sidecar-JSON)
- prompt, seed, width, height, size, steps, guidance, quality, seconds,
  model, created
- /images zeigt die Metadaten (falls vorhanden)
2026-08-19 09:01:34 +02:00
2026-08-18 22:52:48 +02:00

AI Profile Router

Kleiner OpenAI-kompatibler Proxy (Python, nur Standardbibliothek) vor einem lokalen llama.cpp-Server. Er leitet normale OpenAI-Requests transparent weiter (Streaming, Tool Calls, JSON), schaltet zwischen drei festen llama.cpp-Profilen um und orchestriert lokale Bildgenerierung mit FLUX.2 [klein] 4B Base (GPU-Hotswap: Qwen stoppen → FLUX laden → Bild → FLUX entladen → Qwen wiederherstellen).

Zielsystem

Host 192.168.1.196
SSH root mit Key lmstudio_unraid
llama.cpp http://127.0.0.1:8080 (Service mike-ai-llama-ui.service)
Profil-Skript /usr/local/bin/llama-profile {fast|medium|long}
Router-Port 8081
Router-Service mike-ai-profile-router.service

Profile / virtuelle Modelle

Profil Modell Kontext
fast qwen-fast 73728
medium qwen-medium 94208
long qwen-long 131072

Endpunkte

Endpunkt Beschreibung
GET /v1/models Die drei virtuellen Modelle inkl. context_length/context_window
GET /status Aktives Profil, Upstream-Zustand, Modell, Kontext, Uptime
POST /fast /medium /long Profilwechsel (auch GET möglich)
POST /v1/chat/completions Weiterleitung an llama.cpp (Streaming + Tool Calls)
POST /v1/images/generations Bildgenerierung (FLUX.2 [klein] 4B Base, OpenAI-kompatibel)
GET /images Liste der gespeicherten Bilder (max. 200)
GET /images/<datei> PNG-Download (nur images/-Verzeichnis, validiert)
alles andere Transparente Weiterleitung an llama.cpp

Verhalten

  • Virtuelles Modell (qwen-fast/qwen-medium/qwen-long in chat.completions): Der Router stellt sicher, dass das passende Profil aktiv ist (notfalls Wechsel + Warten), ersetzt das Modell durch das echte llama.cpp-Modell und leitet weiter.
  • Profilwechsel (POST /fast …): Führt /usr/local/bin/llama-profile <profil> aus (ohne Shell, feste Argumente → keine Injection), wartet dann, bis llama.cpp wieder erreichbar ist, und liefert erst dann 200.
  • Impliziter Wechsel bei downem llama.cpp: Ein Chat-Request mit virtuellem Modell liefert sofort 502, wenn das Profil bereits aktiv ist, aber llama.cpp down ist (kein stiller Neustart). Der Neustart wird explizit über POST /<profil> angestoßen.
  • Ungültige Profile/Modelle: POST /<anderes> → 400; qwen-<anderes> als Modell → 400. Nur die drei festen Profile sind schaltbar.
  • Fehlerformat: OpenAI-kompatibel ({"error": {"message", "type", "code"}}).

Bildgenerierung (FLUX.2 [klein] 4B Base)

Der Router orchestriert lokale Bildgenerierung mit black-forest-labs/FLUX.2-klein-base-4B (Apache 2.0, ~13 GB, bf16 + CPU-Offload). Da Qwen (llama.cpp) und FLUX denselben GPU/VRAM teilen, macht der Router einen GPU-Hotswap:

  1. Zentrales GPU/Modell-Lock übernehmen (Profilwechsel und Bild teilen sich dasselbe Lock → kein Race).
  2. Aktives Qwen-Profil merken.
  3. mike-ai-llama-ui.service stoppen, warten bis Port + VRAM frei sind.
  4. FLUX-Worker starten (eigener Prozess, Flux2KleinPipeline, bf16 + enable_model_cpu_offload()), Bild generieren, PNG speichern.
  5. Worker beenden (nicht nur entladen), VRAM-Freiheit verifizieren.
  6. Vorheriges Qwen-Profil exakt wiederherstellen, Readiness-Check (Modell geladen + Kontext passt).
  7. Erst dann antworten und das GPU-Lock freigeben.

Endpunkt POST /v1/images/generations

OpenAI-kompatibel. Unterstützt prompt, size, n, seed, quality, response_format.

Parameter Werte Default
prompt Text (Pflicht) –
size 1024x1024, 1536x1024, 1024x1536, 1920x1088, 1088x1920 1024x1024
n 1–4 1
seed int (reproduzierbar) zufällig
quality standard (30 Steps), high (50 Steps) standard
response_format url (Default), b64_json url

Beispiel:

curl -s http://192.168.1.196:8081/v1/images/generations \
  -H 'Content-Type: application/json' \
  -d '{"prompt":"ein roter Würfel auf weißem Grund","size":"1024x1024","quality":"standard"}'

Die Antwort enthält data[].url (absolute URL, über den Router abrufbar) und data[].b64_json (optional). Jedes Bild wird unter /opt/mike-ai/ai-profile-router/images/ gespeichert (kollisionsfreie Namen, img-<zeitstempel>-<seed>-<i>.png) und ist über GET /images/<datei> abrufbar.

Verhalten während eines Bild-Jobs

  • Chat-Requests warten (kein 502): Der Router merkt sich, dass Qwen vorübergehend nicht verfügbar ist (qwen.available=false), und Chat-Requests warten, bis Qwen wieder bereit ist (Timeout CHAT_WAIT_TIMEOUT, Default 300 s). So gibt es keine 502 llama.cpp nicht erreichbar während des Hotswaps.
  • Profilwechsel warten: Ein Profilwechsel während eines Bild-Jobs blockiert auf dem GPU-Lock, bis der Bild-Job fertig ist (kein Race).
  • /status zeigt den aktuellen Zustand: image.phase (idle, stopping-qwen, loading-image, generating, unloading-image, restoring-qwen), image.worker, image.model_loaded, image.last_image, image.last_seconds, image.last_error, qwen.available, qwen.active_chats.

Recovery (robust)

  • try/finally: Qwen wird immer wiederhergestellt, egal ob die Bildgenerierung erfolgreich war, fehlgeschlagen ist (OOM, Python-Fehler, ungültiger Prompt, Speichern-Fehler, Client-Disconnect, Timeout) oder der Worker abstürzt.
  • Worker-Beendigung: Nach jedem Job wird der Worker beendet (SIGTERM → SIGKILL), nicht nur entladen. So wird der VRAM (inkl. CUDA-Kontext) frei.
  • VRAM-Check: Nach dem Worker-Beenden wartet der Router, bis der VRAM unter 1000 MiB fällt (nvidia-smi), bevor Qwen neu startet.
  • Qwen-Readiness: Nach dem Neustart wartet der Router, bis llama.cpp erreichbar ist, das Modell geladen ist und der Kontext zum Profil passt.
  • qwen.available: Bleibt false, wenn die Wiederherstellung fehlschlägt (Chat-Requests warten weiter, statt 502 zu liefern). Der Fehler wird in image.last_error und im Log protokolliert.

Benchmarks (RTX 5080, 16 GB, CPU-Offload, gemessen)

Auflösung Steps Zeit Peak-VRAM (torch)
512×512 10 ~9.3 s ~8.4 GB
1024×1024 30 ~31.3 s ~8.4 GB
1024×1024 50 ~45.3 s ~8.4 GB
1920×1088 50 ~91 s ~8.9 GB

Entscheidung: standard = 30 Steps (Default, ~31 s bei 1024×1024), high = 50 Steps (maximale Qualität, ~45 s bei 1024×1024). Ab 20–30 Steps ist der Qualitätsgewinn bei einfachen Motiven gering; 50 Steps lohnt sich für komplexe Szenen.

Hinweis: FLUX.2 [klein] 4B Base passt nicht vollständig GPU-resident in 16 GB (OOM bei ~15.5 GB). Deshalb wird enable_model_cpu_offload() verwendet (Modelle werden pro Layer zwischen CPU und GPU gewechselt).

Hotswap-Gesamtzeit: Ein vollständiger Bild-Job (Qwen stoppen → FLUX laden → Bild → FLUX entladen → Qwen wiederherstellen) dauert ~41–42 s bei 1024×1024 / 30 Steps (davon ~31 s Generierung, ~10 s Qwen-Stop/Start + VRAM-Check).

Erforderliche Python-Pakete (im Venv)

  • torch (2.11.0+cu128, CUDA 12.8)
  • diffusers (0.40.0.dev0, für Flux2KleinPipeline)
  • transformers (5.15.0)
  • accelerate (1.14.0, für enable_model_cpu_offload())

Das Venv liegt unter /opt/mike-ai/ai-profile-router/venv/ und wird von install.sh automatisch angelegt/aktualisiert.

Repository-Struktur

router/ai_profile_router.py   # der Router (einzige Laufzeit-Datei)
router/image_worker.py        # FLUX-Worker (eigener Prozess, JSON-Protokoll)
deploy/mike-ai-profile-router.service   # systemd-Unit
deploy/install.sh             # läuft auf dem Zielsystem (per SSH)
deploy/deploy.sh              # läuft lokal: SCP + SSH
dev/                          # lokale Tests (Mock-llama.cpp, Mock-Worker, Benchmarks)

Entwicklungsdateien (dev/) und Deployment-Dateien (router/, deploy/) sind getrennt. Auf dem Zielsystem landet nur router/ + deploy/.

Deployment

Voraussetzung: SSH-Key ~/.ssh/lmstudio_unraid (bereits vorhanden).

./deploy/deploy.sh

Das Skript:

  1. Überträgt ai_profile_router.py, image_worker.py, install.sh und die systemd-Unit per SCP nach /tmp/ai-profile-router/ auf dem Zielsystem.
  2. Führt install.sh per SSH aus, das:
    • den alten Router (mike-ai-local-llm-router.service + /opt/mike-ai/local-llm-router) mit Backup entfernt,
    • den neuen Router + Worker nach /opt/mike-ai/ai-profile-router/ installiert,
    • ein Python-Venv mit torch, diffusers, transformers, accelerate anlegt (nur wenn noch nicht vorhanden),
    • das FLUX-Modell nach /opt/mike-ai/models/FLUX.2-klein-base-4B lädt (nur wenn noch nicht vorhanden, ~15 GB),
    • mike-ai-profile-router.service aktiviert (Start beim Boot) und startet,
    • GET /status verifiziert.

Die Installation ist idempotent (Update = erneut ausführen).

Konfiguration (Umgebungsvariablen in der systemd-Unit)

Variable Default Bedeutung
ROUTER_HOST 0.0.0.0 Bind-Adresse
ROUTER_PORT 8081 Port
UPSTREAM_URL http://127.0.0.1:8080 llama.cpp
PROFILE_SCRIPT /usr/local/bin/llama-profile Profil-Skript
PROFILE_DIR /etc/systemd/system/mike-ai-llama-ui.service.d Ort der override.conf
SWITCH_TIMEOUT 600 Warten auf llama.cpp nach Wechsel (s)
REQUEST_TIMEOUT 600 Read-Timeout für Upstream-Requests (s)
CONNECT_TIMEOUT 10 Connect-Timeout Upstream (s)
POLL_INTERVAL 2 Polling-Intervall (s)
LOG_LEVEL INFO Logging-Level
LLAMA_SERVICE mike-ai-llama-ui.service llama.cpp-Service (für Bild-Hotswap)
IMAGE_WORKER <router-dir>/image_worker.py FLUX-Worker-Skript
IMAGE_PYTHON sys.executable Python für den Worker (venv mit torch)
IMAGE_DIR /opt/mike-ai/ai-profile-router/images Bild-Speicherort
IMAGE_WORKER_LOG /opt/mike-ai/ai-profile-router/worker.log Worker-Log
IMAGE_GEN_TIMEOUT 600 Timeout pro Bild (s)
IMAGE_VRAM_FREE_TIMEOUT 120 Warten auf VRAM-Freiheit (s)
CHAT_WAIT_TIMEOUT 300 Chat wartet auf Qwen (s)

Lokale Tests

./dev/test_local.sh

Startet einen Mock-llama.cpp, einen Mock-Bild-Worker und den Router mit einem Fake-Profil-Skript und prüft: /v1/models, /status, Forwarding, Streaming, Tool Calls, Profilwechsel (fast→medium→fast), virtuelles Modell triggert Wechsel, ungültige Profile, Upstream down → 502, Recovery, Bildgenerierung (standard→30 Steps, high→50 Steps, Validierung, Image-Fehler→Qwen wiederhergestellt, Fast/Medium/Long→Image→gleiches Profil, /status während Bild-Job, paralleler Chat während Bild-Job wartet statt 502).

Betrieb

systemctl status mike-ai-profile-router
journalctl -u mike-ai-profile-router -f
curl -s http://192.168.1.196:8081/status | python3 -m json.tool
curl -s -X POST http://192.168.1.196:8081/medium

Sicherheit

  • Keine Shell-Aufrufe: Profil-Skript wird mit subprocess.run([script, profil]) aufgerufen, profil ist Whitelist-geprüft (fast|medium|long).
  • Keine Secrets/Tokens im Code oder in der Unit.
  • systemd-Hardening: NoNewPrivileges=true, PrivateTmp=true.
  • Die bestehende llama.cpp-/Profil-Konfiguration wird nicht verändert; der Router nutzt nur das vorhandene llama-profile-Skript und liest die override.conf.
S
Description
No description provided
Readme
8.6 MiB
Languages
Python 77.5%
Shell 17%
TypeScript 1.9%
Dockerfile 1.4%
Jinja 1.4%
Other 0.7%