Compare commits
3
Commits
7006991843
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dec534cc88 | ||
|
|
0929cdb555 | ||
|
|
8544abd2d4 |
No files matched your search
@@ -12,6 +12,6 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Install test dependencies
|
- name: Install test dependencies
|
||||||
run: pacman -Syu --noconfirm bash shellcheck ripgrep python jq
|
run: pacman -Syu --noconfirm bash shellcheck ripgrep python python-yaml jq
|
||||||
- name: Run smoke tests
|
- name: Run smoke tests
|
||||||
run: ./tests/smoke.sh
|
run: ./tests/smoke.sh
|
||||||
@@ -1,3 +1,23 @@
|
|||||||
|
# 2026-10-23 — Explizites 64K-Kontextfenster für Hermes
|
||||||
|
|
||||||
|
- `model.context_length: 65536` passend zu beiden Hauptmodellprofilen gesetzt;
|
||||||
|
verhindert den beobachteten Alias-Fallback auf 256.000 Tokens.
|
||||||
|
- Bestehende CPU-Kompressionsschwelle von 24.000 Tokens beibehalten.
|
||||||
|
- Bereitstellung, Migration und Betriebsdokumentation aktualisiert; Regression
|
||||||
|
für alte Kontextwerte, Profilabgleich und wiederholte Konfiguration ergänzt.
|
||||||
|
|
||||||
|
# 2026-10-23 — Routineprofil und Begrenzung lokaler Inferenz
|
||||||
|
|
||||||
|
- Qwen3.5-4B als Alltagsmodell, 27B als explizite Alternative und bestehendes
|
||||||
|
CPU-Kompressionsmodell mit festen Revisionen, Größen und SHA-256 erfasst.
|
||||||
|
- Offline-/Download-Vorbereitung, portable Profile und gezielte Hermes-
|
||||||
|
Konfigurationsübernahme mit Backups und optionalem Lösen lokaler Cronbindungen.
|
||||||
|
- 2048-Tokens-Serverstandard, Hermes-Requestlimits und unabhängiger 15-Sekunden-
|
||||||
|
Slot-Watchdog gegen zu lange oder endlose Modellantworten ergänzt.
|
||||||
|
- `hermes-stack-model` ohne Pfad, mit Status, Vorprüfung und Fehler-Rückweg.
|
||||||
|
- Neuinstallation, Migration, Modellwahl, Aktivierung und Grenzen dokumentiert;
|
||||||
|
Tests für Prüfsummen, Konfigurationsschutz, Modellwechsel und Recovery.
|
||||||
|
|
||||||
# 2026-10-21 — Erfolgreiche Hermes-Prüfung ohne Versionswechsel
|
# 2026-10-21 — Erfolgreiche Hermes-Prüfung ohne Versionswechsel
|
||||||
|
|
||||||
- Ein unveränderter Commit nach erfolgreichem `hermes update` gilt als Erfolg:
|
- Ein unveränderter Commit nach erfolgreichem `hermes update` gilt als Erfolg:
|
||||||
|
|||||||
@@ -14,11 +14,15 @@ Reproduzierbares Benutzer-Deployment für Hermes Agent auf Arch Linux mit:
|
|||||||
- optionalem CUDA-Build von llama-server, llama-cli und llama-bench
|
- optionalem CUDA-Build von llama-server, llama-cli und llama-bench
|
||||||
- Qwen-/RTX-3080-Profilen mit prüfbarem Wechsel und Rückweg
|
- Qwen-/RTX-3080-Profilen mit prüfbarem Wechsel und Rückweg
|
||||||
|
|
||||||
Stand: 2026-10-21. Für deine **bestehende Workstation** zuerst
|
Stand: 2026-10-27. Die Anleitung für das **aktuelle Setup auf einem neuen Rechner**
|
||||||
|
steht in [NEW_WORKSTATION.md](docs/NEW_WORKSTATION.md): feste Modellrevisionen und
|
||||||
|
Prüfsummen, Vorbereitung der Profile und Hermes-Limits, Aktivierung und Rückweg.
|
||||||
|
Für deine **bestehende Workstation** zuerst
|
||||||
[`docs/WORKSTATION.md`](docs/WORKSTATION.md) lesen. Der Schnellstart unten ist für
|
[`docs/WORKSTATION.md`](docs/WORKSTATION.md) lesen. Der Schnellstart unten ist für
|
||||||
eine neue Maschine. Hardware-Leistungsgewinne sind noch nicht auf deiner GPU
|
eine neue Maschine. Das Routineprofil erprobt Qwen3.5-9B IQ4_XS mit 64K Kontext. Das bisherige
|
||||||
gemessen; die kurze Funktions- und VRAM-Abnahme vom 12.10.2026 ist in
|
4B-Modell mit 64K bleibt als `fallback` verfügbar. Details und Rückweg:
|
||||||
[RTX3080.md](docs/RTX3080.md) dokumentiert. Der Installer richtet keinen
|
[QWEN9B_TRIAL.md](docs/QWEN9B_TRIAL.md). Das 27B-Modell bleibt optional.
|
||||||
|
Frühere VRAM-Messungen stehen in [RTX3080.md](docs/RTX3080.md). Der Installer richtet keinen
|
||||||
Gitea-Server ein und pusht keine Änderungen.
|
Gitea-Server ein und pusht keine Änderungen.
|
||||||
|
|
||||||
Das Repository enthält **keine Zugangsdaten, Signal-Sitzung oder
|
Das Repository enthält **keine Zugangsdaten, Signal-Sitzung oder
|
||||||
@@ -61,7 +65,9 @@ hermes gateway install --force
|
|||||||
```
|
```
|
||||||
|
|
||||||
In Hermes als lokalen OpenAI-kompatiblen Endpunkt `http://127.0.0.1:8080/v1`
|
In Hermes als lokalen OpenAI-kompatiblen Endpunkt `http://127.0.0.1:8080/v1`
|
||||||
und als Modell-ID `qwen38-27b-local` wählen. Bestehende Hermes-Konfigurationen,
|
und als Modell-ID `hermes-local` wählen, wenn die aktuellen Routine-/Large-Profile
|
||||||
|
nach [NEW_WORKSTATION.md](docs/NEW_WORKSTATION.md) vorbereitet wurden. Der ältere
|
||||||
|
HF-Schnellstart verwendet weiterhin `qwen38-27b-local`. Bestehende Hermes-Konfigurationen,
|
||||||
Profile, Skills und Memories werden nicht durch generische Vorgaben ersetzt.
|
Profile, Skills und Memories werden nicht durch generische Vorgaben ersetzt.
|
||||||
Für Neuinstallationen diese Einstellungen interaktiv vornehmen oder die eigenen
|
Für Neuinstallationen diese Einstellungen interaktiv vornehmen oder die eigenen
|
||||||
Daten gemäß Migration sicher übernehmen. Der Alias allein setzt kein Kontextlimit.
|
Daten gemäß Migration sicher übernehmen. Der Alias allein setzt kein Kontextlimit.
|
||||||
@@ -83,6 +89,7 @@ Maintenance aktivieren:
|
|||||||
systemctl --user enable --now \
|
systemctl --user enable --now \
|
||||||
hermes-stack-hermes-update.timer \
|
hermes-stack-hermes-update.timer \
|
||||||
hermes-stack-health.timer \
|
hermes-stack-health.timer \
|
||||||
|
hermes-stack-llama-watchdog.timer \
|
||||||
hermes-stack-signal-health.timer \
|
hermes-stack-signal-health.timer \
|
||||||
hermes-stack-signal-update-check.timer
|
hermes-stack-signal-update-check.timer
|
||||||
```
|
```
|
||||||
@@ -116,12 +123,29 @@ Abschließend:
|
|||||||
Ohne Optionen installiert das Skript nur die versionierten Dateien und legt
|
Ohne Optionen installiert das Skript nur die versionierten Dateien und legt
|
||||||
fehlende Beispielkonfigurationen an. Bestehende lokale Konfigurationen werden
|
fehlende Beispielkonfigurationen an. Bestehende lokale Konfigurationen werden
|
||||||
nicht überschrieben.
|
nicht überschrieben.
|
||||||
|
`hermes-stack-model` wird über `~/.local/bin` ohne Pfadangabe aufrufbar. Der
|
||||||
|
separate Helfer `scripts/prepare-workstation.py` stellt auf Wunsch die fixierten
|
||||||
|
Modelldateien bereit und übernimmt gezielt die aktuellen Hermes-Einstellungen.
|
||||||
Ersetzte Wrapper und Units werden vorher gesichert. Eine vorhandene
|
Ersetzte Wrapper und Units werden vorher gesichert. Eine vorhandene
|
||||||
hermes-gateway.service wird nicht neu erzeugt. Ein Modell wird erst beim echten
|
hermes-gateway.service wird nicht neu erzeugt. Ein Modell wird erst beim echten
|
||||||
Start des llama-Dienstes geladen, nicht bei der Konfigurationsprüfung.
|
Start des llama-Dienstes geladen, nicht bei der Konfigurationsprüfung.
|
||||||
|
|
||||||
## RTX 3080 und Qwen
|
## RTX 3080 und Qwen
|
||||||
|
|
||||||
|
Die aktuellen Schutzmaßnahmen gegen endlose Modellantworten und die explizite
|
||||||
|
Umschaltung zwischen Routine- und großem Modell stehen in
|
||||||
|
[INFERENCE_LIMITS.md](docs/INFERENCE_LIMITS.md).
|
||||||
|
Hermes verwendet zum aktiven Modell passend 64K Kontext für `routine` (9B),
|
||||||
|
`fallback` (4B) und `large` (27B). Der Modellschalter passt auch die
|
||||||
|
Kompressionsschwelle an: 24.000 Tokens.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hermes-stack-model status
|
||||||
|
hermes-stack-model routine
|
||||||
|
hermes-stack-model fallback # bisheriges 4B-/64K-Setup
|
||||||
|
hermes-stack-model large
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
~/.local/libexec/hermes-stack-llama-run --check
|
~/.local/libexec/hermes-stack-llama-run --check
|
||||||
~/.local/libexec/hermes-stack-llama-profile list
|
~/.local/libexec/hermes-stack-llama-profile list
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ HERMES_BRANCH="main"
|
|||||||
GATEWAY_SERVICE="hermes-gateway.service"
|
GATEWAY_SERVICE="hermes-gateway.service"
|
||||||
LLAMA_SERVICE="llama-qwen.service"
|
LLAMA_SERVICE="llama-qwen.service"
|
||||||
LLAMA_URL="http://127.0.0.1:8080"
|
LLAMA_URL="http://127.0.0.1:8080"
|
||||||
|
# Independent slot watchdog. Active requests only; updates hold the same lock.
|
||||||
|
LLAMA_MAX_REQUEST_SECONDS=600
|
||||||
|
LLAMA_MAX_OUTPUT_TOKENS=2048
|
||||||
# 1: inaktiven Dienst wieder starten. HTTP-Timeouts allein starten nie neu.
|
# 1: inaktiven Dienst wieder starten. HTTP-Timeouts allein starten nie neu.
|
||||||
# Standard 0 verhindert unerwünschte Modellstarts während Wartung/Tuning.
|
# Standard 0 verhindert unerwünschte Modellstarts während Wartung/Tuning.
|
||||||
LLAMA_HEALTH_AUTORESTART=0
|
LLAMA_HEALTH_AUTORESTART=0
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ LLAMA_ALIAS="qwen38-27b-local"
|
|||||||
LLAMA_MMPROJ=""
|
LLAMA_MMPROJ=""
|
||||||
LLAMA_HOST="127.0.0.1"
|
LLAMA_HOST="127.0.0.1"
|
||||||
LLAMA_PORT="8080"
|
LLAMA_PORT="8080"
|
||||||
|
LLAMA_MAX_OUTPUT_TOKENS=2048
|
||||||
|
|
||||||
# Standard für eine gemeinsam mit dem Desktop genutzte GPU: 64K / 4096 MiB.
|
# Standard für eine gemeinsam mit dem Desktop genutzte GPU: 64K / 4096 MiB.
|
||||||
# Fit ist ein Planungsziel; die freie Reserve auf der Zielmaschine messen.
|
# Fit ist ein Planungsziel; die freie Reserve auf der Zielmaschine messen.
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"llama_cpp_commit": "d81235049384534c167caea52b85a694f6103d14",
|
||||||
|
"models": {
|
||||||
|
"routine": {
|
||||||
|
"repo": "unsloth/Qwen3.5-9B-GGUF",
|
||||||
|
"revision": "3885219b6810b007914f3a7950a8d1b469d598a5",
|
||||||
|
"directory": "qwen35-9b/3885219b6810b007914f3a7950a8d1b469d598a5",
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"name": "Qwen3.5-9B-IQ4_XS.gguf",
|
||||||
|
"size": 5168653536,
|
||||||
|
"sha256": "7e918aeca06c52bcb528ea6b04b4ec957e75ee8c0a73138854c0dfcf371ea429"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mmproj-F16.gguf",
|
||||||
|
"sha256": "f70dc3509053962b0d0d3ee8a7eacebf5d60aa560cad78254ae8698516ae029f",
|
||||||
|
"size": 918166080
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"large": {
|
||||||
|
"repo": "ggml-org/Qwen3.8-27B-GGUF",
|
||||||
|
"revision": "0669b98607d47046c7c2b3f801011d54a08cfccf",
|
||||||
|
"directory": "qwen38-27b/0669b98607d47046c7c2b3f801011d54a08cfccf",
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"name": "Qwen3.8-27B-Q4_K_M.gguf",
|
||||||
|
"sha256": "31629f53165ab6a7dad8c9847dcfd1fdf55829dac1e6e748f4a68581b0033d34",
|
||||||
|
"size": 18973870432
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mmproj-Qwen3.8-27B-Q8_0.gguf",
|
||||||
|
"sha256": "2e968a6af97ce35d8971890b257b9b7edabf20ad91450501fa53162a19ee33eb",
|
||||||
|
"size": 629247008
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"compression": {
|
||||||
|
"repo": "ggml-org/Qwen3-1.7B-GGUF",
|
||||||
|
"revision": "daeb8e2d528a760970442092f6bf1e55c3b659eb",
|
||||||
|
"directory": "qwen3-1.7b-compression",
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"name": "Qwen3-1.7B-Q4_K_M.gguf",
|
||||||
|
"sha256": "d2387ca2dbfee2ffabce7120d3770dadca0b293052bc2f0e138fdc940d9bc7b5",
|
||||||
|
"size": 1282439264
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fallback": {
|
||||||
|
"repo": "unsloth/Qwen3.5-4B-GGUF",
|
||||||
|
"revision": "e87f176479d0855a907a41277aca2f8ee7a09523",
|
||||||
|
"directory": "qwen35-4b/e87f176479d0855a907a41277aca2f8ee7a09523",
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"name": "Qwen3.5-4B-Q4_K_M.gguf",
|
||||||
|
"sha256": "00fe7986ff5f6b463e62455821146049db6f9313603938a70800d1fb69ef11a4",
|
||||||
|
"size": 2740937888
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mmproj-F16.gguf",
|
||||||
|
"sha256": "cd88edcf8d031894960bb0c9c5b9b7e1fea6ebee02b9f7ce925a00d12891f864",
|
||||||
|
"size": 672423616
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# Lokale Inferenz begrenzen
|
||||||
|
|
||||||
|
Die Workstation verwendet `hermes-local` als stabilen API-Modellnamen. Das
|
||||||
|
Routineprofil lädt Qwen3.5-9B IQ4_XS (Unsloth-Revision
|
||||||
|
`3885219b6810b007914f3a7950a8d1b469d598a5`), das große Profil weiterhin das
|
||||||
|
vorhandene Qwen3.8-27B. Die tatsächliche Auswahl steht in
|
||||||
|
`~/.config/hermes-stack-maintenance/llama.env`.
|
||||||
|
|
||||||
|
`hermes-stack-model routine`, `fallback` bzw. `large` schaltet explizit um
|
||||||
|
und beendet dabei laufende Gateway-Anfragen. Beide Dateien bleiben erhalten.
|
||||||
|
Es wird nur ein Hauptmodell gleichzeitig geladen. Die lokalen profilspezifischen
|
||||||
|
Konfigurationen `llama-routine.env` und `llama-large.env` enthalten absolute
|
||||||
|
Modellpfade. `scripts/prepare-workstation.py` prüft bzw. lädt die fest versionierten
|
||||||
|
Modelldateien und erzeugt fehlende Profile. Die vollständige Neuinstallation ist
|
||||||
|
in [NEW_WORKSTATION.md](NEW_WORKSTATION.md) beschrieben. Der normale Installer
|
||||||
|
allein lädt keine Gewichte und erzeugt diese Profile nicht.
|
||||||
|
|
||||||
|
Die alten Aliasse `qwen38-27b-local` und `qwen36-27b-local` bleiben als
|
||||||
|
Kompatibilitätsnamen für bestehende Sitzungen erreichbar. Sie bezeichnen dann
|
||||||
|
ebenfalls das aktiv ausgewählte Modell; die tatsächliche Modelldatei zeigt
|
||||||
|
`/props.model_path`. Neue Sitzungen und entbundene Cronjobs verwenden `hermes-local`.
|
||||||
|
|
||||||
|
## Kontextgröße und Kompression
|
||||||
|
|
||||||
|
Das Routineprofil verwendet `--ctx-size 65536`, das bisherige 4B-Profil
|
||||||
|
(`fallback`) und das große Profil jeweils 65536. Der Modellschalter setzt
|
||||||
|
`model.context_length` und `compression.threshold_tokens` zusammen mit dem
|
||||||
|
Modell: 65536/24000 für alle drei Profile. Er sichert
|
||||||
|
beide Konfigurationen und stellt bei einem Wechsel-Fehler beide wieder her.
|
||||||
|
Legacy-Profile ohne `LLAMA_CONTEXT_TOKENS` werden als 64K behandelt; eigene
|
||||||
|
Profile müssen diese Metadaten passend zu `--ctx-size` setzen.
|
||||||
|
|
||||||
|
Die CPU-Kompression bleibt bei `threshold: 0.5` und der absoluten Schwelle
|
||||||
|
von 24.000 Tokens. `target_ratio: 0.2`,
|
||||||
|
Schutz der ersten drei und letzten zehn Nachrichten sowie die bestehenden
|
||||||
|
Zeitbudgets bleiben erhalten. Die Schwelle ist ein Auslöser, keine harte
|
||||||
|
Obergrenze für einzelne Nachrichten oder Werkzeugergebnisse.
|
||||||
|
|
||||||
|
Für eine bestehende Installation genügt nach Abschluss laufender Aufträge:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hermes-stack-model routine # 9B, 64K
|
||||||
|
hermes-stack-model fallback # 4B, 64K inklusive Kompressionsschwelle
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Modellschalter startet Hauptmodell und Gateway kontrolliert neu. Bereits
|
||||||
|
laufende Anfragen werden beendet; CPU-Kompression läuft unverändert weiter.
|
||||||
|
Die aktuellen Messungen und der Rückweg stehen in [QWEN9B_TRIAL.md](QWEN9B_TRIAL.md).
|
||||||
|
|
||||||
|
## Drei Schutzebenen
|
||||||
|
|
||||||
|
- Der Runner setzt `--n-predict 2048` als Serverstandard. Das ist kein harter
|
||||||
|
Schutz gegen abweichende Clientparameter.
|
||||||
|
- In Hermes bekommt der lokale Custom-Provider `extra_body.max_tokens: 2048`,
|
||||||
|
`t_max_predict_ms: 540000` und ein Request-Timeout von 600 Sekunden. Der native
|
||||||
|
Generierungszeitgeber von llama.cpp prüft nur bei Zeilenumbrüchen und ersetzt
|
||||||
|
deshalb keine unabhängige Überwachung. `agent.run_budget_seconds: 1800` begrenzt
|
||||||
|
einen ganzen Auftrag einschließlich mehrerer Modellaufrufe und Wiederholungen.
|
||||||
|
- `hermes-stack-llama-watchdog.timer` prüft alle 15 Sekunden `/slots`. Bleibt
|
||||||
|
dieselbe Anfrage 600 Sekunden beobachtet aktiv oder überschreitet sie 2048
|
||||||
|
Tokens, startet die Überwachung den Hauptmodellserver neu. Dieses letzte
|
||||||
|
Auffangnetz hat eine Verzögerung von bis zu einem Prüfintervall nach erstmaliger
|
||||||
|
Beobachtung. Die Tokenzahl kann zwischen zwei Prüfungen überschritten werden.
|
||||||
|
|
||||||
|
Der Watchdog startet keine absichtlich gestoppten Dienste. Einzelne fehlgeschlagene
|
||||||
|
Proben führen nicht zum Neustart; erst 600 Sekunden zusammenhängende Fehler bei
|
||||||
|
demselben aktiven Server. Wartung, Modellwechsel und Updater verwenden dieselbe
|
||||||
|
Sperre. Neustarts haben mindestens 600 Sekunden Abstand. Ein Bootwechsel setzt
|
||||||
|
monotone Zeitstempel zurück. Der Status speichert keine Prompts oder Antworten;
|
||||||
|
Diagnosen stehen ausschließlich im lokalen Journal, ohne Chatbenachrichtigung.
|
||||||
|
|
||||||
|
Die Grenzen stehen in `config.env` (Watchdog), `llama.env` (Serverstandard) und
|
||||||
|
`~/.hermes/config.yaml` (Hermes). Bei Änderungen diese Werte gemeinsam pflegen.
|
||||||
|
Grenzen gelten pro Hauptmodell; das CPU-Kompressionsmodell bleibt getrennt.
|
||||||
|
|
||||||
|
## Prüfung und Rücknahme
|
||||||
|
|
||||||
|
```
|
||||||
|
systemctl --user status hermes-stack-llama-watchdog.timer
|
||||||
|
journalctl --user -u hermes-stack-llama-watchdog.service
|
||||||
|
cat ~/.local/state/hermes-stack-maintenance/llama-watchdog.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Bereitstellung sichert vorherige Konfigurationen unter
|
||||||
|
`~/.local/state/hermes-stack-maintenance/inference-limits-*`. Ein Rückwechsel
|
||||||
|
auf `large` behält die Schutzmaßnahmen bei. Zum vollständigen Rückbau Timer
|
||||||
|
deaktivieren, die gesicherten Konfigurationen wiederherstellen und Modellserver
|
||||||
|
sowie Gateway neu starten. Cronjob-Modellbindungen sind über `hermes cron edit
|
||||||
|
JOB_ID --model MODELL --provider custom` wiederherstellbar.
|
||||||
|
|
||||||
|
Quellen: [llama.cpp Serverparameter](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md),
|
||||||
|
[Qwen3.5-9B](https://huggingface.co/Qwen/Qwen3.5-9B),
|
||||||
|
[verwendete GGUF-Dateien](https://huggingface.co/unsloth/Qwen3.5-9B-GGUF/tree/3885219b6810b007914f3a7950a8d1b469d598a5).
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
# Migration auf eine neue Arch-Linux-Maschine
|
# Migration auf eine neue Arch-Linux-Maschine
|
||||||
|
|
||||||
|
Für den aktuellen Routine-/Large-Betrieb samt Limits zuerst
|
||||||
|
[NEW_WORKSTATION.md](NEW_WORKSTATION.md) verwenden. Die folgenden Abschnitte
|
||||||
|
ergänzen die Übernahme persönlicher Daten und die ältere manuelle Modellkonfiguration.
|
||||||
|
|
||||||
Das Gitea-Repository reproduziert Programme, Units und Wartungslogik. Persönliche
|
Das Gitea-Repository reproduziert Programme, Units und Wartungslogik. Persönliche
|
||||||
Hermes-Daten, Secrets, Signal-Geräteidentität und Modellgewichte werden separat
|
Hermes-Daten, Secrets, Signal-Geräteidentität und Modellgewichte werden separat
|
||||||
übertragen.
|
übertragen.
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
# Aktuelles Workstation-Setup neu installieren
|
||||||
|
|
||||||
|
Stand: 27.10.2026. Diese Anleitung stellt den aktuellen lokalen Modellbetrieb
|
||||||
|
wieder her: Qwen3.5-9B mit 64K für den Alltag als Erprobung, Qwen3.5-4B mit 64K als
|
||||||
|
Rückfallprofil, Qwen3.8-27B als explizite Alternative und Qwen3-1.7B für
|
||||||
|
Kompression auf der CPU sowie Antwort- und Zeitlimits.
|
||||||
|
Die Modelle werden nacheinander betrieben; der Kompressionsserver läuft separat.
|
||||||
|
|
||||||
|
## 1. Voraussetzungen und Deployment
|
||||||
|
|
||||||
|
Ziel ist Arch Linux mit systemd-User-Sitzung, einer funktionierenden NVIDIA-/CUDA-
|
||||||
|
Installation und ausreichend RAM und VRAM. Die kurze Abnahme erfolgte auf einer
|
||||||
|
RTX 3080 mit 10 GB VRAM und 64 GB RAM. Der Installer richtet keine Grafiktreiber
|
||||||
|
ein. Für andere GPUs muss der CUDA-Build angepasst und die Speicherbelegung
|
||||||
|
neu geprüft werden. Das Routineprofil erzwingt vollständigen GPU-Offload.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://gitea.jr-it-services.de/johannes/hermes-arch-stack.git
|
||||||
|
cd hermes-arch-stack
|
||||||
|
./install.sh --install-packages --install-hermes
|
||||||
|
```
|
||||||
|
|
||||||
|
Für einen CUDA-Quellbuild des hier geprüften llama.cpp-Standes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo pacman -Syu --needed base-devel cmake ninja cuda openssl
|
||||||
|
./scripts/install-llama-cpp.sh \
|
||||||
|
--ref d81235049384534c167caea52b85a694f6103d14 --cuda-arch 86
|
||||||
|
```
|
||||||
|
|
||||||
|
`86` ist für die RTX 3080, auf anderer Hardware die passende Architektur setzen.
|
||||||
|
Alternativ eine vorhandene kompatible `llama-server`-Binary verwenden; später
|
||||||
|
beim Vorbereiten `--server-bin /usr/bin/llama-server` ergänzen. Der Quellbuild
|
||||||
|
setzt einen neuen Compiler/CUDA-Stand voraus und wurde hier nicht erneut gebaut.
|
||||||
|
|
||||||
|
Das Deployment installiert `hermes-stack-model` nach `~/.local/bin`. Falls dieses
|
||||||
|
Verzeichnis in einer neuen Shell noch nicht im PATH steht, einmal in die eigene
|
||||||
|
Shell-Konfiguration aufnehmen und eine neue Shell starten:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Eigene Hermes-Daten und Plattformen
|
||||||
|
|
||||||
|
Bei einer neuen Hermes-Installation zuerst `hermes setup` ausführen. Für die
|
||||||
|
Übernahme der vorhandenen Chats, Skills, Plugins, Cronjobs und Zugangsdaten
|
||||||
|
stattdessen die privaten Daten gemäß [MIGRATION.md](MIGRATION.md) wiederherstellen.
|
||||||
|
Das Repository enthält diese Daten nicht. Die gesamte persönliche Konfiguration
|
||||||
|
kann aus dem Quellcode allein nicht wiederhergestellt werden.
|
||||||
|
|
||||||
|
Vorbereitung auf der alten Maschine: Gateway und Maintenance-Timer kontrolliert
|
||||||
|
anhalten und die privaten Hermes-Daten, `~/.config/hermes-stack-maintenance/`,
|
||||||
|
gegebenenfalls `~/HermesWorkspace/` und die Signal-Geräteidentität verschlüsselt
|
||||||
|
sichern. Auch private lokale Plugins und eigene Skills berücksichtigen.
|
||||||
|
Signal auf dem neuen Rechner separat verknüpfen oder die Geräteidentität nur
|
||||||
|
auf einem Rechner gleichzeitig betreiben. Secrets gehören nicht nach Gitea.
|
||||||
|
|
||||||
|
Nach dem Import absolute Pfade auf alten Benutzernamen prüfen. Besonders
|
||||||
|
`terminal.docker_volumes` und `HERMES_REPO`/`HERMES_ENV` können noch auf das alte
|
||||||
|
Home zeigen. Private Konfigurationsdateien gezielt bearbeiten und nicht blind
|
||||||
|
alte systemd-Units samt Drop-ins über die neue Installation kopieren.
|
||||||
|
|
||||||
|
Wenn das bisherige Docker-Terminal übernommen wird, Docker installieren und
|
||||||
|
starten, dem Benutzer die erforderlichen Docker-Rechte geben und neu anmelden.
|
||||||
|
Das eigene Arbeitsverzeichnis anlegen, das Netzwerk `hermes-net` bereitstellen
|
||||||
|
und das in der persönlichen Hermes-Konfiguration angegebene Container-Image
|
||||||
|
laden. Die alten DNS-Adressen sind nur im ursprünglichen Netzwerk gültig.
|
||||||
|
Der Modellhelfer verändert Terminal-, Netzwerk-, Plugin- und Auth-Einstellungen
|
||||||
|
nicht.
|
||||||
|
|
||||||
|
## 3. Modelle und Schutzmechanismen vorbereiten
|
||||||
|
|
||||||
|
Während dieser Konfigurationsänderung muss der Gateway gestoppt sein:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl --user stop hermes-gateway.service
|
||||||
|
python3 scripts/prepare-workstation.py \
|
||||||
|
--download --models routine,large,compression,fallback \
|
||||||
|
--select routine --configure-hermes --unpin-local-cron
|
||||||
|
```
|
||||||
|
|
||||||
|
Bei noch nicht installierter Gateway-Unit ist deren Stop-Fehler unkritisch.
|
||||||
|
Die Vorbereitung startet selbst keine Dienste. Sie prüft vor Änderungen, dass
|
||||||
|
kein Gateway läuft, und teilt das Maintenance-Lock mit Updater und Watchdog.
|
||||||
|
|
||||||
|
Alle sieben Modelldateien zusammen benötigen rund **28,3 GiB**. Wer zunächst nur
|
||||||
|
das Alltagsmodell möchte, verwendet `--models routine`. Für Routine plus CPU-
|
||||||
|
Kompression `--models routine,compression`. Ohne `--download` müssen die Dateien
|
||||||
|
bereits im erwarteten Modellverzeichnis liegen; das ermöglicht eine Offline-
|
||||||
|
Übernahme. `--model-dir /anderer/absoluter/pfad` unterstützt eine andere Ablage.
|
||||||
|
|
||||||
|
`config/models.lock.json` hält für jede Datei Repository, vollständige Revision,
|
||||||
|
Dateigröße und SHA-256 fest. Downloads erfolgen ausschließlich von der fixierten
|
||||||
|
Revision, werden zunächst als `.part` gespeichert und erst nach erfolgreicher
|
||||||
|
Prüfung umbenannt. Beschädigte vorhandene Dateien werden gemeldet und behalten.
|
||||||
|
Fehlgeschlagene Downloads können erneut gestartet werden; bei einem falschen
|
||||||
|
Partial-Download muss die betroffene `.part` vor erneutem Download entfernt werden.
|
||||||
|
|
||||||
|
Der Helfer erzeugt fehlende Dateien `llama-routine.env`, `llama-fallback.env`, `llama-large.env` und
|
||||||
|
`compression.env` für den jeweiligen Benutzernamen. Bereits vorhandene Profile
|
||||||
|
und deren Tuning bleiben erhalten. `--select routine` übernimmt das vorbereitete
|
||||||
|
Routineprofil ausdrücklich als aktive `llama.env` und sichert deren alten Stand.
|
||||||
|
Soll nach einer Migration ein Modellpfad geändert werden, vorhandene Profile
|
||||||
|
vorher gezielt sichern und anpassen oder zur Neuerzeugung aus dem Konfigurations-
|
||||||
|
verzeichnis verschieben. Das gilt auch für `LLAMA_SERVER_BIN`.
|
||||||
|
|
||||||
|
`--configure-hermes` aktualisiert mit Backup die lokalen Provider-, Vision- und
|
||||||
|
optional Kompressionseinstellungen, das 30-Minuten-Auftragsbudget und die
|
||||||
|
600-Sekunden-/2048-Token-Limits. Dabei setzt er `model.context_length`
|
||||||
|
passend zur Auswahl: 65536 für alle drei Profile. Die
|
||||||
|
Kompressionsschwelle beträgt 24.000 Tokens. Fremde Provider, Terminal, Plugins und andere
|
||||||
|
Einstellungen bleiben bestehen. `--unpin-local-cron` löst nur bekannte lokale
|
||||||
|
Modellbindungen über die Hermes-CLI; eigene Cloud-Provider bleiben gebunden.
|
||||||
|
Die Option legt keine Cronjobs an. Vorhandene Cronjobs kommen aus dem privaten
|
||||||
|
Hermes-Backup. Details und Grenzen des Watchdogs: [INFERENCE_LIMITS.md](INFERENCE_LIMITS.md).
|
||||||
|
|
||||||
|
## 4. Prüfen und aktivieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.local/libexec/hermes-stack-llama-run --check
|
||||||
|
HERMES_LLAMA_CONFIG="$HOME/.config/hermes-stack-maintenance/compression.env" \
|
||||||
|
~/.local/libexec/hermes-stack-llama-run --check
|
||||||
|
hermes config check
|
||||||
|
hermes gateway install --force
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable --now llama-qwen.service
|
||||||
|
systemctl --user enable --now llama-compression.service
|
||||||
|
# Nach Einrichtung von signal-cli und dessen Konfiguration:
|
||||||
|
# systemctl --user enable --now signal-cli.service
|
||||||
|
systemctl --user enable --now hermes-gateway.service
|
||||||
|
systemctl --user enable --now \
|
||||||
|
hermes-stack-llama-watchdog.timer \
|
||||||
|
hermes-stack-health.timer \
|
||||||
|
hermes-stack-hermes-update.timer
|
||||||
|
```
|
||||||
|
|
||||||
|
Bei einer Vorbereitung ohne Kompressionsmodell dessen Prüfung und Dienst
|
||||||
|
weglassen. Signal-Wartungstimer erst nach der Signal-Einrichtung aktivieren.
|
||||||
|
Ein lokaler Modellserver kann beim Laden kurz HTTP 503 liefern.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsS http://127.0.0.1:8080/health
|
||||||
|
hermes-stack-model status
|
||||||
|
systemctl --user status hermes-stack-llama-watchdog.timer
|
||||||
|
~/.local/libexec/hermes-stack-doctor
|
||||||
|
```
|
||||||
|
|
||||||
|
Danach eine deutsche Textantwort, ein Bild und einen Werkzeugaufruf in Hermes
|
||||||
|
prüfen. Messungen zur aktuellen 9B-Erprobung stehen in
|
||||||
|
[QWEN9B_TRIAL.md](QWEN9B_TRIAL.md). Der historische 4B-Test erreichte bei
|
||||||
|
kurzen Textanfragen etwa 130–136 Tokens/s; das ist kein Benchmark für lange Kontexte oder Bildaufgaben.
|
||||||
|
Eine erzwungene lange Antwort endete bei genau 2048 Tokens, und ein reiner
|
||||||
|
Tool-Call wurde korrekt als JSON erzeugt. Ein zehnminütiger echter Hänger wurde
|
||||||
|
nicht absichtlich erzeugt; dessen Watchdog-Recovery ist automatisiert simuliert.
|
||||||
|
|
||||||
|
## Alltag und Rückweg
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hermes-stack-model # aktives Modell anzeigen
|
||||||
|
hermes-stack-model routine # 9B/64K-Erprobung
|
||||||
|
hermes-stack-model fallback # bisheriges 4B/64K-Profil
|
||||||
|
hermes-stack-model large # größeres Modell für bewusste Einzelnutzung
|
||||||
|
```
|
||||||
|
|
||||||
|
Ein Wechsel stoppt laufende Gateway-Anfragen, prüft die Kandidatenkonfiguration,
|
||||||
|
sichert `llama.env` und die Hermes-Konfiguration, setzt Kontext und
|
||||||
|
Kompressionsschwelle passend, wartet nach dem Modellstart auf `/health` und startet den
|
||||||
|
Gateway wieder. Bei Fehlern wird die vorherige Konfiguration zurückkopiert und
|
||||||
|
die Wiederaufnahme der Dienste versucht; Journal und Dienststatus kontrollieren.
|
||||||
|
Alle Hauptmodelle verwenden den stabilen API-Alias `hermes-local` und dieselben Limits.
|
||||||
|
|
||||||
|
Automatischer Wechsel zwischen den Modellen findet nicht statt. Das große Modell
|
||||||
|
bleibt auf der 10-GB-Karte langsam. Für Betrieb ohne Anmeldung optional
|
||||||
|
`sudo loginctl enable-linger "$USER"` setzen. Vollständige Rücknahme und Sicherungs-
|
||||||
|
orte stehen in [INFERENCE_LIMITS.md](INFERENCE_LIMITS.md).
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
# Qwen3.5-9B auf der RTX 3080 erproben
|
||||||
|
|
||||||
|
Stand: 27.10.2026. Ziel ist bessere Alltagsqualität bei vollständigem GPU-Offload
|
||||||
|
des Sprachmodells und weiter nutzbarem Hyprland-/Browser-Desktop. Die Freigabe
|
||||||
|
ist eine Erprobung, keine Langzeit-Stabilitätszusage oder Qualitätsbewertung.
|
||||||
|
|
||||||
|
## Profile und Schutzgrenzen
|
||||||
|
|
||||||
|
- `routine`: Qwen3.5-9B IQ4_XS, 65536 Kontexttokens, Q4_0-K/V-Cache,
|
||||||
|
vollständiger GPU-Offload (`--n-gpu-layers 99 --fit off`), Batch 512,
|
||||||
|
Microbatch 128. Der Bildprojektor läuft mit `--no-mmproj-offload` auf der CPU.
|
||||||
|
- `fallback`: das bisherige Qwen3.5-4B Q4_K_M mit 65536 Kontexttokens und
|
||||||
|
Q8_0-K/V-Cache. Auf Archwork wurde die aktive Ausgangskonfiguration unverändert
|
||||||
|
als `llama-fallback.env` übernommen. Eine Neuinstallation erzeugt sie aus dem Manifest.
|
||||||
|
- `large`: bisheriges Qwen3.8-27B-Profil für bewusste Einzelnutzung.
|
||||||
|
|
||||||
|
Die Gewichte und der Bildprojektor sind in `config/models.lock.json` mit fester
|
||||||
|
Hugging-Face-Revision, Dateigröße und SHA-256 fixiert. Der Projektor bleibt erhalten;
|
||||||
|
die Bildverarbeitung benötigt CPU-Zeit. IQ4_XS und der Q4_0-Kontextcache sparen
|
||||||
|
Speicher gegenüber Q4_K_M/Q8_0 und können die Antwortqualität beeinflussen.
|
||||||
|
|
||||||
|
Hermes und Server bleiben bei 65536 Kontexttokens; die CPU-Kompression beginnt
|
||||||
|
spätestens bei 24000 Tokens. Antwortlimit 2048 Tokens, Request-Watchdog und
|
||||||
|
bestehende Zeitbudgets bleiben bestehen. Die jetzige Hermes-Version verlangt
|
||||||
|
mindestens 64000 Kontexttokens: Eine 16K-Konfiguration wird beim Agentstart
|
||||||
|
abgelehnt, auch wenn der Modellserver selbst antwortet. Das wurde lokal geprüft.
|
||||||
|
Der Modellschalter lehnt bei vorhandener Hermes-Konfiguration Metadaten unter
|
||||||
|
dieser Grenze ab und stellt bei Fehlern die vorherige Modell- und Hermes-Konfiguration
|
||||||
|
wieder her.
|
||||||
|
|
||||||
|
## Messungen und verworfene Konfigurationen
|
||||||
|
|
||||||
|
Alle Speicherangaben sind MiB laut `nvidia-smi`; die freie Menge hängt von den
|
||||||
|
gleichzeitig laufenden Desktop-Anwendungen ab und ist nicht reserviert.
|
||||||
|
|
||||||
|
| Konfiguration | Beobachtung |
|
||||||
|
|---|---|
|
||||||
|
| 4B Q4_K_M, 64K, GPU-Projektor | Vergleichstest: mindestens 2288 MiB frei; kurze Textgenerierung ca. 136 Tokens/s |
|
||||||
|
| 9B Q4_K_M, 16K, GPU-Projektor, Standardpuffer | Nur ca. 900 MiB frei nach Start; verworfen |
|
||||||
|
| 9B Q4_K_M, 16K, CPU-Projektor, kleinere Puffer | Mindestens 1760 MiB frei; ca. 94 Tokens/s, Screenshot ca. 39 s; direktes API funktioniert, Hermes lehnt 16K ab |
|
||||||
|
| 9B Q4_K_M, 64K, CPU-Projektor, Q4_0-Cache | Nur ca. 1439 MiB frei nach Start; zugunsten kleinerer Gewichtsdatei verworfen |
|
||||||
|
| 9B IQ4_XS, 64K, CPU-Projektor, Q4_0-Cache, Batch 512/Microbatch 128 | Nach Start 1988 MiB frei; im Text-/Bild-/Werkzeug-/58315-Token-Test mindestens **1627 MiB frei**; ca. 73–101 Tokens/s |
|
||||||
|
|
||||||
|
Die ersten API-Tests prüfen deutsche Textantwort, Screenshot-Erkennung,
|
||||||
|
strukturierten Werkzeugaufruf (`add`, Argumente 17/25) und knapp 11000 Tokens
|
||||||
|
Eingabekontext. Der Werkzeugaufruf wird nur als JSON geprüft und nicht ausgeführt.
|
||||||
|
Die kurzen Beispiele belegen Funktionsfähigkeit, keinen Qualitätsgewinn gegenüber
|
||||||
|
4B. Die ursprüngliche Modellbewertung beruhte auf Hersteller-Benchmarks und muss
|
||||||
|
sich in Johannes' tatsächlichen Aufgaben bewähren.
|
||||||
|
|
||||||
|
Das endgültige IQ4_XS-Profil beantwortete die 58315-Token-Anfrage ohne
|
||||||
|
Abschneiden in ca. 22,4 Sekunden. Der strukturierte Werkzeugaufruf enthielt
|
||||||
|
korrekt `{"a":17,"b":25}`. Ein echter lokaler Hermes-Lauf mit 65536 Kontext,
|
||||||
|
20243 Zeichen Systemprompt und 19 Signal-Werkzeugbeschreibungen antwortete
|
||||||
|
in ca. 5,2 Sekunden mit „Modelltest erfolgreich“. Werkzeugausführung war für
|
||||||
|
diesen Lauf deaktiviert; es wurden keine Nachrichten an Kontakte versendet.
|
||||||
|
|
||||||
|
**Bildgrenze:** Der Screenshot brauchte mit dem CPU-Projektor ca. 38,9 Sekunden.
|
||||||
|
CPU-Modell und die Angabe 6.0G/10G wurden erkannt, im freien Text wurde aber die
|
||||||
|
Leistungsangabe 106 W fälschlich als „106 GB“ wiedergegeben. Die Bildverarbeitung
|
||||||
|
funktioniert technisch; fehlerfreie OCR oder ein Qualitätsgewinn gegenüber 4B
|
||||||
|
sind damit nicht belegt. Eine gezielte JSON-Nachfrage nach belegtem und gesamtem
|
||||||
|
VRAM lieferte anschließend korrekt `6.0`/`10.0` in ca. 38,9 Sekunden.
|
||||||
|
Die freien 1627 MiB sind ein gemessenes Minimum während
|
||||||
|
dieses kurzen Tests, keine dauerhafte Zusicherung bei zusätzlichen Anwendungen.
|
||||||
|
|
||||||
|
Repository-Prüfungen: 55 Python-Tests und Bash-Syntaxprüfungen bestanden;
|
||||||
|
Shellcheck ist auf der Workstation nicht installiert. Hauptmodell, Gateway und
|
||||||
|
CPU-Kompression waren abschließend `active/running`, jeweils ohne automatische
|
||||||
|
Neustarts. Im zugänglichen Kerneljournal seit Versuchsbeginn keine Treffer für
|
||||||
|
NVIDIA-/Xid-/Speicherfehler. Kein absichtlich erzeugter
|
||||||
|
UI-Absturz, keine Neustart- oder Langzeitabnahme.
|
||||||
|
|
||||||
|
## Bestehende 4B-Installation übernehmen
|
||||||
|
|
||||||
|
Die Vorbereitungshilfe überschreibt vorhandene Profil-Dateien absichtlich nicht.
|
||||||
|
Vor einer manuellen Migration deshalb die aktive `llama.env`, das bisherige
|
||||||
|
`llama-routine.env` und die Hermes-YAML separat sichern, die bisherige aktive
|
||||||
|
4B-Konfiguration als `llama-fallback.env` erhalten und das alte Routineprofil
|
||||||
|
in die Sicherung verschieben. Erst dann `prepare-workstation.py --download
|
||||||
|
--models routine` ausführen (gegebenenfalls `--server-bin /usr/bin/llama-server`).
|
||||||
|
Das erzeugt die neue Routinekonfiguration, ohne bereits umzuschalten.
|
||||||
|
Nach Installation des aktuellen Umschalters erfolgt die Aktivierung ausdrücklich
|
||||||
|
mit `hermes-stack-model routine`. Kein Modellwechsel durch `install.sh` allein.
|
||||||
|
|
||||||
|
## Umschalten und zurückrollen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hermes-stack-model status
|
||||||
|
hermes-stack-model fallback # ursprüngliches 4B-/64K-Setup wieder aktivieren
|
||||||
|
hermes-stack-model routine # 9B-Erprobung wieder aktivieren
|
||||||
|
```
|
||||||
|
|
||||||
|
Ein Wechsel beendet laufende Gateway-Anfragen. Der Umschalter sichert beide
|
||||||
|
Konfigurationen, übernimmt das Profil samt Kontext-/Kompressionsgrenze, wartet
|
||||||
|
auf den Modell-Healthcheck und startet den Gateway. Bei Startfehlern versucht er
|
||||||
|
die Rückkehr zum vorherigen Profil; danach Dienststatus prüfen.
|
||||||
|
|
||||||
|
Der Rückweg auf Archwork wurde während der Erprobung tatsächlich ausgeführt:
|
||||||
|
`llama.env` war danach bytegleich zum Ausgangsstand, die gesamte Hermes-YAML
|
||||||
|
semantisch identisch; ein lokaler Hermes-Lauf mit 19 Signal-Werkzeugbeschreibungen
|
||||||
|
antwortete mit „Modelltest erfolgreich.“. Es wurde keine Nachricht an einen
|
||||||
|
Kontakt gesendet.
|
||||||
|
|
||||||
|
Die vollständige lokale Ausgangssicherung liegt auf Archwork unter
|
||||||
|
`~/.local/state/hermes-stack-maintenance/qwen9b-20261027/` (private Hermes-YAML,
|
||||||
|
aktives Profil, altes Routineprofil, vorheriger Umschalter und Messberichte).
|
||||||
|
Diese privaten Dateien werden nicht eingecheckt. Der normale Rückweg braucht
|
||||||
|
keine Wiederherstellung der gesamten Sicherung, sondern nur den Fallback-Befehl.
|
||||||
|
|
||||||
|
Bei erneutem UI-Stottern, Grafikfehlern oder zu geringer Reserve zuerst
|
||||||
|
`hermes-stack-model fallback` verwenden. Bei einem dauerhaft unbedienbaren
|
||||||
|
Desktop lässt sich derselbe Befehl aus einer Textkonsole ausführen. Ein
|
||||||
|
automatischer Wechsel wegen neuer UI-Probleme ist nicht eingerichtet.
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
# Qwen auf RTX 3080 10 GB / Hyprland
|
# Qwen auf RTX 3080 10 GB / Hyprland
|
||||||
|
|
||||||
|
Aktuelle 9B-Erprobung mit 4B-Rückfallprofil: [QWEN9B_TRIAL.md](QWEN9B_TRIAL.md).
|
||||||
|
Die folgenden Fit-Profile und älteren Messungen betreffen den 27B-Betrieb.
|
||||||
|
|
||||||
Zielhardware: RTX 3080 mit 10 GiB, i9-13900KF, 64 GB RAM. Der Desktop und
|
Zielhardware: RTX 3080 mit 10 GiB, i9-13900KF, 64 GB RAM. Der Desktop und
|
||||||
llama.cpp nutzen dieselbe GPU. Das 27B-Modell in Q4_K_M passt nicht vollständig
|
llama.cpp nutzen dieselbe GPU. Das 27B-Modell in Q4_K_M passt nicht vollständig
|
||||||
in 10 GiB: CPU/RAM-Offload bleibt erforderlich. Mehr VRAM für Gewichte kann
|
in 10 GiB: CPU/RAM-Offload bleibt erforderlich. Mehr VRAM für Gewichte kann
|
||||||
|
|||||||
@@ -1,3 +1,25 @@
|
|||||||
|
# Validierung vom 2026-10-23 — Kontextabgleich
|
||||||
|
|
||||||
|
- `make test`: Syntax-/Secret-Musterprüfung und **52 Funktionstests bestanden**
|
||||||
|
(26 llama-, zehn Watchdog-, elf Workstation- und fünf Updater-Tests).
|
||||||
|
ShellCheck fehlt lokal und wurde nicht ausgeführt.
|
||||||
|
- Regression prüft Ersatz eines veralteten 256.000-Token-Werts, Abgleich mit
|
||||||
|
beiden 65.536-Token-Hauptmodellprofilen, Erhalt der 24.000-Token-
|
||||||
|
Kompressionsschwelle und unveränderten zweiten Konfigurationslauf.
|
||||||
|
- `git diff --check`: erfolgreich.
|
||||||
|
- Auf Archwork ausschließlich `model.context_length: 65536` ergänzt;
|
||||||
|
semantischer YAML-Vergleich bestätigt, dass keine andere Einstellung geändert
|
||||||
|
wurde. Private Sicherung unter
|
||||||
|
`~/.local/state/hermes-stack-maintenance/context-64k-20261023-144440/config.yaml`.
|
||||||
|
- Gateway kontrolliert neu gestartet; Hauptmodell und CPU-Kompression ohne
|
||||||
|
Neustart. `hermes config get model.context_length` liefert `65536`.
|
||||||
|
- Neue lokale Hermes-Sitzung über den Gateway antwortet korrekt mit
|
||||||
|
`HERMES_OK 323`. Seit der Aktivierung keine erneute Kontext-Fallback-Warnung
|
||||||
|
im Hermes-Fehlerprotokoll. Bestehende Plugin-/Docker-Warnungen bleiben sichtbar.
|
||||||
|
- Kein langer 64K-Dialog, keine erzwungene Kompression und kein Test des großen
|
||||||
|
Modells für diese Änderung; der Live-Nachweis gilt für die neue kurze Sitzung
|
||||||
|
mit dem aktiven Routineprofil.
|
||||||
|
|
||||||
# Validierung vom 2026-10-21
|
# Validierung vom 2026-10-21
|
||||||
|
|
||||||
- `make test`: Syntax- und Secret-Musterprüfung sowie **31 Funktionstests
|
- `make test`: Syntax- und Secret-Musterprüfung sowie **31 Funktionstests
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# Bestehende Workstation behutsam übernehmen
|
# Bestehende Workstation behutsam übernehmen
|
||||||
|
|
||||||
|
Aktueller Stand seit 27.10.2026: Erprobung von Qwen3.5-9B IQ4_XS mit 16K
|
||||||
|
Kontext als Routineprofil. Das bisherige Qwen3.5-4B-/64K-Profil bleibt mit
|
||||||
|
`hermes-stack-model fallback` erreichbar. Messungen und Rückweg:
|
||||||
|
[QWEN9B_TRIAL.md](QWEN9B_TRIAL.md). Einrichtung und Migration:
|
||||||
|
[NEW_WORKSTATION.md](NEW_WORKSTATION.md), [INFERENCE_LIMITS.md](INFERENCE_LIMITS.md).
|
||||||
|
Die nachfolgenden historischen Profilumstellungen beschreiben den früheren
|
||||||
|
27B-Betrieb und ersetzen diese aktuelle Auswahl nicht.
|
||||||
|
|
||||||
Nicht blind den Schnellstart für eine neue Maschine ausführen. Vorhandene
|
Nicht blind den Schnellstart für eine neue Maschine ausführen. Vorhandene
|
||||||
Hermes-/Signal-Konfiguration bleibt privat und wird nicht durch das Repo ersetzt.
|
Hermes-/Signal-Konfiguration bleibt privat und wird nicht durch das Repo ersetzt.
|
||||||
Das aktuelle Deployment enthält deine gezeigten Anpassungen; weitere lokale,
|
Das aktuelle Deployment enthält deine gezeigten Anpassungen; weitere lokale,
|
||||||
|
|||||||
+18
-2
@@ -9,6 +9,8 @@ INSTALL_LLAMA=0
|
|||||||
LLAMA_REF=""
|
LLAMA_REF=""
|
||||||
ENABLE_TIMERS=0
|
ENABLE_TIMERS=0
|
||||||
SKIP_BROWSER=1
|
SKIP_BROWSER=1
|
||||||
|
USER_BIN_ON_PATH=0
|
||||||
|
[[ ":$PATH:" != *":$HOME/.local/bin:"* ]] || USER_BIN_ON_PATH=1
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
sed -n '/^## Installationsoptionen/,/^## /p' "$ROOT_DIR/README.md" |
|
sed -n '/^## Installationsoptionen/,/^## /p' "$ROOT_DIR/README.md" |
|
||||||
@@ -43,7 +45,7 @@ export PATH="$HOME/.local/bin:$PATH"
|
|||||||
if ((INSTALL_PACKAGES)); then
|
if ((INSTALL_PACKAGES)); then
|
||||||
# Vollständige Aktualisierung statt partieller Arch-Aktualisierung.
|
# Vollständige Aktualisierung statt partieller Arch-Aktualisierung.
|
||||||
packages=(
|
packages=(
|
||||||
bash git curl xz jq zstd tar ripgrep ffmpeg coreutils util-linux \
|
bash git curl xz jq zstd tar ripgrep ffmpeg coreutils util-linux python python-yaml \
|
||||||
jre-openjdk-headless
|
jre-openjdk-headless
|
||||||
)
|
)
|
||||||
if ((INSTALL_LLAMA)); then
|
if ((INSTALL_LLAMA)); then
|
||||||
@@ -83,9 +85,19 @@ for file in "$ROOT_DIR"/systemd/user/*; do
|
|||||||
done
|
done
|
||||||
printf 'Vorherige Deployment-Dateien: %s\n' "$deploy_backup"
|
printf 'Vorherige Deployment-Dateien: %s\n' "$deploy_backup"
|
||||||
|
|
||||||
|
# Public command entry point; never depend on adding libexec to PATH.
|
||||||
|
mkdir -p "$HOME/.local/bin" "$deploy_backup/bin"
|
||||||
|
command_target="$HOME/.local/bin/hermes-stack-model"
|
||||||
|
if [[ -e "$command_target" || -L "$command_target" ]]; then
|
||||||
|
cp -a -- "$command_target" "$deploy_backup/bin/"
|
||||||
|
fi
|
||||||
|
ln -sfn -- ../libexec/hermes-stack-model "$command_target"
|
||||||
|
|
||||||
for file in "$ROOT_DIR"/libexec/*; do
|
for file in "$ROOT_DIR"/libexec/*; do
|
||||||
install -m 0755 "$file" "$libexec_dir/$(basename "$file")"
|
install -m 0755 "$file" "$libexec_dir/$(basename "$file")"
|
||||||
done
|
done
|
||||||
|
install -d -m 0755 "$HOME/.local/lib/hermes-stack"
|
||||||
|
install -m 0644 "$ROOT_DIR/scripts/llama_watchdog.py" "$HOME/.local/lib/hermes-stack/llama_watchdog.py"
|
||||||
|
|
||||||
for file in "$ROOT_DIR"/systemd/user/*; do
|
for file in "$ROOT_DIR"/systemd/user/*; do
|
||||||
install -m 0644 "$file" "$unit_dir/$(basename "$file")"
|
install -m 0644 "$file" "$unit_dir/$(basename "$file")"
|
||||||
@@ -121,7 +133,7 @@ systemctl --user daemon-reload
|
|||||||
if command -v hermes >/dev/null 2>&1 && [[ ! -e "$unit_dir/hermes-gateway.service" ]]; then
|
if command -v hermes >/dev/null 2>&1 && [[ ! -e "$unit_dir/hermes-gateway.service" ]]; then
|
||||||
if [[ -d "$HOME/.hermes/hermes-agent/.git" ]]; then
|
if [[ -d "$HOME/.hermes/hermes-agent/.git" ]]; then
|
||||||
hermes gateway install ||
|
hermes gateway install ||
|
||||||
printf 'Gateway-Unit konnte noch nicht erzeugt werden; nach `hermes setup` erneut ausführen.\n' >&2
|
printf 'Gateway-Unit konnte noch nicht erzeugt werden; nach hermes setup erneut ausführen.\n' >&2
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -129,9 +141,13 @@ if ((ENABLE_TIMERS)); then
|
|||||||
systemctl --user enable --now \
|
systemctl --user enable --now \
|
||||||
hermes-stack-hermes-update.timer \
|
hermes-stack-hermes-update.timer \
|
||||||
hermes-stack-health.timer \
|
hermes-stack-health.timer \
|
||||||
|
hermes-stack-llama-watchdog.timer \
|
||||||
hermes-stack-signal-health.timer \
|
hermes-stack-signal-health.timer \
|
||||||
hermes-stack-signal-update-check.timer
|
hermes-stack-signal-update-check.timer
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf '\nDeployment installiert. Nächster Schritt:\n %s\n' \
|
printf '\nDeployment installiert. Nächster Schritt:\n %s\n' \
|
||||||
"$libexec_dir/hermes-stack-doctor"
|
"$libexec_dir/hermes-stack-doctor"
|
||||||
|
if ((USER_BIN_ON_PATH == 0)); then
|
||||||
|
printf 'Für Befehle ohne Pfad ~/.local/bin in den Shell-PATH aufnehmen.\n'
|
||||||
|
fi
|
||||||
@@ -15,25 +15,25 @@ bad() { printf '✗ %s\n' "$*"; failures=$((failures + 1)); }
|
|||||||
source "$CONFIG_FILE"
|
source "$CONFIG_FILE"
|
||||||
|
|
||||||
for command in git curl jq tar zstd systemctl hermes; do
|
for command in git curl jq tar zstd systemctl hermes; do
|
||||||
command -v "$command" >/dev/null 2>&1 && ok "$command vorhanden" || bad "$command fehlt"
|
if command -v "$command" >/dev/null 2>&1; then ok "$command vorhanden"; else bad "$command fehlt"; fi
|
||||||
done
|
done
|
||||||
|
|
||||||
for script in "$HOME/.local/libexec"/hermes-stack-*; do
|
for script in "$HOME/.local/libexec"/hermes-stack-*; do
|
||||||
[[ -f "$script" ]] || continue
|
[[ -f "$script" ]] || continue
|
||||||
bash -n "$script" && ok "Syntax: ${script##*/}" || bad "Syntaxfehler: ${script##*/}"
|
if bash -n "$script"; then ok "Syntax: ${script##*/}"; else bad "Syntaxfehler: ${script##*/}"; fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ -d "$HERMES_REPO/.git" ]]; then
|
if [[ -d "$HERMES_REPO/.git" ]]; then
|
||||||
ok "Hermes-Git-Checkout vorhanden"
|
ok "Hermes-Git-Checkout vorhanden"
|
||||||
dirty="$(git -C "$HERMES_REPO" status --porcelain --untracked-files=all |
|
dirty="$(git -C "$HERMES_REPO" status --porcelain --untracked-files=all |
|
||||||
grep -vxF '?? .install_method' || true)"
|
grep -vxF '?? .install_method' || true)"
|
||||||
[[ -z "$dirty" ]] && ok "Hermes-Checkout sauber" || bad "Hermes-Checkout enthält lokale Änderungen"
|
if [[ -z "$dirty" ]]; then ok "Hermes-Checkout sauber"; else bad "Hermes-Checkout enthält lokale Änderungen"; fi
|
||||||
else
|
else
|
||||||
bad "Hermes-Git-Checkout fehlt"
|
bad "Hermes-Git-Checkout fehlt"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
systemctl --user is-active --quiet "$GATEWAY_SERVICE" && ok "Gateway aktiv" || bad "Gateway inaktiv"
|
if systemctl --user is-active --quiet "$GATEWAY_SERVICE"; then ok "Gateway aktiv"; else bad "Gateway inaktiv"; fi
|
||||||
hermes gateway status >/dev/null 2>&1 && ok "Gateway gesund" || bad "Gateway-Status fehlgeschlagen"
|
if hermes gateway status >/dev/null 2>&1; then ok "Gateway gesund"; else bad "Gateway-Status fehlgeschlagen"; fi
|
||||||
|
|
||||||
if curl --fail --silent --max-time 5 "$LLAMA_URL/health" |
|
if curl --fail --silent --max-time 5 "$LLAMA_URL/health" |
|
||||||
jq -e '.status == "ok"' >/dev/null 2>&1; then
|
jq -e '.status == "ok"' >/dev/null 2>&1; then
|
||||||
@@ -51,7 +51,7 @@ set -u
|
|||||||
if [[ -n "${SIGNAL_HTTP_URL:-}" ]]; then
|
if [[ -n "${SIGNAL_HTTP_URL:-}" ]]; then
|
||||||
status="$(curl --silent --output /dev/null --max-time 5 --write-out '%{http_code}' \
|
status="$(curl --silent --output /dev/null --max-time 5 --write-out '%{http_code}' \
|
||||||
"${SIGNAL_HTTP_URL%/}/api/v1/check" || true)"
|
"${SIGNAL_HTTP_URL%/}/api/v1/check" || true)"
|
||||||
[[ "$status" =~ ^2[0-9][0-9]$ ]] && ok "signal-cli HTTP erreichbar" || bad "signal-cli HTTP nicht erreichbar"
|
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then ok "signal-cli HTTP erreichbar"; else bad "signal-cli HTTP nicht erreichbar"; fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf '\nFehler: %d\n' "$failures"
|
printf '\nFehler: %d\n' "$failures"
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ flock -n 9 || {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090,SC1091
|
||||||
source "$config_dir/llama.env"
|
source "$config_dir/llama.env"
|
||||||
[[ -n "${LLAMA_PROFILE:-}" ]] || {
|
[[ -n "${LLAMA_PROFILE:-}" ]] || {
|
||||||
printf 'Profilbetrieb ist nicht konfiguriert.\n' >&2
|
printf 'Profilbetrieb ist nicht konfiguriert.\n' >&2
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
set -Euo pipefail
|
set -Euo pipefail
|
||||||
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance"
|
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance"
|
||||||
if [[ -r "$config_dir/config.env" ]]; then
|
if [[ -r "$config_dir/config.env" ]]; then
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090,SC1091
|
||||||
source "$config_dir/config.env"
|
source "$config_dir/config.env"
|
||||||
fi
|
fi
|
||||||
printf '\nGPU (Gesamtspeicher, belegt, frei in MiB):\n'
|
printf '\nGPU (Gesamtspeicher, belegt, frei in MiB):\n'
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ if [[ "${1:-}" != --jobs-finished || $# -ne 1 ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance"
|
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance"
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090,SC1091
|
||||||
source "$config_dir/config.env"
|
source "$config_dir/config.env"
|
||||||
|
|
||||||
exec 8>"${XDG_RUNTIME_DIR:-/tmp}/hermes-stack-hermes-update.lock"
|
exec 8>"${XDG_RUNTIME_DIR:-/tmp}/hermes-stack-hermes-update.lock"
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ while (($#)); do
|
|||||||
done
|
done
|
||||||
|
|
||||||
if [[ -n "${LLAMA_PROFILE:-}" && -r "$config_dir/llama-profile.env" ]]; then
|
if [[ -n "${LLAMA_PROFILE:-}" && -r "$config_dir/llama-profile.env" ]]; then
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090,SC1091
|
||||||
source "$config_dir/llama-profile.env"
|
source "$config_dir/llama-profile.env"
|
||||||
fi
|
fi
|
||||||
[[ -z "$profile_override" ]] || LLAMA_PROFILE="$profile_override"
|
[[ -z "$profile_override" ]] || LLAMA_PROFILE="$profile_override"
|
||||||
@@ -70,6 +70,9 @@ if [[ -n "${LLAMA_MMPROJ:-}" ]]; then
|
|||||||
fi
|
fi
|
||||||
[[ -z "${LLAMA_ALIAS:-}" ]] || model_args+=(--alias "$LLAMA_ALIAS")
|
[[ -z "${LLAMA_ALIAS:-}" ]] || model_args+=(--alias "$LLAMA_ALIAS")
|
||||||
|
|
||||||
|
# Server defaults are not a wall-clock limit; the independent slot watchdog is.
|
||||||
|
[[ "${LLAMA_MAX_OUTPUT_TOKENS:-2048}" =~ ^[1-9][0-9]*$ ]] || exit 2
|
||||||
|
|
||||||
profile_args=()
|
profile_args=()
|
||||||
if [[ -n "${LLAMA_PROFILE:-}" ]]; then
|
if [[ -n "${LLAMA_PROFILE:-}" ]]; then
|
||||||
reasoning_budget=512
|
reasoning_budget=512
|
||||||
@@ -107,6 +110,7 @@ args=(
|
|||||||
"${model_args[@]}"
|
"${model_args[@]}"
|
||||||
--host "${LLAMA_HOST:-127.0.0.1}"
|
--host "${LLAMA_HOST:-127.0.0.1}"
|
||||||
--port "${LLAMA_PORT:-8080}"
|
--port "${LLAMA_PORT:-8080}"
|
||||||
|
--n-predict "${LLAMA_MAX_OUTPUT_TOKENS:-2048}"
|
||||||
"${profile_args[@]}"
|
"${profile_args[@]}"
|
||||||
"${LLAMA_ARGS[@]}"
|
"${LLAMA_ARGS[@]}"
|
||||||
)
|
)
|
||||||
|
|||||||
Executable
+11
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
umask 077
|
||||||
|
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance"
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "$config_dir/config.env"
|
||||||
|
set +a
|
||||||
|
exec 9>"${XDG_RUNTIME_DIR:-/tmp}/hermes-stack-hermes-update.lock"
|
||||||
|
flock -n 9 || exit 0
|
||||||
|
exec python3 "$HOME/.local/lib/hermes-stack/llama_watchdog.py"
|
||||||
Executable
+90
@@ -0,0 +1,90 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Explicit model selection. Stops current gateway requests; keeps both model files.
|
||||||
|
set -Eeuo pipefail
|
||||||
|
umask 077
|
||||||
|
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance"
|
||||||
|
usage() {
|
||||||
|
printf 'Aufruf: %s [status|routine|fallback|large] (Modellwechsel beendet laufende Anfragen)\n' "$0"
|
||||||
|
}
|
||||||
|
case "${1:-}" in
|
||||||
|
''|status)
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "$config_dir/llama.env"
|
||||||
|
printf 'Aktives Modell: %s\nKonfiguration: %s/llama.env\n' "$LLAMA_MODEL" "$config_dir"
|
||||||
|
exit 0 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
routine|fallback|large) selected="$1" ;;
|
||||||
|
*) usage >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
[[ $# -eq 1 ]] || exit 2
|
||||||
|
exec 9>"${XDG_RUNTIME_DIR:-/tmp}/hermes-stack-hermes-update.lock"
|
||||||
|
flock -n 9 || { printf 'Maintenance läuft.\n' >&2; exit 1; }
|
||||||
|
runner="$(dirname -- "$(readlink -f -- "${BASH_SOURCE[0]}")")/hermes-stack-llama-run"
|
||||||
|
candidate="$config_dir/llama-$selected.env"
|
||||||
|
HERMES_LLAMA_CONFIG="$candidate" "$runner" --check
|
||||||
|
backup="$(mktemp "$config_dir/llama.env.before-model-switch.XXXXXX")"
|
||||||
|
cp -p -- "$config_dir/llama.env" "$backup"
|
||||||
|
hermes_config="${HERMES_HOME:-$HOME/.hermes}/config.yaml"
|
||||||
|
hermes_backup=""
|
||||||
|
if [[ -f "$hermes_config" ]]; then
|
||||||
|
hermes_backup="$(mktemp "$config_dir/hermes.yaml.before-model-switch.XXXXXX")"
|
||||||
|
cp -p -- "$hermes_config" "$hermes_backup"
|
||||||
|
fi
|
||||||
|
rollback() {
|
||||||
|
local rc=$?
|
||||||
|
trap - EXIT
|
||||||
|
if ((rc != 0)); then
|
||||||
|
install -m 0600 "$backup" "$config_dir/llama.env" || true
|
||||||
|
if [[ -n "$hermes_backup" ]]; then
|
||||||
|
install -m 0600 "$hermes_backup" "$hermes_config" || true
|
||||||
|
fi
|
||||||
|
systemctl --user restart llama-qwen.service || true
|
||||||
|
systemctl --user start hermes-gateway.service || true
|
||||||
|
printf 'Modellwechsel fehlgeschlagen; vorherige Konfiguration zurückkopiert. Dienststatus prüfen.\n' >&2
|
||||||
|
fi
|
||||||
|
exit "$rc"
|
||||||
|
}
|
||||||
|
trap rollback EXIT
|
||||||
|
systemctl --user stop hermes-gateway.service
|
||||||
|
install -m 0600 "$candidate" "$config_dir/llama.env"
|
||||||
|
# Keep Hermes' actual context and compression trigger paired with the model.
|
||||||
|
# Legacy profiles without metadata use the original 64K window.
|
||||||
|
LLAMA_CONTEXT_TOKENS=65536
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$candidate"
|
||||||
|
if [[ -n "$hermes_backup" ]]; then
|
||||||
|
python3 - "$hermes_config" "$LLAMA_CONTEXT_TOKENS" <<'PY'
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import yaml
|
||||||
|
path = Path(sys.argv[1])
|
||||||
|
context = int(sys.argv[2])
|
||||||
|
if context < 64000:
|
||||||
|
raise ValueError("Hermes requires at least 64000 context tokens")
|
||||||
|
config = yaml.safe_load(path.read_text())
|
||||||
|
config.setdefault("model", {})["context_length"] = context
|
||||||
|
config.setdefault("compression", {})["threshold_tokens"] = min(24000, context * 3 // 8)
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w", dir=path.parent, delete=False) as out:
|
||||||
|
yaml.safe_dump(config, out, allow_unicode=True, sort_keys=False, width=1000)
|
||||||
|
temporary = Path(out.name)
|
||||||
|
os.chmod(temporary, 0o600)
|
||||||
|
temporary.replace(path)
|
||||||
|
PY
|
||||||
|
fi
|
||||||
|
ready=0
|
||||||
|
if systemctl --user restart llama-qwen.service; then
|
||||||
|
for ((i=0; i<90; i++)); do
|
||||||
|
if curl -fsS --max-time 2 http://127.0.0.1:8080/health 2>/dev/null | jq -e '.status == "ok"' >/dev/null 2>&1; then
|
||||||
|
ready=1; break
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
if ((ready == 0)); then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
systemctl --user start hermes-gateway.service
|
||||||
|
trap - EXIT
|
||||||
|
printf 'Modellprofil %s aktiv. Vorherige Konfiguration: %s\n' "$selected" "$backup"
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Bound a busy llama.cpp slot even if the client loses its cancellation path.
|
||||||
|
|
||||||
|
Only task IDs, monotonic timestamps and service invocation IDs are persisted.
|
||||||
|
The wrapper holds the same maintenance lock as the updater and restart helper.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from urllib.request import urlopen
|
||||||
|
|
||||||
|
|
||||||
|
def observe(previous, slots, invocation, now, seconds, tokens):
|
||||||
|
old = previous if previous.get("invocation") == invocation else {}
|
||||||
|
active = {}
|
||||||
|
reasons = []
|
||||||
|
for slot in slots:
|
||||||
|
if not slot.get("is_processing"):
|
||||||
|
continue
|
||||||
|
if "id_task" not in slot:
|
||||||
|
raise ValueError("busy slot without task ID")
|
||||||
|
key = f'{slot["id"]}:{slot["id_task"]}'
|
||||||
|
started = old.get("active", {}).get(key, now)
|
||||||
|
if started > now:
|
||||||
|
started = now
|
||||||
|
active[key] = started
|
||||||
|
decoded = max((item.get("n_decoded", 0) for item in slot.get("next_token", [])), default=0)
|
||||||
|
if now - started >= seconds:
|
||||||
|
reasons.append(f"slot {slot['id']}: request exceeded {seconds}s")
|
||||||
|
if decoded > tokens:
|
||||||
|
reasons.append(f"slot {slot['id']}: output exceeded {tokens} tokens")
|
||||||
|
return {"invocation": invocation, "active": active}, reasons
|
||||||
|
|
||||||
|
|
||||||
|
def systemctl(*args):
|
||||||
|
return subprocess.run(["systemctl", "--user", *args], check=True,
|
||||||
|
capture_output=True, text=True, timeout=35).stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
service = os.environ.get("LLAMA_SERVICE", "llama-qwen.service")
|
||||||
|
url = os.environ.get("LLAMA_URL", "http://127.0.0.1:8080").rstrip("/")
|
||||||
|
seconds = int(os.environ.get("LLAMA_MAX_REQUEST_SECONDS", "600"))
|
||||||
|
tokens = int(os.environ.get("LLAMA_MAX_OUTPUT_TOKENS", "2048"))
|
||||||
|
if seconds <= 0 or tokens <= 0:
|
||||||
|
raise ValueError("watchdog limits must be positive")
|
||||||
|
state_dir = Path(os.environ.get("XDG_STATE_HOME", str(Path.home() / ".local/state"))) / "hermes-stack-maintenance"
|
||||||
|
state_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||||
|
path = state_dir / "llama-watchdog.json"
|
||||||
|
try:
|
||||||
|
previous = json.loads(path.read_text())
|
||||||
|
except (OSError, ValueError):
|
||||||
|
previous = {}
|
||||||
|
boot = Path("/proc/sys/kernel/random/boot_id").read_text().strip()
|
||||||
|
if previous.get("boot") != boot:
|
||||||
|
previous = {}
|
||||||
|
status = systemctl("show", service, "--property=ActiveState", "--property=InvocationID")
|
||||||
|
info = dict(line.split("=", 1) for line in status.splitlines())
|
||||||
|
if info.get("ActiveState") != "active":
|
||||||
|
return # Respect explicit stops and maintenance; never start an inactive server.
|
||||||
|
invocation = info["InvocationID"]
|
||||||
|
now = time.monotonic()
|
||||||
|
try:
|
||||||
|
with urlopen(url + "/slots", timeout=5) as response:
|
||||||
|
slots = json.load(response)
|
||||||
|
if not isinstance(slots, list):
|
||||||
|
raise ValueError("invalid slot response")
|
||||||
|
state, reasons = observe(previous, slots, invocation, now, seconds, tokens)
|
||||||
|
except (OSError, ValueError, KeyError, TypeError):
|
||||||
|
# A single failed probe is not evidence of a hung model. Require a full
|
||||||
|
# request budget of consecutive failures within the same server process.
|
||||||
|
since = previous.get("unreachable_since", now) if previous.get("invocation") == invocation else now
|
||||||
|
state = {"invocation": invocation, "active": previous.get("active", {}), "unreachable_since": since}
|
||||||
|
reasons = [f"slot endpoint unreachable for {seconds}s"] if now - since >= seconds else []
|
||||||
|
last_restart = previous.get("last_restart", -1e12)
|
||||||
|
state["last_restart"] = last_restart
|
||||||
|
if reasons and now - last_restart >= max(300, seconds):
|
||||||
|
print("llama watchdog: " + "; ".join(reasons) + "; restarting " + service, flush=True)
|
||||||
|
systemctl("restart", "--no-block", service)
|
||||||
|
state = {"invocation": invocation, "active": {}, "last_restart": now}
|
||||||
|
state["boot"] = boot
|
||||||
|
pending = path.with_suffix(".pending")
|
||||||
|
pending.write_text(json.dumps(state) + "\n")
|
||||||
|
pending.chmod(0o600)
|
||||||
|
pending.replace(path)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+206
@@ -0,0 +1,206 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Prepare the documented local model setup; never start services or send messages."""
|
||||||
|
import argparse
|
||||||
|
import fcntl
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import shlex
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
ALIASES = ["hermes-local", "qwen35-9b-local", "qwen35-4b-local", "qwen38-27b-local", "qwen36-27b-local"]
|
||||||
|
LOCAL_URL = "http://127.0.0.1:8080/v1"
|
||||||
|
|
||||||
|
|
||||||
|
def verify(path, item):
|
||||||
|
if not path.is_file() or path.stat().st_size != item["size"]:
|
||||||
|
return False
|
||||||
|
with path.open("rb") as model:
|
||||||
|
return hashlib.file_digest(model, "sha256").hexdigest() == item["sha256"]
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_model(group, model_dir, download=False):
|
||||||
|
directory = model_dir / group["directory"]
|
||||||
|
for item in group["files"]:
|
||||||
|
path = directory / item["name"]
|
||||||
|
if path.exists():
|
||||||
|
if not verify(path, item):
|
||||||
|
raise ValueError(f"Prüfsumme falsch: {path}; vorhandene Datei bleibt unverändert")
|
||||||
|
else:
|
||||||
|
if not download:
|
||||||
|
raise FileNotFoundError(f"Modell fehlt: {path}; mit --download bereitstellen")
|
||||||
|
directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
partial = path.with_name(path.name + ".part")
|
||||||
|
url = f'https://huggingface.co/{group["repo"]}/resolve/{group["revision"]}/{item["name"]}'
|
||||||
|
subprocess.run(["curl", "-fL", "--retry", "3", "--connect-timeout", "15",
|
||||||
|
"--max-time", "14400", "--continue-at", "-", url,
|
||||||
|
"--output", str(partial)], check=True)
|
||||||
|
if not verify(partial, item):
|
||||||
|
raise ValueError(f"Download-Prüfsumme falsch: {partial}; nicht aktiviert")
|
||||||
|
partial.replace(path)
|
||||||
|
print(f"SHA256 OK: {path.name}", flush=True)
|
||||||
|
return [directory / item["name"] for item in group["files"]]
|
||||||
|
|
||||||
|
|
||||||
|
def write_config(path, text, replace=False):
|
||||||
|
if path.exists():
|
||||||
|
if path.read_text() == text:
|
||||||
|
return
|
||||||
|
if not replace:
|
||||||
|
print(f"Vorhandene Konfiguration beibehalten: {path}")
|
||||||
|
return
|
||||||
|
backup = path.with_name(path.name + f".before-setup-{time.time_ns()}")
|
||||||
|
shutil.copy2(path, backup)
|
||||||
|
backup.chmod(0o600)
|
||||||
|
print(f"Sicherung: {backup}")
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w", dir=path.parent, delete=False) as pending:
|
||||||
|
pending.write(text)
|
||||||
|
staging = Path(pending.name)
|
||||||
|
staging.chmod(0o600)
|
||||||
|
staging.replace(path)
|
||||||
|
|
||||||
|
|
||||||
|
def profile_config(name, paths, server_bin=""):
|
||||||
|
context = 65536
|
||||||
|
args = ["--ctx-size", str(context), "--parallel", "1", "--jinja"]
|
||||||
|
if name == "compression":
|
||||||
|
args += ["--rope-scaling", "yarn", "--rope-scale", "2", "--yarn-orig-ctx", "32768",
|
||||||
|
"--reasoning-budget", "0", "--no-reasoning-preserve", "--n-gpu-layers", "0",
|
||||||
|
"--device", "none", "--no-op-offload", "--no-kv-offload",
|
||||||
|
"--threads", "8", "--threads-batch", "8"]
|
||||||
|
else:
|
||||||
|
args += ["--reasoning", "auto", "--reasoning-budget", "128", "--no-reasoning-preserve",
|
||||||
|
"--threads", "6", "--threads-batch", "16"]
|
||||||
|
if name in ("routine", "fallback"):
|
||||||
|
# Explicit full offload: fail instead of silently moving layers to CPU.
|
||||||
|
args += ["--n-gpu-layers", "99", "--fit", "off"]
|
||||||
|
else:
|
||||||
|
args += ["--n-gpu-layers", "auto", "--fit", "on", "--fit-target", "4096"]
|
||||||
|
if name == "routine":
|
||||||
|
args += ["--no-mmproj-offload", "--batch-size", "512", "--ubatch-size", "128"]
|
||||||
|
args += ["--sleep-idle-seconds", "300", "--image-min-tokens", "1024",
|
||||||
|
"--cors-origins", "localhost"]
|
||||||
|
cache = "q4_0" if name == "routine" else "q8_0"
|
||||||
|
args += ["--flash-attn", "auto", "--cache-type-k", cache, "--cache-type-v", cache, "--no-ui"]
|
||||||
|
settings = {"LLAMA_SERVER_BIN": server_bin, "LLAMA_MODEL": str(paths[0]),
|
||||||
|
"LLAMA_MMPROJ": str(paths[1]) if len(paths) > 1 else "", "LLAMA_HF_REPO": "",
|
||||||
|
"LLAMA_ALIAS": "qwen3-1.7b-compression" if name == "compression" else ",".join(ALIASES),
|
||||||
|
"LLAMA_HOST": "127.0.0.1", "LLAMA_PORT": "8081" if name == "compression" else "8080",
|
||||||
|
"LLAMA_PROFILE": "", "LLAMA_CONTEXT_TOKENS": str(context), "LLAMA_MAX_OUTPUT_TOKENS": "2048"}
|
||||||
|
return ("# Generated from config/models.lock.json; no service started.\n"
|
||||||
|
+ "".join(f"{key}={shlex.quote(value)}\n" for key, value in settings.items())
|
||||||
|
+ "LLAMA_ARGS=(\n " + shlex.join(args) + "\n)\n")
|
||||||
|
|
||||||
|
|
||||||
|
def configure_hermes(config, compression=False, context=65536):
|
||||||
|
if context < 64000:
|
||||||
|
raise ValueError("Hermes requires at least 64000 context tokens")
|
||||||
|
config.setdefault("model", {}).update(default="hermes-local", provider="custom",
|
||||||
|
base_url=LOCAL_URL, context_length=context)
|
||||||
|
config.setdefault("agent", {}).update(run_budget_seconds=1800, budget_warning_ratio=0.8)
|
||||||
|
config.setdefault("providers", {}).setdefault("custom", {}).update(
|
||||||
|
request_timeout_seconds=600, stale_timeout_seconds=180)
|
||||||
|
entries = config.setdefault("custom_providers", [])
|
||||||
|
matches = [entry for entry in entries if entry.get("base_url", "").rstrip("/") == LOCAL_URL]
|
||||||
|
if not matches:
|
||||||
|
matches = [{"name": "Local (127.0.0.1:8080)", "base_url": LOCAL_URL}]
|
||||||
|
entries.extend(matches)
|
||||||
|
for entry in matches:
|
||||||
|
entry["model"] = "hermes-local"
|
||||||
|
entry.setdefault("models", {}).update({name: {} for name in ALIASES})
|
||||||
|
entry.setdefault("extra_body", {}).update(max_tokens=2048, t_max_predict_ms=540000)
|
||||||
|
auxiliary = config.setdefault("auxiliary", {})
|
||||||
|
auxiliary.setdefault("vision", {}).update(provider="main", model="hermes-local",
|
||||||
|
base_url=LOCAL_URL, api_key="local-no-key", timeout=300, reasoning_effort="none", max_concurrency=1)
|
||||||
|
if compression:
|
||||||
|
auxiliary.setdefault("compression", {}).update(provider="custom", model="qwen3-1.7b-compression",
|
||||||
|
base_url="http://127.0.0.1:8081/v1", api_key="local-no-key", timeout=300, reasoning_effort="none")
|
||||||
|
config.setdefault("compression", {}).update(enabled=True, threshold=0.5,
|
||||||
|
threshold_tokens=24000, target_ratio=0.2, protect_last_n=10, protect_first_n=3,
|
||||||
|
context_timeout_seconds=120, context_total_ceiling_seconds=600)
|
||||||
|
config.setdefault("compression", {})["threshold_tokens"] = min(24000, context * 3 // 8)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def maintenance_config(text):
|
||||||
|
for key, value in {"LLAMA_SERVICE": "llama-qwen.service", "LLAMA_URL": "http://127.0.0.1:8080",
|
||||||
|
"MODEL_NAME": "hermes-local", "LLAMA_MAX_REQUEST_SECONDS": "600",
|
||||||
|
"LLAMA_MAX_OUTPUT_TOKENS": "2048"}.items():
|
||||||
|
setting = f"{key}={shlex.quote(value)}"
|
||||||
|
pattern = rf"^{key}=.*$"
|
||||||
|
text = re.sub(pattern, lambda _: setting, text, flags=re.MULTILINE) if re.search(pattern, text, re.MULTILINE) else text + "\n" + setting + "\n"
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def unpin_local_jobs(path):
|
||||||
|
if not path.exists():
|
||||||
|
return
|
||||||
|
for job in json.loads(path.read_text()).get("jobs", []):
|
||||||
|
if (job.get("model") in ALIASES and job.get("provider") == "custom"
|
||||||
|
and job.get("base_url") in (None, "", LOCAL_URL)):
|
||||||
|
subprocess.run(["hermes", "cron", "edit", job["id"], "--unpin"], check=True)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--download", action="store_true", help="Fehlende, fest versionierte GGUF-Dateien herunterladen")
|
||||||
|
parser.add_argument("--models", default="routine,large,compression,fallback", help="Kommagetrennt: routine,large,compression,fallback")
|
||||||
|
parser.add_argument("--server-bin", default="", help="Leer: verwalteter CUDA-Build oder llama-server im PATH")
|
||||||
|
parser.add_argument("--model-dir", type=Path, default=Path.home() / ".local/share/hermes-stack/models")
|
||||||
|
parser.add_argument("--config-dir", type=Path, default=Path(os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config"))) / "hermes-stack-maintenance")
|
||||||
|
parser.add_argument("--hermes-config", type=Path, default=Path(os.environ.get("HERMES_HOME", str(Path.home() / ".hermes"))) / "config.yaml")
|
||||||
|
parser.add_argument("--select", choices=["routine", "large", "fallback"], help="Aktive llama.env vorbereiten; kein Dienststart")
|
||||||
|
parser.add_argument("--configure-hermes", action="store_true", help="Gezielte Hermes-/Maintenance-Einstellungen mit Backup übernehmen")
|
||||||
|
parser.add_argument("--unpin-local-cron", action="store_true", help="Lokale Cron-Modellbindungen über Hermes-CLI lösen")
|
||||||
|
options = parser.parse_args()
|
||||||
|
names = list(dict.fromkeys(options.models.split(",")))
|
||||||
|
manifest = json.loads((ROOT / "config/models.lock.json").read_text())["models"]
|
||||||
|
if not names or any(name not in manifest for name in names):
|
||||||
|
parser.error("--models erlaubt nur routine,large,compression,fallback")
|
||||||
|
if options.select and options.select not in names:
|
||||||
|
parser.error("--select muss in --models enthalten sein")
|
||||||
|
if options.configure_hermes and not any(n in names for n in ("routine", "large", "fallback")):
|
||||||
|
parser.error("--configure-hermes benötigt ein Hauptmodell")
|
||||||
|
if options.select or options.configure_hermes or options.unpin_local_cron:
|
||||||
|
state = subprocess.check_output(["systemctl", "--user", "show", "hermes-gateway.service",
|
||||||
|
"--property=ActiveState", "--value"], text=True).strip()
|
||||||
|
if state not in ("inactive", "failed", ""):
|
||||||
|
parser.error("Gateway zuerst mit systemctl --user stop hermes-gateway.service anhalten")
|
||||||
|
# Import before downloads, so a missing dependency does not waste bandwidth.
|
||||||
|
if options.configure_hermes:
|
||||||
|
import yaml
|
||||||
|
lock_path = Path(os.environ.get("XDG_RUNTIME_DIR", "/tmp")) / "hermes-stack-hermes-update.lock"
|
||||||
|
with lock_path.open("w") as lock:
|
||||||
|
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
|
profiles = {}
|
||||||
|
for name in names:
|
||||||
|
paths = ensure_model(manifest[name], options.model_dir, options.download)
|
||||||
|
profiles[name] = profile_config(name, paths, options.server_bin)
|
||||||
|
for name, text in profiles.items():
|
||||||
|
filename = "compression.env" if name == "compression" else f"llama-{name}.env"
|
||||||
|
write_config(options.config_dir / filename, text)
|
||||||
|
if options.select:
|
||||||
|
source = options.config_dir / f"llama-{options.select}.env"
|
||||||
|
write_config(options.config_dir / "llama.env", source.read_text(), replace=True)
|
||||||
|
if options.configure_hermes:
|
||||||
|
text = options.hermes_config.read_text() if options.hermes_config.exists() else ""
|
||||||
|
config = yaml.safe_load(text) or {}
|
||||||
|
configure_hermes(config, "compression" in names, 65536)
|
||||||
|
write_config(options.hermes_config, yaml.safe_dump(config, allow_unicode=True, sort_keys=False, width=1000), replace=True)
|
||||||
|
maintenance = options.config_dir / "config.env"
|
||||||
|
text = maintenance.read_text() if maintenance.exists() else (ROOT / "config/config.env.example").read_text()
|
||||||
|
write_config(maintenance, maintenance_config(text), replace=True)
|
||||||
|
if options.unpin_local_cron:
|
||||||
|
unpin_local_jobs(options.hermes_config.parent / "cron/jobs.json")
|
||||||
|
print("Vorbereitung abgeschlossen. Keine Dienste gestartet. Siehe docs/NEW_WORKSTATION.md.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Enforce local model request duration and output limits
|
||||||
|
After=llama-qwen.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=%h/.local/libexec/hermes-stack-llama-watchdog
|
||||||
|
TimeoutStartSec=45
|
||||||
|
UMask=0077
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Check local model request limits every 15 seconds
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=1min
|
||||||
|
OnUnitActiveSec=15s
|
||||||
|
AccuracySec=1s
|
||||||
|
Unit=hermes-stack-llama-watchdog.service
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Local llama.cpp Qwen model server
|
Description=Hermes local main model (routine/large)
|
||||||
After=network-online.target graphical-session.target
|
After=network-online.target graphical-session.target
|
||||||
StartLimitIntervalSec=300
|
StartLimitIntervalSec=300
|
||||||
StartLimitBurst=3
|
StartLimitBurst=3
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
python3 "$ROOT_DIR/tests/test_llama.py" || failures=$((failures + 1))
|
python3 "$ROOT_DIR/tests/test_llama.py" || failures=$((failures + 1))
|
||||||
|
python3 "$ROOT_DIR/tests/test_llama_watchdog.py" || failures=$((failures + 1))
|
||||||
|
python3 "$ROOT_DIR/tests/test_workstation.py" || failures=$((failures + 1))
|
||||||
python3 "$ROOT_DIR/tests/test_hermes_update.py" || failures=$((failures + 1))
|
python3 "$ROOT_DIR/tests/test_hermes_update.py" || failures=$((failures + 1))
|
||||||
|
|
||||||
if (cd -- "$ROOT_DIR" && rg -n --hidden --glob '!tests/smoke.sh' --glob '!tests/test_llama.py' --glob '!.git/**' \
|
if (cd -- "$ROOT_DIR" && rg -n --hidden --glob '!tests/smoke.sh' --glob '!tests/test_llama.py' --glob '!.git/**' \
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ OPTIONS = """--model --hf-repo --mmproj --alias --host --port --ctx-size --paral
|
|||||||
--jinja --reasoning --reasoning-budget --reasoning-preserve --no-reasoning-preserve --flash-attn --threads
|
--jinja --reasoning --reasoning-budget --reasoning-preserve --no-reasoning-preserve --flash-attn --threads
|
||||||
--threads-batch --n-gpu-layers --fit --fit-target --cache-type-k --cache-type-v
|
--threads-batch --n-gpu-layers --fit --fit-target --cache-type-k --cache-type-v
|
||||||
--sleep-idle-seconds --image-min-tokens --cors-origins --no-ui --rope-scaling --rope-scale --yarn-orig-ctx
|
--sleep-idle-seconds --image-min-tokens --cors-origins --no-ui --rope-scaling --rope-scale --yarn-orig-ctx
|
||||||
--device --no-op-offload --no-kv-offload"""
|
--device --no-op-offload --no-kv-offload --n-predict"""
|
||||||
|
|
||||||
|
|
||||||
class LlamaTests(unittest.TestCase):
|
class LlamaTests(unittest.TestCase):
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import importlib.util
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
spec = importlib.util.spec_from_file_location("watchdog", Path(__file__).resolve().parents[1] / "scripts/llama_watchdog.py")
|
||||||
|
watchdog = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(watchdog)
|
||||||
|
|
||||||
|
|
||||||
|
def busy(task=1, decoded=20):
|
||||||
|
return [{"id": 0, "id_task": task, "is_processing": True,
|
||||||
|
"next_token": [{"n_decoded": decoded}]}]
|
||||||
|
|
||||||
|
|
||||||
|
class WatchdogTests(unittest.TestCase):
|
||||||
|
def test_busy_request_is_allowed_then_expires_despite_progress(self):
|
||||||
|
state, reasons = watchdog.observe({}, busy(), "server1", 100, 600, 2048)
|
||||||
|
self.assertFalse(reasons)
|
||||||
|
state, reasons = watchdog.observe(state, busy(decoded=1000), "server1", 699, 600, 2048)
|
||||||
|
self.assertFalse(reasons)
|
||||||
|
_, reasons = watchdog.observe(state, busy(decoded=1100), "server1", 700, 600, 2048)
|
||||||
|
self.assertIn("600s", reasons[0])
|
||||||
|
|
||||||
|
def test_new_request_and_restarted_server_do_not_inherit_deadline(self):
|
||||||
|
state, _ = watchdog.observe({}, busy(), "server1", 100, 600, 2048)
|
||||||
|
for slots, invocation in [(busy(2), "server1"), (busy(), "server2")]:
|
||||||
|
_, reasons = watchdog.observe(state, slots, invocation, 900, 600, 2048)
|
||||||
|
self.assertFalse(reasons)
|
||||||
|
|
||||||
|
def test_idle_slot_drops_old_deadline(self):
|
||||||
|
state, _ = watchdog.observe({}, busy(), "server1", 100, 600, 2048)
|
||||||
|
state, reasons = watchdog.observe(state, [{"id": 0, "is_processing": False}], "server1", 900, 600, 2048)
|
||||||
|
self.assertEqual(state["active"], {})
|
||||||
|
self.assertFalse(reasons)
|
||||||
|
|
||||||
|
def test_excess_output_is_stopped_even_when_client_overrides_default(self):
|
||||||
|
_, reasons = watchdog.observe({}, busy(decoded=2049), "server1", 100, 600, 2048)
|
||||||
|
self.assertIn("2048 tokens", reasons[0])
|
||||||
|
|
||||||
|
def test_prompt_and_generated_text_are_not_persisted(self):
|
||||||
|
slots = busy()
|
||||||
|
slots[0].update(prompt="private prompt", generated="private output")
|
||||||
|
state, _ = watchdog.observe({}, slots, "server1", 100, 600, 2048)
|
||||||
|
self.assertNotIn("private", str(state))
|
||||||
|
|
||||||
|
|
||||||
|
class WatchdogIntegrationTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.temp = tempfile.TemporaryDirectory()
|
||||||
|
self.addCleanup(self.temp.cleanup)
|
||||||
|
self.environment = patch.dict(watchdog.os.environ, {"XDG_STATE_HOME": self.temp.name,
|
||||||
|
"LLAMA_MAX_REQUEST_SECONDS": "600", "LLAMA_MAX_OUTPUT_TOKENS": "2048"})
|
||||||
|
self.environment.start()
|
||||||
|
self.addCleanup(self.environment.stop)
|
||||||
|
self.path = Path(self.temp.name) / "hermes-stack-maintenance/llama-watchdog.json"
|
||||||
|
self.boot = Path("/proc/sys/kernel/random/boot_id").read_text().strip()
|
||||||
|
self.calls = []
|
||||||
|
|
||||||
|
def control(self, *args):
|
||||||
|
self.calls.append(args)
|
||||||
|
return "ActiveState=active\nInvocationID=test-server" if args[0] == "show" else ""
|
||||||
|
|
||||||
|
def tick(self, now, slots=None, error=False):
|
||||||
|
response = io.BytesIO(json.dumps(slots or []).encode())
|
||||||
|
with patch.object(watchdog, "systemctl", self.control), \
|
||||||
|
patch.object(watchdog.time, "monotonic", return_value=now), \
|
||||||
|
patch.object(watchdog, "urlopen", side_effect=OSError() if error else None, return_value=response):
|
||||||
|
watchdog.main()
|
||||||
|
|
||||||
|
def restarts(self):
|
||||||
|
return [call for call in self.calls if call[0] == "restart"]
|
||||||
|
|
||||||
|
def test_deadline_restarts_actual_configured_service_once(self):
|
||||||
|
self.tick(100, busy())
|
||||||
|
self.tick(699, busy())
|
||||||
|
self.assertFalse(self.restarts())
|
||||||
|
self.tick(700, busy())
|
||||||
|
self.assertEqual(self.restarts(), [("restart", "--no-block", "llama-qwen.service")])
|
||||||
|
self.tick(701, busy(decoded=3000))
|
||||||
|
self.assertEqual(len(self.restarts()), 1)
|
||||||
|
self.assertEqual(self.path.stat().st_mode & 0o777, 0o600)
|
||||||
|
|
||||||
|
def test_only_persistent_probe_failure_triggers_recovery(self):
|
||||||
|
self.tick(100, error=True)
|
||||||
|
self.tick(699, error=True)
|
||||||
|
self.assertFalse(self.restarts())
|
||||||
|
self.tick(700, error=True)
|
||||||
|
self.assertEqual(len(self.restarts()), 1)
|
||||||
|
|
||||||
|
def test_successful_probe_resets_failure_deadline(self):
|
||||||
|
self.tick(100, error=True)
|
||||||
|
self.tick(500)
|
||||||
|
self.tick(700, error=True)
|
||||||
|
self.assertFalse(self.restarts())
|
||||||
|
|
||||||
|
def test_inactive_service_never_started_or_probed(self):
|
||||||
|
with patch.object(watchdog, "systemctl", return_value="ActiveState=inactive\nInvocationID=") as control, \
|
||||||
|
patch.object(watchdog, "urlopen") as probe:
|
||||||
|
watchdog.main()
|
||||||
|
probe.assert_not_called()
|
||||||
|
self.assertEqual(control.call_count, 1)
|
||||||
|
|
||||||
|
def test_reboot_clears_old_monotonic_deadlines_and_cooldown(self):
|
||||||
|
self.path.parent.mkdir()
|
||||||
|
self.path.write_text(json.dumps({"boot": "previous-boot", "invocation": "test-server",
|
||||||
|
"active": {"0:1": 100}, "last_restart": 10000, "unreachable_since": 50}))
|
||||||
|
self.tick(100, busy())
|
||||||
|
self.tick(700, busy())
|
||||||
|
self.assertEqual(len(self.restarts()), 1)
|
||||||
|
self.assertEqual(json.loads(self.path.read_text())["boot"], self.boot)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
import hashlib
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
spec = importlib.util.spec_from_file_location("prepare", ROOT / "scripts/prepare-workstation.py")
|
||||||
|
prepare = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(prepare)
|
||||||
|
|
||||||
|
|
||||||
|
class PreparationTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.temp = tempfile.TemporaryDirectory()
|
||||||
|
self.addCleanup(self.temp.cleanup)
|
||||||
|
self.root = Path(self.temp.name)
|
||||||
|
self.data = b"test model weights"
|
||||||
|
self.item = {"name": "weights.gguf", "sha256": hashlib.sha256(self.data).hexdigest(), "size": len(self.data)}
|
||||||
|
self.group = {"repo": "owner/model", "revision": "a" * 40, "directory": "model/revision", "files": [self.item]}
|
||||||
|
|
||||||
|
def test_download_uses_fixed_revision_and_publishes_only_verified_file(self):
|
||||||
|
def download(args, **kwargs):
|
||||||
|
self.assertIn("/resolve/" + "a" * 40 + "/weights.gguf", args[-3])
|
||||||
|
Path(args[-1]).write_bytes(self.data)
|
||||||
|
with patch.object(prepare.subprocess, "run", side_effect=download):
|
||||||
|
paths = prepare.ensure_model(self.group, self.root, True)
|
||||||
|
self.assertEqual(paths[0].read_bytes(), self.data)
|
||||||
|
self.assertFalse(paths[0].with_name("weights.gguf.part").exists())
|
||||||
|
|
||||||
|
def test_corrupt_download_cannot_become_live_model(self):
|
||||||
|
with patch.object(prepare.subprocess, "run", side_effect=lambda args, **_: Path(args[-1]).write_bytes(b"broken")):
|
||||||
|
with self.assertRaisesRegex(ValueError, "nicht aktiviert"):
|
||||||
|
prepare.ensure_model(self.group, self.root, True)
|
||||||
|
self.assertFalse((self.root / "model/revision/weights.gguf").exists())
|
||||||
|
|
||||||
|
def test_existing_wrong_file_is_preserved_without_download(self):
|
||||||
|
path = self.root / "model/revision/weights.gguf"
|
||||||
|
path.parent.mkdir(parents=True)
|
||||||
|
path.write_bytes(b"user file")
|
||||||
|
with patch.object(prepare.subprocess, "run") as download, self.assertRaises(ValueError):
|
||||||
|
prepare.ensure_model(self.group, self.root, True)
|
||||||
|
download.assert_not_called()
|
||||||
|
self.assertEqual(path.read_bytes(), b"user file")
|
||||||
|
|
||||||
|
def test_config_merge_preserves_unrelated_settings_and_external_provider(self):
|
||||||
|
config = {"terminal": {"backend": "docker"}, "plugins": {"enabled": ["private"]},
|
||||||
|
"custom_providers": [{"name": "Other", "base_url": "https://other.invalid/v1", "model": "other"}]}
|
||||||
|
updated = prepare.configure_hermes(config, True)
|
||||||
|
self.assertEqual(updated["terminal"]["backend"], "docker")
|
||||||
|
self.assertEqual(updated["plugins"]["enabled"], ["private"])
|
||||||
|
self.assertEqual(updated["custom_providers"][0]["model"], "other")
|
||||||
|
self.assertEqual(updated["custom_providers"][1]["extra_body"]["max_tokens"], 2048)
|
||||||
|
self.assertEqual(updated["auxiliary"]["compression"]["base_url"], "http://127.0.0.1:8081/v1")
|
||||||
|
|
||||||
|
def test_context_and_compression_follow_selected_model(self):
|
||||||
|
for name, window, trigger in (("routine", 65536, 24000), ("fallback", 65536, 24000), ("large", 65536, 24000)):
|
||||||
|
with self.subTest(name=name):
|
||||||
|
config = {"model": {"context_length": 256000}, "compression": {"threshold_tokens": 24000}}
|
||||||
|
prepare.configure_hermes(config, context=window)
|
||||||
|
self.assertEqual(config["model"]["context_length"], window)
|
||||||
|
self.assertEqual(config["compression"]["threshold_tokens"], trigger)
|
||||||
|
text = prepare.profile_config(name, [self.root / "weights.gguf"])
|
||||||
|
self.assertIn(f"--ctx-size {window}", text)
|
||||||
|
before = json.dumps(config, sort_keys=True)
|
||||||
|
prepare.configure_hermes(config, context=window)
|
||||||
|
self.assertEqual(json.dumps(config, sort_keys=True), before)
|
||||||
|
|
||||||
|
def test_configuration_refuses_below_hermes_context_floor(self):
|
||||||
|
config = {"model": {"context_length": 65536}}
|
||||||
|
with self.assertRaisesRegex(ValueError, "at least 64000"):
|
||||||
|
prepare.configure_hermes(config, context=16384)
|
||||||
|
self.assertEqual(config, {"model": {"context_length": 65536}})
|
||||||
|
|
||||||
|
def test_existing_profiles_preserved_and_explicit_selection_backed_up(self):
|
||||||
|
path = self.root / "llama.env"
|
||||||
|
path.write_text("custom tuning\n")
|
||||||
|
prepare.write_config(path, "generated\n")
|
||||||
|
self.assertEqual(path.read_text(), "custom tuning\n")
|
||||||
|
prepare.write_config(path, "generated\n", replace=True)
|
||||||
|
prepare.write_config(path, "generated\n", replace=True)
|
||||||
|
self.assertEqual(len(list(self.root.glob("llama.env.before-setup-*"))), 1)
|
||||||
|
self.assertEqual(path.stat().st_mode & 0o777, 0o600)
|
||||||
|
|
||||||
|
def test_profiles_quote_other_user_paths_and_do_not_use_remote_hf_start(self):
|
||||||
|
text = prepare.profile_config("routine", [self.root / "user space/model.gguf", self.root / "projector.gguf"])
|
||||||
|
result = subprocess.run(["bash", "-c", text + '\nprintf "%s|%s|%s" "$LLAMA_MODEL" "$LLAMA_HF_REPO" "${LLAMA_ARGS[*]}"'], text=True, capture_output=True, check=True)
|
||||||
|
self.assertTrue(result.stdout.startswith(str(self.root / "user space/model.gguf") + "||"))
|
||||||
|
self.assertIn("--n-gpu-layers 99 --fit off", result.stdout)
|
||||||
|
|
||||||
|
def test_prepare_whole_new_workstation_offline_without_home_override(self):
|
||||||
|
repository = self.root / "repository"
|
||||||
|
(repository / "config").mkdir(parents=True)
|
||||||
|
shutil.copy2(ROOT / "config/config.env.example", repository / "config")
|
||||||
|
(repository / "config/models.lock.json").write_text(json.dumps({"models": {"routine": self.group}}))
|
||||||
|
path = self.root / "models/model/revision/weights.gguf"
|
||||||
|
path.parent.mkdir(parents=True)
|
||||||
|
path.write_bytes(self.data)
|
||||||
|
config = self.root / "config/hermes-stack-maintenance"
|
||||||
|
hermes = self.root / "hermes/config.yaml"
|
||||||
|
args = ["prepare", "--models", "routine", "--model-dir", str(self.root / "models"),
|
||||||
|
"--config-dir", str(config), "--hermes-config", str(hermes),
|
||||||
|
"--configure-hermes", "--select", "routine"]
|
||||||
|
with patch.object(prepare, "ROOT", repository), patch("sys.argv", args), \
|
||||||
|
patch.dict(os.environ, {"XDG_RUNTIME_DIR": str(self.root)}), \
|
||||||
|
patch.object(prepare.subprocess, "check_output", return_value="inactive\n"):
|
||||||
|
prepare.main()
|
||||||
|
self.assertEqual((config / "llama.env").read_text(), (config / "llama-routine.env").read_text())
|
||||||
|
self.assertIn("LLAMA_MAX_REQUEST_SECONDS=600", (config / "config.env").read_text())
|
||||||
|
self.assertIn("run_budget_seconds: 1800", hermes.read_text())
|
||||||
|
|
||||||
|
def test_local_cron_unpin_does_not_touch_other_provider_jobs(self):
|
||||||
|
path = self.root / "jobs.json"
|
||||||
|
path.write_text(json.dumps({"jobs": [
|
||||||
|
{"id": "local", "model": "qwen38-27b-local", "provider": "custom"},
|
||||||
|
{"id": "cloud", "model": "qwen38-27b-local", "provider": "other"},
|
||||||
|
{"id": "remote", "model": "qwen38-27b-local", "provider": "custom", "base_url": "https://other.invalid/v1"}]}))
|
||||||
|
with patch.object(prepare.subprocess, "run") as edit:
|
||||||
|
prepare.unpin_local_jobs(path)
|
||||||
|
edit.assert_called_once_with(["hermes", "cron", "edit", "local", "--unpin"], check=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ModelSwitchTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.temp = tempfile.TemporaryDirectory()
|
||||||
|
self.addCleanup(self.temp.cleanup)
|
||||||
|
self.root = Path(self.temp.name)
|
||||||
|
self.bin = self.root / "bin"
|
||||||
|
self.bin.mkdir()
|
||||||
|
self.config = self.root / "config/hermes-stack-maintenance"
|
||||||
|
self.config.mkdir(parents=True)
|
||||||
|
(self.config / "llama.env").write_text("LLAMA_MODEL=old.gguf\n")
|
||||||
|
(self.config / "llama-routine.env").write_text("LLAMA_MODEL=new.gguf\n")
|
||||||
|
shutil.copy2(ROOT / "libexec/hermes-stack-model", self.bin)
|
||||||
|
for name, body in {"hermes-stack-llama-run": "exit 0",
|
||||||
|
"systemctl": 'echo "$*" >>"$TEST_CALLS"; if [[ "$*" == *"restart llama-qwen"* && "${TEST_FAIL:-0}" == 1 ]]; then exit 1; fi',
|
||||||
|
"curl": "echo '{\"status\":\"ok\"}'"}.items():
|
||||||
|
p = self.bin / name
|
||||||
|
p.write_text("#!/usr/bin/env bash\n" + body + "\n")
|
||||||
|
p.chmod(0o755)
|
||||||
|
self.env = dict(os.environ, PATH=str(self.bin) + ":" + os.environ["PATH"],
|
||||||
|
HERMES_HOME=str(self.root / "hermes"), XDG_CONFIG_HOME=str(self.config.parent), XDG_RUNTIME_DIR=str(self.root), TEST_CALLS=str(self.root / "calls"))
|
||||||
|
|
||||||
|
def test_switch_and_failure_keep_hermes_window_paired_with_model(self):
|
||||||
|
import yaml
|
||||||
|
directory = self.root / "hermes"
|
||||||
|
directory.mkdir()
|
||||||
|
path = directory / "config.yaml"
|
||||||
|
original = "model:\n context_length: 65536\ncompression:\n threshold_tokens: 24000\nterminal:\n backend: docker\n"
|
||||||
|
(self.config / "llama-routine.env").write_text("LLAMA_MODEL=new.gguf\nLLAMA_CONTEXT_TOKENS=64000\n")
|
||||||
|
for fail in (False, True):
|
||||||
|
path.write_text(original)
|
||||||
|
(self.config / "llama.env").write_text("LLAMA_MODEL=old.gguf\n")
|
||||||
|
result = subprocess.run(["bash", str(self.bin / "hermes-stack-model"), "routine"],
|
||||||
|
env=dict(self.env, TEST_FAIL=str(int(fail))), capture_output=True, text=True)
|
||||||
|
if fail:
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertEqual(path.read_text(), original)
|
||||||
|
self.assertEqual((self.config / "llama.env").read_text(), "LLAMA_MODEL=old.gguf\n")
|
||||||
|
else:
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
config = yaml.safe_load(path.read_text())
|
||||||
|
self.assertEqual(config["model"]["context_length"], 64000)
|
||||||
|
self.assertEqual(config["compression"]["threshold_tokens"], 24000)
|
||||||
|
self.assertEqual(config["terminal"]["backend"], "docker")
|
||||||
|
|
||||||
|
def test_hermes_rejects_too_small_context_and_restores_pair(self):
|
||||||
|
directory = self.root / "hermes"
|
||||||
|
directory.mkdir()
|
||||||
|
path = directory / "config.yaml"
|
||||||
|
original = "model:\n context_length: 65536\n"
|
||||||
|
path.write_text(original)
|
||||||
|
(self.config / "llama-routine.env").write_text("LLAMA_MODEL=new.gguf\nLLAMA_CONTEXT_TOKENS=16384\n")
|
||||||
|
result = subprocess.run(["bash", str(self.bin / "hermes-stack-model"), "routine"],
|
||||||
|
env=self.env, capture_output=True, text=True)
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertIn("at least 64000", result.stderr)
|
||||||
|
self.assertEqual(path.read_text(), original)
|
||||||
|
self.assertEqual((self.config / "llama.env").read_text(), "LLAMA_MODEL=old.gguf\n")
|
||||||
|
|
||||||
|
def test_successful_switch_through_path_entrypoint(self):
|
||||||
|
(self.root / "entrypoint").symlink_to(self.bin / "hermes-stack-model")
|
||||||
|
result = subprocess.run(["bash", str(self.root / "entrypoint"), "routine"], env=self.env, capture_output=True, text=True)
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
self.assertEqual((self.config / "llama.env").read_text(), "LLAMA_MODEL=new.gguf\n")
|
||||||
|
self.assertIn("start hermes-gateway.service", (self.root / "calls").read_text())
|
||||||
|
|
||||||
|
def test_failed_switch_restores_configuration_and_still_attempts_gateway_start(self):
|
||||||
|
result = subprocess.run(["bash", str(self.bin / "hermes-stack-model"), "routine"],
|
||||||
|
env=dict(self.env, TEST_FAIL="1"), capture_output=True, text=True)
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertEqual((self.config / "llama.env").read_text(), "LLAMA_MODEL=old.gguf\n")
|
||||||
|
self.assertIn("start hermes-gateway.service", (self.root / "calls").read_text())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
+9
-1
@@ -6,6 +6,7 @@ set -Eeuo pipefail
|
|||||||
units=(
|
units=(
|
||||||
hermes-stack-hermes-update.service hermes-stack-hermes-update.timer
|
hermes-stack-hermes-update.service hermes-stack-hermes-update.timer
|
||||||
hermes-stack-health.service hermes-stack-health.timer
|
hermes-stack-health.service hermes-stack-health.timer
|
||||||
|
hermes-stack-llama-watchdog.service hermes-stack-llama-watchdog.timer
|
||||||
hermes-stack-signal-health.service hermes-stack-signal-health.timer
|
hermes-stack-signal-health.service hermes-stack-signal-health.timer
|
||||||
hermes-stack-signal-update-check.service hermes-stack-signal-update-check.timer
|
hermes-stack-signal-update-check.service hermes-stack-signal-update-check.timer
|
||||||
llama-qwen.service llama-compression.service signal-cli.service
|
llama-qwen.service llama-compression.service signal-cli.service
|
||||||
@@ -16,6 +17,7 @@ systemctl --user disable --now llama-compression.service 2>/dev/null || true
|
|||||||
systemctl --user disable --now \
|
systemctl --user disable --now \
|
||||||
hermes-stack-hermes-update.timer \
|
hermes-stack-hermes-update.timer \
|
||||||
hermes-stack-health.timer \
|
hermes-stack-health.timer \
|
||||||
|
hermes-stack-llama-watchdog.timer \
|
||||||
hermes-stack-signal-health.timer \
|
hermes-stack-signal-health.timer \
|
||||||
hermes-stack-signal-update-check.timer 2>/dev/null || true
|
hermes-stack-signal-update-check.timer 2>/dev/null || true
|
||||||
|
|
||||||
@@ -27,10 +29,16 @@ for file in \
|
|||||||
hermes-stack-notify hermes-stack-hermes-update hermes-stack-health \
|
hermes-stack-notify hermes-stack-hermes-update hermes-stack-health \
|
||||||
hermes-stack-signal-health hermes-stack-signal-update-check hermes-stack-signal-update \
|
hermes-stack-signal-health hermes-stack-signal-update-check hermes-stack-signal-update \
|
||||||
hermes-stack-llama-run hermes-stack-llama-profile hermes-stack-llama-restart \
|
hermes-stack-llama-run hermes-stack-llama-profile hermes-stack-llama-restart \
|
||||||
hermes-stack-llama-report \
|
hermes-stack-llama-report hermes-stack-llama-watchdog hermes-stack-model \
|
||||||
hermes-stack-signal-run hermes-stack-doctor; do
|
hermes-stack-signal-run hermes-stack-doctor; do
|
||||||
rm -f -- "$HOME/.local/libexec/$file"
|
rm -f -- "$HOME/.local/libexec/$file"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
rm -f -- "$HOME/.local/lib/hermes-stack/llama_watchdog.py"
|
||||||
|
|
||||||
systemctl --user daemon-reload
|
systemctl --user daemon-reload
|
||||||
|
command_target="$HOME/.local/bin/hermes-stack-model"
|
||||||
|
if [[ -L "$command_target" && "$(readlink -- "$command_target")" == ../libexec/hermes-stack-model ]]; then
|
||||||
|
rm -f -- "$command_target"
|
||||||
|
fi
|
||||||
printf 'Deployment-Dateien entfernt; Benutzerdaten und Konfigurationen wurden beibehalten.\n'
|
printf 'Deployment-Dateien entfernt; Benutzerdaten und Konfigurationen wurden beibehalten.\n'
|
||||||
Reference in new issue
Block a user