The eval-containers harness runs agentic benchmarks on the llm-d infrastructure for
generating and evaluating real agent workloads in real time.
- Concept & data flow — below
- Quick start — Quick start
- Verified scenarios — Verified benchmark × agent combinations
- Write your own — Constructing a scenario
- Model config — Model serving
- Debugging — Reading results and Debugging
llmdbenchmark CLI (your workstation)
│ renders a scenario into a pipeline of steps
│ talks to the cluster with kubectl/oc
▼
Cluster (kind, or any Kubernetes / OpenShift)
│ deploys N "harness pods" running the eval image
│ each image = agent + in-pod gateway + grader + OTel collector
▼
Each agent's model calls flow:
agent → in-pod gateway → your model endpoint → the model
▼
Results are written to a PVC, then copied back to a local results dir
This runs one task from the GAIA benchmark with the codex agent, in run-only mode against an
existing OpenAI-compatible endpoint (see Model serving for the
alternatives).
- The
llmdbenchmarkCLI installed (see the top-level README). kubectl(orocfor OpenShift) pointed at your cluster.- An OpenAI-compatible model endpoint URL, and a key if it requires auth.
- A
ReadWriteManystorage class for-j > 1(parallel pods share the results PVC).
export OPENAI_API_KEY="<your-key>" # exported so -g forwards it by name # pragma: allowlist secret
llmdbenchmark --spec examples/eval-containers-gaia run \
-U "https://<your-endpoint>" -m "<model-id>" -p <namespace> \
-g OPENAI_API_KEY -j 1 --base-dir . --analyze --wait-timeout 3600"Verified" means the serving path was driven end-to-end and the model's calls landed and were graded. So far that's only these two, and only with a limited model set (self-served Qwen on GPU, and a proxy/LiteLLM-style endpoint). Scores are incidental — they reflect model/benchmark difficulty, not the pipeline.
| Benchmark | Agent | Model | Run mode |
|---|---|---|---|
gaia |
codex |
Qwen3 (self-served GPU) · proxy endpoint | GPU-served, run-only |
aider-polyglot |
gemini-cli |
Qwen3 (self-served GPU) | GPU-served |
A scenario is a YAML describing one run (model, eval image, storage, harness
wiring); anything unset falls back to config/templates/values/defaults.yaml.
Shipped examples: config/scenarios/examples/eval-containers-{gaia,aider-polyglot}.yaml
(run-only) plus their -gpu variants. The three blocks that make it an
eval-containers scenario:
images:
benchmark: # the eval image the harness pod runs
repository: ghcr.io/exgentic/evals/<benchmark>--<agent>
tag: latest
harness:
name: eval-containers # selects this harness
experimentProfile: gaia.yaml # per-benchmark profile
entrypoint: /workspace/harnesses/eval-containers-llm-d-benchmark.sh
resources: { cpu: "2", memory: 6Gi }
model:
name: <model-id> # becomes EVAL_MODEL=<name>, BARE
huggingfaceId: <hf-id-or-served-id>Change images.benchmark + harness.experimentProfile to point at a different
benchmark/agent. The model/decode/prefill/storage blocks describe how
the model is served (next section).
EVAL_MODEL must stay a bare handle: the in-pod gateway carries the provider
separately, so an openai/ prefix becomes part of the model name, is looked up
in the OpenAI catalog, and 404s every agent call — reward 0.0, empty agent
stdout, harness rc 0. Silent green.
Useful harness.extraEnvVars (see the shipped examples): EVAL_TIMEOUT raises
the image's 300s per-task cap, which truncates reasoning models — it is applied
per attempt, so images with 2-attempt grading can run ~2x it;
EVAL_GAIA_SUBSET=no-search keeps GAIA's 54 egress-free tasks; and for
gemini-cli images EVAL_MODEL_API=openai is required, not optional.
Keep scenarios cluster-agnostic. RWX storage class, service account, and root
privilege belong in a --cluster-config file
(openshift-setup.md), merged over the scenario at run time:
defaults.yaml → scenario.yaml → --cluster-config → --set → CLI flags.
For a one-off tweak that does not warrant a file, --set takes the same
dotted paths on the command line -- e.g.
--set storage.workloadPvc.storageClassName=ocs-storagecluster-cephfs
(standup.md).
The agent needs an OpenAI-compatible endpoint — two modes, identical downstream of the gateway.
Point the agent at an existing endpoint (e.g. a model proxy/router) — no model is stood up:
llmdbenchmark --spec examples/eval-containers-gaia run \
-U "https://<proxy-endpoint>" -m "<model-id>" \
-p <ns> -g OPENAI_API_KEY -j 1 --base-dir . --analyzeThe harness pod needs egress to the endpoint; model access is usually
key-dependent (per-key/per-team allow-lists) — confirm the exact key + model can
infer before a full run (a 401/403/429 there saves a wasted run).
Stand up the model with llm-d on GPUs, then run the eval against that in-cluster
endpoint. run does not auto-standup.
llmdbenchmark --spec <gpu-scenario> standup -p <ns> -t modelservice --base-dir .
llmdbenchmark --spec <gpu-scenario> run -p <ns> -t modelservice -j 1 --base-dir . --analyze # NO -U / -g
llmdbenchmark --spec <gpu-scenario> teardown -p <ns> -t modelservice --base-dir .A GPU-served scenario (vs run-only) must, beyond the run-only fields: cap
model.maxModelLen to fit the KV cache; select GPU nodes (affinity.nodeSelector
decode.acceleratorType) and remove anyaccelerator.count: 0(zero forces CPU serving); use a real vLLM image; setdecode.vllm.modelCommand: customso the serve command actually emits--max-model-len/--tensor-parallel-size/--gpu-memory-utilization; provide a model PVC (+ HF token if gated); and scaledecode.replicaswith-j. No-U/-g— the endpoint is auto-discovered. See the-gpuexample scenarios. On OpenShift the images run as root, so a cluster-admin binds ananyuidServiceAccount once per namespace and your--cluster-configreferences it (openshift-setup.md).
Per task, eval-containers-<id>_<n>/ contains:
| File | Tells you |
|---|---|
task/result.json |
The score: {"reward":1.0,"passed":true} (grader output in task/verifier.log). |
agent/stdout.log |
The agent's work. Empty = it produced nothing. |
model/gateway.log |
The in-pod gateway — first place to look on a call failure. |
traces.jsonl |
One OTel batch per model call. 0 lines = no calls reached the model. |
A healthy run: gateway logs a successful startup, traces.jsonl has spans with
status 200, and stdout.log is non-empty and free of API Error.
--analyze also rolls every task up into agentic-summary/ at the results root
(one task per pod means the per-task reports alone can't answer "how did the run
do"):
| File | Contains |
|---|---|
agentic_run_report.yaml |
Benchmark-report v0.2 for the whole run: pass rate, pooled LLM-call latency, token totals, and per-language pass rates for aider-polyglot. |
agentic_tasks.csv |
One row per task — score, exit code, call count, latency, tokens. Units are in the column names. |
grep -E 'tasks|passed_count|pass_rate|total_tokens' \
<results>/agentic-summary/agentic_run_report.yaml
column -s, -t < <results>/agentic-summary/agentic_tasks.csvScores live under results.observability as eval_containers_* keys. Read
_tasks_exit_timeout and _tasks_exit_error beside the pass rate: a task killed
at EVAL_TIMEOUT scores 0 without the model having failed. _ttft_discarded_calls
counts time-to-first-token values rejected as impossible (larger than their own
call), so a low _ttft_valid_calls means treat that metric as thin.
| Symptom | Cause → Fix |
|---|---|
Agent runs but the served model rejects a tool (e.g. unsupported call: web_search, or a 400 on a tool field) |
The served model doesn't support a tool the agent sends. Serve a model/route that accepts it, or use an agent whose tools the model supports. |
Agent errors on structured output (e.g. gemini-cli NumericalClassifierStrategy / "invalid content, retries exhausted") |
The served model returns tool-call/JSON in a format the agent's internal router can't parse. Pair the agent with a model whose output format it handles (verified: Qwen-class works with both codex and gemini-cli). |
At -j N, agent stdout ends mid-work (task cut off) |
Too many agents per model replica → per-task timeout. Raise decode.replicas (~2–3 tasks/replica). |
See also: run.md (harness/profile mechanics) ·
openshift-setup.md (cluster-config + OpenShift prereqs) ·
analysis.md (--analyze report).