- POST /v1/images/generations (OpenAI-kompatibel, prompt/size/n/seed/quality) - quality: standard=30 Steps (Default), high=50 Steps - Größen: 1024x1024, 1536x1024, 1024x1536, 1920x1088, 1088x1920 - GPU-Hotswap: Qwen stoppen -> FLUX laden -> Bild -> FLUX entladen -> Qwen wiederherstellen (exakt vorheriges Profil) - Zentrales GPU/Modell-Lock (Profilwechsel und Bild teilen sich das Lock) - Chat-Requests warten während Bild-Job (kein 502), Timeout CHAT_WAIT_TIMEOUT - Robuste Recovery: try/finally, Worker-Beendigung, VRAM-Check, Qwen-Readiness - /status: image.phase, image.worker, image.model_loaded, qwen.available, qwen.active_chats - GET /images, GET /images/<datei> (validiert, nur images/-Verzeichnis) - image_worker.py: FLUX-Worker (eigener Prozess, JSON-Protokoll, bf16 + enable_model_cpu_offload) - deploy: venv (torch/diffusers/transformers/accelerate), Modell-Download, Image-Dir, systemd-Unit mit Image-Umgebungsvariablen - dev: Mock-Worker, fake-systemctl, Benchmarks (GPU-Resident, Offload, Steps, Quality-Compare), 32 lokale Tests - README: Bildgenerierung, Hotswap, Recovery, Benchmarks (RTX 5080), Python-Pakete Benchmarks (RTX 5080, 16 GB, CPU-Offload): - 512x512 / 10 Steps: ~9.3 s - 1024x1024 / 30 Steps: ~31.3 s - 1024x1024 / 50 Steps: ~45.3 s - 1920x1088 / 50 Steps: ~91 s - Peak-VRAM: ~8.4-8.9 GB - Hotswap-Gesamtzeit: ~41-42 s (1024x1024 / 30 Steps)
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-longinchat.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 dann200. - 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 überPOST /<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:
- Zentrales GPU/Modell-Lock übernehmen (Profilwechsel und Bild teilen sich dasselbe Lock → kein Race).
- Aktives Qwen-Profil merken.
mike-ai-llama-ui.servicestoppen, warten bis Port + VRAM frei sind.- FLUX-Worker starten (eigener Prozess,
Flux2KleinPipeline, bf16 +enable_model_cpu_offload()), Bild generieren, PNG speichern. - Worker beenden (nicht nur entladen), VRAM-Freiheit verifizieren.
- Vorheriges Qwen-Profil exakt wiederherstellen, Readiness-Check (Modell geladen + Kontext passt).
- 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 (TimeoutCHAT_WAIT_TIMEOUT, Default 300 s). So gibt es keine502 llama.cpp nicht erreichbarwä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).
/statuszeigt 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: Bleibtfalse, wenn die Wiederherstellung fehlschlägt (Chat-Requests warten weiter, statt 502 zu liefern). Der Fehler wird inimage.last_errorund 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ürFlux2KleinPipeline)transformers(5.15.0)accelerate(1.14.0, fürenable_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:
- Überträgt
ai_profile_router.py,image_worker.py,install.shund die systemd-Unit per SCP nach/tmp/ai-profile-router/auf dem Zielsystem. - Führt
install.shper 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,accelerateanlegt (nur wenn noch nicht vorhanden), - das FLUX-Modell nach
/opt/mike-ai/models/FLUX.2-klein-base-4Blädt (nur wenn noch nicht vorhanden, ~15 GB), mike-ai-profile-router.serviceaktiviert (Start beim Boot) und startet,GET /statusverifiziert.
- den alten Router (
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,profilist 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 dieoverride.conf.