first try
This commit is contained in:
parent
b10435f5f7
commit
99517f483a
30
.env.example
Normal file
30
.env.example
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Kopieren nach .env und anpassen: cp .env.example .env
|
||||||
|
# API-Key erzeugen: openssl rand -hex 32
|
||||||
|
|
||||||
|
LLM_API_KEY=CHANGE_ME
|
||||||
|
|
||||||
|
# Netze, die auf die API zugreifen dürfen (Leerzeichen-getrennt)
|
||||||
|
# localhost | LAN | NetBird
|
||||||
|
ALLOWED_NETS="127.0.0.0/8 ::1 192.168.178.0/24 100.73.0.0/16"
|
||||||
|
|
||||||
|
# Port, auf dem Caddy lauscht (extern) und llama-swap (nur 127.0.0.1)
|
||||||
|
LISTEN_PORT=8800
|
||||||
|
BACKEND_PORT=8080
|
||||||
|
|
||||||
|
# Host-Nutzer, dem die Modelldateien in ./models gehören: id -u / id -g
|
||||||
|
HOST_UID=1000
|
||||||
|
HOST_GID=1000
|
||||||
|
|
||||||
|
# GIDs der Gruppen render/video auf dem Host: getent group render video
|
||||||
|
RENDER_GID=987
|
||||||
|
VIDEO_GID=983
|
||||||
|
|
||||||
|
# GPU-Auswahl (0 = erste ROCm-GPU; iGPU ausblenden)
|
||||||
|
HIP_VISIBLE_DEVICES=0
|
||||||
|
|
||||||
|
# Optional: Hugging-Face-Token für gated Modelle / höhere Download-Limits
|
||||||
|
HF_TOKEN=
|
||||||
|
|
||||||
|
# Image-Versionen (gepinnt)
|
||||||
|
LLAMA_SWAP_TAG=v265-rocm-b11610
|
||||||
|
CADDY_TAG=2.10.2-alpine
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.env
|
||||||
|
models/
|
||||||
|
client/continue/.env
|
||||||
7
PLAN.md
7
PLAN.md
@ -1,5 +1,12 @@
|
|||||||
# Plan: Lokaler LLM-Server für opencode per Docker Compose
|
# 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
|
## 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.
|
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:
|
Anforderungen:
|
||||||
|
|||||||
117
README.md
Normal file
117
README.md
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
# LocalAI – zentraler LLM-Server für opencode & Continue
|
||||||
|
|
||||||
|
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──┐
|
||||||
|
▼
|
||||||
|
caddy (Host-Netz, :8800) IP-Filter + API-Key
|
||||||
|
└─► llama-swap (nur 127.0.0.1:8080) lädt/wechselt Modelle
|
||||||
|
└─► llama-server (llama.cpp, ROCm) ─► GPU + RAM
|
||||||
|
Modelle: ./models (GGUF, Download von Hugging Face beim ersten Aufruf)
|
||||||
|
```
|
||||||
|
|
||||||
|
| Modell-ID | Zweck | Verhalten |
|
||||||
|
|---|---|---|
|
||||||
|
| `qwen3.5-35b-a3b` | Hauptmodell Chat/Agent (MoE) | eines der Chat-Modelle gleichzeitig geladen |
|
||||||
|
| `glm-4.7-flash` | Alternative Chat/Agent (MoE) | 〃 |
|
||||||
|
| `qwen3.5-9b` | schnelles kleines Chat-Modell | 〃 |
|
||||||
|
| `qwen2.5-coder-1.5b` | Autocomplete (FIM) für Continue | dauerhaft geladen (GPU) |
|
||||||
|
| `nomic-embed` | Embeddings für Continue `@codebase` | dauerhaft geladen (CPU) |
|
||||||
|
|
||||||
|
## Server einrichten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/workspace/localAI
|
||||||
|
cp .env.example .env
|
||||||
|
sed -i "s/^LLM_API_KEY=.*/LLM_API_KEY=$(openssl rand -hex 32)/" .env
|
||||||
|
chmod 600 .env
|
||||||
|
$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
|
||||||
|
./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:
|
||||||
|
|
||||||
|
| Aufruf | Wirkung |
|
||||||
|
|---|---|
|
||||||
|
| `./scripts/start.sh` | starten, Autocomplete + Embeddings + Hauptmodell vorladen |
|
||||||
|
| `./scripts/start.sh --no-preload` | nur starten |
|
||||||
|
| `./scripts/start.sh --model glm-4.7-flash` | anderes Chat-Modell vorladen |
|
||||||
|
| `./scripts/start.sh --show-key` | API-Key im Klartext anzeigen (für Client-Setup) |
|
||||||
|
| `./scripts/start.sh --pull` | Images vorher aktualisieren |
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
Der erste Aufruf eines Modells lädt es von Hugging Face herunter (Hauptmodell ~22 GB). Danach läuft alles offline.
|
||||||
|
|
||||||
|
### 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:
|
||||||
|
1. **ufw** lässt Port 8800 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`).
|
||||||
|
|
||||||
|
Die API läuft über plain HTTP. Über NetBird ist das unkritisch (WireGuard verschlüsselt den Tunnel), im Heim-LAN ist es akzeptabel.
|
||||||
|
|
||||||
|
## Clients einrichten
|
||||||
|
|
||||||
|
- **opencode** (Terminal-Agent): [docs/opencode.md](docs/opencode.md)
|
||||||
|
- **Continue** (VS-Code-Plugin): [docs/continue.md](docs/continue.md)
|
||||||
|
|
||||||
|
Schnelltest von jedem Client:
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer <KEY>" http://<server>:8800/v1/models
|
||||||
|
```
|
||||||
|
|
||||||
|
## Betrieb
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose ps # Status
|
||||||
|
docker compose logs -f llama-swap # Modell-Laden, Requests
|
||||||
|
docker compose logs -f caddy # Zugriffe
|
||||||
|
docker compose restart # nach Änderungen an .env / Caddyfile
|
||||||
|
```
|
||||||
|
- `llama-swap/config.yaml` wird **automatisch neu geladen**, ein Neustart ist nicht nötig.
|
||||||
|
- Web-Oberfläche von llama-swap (Modellstatus, Logs): `http://127.0.0.1:8080/ui`, nur lokal auf dem Server erreichbar.
|
||||||
|
- VRAM-Auslastung: `rocm-smi --showmeminfo vram`
|
||||||
|
|
||||||
|
### Modell hinzufügen
|
||||||
|
Einen Eintrag in `llama-swap/config.yaml` anlegen, z. B.
|
||||||
|
```yaml
|
||||||
|
mein-modell:
|
||||||
|
cmd: ${server} ${agent-ctx} -hf <hf-user>/<repo>-GGUF:Q4_K_M
|
||||||
|
ttl: 3600
|
||||||
|
```
|
||||||
|
Den Eintrag unter `routing.router.settings.groups.chat.members` aufnehmen. Danach die Client-Configs (`client/opencode.json`, `client/continue/config.yaml`) um die neue Modell-ID ergänzen.
|
||||||
|
|
||||||
|
GPU-Layer und MoE-Auslagerung in den RAM verteilt llama.cpp automatisch (`--fit`, Standard: an). `--fit-target 3072` hält ~3 GB VRAM für das Autocomplete-Modell frei.
|
||||||
|
|
||||||
|
## Auf einen anderen PC übertragen
|
||||||
|
|
||||||
|
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`.
|
||||||
|
3. Je nach GPU das Image in `.env` wählen (`LLAMA_SWAP_TAG`):
|
||||||
|
|
||||||
|
| GPU | Tag-Variante |
|
||||||
|
|---|---|
|
||||||
|
| AMD (ROCm) | `vXXX-rocm-bYYYY` (wie hier) |
|
||||||
|
| NVIDIA | `vXXX-cuda-bYYYY`. Dann in `docker-compose.yml` `devices`/`group_add`/`security_opt` durch `deploy.resources.reservations.devices` (nvidia) ersetzen. |
|
||||||
|
| herstellerunabhängig | `vXXX-vulkan-bYYYY` (nur `/dev/dri` nötig) |
|
||||||
|
| nur CPU | `vXXX-cpu-bYYYY` |
|
||||||
|
|
||||||
|
Verfügbare Tags: https://github.com/mostlygeek/llama-swap/pkgs/container/llama-swap
|
||||||
|
4. Threads (`--threads` im Makro `server`) an die CPU anpassen.
|
||||||
|
5. Firewall des Ziel-PCs entsprechend `scripts/firewall-ufw.sh` konfigurieren.
|
||||||
|
|
||||||
|
## Dateien
|
||||||
|
|
||||||
|
| Datei | Inhalt |
|
||||||
|
|---|---|
|
||||||
|
| `docker-compose.yml` | Dienste llama-swap + caddy |
|
||||||
|
| `.env` / `.env.example` | Secrets, Netze, Ports, GIDs, Image-Tags |
|
||||||
|
| `llama-swap/config.yaml` | Modelle, Startparameter, Gruppen |
|
||||||
|
| `caddy/Caddyfile` | IP-Filter, API-Key, Reverse Proxy |
|
||||||
|
| `client/opencode.json` | Vorlage für opencode-Clients |
|
||||||
|
| `client/continue/` | Vorlage für Continue-Clients |
|
||||||
|
| `scripts/start.sh` | Start mit Prüfungen, Preload und Statusausgabe |
|
||||||
|
| `scripts/firewall-ufw.sh` | ufw-Freigaben |
|
||||||
|
| `PLAN.md` | ursprünglicher Umsetzungsplan |
|
||||||
22
caddy/Caddyfile
Normal file
22
caddy/Caddyfile
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
admin off
|
||||||
|
auto_https off
|
||||||
|
}
|
||||||
|
|
||||||
|
:{$LISTEN_PORT} {
|
||||||
|
# 1. nur erlaubte Netze (localhost, LAN, NetBird) – siehe ALLOWED_NETS in .env
|
||||||
|
@blocked not remote_ip {$ALLOWED_NETS}
|
||||||
|
respond @blocked "forbidden" 403
|
||||||
|
|
||||||
|
# 2. API-Key als Bearer-Token
|
||||||
|
@noauth not header Authorization "Bearer {$LLM_API_KEY}"
|
||||||
|
respond @noauth "unauthorized" 401
|
||||||
|
|
||||||
|
# 3. an llama-swap; flush_interval -1 = Streaming (SSE) sofort durchreichen
|
||||||
|
reverse_proxy 127.0.0.1:{$BACKEND_PORT} {
|
||||||
|
flush_interval -1
|
||||||
|
transport http {
|
||||||
|
read_timeout 30m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
client/continue/.env.example
Normal file
3
client/continue/.env.example
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Nach ~/.continue/.env kopieren (Windows: %USERPROFILE%\.continue\.env)
|
||||||
|
# Wert = LLM_API_KEY aus der .env auf dem Server
|
||||||
|
LOCALAI_API_KEY=CHANGE_ME
|
||||||
85
client/continue/config.yaml
Normal file
85
client/continue/config.yaml
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
# Continue (VS Code) – Konfiguration für den zentralen LocalAI-Server
|
||||||
|
# Ziel: ~/.continue/config.yaml (Windows: %USERPROFILE%\.continue\config.yaml)
|
||||||
|
# Secret: ~/.continue/.env mit LOCALAI_API_KEY=<Key aus der .env des Servers>
|
||||||
|
#
|
||||||
|
# apiBase je nach Standort anpassen (nur EINE Zeile unten aktiv lassen):
|
||||||
|
# auf dem Server selbst: http://127.0.0.1:8800/v1
|
||||||
|
# im Heim-LAN: http://192.168.178.72:8800/v1
|
||||||
|
# über NetBird: http://100.73.82.219:8800/v1 (oder NetBird-DNS-Name)
|
||||||
|
|
||||||
|
---
|
||||||
|
name: LocalAI
|
||||||
|
version: 1.0.0
|
||||||
|
schema: v1
|
||||||
|
|
||||||
|
# gemeinsame Verbindungsdaten (YAML-Anker, braucht den %YAML-1.1-Header oben)
|
||||||
|
model_defaults: &localai
|
||||||
|
provider: openai
|
||||||
|
apiBase: http://192.168.178.72:8800/v1
|
||||||
|
apiKey: ${{ secrets.LOCALAI_API_KEY }}
|
||||||
|
requestOptions:
|
||||||
|
timeout: 1800 # Sekunden; erster Request lädt ggf. ein Modell
|
||||||
|
|
||||||
|
models:
|
||||||
|
# --- Chat / Agent / Edit ------------------------------------------------
|
||||||
|
- name: Qwen3.5 35B-A3B
|
||||||
|
<<: *localai
|
||||||
|
model: qwen3.5-35b-a3b
|
||||||
|
roles: [chat, edit, apply, summarize]
|
||||||
|
capabilities: [tool_use]
|
||||||
|
defaultCompletionOptions:
|
||||||
|
contextLength: 65536
|
||||||
|
maxTokens: 8192
|
||||||
|
|
||||||
|
- name: GLM-4.7 Flash
|
||||||
|
<<: *localai
|
||||||
|
model: glm-4.7-flash
|
||||||
|
roles: [chat, edit, apply]
|
||||||
|
capabilities: [tool_use]
|
||||||
|
defaultCompletionOptions:
|
||||||
|
contextLength: 65536
|
||||||
|
maxTokens: 8192
|
||||||
|
|
||||||
|
- name: Qwen3.5 9B (schnell)
|
||||||
|
<<: *localai
|
||||||
|
model: qwen3.5-9b
|
||||||
|
roles: [chat, edit, apply]
|
||||||
|
capabilities: [tool_use]
|
||||||
|
defaultCompletionOptions:
|
||||||
|
contextLength: 32768
|
||||||
|
maxTokens: 4096
|
||||||
|
|
||||||
|
# --- Tab-Autocomplete (Fill-in-the-Middle) ------------------------------
|
||||||
|
- name: Qwen2.5-Coder 1.5B (Autocomplete)
|
||||||
|
<<: *localai
|
||||||
|
model: qwen2.5-coder-1.5b
|
||||||
|
roles: [autocomplete]
|
||||||
|
useLegacyCompletionsEndpoint: true # /v1/completions statt Chat
|
||||||
|
promptTemplates:
|
||||||
|
autocomplete: "<|fim_prefix|>{{{prefix}}}<|fim_suffix|>{{{suffix}}}<|fim_middle|>"
|
||||||
|
defaultCompletionOptions:
|
||||||
|
contextLength: 8192
|
||||||
|
maxTokens: 256
|
||||||
|
temperature: 0.1
|
||||||
|
stop: ["<|endoftext|>", "<|fim_prefix|>", "<|fim_suffix|>", "<|fim_middle|>", "<|file_sep|>", "<|fim_pad|>", "<|repo_name|>"]
|
||||||
|
autocompleteOptions:
|
||||||
|
debounceDelay: 250
|
||||||
|
maxPromptTokens: 2048
|
||||||
|
onlyMyCode: true
|
||||||
|
|
||||||
|
# --- Embeddings für @codebase / Indexing --------------------------------
|
||||||
|
- name: Nomic Embed
|
||||||
|
<<: *localai
|
||||||
|
model: nomic-embed
|
||||||
|
roles: [embed]
|
||||||
|
|
||||||
|
context:
|
||||||
|
- provider: code
|
||||||
|
- provider: codebase
|
||||||
|
- provider: diff
|
||||||
|
- provider: terminal
|
||||||
|
- provider: problems
|
||||||
|
- provider: folder
|
||||||
|
- provider: file
|
||||||
|
- provider: currentFile
|
||||||
36
client/opencode.json
Normal file
36
client/opencode.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"$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}:8800/v1",
|
||||||
|
"apiKey": "{env:LOCALAI_API_KEY}",
|
||||||
|
"timeout": 1800000
|
||||||
|
},
|
||||||
|
"models": {
|
||||||
|
"qwen3.5-35b-a3b": {
|
||||||
|
"name": "Qwen3.5 35B-A3B",
|
||||||
|
"tool_call": true,
|
||||||
|
"limit": { "context": 65536, "output": 16384 }
|
||||||
|
},
|
||||||
|
"glm-4.7-flash": {
|
||||||
|
"name": "GLM-4.7 Flash",
|
||||||
|
"tool_call": true,
|
||||||
|
"limit": { "context": 65536, "output": 16384 }
|
||||||
|
},
|
||||||
|
"qwen3.5-9b": {
|
||||||
|
"name": "Qwen3.5 9B (schnell)",
|
||||||
|
"tool_call": true,
|
||||||
|
"limit": { "context": 32768, "output": 8192 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
docker-compose.yml
Normal file
49
docker-compose.yml
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# Lokaler LLM-Server: llama-swap (llama.cpp/ROCm) hinter Caddy (IP-Filter + API-Key)
|
||||||
|
# Start: docker compose up -d Logs: docker compose logs -f llama-swap
|
||||||
|
name: localai
|
||||||
|
|
||||||
|
services:
|
||||||
|
llama-swap:
|
||||||
|
image: ghcr.io/mostlygeek/llama-swap:${LLAMA_SWAP_TAG:-v265-rocm-b11610}
|
||||||
|
restart: unless-stopped
|
||||||
|
# nur lokal erreichbar – von außen ausschließlich über Caddy
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:${BACKEND_PORT:-8080}:8080"
|
||||||
|
user: "${HOST_UID:-1000}:${HOST_GID:-1000}"
|
||||||
|
group_add:
|
||||||
|
- "${RENDER_GID}"
|
||||||
|
- "${VIDEO_GID}"
|
||||||
|
devices:
|
||||||
|
- /dev/kfd
|
||||||
|
- /dev/dri
|
||||||
|
security_opt:
|
||||||
|
- seccomp=unconfined
|
||||||
|
ipc: host
|
||||||
|
environment:
|
||||||
|
HOME: /tmp
|
||||||
|
LLAMA_CACHE: /models
|
||||||
|
HIP_VISIBLE_DEVICES: "${HIP_VISIBLE_DEVICES:-0}"
|
||||||
|
HF_TOKEN: "${HF_TOKEN:-}"
|
||||||
|
volumes:
|
||||||
|
- ./models:/models
|
||||||
|
- ./llama-swap/config.yaml:/app/config.yaml:ro
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
caddy:
|
||||||
|
image: caddy:${CADDY_TAG:-2.10.2-alpine}
|
||||||
|
restart: unless-stopped
|
||||||
|
# Host-Netz: Quell-IP bleibt erhalten und ufw greift (Docker-Port-Publishing umgeht ufw)
|
||||||
|
network_mode: host
|
||||||
|
environment:
|
||||||
|
LLM_API_KEY: "${LLM_API_KEY:?LLM_API_KEY in .env setzen}"
|
||||||
|
ALLOWED_NETS: "${ALLOWED_NETS:-127.0.0.0/8 ::1}"
|
||||||
|
LISTEN_PORT: "${LISTEN_PORT:-8800}"
|
||||||
|
BACKEND_PORT: "${BACKEND_PORT:-8080}"
|
||||||
|
volumes:
|
||||||
|
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
|
depends_on:
|
||||||
|
- llama-swap
|
||||||
151
docs/continue.md
Normal file
151
docs/continue.md
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
# Continue (VS Code) mit dem zentralen LocalAI-Server
|
||||||
|
|
||||||
|
Continue ist ein Open-Source-Plugin für VS Code (und JetBrains). Es bietet:
|
||||||
|
|
||||||
|
| Funktion | Wofür | Modell auf dem Server |
|
||||||
|
|---|---|---|
|
||||||
|
| **Chat / Agent** | Fragen zum Code, Agent-Modus mit Tools (Dateien lesen/schreiben, Terminal) | `qwen3.5-35b-a3b` (oder `glm-4.7-flash`, `qwen3.5-9b`) |
|
||||||
|
| **Edit / Apply** | markierten Code umschreiben lassen (`Ctrl+I`), Vorschläge übernehmen | wie Chat |
|
||||||
|
| **Autocomplete** | Ghost-Text-Vervollständigung beim Tippen (Tab) | `qwen2.5-coder-1.5b` (läuft dauerhaft) |
|
||||||
|
| **Embeddings** | `@codebase`-Suche über das ganze Projekt | `nomic-embed` (läuft dauerhaft, CPU) |
|
||||||
|
|
||||||
|
Alles läuft über die eine API `http://<server>:8800/v1` mit API-Key. Es geht nichts in die Cloud.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Voraussetzungen
|
||||||
|
|
||||||
|
- Der Server läuft (`docker compose ps` auf alegab-pc, siehe [README](../README.md)).
|
||||||
|
- Der Client-PC ist **im Heim-LAN (192.168.178.0/24)** oder **mit NetBird verbunden**. Andere Netze bekommen `403`.
|
||||||
|
- Du hast den **API-Key**: Das ist `LLM_API_KEY` aus `.env` auf dem Server.
|
||||||
|
```bash
|
||||||
|
grep ^LLM_API_KEY ~/workspace/localAI/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
Verbindungstest vom Client aus:
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer <KEY>" http://192.168.178.72:8800/v1/models
|
||||||
|
# NetBird: http://100.73.82.219:8800/v1/models
|
||||||
|
```
|
||||||
|
Kommt eine JSON-Liste mit den Modellen zurück, steht die Verbindung.
|
||||||
|
|
||||||
|
## 2. Extension installieren
|
||||||
|
|
||||||
|
- **VS Code:** Extensions (`Ctrl+Shift+X`) öffnen, nach **„Continue"** suchen (Herausgeber: Continue) und installieren.
|
||||||
|
Alternativ auf der Kommandozeile: `code --install-extension Continue.continue`
|
||||||
|
- **VSCodium / Code-OSS:** gleicher Name, die Extension kommt dort aus Open VSX.
|
||||||
|
|
||||||
|
Beim ersten Start zeigt Continue eine Anmeldeseite für den „Continue Hub". **Melde dich nicht an.** Wähle stattdessen *„Or, configure your own models"* bzw. *„Skip"* oder *„Local config"*. Damit bleibt alles lokal.
|
||||||
|
|
||||||
|
## 3. Konfiguration einspielen
|
||||||
|
|
||||||
|
Continue liest die lokale Konfiguration aus:
|
||||||
|
|
||||||
|
| System | Pfad |
|
||||||
|
|---|---|
|
||||||
|
| Linux / macOS | `~/.continue/config.yaml` und `~/.continue/.env` |
|
||||||
|
| Windows | `%USERPROFILE%\.continue\config.yaml` und `%USERPROFILE%\.continue\.env` |
|
||||||
|
|
||||||
|
**Linux/macOS:**
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.continue
|
||||||
|
cp ~/.continue/config.yaml ~/.continue/config.yaml.bak 2>/dev/null # Backup, falls vorhanden
|
||||||
|
cp client/continue/config.yaml ~/.continue/config.yaml
|
||||||
|
cp client/continue/.env.example ~/.continue/.env
|
||||||
|
chmod 600 ~/.continue/.env
|
||||||
|
$EDITOR ~/.continue/.env # LOCALAI_API_KEY=<KEY> eintragen
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows (PowerShell):**
|
||||||
|
```powershell
|
||||||
|
New-Item -ItemType Directory -Force $env:USERPROFILE\.continue
|
||||||
|
Copy-Item client\continue\config.yaml $env:USERPROFILE\.continue\config.yaml
|
||||||
|
Copy-Item client\continue\.env.example $env:USERPROFILE\.continue\.env
|
||||||
|
notepad $env:USERPROFILE\.continue\.env
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Dateien `config.yaml` und `.env.example` liegen in diesem Repo unter [`client/continue/`](../client/continue/). Auf einen anderen PC kopierst du sie per USB-Stick, scp oder Git.
|
||||||
|
|
||||||
|
## 4. Server-Adresse eintragen
|
||||||
|
|
||||||
|
Oben in `config.yaml` gibt es den Block `model_defaults:`. Dort steht **eine** `apiBase`, die für alle Modelle gilt:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
model_defaults: &localai
|
||||||
|
provider: openai
|
||||||
|
apiBase: http://192.168.178.72:8800/v1 # ← hier anpassen
|
||||||
|
apiKey: ${{ secrets.LOCALAI_API_KEY }}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Standort des Client-PCs | `apiBase` |
|
||||||
|
|---|---|
|
||||||
|
| auf alegab-pc selbst | `http://127.0.0.1:8800/v1` |
|
||||||
|
| im Heim-LAN | `http://192.168.178.72:8800/v1` |
|
||||||
|
| unterwegs über NetBird | `http://100.73.82.219:8800/v1` oder `http://<netbird-name>.netbird.cloud:8800/v1` |
|
||||||
|
|
||||||
|
> **Tipp für Laptops**, die mal im LAN und mal unterwegs sind: Nimm immer die **NetBird-Adresse**. Sie funktioniert auch zu Hause, solange NetBird verbunden ist. Dann musst du nie umschalten.
|
||||||
|
|
||||||
|
Den NetBird-Namen und die NetBird-IP des Servers zeigt `netbird status -d` auf alegab-pc an.
|
||||||
|
|
||||||
|
Nach dem Speichern lädt Continue die Konfiguration automatisch neu. Falls nicht: Command Palette (`Ctrl+Shift+P`) → *„Continue: Reload"* oder VS Code neu laden.
|
||||||
|
|
||||||
|
## 5. Telemetrie abschalten
|
||||||
|
|
||||||
|
Continue sendet standardmäßig anonyme Nutzungsstatistiken. So schaltest du das ab:
|
||||||
|
|
||||||
|
VS Code-Einstellungen (`Ctrl+,`) → nach `continue telemetry` suchen → **„Continue: Telemetry Enabled"** abhaken.
|
||||||
|
Oder direkt in `settings.json`:
|
||||||
|
```json
|
||||||
|
"continue.telemetryEnabled": false
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Benutzung
|
||||||
|
|
||||||
|
| Aktion | Tastenkürzel (Standard) |
|
||||||
|
|---|---|
|
||||||
|
| Continue-Seitenleiste öffnen / Chat | `Ctrl+L` (markierten Code mitnehmen) |
|
||||||
|
| Code inline bearbeiten (Edit) | Code markieren → `Ctrl+I` |
|
||||||
|
| Autocomplete annehmen | `Tab` (Wort für Wort: `Ctrl+→`) |
|
||||||
|
| Autocomplete ablehnen | `Esc` |
|
||||||
|
| Modell wechseln | Dropdown unten in der Chat-Box |
|
||||||
|
| Modus wechseln (Chat / Plan / Agent) | Dropdown links unten in der Chat-Box |
|
||||||
|
|
||||||
|
**Kontext mit `@`** im Chat: `@codebase` (semantische Suche im Projekt), `@file`, `@folder`, `@diff` (aktuelle Git-Änderungen), `@terminal`, `@problems`.
|
||||||
|
|
||||||
|
### Hinweise zu den Modellen
|
||||||
|
- **Erster Request nach längerer Pause:** Das Chat-Modell wird in den VRAM geladen, das dauert 10–60 s. Danach bleibt es 1 h geladen.
|
||||||
|
- **Modellwechsel** zwischen den Chat-Modellen dauert ebenso. Am besten bei einem Modell bleiben.
|
||||||
|
- Das **Autocomplete-** und das **Embedding-Modell** laufen dauerhaft und werden durch den Chat nie entladen.
|
||||||
|
- Der Server ist **ein gemeinsames System**. Wenn mehrere Leute gleichzeitig chatten, werden die Anfragen nacheinander abgearbeitet. Wer ein *anderes* Chat-Modell wählt, löst bei allen einen Modellwechsel aus.
|
||||||
|
- **Agent-Modus**: Gib kleine, klar umrissene Aufgaben. Lokale Modelle verlieren bei langen Mehrschritt-Aufträgen eher den Faden als Cloud-Modelle.
|
||||||
|
|
||||||
|
## 7. Fehlersuche
|
||||||
|
|
||||||
|
| Symptom | Ursache / Lösung |
|
||||||
|
|---|---|
|
||||||
|
| `401 unauthorized` | API-Key fehlt oder ist falsch. `~/.continue/.env` prüfen (kein Leerzeichen, keine Anführungszeichen). |
|
||||||
|
| `403 forbidden` | Der Client ist in einem nicht erlaubten Netz. LAN oder NetBird verbinden. |
|
||||||
|
| Timeout / „connection refused" | Falsche `apiBase`, Server aus oder Firewall. `curl`-Test aus Abschnitt 1 ausprobieren. |
|
||||||
|
| Erste Antwort dauert lange | Das Modell wird geladen, beim allerersten Mal sogar heruntergeladen. Abwarten. |
|
||||||
|
| Autocomplete zeigt nichts | Unten rechts in der Statusleiste muss „Continue" aktiv sein (Klick → *Enable autocomplete*). Im Output-Panel *„Continue"* nach Fehlern schauen. Das Template in `config.yaml` (`promptTemplates.autocomplete`) muss unverändert sein. |
|
||||||
|
| Autocomplete liefert Müll / Chat-Text | Für Autocomplete muss das Modell `qwen2.5-coder-1.5b` mit `useLegacyCompletionsEndpoint: true` eingestellt sein, kein Chat-Modell. |
|
||||||
|
| Agent ruft keine Tools auf | Das Modell braucht `capabilities: [tool_use]`. `qwen3.5-35b-a3b` oder `glm-4.7-flash` nehmen. |
|
||||||
|
| `@codebase` findet nichts | Indexierung läuft noch, siehe Fortschritt in der Continue-Seitenleiste. Neu anstoßen: Command Palette (`Ctrl+Shift+P`) → „Continue" eintippen → den Befehl zum Neu-Indexieren (*Re-Index* / *Rebuild codebase index*) wählen. |
|
||||||
|
|
||||||
|
Logs des Servers (auf alegab-pc):
|
||||||
|
```bash
|
||||||
|
cd ~/workspace/localAI
|
||||||
|
docker compose logs -f llama-swap # Modell-Laden, Requests
|
||||||
|
docker compose logs -f caddy # Zugriffe, 401/403
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Fertige Varianten
|
||||||
|
|
||||||
|
### a) Nur ein Modell, minimal
|
||||||
|
Wenn du nur Chat und Autocomplete willst, lösche in `config.yaml` die Einträge `GLM-4.7 Flash`, `Qwen3.5 9B` und `Nomic Embed`.
|
||||||
|
|
||||||
|
### b) Schwacher Client, schnelle Antworten
|
||||||
|
Nimm `Qwen3.5 9B (schnell)` als Chat-Modell. Es liegt komplett im VRAM und antwortet deutlich schneller, ist aber weniger klug.
|
||||||
|
|
||||||
|
### c) Projektbezogene Konfiguration
|
||||||
|
Continue liest zusätzlich `.continue/`-Ordner im Projekt. Dort kannst du z. B. projektspezifische Regeln ablegen (`.continue/rules/*.md`) und sie mit ins Repo einchecken. Die Modelle bleiben in der globalen `~/.continue/config.yaml`.
|
||||||
64
docs/opencode.md
Normal file
64
docs/opencode.md
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# opencode mit dem zentralen LocalAI-Server
|
||||||
|
|
||||||
|
opencode ist ein Coding-Agent für das Terminal. Er läuft auf dem Client-PC und arbeitet dort auf deinem Code. Nur die Modell-Anfragen gehen an den Server.
|
||||||
|
|
||||||
|
## 1. opencode installieren
|
||||||
|
|
||||||
|
| System | Befehl |
|
||||||
|
|---|---|
|
||||||
|
| Arch / CachyOS | `sudo pacman -S opencode` (oder AUR `opencode-bin`) |
|
||||||
|
| Linux / macOS allgemein | `curl -fsSL https://opencode.ai/install \| bash` |
|
||||||
|
| macOS (Homebrew) | `brew install sst/tap/opencode` |
|
||||||
|
| Windows | `npm i -g opencode-ai` (Node.js nötig) oder `scoop install opencode` |
|
||||||
|
|
||||||
|
## 2. Konfiguration einspielen
|
||||||
|
|
||||||
|
| System | Pfad |
|
||||||
|
|---|---|
|
||||||
|
| Linux / macOS | `~/.config/opencode/opencode.json` |
|
||||||
|
| Windows | `%USERPROFILE%\.config\opencode\opencode.json` |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.config/opencode
|
||||||
|
cp ~/.config/opencode/opencode.json ~/.config/opencode/opencode.json.bak 2>/dev/null
|
||||||
|
cp client/opencode.json ~/.config/opencode/opencode.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Vorlage liegt unter [`client/opencode.json`](../client/opencode.json). Sie nutzt zwei Umgebungsvariablen:
|
||||||
|
|
||||||
|
| Variable | Wert |
|
||||||
|
|---|---|
|
||||||
|
| `LOCALAI_HOST` | `127.0.0.1` (auf dem Server), `192.168.178.72` (LAN), `100.73.82.219` (NetBird) |
|
||||||
|
| `LOCALAI_API_KEY` | `LLM_API_KEY` aus der `.env` des Servers |
|
||||||
|
|
||||||
|
**fish** (`~/.config/fish/config.fish`):
|
||||||
|
```fish
|
||||||
|
set -gx LOCALAI_HOST 100.73.82.219
|
||||||
|
set -gx LOCALAI_API_KEY <KEY>
|
||||||
|
```
|
||||||
|
**bash/zsh** (`~/.bashrc` / `~/.zshrc`):
|
||||||
|
```bash
|
||||||
|
export LOCALAI_HOST=100.73.82.219
|
||||||
|
export LOCALAI_API_KEY=<KEY>
|
||||||
|
```
|
||||||
|
**Windows (PowerShell, dauerhaft):**
|
||||||
|
```powershell
|
||||||
|
[Environment]::SetEnvironmentVariable("LOCALAI_HOST", "100.73.82.219", "User")
|
||||||
|
[Environment]::SetEnvironmentVariable("LOCALAI_API_KEY", "<KEY>", "User")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Benutzen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/mein-projekt
|
||||||
|
opencode
|
||||||
|
```
|
||||||
|
- `/models` zeigt die Modelle. Dort sollten nur `LocalAI (alegab-pc)`-Einträge stehen.
|
||||||
|
- `/init` erzeugt eine `AGENTS.md` mit Projektwissen. Das hilft lokalen Modellen sehr und lohnt sich einmal pro Projekt.
|
||||||
|
- `Tab` wechselt zwischen dem **Build**-Agenten (darf ändern) und dem **Plan**-Agenten (nur lesen).
|
||||||
|
|
||||||
|
## Hinweise
|
||||||
|
|
||||||
|
- `small_model` zeigt bewusst auf dasselbe Modell wie `model`. Ein zweites Chat-Modell würde auf dem Server bei jedem Titel- oder Zusammenfassungs-Aufruf einen Modellwechsel auslösen.
|
||||||
|
- Die Kontextgrenzen in `limit.context` passen zur Server-Konfiguration (`-c` in `llama-swap/config.yaml`). Ändert man die eine Seite, muss man die andere mitziehen.
|
||||||
|
- Fehlercodes (401/403/Timeout): siehe Tabelle in [continue.md](continue.md#7-fehlersuche). Die Ursachen sind dieselben.
|
||||||
93
llama-swap/config.yaml
Normal file
93
llama-swap/config.yaml
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
# yaml-language-server: $schema=https://raw.githubusercontent.com/mostlygeek/llama-swap/refs/heads/main/config-schema.json
|
||||||
|
#
|
||||||
|
# Modelle werden beim ersten Aufruf per -hf von Hugging Face nach ./models geladen
|
||||||
|
# (LLAMA_CACHE=/models). Danach läuft alles offline.
|
||||||
|
#
|
||||||
|
# VRAM-Budget RX 6950 XT (16 GB):
|
||||||
|
# - Autocomplete-Modell (~2 GB) bleibt dauerhaft geladen
|
||||||
|
# - genau ein Chat-Modell gleichzeitig; llama.cpp --fit (Standard: an) verteilt
|
||||||
|
# Layer/MoE-Experten automatisch auf VRAM und RAM, --fit-target hält ~3 GB frei
|
||||||
|
# - Embeddings laufen auf der CPU
|
||||||
|
|
||||||
|
healthCheckTimeout: 900 # erster Start kann einen 20-GB-Download enthalten
|
||||||
|
logLevel: info
|
||||||
|
startPort: 10001
|
||||||
|
|
||||||
|
macros:
|
||||||
|
server: >-
|
||||||
|
/app/llama-server --host 127.0.0.1 --port ${PORT}
|
||||||
|
--jinja -fa on --threads 12 --no-webui
|
||||||
|
# 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
|
||||||
|
|
||||||
|
models:
|
||||||
|
# --- Chat / Agent (opencode, Continue Chat/Agent/Edit) -----------------
|
||||||
|
qwen3.5-35b-a3b:
|
||||||
|
name: Qwen3.5 35B-A3B (MoE)
|
||||||
|
cmd: >-
|
||||||
|
${server} ${agent-ctx}
|
||||||
|
-hf unsloth/Qwen3.5-35B-A3B-GGUF:Q4_K_M
|
||||||
|
ttl: 3600
|
||||||
|
|
||||||
|
glm-4.7-flash:
|
||||||
|
name: GLM-4.7 Flash (MoE)
|
||||||
|
cmd: >-
|
||||||
|
${server} ${agent-ctx}
|
||||||
|
-hf unsloth/GLM-4.7-Flash-GGUF:Q4_K_M
|
||||||
|
ttl: 3600
|
||||||
|
|
||||||
|
qwen3.5-9b:
|
||||||
|
name: Qwen3.5 9B (schnell, komplett im VRAM)
|
||||||
|
cmd: >-
|
||||||
|
${server} -c 32768 -ctk q8_0 -ctv q8_0 --fit-target 3072
|
||||||
|
-hf unsloth/Qwen3.5-9B-GGUF:Q4_K_M
|
||||||
|
ttl: 3600
|
||||||
|
|
||||||
|
# --- Autocomplete (Continue Tab-Completion, FIM) -----------------------
|
||||||
|
qwen2.5-coder-1.5b:
|
||||||
|
name: Qwen2.5-Coder 1.5B Base (FIM)
|
||||||
|
cmd: >-
|
||||||
|
${server} -c 8192
|
||||||
|
-hf ggml-org/Qwen2.5-Coder-1.5B-Q8_0-GGUF
|
||||||
|
-ngl 99
|
||||||
|
ttl: 0
|
||||||
|
|
||||||
|
# --- Embeddings (Continue @codebase) -----------------------------------
|
||||||
|
nomic-embed:
|
||||||
|
name: Nomic Embed Text v1.5
|
||||||
|
cmd: >-
|
||||||
|
/app/llama-server --host 127.0.0.1 --port ${PORT} --no-webui
|
||||||
|
-hf nomic-ai/nomic-embed-text-v1.5-GGUF:Q8_0
|
||||||
|
--embedding --pooling mean -c 8192 -b 8192 -ub 8192 -ngl 0 --threads 8
|
||||||
|
ttl: 0
|
||||||
|
|
||||||
|
# Helfer-Modelle beim Container-Start laden (auch nach Reboot), damit
|
||||||
|
# Autocomplete sofort reagiert. Das Chat-Modell lädt beim ersten Request
|
||||||
|
# bzw. per scripts/start.sh.
|
||||||
|
hooks:
|
||||||
|
on_startup:
|
||||||
|
preload:
|
||||||
|
- qwen2.5-coder-1.5b
|
||||||
|
- nomic-embed
|
||||||
|
|
||||||
|
routing:
|
||||||
|
router:
|
||||||
|
use: group
|
||||||
|
settings:
|
||||||
|
groups:
|
||||||
|
# Chat-Modelle wechseln sich ab, lassen aber die Helfer in Ruhe
|
||||||
|
chat:
|
||||||
|
swap: true
|
||||||
|
exclusive: false
|
||||||
|
members:
|
||||||
|
- qwen3.5-35b-a3b
|
||||||
|
- glm-4.7-flash
|
||||||
|
- qwen3.5-9b
|
||||||
|
# Helfer bleiben dauerhaft geladen
|
||||||
|
always-on:
|
||||||
|
swap: false
|
||||||
|
exclusive: false
|
||||||
|
persistent: true
|
||||||
|
members:
|
||||||
|
- qwen2.5-coder-1.5b
|
||||||
|
- nomic-embed
|
||||||
12
scripts/firewall-ufw.sh
Executable file
12
scripts/firewall-ufw.sh
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Einmalig ausführen: sudo ./scripts/firewall-ufw.sh
|
||||||
|
# Öffnet den Caddy-Port nur für LAN und NetBird (wt0). Caddy filtert zusätzlich selbst.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PORT="${LISTEN_PORT:-8800}"
|
||||||
|
LAN="${LAN_NET:-192.168.178.0/24}"
|
||||||
|
VPN_IF="${VPN_IF:-wt0}"
|
||||||
|
|
||||||
|
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 status numbered | grep -E "$PORT" || true
|
||||||
150
scripts/start.sh
Executable file
150
scripts/start.sh
Executable file
@ -0,0 +1,150 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Startet den LocalAI-Stack, wartet bis er bereit ist und zeigt die Verbindungsdaten.
|
||||||
|
#
|
||||||
|
# ./scripts/start.sh starten + Chat-Modell vorladen
|
||||||
|
# ./scripts/start.sh --no-preload nur starten
|
||||||
|
# ./scripts/start.sh --model glm-4.7-flash anderes Chat-Modell vorladen
|
||||||
|
# ./scripts/start.sh --show-key API-Key im Klartext ausgeben
|
||||||
|
# ./scripts/start.sh --pull Images vorher aktualisieren
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$(readlink -f "$0")")/.."
|
||||||
|
|
||||||
|
PRELOAD=1
|
||||||
|
MODEL="qwen3.5-35b-a3b"
|
||||||
|
SHOW_KEY=0
|
||||||
|
PULL=0
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--no-preload) PRELOAD=0 ;;
|
||||||
|
--model) MODEL="$2"; shift ;;
|
||||||
|
--show-key) SHOW_KEY=1 ;;
|
||||||
|
--pull) PULL=1 ;;
|
||||||
|
-h|--help) sed -n '2,9p' "$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}"; }
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- Prüfungen
|
||||||
|
step "Prüfungen"
|
||||||
|
|
||||||
|
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'?)"
|
||||||
|
docker compose version >/dev/null 2>&1 || die "docker compose (v2) fehlt"
|
||||||
|
ok "Docker $(docker version --format '{{.Server.Version}}')"
|
||||||
|
|
||||||
|
[[ -e /dev/kfd && -d /dev/dri ]] || die "/dev/kfd oder /dev/dri fehlt – AMD-GPU-Treiber (amdgpu/ROCm) prüfen"
|
||||||
|
ok "GPU-Geräte vorhanden"
|
||||||
|
|
||||||
|
if [[ ! -f .env ]]; then
|
||||||
|
warn ".env fehlt – wird aus .env.example erzeugt"
|
||||||
|
cp .env.example .env
|
||||||
|
sed -i "s/^LLM_API_KEY=.*/LLM_API_KEY=$(openssl rand -hex 32)/" .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)
|
||||||
|
[[ -n "$rg" ]] && sed -i "s/^RENDER_GID=.*/RENDER_GID=$rg/" .env
|
||||||
|
[[ -n "$vg" ]] && sed -i "s/^VIDEO_GID=.*/VIDEO_GID=$vg/" .env
|
||||||
|
chmod 600 .env
|
||||||
|
warn "Bitte ALLOWED_NETS in .env prüfen (LAN-/NetBird-Netz)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -a; . ./.env; set +a
|
||||||
|
LISTEN_PORT="${LISTEN_PORT:-8800}"
|
||||||
|
BACKEND_PORT="${BACKEND_PORT:-8080}"
|
||||||
|
[[ -n "${LLM_API_KEY:-}" && "$LLM_API_KEY" != "CHANGE_ME" ]] || die "LLM_API_KEY in .env setzen (openssl rand -hex 32)"
|
||||||
|
ok ".env geladen"
|
||||||
|
|
||||||
|
# Ports dürfen nur von unserem eigenen Stack belegt sein
|
||||||
|
running=$(docker compose ps --status running -q 2>/dev/null | wc -l)
|
||||||
|
if [[ "$running" -eq 0 ]]; then
|
||||||
|
for p in "$LISTEN_PORT" "$BACKEND_PORT"; do
|
||||||
|
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"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
ok "Ports $LISTEN_PORT und $BACKEND_PORT frei"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if systemctl is-active --quiet ollama 2>/dev/null; then
|
||||||
|
warn "Host-Ollama läuft noch (belegt ggf. VRAM, lauscht auf dem Netz) → sudo systemctl disable --now ollama"
|
||||||
|
fi
|
||||||
|
|
||||||
|
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
|
||||||
|
warn "Keine ufw-Freigabe für Port $LISTEN_PORT gefunden → sudo ./scripts/firewall-ufw.sh"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p models
|
||||||
|
free_gb=$(df -BG --output=avail models | tail -1 | tr -dc '0-9')
|
||||||
|
(( free_gb < 30 )) && warn "Nur ${free_gb} GB frei – Modell-Downloads brauchen bis zu 25 GB"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------- Start
|
||||||
|
step "Starte Container"
|
||||||
|
(( PULL )) && docker compose pull
|
||||||
|
docker compose up -d --remove-orphans
|
||||||
|
|
||||||
|
step "Warte auf Bereitschaft"
|
||||||
|
for i in $(seq 1 60); do
|
||||||
|
curl -fsS -m 2 "http://127.0.0.1:$BACKEND_PORT/health" >/dev/null 2>&1 && break
|
||||||
|
(( i == 60 )) && die "llama-swap antwortet nicht – docker compose logs llama-swap"
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
ok "llama-swap bereit"
|
||||||
|
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
code=$(curl -s -o /dev/null -m 2 -w '%{http_code}' "http://127.0.0.1:$LISTEN_PORT/v1/models" || true)
|
||||||
|
[[ "$code" == "401" ]] && break
|
||||||
|
(( i == 30 )) && die "Caddy antwortet nicht wie erwartet (HTTP $code) – docker compose logs caddy"
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
code=$(curl -s -o /dev/null -m 5 -w '%{http_code}' -H "Authorization: Bearer $LLM_API_KEY" "http://127.0.0.1:$LISTEN_PORT/v1/models")
|
||||||
|
[[ "$code" == "200" ]] || die "API mit Key liefert HTTP $code"
|
||||||
|
ok "Caddy bereit (Auth aktiv)"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- Preload
|
||||||
|
if (( PRELOAD )); then
|
||||||
|
step "Lade Modelle vor (erster Start lädt ggf. von Hugging Face herunter)"
|
||||||
|
preload() { # $1 = Modell, $2 = Endpoint, $3 = JSON-Body
|
||||||
|
local t0=$SECONDS code
|
||||||
|
printf ' %-22s … ' "$1"
|
||||||
|
code=$(curl -s -o /dev/null -m 3600 -w '%{http_code}' -H 'Content-Type: application/json' \
|
||||||
|
"http://127.0.0.1:$BACKEND_PORT$2" -d "$3" || true)
|
||||||
|
if [[ "$code" == "200" ]]; then echo "${G}ok${N} ($((SECONDS - t0)) s)"; else echo "${R}HTTP $code${N}"; fi
|
||||||
|
}
|
||||||
|
preload qwen2.5-coder-1.5b /v1/completions '{"model":"qwen2.5-coder-1.5b","prompt":"x","max_tokens":1}'
|
||||||
|
preload nomic-embed /v1/embeddings '{"model":"nomic-embed","input":"x"}'
|
||||||
|
preload "$MODEL" /v1/chat/completions "{\"model\":\"$MODEL\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],\"max_tokens\":1}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ Status
|
||||||
|
step "Status"
|
||||||
|
docker compose ps --format '{{.Service}}: {{.Status}}' | sed 's/^/ /'
|
||||||
|
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"
|
||||||
|
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}'
|
||||||
|
|
||||||
|
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}')
|
||||||
|
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)
|
||||||
|
echo " lokal: http://127.0.0.1:$LISTEN_PORT/v1"
|
||||||
|
[[ -n "$lan_ip" ]] && echo " 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)}"
|
||||||
|
if (( SHOW_KEY )); then
|
||||||
|
echo " API-Key: $LLM_API_KEY"
|
||||||
|
else
|
||||||
|
echo " API-Key: ${LLM_API_KEY:0:6}… (vollständig: --show-key)"
|
||||||
|
fi
|
||||||
|
echo " Web-UI: http://127.0.0.1:$BACKEND_PORT/ui (nur lokal)"
|
||||||
|
echo
|
||||||
|
echo " Clients einrichten: docs/opencode.md, docs/continue.md"
|
||||||
Loading…
Reference in New Issue
Block a user