# llm-testbench — LLM accuracy testing framework
Plan authored by fable (planner/orchestrator). Implementation: sonnet. Verification: opus.
## Goal
Go framework that runs a catalog of realistic tests (category → subcategory) against
multiple LLMs via an OpenAI-compatible endpoint, scores responses deterministically,
and renders a comparison table.
## Non-goals / constraints
- **No Microsoft-related content anywhere** (no Azure, PowerShell, Windows, TypeScript-the-MS-angle is fine as a language).
- No LLM-as-judge in v1 — deterministic evaluators only.
- Unit tests never hit the network. Live runs are explicit (`llmtest run`).
## Verified facts (grounded this session)
- Endpoint: `https://llm-gateway.example.com/v1` — keyless, standard OpenAI chat/completions.
- Models all available: `uni/deepseek-v4-flash-0731`, `uni/qwen3.6-27b`, `uni/btl-4`.
- Real response observed: content may carry leading whitespace (`"\n\nOK"`) — **normalize before evaluating**: trim, strip a leading `...` block if present.
- Toolchain on this machine: go1.26.5 darwin/arm64. `python3` and `cc` (clang) assumed present on macOS; exec-evaluators must detect and mark `skipped` if a toolchain is missing (score excluded, not zero).
## Architecture
Module: `github.com/lukaszraczylo/llm-testbench`. Go ≥1.23. Dependencies allowed:
`gopkg.in/yaml.v3`, `golang.org/x/sync/errgroup`. Nothing else without a reason.
```
cmd/llmtest/main.go CLI: run | list. Flags: --config, --category, --subcategory,
--models (csv override), --format table|markdown|json,
--concurrency, --timeout, --verbose
internal/config/ Config load (YAML) + env override LLMTB_API_KEY, validation
internal/llm/ Client interface + OpenAI-compatible impl (retry w/ backoff ×2
on transport/5xx, temperature 0, per-request timeout)
internal/eval/ Evaluator interface + implementations (below)
internal/testkit/ Test type, Registry, response normalization
internal/runner/ model×test fan-out, bounded concurrency (errgroup), Result
internal/report/ table (text/tabwriter), markdown, json renderers
internal/tests/ the catalog: one file per category
```
### Core types (shape, adjust idiomatically)
```go
// llm
type Message struct { Role, Content string }
type Request struct { Model string; Messages []Message; MaxTokens int; Temperature float64 }
type Response struct { Text string; PromptTokens, CompletionTokens int; Latency time.Duration }
type Client interface { Complete(ctx context.Context, req Request) (Response, error) }
// eval
type Score struct { Value float64; Detail string; Skipped bool } // Value 0..1
type Evaluator interface { Evaluate(ctx context.Context, response string) Score }
// testkit
type Test struct {
ID, Category, Subcategory, Description string
System, Prompt string
MaxTokens int
Eval eval.Evaluator
}
// runner
type Result struct { Model, TestID string; Score eval.Score; Latency time.Duration; Tokens int; Err error }
```
### Evaluators (DRY, generics where natural)
- `ContainsAll(substrings...)` / `ContainsAny(...)` — case-insensitive; partial credit for All (matched/total).
- `NotContains(...)` — composable guard (e.g. must NOT suggest `kubectl edit`).
- `Regex(pattern)` — full credit on match.
- `Equals(want)` — trimmed, case-insensitive.
- `Numeric[T constraints.Float | constraints.Integer](extract func(string) (T, error), want, tol T)` — extracts last number (or JSON field) from response.
- `JSONField(path, want)` — parses first JSON object/array in response (strip code fences), compares field. Use generics for want type.
- `All(weighted ...WeightedEvaluator)` / composite — weighted mean.
- Exec evaluators (compile/run extracted code, compare stdout):
- `GoRun(harness string)` — extract first fenced code block, append/wrap with per-test harness `main`/driver, `go run` in temp dir (own go.mod), compare stdout to expected. Timeout 30s.
- `PyRun(harness string)` — same via `python3`.
- `CRun(harness string)` — compile with `cc`, run, compare stdout.
- Toolchain missing → `Score{Skipped: true, Detail: "toolchain missing: X"}`.
- Shared helper: `ExtractCodeBlock(response, lang string)` — first matching fenced block, fallback to whole response if no fences.
**Response normalization** (testkit, applied once before eval): trim space, strip leading `…` (and `` variants), collapse Windows line endings.
### Scoring & report
- Per test: 0..1. Skipped tests excluded from aggregates (shown as `skip`).
- Table 1: rows = tests (grouped by category/subcategory), columns = models, cells = `score (Ntok)`
where Ntok = prompt+completion tokens that test burned on that model — or `ERR`/`skip`.
- Table 2: category rollup — rows = category, columns = models, cells = mean score.
- Table 3: model summary — overall mean, tests passed (≥0.99), partial, failed, errors, mean latency,
total tokens, and **avg tok/s** = Σ completion_tokens / Σ latency-seconds across all non-skipped tests
(aggregate ratio, not mean of per-test ratios — robust to short responses).
- `--format markdown` renders the same as GitHub tables; `json` dumps raw results.
### Config (`config.yaml`, example committed as `config.example.yaml`)
```yaml
endpoint: https://llm-gateway.example.com/v1
api_key: "" # optional; env LLMTB_API_KEY wins
models:
- uni/deepseek-v4-flash-0731
- uni/qwen3.6-27b
- uni/btl-4
concurrency: 4
request_timeout: 120s
max_tokens_default: 4000
```
## Test catalog (16 tests — implement ALL)
Grounded in the operator's real historical tasks (vector search engine work, Proxmox/LXC homelab,
GitOps k8s home cluster, golangci strictness, semver tooling, macOS BSD-vs-GNU pain, Vue 3 frontends,
agent orchestration). No hello-worlds. Exact prompt wording: sonnet authors it, but each prompt must
be fully self-contained (all needed data inline) and pinned to one deterministic expected answer.
**Ground-truth rule (non-negotiable): every constant expected value (numbers, sizes, orderings) must
carry a `// ground truth:` comment showing the derivation, and where cheaply possible the unit test
must recompute it (e.g. compute expected cosine similarity in the Go unit test rather than hardcode).**
### programming/golang
1. `go-struct-align` — give a Go struct with wasteful field order (mix of bool/int64/int32/string/byte);
ask for reordered struct minimizing padding on 64-bit. Eval: Regex/ContainsAll on required field order.
Ground truth: derive by alignment rules in comment; unit test may verify with unsafe.Sizeof on both variants.
2. `go-worker-pool` — implement `Pool[T, R any](in []T, workers int, f func(T) R) []R` preserving input order.
Eval: GoRun harness feeding 100 items with a slow f, checks order + concurrency actually used (duration bound or counter).
3. `go-semver-classify` — given 5 conventional-commit messages (incl. a body containing the word "breaking"
in a non-breaking sense trap, a `feat:`, a `fix:`, a `chore:`), ask for the resulting semver bump as JSON
`{"bump":"major|minor|patch"}`. Eval: JSONField.
### programming/python
4. `py-log-triage` — inline ~15 lines of kubelet/pod log output (mixed OOMKilled, ImagePullBackOff, normal);
ask for a python3 script printing counts per failure type as `type=count` sorted. Eval: PyRun compare stdout.
5. `py-cosine` — two 8-dim float vectors inline; ask for cosine similarity to 4dp (plain number only).
Eval: Numeric with tol 0.0005. Ground truth computed in unit test with math.
### programming/typescript
6. `ts-debounce-composable` — Vue 3 composable `useDebouncedRef(source: Ref, ms: number): Ref`
with proper cleanup. Eval: ContainsAll(`export function useDebouncedRef`, `watch`, `clearTimeout`/`onScopeDispose` any, generic ``) + NotContains(`any`) — weighted composite.
### programming/c
7. `c-struct-size` — C struct with char/double/int/short members inline; ask `sizeof` on LP64 (answer: number only).
Eval: Numeric exact. Ground truth: derivation comment + optional CRun cross-check in unit test guarded by cc presence.
### operations/macos
8. `macos-timeout-portability` — script fails with `timeout: command not found` on stock macOS; why + two portable fixes.
Eval: composite ContainsAny(coreutils|gtimeout) + ContainsAny(perl -e 'alarm'|background & kill|builtin) + ContainsAll(GNU).
9. `macos-launchd-cron` — persistent job on macOS that survives reboot and runs every 5 min: which mechanism + minimal plist keys.
Eval: ContainsAll(launchd/launchctl, StartInterval or StartCalendarInterval, LaunchAgents/LaunchDaemons).
### operations/linux
10. `linux-pct-exec` — CT 251 lives on Proxmox host 10.0.0.100, not directly reachable from laptop; one-line command
to check `systemctl status myservice` inside it. Eval: Regex requiring `ssh` + `10.0.0.100` + `pct exec 251` + command.
11. `linux-systemd-oneshot` — write a systemd unit that runs a script once at boot after network-online, no restart loops.
Eval: ContainsAll(`Type=oneshot`, `network-online.target`, `WantedBy=multi-user.target`, `RemainAfterExit` optional credit).
### operations/kubernetes
12. `k8s-crashloop-gitops` — inline `kubectl describe` excerpt: CrashLoopBackOff, last state OOMKilled exit 137,
limits 128Mi. Cluster is ArgoCD-managed. Ask: root cause + correct fix procedure. Eval: composite
ContainsAll(OOM/memory, limit raise) + ContainsAny(git commit|ArgoCD|GitOps) + NotContains(`kubectl edit`, `kubectl patch`).
### research/web
13. `web-robots-ai-crawlers` — inline a robots.txt with mixed rules (GPTBot disallowed, ClaudeBot allowed,
PerplexityBot disallowed, Googlebot allowed); ask which AI crawlers can fetch `/docs/`. JSON array answer.
Eval: JSONField/exact set compare.
### research/whitepapers
14. `paper-hnsw-params` — inline a 150-word excerpt describing HNSW parameters (M, efConstruction, efSearch —
sonnet writes a technically correct excerpt); ask three precise extraction questions answered as JSON.
Eval: JSONField ×3 composite.
### research/codebase
15. `code-trace-go` — inline a ~20-line Go function (bit manipulation + recursion, deterministic); ask for exact
output for a given input, number only. Eval: Numeric exact. Ground truth: unit test RUNS the same function.
### agents (subcategories: tool-routing, planning)
16. `agent-tool-routing` — inline roster of 6 tools with descriptions (search_web, read_file, run_shell,
query_db, send_email, none) + 4 mini-tasks; ask JSON mapping task→tool. Eval: JSONField ×4.
17. `agent-plan-ordering` — deployment scenario with dependencies (build→test→backup→deploy→verify→rollback-on-fail);
ask for ordered JSON array of step ids. Eval: exact array compare (order matters).
(16–17: both under `agents`; subcategories `tool-routing` and `planning`. Total = 17 tests.)
## Quality gates (must pass before you report done)
1. `go build ./...`
2. `go vet ./...`
3. `go test ./...` — offline only, mock Client; evaluator tests cover good/bad/edge; exec-evaluator tests
guarded by toolchain detection.
4. `golangci-lint run` if the binary exists — must be clean incl. fieldalignment (fix orders, do not disable linters).
5. `gofmt -l .` empty.
## Deliverables
- Full source per layout above, `go.mod`, `config.example.yaml`, `.gitignore` (binaries, config.yaml), `README.md`
(usage, adding a test, adding an evaluator).
- Do NOT `git commit` — orchestrator commits after opus verification.
- Do NOT call the live endpoint anywhere in code paths exercised by `go test`.
## Report back
Files created; output of each quality gate (pass/fail + failure detail); any deviation from this plan with one-line reason; open concerns.