Compare commits
5
Commits
2e2490c1bc
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3eacfa2a75 | ||
|
|
4e7f9425e3 | ||
|
|
571652f480 | ||
|
|
796a63671b | ||
|
|
a465821bb8 |
No files matched your search
+10
-3
@@ -11,6 +11,12 @@ ALLOWED_NETS="127.0.0.0/8 ::1 192.168.178.0/24 100.73.0.0/16"
|
|||||||
LISTEN_PORT=8800
|
LISTEN_PORT=8800
|
||||||
BACKEND_PORT=8080
|
BACKEND_PORT=8080
|
||||||
|
|
||||||
|
# Web-Chat (Open WebUI): Port nach außen (Caddy) und intern (nur 127.0.0.1)
|
||||||
|
WEBUI_PORT=8801
|
||||||
|
WEBUI_BACKEND_PORT=3000
|
||||||
|
# Schlüssel für Login-Sitzungen: openssl rand -hex 32
|
||||||
|
WEBUI_SECRET_KEY=CHANGE_ME
|
||||||
|
|
||||||
# Host-Nutzer, dem die Modelldateien in ./models gehören: id -u / id -g
|
# Host-Nutzer, dem die Modelldateien in ./models gehören: id -u / id -g
|
||||||
HOST_UID=1000
|
HOST_UID=1000
|
||||||
HOST_GID=1000
|
HOST_GID=1000
|
||||||
@@ -21,9 +27,9 @@ VIDEO_GID=983
|
|||||||
|
|
||||||
# GPU-Auswahl (0 = erste ROCm-GPU; iGPU ausblenden)
|
# GPU-Auswahl (0 = erste ROCm-GPU; iGPU ausblenden)
|
||||||
HIP_VISIBLE_DEVICES=0
|
HIP_VISIBLE_DEVICES=0
|
||||||
# GPU-Auswahl für Vulkan (sd-server). Vulkan zählt die iGPU mit – Reihenfolge steht
|
# GPU für Vulkan (whisper-/sd-server) als PCI-ID vendor:device – stabil, anders als
|
||||||
# beim Laden eines Bildmodells im Log: docker compose logs llama-swap | grep ggml_vulkan
|
# die Geräte-Nummer, die zwischen Reboots wechselt: lspci -nn | grep -i vga
|
||||||
VK_VISIBLE_DEVICES=1
|
VK_DEVICE=1002:73a5
|
||||||
|
|
||||||
# Optional: Hugging-Face-Token für gated Modelle / höhere Download-Limits
|
# Optional: Hugging-Face-Token für gated Modelle / höhere Download-Limits
|
||||||
HF_TOKEN=
|
HF_TOKEN=
|
||||||
@@ -33,3 +39,4 @@ LLAMA_SWAP_TAG=v265-rocm-b11610
|
|||||||
# Quelle für whisper-server/sd-server (gleiche llama-swap-Version wie oben wählen)
|
# Quelle für whisper-server/sd-server (gleiche llama-swap-Version wie oben wählen)
|
||||||
UNIFIED_TAG=unified-vulkan-265
|
UNIFIED_TAG=unified-vulkan-265
|
||||||
CADDY_TAG=2.10.2-alpine
|
CADDY_TAG=2.10.2-alpine
|
||||||
|
OPEN_WEBUI_TAG=v0.11.4
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# Plan: NVIDIA-Windows-Rechner als flexibler Cluster hinter dem bestehenden Gateway
|
||||||
|
|
||||||
|
## Context
|
||||||
|
Mehrere Windows-PCs mit NVIDIA-GPUs sollen zusätzliche Rechenleistung liefern. Der bestehende
|
||||||
|
Linux-PC (AMD, Docker: Caddy + llama-swap) bleibt der **einzige Endpunkt** (:8800, gleicher API-Key).
|
||||||
|
**Jeder Knoten kann jedes Cluster-Modell ausführen** und wechselt bei Bedarf (llama-swap). Linux ist
|
||||||
|
auf den Knoten nicht nötig: llama-swap (`llama-swap_265_windows_amd64.zip`) und llama.cpp
|
||||||
|
(`llama-bXXXXX-bin-win-cuda-{12.4|13.4}-x64.zip` + `cudart-…`) gibt es fertig für Windows.
|
||||||
|
|
||||||
|
Erkenntnisse aus der Recherche:
|
||||||
|
- llama-swap v265 leitet über `peers` pro Modell an **einen** Server weiter (URL-Pfad wird angehängt). Das Load-Balancing übernimmt deshalb Caddy.
|
||||||
|
- llama-swap löst `${env.VAR}` in der Config auf, also kommen keine Secrets ins Git.
|
||||||
|
- Ältere llama.cpp-Builds werden gelöscht (b11610 gibt es schon nicht mehr). Der Installer wählt den Build über die GitHub-API (`latest` oder fest).
|
||||||
|
- Caddy auf dem Host-Netz ist für den Container wegen ufw schwer erreichbar. Deshalb läuft der Loadbalancer als **eigener Container im Compose-Netz**.
|
||||||
|
|
||||||
|
## Architektur
|
||||||
|
```
|
||||||
|
Client ─► Caddy :8800 (IP-Filter, API-Key) ─► llama-swap (AMD, lokal)
|
||||||
|
│ lokale Modelle wie bisher
|
||||||
|
└─ peers: cluster-<m> ─► http://cluster-lb:8090/m/cluster-<m>
|
||||||
|
cluster-lb (Caddy-Container, generiert):
|
||||||
|
/m/cluster-qwen3.5-35b-a3b/* ─► Knoten [A, B, C] (Stammknoten A)
|
||||||
|
/m/cluster-glm-4.7-flash/* ─► Knoten [B, C, A] (Stammknoten B)
|
||||||
|
/m/cluster-qwen3.5-9b/* ─► Knoten [C, A, B] (Stammknoten C)
|
||||||
|
Win-Knoten: llama-swap.exe :8080 mit ALLEN cluster-Modellen, API-Key, Firewall nur Gateway-IP
|
||||||
|
```
|
||||||
|
**Routing ohne unnötige Modellwechsel:** Für jedes Modell enthält der Pool alle Knoten, aber in rotierter Reihenfolge. Dazu kommen `lb_policy first` und `unhealthy_request_count <Slots>`:
|
||||||
|
- Eine Anfrage geht an den Stammknoten des Modells. Dort ist das Modell meist schon geladen und der Prompt-Cache noch vorhanden.
|
||||||
|
- Ist der Stammknoten belegt oder ausgefallen, geht sie an den nächsten Knoten. Der lädt das Modell dann, das ist der flexible Fall.
|
||||||
|
- Sind alle Knoten belegt, wartet Caddy (`lb_try_duration 5m`, `lb_try_interval 1s`), bis einer frei wird.
|
||||||
|
- Health-Checks auf `/health` alle 10 s, ausgefallene Knoten werden übersprungen.
|
||||||
|
- Wenn es mehr Modelle als Knoten gibt, teilen sich Modelle einen Stammknoten (Zuordnung reihum).
|
||||||
|
|
||||||
|
## Änderungen auf dem Gateway (Linux)
|
||||||
|
1. **[docker-compose.yml](docker-compose.yml)**: neuer Service `cluster-lb` (`caddy:${CADDY_TAG}`), nur im Compose-Netz und ohne Port nach außen. Er mountet `caddy/cluster.Caddyfile`. `llama-swap` bekommt `CLUSTER_API_KEY` als Umgebungsvariable.
|
||||||
|
2. **Neu `scripts/cluster-gen.sh`**: liest `CLUSTER_NODES="192.168.178.50:8080 192.168.178.51:8080 …"` und `CLUSTER_SLOTS` (Standard 1) aus `.env`. Die Modellliste kommt aus `cluster/windows/node-config.yaml` (einzige Quelle, Schlüssel `cluster-*`). Daraus erzeugt das Skript `caddy/cluster.Caddyfile` (in `.gitignore`), mit einem `handle_path /m/<modell>/*` und rotierter Upstream-Liste pro Modell. Ohne Knoten entsteht eine gültige Datei, die 503 liefert. [scripts/start.sh](scripts/start.sh) ruft das Skript vor `docker compose up` auf.
|
||||||
|
3. **[llama-swap/config.yaml](llama-swap/config.yaml)**: Block `peers:` mit einem Eintrag pro Cluster-Modell, `proxy: http://cluster-lb:8090/m/<modell>`, `apiKey: ${env.CLUSTER_API_KEY}`, dazu großzügige `timeouts.responseHeader` für Modellwechsel und Download.
|
||||||
|
4. **`.env.example` / `.env`**: `CLUSTER_API_KEY` (start.sh erzeugt ihn wie `LLM_API_KEY`, wenn er fehlt), `CLUSTER_NODES`, `CLUSTER_SLOTS`.
|
||||||
|
5. **Neu `scripts/cluster-status.sh`**: zeigt pro Knoten `/health`, das geladene Modell (`/running`) und die Antwortzeit. start.sh zeigt die Ausgabe im Statusblock, wenn Knoten gesetzt sind.
|
||||||
|
|
||||||
|
## Windows-Knoten-Skripte (`cluster/windows/`)
|
||||||
|
- **`install-node.ps1`** (als Admin; wiederholbar, dient auch zum Update):
|
||||||
|
Parameter `-GatewayIP` und `-ApiKey` (Pflicht), `-Port 8080`, `-InstallDir C:\localai-node`, `-LlamaBuild latest`, `-Cuda auto|12.4|13.4`, `-Preload <modell>` (optional), `-HfToken`, `-KeepSleep`, `-Force`.
|
||||||
|
1. Prüft Admin-Rechte, `nvidia-smi` (GPU, VRAM, Compute Capability, Treiber) und freien Speicherplatz (Warnung unter 60 GB, weil alle Modelle auf jedem Knoten liegen können).
|
||||||
|
2. CUDA-Variante: 13.4, wenn Compute Capability ≥ 7.5 und Treiber ≥ 580, sonst 12.4.
|
||||||
|
3. Lädt llama-swap und llama.cpp CUDA + cudart herunter, entpackt sie nach `bin\` und testet `llama-server.exe --version`.
|
||||||
|
4. Schreibt `config.yaml` aus der Vorlage. Bestehende Dateien bleiben unangetastet, außer bei `-Force`.
|
||||||
|
5. Erzeugt `start-node.cmd`: setzt `LLAMA_CACHE`, `HF_TOKEN` und `NODE_API_KEY`, startet `llama-swap.exe -config … -listen 0.0.0.0:PORT` und schreibt das Log nach `logs\`.
|
||||||
|
6. Firewall-Regel: TCP-Port eingehend nur von `GatewayIP`.
|
||||||
|
7. Geplante Aufgabe `LocalAI-Node`: Start beim Booten, als SYSTEM, ohne Laufzeitlimit, Neustart bei Absturz.
|
||||||
|
8. Standby am Netzteil aus (`powercfg`), außer bei `-KeepSleep`.
|
||||||
|
9. Startet die Aufgabe, wartet auf `/health` und gibt die Zeile für `CLUSTER_NODES` aus.
|
||||||
|
- **`download-models.ps1`** (optional): lädt alle Cluster-Modelle vorab herunter, indem es jedes Modell einmal kurz anfragt. So dauert die erste echte Anfrage nicht wegen eines 20-GB-Downloads.
|
||||||
|
- **`node-config.yaml`** (Vorlage, Quelle der Modellliste): alle Cluster-Chat-Modelle (`cluster-qwen3.5-35b-a3b`, `cluster-glm-4.7-flash`, `cluster-qwen3.5-9b`) in **einer** Swap-Gruppe, Pfade mit `/`, `--fit` mit `--fit-target 1024` (passt sich automatisch an den VRAM des Knotens an), `-np <Slots>`. Dazu `apiKeys: ["${env.NODE_API_KEY}"]`, optional Preload, `healthCheckTimeout: 3600`.
|
||||||
|
- **`install-node.cmd`**: Doppelklick-Wrapper, fordert Admin-Rechte an, setzt `-ExecutionPolicy Bypass` und fragt Gateway-IP und Key ab.
|
||||||
|
- **`status-node.ps1`**: Zustand der Aufgabe, `/health`, `/running`, `nvidia-smi`, letzte Logzeilen.
|
||||||
|
- **`uninstall-node.ps1`**: entfernt Aufgabe und Firewall-Regel und löscht optional das Verzeichnis (`-KeepModels`).
|
||||||
|
|
||||||
|
## Doku
|
||||||
|
- Neu **`docs/cluster.md`**: Architektur und Routing (Stammknoten, Überlauf), Ablauf (Key → Knoten installieren → IP in `CLUSTER_NODES` → `./scripts/start.sh`), Modell hinzufügen (nur in `node-config.yaml` + `peers`, dann Knoten mit `-Force` aktualisieren). Grenzen: Ein Modell, das größer als VRAM+RAM eines Knotens ist, wird nicht über mehrere PCs verteilt. Kleine GPUs lagern per `--fit` in den RAM aus und sind dann langsamer.
|
||||||
|
- **[README.md](README.md)**: Abschnitt „Cluster“ mit Link, Dateiliste ergänzen. Die Client-Doku nennt die `cluster-*`-Modellnamen.
|
||||||
|
|
||||||
|
## Verifikation
|
||||||
|
1. `cluster-gen.sh` mit 0, 1 und 3 Test-Knoten ausführen und jeweils `caddy validate` (Docker, `caddy:2.10.2-alpine`) laufen lassen. Außerdem `docker compose config`.
|
||||||
|
2. Kette ohne Windows testen: Als Fake-Knoten dient eine zweite llama-swap-Linux-Binary bzw. ein Echo-Server im Scratchpad, auf zwei Ports. Prüfen:
|
||||||
|
- `/v1/models` zeigt `cluster-*`.
|
||||||
|
- Die Anfrage läuft über :8800 → peer → cluster-lb → Stammknoten.
|
||||||
|
- Streaming funktioniert.
|
||||||
|
- Bei belegtem Stammknoten geht die Anfrage an den nächsten Knoten.
|
||||||
|
- Ein gestoppter Knoten wird übersprungen.
|
||||||
|
3. PowerShell-Syntax mit `pwsh` parsen, falls verfügbar. Den echten Lauf auf einem Windows-PC macht der Nutzer; der Installer endet mit einem Selbsttest (`/health`).
|
||||||
|
4. Der bestehende Betrieb bleibt unverändert: `./scripts/start.sh` startet weiter erfolgreich, auch ohne Knoten.
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
# Plan: Transkription + Bildgenerierung, Anbindung an Laravel (laravel/ai)
|
|
||||||
|
|
||||||
> **Stand der Umsetzung (2026-10-15) – Server fertig, Laravel-Teil dokumentiert:**
|
|
||||||
> - **Weder A noch B, sondern Variante C:** Vulkan schafft nur 54 % der ROCm-Prompt-Verarbeitung (Messwerte unten). Deshalb baut [`llama-swap/Dockerfile`](llama-swap/Dockerfile) auf dem ROCm-Image auf und kopiert `whisper-server` + `sd-server` aus `unified-vulkan-265` dazu. LLMs laufen weiter auf ROCm, Whisper und Bilder über Vulkan, und alles verwaltet ein llama-swap.
|
|
||||||
> - Vulkan-Gerät 0 ist die **iGPU**. Die RX 6950 XT ist Gerät 1 → `VK_VISIBLE_DEVICES=1`.
|
|
||||||
> - **Whisper läuft auf der GPU statt der CPU:** 0,46 s statt 3,3 s (CPU, 12 Threads, `-l de`) bzw. 9,3 s (CPU, 8 Threads, `-l auto`) für 6,6 s Sprache. Es braucht ~1,2 GB VRAM und kostet die Chat-Geschwindigkeit nachweislich nichts (852 t/s mit und ohne).
|
|
||||||
> - ffmpeg ist im ROCm-Image schon enthalten, `--convert` funktioniert: WAV, WebM/Opus und M4A/AAC werden fehlerfrei transkribiert.
|
|
||||||
> - Bilder: Z-Image-Turbo Q8, VAE von `Comfy-Org/z_image_turbo` (nicht gated). Die Dauer hängt an der GPU-Rechenleistung (~4 s/Schritt bei 1024²). `--offload-to-cpu` / `--backend te=cpu` ändern daran nichts, `--offload-to-cpu` bleibt (spart VRAM).
|
|
||||||
> - OpenAI-Format getestet: Alias `gpt-image-1`, `size: 1536x1024`, `quality` (wird ignoriert), ohne `response_format` → `b64_json`.
|
|
||||||
> - `scripts/start.sh`: baut das Image, lädt Whisper vor, `--pull` aktualisiert die Basis-Images und baut neu.
|
|
||||||
> - Phase 5 (Laravel) ist als Anleitung in [`docs/laravel.md`](docs/laravel.md) umgesetzt. In einer echten Laravel-App ist sie **noch nicht getestet** (offen: wo die App läuft).
|
|
||||||
> - Nebenbefund, älter als dieser Plan: Der Startup-Preload von llama-swap meldet für `qwen2.5-coder-1.5b`/`nomic-embed` „status 404“ (llama-server ohne Web-UI antwortet auf `/` mit 404). Die Modelle laden trotzdem.
|
|
||||||
|
|
||||||
## Context
|
|
||||||
Eine Laravel-Anwendung soll über das **Laravel AI SDK** (`laravel/ai`) diesen Server nutzen:
|
|
||||||
1. **Agent** (Chat, Tools, Streaming, Embeddings). Das geht schon heute ohne Server-Änderung.
|
|
||||||
2. **Spracheingabe**: Aufnahmetaste im Browser → Transkription (Whisper) → Text als Prompt an den Agenten.
|
|
||||||
3. **Bildgenerierung**, auch als Tool des Agenten.
|
|
||||||
|
|
||||||
Was das SDK laut Doku (https://laravel.com/docs/ai-sdk) kann:
|
|
||||||
|
|
||||||
| Funktion | Treiber `openai-compatible` | Bemerkung |
|
|
||||||
|---|---|---|
|
|
||||||
| Text, Streaming, Tools, Structured Output | ✅ | |
|
|
||||||
| Embeddings | ✅ | Standardmodell in der Config nötig |
|
|
||||||
| Transkription (STT) | ✅ | Standardmodell in der Config nötig, keine Sprecher-Trennung (Diarization) |
|
|
||||||
| Bilder | ❌ | Umweg: Treiber `openai` mit eigener `url` oder ein eigener HTTP-Aufruf |
|
|
||||||
| TTS | ❌ | nicht Teil dieses Plans |
|
|
||||||
|
|
||||||
## Kernproblem: Image-Wahl (ROCm vs. Vulkan)
|
|
||||||
Das aktuelle Image `llama-swap:vXXX-rocm-bYYYY` enthält **nur llama-server**.
|
|
||||||
`whisper-server` und `sd-server` gibt es nur im **unified**-Image, und das für AMD nur als
|
|
||||||
**`unified-vulkan`** (versionierte Tags: `unified-vulkan-NNN`). Ein ROCm-unified-Image gibt es nicht.
|
|
||||||
|
|
||||||
| | A: alles auf `unified-vulkan` (**bevorzugt**) | B: ROCm bleibt, Whisper separat |
|
|
||||||
|---|---|---|
|
|
||||||
| Container | 1 × llama-swap | llama-swap (ROCm) + whisper-Container (CPU) |
|
|
||||||
| VRAM-Koordination Chat ↔ Bildmodell | llama-swap übernimmt das (Gruppe `chat`) | keine. Bildmodell und Chat-Modell würden sich um VRAM streiten, **Bilder fallen in B weg** |
|
|
||||||
| Routing | alles über llama-swap | Caddy leitet `/v1/audio/*` am llama-swap vorbei |
|
|
||||||
| Risiko | Vulkan ist evtl. langsamer als ROCm, deshalb Benchmark | mehr Teile, keine Bilder |
|
|
||||||
|
|
||||||
**Entscheidung über Benchmark (Phase 1):** Kommt Vulkan bei Token-Generierung **und** Prompt-Verarbeitung
|
|
||||||
auf ≥ ~85 % von ROCm, dann Variante A. Sonst Variante B, und die Bilder bleiben vorerst weg.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 1: Benchmark Vulkan vs. ROCm (~1 h)
|
|
||||||
1. Den Tag ermitteln: https://github.com/mostlygeek/llama-swap/pkgs/container/llama-swap → neuestes `unified-vulkan-NNN`.
|
|
||||||
2. Messen, während der laufende Stack aus ist (`docker compose stop`), mit denselben Modelldateien aus `./models`:
|
|
||||||
```bash
|
|
||||||
docker run --rm --device /dev/dri --group-add 987 --group-add 983 \
|
|
||||||
-e GGML_VK_VISIBLE_DEVICES=0 -v ./models:/models \
|
|
||||||
--entrypoint llama-bench ghcr.io/mostlygeek/llama-swap:unified-vulkan-NNN \
|
|
||||||
-m /models/<Pfad zur Qwen3.5-35B-A3B-Q4_K_M.gguf> -p 2048 -n 128 -fa 1
|
|
||||||
```
|
|
||||||
Dasselbe mit dem ROCm-Image (`/dev/kfd` dazu, `HIP_VISIBLE_DEVICES=0`). Den GGUF-Pfad unter
|
|
||||||
`models/models--unsloth--Qwen3.5-35B-A3B-GGUF/snapshots/*/` suchen.
|
|
||||||
- Ob `llama-bench` in beiden Images vorhanden ist, prüfen. Falls nicht: `llama-server` starten und die t/s aus dem Log einer Test-Anfrage nehmen.
|
|
||||||
- Wichtig: Für den 35B-MoE muss die Auslagerung in den RAM gleich sein. Notfalls mit `--n-cpu-moe` vergleichbar festlegen.
|
|
||||||
3. `GGML_VK_VISIBLE_DEVICES=0`: Der 7900X3D hat eine iGPU, die Vulkan sonst als zweites Gerät sieht.
|
|
||||||
Mit `vulkaninfo --summary` bzw. dem Startlog prüfen, dass Gerät 0 die RX 6950 XT ist.
|
|
||||||
4. Die Ergebnisse unten unter „Messwerte“ eintragen und A oder B entscheiden.
|
|
||||||
|
|
||||||
## Phase 2 (A): Stack auf `unified-vulkan` umstellen
|
|
||||||
- `.env` / `.env.example`: `LLAMA_SWAP_TAG=unified-vulkan-NNN`. Den alten ROCm-Tag als Kommentar behalten (Rollback).
|
|
||||||
- `docker-compose.yml` (llama-swap):
|
|
||||||
- `devices`: nur noch `/dev/dri` nötig (`/dev/kfd` schadet nicht, kann aber weg).
|
|
||||||
- `environment`: `GGML_VK_VISIBLE_DEVICES: "${VK_VISIBLE_DEVICES:-0}"` statt bzw. zusätzlich zu `HIP_VISIBLE_DEVICES`.
|
|
||||||
- Prüfen, ob das Image mit `user: 1000:1000` läuft. Es gibt auch `-rootless`-Varianten.
|
|
||||||
- Prüfen, ob Binärpfade (`/app/llama-server` vs. `llama-server` im PATH) zum `server`-Makro passen.
|
|
||||||
- `llama-swap/config.yaml`: Bestehende Modelle unverändert lassen. Prüfen, ob `--fit` / `--fit-target` unter Vulkan genauso greift (VRAM mit `rocm-smi` beobachten).
|
|
||||||
- Gegenprobe: opencode + Continue (Chat, Autocomplete, `@codebase`) funktionieren wie vorher.
|
|
||||||
- README: Tabelle „Auf einen anderen PC übertragen“ anpassen (unified-vulkan ist jetzt Standard).
|
|
||||||
|
|
||||||
## Phase 3: Whisper (Transkription)
|
|
||||||
1. Modell herunterladen (whisper-server kann kein `-hf`):
|
|
||||||
```bash
|
|
||||||
mkdir -p models/whisper
|
|
||||||
curl -L -o models/whisper/ggml-large-v3-turbo-q8_0.bin \
|
|
||||||
https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo-q8_0.bin
|
|
||||||
```
|
|
||||||
2. Eintrag in `llama-swap/config.yaml`, **auf der CPU** und in `always-on`, damit er nie das Chat-Modell verdrängt:
|
|
||||||
```yaml
|
|
||||||
whisper:
|
|
||||||
name: Whisper large-v3-turbo (CPU)
|
|
||||||
checkEndpoint: / # prüfen: hat whisper-server /health?
|
|
||||||
cmd: >-
|
|
||||||
whisper-server --host 127.0.0.1 --port ${PORT}
|
|
||||||
-m /models/whisper/ggml-large-v3-turbo-q8_0.bin
|
|
||||||
--inference-path /v1/audio/transcriptions
|
|
||||||
--convert --tmp-dir /tmp
|
|
||||||
-ng -t 8 -l auto
|
|
||||||
ttl: 0
|
|
||||||
```
|
|
||||||
- Die Flags mit `docker compose exec llama-swap whisper-server --help` prüfen: `--inference-path`, `--convert` (braucht **ffmpeg** im Image, mit `which ffmpeg` prüfen), `-ng`/`--no-gpu`, `--tmp-dir`.
|
|
||||||
- Ohne ffmpeg: `--convert` weglassen, dann wandelt Laravel vorher in 16-kHz-Mono-WAV um (siehe Phase 5).
|
|
||||||
- `whisper` zu `routing…groups.always-on.members` und zu `hooks.on_startup.preload` hinzufügen.
|
|
||||||
3. `scripts/start.sh`: Beim Preload einen Whisper-Request schicken. Dafür eine kurze Test-WAV unter `scripts/` ablegen, z. B. 1 s Stille mit `ffmpeg -f lavfi -i anullsrc=r=16000:cl=mono -t 1 scripts/silence.wav`.
|
|
||||||
4. Test:
|
|
||||||
```bash
|
|
||||||
curl -H "Authorization: Bearer $KEY" http://127.0.0.1:8800/v1/audio/transcriptions \
|
|
||||||
-F model=whisper -F file=@test.webm -F response_format=json
|
|
||||||
```
|
|
||||||
Mit einer echten deutschen Sprachaufnahme testen und die Dauer bei ~10 s Audio notieren (Ziel < 3 s).
|
|
||||||
5. Caddy: keine Änderung nötig. Es gibt standardmäßig kein Upload-Limit, und der `read_timeout` von 30 min reicht.
|
|
||||||
|
|
||||||
## Phase 4: Bildgenerierung (nur Variante A)
|
|
||||||
1. Modell: **Z-Image-Turbo** (Vorlage aus `docker/config.example.yaml` von llama-swap, 8 Steps, schnell).
|
|
||||||
Benötigt drei Dateien in `models/sd/`: `z_image_turbo-Q8_0.gguf`, `ae.safetensors` (VAE), `qwen3-4b-instruct-2507-q8_0.gguf` (Text-Encoder).
|
|
||||||
Die Hugging-Face-Quellen beim Umsetzen heraussuchen. Alternative: FLUX.1-schnell (GGUF).
|
|
||||||
2. Eintrag in der **`chat`-Gruppe**, damit llama-swap das Chat-Modell entlädt, solange ein Bild generiert wird:
|
|
||||||
```yaml
|
|
||||||
z-image:
|
|
||||||
name: Z-Image Turbo (Bilder)
|
|
||||||
checkEndpoint: /
|
|
||||||
cmd: |
|
|
||||||
sd-server --listen-ip 127.0.0.1 --listen-port ${PORT}
|
|
||||||
--diffusion-fa
|
|
||||||
--diffusion-model /models/sd/z_image_turbo-Q8_0.gguf
|
|
||||||
--vae /models/sd/ae.safetensors
|
|
||||||
--llm /models/sd/qwen3-4b-instruct-2507-q8_0.gguf
|
|
||||||
--offload-to-cpu --cfg-scale 1.0 --steps 8
|
|
||||||
--height 1024 --width 1024
|
|
||||||
aliases: [gpt-image-1, dall-e-3]
|
|
||||||
ttl: 600
|
|
||||||
```
|
|
||||||
- Die Flags mit `sd-server --help` prüfen, besonders den Namen von `--listen-ip`.
|
|
||||||
- Die `aliases` sind wichtig: Der OpenAI-Treiber von Laravel schickt seinen eigenen Modellnamen.
|
|
||||||
3. Test: `curl …/v1/images/generations -d '{"model":"z-image","prompt":"a red bicycle","response_format":"b64_json"}'`.
|
|
||||||
Dabei messen: Dauer inkl. Modellwechsel, VRAM-Spitze, ob Autocomplete (Continue) weiterläuft.
|
|
||||||
4. **Hinweis für den Alltag:** Jede Bildgenerierung wirft das Chat-Modell raus (Neuladen dauert einige zehn Sekunden).
|
|
||||||
Für einen Agenten, der Bilder als Tool erzeugt, heißt das: Chat → Bild → Chat bedeutet zwei Modellwechsel.
|
|
||||||
|
|
||||||
## Phase 5: Laravel-Anwendung
|
|
||||||
### 5.1 Config (`config/ai.php` + `.env`)
|
|
||||||
```php
|
|
||||||
'providers' => [
|
|
||||||
// Text, Agent, Embeddings, Transkription
|
|
||||||
'local' => [
|
|
||||||
'driver' => 'openai-compatible',
|
|
||||||
'url' => env('LOCAL_AI_URL'), // http://<server>:8800/v1
|
|
||||||
'key' => env('LOCAL_AI_API_KEY'),
|
|
||||||
'models' => [
|
|
||||||
'text' => ['default' => 'qwen3.5-35b-a3b'],
|
|
||||||
'embeddings' => ['default' => 'nomic-embed', 'dimensions' => 768],
|
|
||||||
'transcription' => ['default' => 'whisper'],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
// nur für Bilder: OpenAI-Treiber auf den eigenen Server umgebogen
|
|
||||||
'local-images' => [
|
|
||||||
'driver' => 'openai',
|
|
||||||
'key' => env('LOCAL_AI_API_KEY'),
|
|
||||||
'url' => env('LOCAL_AI_URL'),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
```
|
|
||||||
- Den Env-Namen für die URL-Überschreibung je nach SDK-Version prüfen (Doku 13.x: `url`/`OPENAI_URL`, 12.x: `OPENAI_BASE_URL`).
|
|
||||||
- Den Schlüssel prüfen, mit dem `local` zum Standard-Provider für Transkription wird (`Transcription` hat in der Doku keinen `provider:`-Parameter).
|
|
||||||
- Netz: Der Laravel-Server muss in `ALLOWED_NETS` liegen, im LAN oder als NetBird-Peer.
|
|
||||||
- Timeouts großzügig setzen (`timeout: 300` beim Prompt), Agent-Läufe mit Tools als Queue-Job.
|
|
||||||
|
|
||||||
### 5.2 Agent
|
|
||||||
- `php artisan make:agent Assistant`: Anweisungen, Tools.
|
|
||||||
- Smoke-Test: `(new Assistant)->prompt('Hallo', provider: 'local')`, danach ein Tool-Call-Test.
|
|
||||||
|
|
||||||
### 5.3 Spracheingabe (Aufnahmetaste)
|
|
||||||
- **Frontend:** `getUserMedia` + `MediaRecorder` (gedrückt halten = aufnehmen). Bevorzugt `audio/webm;codecs=opus`, Safari liefert `audio/mp4`.
|
|
||||||
Den Blob per `FormData` an `POST /voice` schicken, die Antwort als Stream lesen.
|
|
||||||
- **HTTPS ist Pflicht.** Das Mikrofon geht nur in einem sicheren Kontext. Die Laravel-App braucht deshalb HTTPS, lokal geht auch `localhost`.
|
|
||||||
- **Backend:**
|
|
||||||
```php
|
|
||||||
Route::post('/voice/transcribe', fn (Request $r) =>
|
|
||||||
['text' => (string) Transcription::fromUpload($r->file('audio'))->generate()]);
|
|
||||||
Route::post('/voice/ask', fn (Request $r) =>
|
|
||||||
(new Assistant)->stream($r->string('text')));
|
|
||||||
```
|
|
||||||
Zwei Schritte, damit der User den erkannten Text **vor** dem Absenden sieht und korrigieren kann.
|
|
||||||
Das ist wichtig bei Agenten, deren Tools etwas ausführen.
|
|
||||||
- Wenn `whisper-server` kein `--convert` kann: In Laravel vorher umwandeln mit
|
|
||||||
`ffmpeg -i in.webm -ar 16000 -ac 1 out.wav` (Symfony Process), dann `Transcription::fromPath(...)`.
|
|
||||||
- Validierung: Upload-Limit (z. B. 20 MB), Mindestlänge, leere Transkripte abfangen.
|
|
||||||
|
|
||||||
### 5.4 Bilder
|
|
||||||
- Direkt: `Image::of('...')->generate(provider: 'local-images')` (die genaue API in der Doku nachsehen).
|
|
||||||
Falls der OpenAI-Treiber Parameter schickt, die `sd-server` ablehnt: Fallback auf `Http::withToken(...)->post(.../images/generations)`
|
|
||||||
und `b64_json` selbst in den Storage schreiben.
|
|
||||||
- Als Agent-Tool: `GenerateImage`-Tool, das eins von beidem aufruft und eine URL bzw. einen Storage-Pfad zurückgibt.
|
|
||||||
|
|
||||||
## Phase 6: Doku im Repo
|
|
||||||
- `docs/laravel.md`: Config, Netz/Key, Spracheingabe, Bilder, Stolperfallen (HTTPS, Formate, Modellwechsel).
|
|
||||||
- README: Modelltabelle um `whisper` und `z-image` ergänzen, Images und Rollback beschreiben.
|
|
||||||
- `PLAN.md`-Stil: Abweichungen oben in diesem Plan unter „Stand der Umsetzung“ festhalten.
|
|
||||||
|
|
||||||
## Verifikation
|
|
||||||
1. opencode + Continue funktionieren unverändert (nach dem Image-Wechsel!), die t/s liegen im erwarteten Bereich.
|
|
||||||
2. `GET /v1/models` listet `whisper` und `z-image`.
|
|
||||||
3. Transkription per curl (deutsch, webm und wav) liefert korrekten Text. Im Log wird dabei **kein** Chat-Modell entladen.
|
|
||||||
4. Bild per curl: PNG kommt zurück, danach lädt das Chat-Modell beim nächsten Request wieder.
|
|
||||||
5. Laravel: Agent-Prompt mit Tool-Call, Spracheingabe Ende-zu-Ende im Browser (Chrome + Firefox, wenn möglich Safari), Bild über das Agent-Tool.
|
|
||||||
6. Nach einem Reboot ist Whisper vorgeladen (`/running`).
|
|
||||||
|
|
||||||
## Rollback
|
|
||||||
`LLAMA_SWAP_TAG` in `.env` zurück auf `v265-rocm-b11610`, `/dev/kfd` + `HIP_VISIBLE_DEVICES` wieder rein,
|
|
||||||
die Einträge `whisper`/`z-image` auskommentieren, `docker compose up -d`.
|
|
||||||
|
|
||||||
## Messwerte (in Phase 1 ausfüllen)
|
|
||||||
| Image | pp2048 t/s | tg128 t/s | Bemerkung |
|
|
||||||
|---|---|---|---|
|
|
||||||
| ROCm `v265-rocm-b11610` | ~1030 | ~49,8 | nur 35B geladen, `--fit-target 3072` |
|
|
||||||
| `unified-vulkan-265` | ~555 | ~43,2 | gleiche Bedingungen |
|
|
||||||
| ROCm im Betrieb (mit Autocomplete + Embeddings) | ~850 | ~47,5 | identisch mit und ohne Whisper auf der GPU |
|
|
||||||
|
|
||||||
Gemessen mit `llama-server` (Betriebs-Parameter), 2302 Prompt-Tokens, 256 generierte Tokens, `cache_prompt: false`.
|
|
||||||
|
|
||||||
| Bilder (Z-Image-Turbo Q8, 8 Schritte) | Dauer |
|
|
||||||
|---|---|
|
|
||||||
| 512×512 | ~11 s |
|
|
||||||
| 1024×1024 | ~40 s (Diffusion 32 s, VAE 6 s) |
|
|
||||||
| 1536×1024 | ~66 s |
|
|
||||||
| erster Request inkl. Laden | +4 s |
|
|
||||||
| Chat-Modell danach zurückladen | ~5 s |
|
|
||||||
| VRAM-Spitze beim Bild | 8,4 GB (mit Autocomplete + Whisper) |
|
|
||||||
|
|
||||||
## Offene Punkte
|
|
||||||
- Wo läuft die Laravel-App (gleiches LAN, NetBird, öffentlicher Server)? Davon hängen `ALLOWED_NETS` und HTTPS ab.
|
|
||||||
- Bildmodell: Reichen Qualität und Geschwindigkeit von Z-Image-Turbo, oder lieber FLUX.1-schnell?
|
|
||||||
- Später ggf. TTS (die Antwort vorlesen): audio.cpp ist im unified-Image, aber das Laravel SDK kann TTS nicht über `openai-compatible`.
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
# Plan: Lokaler LLM-Server für opencode per Docker Compose
|
|
||||||
|
|
||||||
> **Stand der Umsetzung (2026-10-14) – Abweichungen vom Plan:**
|
|
||||||
> - Externer Port ist **8800** statt 8000 (8000 ist lokal von VS Code belegt).
|
|
||||||
> - Kein manuelles `--n-cpu-moe`: llama.cpp `--fit` (Standard an) verteilt automatisch, `--fit-target 3072` reserviert VRAM fürs Autocomplete-Modell. `--no-mmap` existiert in diesem Build nicht mehr.
|
|
||||||
> - Autocomplete-Modell: Base-Variante `ggml-org/Qwen2.5-Coder-1.5B-Q8_0-GGUF` (besser für FIM als Instruct).
|
|
||||||
> - Image: `ghcr.io/mostlygeek/llama-swap:v265-rocm-b11610`, läuft als UID 1000.
|
|
||||||
> - Offen (braucht sudo / deine Entscheidung): ufw-Freigabe, Host-Ollama deaktivieren, alte Ollama-Modelle löschen, lokale opencode-Config ersetzen.
|
|
||||||
|
|
||||||
## Context
|
|
||||||
Ziel: Programmieren mit opencode **und dem VS-Code-Plugin Continue** ohne Cloud-KI. Diese Maschine (RX 6950 XT 16 GB, gfx1030; Ryzen 9 7900X3D; 62 GB RAM) stellt **zentral eine OpenAI-kompatible LLM-API** bereit. opencode läuft auf den Client-PCs (auch auf dieser Maschine) mit deren eigenem Code und nutzt diese API.
|
|
||||||
Anforderungen:
|
|
||||||
- Alles als `docker-compose` in `/home/alegab/workspace/localAI`, damit es portabel ist. Die Software auf dem Host (systemd-Ollama, llama-cpp-Paket) wird nicht genutzt.
|
|
||||||
- Zugriff nur von localhost, aus dem LAN (`br0` 192.168.178.0/24) und über NetBird (`wt0` 100.73.0.0/16), zusätzlich mit API-Key.
|
|
||||||
- Backend: llama.cpp mit ROCm (schnell bei MoE dank `--n-cpu-moe`). Die Modelle liegen als GGUF in `./models`.
|
|
||||||
|
|
||||||
Wichtig: ufw ist aktiv, aber Ports, die Docker veröffentlicht, **umgehen ufw**. Deshalb läuft der Proxy im Host-Netz (dort greift ufw) und filtert zusätzlich selbst nach Quell-IP. Das Backend ist nur auf 127.0.0.1 gebunden.
|
|
||||||
|
|
||||||
## Architektur
|
|
||||||
```
|
|
||||||
Client-PC (opencode) ──LAN / NetBird──┐
|
|
||||||
Lokales opencode ─────────────────────┤
|
|
||||||
▼
|
|
||||||
caddy (network_mode: host, :8000)
|
|
||||||
├─ IP-Filter: 127.0.0.0/8, ::1, 192.168.178.0/24, 100.73.0.0/16
|
|
||||||
├─ Bearer-Token-Prüfung (LLM_API_KEY)
|
|
||||||
└─ reverse_proxy → 127.0.0.1:8080
|
|
||||||
▼
|
|
||||||
llama-swap (ghcr.io/mostlygeek/llama-swap:rocm, nur 127.0.0.1:8080)
|
|
||||||
└─ startet/wechselt llama-server je nach "model"-Feld
|
|
||||||
/dev/kfd + /dev/dri → RX 6950 XT, Modelle in ./models
|
|
||||||
```
|
|
||||||
|
|
||||||
## Dateien (alle neu)
|
|
||||||
```
|
|
||||||
localAI/
|
|
||||||
├── docker-compose.yml
|
|
||||||
├── .env.example # Vorlage. .env wird lokal erzeugt (git-ignored)
|
|
||||||
├── .gitignore # .env, models/
|
|
||||||
├── llama-swap/config.yaml
|
|
||||||
├── caddy/Caddyfile
|
|
||||||
├── client/opencode.json # Vorlage für ~/.config/opencode/opencode.json der Clients
|
|
||||||
├── client/continue/config.yaml # Vorlage für ~/.continue/config.yaml
|
|
||||||
├── client/continue/.env.example # LOCALAI_API_KEY für Continue-Secrets
|
|
||||||
├── docs/opencode.md # Client-Einrichtung opencode (Linux/Windows/macOS)
|
|
||||||
├── docs/continue.md # Client-Einrichtung Continue in VS Code, Schritt für Schritt
|
|
||||||
├── scripts/firewall-ufw.sh # einmalige ufw-Freigaben für Port 8000 (LAN + wt0)
|
|
||||||
├── PLAN.md
|
|
||||||
└── README.md # Überblick, Server-Setup, Portieren, Links auf docs/
|
|
||||||
```
|
|
||||||
|
|
||||||
### docker-compose.yml (Kern)
|
|
||||||
- **llama-swap**
|
|
||||||
- Image: `ghcr.io/mostlygeek/llama-swap:rocm`, beim Umsetzen auf einen festen Tag pinnen. Das Image bringt llama-server mit ROCm mit.
|
|
||||||
- Ports: `127.0.0.1:8080:8080`.
|
|
||||||
- `devices: [/dev/kfd, /dev/dri]`, `group_add: ["${RENDER_GID}", "${VIDEO_GID}"]` (hier 987/983), `security_opt: [seccomp=unconfined]`, `ipc: host`.
|
|
||||||
- Umgebung: `HIP_VISIBLE_DEVICES=0` (iGPU ausblenden), `LLAMA_CACHE=/models`.
|
|
||||||
- Volumes: `./models:/models` und `./llama-swap/config.yaml:/app/config.yaml:ro`.
|
|
||||||
- `restart: unless-stopped`, Healthcheck auf `/health`.
|
|
||||||
- **caddy**
|
|
||||||
- Image: `caddy:2-alpine` (Tag pinnen), `network_mode: host`.
|
|
||||||
- Volume: `./caddy/Caddyfile`, Umgebung `LLM_API_KEY`, `ALLOWED_NETS`, `LISTEN_PORT`.
|
|
||||||
- `depends_on: llama-swap`.
|
|
||||||
|
|
||||||
### caddy/Caddyfile
|
|
||||||
```
|
|
||||||
{ admin off
|
|
||||||
auto_https off }
|
|
||||||
:{$LISTEN_PORT:8000} {
|
|
||||||
@blocked not remote_ip {$ALLOWED_NETS}
|
|
||||||
respond @blocked 403
|
|
||||||
@noauth not header Authorization "Bearer {$LLM_API_KEY}"
|
|
||||||
respond @noauth 401
|
|
||||||
reverse_proxy 127.0.0.1:8080 { flush_interval -1 }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
`ALLOWED_NETS` in `.env`: `127.0.0.0/8 ::1 192.168.178.0/24 100.73.0.0/16`. Auf einem anderen PC passt man nur diese Zeile an.
|
|
||||||
|
|
||||||
### llama-swap/config.yaml
|
|
||||||
Nur ein Modell gleichzeitig (16 GB VRAM). Die Modelle werden beim ersten Aufruf per `-hf` nach `/models` geladen. Repo- und Quant-Namen beim Umsetzen auf Hugging Face verifizieren.
|
|
||||||
```yaml
|
|
||||||
healthCheckTimeout: 900 # erster Start lädt ~20 GB
|
|
||||||
models:
|
|
||||||
qwen3.5-35b-a3b: # Hauptmodell (MoE, ~3B aktiv)
|
|
||||||
cmd: >
|
|
||||||
llama-server --port ${PORT} --host 127.0.0.1
|
|
||||||
-hf unsloth/Qwen3.5-35B-A3B-GGUF:Q4_K_M --jinja
|
|
||||||
-c 65536 -ngl 99 --n-cpu-moe 16 -fa on -ctk q8_0 -ctv q8_0
|
|
||||||
--threads 12 --no-mmap
|
|
||||||
ttl: 1800
|
|
||||||
glm-4.7-flash: # Alternative mit starkem Tool-Calling
|
|
||||||
cmd: > ...analog (unsloth/GLM-4.7-Flash-GGUF:Q4_K_M)
|
|
||||||
ttl: 1800
|
|
||||||
qwen3.5-9b: # klein, komplett im VRAM, schnell
|
|
||||||
cmd: > ...analog, -c 32768, ohne --n-cpu-moe
|
|
||||||
ttl: 1800
|
|
||||||
```
|
|
||||||
`--n-cpu-moe` wird beim Test eingestellt: so niedrig wie möglich, ohne dass der VRAM überläuft. Den Verbrauch dabei mit `rocm-smi` beobachten.
|
|
||||||
|
|
||||||
**Ergänzung für Continue:** Zwei weitere Modelle laufen dauerhaft neben dem jeweils aktiven Chat-Modell.
|
|
||||||
```yaml
|
|
||||||
qwen2.5-coder-1.5b: # Autocomplete (FIM), GPU, ~2 GB VRAM
|
|
||||||
cmd: llama-server ... -hf Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF:Q8_0 -c 8192 -ngl 99 -fa on
|
|
||||||
nomic-embed: # Embeddings für @codebase, auf der CPU
|
|
||||||
cmd: llama-server ... -hf nomic-ai/nomic-embed-text-v1.5-GGUF:Q8_0 --embedding --pooling mean -ngl 0 -c 8192
|
|
||||||
groups:
|
|
||||||
chat: { swap: true, exclusive: false, members: [qwen3.5-35b-a3b, glm-4.7-flash, qwen3.5-9b] }
|
|
||||||
always-on: { swap: false, exclusive: false, persistent: true, members: [qwen2.5-coder-1.5b, nomic-embed] }
|
|
||||||
```
|
|
||||||
Der VRAM-Bedarf des Autocomplete-Modells (~2 GB) wird beim Einstellen von `--n-cpu-moe` der Chat-Modelle eingeplant, also etwas höher ansetzen. Die Gruppen-Syntax beim Umsetzen gegen die llama-swap-Doku prüfen. Optional kommt ein Reranker hinzu, z. B. bge-reranker-v2-m3 mit `--reranking`.
|
|
||||||
|
|
||||||
### client/opencode.json (Vorlage)
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"$schema": "https://opencode.ai/config.json",
|
|
||||||
"model": "localai/qwen3.5-35b-a3b",
|
|
||||||
"small_model": "localai/qwen3.5-35b-a3b",
|
|
||||||
"share": "disabled",
|
|
||||||
"autoupdate": false,
|
|
||||||
"disabled_providers": ["opencode", "anthropic", "openai", "google", "ollama"],
|
|
||||||
"provider": {
|
|
||||||
"localai": {
|
|
||||||
"name": "LocalAI (alegab-pc)",
|
|
||||||
"npm": "@ai-sdk/openai-compatible",
|
|
||||||
"options": {
|
|
||||||
"baseURL": "http://{env:LOCALAI_HOST}:8000/v1",
|
|
||||||
"apiKey": "{env:LOCALAI_API_KEY}"
|
|
||||||
},
|
|
||||||
"models": {
|
|
||||||
"qwen3.5-35b-a3b": { "name": "Qwen3.5 35B-A3B", "limit": { "context": 65536, "output": 16384 } },
|
|
||||||
"glm-4.7-flash": { "name": "GLM-4.7 Flash", "limit": { "context": 65536, "output": 16384 } },
|
|
||||||
"qwen3.5-9b": { "name": "Qwen3.5 9B", "limit": { "context": 32768, "output": 8192 } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
`small_model` zeigt bewusst auf das Hauptmodell. Ein eigenes kleines Modell würde llama-swap bei jedem Titel- oder Zusammenfassungs-Aufruf zum Modellwechsel zwingen.
|
|
||||||
`LOCALAI_HOST` ist hier `127.0.0.1`, im LAN `192.168.178.72` und über NetBird `100.73.82.219` bzw. der NetBird-DNS-Name.
|
|
||||||
|
|
||||||
### client/continue/config.yaml (Vorlage)
|
|
||||||
```yaml
|
|
||||||
name: LocalAI
|
|
||||||
version: 1.0.0
|
|
||||||
schema: v1
|
|
||||||
models:
|
|
||||||
- name: Qwen3.5 35B-A3B (Chat/Agent)
|
|
||||||
provider: openai
|
|
||||||
model: qwen3.5-35b-a3b
|
|
||||||
apiBase: http://192.168.178.72:8000/v1 # bzw. NetBird-IP/-Name
|
|
||||||
apiKey: ${{ secrets.LOCALAI_API_KEY }}
|
|
||||||
roles: [chat, edit, apply]
|
|
||||||
capabilities: [tool_use]
|
|
||||||
defaultCompletionOptions: { contextLength: 65536, maxTokens: 8192 }
|
|
||||||
- name: GLM-4.7 Flash
|
|
||||||
provider: openai
|
|
||||||
model: glm-4.7-flash
|
|
||||||
... (gleiche apiBase/apiKey), roles: [chat, edit, apply], capabilities: [tool_use]
|
|
||||||
- name: Qwen2.5-Coder 1.5B (Autocomplete)
|
|
||||||
provider: openai
|
|
||||||
model: qwen2.5-coder-1.5b
|
|
||||||
...
|
|
||||||
roles: [autocomplete]
|
|
||||||
autocompleteOptions: { debounceDelay: 250, maxPromptTokens: 2048 }
|
|
||||||
- name: Nomic Embed
|
|
||||||
provider: openai
|
|
||||||
model: nomic-embed
|
|
||||||
...
|
|
||||||
roles: [embed]
|
|
||||||
```
|
|
||||||
- Das Secret steht in `~/.continue/.env` als `LOCALAI_API_KEY=...`.
|
|
||||||
- Beim Umsetzen anhand der Continue-Doku verifizieren: FIM über den OpenAI-Provider (ggf. `useLegacyCompletionsEndpoint: true` oder explizites `promptTemplates.autocomplete` im Qwen-FIM-Format), die Schlüssel für Telemetrie und Remote-Config sowie die passende Kontext-Länge.
|
|
||||||
- `docs/continue.md` enthält:
|
|
||||||
1. Extension installieren (Marketplace bzw. Open VSX für VSCodium).
|
|
||||||
2. Bei Continue *nicht* anmelden, Hub-Login überspringen.
|
|
||||||
3. `config.yaml` und `.env` kopieren.
|
|
||||||
4. Host-IP je nach Netz eintragen (LAN oder NetBird).
|
|
||||||
5. Telemetrie aus (VS-Code-Setting `continue.telemetryEnabled: false`).
|
|
||||||
6. Die Rollen erklären (Chat/Agent, Edit, Autocomplete, @codebase-Indexing).
|
|
||||||
7. Troubleshooting: 401 = Key falsch, 403 = falsches Netz, Timeout beim ersten Request = Modell lädt, Autocomplete leer = FIM-Template prüfen.
|
|
||||||
8. Fertige Varianten: nur LAN, nur NetBird, Windows-Pfade (`%USERPROFILE%\.continue`).
|
|
||||||
|
|
||||||
### scripts/firewall-ufw.sh
|
|
||||||
Einmalig mit sudo ausführen, nicht Teil von Compose:
|
|
||||||
`ufw allow from 192.168.178.0/24 to any port 8000 proto tcp` und `ufw allow in on wt0 to any port 8000 proto tcp`.
|
|
||||||
|
|
||||||
## Umsetzungsschritte
|
|
||||||
0. Diesen Plan als `/home/alegab/workspace/localAI/PLAN.md` ablegen.
|
|
||||||
1. Die Dateien oben anlegen. `.env` aus `.env.example` erzeugen, mit `LLM_API_KEY=$(openssl rand -hex 32)` und den GIDs aus `getent group render video`.
|
|
||||||
2. Den Host-Ollama-Dienst stoppen und deaktivieren (`sudo systemctl disable --now ollama`). Er belegt VRAM und lauscht auf 0.0.0.0. **Vorher bei dir nachfragen.**
|
|
||||||
3. `docker compose pull && docker compose up -d`, dann `scripts/firewall-ufw.sh` ausführen (sudo, bei dir nachfragen).
|
|
||||||
4. Den ersten Request schicken, damit der Modell-Download startet. `--n-cpu-moe` einstellen.
|
|
||||||
5. Die lokale `~/.config/opencode/opencode.json` sichern und durch die Client-Vorlage ersetzen.
|
|
||||||
6. Continue-Vorlage, `docs/continue.md`, `docs/opencode.md` und README schreiben. Continue in VS Code auf dieser Maschine mit der Vorlage testen.
|
|
||||||
7. Erst nachdem alles verifiziert ist und du zugestimmt hast: die alten Ollama-Modelle unter `/usr/share/ollama` löschen.
|
|
||||||
|
|
||||||
## Verifikation
|
|
||||||
1. `docker compose ps`: beide Dienste healthy. `docker compose logs llama-swap` zeigt beim Laden ROCm/gfx1030 und die Zahl der offloaded layers.
|
|
||||||
2. Auth: `curl 127.0.0.1:8000/v1/models` → 401. Mit `-H "Authorization: Bearer $KEY"` → Modellliste.
|
|
||||||
3. Bindung: `ss -tlnp` zeigt 8080 nur auf 127.0.0.1 und 8000 auf allen Interfaces.
|
|
||||||
4. Chat-Test: `curl .../v1/chat/completions` mit einem Tool-Definitions-Payload liefert einen `tool_calls`-Response. Dabei die Tokens/s im Log notieren.
|
|
||||||
5. Remote: von einem LAN-PC und über NetBird `curl http://<ip>:8000/v1/models` mit Key → 200. Ein Zugriff aus einem nicht freigegebenen Netz, z. B. aus einem Docker-Bridge-Container (`docker run --rm curlimages/curl http://172.17.0.1:8000/v1/models`), → 403.
|
|
||||||
6. opencode lokal: `/models` zeigt nur `localai/*`. Testauftrag „lege hello.py an und führe sie aus" prüft Read/Write/Bash-Tool-Calls.
|
|
||||||
7. Continue:
|
|
||||||
- Chat-Antwort kommt an.
|
|
||||||
- Agent-Modus führt einen Tool-Call aus.
|
|
||||||
- Ghost-Text-Autocomplete erscheint in unter ~1 s. Im llama-swap-Log stehen Requests an `qwen2.5-coder-1.5b`, ohne dass das Chat-Modell entladen wird.
|
|
||||||
- `@codebase` indexiert, im Log stehen Requests an `nomic-embed`.
|
|
||||||
8. Gegenprobe offline: Das Internet trennen (nach dem Modell-Download), opencode muss weiter funktionieren.
|
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
# LocalAI – zentraler LLM-Server für opencode, Continue & Laravel
|
# LocalAI – zentraler LLM-Server für opencode, Continue, Laravel & Web-Chat
|
||||||
|
|
||||||
Ein Docker-Compose-Stack, der auf **alegab-pc** (RX 6950 XT 16 GB, Ryzen 9 7900X3D, 62 GB RAM) eine **OpenAI-kompatible API** bereitstellt. Clients im Heim-LAN und über NetBird nutzen sie zum Programmieren, ganz ohne Cloud-KI.
|
Ein Docker-Compose-Stack, der auf **alegab-pc** (RX 6950 XT 16 GB, Ryzen 9 7900X3D, 62 GB RAM) eine **OpenAI-kompatible API** bereitstellt. Clients im Heim-LAN und über NetBird nutzen sie zum Programmieren, ganz ohne Cloud-KI.
|
||||||
|
|
||||||
```
|
```
|
||||||
Client-PC (opencode / VS Code + Continue) ──LAN / NetBird──┐
|
Client-PC (opencode / VS Code + Continue / Browser) ──LAN / NetBird──┐
|
||||||
▼
|
▼
|
||||||
caddy (Host-Netz, :8800) IP-Filter + API-Key
|
caddy (Host-Netz) :8800 API: IP-Filter + API-Key :8801 Web-Chat: IP-Filter
|
||||||
└─► llama-swap (nur 127.0.0.1:8080) lädt/wechselt Modelle
|
├─► open-webui (nur 127.0.0.1:3000) Web-Chat mit eigenem Login ──┐
|
||||||
|
└─► llama-swap (nur 127.0.0.1:8080) lädt/wechselt Modelle ◄────┘
|
||||||
├─► llama-server (llama.cpp, ROCm) Chat, Autocomplete, Embeddings
|
├─► llama-server (llama.cpp, ROCm) Chat, Autocomplete, Embeddings
|
||||||
├─► whisper-server (whisper.cpp, Vulkan) Transkription
|
├─► whisper-server (whisper.cpp, Vulkan) Transkription
|
||||||
└─► sd-server (stable-diffusion.cpp, Vulkan) Bilder
|
└─► sd-server (stable-diffusion.cpp, Vulkan) Bilder
|
||||||
@@ -31,11 +32,11 @@ cp .env.example .env
|
|||||||
sed -i "s/^LLM_API_KEY=.*/LLM_API_KEY=$(openssl rand -hex 32)/" .env
|
sed -i "s/^LLM_API_KEY=.*/LLM_API_KEY=$(openssl rand -hex 32)/" .env
|
||||||
chmod 600 .env
|
chmod 600 .env
|
||||||
$EDITOR .env # ALLOWED_NETS, GIDs (getent group render video), UID/GID prüfen
|
$EDITOR .env # ALLOWED_NETS, GIDs (getent group render video), UID/GID prüfen
|
||||||
sudo ./scripts/firewall-ufw.sh # einmalig: Port 8800 für LAN + wt0 in ufw öffnen
|
sudo ./scripts/firewall-ufw.sh # einmalig: Ports 8800 + 8801 für LAN + wt0 in ufw öffnen
|
||||||
./scripts/start.sh # prüfen, starten, Modelle vorladen, Verbindungsdaten zeigen
|
./scripts/start.sh # prüfen, starten, Modelle vorladen, Verbindungsdaten zeigen
|
||||||
```
|
```
|
||||||
|
|
||||||
`scripts/start.sh` legt eine fehlende `.env` beim ersten Start auch selbst an: mit neuem API-Key, UID/GID und den GIDs von render/video. Weitere Optionen:
|
`scripts/start.sh` legt eine fehlende `.env` beim ersten Start auch selbst an: mit neuem API-Key, WebUI-Schlüssel, UID/GID und den GIDs von render/video. Weitere Optionen:
|
||||||
|
|
||||||
| Aufruf | Wirkung |
|
| Aufruf | Wirkung |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -47,6 +48,16 @@ sudo ./scripts/firewall-ufw.sh # einmalig: Port 8800 für LAN + wt0 in ufw öf
|
|||||||
|
|
||||||
**Nach einem Reboot** startet der Stack von selbst: Der Docker-Dienst ist aktiviert, und die Container haben `restart: unless-stopped`. Autocomplete und Embeddings lädt llama-swap per `hooks.on_startup.preload` ebenfalls von selbst, das Chat-Modell kommt beim ersten Request dazu.
|
**Nach einem Reboot** startet der Stack von selbst: Der Docker-Dienst ist aktiviert, und die Container haben `restart: unless-stopped`. Autocomplete und Embeddings lädt llama-swap per `hooks.on_startup.preload` ebenfalls von selbst, das Chat-Modell kommt beim ersten Request dazu.
|
||||||
|
|
||||||
|
Gestoppt bzw. der VRAM freigegeben wird mit `scripts/stop.sh`. Laufen Modelle, entlädt das Skript sie vorher über llama-swap, damit die Server sauber beendet werden. Danach zeigt es Container-Status und VRAM-Belegung.
|
||||||
|
|
||||||
|
| Aufruf | Wirkung |
|
||||||
|
|---|---|
|
||||||
|
| `./scripts/stop.sh` | Modelle entladen, Container stoppen (bleiben auch nach einem Reboot aus) |
|
||||||
|
| `./scripts/stop.sh --unload` | nur alle Modelle entladen, Container laufen weiter |
|
||||||
|
| `./scripts/stop.sh --down` | Container stoppen und entfernen, Modelle und Daten bleiben erhalten |
|
||||||
|
|
||||||
|
Nach dem Stoppen (mit oder ohne `--down`) startet `./scripts/start.sh` den Stack wieder. Nach `--unload` lädt llama-swap Autocomplete und Embeddings beim nächsten Request von selbst neu.
|
||||||
|
|
||||||
Der erste Aufruf eines Modells lädt es von Hugging Face herunter (Hauptmodell ~22 GB). Danach läuft alles offline.
|
Der erste Aufruf eines Modells lädt es von Hugging Face herunter (Hauptmodell ~22 GB). Danach läuft alles offline.
|
||||||
|
|
||||||
Whisper und das Bildmodell lädt llama-swap **nicht** selbst herunter (~12 GB):
|
Whisper und das Bildmodell lädt llama-swap **nicht** selbst herunter (~12 GB):
|
||||||
@@ -62,12 +73,12 @@ curl -L -o models/sd/Qwen3-4B-Instruct-2507-Q8_0.gguf $HF/unsloth/Qwen3-4B-Instr
|
|||||||
### Warum ein eigenes Image?
|
### Warum ein eigenes Image?
|
||||||
`whisper-server` und `sd-server` gibt es nur im „unified“-Image von llama-swap, und das für AMD nur als Vulkan-Build. Unter Vulkan ist die Prompt-Verarbeitung von llama.cpp auf der RX 6950 XT aber nur halb so schnell wie unter ROCm (~555 statt ~1030 t/s). [`llama-swap/Dockerfile`](llama-swap/Dockerfile) nimmt deshalb das ROCm-Image als Basis und kopiert die beiden Server aus dem unified-Image dazu. `docker compose up -d --build` bzw. `scripts/start.sh` bauen es automatisch. Beide Tags stehen in `.env` (`LLAMA_SWAP_TAG`, `UNIFIED_TAG`). Bei einem Update beide auf dieselbe llama-swap-Version setzen und `./scripts/start.sh --pull` ausführen.
|
`whisper-server` und `sd-server` gibt es nur im „unified“-Image von llama-swap, und das für AMD nur als Vulkan-Build. Unter Vulkan ist die Prompt-Verarbeitung von llama.cpp auf der RX 6950 XT aber nur halb so schnell wie unter ROCm (~555 statt ~1030 t/s). [`llama-swap/Dockerfile`](llama-swap/Dockerfile) nimmt deshalb das ROCm-Image als Basis und kopiert die beiden Server aus dem unified-Image dazu. `docker compose up -d --build` bzw. `scripts/start.sh` bauen es automatisch. Beide Tags stehen in `.env` (`LLAMA_SWAP_TAG`, `UNIFIED_TAG`). Bei einem Update beide auf dieselbe llama-swap-Version setzen und `./scripts/start.sh --pull` ausführen.
|
||||||
|
|
||||||
Vulkan zählt die iGPU des Ryzen mit. `VK_VISIBLE_DEVICES=1` in `.env` wählt die RX 6950 XT.
|
Vulkan sieht auch die iGPU des Ryzen, und die Geräte-Reihenfolge wechselt zwischen Reboots. `VK_DEVICE=1002:73a5` in `.env` (PCI-ID der RX 6950 XT, `lspci -nn | grep -i vga`) lässt Mesa nur diese Karte anzeigen. Kontrolle: `curl -s 127.0.0.1:8080/logs/stream/whisper | grep ggml_vulkan` muss die RX 6950 XT zeigen.
|
||||||
|
|
||||||
### Warum Caddy im Host-Netz?
|
### Warum Caddy im Host-Netz?
|
||||||
Ports, die Docker veröffentlicht, **umgehen ufw**. Deshalb ist llama-swap nur an `127.0.0.1` gebunden. Caddy läuft mit `network_mode: host`, dort greift ufw, und Caddy sieht die echte Quell-IP. Daraus ergeben sich zwei Schutzschichten:
|
Ports, die Docker veröffentlicht, **umgehen ufw**. Deshalb ist llama-swap nur an `127.0.0.1` gebunden. Caddy läuft mit `network_mode: host`, dort greift ufw, und Caddy sieht die echte Quell-IP. Daraus ergeben sich zwei Schutzschichten:
|
||||||
1. **ufw** lässt Port 8800 nur aus dem LAN und über `wt0` zu.
|
1. **ufw** lässt die Ports 8800 (API) und 8801 (Web-Chat) nur aus dem LAN und über `wt0` zu.
|
||||||
2. **Caddy** prüft zusätzlich die Quell-IP (`ALLOWED_NETS`) und den Bearer-Token (`LLM_API_KEY`).
|
2. **Caddy** prüft zusätzlich die Quell-IP (`ALLOWED_NETS`), bei der API außerdem den Bearer-Token (`LLM_API_KEY`). Beim Web-Chat übernimmt der Login von Open WebUI diese Rolle.
|
||||||
|
|
||||||
Die API läuft über plain HTTP. Über NetBird ist das unkritisch (WireGuard verschlüsselt den Tunnel), im Heim-LAN ist es akzeptabel.
|
Die API läuft über plain HTTP. Über NetBird ist das unkritisch (WireGuard verschlüsselt den Tunnel), im Heim-LAN ist es akzeptabel.
|
||||||
|
|
||||||
@@ -76,6 +87,7 @@ Die API läuft über plain HTTP. Über NetBird ist das unkritisch (WireGuard ver
|
|||||||
- **opencode** (Terminal-Agent): [docs/opencode.md](docs/opencode.md)
|
- **opencode** (Terminal-Agent): [docs/opencode.md](docs/opencode.md)
|
||||||
- **Continue** (VS-Code-Plugin): [docs/continue.md](docs/continue.md)
|
- **Continue** (VS-Code-Plugin): [docs/continue.md](docs/continue.md)
|
||||||
- **Laravel** (laravel/ai – Agent, Spracheingabe, Bilder): [docs/laravel.md](docs/laravel.md)
|
- **Laravel** (laravel/ai – Agent, Spracheingabe, Bilder): [docs/laravel.md](docs/laravel.md)
|
||||||
|
- **Web-Chat** (Open WebUI im Browser, `http://<server>:8801`): [docs/webchat.md](docs/webchat.md)
|
||||||
|
|
||||||
Schnelltest von jedem Client:
|
Schnelltest von jedem Client:
|
||||||
```bash
|
```bash
|
||||||
@@ -88,6 +100,7 @@ curl -H "Authorization: Bearer <KEY>" http://<server>:8800/v1/models
|
|||||||
docker compose ps # Status
|
docker compose ps # Status
|
||||||
docker compose logs -f llama-swap # Modell-Laden, Requests
|
docker compose logs -f llama-swap # Modell-Laden, Requests
|
||||||
docker compose logs -f caddy # Zugriffe
|
docker compose logs -f caddy # Zugriffe
|
||||||
|
docker compose logs -f open-webui # Web-Chat
|
||||||
docker compose restart # nach Änderungen an .env / Caddyfile
|
docker compose restart # nach Änderungen an .env / Caddyfile
|
||||||
docker compose up -d --build # nach Änderungen an llama-swap/Dockerfile oder den Image-Tags
|
docker compose up -d --build # nach Änderungen an llama-swap/Dockerfile oder den Image-Tags
|
||||||
```
|
```
|
||||||
@@ -110,7 +123,7 @@ GPU-Layer und MoE-Auslagerung in den RAM verteilt llama.cpp automatisch (`--fit`
|
|||||||
|
|
||||||
1. Den Ordner kopieren. `models/` kann man mitnehmen, um den Download zu sparen.
|
1. Den Ordner kopieren. `models/` kann man mitnehmen, um den Download zu sparen.
|
||||||
2. `.env` anpassen: `ALLOWED_NETS` (LAN-Subnetz, NetBird-Netz), `RENDER_GID`/`VIDEO_GID`, `HOST_UID`/`HOST_GID`.
|
2. `.env` anpassen: `ALLOWED_NETS` (LAN-Subnetz, NetBird-Netz), `RENDER_GID`/`VIDEO_GID`, `HOST_UID`/`HOST_GID`.
|
||||||
3. Je nach GPU das Image in `.env` wählen (`LLAMA_SWAP_TAG`). Das eigene Image ([`llama-swap/Dockerfile`](llama-swap/Dockerfile)) ist für ROCm gebaut. Bei NVIDIA oder Vulkan nimmt man einfacher direkt das passende `unified-…`-Image (enthält whisper-/sd-server schon) und ersetzt `build:` in `docker-compose.yml` durch `image: ghcr.io/mostlygeek/llama-swap:unified-…`. `VK_VISIBLE_DEVICES` an die Geräte-Reihenfolge anpassen.
|
3. Je nach GPU das Image in `.env` wählen (`LLAMA_SWAP_TAG`). Das eigene Image ([`llama-swap/Dockerfile`](llama-swap/Dockerfile)) ist für ROCm gebaut. Bei NVIDIA oder Vulkan nimmt man einfacher direkt das passende `unified-…`-Image (enthält whisper-/sd-server schon) und ersetzt `build:` in `docker-compose.yml` durch `image: ghcr.io/mostlygeek/llama-swap:unified-…`. `VK_DEVICE` auf die PCI-ID der neuen GPU setzen.
|
||||||
|
|
||||||
| GPU | Tag-Variante |
|
| GPU | Tag-Variante |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -127,15 +140,15 @@ GPU-Layer und MoE-Auslagerung in den RAM verteilt llama.cpp automatisch (`--fit`
|
|||||||
|
|
||||||
| Datei | Inhalt |
|
| Datei | Inhalt |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `docker-compose.yml` | Dienste llama-swap + caddy |
|
| `docker-compose.yml` | Dienste llama-swap, open-webui, caddy |
|
||||||
| `llama-swap/Dockerfile` | ROCm-Image + whisper-server/sd-server |
|
| `llama-swap/Dockerfile` | ROCm-Image + whisper-server/sd-server |
|
||||||
| `.env` / `.env.example` | Secrets, Netze, Ports, GIDs, Image-Tags |
|
| `.env` / `.env.example` | Secrets, Netze, Ports, GIDs, Image-Tags |
|
||||||
| `llama-swap/config.yaml` | Modelle, Startparameter, Gruppen |
|
| `llama-swap/config.yaml` | Modelle, Startparameter, Gruppen |
|
||||||
| `caddy/Caddyfile` | IP-Filter, API-Key, Reverse Proxy |
|
| `caddy/Caddyfile` | IP-Filter, API-Key, Reverse Proxy (API + Web-Chat) |
|
||||||
|
| `docs/webchat.md` | Web-Chat (Open WebUI) |
|
||||||
| `client/opencode.json` | Vorlage für opencode-Clients |
|
| `client/opencode.json` | Vorlage für opencode-Clients |
|
||||||
| `client/continue/` | Vorlage für Continue-Clients |
|
| `client/continue/` | Vorlage für Continue-Clients |
|
||||||
| `docs/laravel.md` | Anbindung einer Laravel-App (laravel/ai) |
|
| `docs/laravel.md` | Anbindung einer Laravel-App (laravel/ai) |
|
||||||
| `scripts/start.sh` | Start mit Prüfungen, Preload und Statusausgabe |
|
| `scripts/start.sh` | Start mit Prüfungen, Preload und Statusausgabe |
|
||||||
|
| `scripts/stop.sh` | Stoppen bzw. Modelle entladen, mit Statusausgabe |
|
||||||
| `scripts/firewall-ufw.sh` | ufw-Freigaben |
|
| `scripts/firewall-ufw.sh` | ufw-Freigaben |
|
||||||
| `PLAN.md` | ursprünglicher Umsetzungsplan |
|
|
||||||
| `PLAN-multimodal.md` | Plan + Messwerte für Transkription/Bilder |
|
|
||||||
@@ -20,3 +20,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Web-Chat (Open WebUI): nur erlaubte Netze, Login übernimmt Open WebUI selbst
|
||||||
|
:{$WEBUI_PORT} {
|
||||||
|
@blocked not remote_ip {$ALLOWED_NETS}
|
||||||
|
respond @blocked "forbidden" 403
|
||||||
|
|
||||||
|
reverse_proxy 127.0.0.1:{$WEBUI_BACKEND_PORT} {
|
||||||
|
flush_interval -1
|
||||||
|
transport http {
|
||||||
|
read_timeout 30m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,7 +29,7 @@ models:
|
|||||||
roles: [chat, edit, apply, summarize]
|
roles: [chat, edit, apply, summarize]
|
||||||
capabilities: [tool_use]
|
capabilities: [tool_use]
|
||||||
defaultCompletionOptions:
|
defaultCompletionOptions:
|
||||||
contextLength: 65536
|
contextLength: 122880
|
||||||
maxTokens: 8192
|
maxTokens: 8192
|
||||||
|
|
||||||
- name: GLM-4.7 Flash
|
- name: GLM-4.7 Flash
|
||||||
@@ -38,7 +38,7 @@ models:
|
|||||||
roles: [chat, edit, apply]
|
roles: [chat, edit, apply]
|
||||||
capabilities: [tool_use]
|
capabilities: [tool_use]
|
||||||
defaultCompletionOptions:
|
defaultCompletionOptions:
|
||||||
contextLength: 65536
|
contextLength: 122880
|
||||||
maxTokens: 8192
|
maxTokens: 8192
|
||||||
|
|
||||||
- name: Qwen3.5 9B (schnell)
|
- name: Qwen3.5 9B (schnell)
|
||||||
@@ -47,7 +47,7 @@ models:
|
|||||||
roles: [chat, edit, apply]
|
roles: [chat, edit, apply]
|
||||||
capabilities: [tool_use]
|
capabilities: [tool_use]
|
||||||
defaultCompletionOptions:
|
defaultCompletionOptions:
|
||||||
contextLength: 32768
|
contextLength: 28672
|
||||||
maxTokens: 4096
|
maxTokens: 4096
|
||||||
|
|
||||||
# --- Tab-Autocomplete (Fill-in-the-Middle) ------------------------------
|
# --- Tab-Autocomplete (Fill-in-the-Middle) ------------------------------
|
||||||
|
|||||||
@@ -18,17 +18,17 @@
|
|||||||
"qwen3.5-35b-a3b": {
|
"qwen3.5-35b-a3b": {
|
||||||
"name": "Qwen3.5 35B-A3B",
|
"name": "Qwen3.5 35B-A3B",
|
||||||
"tool_call": true,
|
"tool_call": true,
|
||||||
"limit": { "context": 65536, "output": 16384 }
|
"limit": { "context": 122880, "output": 16384 }
|
||||||
},
|
},
|
||||||
"glm-4.7-flash": {
|
"glm-4.7-flash": {
|
||||||
"name": "GLM-4.7 Flash",
|
"name": "GLM-4.7 Flash",
|
||||||
"tool_call": true,
|
"tool_call": true,
|
||||||
"limit": { "context": 65536, "output": 16384 }
|
"limit": { "context": 122880, "output": 16384 }
|
||||||
},
|
},
|
||||||
"qwen3.5-9b": {
|
"qwen3.5-9b": {
|
||||||
"name": "Qwen3.5 9B (schnell)",
|
"name": "Qwen3.5 9B (schnell)",
|
||||||
"tool_call": true,
|
"tool_call": true,
|
||||||
"limit": { "context": 32768, "output": 8192 }
|
"limit": { "context": 28672, "output": 8192 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+55
-2
@@ -1,4 +1,5 @@
|
|||||||
# Lokaler LLM-Server: llama-swap (llama.cpp/ROCm + whisper/sd-server) hinter Caddy (IP-Filter + API-Key)
|
# Lokaler LLM-Server: llama-swap (llama.cpp/ROCm + whisper/sd-server) hinter Caddy (IP-Filter + API-Key)
|
||||||
|
# Web-Chat: Open WebUI, ebenfalls hinter Caddy (IP-Filter + eigener Login)
|
||||||
# Start: docker compose up -d --build Logs: docker compose logs -f llama-swap
|
# Start: docker compose up -d --build Logs: docker compose logs -f llama-swap
|
||||||
name: localai
|
name: localai
|
||||||
|
|
||||||
@@ -29,8 +30,10 @@ services:
|
|||||||
HOME: /tmp
|
HOME: /tmp
|
||||||
LLAMA_CACHE: /models
|
LLAMA_CACHE: /models
|
||||||
HIP_VISIBLE_DEVICES: "${HIP_VISIBLE_DEVICES:-0}"
|
HIP_VISIBLE_DEVICES: "${HIP_VISIBLE_DEVICES:-0}"
|
||||||
# Vulkan (sd-server) zählt die iGPU mit – hier ist die RX 6950 XT Gerät 1
|
# Vulkan (whisper-/sd-server) sieht auch die iGPU, und die Reihenfolge wechselt
|
||||||
GGML_VK_VISIBLE_DEVICES: "${VK_VISIBLE_DEVICES:-1}"
|
# zwischen Reboots. Mesa blendet deshalb alles außer der Karte mit dieser PCI-ID aus.
|
||||||
|
MESA_VK_DEVICE_SELECT: "${VK_DEVICE:?VK_DEVICE in .env setzen}"
|
||||||
|
MESA_VK_DEVICE_SELECT_FORCE_DEFAULT_DEVICE: "1"
|
||||||
HF_TOKEN: "${HF_TOKEN:-}"
|
HF_TOKEN: "${HF_TOKEN:-}"
|
||||||
volumes:
|
volumes:
|
||||||
- ./models:/models
|
- ./models:/models
|
||||||
@@ -51,7 +54,57 @@ services:
|
|||||||
ALLOWED_NETS: "${ALLOWED_NETS:-127.0.0.0/8 ::1}"
|
ALLOWED_NETS: "${ALLOWED_NETS:-127.0.0.0/8 ::1}"
|
||||||
LISTEN_PORT: "${LISTEN_PORT:-8800}"
|
LISTEN_PORT: "${LISTEN_PORT:-8800}"
|
||||||
BACKEND_PORT: "${BACKEND_PORT:-8080}"
|
BACKEND_PORT: "${BACKEND_PORT:-8080}"
|
||||||
|
WEBUI_PORT: "${WEBUI_PORT:-8801}"
|
||||||
|
WEBUI_BACKEND_PORT: "${WEBUI_BACKEND_PORT:-3000}"
|
||||||
volumes:
|
volumes:
|
||||||
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
|
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
- llama-swap
|
- llama-swap
|
||||||
|
- open-webui
|
||||||
|
|
||||||
|
# Web-Chat im Browser. Spricht intern direkt mit llama-swap (Docker-Netz, ohne Caddy).
|
||||||
|
# Die Werte unten gelten nur beim ersten Start – danach im Admin-Bereich ändern.
|
||||||
|
open-webui:
|
||||||
|
image: ghcr.io/open-webui/open-webui:${OPEN_WEBUI_TAG:-v0.11.4}
|
||||||
|
restart: unless-stopped
|
||||||
|
# nur lokal erreichbar – von außen ausschließlich über Caddy
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:${WEBUI_BACKEND_PORT:-3000}:8080"
|
||||||
|
environment:
|
||||||
|
WEBUI_SECRET_KEY: "${WEBUI_SECRET_KEY:?WEBUI_SECRET_KEY in .env setzen}"
|
||||||
|
# erster Account wird Admin, weitere müssen freigeschaltet werden
|
||||||
|
DEFAULT_USER_ROLE: pending
|
||||||
|
ENABLE_VERSION_UPDATE_CHECK: "false"
|
||||||
|
ENABLE_OLLAMA_API: "false"
|
||||||
|
OPENAI_API_BASE_URL: http://llama-swap:8080/v1
|
||||||
|
OPENAI_API_KEY: none
|
||||||
|
# nur eine Chat-Sitzung gleichzeitig (-np 1): keine Zusatz-Requests beim Tippen
|
||||||
|
ENABLE_AUTOCOMPLETE_GENERATION: "false"
|
||||||
|
ENABLE_FOLLOW_UP_GENERATION: "false"
|
||||||
|
ENABLE_EVALUATION_ARENA_MODELS: "false"
|
||||||
|
# Dokumente/RAG: Embeddings von nomic-embed statt eines lokalen Downloads
|
||||||
|
RAG_EMBEDDING_ENGINE: openai
|
||||||
|
RAG_EMBEDDING_MODEL: nomic-embed
|
||||||
|
RAG_OPENAI_API_BASE_URL: http://llama-swap:8080/v1
|
||||||
|
RAG_OPENAI_API_KEY: none
|
||||||
|
RAG_EMBEDDING_MODEL_AUTO_UPDATE: "false"
|
||||||
|
# Spracheingabe über whisper
|
||||||
|
AUDIO_STT_ENGINE: openai
|
||||||
|
AUDIO_STT_MODEL: whisper
|
||||||
|
AUDIO_STT_OPENAI_API_BASE_URL: http://llama-swap:8080/v1
|
||||||
|
AUDIO_STT_OPENAI_API_KEY: none
|
||||||
|
WHISPER_MODEL_AUTO_UPDATE: "false"
|
||||||
|
# Bilder über z-image (verdrängt das Chat-Modell, siehe config.yaml)
|
||||||
|
ENABLE_IMAGE_GENERATION: "true"
|
||||||
|
IMAGE_GENERATION_ENGINE: openai
|
||||||
|
IMAGE_GENERATION_MODEL: z-image
|
||||||
|
IMAGE_SIZE: 1024x1024
|
||||||
|
IMAGES_OPENAI_API_BASE_URL: http://llama-swap:8080/v1
|
||||||
|
IMAGES_OPENAI_API_KEY: none
|
||||||
|
volumes:
|
||||||
|
- open-webui:/app/backend/data
|
||||||
|
depends_on:
|
||||||
|
- llama-swap
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
open-webui:
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# Web-Chat (Open WebUI)
|
||||||
|
|
||||||
|
Chat im Browser, ohne Client-Installation. Open WebUI läuft im selben Stack und spricht intern direkt mit llama-swap. Nach außen liegt es hinter Caddy auf Port **8801** (`WEBUI_PORT`). Dort greifen derselbe IP-Filter (`ALLOWED_NETS`) und ufw wie bei der API. Einen API-Key braucht der Browser nicht, Open WebUI hat einen eigenen Login.
|
||||||
|
|
||||||
|
| Zugang | URL |
|
||||||
|
|---|---|
|
||||||
|
| lokal | http://127.0.0.1:8801 |
|
||||||
|
| LAN | http://192.168.178.72:8801 |
|
||||||
|
| NetBird | http://alegab-pc.vpn.local:8801 |
|
||||||
|
|
||||||
|
`./scripts/start.sh` zeigt die aktuellen Adressen an.
|
||||||
|
|
||||||
|
## Erster Start
|
||||||
|
|
||||||
|
1. Einmalig den Port in der Firewall öffnen: `sudo ./scripts/firewall-ufw.sh`
|
||||||
|
2. Die URL öffnen und **sofort einen Account anlegen**. Der erste Account wird Admin.
|
||||||
|
3. Weitere Accounts landen als „ausstehend“ und müssen unter *Admin-Bereich → Benutzer* freigeschaltet werden. Wer niemanden sonst zulassen will: *Admin-Bereich → Einstellungen → Allgemein → Registrierung* abschalten.
|
||||||
|
|
||||||
|
## Was vorkonfiguriert ist
|
||||||
|
|
||||||
|
| Funktion | Modell | Hinweis |
|
||||||
|
|---|---|---|
|
||||||
|
| Chat | `qwen3.5-35b-a3b`, `glm-4.7-flash`, `qwen3.5-9b` | Modellwechsel lädt das Modell in llama-swap um (dauert einige Sekunden) |
|
||||||
|
| Dokumente / RAG (Datei in den Chat ziehen) | `nomic-embed` | |
|
||||||
|
| Spracheingabe (Mikrofon) | `whisper` | nur über HTTPS oder `localhost`, siehe unten |
|
||||||
|
| Bilder (Bild-Knopf im Chat) | `z-image` | verdrängt das Chat-Modell; der nächste Chat lädt es neu |
|
||||||
|
|
||||||
|
Die Auswahlliste zeigt nur die Chat-Modelle. `qwen2.5-coder-1.5b`, `nomic-embed`, `whisper` und `z-image` sind in `llama-swap/config.yaml` als `unlisted: true` markiert, weil sie im Chat-Feld nur einen Fehler liefern würden. Bilder erzeugt man über den Bild-Knopf im Chat, nicht über die Modellauswahl.
|
||||||
|
|
||||||
|
Die Chat-Modelle laufen mit `-np 1`, also mit einer Sitzung gleichzeitig. Wenn opencode gerade arbeitet, wartet der Web-Chat (und umgekehrt). Titel-Generierung nutzt dasselbe Modell und hängt sich hinten an. Autocomplete- und Folgefragen-Vorschläge sind deshalb abgeschaltet.
|
||||||
|
|
||||||
|
## Einstellungen ändern
|
||||||
|
|
||||||
|
Die Umgebungsvariablen in `docker-compose.yml` gelten **nur beim allerersten Start**. Danach speichert Open WebUI alles in seiner Datenbank. Spätere Änderungen macht man im *Admin-Bereich → Einstellungen* (Verbindungen, Dokumente, Audio, Bilder).
|
||||||
|
|
||||||
|
Die Daten (Accounts, Chats, Dokumente) liegen im Docker-Volume `localai_open-webui`.
|
||||||
|
- Sichern: `docker run --rm -v localai_open-webui:/d -v "$PWD":/b alpine tar czf /b/open-webui-backup.tgz -C /d .`
|
||||||
|
- Komplett zurücksetzen: `docker compose down && docker volume rm localai_open-webui`
|
||||||
|
|
||||||
|
## Spracheingabe von anderen Rechnern
|
||||||
|
|
||||||
|
Browser geben das Mikrofon nur auf HTTPS-Seiten oder `localhost` frei. Über `http://<server>:8801` bleibt der Mikrofon-Knopf daher wirkungslos. Für Chrome/Chromium gibt es eine Ausnahme pro Adresse:
|
||||||
|
`chrome://flags/#unsafely-treat-insecure-origin-as-secure` → `http://alegab-pc.vpn.local:8801` eintragen, neu starten.
|
||||||
|
|
||||||
|
Dateien hochladen (Audio-Datei transkribieren) funktioniert auch ohne HTTPS.
|
||||||
|
|
||||||
|
## Update
|
||||||
|
|
||||||
|
`OPEN_WEBUI_TAG` in `.env` anheben (Releases: https://github.com/open-webui/open-webui/releases), dann `docker compose up -d`. Open WebUI migriert die Datenbank selbst. Vorher sichern.
|
||||||
+12
-3
@@ -12,6 +12,10 @@
|
|||||||
# - Embeddings laufen auf der CPU
|
# - Embeddings laufen auf der CPU
|
||||||
# - das Bildmodell (z-image) ist in der chat-Gruppe: es verdrängt das Chat-Modell
|
# - das Bildmodell (z-image) ist in der chat-Gruppe: es verdrängt das Chat-Modell
|
||||||
#
|
#
|
||||||
|
# Hilfsmodelle (Autocomplete, Embeddings, Whisper, Bilder) sind "unlisted": Sie fehlen in
|
||||||
|
# /v1/models, damit man sie im Web-Chat nicht als Chat-Modell wählt. Per ID bleiben
|
||||||
|
# sie normal nutzbar.
|
||||||
|
#
|
||||||
# whisper-server und sd-server kommen aus dem unified-Image (llama-swap/Dockerfile).
|
# whisper-server und sd-server kommen aus dem unified-Image (llama-swap/Dockerfile).
|
||||||
# Ihre Modelle liegen fest in ./models/whisper und ./models/sd (kein -hf), siehe README.
|
# Ihre Modelle liegen fest in ./models/whisper und ./models/sd (kein -hf), siehe README.
|
||||||
|
|
||||||
@@ -23,8 +27,9 @@ macros:
|
|||||||
server: >-
|
server: >-
|
||||||
/app/llama-server --host 127.0.0.1 --port ${PORT}
|
/app/llama-server --host 127.0.0.1 --port ${PORT}
|
||||||
--jinja -fa on --threads 12 --no-webui
|
--jinja -fa on --threads 12 --no-webui
|
||||||
# Agent-Betrieb: großer Kontext, KV-Cache quantisiert, VRAM-Reserve für Autocomplete
|
# Agent-Betrieb: großer Kontext, KV-Cache quantisiert, VRAM-Reserve für Autocomplete;
|
||||||
agent-ctx: -c 65536 -ctk q8_0 -ctv q8_0 --fit-target 3072
|
# -np 1 = eine Sitzung gleichzeitig, sie bekommt den ganzen Kontext (sonst teilen sich 4 Slots)
|
||||||
|
agent-ctx: -c 131072 -np 1 -ctk q8_0 -ctv q8_0 --fit-target 3072
|
||||||
|
|
||||||
models:
|
models:
|
||||||
# --- Chat / Agent (opencode, Continue Chat/Agent/Edit) -----------------
|
# --- Chat / Agent (opencode, Continue Chat/Agent/Edit) -----------------
|
||||||
@@ -45,7 +50,7 @@ models:
|
|||||||
qwen3.5-9b:
|
qwen3.5-9b:
|
||||||
name: Qwen3.5 9B (schnell, komplett im VRAM)
|
name: Qwen3.5 9B (schnell, komplett im VRAM)
|
||||||
cmd: >-
|
cmd: >-
|
||||||
${server} -c 32768 -ctk q8_0 -ctv q8_0 --fit-target 3072
|
${server} -c 32768 -np 1 -ctk q8_0 -ctv q8_0 --fit-target 3072
|
||||||
-hf unsloth/Qwen3.5-9B-GGUF:Q4_K_M
|
-hf unsloth/Qwen3.5-9B-GGUF:Q4_K_M
|
||||||
ttl: 3600
|
ttl: 3600
|
||||||
|
|
||||||
@@ -57,6 +62,7 @@ models:
|
|||||||
-hf ggml-org/Qwen2.5-Coder-1.5B-Q8_0-GGUF
|
-hf ggml-org/Qwen2.5-Coder-1.5B-Q8_0-GGUF
|
||||||
-ngl 99
|
-ngl 99
|
||||||
ttl: 0
|
ttl: 0
|
||||||
|
unlisted: true
|
||||||
|
|
||||||
# --- Embeddings (Continue @codebase) -----------------------------------
|
# --- Embeddings (Continue @codebase) -----------------------------------
|
||||||
nomic-embed:
|
nomic-embed:
|
||||||
@@ -66,6 +72,7 @@ models:
|
|||||||
-hf nomic-ai/nomic-embed-text-v1.5-GGUF:Q8_0
|
-hf nomic-ai/nomic-embed-text-v1.5-GGUF:Q8_0
|
||||||
--embedding --pooling mean -c 8192 -b 8192 -ub 8192 -ngl 0 --threads 8
|
--embedding --pooling mean -c 8192 -b 8192 -ub 8192 -ngl 0 --threads 8
|
||||||
ttl: 0
|
ttl: 0
|
||||||
|
unlisted: true
|
||||||
|
|
||||||
# --- Transkription (/v1/audio/transcriptions) --------------------------
|
# --- Transkription (/v1/audio/transcriptions) --------------------------
|
||||||
whisper:
|
whisper:
|
||||||
@@ -80,6 +87,7 @@ models:
|
|||||||
-t 8 -l auto
|
-t 8 -l auto
|
||||||
aliases: [whisper-1]
|
aliases: [whisper-1]
|
||||||
ttl: 0
|
ttl: 0
|
||||||
|
unlisted: true
|
||||||
|
|
||||||
# --- Bilder (/v1/images/generations), sd-server über Vulkan ------------
|
# --- Bilder (/v1/images/generations), sd-server über Vulkan ------------
|
||||||
z-image:
|
z-image:
|
||||||
@@ -96,6 +104,7 @@ models:
|
|||||||
# Der OpenAI-Treiber (z. B. Laravel AI SDK) schickt seine eigenen Modellnamen
|
# Der OpenAI-Treiber (z. B. Laravel AI SDK) schickt seine eigenen Modellnamen
|
||||||
aliases: [gpt-image-1, dall-e-3, dall-e-2]
|
aliases: [gpt-image-1, dall-e-3, dall-e-2]
|
||||||
ttl: 600
|
ttl: 600
|
||||||
|
unlisted: true
|
||||||
|
|
||||||
# Helfer-Modelle beim Container-Start laden (auch nach Reboot), damit
|
# Helfer-Modelle beim Container-Start laden (auch nach Reboot), damit
|
||||||
# Autocomplete sofort reagiert. Das Chat-Modell lädt beim ersten Request
|
# Autocomplete sofort reagiert. Das Chat-Modell lädt beim ersten Request
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Einmalig ausführen: sudo ./scripts/firewall-ufw.sh
|
# Einmalig ausführen: sudo ./scripts/firewall-ufw.sh
|
||||||
# Öffnet den Caddy-Port nur für LAN und NetBird (wt0). Caddy filtert zusätzlich selbst.
|
# Öffnet die Caddy-Ports (API + Web-Chat) nur für LAN und NetBird (wt0). Caddy filtert zusätzlich selbst.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
PORT="${LISTEN_PORT:-8800}"
|
PORT="${LISTEN_PORT:-8800}"
|
||||||
|
WEBUI_PORT="${WEBUI_PORT:-8801}"
|
||||||
LAN="${LAN_NET:-192.168.178.0/24}"
|
LAN="${LAN_NET:-192.168.178.0/24}"
|
||||||
VPN_IF="${VPN_IF:-wt0}"
|
VPN_IF="${VPN_IF:-wt0}"
|
||||||
|
|
||||||
ufw allow from "$LAN" to any port "$PORT" proto tcp comment 'localai LAN'
|
ufw allow from "$LAN" to any port "$PORT" proto tcp comment 'localai LAN'
|
||||||
ufw allow in on "$VPN_IF" to any port "$PORT" proto tcp comment 'localai NetBird'
|
ufw allow in on "$VPN_IF" to any port "$PORT" proto tcp comment 'localai NetBird'
|
||||||
ufw status numbered | grep -E "$PORT" || true
|
ufw allow from "$LAN" to any port "$WEBUI_PORT" proto tcp comment 'localai webchat LAN'
|
||||||
|
ufw allow in on "$VPN_IF" to any port "$WEBUI_PORT" proto tcp comment 'localai webchat NetBird'
|
||||||
|
ufw status numbered | grep -E "$PORT|$WEBUI_PORT" || true
|
||||||
+35
-9
@@ -47,6 +47,7 @@ if [[ ! -f .env ]]; then
|
|||||||
warn ".env fehlt – wird aus .env.example erzeugt"
|
warn ".env fehlt – wird aus .env.example erzeugt"
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
sed -i "s/^LLM_API_KEY=.*/LLM_API_KEY=$(openssl rand -hex 32)/" .env
|
sed -i "s/^LLM_API_KEY=.*/LLM_API_KEY=$(openssl rand -hex 32)/" .env
|
||||||
|
sed -i "s/^WEBUI_SECRET_KEY=.*/WEBUI_SECRET_KEY=$(openssl rand -hex 32)/" .env
|
||||||
sed -i "s/^HOST_UID=.*/HOST_UID=$(id -u)/; s/^HOST_GID=.*/HOST_GID=$(id -g)/" .env
|
sed -i "s/^HOST_UID=.*/HOST_UID=$(id -u)/; s/^HOST_GID=.*/HOST_GID=$(id -g)/" .env
|
||||||
rg=$(getent group render | cut -d: -f3 || true); vg=$(getent group video | cut -d: -f3 || true)
|
rg=$(getent group render | cut -d: -f3 || true); vg=$(getent group video | cut -d: -f3 || true)
|
||||||
[[ -n "$rg" ]] && sed -i "s/^RENDER_GID=.*/RENDER_GID=$rg/" .env
|
[[ -n "$rg" ]] && sed -i "s/^RENDER_GID=.*/RENDER_GID=$rg/" .env
|
||||||
@@ -55,21 +56,33 @@ if [[ ! -f .env ]]; then
|
|||||||
warn "Bitte ALLOWED_NETS in .env prüfen (LAN-/NetBird-Netz)"
|
warn "Bitte ALLOWED_NETS in .env prüfen (LAN-/NetBird-Netz)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ältere .env ohne Web-Chat-Einträge ergänzen
|
||||||
|
if ! grep -q '^WEBUI_SECRET_KEY=' .env; then
|
||||||
|
printf '\nWEBUI_PORT=8801\nWEBUI_BACKEND_PORT=3000\nWEBUI_SECRET_KEY=%s\n' "$(openssl rand -hex 32)" >> .env
|
||||||
|
warn "Web-Chat-Einträge (WEBUI_*) in .env ergänzt"
|
||||||
|
fi
|
||||||
|
|
||||||
set -a; . ./.env; set +a
|
set -a; . ./.env; set +a
|
||||||
LISTEN_PORT="${LISTEN_PORT:-8800}"
|
LISTEN_PORT="${LISTEN_PORT:-8800}"
|
||||||
BACKEND_PORT="${BACKEND_PORT:-8080}"
|
BACKEND_PORT="${BACKEND_PORT:-8080}"
|
||||||
|
WEBUI_PORT="${WEBUI_PORT:-8801}"
|
||||||
|
WEBUI_BACKEND_PORT="${WEBUI_BACKEND_PORT:-3000}"
|
||||||
[[ -n "${LLM_API_KEY:-}" && "$LLM_API_KEY" != "CHANGE_ME" ]] || die "LLM_API_KEY in .env setzen (openssl rand -hex 32)"
|
[[ -n "${LLM_API_KEY:-}" && "$LLM_API_KEY" != "CHANGE_ME" ]] || die "LLM_API_KEY in .env setzen (openssl rand -hex 32)"
|
||||||
|
[[ -n "${WEBUI_SECRET_KEY:-}" && "$WEBUI_SECRET_KEY" != "CHANGE_ME" ]] || die "WEBUI_SECRET_KEY in .env setzen (openssl rand -hex 32)"
|
||||||
ok ".env geladen"
|
ok ".env geladen"
|
||||||
|
if command -v lspci >/dev/null && [[ "$(lspci -nn)" != *"[${VK_DEVICE:-}]"* ]]; then
|
||||||
|
warn "VK_DEVICE=${VK_DEVICE:-} passt zu keiner GPU – whisper/sd-server laufen dann auf der CPU/iGPU (lspci -nn | grep -i vga)"
|
||||||
|
fi
|
||||||
|
|
||||||
# Ports dürfen nur von unserem eigenen Stack belegt sein
|
# Ports dürfen nur von unserem eigenen Stack belegt sein
|
||||||
running=$(docker compose ps --status running -q 2>/dev/null | wc -l)
|
running=$(docker compose ps --status running -q 2>/dev/null | wc -l)
|
||||||
if [[ "$running" -eq 0 ]]; then
|
if [[ "$running" -eq 0 ]]; then
|
||||||
for p in "$LISTEN_PORT" "$BACKEND_PORT"; do
|
for p in "$LISTEN_PORT" "$BACKEND_PORT" "$WEBUI_PORT" "$WEBUI_BACKEND_PORT"; do
|
||||||
if ss -Htln "sport = :$p" | grep -q .; then
|
if ss -Htln "sport = :$p" | grep -q .; then
|
||||||
die "Port $p ist bereits belegt: $(ss -Htlnp "sport = :$p" | awk '{print $NF}' | head -1) – in .env ändern"
|
die "Port $p ist bereits belegt: $(ss -Htlnp "sport = :$p" | awk '{print $NF}' | head -1) – in .env ändern"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
ok "Ports $LISTEN_PORT und $BACKEND_PORT frei"
|
ok "Ports $LISTEN_PORT, $BACKEND_PORT, $WEBUI_PORT und $WEBUI_BACKEND_PORT frei"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if systemctl is-active --quiet ollama 2>/dev/null; then
|
if systemctl is-active --quiet ollama 2>/dev/null; then
|
||||||
@@ -77,9 +90,11 @@ if systemctl is-active --quiet ollama 2>/dev/null; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if command -v ufw >/dev/null && systemctl is-active --quiet ufw 2>/dev/null; then
|
if command -v ufw >/dev/null && systemctl is-active --quiet ufw 2>/dev/null; then
|
||||||
if [[ -r /etc/ufw/user.rules ]] && ! grep -q "dport $LISTEN_PORT" /etc/ufw/user.rules; then
|
for p in "$LISTEN_PORT" "$WEBUI_PORT"; do
|
||||||
warn "Keine ufw-Freigabe für Port $LISTEN_PORT gefunden → sudo ./scripts/firewall-ufw.sh"
|
if [[ -r /etc/ufw/user.rules ]] && ! grep -q "dport $p" /etc/ufw/user.rules; then
|
||||||
|
warn "Keine ufw-Freigabe für Port $p gefunden → sudo ./scripts/firewall-ufw.sh"
|
||||||
fi
|
fi
|
||||||
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p models
|
mkdir -p models
|
||||||
@@ -113,6 +128,14 @@ code=$(curl -s -o /dev/null -m 5 -w '%{http_code}' -H "Authorization: Bearer $LL
|
|||||||
[[ "$code" == "200" ]] || die "API mit Key liefert HTTP $code"
|
[[ "$code" == "200" ]] || die "API mit Key liefert HTTP $code"
|
||||||
ok "Caddy bereit (Auth aktiv)"
|
ok "Caddy bereit (Auth aktiv)"
|
||||||
|
|
||||||
|
# Open WebUI braucht beim ersten Start etwas länger (Datenbank anlegen)
|
||||||
|
for i in $(seq 1 120); do
|
||||||
|
curl -fsS -m 2 "http://127.0.0.1:$WEBUI_PORT/health" >/dev/null 2>&1 && break
|
||||||
|
(( i == 120 )) && die "Web-Chat antwortet nicht – docker compose logs open-webui"
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
ok "Web-Chat bereit"
|
||||||
|
|
||||||
# ---------------------------------------------------------------- Preload
|
# ---------------------------------------------------------------- Preload
|
||||||
if (( PRELOAD )); then
|
if (( PRELOAD )); then
|
||||||
step "Lade Modelle vor (erster Start lädt ggf. von Hugging Face herunter)"
|
step "Lade Modelle vor (erster Start lädt ggf. von Hugging Face herunter)"
|
||||||
@@ -149,14 +172,17 @@ step "Verbindung"
|
|||||||
lan_ip=$(ip -4 -o addr show scope global | awk '$2 !~ /^(wt|docker|virbr|br-|veth)/ {split($4,a,"/"); print a[1]; exit}')
|
lan_ip=$(ip -4 -o addr show scope global | awk '$2 !~ /^(wt|docker|virbr|br-|veth)/ {split($4,a,"/"); print a[1]; exit}')
|
||||||
nb_ip=$(ip -4 -o addr show wt0 2>/dev/null | awk '{split($4,a,"/"); print a[1]}')
|
nb_ip=$(ip -4 -o addr show wt0 2>/dev/null | awk '{split($4,a,"/"); print a[1]}')
|
||||||
nb_fqdn=$(netbird status 2>/dev/null | awk '/^FQDN:/{print $2}' || true)
|
nb_fqdn=$(netbird status 2>/dev/null | awk '/^FQDN:/{print $2}' || true)
|
||||||
echo " lokal: http://127.0.0.1:$LISTEN_PORT/v1"
|
echo " API lokal: http://127.0.0.1:$LISTEN_PORT/v1"
|
||||||
[[ -n "$lan_ip" ]] && echo " LAN: http://$lan_ip:$LISTEN_PORT/v1"
|
[[ -n "$lan_ip" ]] && echo " API LAN: http://$lan_ip:$LISTEN_PORT/v1"
|
||||||
[[ -n "$nb_ip" ]] && echo " NetBird: http://$nb_ip:$LISTEN_PORT/v1${nb_fqdn:+ (http://$nb_fqdn:$LISTEN_PORT/v1)}"
|
[[ -n "$nb_ip" ]] && echo " API NetBird: http://$nb_ip:$LISTEN_PORT/v1${nb_fqdn:+ (http://$nb_fqdn:$LISTEN_PORT/v1)}"
|
||||||
|
echo " Chat lokal: http://127.0.0.1:$WEBUI_PORT"
|
||||||
|
[[ -n "$lan_ip" ]] && echo " Chat LAN: http://$lan_ip:$WEBUI_PORT"
|
||||||
|
[[ -n "$nb_ip" ]] && echo " Chat NetBird: http://$nb_ip:$WEBUI_PORT${nb_fqdn:+ (http://$nb_fqdn:$WEBUI_PORT)}"
|
||||||
if (( SHOW_KEY )); then
|
if (( SHOW_KEY )); then
|
||||||
echo " API-Key: $LLM_API_KEY"
|
echo " API-Key: $LLM_API_KEY"
|
||||||
else
|
else
|
||||||
echo " API-Key: ${LLM_API_KEY:0:6}… (vollständig: --show-key)"
|
echo " API-Key: ${LLM_API_KEY:0:6}… (vollständig: --show-key)"
|
||||||
fi
|
fi
|
||||||
echo " Web-UI: http://127.0.0.1:$BACKEND_PORT/ui (nur lokal)"
|
echo " llama-swap: http://127.0.0.1:$BACKEND_PORT/ui (nur lokal)"
|
||||||
echo
|
echo
|
||||||
echo " Clients einrichten: docs/opencode.md, docs/continue.md"
|
echo " Clients einrichten: docs/opencode.md, docs/continue.md, docs/webchat.md"
|
||||||
Executable
+75
@@ -0,0 +1,75 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Stoppt den LocalAI-Stack bzw. gibt den VRAM frei.
|
||||||
|
#
|
||||||
|
# ./scripts/stop.sh Container stoppen (bleiben auch nach Reboot aus)
|
||||||
|
# ./scripts/stop.sh --unload nur alle Modelle entladen, Container laufen weiter
|
||||||
|
# ./scripts/stop.sh --down Container stoppen und entfernen (Modelle/Daten bleiben)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$(readlink -f "$0")")/.."
|
||||||
|
|
||||||
|
MODE=stop
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--unload) MODE=unload ;;
|
||||||
|
--down) MODE=down ;;
|
||||||
|
-h|--help) sed -n '2,6p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||||
|
*) echo "Unbekannte Option: $1" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -t 1 ]]; then B=$'\e[1m'; G=$'\e[32m'; Y=$'\e[33m'; R=$'\e[31m'; N=$'\e[0m'; else B= G= Y= R= N=; fi
|
||||||
|
ok() { echo "${G}✔${N} $*"; }
|
||||||
|
warn() { echo "${Y}!${N} $*"; }
|
||||||
|
die() { echo "${R}✘${N} $*" >&2; exit 1; }
|
||||||
|
step() { echo; echo "${B}== $*${N}"; }
|
||||||
|
|
||||||
|
command -v docker >/dev/null || die "docker nicht installiert"
|
||||||
|
docker info >/dev/null 2>&1 || die "Docker-Daemon läuft nicht oder keine Rechte (Gruppe 'docker'?)"
|
||||||
|
|
||||||
|
BACKEND_PORT=8080
|
||||||
|
[[ -f .env ]] && BACKEND_PORT=$(set -a; . ./.env; echo "${BACKEND_PORT:-8080}")
|
||||||
|
|
||||||
|
running=$(docker compose ps --status running -q 2>/dev/null | wc -l)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- Entladen
|
||||||
|
# vor dem Stoppen entladen, damit llama-swap die Server sauber beendet
|
||||||
|
if [[ "$running" -gt 0 ]] && curl -fsS -m 2 "http://127.0.0.1:$BACKEND_PORT/health" >/dev/null 2>&1; then
|
||||||
|
step "Entlade Modelle"
|
||||||
|
loaded=$(curl -s -m 5 "http://127.0.0.1:$BACKEND_PORT/running" \
|
||||||
|
| python3 -c 'import sys,json; print(", ".join(m["model"] for m in json.load(sys.stdin)["running"]) or "-")' 2>/dev/null || echo "?")
|
||||||
|
echo " geladen: $loaded"
|
||||||
|
if curl -fsS -m 60 -o /dev/null "http://127.0.0.1:$BACKEND_PORT/unload"; then
|
||||||
|
ok "Modelle entladen"
|
||||||
|
else
|
||||||
|
warn "Entladen über llama-swap fehlgeschlagen"
|
||||||
|
fi
|
||||||
|
elif [[ "$MODE" == unload ]]; then
|
||||||
|
die "llama-swap läuft nicht – nichts zu entladen"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------------------- Stoppen
|
||||||
|
case "$MODE" in
|
||||||
|
stop)
|
||||||
|
step "Stoppe Container"
|
||||||
|
docker compose stop
|
||||||
|
ok "Stack gestoppt – Neustart mit ./scripts/start.sh"
|
||||||
|
;;
|
||||||
|
down)
|
||||||
|
step "Stoppe und entferne Container"
|
||||||
|
docker compose down --remove-orphans
|
||||||
|
ok "Stack entfernt – Neustart mit ./scripts/start.sh"
|
||||||
|
;;
|
||||||
|
unload)
|
||||||
|
warn "Container laufen weiter – Autocomplete/Embeddings werden beim nächsten Request neu geladen"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ Status
|
||||||
|
step "Status"
|
||||||
|
docker compose ps -a --format '{{.Service}}: {{.Status}}' | sed 's/^/ /'
|
||||||
|
[[ "$(docker compose ps -a -q | wc -l)" -eq 0 ]] && echo " keine Container"
|
||||||
|
command -v rocm-smi >/dev/null && rocm-smi --showmeminfo vram 2>/dev/null \
|
||||||
|
| awk '/GPU\[0\].*Total Memory/{t=$NF} /GPU\[0\].*Used Memory/{u=$NF} END{if(t) printf " VRAM: %.1f / %.1f GB\n", u/1e9, t/1e9}'
|
||||||
|
true
|
||||||
Reference in new issue
Block a user