Added Plan
This commit is contained in:
commit
b10435f5f7
202
PLAN.md
Normal file
202
PLAN.md
Normal file
@ -0,0 +1,202 @@
|
||||
# Plan: Lokaler LLM-Server für opencode per Docker Compose
|
||||
|
||||
## 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.
|
||||
Loading…
Reference in New Issue
Block a user