Every LLM developer I know has the same workflow: tweak the prompt, eyeball three outputs, ship it. Then a user reports that the refund flow has been answering wrong for two days.
The fix is evals, but the popular options all asked for more than I wanted to give: a SaaS account, a framework dependency, a YAML religion. I wanted pytest energy: one file, run it, exit code.
So I wrote Litmus: a single Python file, stdlib only, that evals an LLM app against a JSONL dataset and gates regressions in CI.
What a case looks like
{"id": "refund-q", "input": "How do I get a refund?", "expect": {"contains": ["30 days"], "regex": "refund"}} That's it. Input, plus what a good answer must contain. Run it:
python3 litmus.py run --dataset cases.jsonl --endpoint https://api.openai.com/v1 --model gpt-4o-mini LITMUS REPORT ============================================ pass rate 97.4% (76/78) failed 1 errored 1 latency mean 0.84s p50 0.71s p95 1.9s ============================================================ [FAIL] refund-question (0.91s) contains: missing: 30 days The part that actually saves you: compare
Run Litmus against your production prompt, save the JSON report as the baseline, and check every change against it:
python3 litmus.py compare baseline.json current.json --threshold 0.02 It lists the exact cases that used to pass and now fail, and exits 1 - so GitHub Actions blocks the merge. The workflow file is 15 lines, included in the guide.
Design rules I stuck to
- One file, no dependencies.
pip installis where side tools go to die. Litmus is Python 3.8+ stdlib only. You can read all of it before running it. - No lock-in. Your dataset is JSONL in your repo. If you stop using Litmus tomorrow, your evals still mean something.
- Any app. Pipe mode tests anything that reads a case on stdin and prints an answer - Python, Node, a shell script. Endpoint mode talks to any OpenAI-compatible server, including local Ollama models, which makes the whole loop free.
- Cheap scorers first, judge last.
containsandregexare deterministic and free. LLM-as-judge is there for tone and helpfulness, with your rubric - and judge failures are reported as errors, never silently passed.
What I learned building it
- Mine real failures. Every user-reported bad answer becomes a case the same day. Ten real failures beat a hundred synthetic cases.
- Small cases. One expectation per case, so the failure list reads like a bug report.
- Latency is a regression too. A prompt change that doubled p95 response time is a production incident you haven't had yet.
Litmus is $12, one time: https://litmustools.gumroad.com/l/litmus. You get the tool, the field guide (eval-set patterns, LLM-judge rules, the CI workflow), and a runnable example. If it saves one bad deploy, it paid for itself.


