llama.cpp-Server-Instanzen, die als Docker-Container laufen —
inklusive einer SSRF-gehärteten Eingabeschicht für System- und User-Prompts aus
Text, Datei oder Remote-URL.
- Python 94.4%
- Shell 5.6%
classify_refusal() separates refusal (head), preamble, appended disclaimer
(tail) and moralising insert (inline). Only the first three are meaningful, and
only where a prompt forbids them: a preamble on a coding answer is normal, so it
no longer counts as evasion.
Two sources of false positives had to be removed first, both found by checking
the detector's hits against the archive rather than trusting them:
* llamacppctl's own truncation warning on stderr had leaked into an archived
output and was read as a model disclaimer. It also inflated that run's word
count, so count_words() strips it too.
* An AI character saying "Bitte beachten Sie:" inside a dystopian story is
plot, not distancing. Quoted speech is removed before markers are matched.
run_prompt_suite.sh gains a CASES filter so a single prompt can be re-run.
Measured result, recorded in KI_TOOLS_PROFILES.md: across seven prose prompts
neither the abliterated model nor the aligned base model evaded once. On
literary prose the abliteration buys nothing measurable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|---|---|---|
| .forgejo/workflows | ||
| .githooks | ||
| docs | ||
| example_system_prompts | ||
| example_user_prompts | ||
| man | ||
| scripts | ||
| src/llamacppctl | ||
| tests | ||
| .gitignore | ||
| build_archive.py | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| LICENSE | ||
| llama.cpp.config.example | ||
| pyproject.toml | ||
| README.md | ||
| requirements-dev.txt | ||
| requirements.txt | ||
llamacppctl
Ein einziges, sicherheitsgehärtetes CLI-Werkzeug zum Starten, Prüfen, Stoppen und
Wechseln von llama.cpp-Server-Instanzen, die als Docker-Container laufen —
inklusive einer SSRF-gehärteten Eingabeschicht für System- und User-Prompts aus
Text, Datei oder Remote-URL.
llamacppctl ersetzt eine Sammlung einzelner Shell-Skripte
(start-llm-server.sh, status-llm-server.sh, switch-llm.sh, …) durch ein
einziges Python-Paket mit konsistenter Konfiguration, Locking und
Fehlerbehandlung.
Dokumentation
| Dokument | Für wen / wofür |
|---|---|
| README (diese Datei) | Schnelleinstieg: Installation, Konfiguration, Verwendung |
docs/BEDIENUNGSANLEITUNG.md |
Vollständiges Benutzerhandbuch (alle Aktionen, Optionen, Fehlersuche) |
docs/INSTALL_FROM_ARCHIVE.md |
Installation aus dem .tar.gz-Archiv + Archiv selbst bauen/verifizieren |
docs/SECURITY_AND_OPERATIONS.md |
Architektur, Modul-/Repo-Übersicht, Sicherheits- & Betriebsmodell |
man/llamacppctl.1 |
Vollständige Optionsreferenz (man llamacppctl bzw. man ./man/llamacppctl.1) |
CHANGELOG.md |
Versionshistorie |
Installation
tar xzf llamacppctl-installable.tar.gz
cd llamacppctl
python3 -m venv .venv
source .venv/bin/activate
pip install .
Danach steht der Befehl llamacppctl innerhalb der aktivierten virtuellen
Umgebung zur Verfügung. Für eine systemweite Installation (ohne venv):
pip install --user .
Voraussetzungen auf dem Zielsystem:
- Python >= 3.10
- Docker (CLI + laufender Daemon), für
--start/--check/--stop/--change` - Netzwerkzugriff auf den Container-Host-Port für
--chat/--check`
--print-effective-config ist eine eigenständige, nebenwirkungsfreie Aktion und
benötigt keinen laufenden Docker-Daemon. --start/--check/--stop/--changeerfordern einen erreichbaren Docker-Daemon — auch zusammen mit--dry-run`, weil
die Verfügbarkeit geprüft wird, bevor der Trockenlauf greift.
Konfiguration
Kopiere llama.cpp.config.example nach llama.cpp.config und passe es an:
cp llama.cpp.config.example llama.cpp.config
Abschnittstypen:
[default]— globale Defaults, überschreiben die eingebauten Fallback-Werte[model.<name>]— ein Modell-Profil, ausgewählt über--profile <name>[prompt.<name>]— ein System-Prompt-Profil, ausgewählt über--system-prompt-profile <name>(liefert nur einen Fallback-Systemprompt; ein explizites-s/--system-file/--system-urlgewinnt immer)
container_name ist Pflicht und muss für jedes Profil, das parallel
laufen soll, eindeutig sein — er ist der einzige Identitätsanker für
Docker-Namensgebung, Locking (--start/--change) sowie
--stop/--check-Ziel.
hf_home unterstützt Environment-Variablen und ~, z. B. hf_home = ${HF_HOME}.
Das Chat-Antwortbudget ist ebenfalls konfigurierbar (wirkt auf --chat und die
Antwort nach --start, nicht auf den Container): max_tokens (Default 2048 —
für Reasoning-Modelle / lange Texte hochsetzen, sonst bricht die Antwort mitten
im „Denken" ab und content bleibt leer) und chat_temperature (leer =
Server-temp gilt). CLI-Overrides: --max-tokens, --chat-temp.
Auflösungsreihenfolge (niedrigste zu höchster Priorität):
- Eingebaute Defaults
[default]-Sektion[model.<profile>]-Sektion (falls--profilegesetzt)- CLI-Overrides (
--image,--host-port, …)
Verwendung
# Server starten (nutzt die [default]-Sektion der Config)
llamacppctl --start --config llama.cpp.config
# Nur den geplanten docker-run-Befehl anzeigen, nichts ausführen
llamacppctl --start --config llama.cpp.config --dry-run
# Effektive Konfiguration als JSON ausgeben (startet nichts, braucht kein Docker)
llamacppctl --print-effective-config --config llama.cpp.config
# Status prüfen
llamacppctl --check --config llama.cpp.config
# Server stoppen und Container entfernen
llamacppctl --stop --config llama.cpp.config
# Ein bestimmtes Modell-Profil starten (siehe Hinweis unter dem Block)
llamacppctl --start --config llama.cpp.config --profile qwopus
# Modell wechseln (stop, neu konfigurieren, restart) unter Lock-Schutz
llamacppctl --change --config llama.cpp.config --profile carnice
# Direkten Chat-Request an einen laufenden Server senden
llamacppctl --chat --config llama.cpp.config \
-s "Du antwortest kurz." -p "Was ist 2+2?"
# Chat mit Live-Streaming und großem Antwortbudget (lange Texte)
llamacppctl --chat --config llama.cpp.config \
--stream --max-tokens 8000 -p "Schreibe eine kurze Rede zum Jahreswechsel."
# System-Prompt aus einer lokalen Datei, User-Prompt als Literal
llamacppctl --start --config llama.cpp.config \
--system-file ./prompts/coding.md -p "Refaktoriere diese Funktion."
# System-Prompt von einer HTTPS-URL (öffentliches, nicht-privates Ziel)
llamacppctl --chat --config llama.cpp.config \
--system-url https://example.com/prompts/system.txt -p "Hallo"
Ohne --profile gilt die [default]-Sektion. Mit --profile <name> wählst du
ein [model.<name>]-Profil; in llama.cpp.config sind das z. B. carnice,
qwen27b, qwopus, in der Vorlage llama.cpp.config.example qwen35b und
deepseek. Die Ziffern (z. B. 35b) meinen die Modellgröße in Milliarden
Parametern, nicht die Qwen-Version.
Vollständige Optionsliste: llamacppctl --help oder die Manpage
(man/llamacppctl.1, siehe unten).
Migration von den Shell-Skripten
llamacppctl ersetzt die einzelnen Shell-Skripte durch Actions eines
Werkzeugs (--profile ist optional; ohne greift die [default]-Sektion):
| Altes Skript | Neuer Aufruf |
|---|---|
start-llm-server.sh |
llamacppctl --start --config llama.cpp.config [--profile <name>] |
status-llm-server.sh |
llamacppctl --check --config llama.cpp.config [--profile <name>] |
| Stop-Skript | llamacppctl --stop --config llama.cpp.config [--profile <name>] |
switch-llm.sh (Modellwechsel) |
llamacppctl --change --config llama.cpp.config [--profile <name>] |
| (neu) direkter Chat-Test | llamacppctl --chat --config llama.cpp.config -p "..." |
Sicherheitsmodell (Kurzfassung)
Für Details siehe docs/SECURITY_AND_OPERATIONS.md.
- Alle Docker-Aufrufe laufen über
subprocess.run()mit Argumentlisten — niemals über Shell-Strings, somit keine Shell-Injection-Fläche. --system-file/--prompt-filelesen ausschließlich reguläre Dateien, Größenlimit (--max-input-bytes, Default 1 MiB), UTF-8-Text erzwungen, Symlinks standardmäßig abgelehnt, per--allow-symlinks` freischaltbar.--system-url/--prompt-urlsind standardmäßig HTTPS-only, blockieren IP-Literale, private/loopback/link-local/reserved Adressbereiche sowie die Cloud-Metadata-IP169.254.169.254, verbieten eingebettete Zugangsdaten in der URL, erlauben standardmäßig keine Redirects (jeder Hop wird bei Aktivierung erneut validiert), erzwingen eine Content-Type-Allowlist und ein hartes Größenlimit sowohl überContent-Length` als auch beim Streaming.- Der Port wird standardmäßig nur auf
127.0.0.1veröffentlicht — der ungeauthentifizierte OpenAI-Endpoint ist also nicht im LAN erreichbar.--expose/expose = truebindet auf alle Interfaces; dann sollte per--api-key/api_keyein Schlüssel gesetzt werden (Server verlangt ihn, das Tool sendet ihn als Bearer-Token). container_nameist verpflichtend und muss pro parallel betriebenem Profil eindeutig sein, um Docker-Namenskollisionen und ungewollte Container-Übernahmen zu verhindern.--start/--change` nutzen echte Chat-Completions zum Readiness-Check (nicht nur einen offenen Port), um sicherzustellen, dass das Modell tatsächlich geladen und inferenzfähig ist.--changeverwendet eine exklusive, nicht-blockierende Dateisperre (/tmp/llamacppctl.<container_name>.lock), um parallele Änderungen am selben Container zu verhindern.
Entwicklung / Tests
pip install -e ".[dev]"
python -m pytest -q
python -m pytest (statt des pytest-Skripts) legt das Repo-Root auf
sys.path, damit die Testmodule import tests.* auflösen.
Lokales CI-Gate: scripts/check.sh bündelt ruff + mypy + pytest
(spiegelt .forgejo/workflows/ci.yml). Als Pre-Push-Hook aktivieren — er
blockt einen Push bei Fehlern:
git config core.hooksPath .githooks # einmalig pro Clone
Einen einzelnen Push im Notfall umgehen: git push --no-verify.
Die Test-Suite deckt die Prompt-Eingabeschicht (Datei- und URL-Quellen inkl.
SSRF-Abwehr mit gemockter DNS-Auflösung und DNS-Pinning), die
Konfigurationsauflösung (inkl. ${ENV}-Expansion), die CLI-Validierung, die
Action-Orchestrierung (do_start/do_change/do_check/do_chat, Locking,
dry-run) sowie Docker-/HTTP-Operationen (mit gemocktem subprocess bzw.
requests) ab.
Für einen End-to-End-Test gegen einen echten Docker + llama.cpp-Server (u. a.
--api-key-Round-Trip, Streaming, --check-Exit-Codes) gibt es einen opt-in
Rauchtest — er startet einen separaten Container und räumt danach auf:
SMOKE_GPU=1 scripts/smoke.sh
Lizenz
MIT License
Copyright (c) 2026 Dieter Schlüter