Add 9 tests to research/web, research/whitepapers, and research/codebase, bringing each subcategory from 1 test to 10. Web tests extract data from inline HTML, headers, and URLs, and answer protocol scenarios for HTTP status codes, CORS, DNS, and canonicalization. Every fixture stays inline; no test makes a live web call. Whitepaper tests each pair a 120 to 180 word, technically accurate excerpt with numeric or JSON extraction questions. Topics cover product quantization, Bloom filters, TF-IDF, recall@k, LSM-tree write amplification, Raft quorum sizing, B-tree capacity, the CAP theorem, and attention scaling. Every derived constant carries a ground-truth comment and a unit test that recomputes the value with an independent formula. Codebase tests trace a Python function and a TypeScript function to exact output, then reason over an inline diff, a data race, dead code, Big-O complexity, an import cycle, a generic return type, and a git-bisect scenario. The Python trace test runs the same source through python3 when available and skips otherwise; the TypeScript trace test cross-checks its hand-derivation against an equivalent Go function. New package-level helpers use a wp or code prefix to avoid name collisions with sibling category authors sharing this package. Test IDs keep the paper, web, and code prefixes already established by the existing catalog entries. New tests omit MaxTokens so the runner falls back to its default, per the catalog's authoring rules. catalog_test.go's exact-map assertion now fails as expected, since it still lists only the original 17-test catalog; that assertion is owned centrally and is not touched here.
llm-testbench
llm-testbench runs a catalog of realistic tests against multiple language models through an OpenAI-compatible endpoint, scores each response with a deterministic evaluator, and renders a comparison table. It does not use an LLM as a judge.
Requirements
- Go 1.25 or later.
python3andccon PATH, only if you want the Python and C exec-evaluator tests to run instead of skip.- An OpenAI-compatible chat completions endpoint to test against.
Warning: the go-worker-pool, py-log-triage, and c-struct-size
exec-evaluator tests compile and run model-generated code (internal/eval's
GoRun/PyRun/CRun), with the same privileges as the llmtest
process itself. Each call runs in its own temp directory with a minimal
environment (PATH, an isolated HOME/TMPDIR, and GOCACHE/GOMODCACHE
for GoRun), not the operator's full environment - but this is not a
sandbox, and it does not stop a malicious payload from reading files the
operating-system user can read or making outbound network calls. Run
llmtest run in a throwaway VM or container if you test models you do not
already trust.
Setup
Copy the example config and set your endpoint and models:
cp config.example.yaml config.yaml
Set an API key through the config file's api_key field, or through the
LLMTB_API_KEY environment variable. The environment variable always wins
over the file value.
Usage
List the test catalog:
go run ./cmd/llmtest list
go run ./cmd/llmtest list --category programming --subcategory golang
Run the catalog against the configured models:
go run ./cmd/llmtest run
go run ./cmd/llmtest run --category operations --format markdown
go run ./cmd/llmtest run --models uni/deepseek-v4-flash-0731,uni/qwen3.6-27b
Flags
Both list and run accept:
--config- path to the YAML config file. Default:config.yaml.--category- filter to one category (for exampleprogramming).--subcategory- filter to one subcategory (for examplegolang).
run also accepts:
--models- comma-separated model list, overriding the config file.--format-table,markdown, orjson. Default:table.--concurrency- override the config file'sconcurrency.--timeout- override the config file'srequest_timeout(for example60s).--verbose- print one progress line per completed test to stderr.
Output
table and markdown render three views: per-test scores grouped by
category and subcategory, a category rollup with mean score per model, and
a model summary with pass/partial/fail/error counts, mean latency, total
tokens, and average tokens per second. json dumps the raw per-test
results, joined with each test's category and subcategory.
A scored cell in the per-test table reads score (Ntok), where Ntok is
the prompt plus completion tokens that test burned on that model (for
example 0.85 (312tok)). The model summary's TOK/S column is an
aggregate ratio, sum of completion tokens over sum of elapsed seconds
across all non-skipped, non-error calls, not a mean of each test's own
ratio; this keeps a handful of very short responses from skewing the rate.
A cell reads ERR when the model call itself failed (a transport error or
a non-2xx response after retries), and skip when the test's evaluator
needs a local toolchain that is not installed (for example, a Python
exec-evaluator when python3 is missing). Skipped tests are excluded from
every mean.
Test catalog
The catalog lives in internal/tests, one file per category. Every test
has a fully self-contained prompt (all data needed to answer is inline) and
a single deterministic evaluator. Every hardcoded expected value carries a
// ground truth: comment showing its derivation, and where the derivation
is cheap, the test file's _test.go recomputes it independently (for
example, internal/tests/python_test.go recomputes the cosine similarity
with math.Sqrt rather than trusting the constant).
| Category / subcategory | Tests |
|---|---|
| programming / golang | go-struct-align, go-worker-pool, go-semver-classify |
| programming / python | py-log-triage, py-cosine |
| programming / typescript | ts-debounce-composable |
| programming / c | c-struct-size |
| operations / macos | macos-timeout-portability, macos-launchd-cron |
| operations / linux | linux-pct-exec, linux-systemd-oneshot |
| operations / kubernetes | k8s-crashloop-gitops |
| research / web | web-robots-ai-crawlers |
| research / whitepapers | paper-hnsw-params |
| research / codebase | code-trace-go |
| agents / tool-routing | agent-tool-routing |
| agents / planning | agent-plan-ordering |
Adding a test
- Add a
testkit.Testvalue to the relevant file ininternal/tests(or a new file, for a new category). Give it a unique, kebab-caseID, aCategoryandSubcategory, a self-containedPrompt, and aneval.Evaluator. - Register it in that file's
register<Category>Testsfunction, called frominternal/tests/catalog.go'sAll. - Add a
// ground truth:comment above any hardcoded expected value, showing how you derived it. Recompute it in the test file when that is cheap (arithmetic, a struct'sunsafe.Sizeof, running the traced function). - Add table-driven
_test.gocases covering a correct answer, a wrong answer, and at least one edge case (a common trap, a partial match, or a malformed response). - Add the test's ID to the
wantCatalogmap ininternal/tests/catalog_test.go.
Adding an evaluator
Evaluators live in internal/eval and implement:
type Evaluator interface {
Evaluate(ctx context.Context, response string) Score
}
Score.Value is in the range 0 to 1; set Score.Skipped only when a
required toolchain is missing, not for a wrong answer. Compose existing
evaluators with eval.All (weighted mean) or eval.Mean (equal-weighted
mean) rather than writing a one-off composite by hand. For a check that the
built-in evaluators cannot express, wrap a closure with
eval.EvaluatorFunc. Add table-driven tests in internal/eval covering a
match, a non-match, and an edge case (empty input, malformed JSON, and so
on).
Quality gates
go build ./...
go vet ./...
go test ./...
gofmt -l . # must print nothing
golangci-lint run # if the binary is installed
Every unit test runs offline against a mock llm.Client or a local
httptest.Server; none of them call the live endpoint. The exec-evaluator
tests (GoRun, PyRun, CRun) detect a missing go, python3, or cc
toolchain and skip rather than fail.