llamacppctl/CLAUDE.md
dschlueter bee6a71de0 docs: add model profiles, evaluation rubric, and CLAUDE.md
KI_TOOLS_PROFILES.md records what each local GGUF is actually for, based on
the model cards: ornith is a purpose-built agentic coding model, Carnice targets
agent runtimes, Qwopus is reasoning plus vision, and the two HauhauCS models are
abliterated — an axis about refusals, not literary quality. It also maps which
models the local mmproj.gguf fits (the Qwen3.6-35B-A3B based ones, not ornith).
Capability and benchmark claims are attributed to their authors, not asserted.

EVAL_RUBRIC.md separates what a script can measure from what needs reading, and
warns that a passing test suite only proves the code satisfies its own tests.

CHANGELOG covers the mmproj feature, the --print-effective-config fix, and the
new tooling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:25:26 +02:00

5.8 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Commands

pip install -e ".[dev]"          # dev install
scripts/check.sh                 # full local CI gate (ruff + mypy + pytest) — mirrors .forgejo/workflows/ci.yml
python -m pytest -q              # tests
python -m pytest tests/test_prompt_urls.py -q            # one file
python -m pytest tests/test_actions.py::test_do_start -q # one test
ruff check src/ tests/ build_archive.py
mypy                             # config in pyproject.toml (files = src/llamacppctl)

Always use python -m pytest, never the pytest console script: the test modules do from tests.test_docker_ops import ..., which needs the repo root on sys.path.

git config core.hooksPath .githooks (once per clone) enables the pre-push hook that runs scripts/check.sh. CI runs on Python 3.10 and 3.12; requires-python = ">=3.10".

Other entry points:

  • python3 build_archive.py — builds and self-verifies llamacppctl-installable.tar.gz (has its own required-file manifest; adding a top-level file that must ship means updating it).
  • SMOKE_GPU=1 scripts/smoke.sh — opt-in end-to-end test against a real Docker + GPU + GGUF model. Not part of the pytest suite.

Architecture

A single CLI (llamacppctl) that starts/checks/stops/switches llama.cpp servers running as Docker containers, plus a security-hardened input layer for system/user prompts from text, file, or URL.

main.py is a thin pipeline: parse (cli.build_parser) → validate (cli.validate_args) → docker_available() → resolve prompt sources (prompt_io) → resolve config (config.resolve_effective_config) → dispatch to actions.do_*.

Layering is deliberate and must be preserved — it keeps the security-critical input validation independently testable from orchestration:

  • prompt_io.py knows nothing about argparse; it works only with PromptSource + InputPolicy.
  • config.py knows nothing about Docker or HTTP.
  • docker_ops.py / http_ops.py know nothing about CLI semantics.
  • actions.py orchestrates; schema.py holds the shared dataclasses (ServerConfig, PromptConfig, ChatReply, CheckResult).

Two cross-module couplings to know about:

  • main._resolve_prompts() stashes loaded prompts on the argparse namespace as args._resolved_system_prompt / args._resolved_user_prompt; config.py picks them up there.
  • main.run() calls docker_available() before config resolution, so --dry-run still needs a reachable Docker daemon. Only --print-effective-config skips that check — it is a standalone action in the mutually-exclusive group, never a modifier, so it cannot start anything.

Config resolution

INI file (llama.cpp.config, template in llama.cpp.config.example), lowest to highest priority: builtin defaults (config.builtin_defaults()) → [default][model.<profile>] (via --profile) → CLI overrides. [prompt.<name>] sections resolve separately and only supply a fallback system prompt — an explicit -s/--system-file/--system-url always wins.

container_name is mandatory (empty → ConfigError). It is the only identity anchor: Docker --name, the --stop/--check target, and the derived lock path /tmp/llamacppctl.<container_name>.lock. Profiles meant to run in parallel need distinct container_name and host_port, otherwise a second --start silently replaces the first container.

max_tokens / chat_temperature / connect_timeout / read_timeout affect only the chat request (--chat and the reply printed after --start), never the container.

Exit codes (actions.py)

0 ok · 1 general/config/prompt error · 2 argparse validation · 3 model path missing · 4 docker error · 5 HTTP not ready (also --check failing) · 6 lock busy. --check is meant to be scriptable; keep those codes stable.

Invariants worth not breaking

  • All Docker calls go through subprocess.run() with argument lists — never shell=True, never composed shell strings.
  • The port publishes to 127.0.0.1 by default (docker_ops._port_publish()); the llama.cpp OpenAI endpoint is unauthenticated unless api_key is set. --expose binds all interfaces.
  • Every HTTP request in prompt_io — including each redirect hop — passes validate_url_target(): HTTPS-only, no IP literals, no embedded credentials, no localhost, all resolved addresses classified (private/loopback/link-local/multicast/reserved/unspecified → reject), metadata IP 169.254.169.254 blocked. _pin_dns() then restricts getaddrinfo to the already-validated IPs for the duration of the request, closing the TOCTOU/DNS-rebinding window. Size limits are enforced both from Content-Length and while streaming.
  • Escape hatches (--allow-private-url, --allow-insecure-http, --allow-ip-host, --allow-symlinks, --follow-redirects, --allow-html-input) are opt-in and off by default. --allow-private-url and --url-allow-host are mutually exclusive.
  • --start and --change hold an exclusive non-blocking fcntl.flock; --change validates the model path before removing the running container. --dry-run takes no lock and touches nothing.

Tests mock subprocess and requests (and DNS resolution for the SSRF cases); nothing in the pytest suite requires Docker or the network.

Conventions

  • Docs, comments in docs/, and the example prompts are in German; source code, docstrings, and commit subjects are in English. Commits follow Conventional Commits (feat(chat):, docs(prompts):).
  • Line length 100, from __future__ import annotations in every module, py.typed is shipped — keep annotations complete enough for mypy to pass.
  • Behavior changes should be reflected in man/llamacppctl.1, CHANGELOG.md, and — for security- relevant ones — docs/SECURITY_AND_OPERATIONS.md.