From a465821bb8c9691f9e4a1c0320d703827489b1bf Mon Sep 17 00:00:00 2001 From: Alexander Gabriel Date: Fri, 16 Oct 2026 22:45:26 +0200 Subject: [PATCH] =?UTF-8?q?pl=C3=A4ne=20gel=C3=B6scht?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PLAN-multimodal.md | 227 --------------------------------------------- PLAN.md | 209 ----------------------------------------- 2 files changed, 436 deletions(-) delete mode 100644 PLAN-multimodal.md delete mode 100644 PLAN.md diff --git a/PLAN-multimodal.md b/PLAN-multimodal.md deleted file mode 100644 index 546dc05..0000000 --- a/PLAN-multimodal.md +++ /dev/null @@ -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/ -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://: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`. diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index 97abc00..0000000 --- a/PLAN.md +++ /dev/null @@ -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://: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.