From llm-eval-harness
Measures LLM speed (TTFT, tok/s), concurrency limits, Anthropic protocol compliance, and quality regression. Use to verify vendor claims, compare models, or test stability before adoption.
How this skill is triggered — by the user, by Claude, or both
Slash command
/llm-eval-harness:llm-eval-harnessThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Give this skill an endpoint (`base_url` + `model` + an API key in an env var) and it
Give this skill an endpoint (base_url + model + an API key in an env var) and it
measures whether the model is actually fast, stable, protocol-correct, and good enough —
instead of trusting the vendor's headline numbers. It unifies four evaluation dimensions
that are usually scattered across ad-hoc scripts:
| Dimension | Script | Answers |
|---|---|---|
| Speed | scripts/speed_probe.py | TTFT + sustained decode tok/s, thinking-aware |
| Concurrency / stability | scripts/concurrency_probe.py | success rate, p50/p90 latency, where it breaks |
| Protocol compliance | scripts/protocol_probe.py | does the Anthropic thinking block actually fire (N≥10)? |
| Quality / use-case regression | scripts/usecase_runner.py + blind judges | does it pass your accumulated cases? |
Key handling (non-negotiable): every script takes the API key by env-var name
(--key-env MY_KEY), never the key value on the command line — so it stays out of ps,
shell history, and any saved report. Never hardcode a key into a use-case file or a
wrapper. Read references/evaluation_disciplines.md
for the full reasoning behind this and the other disciplines.
Your private data lives outside this bundle. Use-case libraries, model rosters, and
keys belong in ~/.llm-eval/ (or wherever you keep secrets), NOT in this skill directory —
the skill is generic and public; your test suite is yours. See "Use-case library" below.
Detect what you have, then run the dimensions that apply. For an OpenAI-compatible model:
export MY_KEY=sk-... # the key never appears in a command below
# Speed: real-task throughput + sustained decode ceiling
uv run --with openai python scripts/speed_probe.py \
--base-url https://api.example.com/v1 --model some-model --key-env MY_KEY --mode both
# Concurrency: ramp until it breaks
uv run --with aiohttp python scripts/concurrency_probe.py \
--url https://api.example.com/v1/chat/completions --model some-model --key-env MY_KEY \
--format openai --concurrency 10 20 40 60
If the endpoint is Anthropic-Messages-shaped (/v1/messages), also run the protocol probe
(below). Pick dimensions by what the user actually asked — don't run all four if they only
asked "is it fast?".
uv run --with openai python scripts/speed_probe.py \
--base-url <…/v1> --model <model> --key-env <ENV> --mode both --output /tmp/speed.json
mixed runs representative tasks (what real usage feels like); decode forces one long
output to find the sustained ceiling (the number to compare against a vendor's claim);
both does both.reasoning_content field, but completion_tokens counts it. Collecting only content
while dividing by completion_tokens produces wildly inflated numbers — a real ~750 tok/s
model once measured as 4700 tok/s this way. The script captures both, takes TTFT as the
first token of either kind, and reports completion_tokens / (total − TTFT).uv run --with aiohttp python scripts/concurrency_probe.py \
--url <full endpoint URL> --model <model> --key-env <ENV> \
--format openai|anthropic --concurrency 10 20 40 60 --output /tmp/conc.json
--concurrency levels to ramp and find the ceiling — the level where success
rate drops or latency explodes. A model that's fast single-threaded can still collapse at
modest concurrency (real example: one provider held 50 concurrent at 0.4s while another
dropped requests at just 5 concurrent).trust_env=False) and disables keep-alive
pooling (force_close) — otherwise you measure the proxy's limit or one pinned upstream
replica, not the model. It prints a "concurrency proof" (overlapping request pairs) so you
can confirm requests really ran in parallel.uv run python scripts/protocol_probe.py \
--url <…/v1/messages> --model <model> --key-env <ENV> --repeat 10 --output /tmp/proto.json
/v1/messages compatibility. It checks
whether thinking: {type: enabled} actually produces thinking_delta / signature_delta
SSE events.--repeat
defaults to 10 and the verdict has three states: fully-implemented, intermittent (k/N), not-implemented. Never conclude from a single sample.Connection: close per request so a load balancer can't pin all samples to one
replica and hide the real distribution (a real probe saw 0/10 with keep-alive vs 17/90
with close on the same endpoint).This is two halves on purpose: collect, then judge independently.
Step 1 — collect the model's answers to your use-case library:
uv run --with openai python scripts/usecase_runner.py \
--base-url <…/v1> --model <model> --key-env <ENV> \
--usecases ~/.llm-eval/usecases.json --output-dir ~/.llm-eval/runs/<model>
Step 2 — judge with independent blind judges (orchestrate inline — do NOT let the model
grade itself). For each answer in the run directory, spawn 3 independent Task agents (or
fewer for a quick pass). Each judge gets ONLY: the prompt, the answer, and the case's
rubric — and is explicitly told it is judging in isolation, with no knowledge of other
judges' scores or any prior evaluation (this prevents anchoring). Then aggregate:
tags): a category where judges
systematically disagree with the rubric is a real weakness — on one real eval, a whole
category scored 12.5% precision and exposed a systematic misclassification that a single
grader would have missed.For the rubric-scoring mechanics (LLM-as-judge thresholds, llm-rubric), you can also
compose with the promptfoo-evaluation skill — point its providers at the same endpoint.
This harness's blind-judge method and promptfoo's rubric assertions are complementary: use
promptfoo for fast per-case pass/fail gating, blind judges for precision on a category you
suspect is weak. Full method: references/quality_blind_judge.md.
Keep it OUTSIDE this bundle (e.g. ~/.llm-eval/usecases.json) so it survives skill updates
and never lands in a public repo. It's a plain JSON list — version it in a private repo to
accumulate a regression suite over time:
[
{"id": "refund-window", "prompt": "A customer asks for a refund 20 days after purchase. Reply as support.",
"rubric": "1.0 if it correctly cites the 30-day refund window; 0.0 if it refuses or invents a different window.",
"tags": ["support", "policy"]},
{"id": "lru-cache", "prompt": "Implement an LRU cache in Python with O(1) get/put.",
"rubric": "1.0 if get and put are both O(1) via dict + doubly linked list and the self-test passes.",
"tags": ["code"]}
]
assets/example_usecases.json is a starter you can copy. Only id and prompt are required;
rubric, expected, and tags make judging sharper.
When the user says "evaluate / benchmark this model", the typical flow is:
/v1/chat/completions) or Anthropic-Messages
(/v1/messages)? Hit GET /v1/models or read the vendor docs; don't assume. This decides
which probes apply (protocol probe is Anthropic-only).--output JSON to a run directory.After a run, offer the natural follow-ups:
Evaluation complete for <model>.
Options:
A) Render an HTML dashboard of the results — compose with a visualization skill (Recommended if sharing)
B) Compare against another model — same probes, side-by-side
C) Add the failing cases to ~/.llm-eval/usecases.json as a permanent regression guard
D) Done — the numbers answer the question
npx claudepluginhub p/daymade-llm-eval-harness-llm-eval-harnessEvaluates LLM apps using automated metrics (BLEU, ROUGE, BERTScore, MRR), human feedback, and LLM-as-judge. For testing performance, benchmarking, and regressions.
Implements LLM evaluation strategies: automated metrics, LLM-as-judge, human feedback, and benchmarking for RAG pipelines, agentic tasks, and structured outputs.