Saltnitor is a Terminal User Interface (TUI) built in Rust for Linux with NVIDIA GPUs, serving as a command center for orchestrating local Large Language Models (LLMs) and monitoring hybrid hardware pressure between GPU VRAM and System RAM.
Designed for developers running llama.cpp on Linux, Saltnitor provides hardware telemetry, log viewing and service control in a single binary. It also exposes an OpenAI-compatible control endpoint, so any IDE or AI coding agent that speaks the OpenAI chat API (Pi, OpenCode, Cline, Aider, Continue, β¦) can drive automatic model hot-swaps just by addressing different models.
-
Hardware Telemetry: Reads CPU and NVIDIA GPU specifications on boot (via
sysinfoandnvidia-smi) and shows CPU load sparklines and VRAM/RAM saturation gauges. Linux and NVIDIA only. -
Hardware Inspectors:
-
GPU Inspector (
g): VRAM allocation, temperature, power draw and fan speed, plus the compute processes using VRAM (PID, user, RAM, VRAM). -
CPU/System Inspector (
c): Inline gauges, per-thread load bars and the top RAM processes (PID, user, RAM, VRAM). -
Process control (
x/X): In either inspector,xsends SIGTERM and escalates after[process] term_grace_ms;Xasksy/Nand then sends SIGKILL. Both act on the exact PID you selected (checked against its start time and owner, signalled through a pidfd). Nothing is ever killed by name, and PID 1, Saltnitor itself, the llama-server runtime processes and processes owned by other users are refused with a reason.
-
-
Live Model Orchestration (Dual-Mode Bottom Deck):
-
Hot-Swap: Cycle the available
.ggufmodels. A VRAM oracle estimates the footprint (from the file name, or fromest_vram_gb/est_ram_gbin the profile) and warns before you swap. It is an estimate: it does not yet account for VRAM held by other processes. -
Router tuner (
t): A paginated editor that writes the tuned flags into the active model's[section]of therouter.ininamed byrouter_iniinconfig.toml, then restarts the unit. Withoutrouter_inithe tuner refuses to apply. Only these keys are written:ngl,ctx-size,batch-size,ubatch-size,threads,threads-batch,parallel,flash-attn,cache-type-k,cache-type-v,cont-batching,rope-freq-base,rope-freq-scale,defrag-thold,mlock,no-mmap.- Page 1 (Compute & Memory):
ngl,ctx, threads, batch, parallel slots, Flash Attention,mlock,no_mmap, KV cache types. - Page 2 (Context & Speculation): RoPE scaling, defrag threshold, and the draft-model controls. The draft-model controls are displayed but not yet written to
router.ini(BD-17). - Page 3 (Orchestration): threads per batch, u-batch, continuous batching, plus context-shift, metrics and API Key toggles. Context shift and metrics are not yet written (BD-17). The API Key toggle only decides whether the hot-swap's direct warm-up call to the router sends
infer_bearer(never the client key, never to a non-loopbackhost); it does not change the router.
- Page 1 (Compute & Memory):
-
-
API Interrogator (
i): A mini-console that sends a request through Saltnitor's own endpoint (http://127.0.0.1:<control_port>/v1/chat/completions) with the client key.- Metrics: Client-side time-to-first-token, and prompt / generation tokens-per-second taken from the runtime's
timings. When the runtime sends no timings the rate is shown as an estimate (est.) orn/a, never as0. - Client key: Set
client_key_envto the name of an environment variable holding the key; the interrogator sends it as a Bearer token. - History: The last 10 commands are saved on exit to
$XDG_STATE_HOME/saltnitor/history, or~/.local/state/saltnitor/historywhenXDG_STATE_HOMEis unset.
- Metrics: Client-side time-to-first-token, and prompt / generation tokens-per-second taken from the runtime's
-
Incident Response:
- Crash Dump (
Ctrl+D): Exports system state (VRAM/RAM pressure, temperatures, active model, last 100 log lines) to$HOME/saltnitor_crash_<timestamp>.txt. The path is printed to the log and write failures are reported. Configured tokens are redacted from the dump and from logs. - Kill-Switch (
Ctrl+K): Runssudo -n systemctl stop <service>. Because the service runs withRestart=always, a plain process kill is respawned within seconds; stopping the unit is what frees VRAM and keeps it down until you restart it (Shift+S).
- Crash Dump (
- OS: Linux (Optimized for Pop!_OS / Ubuntu / Arch).
- Systemd: Required for log streaming and service management.
- NVIDIA Drivers: Required for GPU telemetry (via
nvidia-smi). - llama.cpp: A build whose
llama-serversupports the native router (--models-preset), orchestrated via thelaunch_router.shbash wrapper (example inexamples/external-mode/).
- Clone the Repository
git clone https://github.com/Saltless-bruh/saltnitor.git cd saltnitor - Build for Release
cargo build --release
- Set up the router + hot-swap β see Hot-Swap Setup below to create
router.ini,launch_router.sh, and thellama-routerservice. - Run Saltnitor (as your normal user once the sudoers drop-in below is in place)
./target/release/saltnitor
The hot-swap is driven by llama.cpp's native router. Saltnitor adds the VRAM oracle, the live TUI, and an OpenAI-compatible proxy in front of it. Setup is four small pieces.
Each [section] is the model id that callers put in the "model" field. Keys are llama-server flags without the leading dashes; [*] holds global defaults. Section names are arbitrary β pick whatever you'll address from your IDE.
[*] # defaults applied to every model
flash-attn = on
threads = 7
ctx-size = 32768
[fast] # an agent requests "model": "fast"
model = /home/you/models/qwen3-9b-Q5_K_XL.gguf
ngl = 99
[deep] # an agent requests "model": "deep"
model = /home/you/models/qwen3-30b-A3B-Q4_K_XL.gguf
ngl = 99
override-tensor = .ffn_.*_exps.=CPU # offload MoE experts to RAM--models-max 1 keeps exactly one model resident, so a request for a different id evicts the incumbent and loads the new one. exec makes llama-server the unit's main process so the Kill-Switch can actually stop it.
#!/usr/bin/env bash
set -euo pipefail
exec /usr/local/bin/llama-server \
--models-preset /home/you/llama.cpp/router.ini \
--models-max 1 --host 127.0.0.1 --port 8080Run it under a systemd unit (llama-router.service, User=<you>, Restart=always, plus KillSignal=SIGKILL + TimeoutStopSec=10 so the Kill-Switch is instant). Then grant your user passwordless control of just that service:
# /etc/sudoers.d/saltnitor (visudo -f)
you ALL=(root) NOPASSWD: /usr/bin/systemctl start llama-router, \
/usr/bin/systemctl stop llama-router, /usr/bin/systemctl restart llama-router
Saltnitor reads --config <path>, else $XDG_CONFIG_HOME/saltnitor/config.toml, else ~/.config/saltnitor/config.toml. The file is strict: a syntax error, a wrong type or an unknown key makes Saltnitor print the file, line, key and expectation and exit with status 2. It never falls back to defaults. CLI flags override the file. Profile keys must match the router.ini section names.
control_port = 8765
router_base = "http://127.0.0.1:8080"
router_ini = "/home/you/llama.cpp/router.ini" # required for the tuner (Enter) to write anything
infer_bearer = "<router api-key if the router runs with --api-key>" # only if the router uses --api-key
reserve_vram_gb = 0.8
reserve_ram_gb = 1.0
# Secrets: prefer an environment variable or a private file over a literal in this file.
control_token_env = "SALTNITOR_TOKEN" # name of the env var holding the control token ...
# control_token_file = "/home/you/.config/saltnitor/token" # ... or a file (mode 0600, owned by you)
client_key_env = "SALTNITOR_CLIENT_KEY" # env var the interrogator sends as its Bearer key
allow_query_token = false # default; true re-enables ?token= on GET /v1/ensure/stream only
max_body_bytes = 33554432 # request body limit (default 32 MiB)
[timeouts] # proxy to llama-server, in milliseconds (defaults shown)
connect_ms = 5000
first_byte_ms = 600000
idle_ms = 120000 # max silence between streamed chunks
[process]
term_grace_ms = 5000 # SIGTERM -> escalation window for the `x` key
[profiles.fast]
model = "qwen3-9b-Q5_K_XL.gguf"
est_vram_gb = 9.0
[profiles.deep]
model = "qwen3-30b-A3B-Q4_K_XL.gguf"
offload = true
est_vram_gb = 9.0
est_ram_gb = 18.0Authentication: when a control token is configured, every /v1 route requires Authorization: Bearer <token>. Failures are JSON error envelopes. Client credentials are never forwarded to llama-server (use infer_bearer for that). With no token configured the API is open on loopback only. See SECURITY.md. Which key goes where: the interrogator (i) talks to Saltnitor with client_key_env (set it to the control token); your agents use the control token as their API key; Saltnitor talks to llama-server with infer_bearer; the TUI hot-swap's direct call to the router also uses infer_bearer.
Use your section names as the model ids. Two endpoints are available:
| Endpoint | URL | Behavior |
|---|---|---|
| Through Saltnitor (recommended) | http://127.0.0.1:8765/v1 |
Oracle-gated (refuses loads it estimates will not fit); authenticated; responses are streamed through chunk by chunk; swap shown live in the TUI |
| Straight to the router | http://127.0.0.1:8080/v1 |
Router auto-swaps; no oracle gate or TUI indicator |
Set one agent/model to fast and another to deep, and switching agents switches the model β automatically. Point any OpenAI-compatible client at the base URL above and set its API key to your control token.
| Key | Action |
|---|---|
q |
Quit Program |
h |
Open Interactive Command Manual |
PgUp / PgDn |
Scroll Log Streamer History |
t |
Open Deep Engine Tuner |
Tab |
Toggle Bottom Deck (Interrogator vs Hot-Swap) |
Enter |
Apply Tuner (write router.ini section + restart) / Fire Payload / Pin Model |
i |
Focus Active Bottom Deck (Insert Mode) |
Esc |
Exit Insert Mode |
Up / Down |
Cycle History / Inspector Process Selection |
g |
Toggle GPU Hardware Inspector |
c |
Toggle CPU/System Hardware Inspector |
x / X (in an inspector) |
Terminate (SIGTERM, then escalate) / Kill (SIGKILL, y/N confirm) the selected PID |
Shift + S/X/R |
Daemon Start / Stop / Restart (outside an inspector) |
Ctrl+D |
Tactical Crash Dump (save state to $HOME/saltnitor_crash_*.txt) |
Ctrl+K |
Tactical Kill-Switch (systemctl stop llama-router) |
- Permission model: Saltnitor uses
sudo -nonly for the threesystemctlactions on the configured service (llama-routerby default). The recommended setup is the sudoers drop-in above, which lets you run the TUI as your normal user with no password prompts. Running the whole TUI withsudoalso works but isn't necessary, and the service itself should run as your user (User=<you>), not root. - Model ids are your contract: the id an agent sends must match a
router.inisection name and a[profiles.*]key inconfig.toml. They are case-sensitive. - First call to a cold model is slower (the load plus a one-token warm-up); later calls to the resident model skip it. Pre-warm with the control API's
/v1/ensureif you want to hide it. - Terminal Sizing Guardrails: Saltnitor requires a minimum terminal footprint of 80x16. If the window is resized below this threshold, rendering will halt to prevent mathematical panics.