pläne gelöscht

This commit is contained in:
Alexander Gabriel 2026-10-16 22:45:26 +02:00
parent 2e2490c1bc
commit a465821bb8
2 changed files with 0 additions and 436 deletions

View File

@ -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`.

209
PLAN.md
View File

@ -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.