A terminal monitor for your own LLM server. One small collector runs on the machine that
serves the model; lss, a full-screen terminal view plus a plain-text / JSON CLI, runs on any
machine that can reach it. No Grafana, no Prometheus, no browser, no account, no cloud.
curl -fsSL https://github.com/kachowtowmater/lss/releases/latest/download/install.sh | bash
lss setup
lssIt answers, in plain words:
- What is worst right now, and what does it cost me? One line at the top names the current bottleneck - a hot GPU, a full KV cache, requests held at a gateway, an engine that stopped producing tokens - and says what it means for you, not just which number moved.
- Is it up, and how is it serving? time to first token (p50/p90/p99), reading (prefill) and writing (decode) speed, what is running and waiting, KV memory, speculative acceptance, cache hits.
- What is it costing me? your GPUs' power priced against your electricity plan, right now and for today, yesterday, this week and this month, and the cost per million tokens of real work.
- Who is using it, and how much? tokens served by hour / day / week / month, and per user when a gateway in front of the engine publishes them.
- How good is THIS model on THIS hardware? speed at 1, 2, 4, 8 users at once, reading speed, memory, energy per million tokens, a repeatable benchmark per model, and a side-by-side comparison with the model before.
- What went wrong, and when? alerts that fire and clear, and a dated record of incidents: outages, restarts, GPU Xid errors.
- A machine serving a model with SGLang, vLLM, llama.cpp server, Ollama, LM Studio, TGI, or
anything OpenAI-compatible.
lss-collector --detectfinds it; you do not have to say which. - Linux or macOS. NVIDIA GPUs are read through NVML (the driver's own library, falling back
to
nvidia-smi); AMD throughamd-smi/rocm-smi; Apple silicon throughioreg. No GPU tool at all is fine - the GPU parts say so. - Docker is optional (it adds container restarts and the exact launch settings of each model).
- A gateway in front of the engine is optional (it adds lanes, per-user numbers and the gateway's own counters).
- Rust only if there is no release binary for your machine - see Install.
Nothing talks to the internet on its own. The one exception is the cost wizard's IP lookup, which makes a single HTTPS request and only when you say yes. The only request lss ever sends your model is a tiny idle test (below), and a benchmark only when you ask for one.
One line, on the machine that serves the model (or any machine that can reach it):
curl -fsSL https://github.com/kachowtowmater/lss/releases/latest/download/install.sh | bash
lss
Or download the installer first, to read it before it runs (the same release, the same checks):
curl -fsSLo install.sh https://github.com/kachowtowmater/lss/releases/latest/download/install.sh
bash install.sh
Or from a clone:
git clone https://github.com/kachowtowmater/lss && cd lss
./install.sh # asks before each step
./install.sh --dry-run # prints every step it would take, changes nothing
./install.sh --yes # accepts every default, asks nothing (CI, scripts)
Piped from curl, the installer reads its questions from your terminal (/dev/tty), never from
the pipe, so the one-liner is just as interactive as ./install.sh. Every option also works
piped: curl -fsSL .../install.sh | bash -s -- --yes.
The installer does three things, and nothing needs root:
- Gets the programs.
lss(the screen) andlss-collector(the background part) go into~/.local/bin. It downloads the release built for your machine and checks it against the release's published.sha256file before installing it. From a clone with no matching release it builds with Rust. With no release and no Rust it stops, installs nothing, and says where to get Rust (https://rustup.rs). It never leaves a half-install. - Runs the setup wizard (
lss setup, below): finds your LLM server, tests it live, sets the electricity rate, writes~/.config/lss/. - Runs the collector in the background, then prints the one command to type:
lss.-
macOS: a launchd agent (
ai.lss.collector). It starts at login and restarts by itself. -
Linux with a systemd user session (a normal login, or ssh): a systemd
--userunit,lss-collector. It starts at login and restarts by itself.sudo loginctl enable-linger $USERkeeps it running while you are logged out. -
Linux without one (a container, WSL, a shell reached with
suorsudo -u): the installer saysno systemd --user session and no launchd here (a container?)and starts the collector withnohupinstead. Its process id goes in~/.local/state/lss/lss-collector.pidand its log in~/.local/state/lss/lss-collector.log. It does not come back after a reboot. To start it again, run the command the installer printed (it writes the log and a fresh pid file, as the installer did):nohup ~/.local/bin/lss-collector --config ~/.config/lss/collector.toml >>~/.local/state/lss/lss-collector.log 2>&1 & echo $! > ~/.local/state/lss/lss-collector.pid. To get the systemd unit instead on a machine that has systemd:- Run
sudo loginctl enable-linger $USER. - Log in as that user directly (ssh, not
su), orexport XDG_RUNTIME_DIR=/run/user/$(id -u).systemctl --userneeds one of these. - Run the installer again. It first stops the
nohupcollector it started before (the one in the pid file, and only if that process really islss-collector), then starts the unit. It keeps your settings and asks before replacing any file.
In a container, start the collector from the container's own start command instead.
- Run
-
Installer options: --prefix DIR, --version vX.Y.Z, --repo OWNER/NAME (or $LSS_REPO),
--binary-dir DIR (install programs you already have), --no-build, --no-service, and every
setup-wizard flag below. ./install.sh --help lists them all.
What each engine publishes, and so which numbers read n/a (not reported by <engine>) instead of
a made-up zero, is in docs/ENGINES.md.
The wizard scans this machine only. When the engine runs on another box (the GPU server on your
LAN), install lss here anyway: at Which one should lss watch? answer r (r = the server is on another machine); when nothing answered here it asks for the address straight away. Type it as
host:port (e.g. 192.0.2.20:8000), pick the engine (0 = recognise it) and its API key if it
has one. The address is tested live before anything is written: one that refuses, times out or is
not an LLM server is explained in one plain line and asked again (e = change the address or key) - it is never written silently, and with no one to ask the wizard stops with nothing written.
Once it answers, the wizard says what this split shows: the collector on this machine reads the
engine's speed, queue and cache over HTTP, but GPU temperature, power and memory, Docker restarts
and the kernel Xid log need the collector ON the GPU box. Install it there with the same
one-liner, curl -fsSL https://github.com/kachowtowmater/lss/releases/latest/download/install.sh | bash,
then read that collector from here (Point it at YOUR machine, below).
The installer runs it for you. Run it again any time, for example to point lss at another engine:
lss setup
A real run, answered at a terminal (the e2e gate on macOS at commit dd18a2b, against a stand-in
vLLM server). Steps 1-4 are shown. Two edits: the throwaway home directory is shortened to ~,
and the test driver's own notes (the WIZARD-ANSWER lines that log each answer it typed) and
one blank line are left out. Every other line is as printed:
== 1/5 Finding your LLM server
asking the usual ports on this machine (SGLang, vLLM, llama.cpp, Ollama, LM Studio, TGI, any OpenAI-compatible server) ...
1) vLLM at http://127.0.0.1:18431 - e2e-fake-vllm [sure: /metrics carries `vllm:` series]
Which one should lss watch? (1, r = the server is on another machine, q = quit) [1]
== 2/5 Testing http://127.0.0.1:18431
[ok] models: e2e-fake-vllm
[ok] engine: vLLM (/metrics carries `vllm:` series)
[ok] metrics: vLLM publishes 5 of the 16 numbers lss shows (the rest read 'n/a' - docs/ENGINES.md says which and why)
== 3/5 Electricity cost (optional)
Your country (2-letter code or name; blank = US) [US]:
Electricity cost - how should lss price the power your GPUs draw?
1) my ZIP code - my state's average home rate (U.S. EIA, 2026-06); nothing is sent anywhere
2) look it up - from my internet address, via ONE request to ipinfo.io (asks first)
3) type my own rate - $ per kWh from my electricity bill (the most accurate)
4) skip - no cost figures for now
Choose 1-4 [1]
Your 5-digit ZIP code:
94103
ZIP 94103 is in California: average home rate 34.74c/kWh (U.S. EIA, 2026-06, published August 26, 2026).
Use it? [Y/n]
rate: CA avg (EIA 2026-06) = $0.3474 /kWh -> ~/.config/lss/rates.toml
restart the collector to use it (it reads rates.toml at start).
== 4/5 The settings in ~/.config/lss
wrote ~/.config/lss/collector.toml
wrote ~/.config/lss/lss.toml
It goes through these steps:
- Find. It looks for SGLang, vLLM, llama.cpp, Ollama, LM Studio, TGI or anything
OpenAI-compatible on this machine, and shows what answered and how sure it is. Found nothing,
or the engine is on another machine? Type its address (
localhost:8000,192.0.2.20:8000,http://host:portorhttps://host:port) and, if the engine was started with one, its API key. - Test, live. It asks the engine for its models and its metrics, and says in plain words what
is wrong if something is: nothing listening on that port, a host name the network does not
know, a missing or wrong API key, a server still loading its model. It also says which metrics
you will get. For example, SGLang publishes them only with
--enable-metricsand llama.cpp only with--metrics, while Ollama and LM Studio have no metrics endpoint at all (that is normal: lss reads what they do publish). Anhttps://engine whose certificate this machine does not trust (usual for a box that made its own) gets two choices: [c] give the path of its certificate or of the CA that signed it (saved astls_ca_file), or [i] do not check its certificate (tls_verify = falsefor that engine, with a warning at every start). - Electricity cost. This is the cost wizard, below.
- Write the configs:
~/.config/lss/collector.tomlandlss.toml. It never replaces a file without asking, and it keeps a.bakcopy when you say yes. A re-run changes only the engine part of an existingcollector.toml. - Restart the background collector, then tell you to type
lss.
Without questions (all optional; the installer accepts the same flags):
lss setup --url 192.0.2.20:8000 --kind vllm --api-key "$KEY" --zip 94103 --yes
--url ADDR skips the scan · --kind sglang|vllm|llamacpp|ollama|lmstudio|tgi|openai|auto ·
--api-key KEY (or $LSS_API_KEY) · --all watches every engine found (one collector each) ·
--zip ZIP | --rate USD_PER_KWH | --from-ip | --skip-cost answers the cost step · --yes asks
nothing · --force replaces existing configs (keeping a .bak) · --no-service leaves the
background collector alone. Exit codes: 0 done, 1 could not write, or a --zip/--rate/--from-ip
you gave was refused or failed (e.g. --rate 2.5: cents or dollars? - setup stops at the cost step
and writes nothing; pass --rate 2.5c or --rate 0.31, or --skip-cost), 2 bad arguments, 3 you
stopped it (nothing written).
Which collector lss reads: --url, else $LSS_URL, else url in ~/.config/lss/lss.toml
(example), else http://127.0.0.1:8099. To read the collector from
another machine, add the GPU box's LAN or VPN address to listen in ~/.config/lss/collector.toml
and restart it.
Which engine the collector watches: nothing to do by default (engine_url = "auto" finds it
and keeps looking until one answers). To pin it, add an [[engine]] block - lss-collector --detect prints the exact block to paste.
Several servers: one [[server]] name, url entry per collector in lss.toml. [ and ]
switch between them, a FLEET line shows every one, and f opens a page with one row per server.
Cost is off until you give it a rate. lss never guesses one. The setup wizard asks, and you can
re-run just this step with lss-collector cost-setup. It asks your country first: blank or
US gets the four choices below; any other country skips the US-only averages and asks for the
price per kWh from your bill, in your own currency (0.25 for 25 cents - no cents guessing, so 31
is 31 yen), and records the country and currency in the file (country = "DE", currency = "EUR";
lss still prints a $ sign - the number is what you typed). In the US there are four ways to answer:
| choice | what it does | network |
|---|---|---|
your ZIP code (--zip 94103) |
your state's average residential rate, from a table built into the program: the U.S. EIA's Electric Power Monthly, dated in the file it writes | none |
look it up from my IP (--from-ip) |
finds your state from this machine's public IP, then uses the same table. Only asked with your consent, and the flag IS the consent | one HTTPS request to ipinfo.io |
type your rate (--rate 0.31) |
your own $/kWh from your bill, the most accurate choice. 31c and 31 also read as cents |
none |
skip (--skip) |
writes nothing; cost stays off | none |
With --zip 94103 and no questions, the same step printed this in the gate's run:
ZIP 94103 -> California: 34.74c/kWh average residential (EIA, 2026-06)
rate: CA avg (EIA 2026-06) = $0.3474 /kWh -> ~/.config/lss/rates.toml
restart the collector to use it (it reads rates.toml at start).
A ZIP or IP answer is an average for the state, not your tariff. The file it writes,
~/.config/lss/rates.toml, says where the rate came from and how old it is, and lss shows that
source next to the cost. It never replaces an existing rates.toml without asking (--force
does; --dry-run prints the file instead of writing it). Exit codes: 0 written or skipped,
1 lookup failed (a ZIP it does not cover, outside the US, no network) or --rate refused (a bare
small number like 2.5 could be cents or dollars: pass 2.5c or 0.025), 2 bad arguments,
3 an existing file was kept.
Time-of-use, or your exact tariff: write the file yourself.
cp packaging/rates.toml.example ~/.config/lss/rates.toml
$EDITOR ~/.config/lss/rates.toml
The example holds two shapes, both with obviously fake numbers:
- flat - one $/kWh at every hour;
- time-of-use - rates by season, weekday vs weekend/holiday, and hour, the way most utility tariffs actually bill. Each 5-second power sample is priced at the rate in force at ITS OWN time, so a 4-9pm peak is never averaged away.
Copy the numbers from your own bill or your utility's published tariff, and set effective_date
to the tariff's own date: it is printed next to every dollar figure, so a table that has gone
stale stays visible instead of being trusted forever. A daily fixed charge you set is shown for
context and never added into a per-hour or per-token figure. An amount you are not sure
applies (a line item that may already be inside the headline rate) goes in
unresolved_usd_per_kwh and is shown as a stated uncertainty rather than silently counted.
What you then see: the rate right now and its period, dollars per hour at the current draw,
today and the last 24 h on page 1, today / yesterday / this week / this month in lss status
(each saying how much of its window it actually covers), a 30-day daily chart on the TOKENS page, and cost per million tokens of real
work - the energy divided by the tokens the engine actually computed (uncached prompt tokens +
generated tokens), with idle standby power reported separately so a quiet day does not make a
token look expensive. Every figure says what it excludes: this is the electricity your GPUs drew
at your tariff, not your whole bill, not the rest of the machine, not the hardware.
The WATCH page (w) shows what the projects you follow for serving recipes have published
lately - engine releases, model repositories, recipe repos - with a NEW mark on anything since
you last looked. lss never fetches anything to build it. You choose the sources and the
schedule; lss only reads the result back.
cp packaging/watch-sources.txt.example ~/.config/lss/watch-sources.txt # one source per line: name|what it feeds|owner/repo
$EDITOR ~/.config/lss/watch-sources.txt
scripts/watch-check.sh # writes ~/.config/lss/watch.json
Run scripts/watch-check.sh on whatever schedule you like (a systemd timer is in
packaging/lss-watch.timer), or write watch.json with anything you
prefer - its shape is in packaging/watch.json.example. A summary
with nothing behind it (no release, commit or issue link) is marked UNVERIFIED, never shown
as a finding. No file = the page says tracking is off.
The collector runs any program you name as alert_cmd <severity> <message> (info, warn,
page, hardware). The wizard installs lss-notify.sh and points alert_cmd at it; tell it
where to deliver in ~/.config/lss/alert.env (example):
LSS_NOTIFY_NTFY=https://ntfy.sh/<your-private-topic>- a push to your phone, no account;LSS_NOTIFY_WEBHOOK=<url>- any HTTP endpoint, POSTed{"severity":…,"message":…};LSS_NOTIFY_CMD="notify-send LLM-SERVER"- any program you already have.
With none set it shows a desktop notification where it can and otherwise says plainly that nobody
is listening. Prove the path end to end: lss-collector --test-alert "install check", then
lss alerts. LSS_NOTIFY_DRY_RUN=1 ~/.local/bin/lss-notify.sh warn test prints what it would send.
A machine that is down cannot report that it is down, so scripts/install-upcheck.sh installs a
small check on a second Mac that asks the GPU box every 60 s. Name what it should ask in
~/.config/lss/upcheck.env: LSS_UPCHECK_SERVE_URL (your engine's own port, or your gateway)
and LSS_UPCHECK_COLLECTOR_URL. The serve URL has no default on purpose - no port is right for
every engine, and a wrong guess would report "down" forever over a healthy server.
./install.sh --uninstall
curl -fsSL https://github.com/kachowtowmater/lss/releases/latest/download/install.sh | bash -s -- --uninstall
Either one asks first, then stops and removes the background service(s) and the programs
(lss, lss-collector, lss-notify.sh) from ~/.local/bin. Your configs in ~/.config/lss/
and your history in ~/.local/state/lss/ stay. Delete those two directories yourself to remove
everything.
| you see | what to do |
|---|---|
lss shows the collector as not answering |
Is it running? Check the way the installer started it (step 3 of Install), then curl http://127.0.0.1:8099/health. Linux, systemd unit: systemctl --user status lss-collector; log: journalctl --user -u lss-collector. Linux, nohup (the installer said no systemd --user session): kill -0 $(cat ~/.local/state/lss/lss-collector.pid) && echo running; log: ~/.local/state/lss/lss-collector.log; stop: kill $(cat ~/.local/state/lss/lss-collector.pid); start: nohup ~/.local/bin/lss-collector --config ~/.config/lss/collector.toml >>~/.local/state/lss/lss-collector.log 2>&1 & echo $! > ~/.local/state/lss/lss-collector.pid. Running lss setup again also restarts it. macOS: launchctl list | grep ai.lss.collector; log: ~/.local/state/lss/. |
| the collector is gone after a reboot (Linux) | It was started with nohup (no systemd user session at install time). Start it as above, or get a systemd unit: see step 3 of Install. |
| the wizard found no engine | Start the engine first, or give its address: lss setup --url localhost:8000. lss-collector --detect prints what it found on this machine, and why. |
| "refused the connection" for an engine on another machine | The engine is listening only on its own 127.0.0.1. Start it with --host 0.0.0.0 (llama.cpp, vLLM, SGLang), or for Ollama set OLLAMA_HOST=0.0.0.0. |
| "wants an API key" / "refused the API key" (HTTP 401/403) | Re-run lss setup and type the key the engine was started with (--api-key, or OPENAI_API_KEY). |
many numbers read n/a (not reported by …) |
That engine does not publish them, or needs a flag: SGLang --enable-metrics, llama.cpp --metrics. docs/ENGINES.md lists each engine. |
| no dollar figures | No rate is set yet: lss-collector cost-setup. |
| stops working after you log out (Linux, systemd unit) | sudo loginctl enable-linger $USER |
note: ~/.local/bin is not on your PATH |
Add the export PATH=... line the installer printed to your shell profile, or type ~/.local/bin/lss. |
the download does NOT match its published checksum ... - not installed |
The download was damaged or changed on the way. Nothing was installed. Run the installer again; if it happens every time, install from a clone (./install.sh, which builds with Rust). |
the certificate of an https:// engine is not accepted |
Re-run lss setup and choose [c] (its certificate or CA file) or [i] (do not check it). Details: docs/ENGINES.md. |
lss from another machine cannot connect |
Add that machine's reachable address to listen in the GPU box's ~/.config/lss/collector.toml, restart the collector, and give lss the same address with --url. |
What each alert on the screen means, and what to do about it: docs/RUNBOOK.md.
- It never changes your server. No restarts, reloads, config edits or model swaps. It reads.
- It does not route or limit traffic. That is a gateway's job; lss only reads one if you run it.
- It does not send your data anywhere. No telemetry, no cloud, no account. The collector listens on 127.0.0.1 unless you add an address.
- It does not load your model with its own traffic. The only request it sends on its own is the idle test: one short streamed completion every 5 minutes, only while nothing else is running. A test that overlapped real traffic is stored as invalid and kept out of every baseline and alert. The test and the benchmark are never counted as users.
- It does not benchmark unless you ask (
lss bench), and a benchmark refuses to start unless the server has been idle, and stops the moment anyone else uses it. - It does not guess your electricity price, your model's speed, or anything else it cannot measure - see below.
- It is not a full bill. Cost is the GPUs' measured power at your tariff; the rest of the machine, cooling, fixed charges and hardware are not in it.
These hold on every screen and in lss status, and tests pin them:
- A number that cannot be computed is an em dash with the reason -
— no priced samples yet today,— not enough rollup history yet- never a0standing in for "unknown", and never a confident sentence over a missing value. - A number the engine does not publish says so:
n/a (not reported by <engine>). - No cost figure without its rate's effective date and what it excludes.
- A window that is only partly covered says how much (a 3-day-old month says it is 3 days), instead of presenting a partial number as the full one.
- Totals never hide a missing server: across several servers, one the screen cannot reach drops out of every total and the totals line says how many were not counted - it never counts as a zero.
- One severity scale. The "worst now" line and the per-GPU boxes rank problems on one scale
with one set of words (
watch,high,critical,offline); a dead source always outranks a merely busy one; a fact about the setup (cost, model id, flags) is shown but can never be the headline. - Red means a problem and nothing else. A calm server has no red on screen.
Press v for page 1: the "worst now" line, then ALERTS and INCIDENTS, LOADOUT, ELECTRICITY
and COST PER 1M TOKENS on the left, and SERVE, LANES, TOKENS, USERS and one box per GPU on the
right (one column on a narrow pane; ALERTS/INCIDENTS lead the column at a tall enough pane so
they need no scrolling - a short, wide one keeps LOADOUT+ELECTRICITY first instead, since there
isn't room for both pairs). Every box shares one label column; a long value wraps under its own
column rather than being cut off; the page scrolls, and the footer says how much is below
(1-20 of 63). A GPU titled GPU0* is one you listed in thermal_exclude: its heat is still
shown and still counted in the digest, it just never raises a thermal alert and never becomes the
"worst now" line. v again returns to the classic overview (SERVE, GPUS, LANES, USERS, ADVICE,
INCIDENTS, ALERTS as a grid that adapts to the pane, L pins a layout).
| keys | |
|---|---|
v |
page 1 / the classic overview |
tab / shift+tab |
step through the overview and all ten pages, wraps - from page 1, the classic overview, or any page |
1 … 9 a |
jump straight to a page (a is ADVICE, the tenth) · esc or 0 back to the overview |
← → |
a page: same as tab / shift-tab · classic overview: move focus (see below) |
arrows, enter |
classic overview: move focus between boxes, enter opens the focused one full screen |
↑ ↓ PgUp PgDn Home End |
scroll |
r |
time range 15m → 1h → 6h → 24h → 7d |
c |
charts as lines / blocks |
w · f |
WATCH page · FLEET page (with more than one server) |
[ ] |
previous / next server |
s |
USERS: cycle the sort |
b |
MODEL: benchmark this model (asks first; from another machine it shows the ssh command) |
T · ? · q |
theme · help · quit |
The screen remembers theme, pinned layout and range in ~/.config/lss/ui.json ($LSS_PREFS
names another file). If the collector cannot be reached the screen says so, with the last-seen
time, and keeps showing the last data it had.
| page | what it answers |
|---|---|
| 1 LATENCY | time to first token, end-to-end, inter-token and queue time: p50/p90/p99 over time |
| 2 LOAD | decode and prompt tok/s, running and queued, KV use, cache hit rate, speculative acceptance, the idle-test history |
| 3 GPUS | per-GPU temperature, power, clock, utilisation, memory, throttle reasons with Xid errors, link / ECC health |
| 4 USERS | per user: lane, running now, requests and tokens, refusals, last seen (needs a gateway that publishes per-user numbers; it says so otherwise) |
| 5 TOKENS | exact tokens today / 1h / 24h / 7d / all time, tokens per hour, length distributions, and the 30-day spending chart |
| 6 MODEL | this model's scorecard on this hardware, the benchmark box, the comparison with the previous and the best model |
| 7 GATEWAY | per-lane and per-key requests and status codes, in-flight tokens (with a gateway) |
| 8 ALERTS | every rule with its state and threshold, alert history with delivery status |
| 9 INCIDENTS | dated record of what happened - restarts, outages, Xid errors, maintenance, uptime % |
| a ADVICE | one plain sentence per finding about settings and hardware - fine / watch / act (docs/ADVICE.md) |
lss live screen (in a terminal) · plain status once (when piped)
lss status one-shot status, including cost lss incidents | alerts | probe
lss users who is on the server, requests and tokens per user
lss tokens tokens served: today / 1h / 24h / 7d / all time
lss model how good this model is here: speed by users at once, memory, energy
lss loadouts every model + settings ever served, with its headline numbers
lss compare A B two loadouts side by side (current | previous | best | id | model name)
lss bench quick|full|accuracy|dry-run|status|cancel
lss advice evidence for settings and upgrade decisions (--range 24h|7d|30d)
lss latency|load|gpus|gateway [--range 15m|1h|6h|24h|7d] lss rules
lss maintenance start "reason" [--minutes N] | stop | status planned work: its restart
shows "planned", not a warning
lss --demo the live screen on built-in sample data (no collector needed; piped or --json: the sample status once)
exit codes: 0 serve up · 1 serve DOWN / refused · 2 collector unreachable / bad usage
Every command except the bare lss is safe for scripts, cron and agents: plain text with stable
labels, or --json = the collector's own documents, whose every key is documented in
docs/STATUS-JSON.md.
A loadout is one model served one way: model id, container image and a hash of the launch
settings, read from docker inspect whenever the serving container starts. Restarting the same
configuration is the same loadout; any changed setting starts a new one. Launch settings are
hashed, never stored. Every speed, memory and energy figure is kept per loadout.
lss bench [quick|full|accuracy] [--force] [--note "…"] runs on the GPU box; from another machine
it prints the exact ssh command (or runs it when bench_ssh is set in lss.toml). It wraps
llm_decode_bench.py when you point
[bench] harness at a clone of it; without it, every profile except accuracy runs a small
built-in bench instead.
| profile | about | what |
|---|---|---|
quick |
5 min | decode at 1, 2, 4, 8 users (capped at your slots) x short and 16k context · prefill 8k + 64k · three sanity checks |
full |
30 min | quick with longer cells + prefill 128k + retrieval from a ~250k-token context + acceptance, KV capacity and tokens per joule from the server |
accuracy |
hours | a pinned dataset (gsm8k, mmlu-pro, gpqa-diamond); lss compare A B --accuracy runs a paired significance test |
dry-run |
seconds | proves the setup without sending the model a token |
It refuses to start unless the server has been idle for 5 minutes (--force overrides), checks
every 2 seconds and aborts the moment anyone else sends a request, pauses the idle test and the
queue alerts while it runs (outages, restarts and heat still alert), runs one at a time with a
hard timeout, and never lets the harness update itself.
lss compare <A> <B> prints two loadouts side by side with the change in %: within ±3% is
~ same (noise), and it ends with one plain verdict per category. A benchmark number is only
compared with a benchmark number, a live number with a live number. The routine after swapping
models: docs/MODEL-SWAP.md.
A gateway is a proxy between your users and the engine - API keys, separate lanes for outside
users and your own tools, admission control. lss does not ship one and does not need one. If
yours publishes a health document in the shape described in
docs/STATUS-JSON.md (gate_url in collector.toml), the LANES box, the
USERS and GATEWAY pages and the per-user numbers fill in; without it they say there is no
gateway, and everything else works the same.
Optional: packaging/collector.toml.example lists every key with
its default (a test holds that file to the built-in defaults). Unknown keys are an error.
lss-collector --check-config prints the effective config. Restart the service after editing.
| key | default | |
|---|---|---|
host |
"" = this machine's name |
the name shown on screen |
poll_secs |
5 |
|
engine_url, engine_kind |
"auto", "auto" |
the LLM server; auto finds it (docs/ENGINES.md) |
[[engine]] kind, url, name |
none | pins what --detect would find |
serve_container, serve_port |
auto, 0 |
auto = whichever container publishes the engine's port (survives a model swap). No docker = no container facts |
slots |
0 |
0 = ask the engine (SGLang, llama.cpp and TGI say); set it for the others. Asked again whenever the served model id or the serve container's start time changes, and at least every 300 s (LSS_SLOTS_RECHECK_SECS overrides, 0 = every poll); a configured value is never re-fetched, a failed fetch drops to 0 and is retried |
listen |
["127.0.0.1:8099"] |
add a LAN / VPN address to read it from another machine |
db_path |
~/.local/state/lss/lss.db |
|
raw_hours, retention_days, rollup_10m_days |
24, 14, 90 |
what each storage tier keeps |
alert_cmd, alert_flush_secs |
set by the wizard, 300 |
run as alert_cmd <severity> <message> |
[rates] path |
~/.config/lss/rates.toml |
your electricity rate table; "" = cost off |
[watch] path |
~/.config/lss/watch.json |
the WATCH page's file; "" = off |
gate_url, gate_container |
"" |
your gateway, if you run one |
public_priority / trusted_priority |
"10" / "0" |
the priority labels your gateway stamps on each lane, if it does |
temp_units |
"both" |
temperatures in alert text: both, c, f |
[targets] ttft_p95_s, min_tok_s_per_user, max_queue_wait_s, uptime_pct |
5.0, 30.0, 2.0, 99.0 |
the service you want; the screen and ADVICE say how much of the last 24 h met each; 0 switches one off |
[[user_alias]] ip, name |
none | friendly names for users seen only by address |
[probe] enabled, interval_secs, max_tokens, timeout_secs, prompt |
true, 300, 128, 60, … |
the idle test |
[rules] … |
see the example | alert thresholds; thermal_exclude lists GPUs that never raise a heat alert |
[bench] … |
see the example | harness path, idle gate, durations, timeouts |
[advice] … |
docs/ADVICE.md | every ADVICE threshold |
| key | default | |
|---|---|---|
url |
http://127.0.0.1:8099 |
the collector; $LSS_URL and --url win |
theme |
"" |
dark / light for a first start |
temp_units |
both | both, c, f |
bench_ssh |
"" |
the ssh host lss bench runs on; empty = print the command |
[[server]] name, url, bench_ssh |
none | one entry per collector; lss --server NAME … picks one |
One SQLite file (WAL), bounded by construction. Measured with 4 GPUs and ~90 metrics:
| tier | per day | kept | steady state |
|---|---|---|---|
| raw 5 s samples | 43 MB | 24 h | ~43 MB |
| 1-minute rollups | 3.5 MB | 14 d | ~50 MB |
| 10-minute rollups | 0.35 MB | 90 d | ~32 MB |
| latency / size histograms | <= 0.7 MB | 14 d / 90 d | <= 15 MB |
| incidents, alerts, tests, loadouts, scorecards | < 0.1 MB | 90 d | < 10 MB |
The collector itself stays around 15 MB of memory; its service is capped at 512 MB. A database from an older collector is migrated in place; an older collector ignores newer tables, so rolling back is starting the old binary.
cargo test --workspace && cargo clippy --workspace --all-targets -- -D warnings
LSS_BUILD_HOST=<host> scripts/remote-build.sh test|clippy|build|bless # the same on a remote docker host
LSS_BUILD_HOST=<host> scripts/remote-build.sh test --ref <sha> # a pinned commit; cleans up after itself (--keep keeps it)
cargo test -p lss --test render -- --ignored --nocapture print_renders # every layout and page as text
lss --demo --no-save # the screen on sample data
Anything that drives the live screen for a test uses --no-save or LSS_PREFS=$(mktemp), or
it overwrites the person's saved theme and layout.
Keep your own deployment out of the repository. Real configs, host names and notes go under
site/<name>/, which is git-ignored whole; start from packaging/*.example. Before your first
commit run scripts/install-privacy-hook.sh (blocks a commit that carries a host name, a real
address, an e-mail, a credential, a value from your own rates file, or any word in your
git-ignored .privacy-words) and scripts/install-push-hook.sh (refuses a push from a branch that does not
contain its remote, which is how someone else's work gets silently undone). To produce a copy you
could publish: scripts/export-public.sh <new-dir> writes the tree without site/ and runs the
same privacy check on the RESULT, deleting it on any hit.
CI (.github/workflows/ci.yml) runs clippy, the tests and shellcheck, then exports a copy
(which runs the privacy scan on it) and runs the whole test suite again inside that copy. Parsers
are tested against real captures (what is real, what is synthetic); rules,
incidents, rollups and the token ledger run under a fake clock; /status and the other documents
have byte-for-byte goldens; the benchmark runner is tested end to end against a fake harness; the
screen and every page render in memory at the pane sizes people use.