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.
  • Python 94.4%
  • Shell 5.6%
Find a file
dschlueter b0c215abbb feat(scripts): detect how a model evades, not just whether
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>
2026-07-10 17:10:39 +02:00
.forgejo/workflows feat(scripts): add prompt-test evaluator and suite runner 2026-07-10 16:25:26 +02:00
.githooks ci: add local pre-push gate (ruff + mypy + pytest) 2026-07-07 10:48:57 +02:00
docs feat(scripts): detect how a model evades, not just whether 2026-07-10 17:10:39 +02:00
example_system_prompts docs(prompts): further harden coding prompt against verified defects 2026-07-07 22:40:58 +02:00
example_user_prompts docs(prompts): add three alignment probe prompts for prosa 2026-07-10 17:10:39 +02:00
man docs(config): clarify profile names and make examples copy-paste safe 2026-07-07 09:53:43 +02:00
scripts feat(scripts): detect how a model evades, not just whether 2026-07-10 17:10:39 +02:00
src/llamacppctl fix(cli): stop --print-effective-config from starting a container 2026-07-10 16:25:07 +02:00
tests fix(cli): stop --print-effective-config from starting a container 2026-07-10 16:25:07 +02:00
.gitignore chore(repo): add LICENSE, CI workflow, and untrack local config 2026-07-07 10:23:05 +02:00
build_archive.py test: cover build_archive and add it to the ruff scope 2026-07-07 11:20:57 +02:00
CHANGELOG.md docs: add model profiles, evaluation rubric, and CLAUDE.md 2026-07-10 16:25:26 +02:00
CLAUDE.md docs: add model profiles, evaluation rubric, and CLAUDE.md 2026-07-10 16:25:26 +02:00
LICENSE chore(repo): add LICENSE, CI workflow, and untrack local config 2026-07-07 10:23:05 +02:00
llama.cpp.config.example feat(docker): support a multimodal projector via --mmproj 2026-07-10 16:24:47 +02:00
pyproject.toml chore(ci): add ruff and mypy, wire them into CI 2026-07-07 10:29:07 +02:00
README.md fix(cli): stop --print-effective-config from starting a container 2026-07-10 16:25:07 +02:00
requirements-dev.txt build: make pyproject the single source of dependency truth 2026-07-07 10:36:00 +02:00
requirements.txt build: make pyproject the single source of dependency truth 2026-07-07 10:36:00 +02:00

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-url gewinnt 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):

  1. Eingebaute Defaults
  2. [default]-Sektion
  3. [model.<profile>]-Sektion (falls --profile gesetzt)
  4. 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-file lesen 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 über Content-Length` als auch beim Streaming.
  • Der Port wird standardmäßig nur auf 127.0.0.1 veröffentlicht — der ungeauthentifizierte OpenAI-Endpoint ist also nicht im LAN erreichbar. --expose/expose = true bindet auf alle Interfaces; dann sollte per --api-key/api_key ein Schlüssel gesetzt werden (Server verlangt ihn, das Tool sendet ihn als Bearer-Token).
  • container_name ist 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.
  • --change verwendet 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