Model sweeps
A model sweep replays one frozen golden dataset against several models at once — each model behind its own agent endpoint — under identical conditions, and requires every model to pass k independent runs before it is called a pass. It answers the question a single pre-prod evaluation cannot: which of these models is good enough for this job, and what does each one cost?
Where a pre-prod eval asks “is this candidate version safe to ship”, a sweep asks “which model should this agent run on” — and it refuses to answer at all if the comparison it would draw is not a fair one.
At a glance
| Where | The Agent model tab on the Cost & Quality page |
| Key API routes | POST /model-sweeps/preview, POST /model-sweeps, GET /model-sweeps, GET /model-sweeps/{id}, POST /model-sweeps/{id}/cancel, GET /model-sweeps/{id}/comparison |
| Auth | The same nk_live_ agent API key that drives pre-prod evals — reads need READ, launching a sweep needs the same authority as scheduling a pre-prod run |
| Needs | A dataset with a golden version, one Agent endpoint connection per model, and enabled judges |
| Produces | Per-arm pass^k (greens/k), per-arm run detail, a pre-flight cost estimate, a decidable comparison, and — when the arms did not run under identical conditions — a void sweep that renders no comparison |
| Runs on | The dedicated pre-prod worker fleet, so an N×k sweep never starves live scoring |
A sweep launches ordinary pre-prod runs. Every child run is a normal pre-prod evaluation you can open, inspect, and re-read item by item — the sweep adds the fair-comparison rules and the roll-up on top, it does not invent a second kind of evaluation.
Model sweeps moved twice. It used to have its own sidebar entry, then the Sweeps tab on
Model Bench. It is now the evidence behind the Agent model tab on
Cost & Quality, which reads the newest finished sweep per agent
version and states the verdict. An old /model-sweeps link still works: it opens that tab, and an
individual sweep (/model-sweeps/{id} and its comparison) is unchanged.
When to reach for one
- Model selection. You are deciding between a frontier model, a cheaper hosted model and a self-hosted one for the same agent. Run all three against the same golden set and read the pass rates side by side.
- A vendor price or model change. A new model version lands, or a price moves. Re-run last month’s sweep and see whether the answer changed.
- Monthly in CI. A sweep is cheap enough to schedule and expensive enough to want a budget ceiling on. The CLI’s preview-then-launch flow is built for exactly this (see Run a sweep from CI).
- Against your own failures, not a public benchmark. The strongest golden set for this decision is a stress-test suite generated from your confirmed failure modes. A public benchmark tells you how a model does on somebody else’s problems; a suite built from last quarter’s incidents tells you whether it survives yours. Pass the suite id when you create the sweep and Neens uses its frozen synthetic dataset version.
Arms, k, and what stays fixed
A sweep is made of arms. One arm = one model, reached through one Agent endpoint connection. Arms differ in exactly one thing — the endpoint (and therefore the model). Every other input is pinned once on the sweep and reused verbatim for every child run:
| Pinned for the whole sweep | Why |
|---|---|
| Dataset version | The same frozen prompts, in the same frozen state, for every arm |
| Judge set | The same graders, at the same deployed versions |
| k | The same number of repeats per arm |
| Version label | The same candidate label recorded on every captured trace |
| Gate policy | The same thresholds applied to every child run |
Two arms may not point at the same endpoint connection: that is not a comparison, it is the same model twice, and it would render as two independent results that look like corroboration.
pass^k is per arm — not per sweep
A single green run can be luck: a non-deterministic model, a flaky tool call, a judge that graded generously on the day. pass^k requires an arm to pass k independent runs — all green, zero regressions — before that model is reported as passing.
The critical detail is that k is evaluated per arm, not across the sweep:
A cheap model that passes once and fails twice has not hit the number. It reports
1/3 green,passed: false— not “passed, with variance”.
This is the same rule the eval-verified fix engine uses to decide
whether a fix may open a pull request, applied to a model choice instead of a patch. k = 3 is the
default and is what defeats a flaky green; a run-to-run difference that a single run would have
hidden shows up as an arm that cannot get to 3/3.
An arm with fewer than k finished runs has no verdict at all — it stays in progress with
passed unset rather than reporting the greens it has so far as a result. A partially finished arm
never borrows the benefit of the doubt.
Identical conditions, and what void means
The whole value of a sweep is that the only difference between the arms is the model. If that stops being true, the comparison is worthless — and a worthless comparison that still renders a chart is worse than no chart, because somebody will screenshot it into a decision doc.
So Neens pins the controlled variables at create time as a conditions fingerprint (the dataset version, the sorted judge set, k, the version label, and the gate policy — deliberately not the model, which is the independent variable), and re-derives that fingerprint from each child run’s actual persisted values when it rolls the sweep up.
If any arm’s real conditions diverge from the pinned ones, the sweep becomes void:
- The status is terminal — a void sweep never recovers into a comparable one.
- The detail page shows a prominent banner naming which arms diverged, and suppresses every cross-arm comparison affordance. No chart, no ranking, no “winner”.
- A child run that vanished, or was never created, counts as divergence — not as a pass.
A void sweep is not a failure of the models — it is a failure of the experiment. Read the void reason, fix the thing that drifted (usually a judge redeployed mid-sweep or a dataset version pinned differently on one arm), and run a fresh sweep. Neens deliberately gives you no way to “just show it anyway”.
Cost: estimate first, then decide
Every arm multiplies the bill: an arm costs k × item_count model calls, and a sweep costs that
again for every arm. So the cost pre-flight is a first-class step, not a nicety.
Preview
POST /model-sweeps/preview (the Estimate step in the launch form) computes the estimate and
writes nothing and spends nothing. In the UI the launch button stays disabled until the
estimate has been rendered.
Read the basis, not just the total
The estimate reports where each number came from in a basis block — input tokens are
measured from the frozen items’ own text, and output tokens are either observed from your
agent’s recent history or, when there is no history, a stated default assumption. An
assumption is never presented as a measurement.
Check the unpriced arms
An arm whose model has no price in your price catalogue
contributes nothing to the total — not zero. The estimate comes back with partial: true and
names the unpriced models and arms, so the total is explicitly a floor, not the bill. Set a
rate for that model (Settings → Model pricing) and re-run the preview to get a complete number.
Launch
POST /model-sweeps re-runs the same estimate and refuses the launch if it breaks a limit.
The budget ceiling and the run caps
| Refusal | When |
|---|---|
| Over budget | The estimate’s priced total already exceeds the sweep’s budgetUsd (or the $50 platform ceiling, whichever is lower). A partial estimate whose priced portion alone is over budget still refuses — the real cost is higher than the number shown, never lower. |
| Too many runs | arms × k exceeds the platform ceiling of 40 child runs. |
| Too many arms / too high k | Above the per-sweep ceiling of 8 arms or k = 5 — a higher passK is clamped down to 5. |
A fully unpriced estimate does not refuse on budget — Neens will not block on a number it
does not have. It surfaces partial and leaves the decision with you.
Per-arm failure isolation
A dead endpoint fails its own arm and nothing else. If one model’s endpoint is unreachable, times out, or returns nothing usable:
- That arm goes
failedwith the error recorded on it. - Every other arm keeps running and is reported normally.
- The sweep finishes as
completed_with_failures— every arm terminal, at least one failed, at least one completed — which is a different, and honest, status fromcompleted.
Partial results are the normal case, not an error state: each arm carries its own status and its own progress, so a sweep detail page shows three arms in three different states rather than one spinner over the whole page.
Statuses
Sweep: queued · running · completed · completed_with_failures · failed · cancelled
· void
completed— every arm terminal and no arm failed.completed_with_failures— every arm terminal, at least one failed, at least one completed.failed— every arm failed, or the launch itself failed.void— condition divergence. Terminal, and never comparable.
Arm: pending · running · completed · failed · cancelled
A sweep never reports a terminal status while an arm is still non-terminal.
Create a sweep
On Cost & Quality → Agent model, press New sweep in the top right to open the launch dialog. In it, fill in Sweep name and Version label, choose the Source (Golden dataset or Stress-test suite), add one entry per model under Model arms — each an Agent endpoint connection you have already registered — set Runs per arm (k) and an optional Budget (USD), then press Estimate cost.
The estimate renders per arm under Before you spend anything, with Some arms have no price called out when an arm is unpriced and a Where these numbers come from block for the basis. Launch sweep stays disabled until you have run an estimate — and if you change the sweep after estimating, it asks you to estimate again.
Request fields
| Field | Required | Meaning |
|---|---|---|
name | yes (create) | Human name for the sweep |
versionLabel | yes (create) | Candidate label recorded on every captured trace, identical across arms |
arms | yes | [{label, agentConnectionId}] — one per model. Labels must be unique; two arms may not share a connection |
datasetId | one of | The golden dataset to replay |
datasetVersionId | one of | Pin an explicit frozen version instead of the golden one |
scenarioSuiteId | one of | Replay a stress-test suite’s frozen synthetic version |
passK | no | Repeats per arm (default 3, clamped to the platform ceiling) |
judgeDeploymentIds | no | Judges that score every arm; defaults to your agent’s Primary Score |
budgetUsd | no (create) | Refuse the launch above this estimated spend |
gate / baseline | no (create) | Applied identically to every child run |
A dataset with no golden version is rejected: a sweep whose arms could see different prompts cannot guarantee identical inputs, which is the entire point.
An arm whose endpoint declares no model is allowed — Neens never invents a model name — but it
is unpriced in the estimate and named in unpricedArms.
Then read the answer
A finished sweep is evidence, not a decision. Press Compare arms on the sweep detail page to get the verdict — which model to ship, at what bar, at what cost per case, and per agent, with the sample size, confidence interval and unpriced arms shown rather than rounded away:
curl -sf "$NEENS_BASE_URL/model-sweeps/{sweep_id}/comparison" \
-H "Authorization: Bearer $NEENS_API_KEY"That surface has its own guide: Sweep decisions. It also covers the
regression drill-down (where a cheaper arm breaks), the neens sweep decide CI verb, and the
redacted share link you can paste into a decision doc.
The Sweeps tab also renders the finished sweep as a cost–quality frontier — each arm plotted by cost per case against its pass rate, with the cheapest arm that clears your bar highlighted as the recommended move. It’s the same chart the Cost tab uses for judge scorer models, and a fast way to see which cheaper model still clears the quality bar. A void sweep draws no frontier, for the same reason it renders no comparison.
Run a sweep from CI
The neens-eval SDK (Python and TypeScript, same flags, same exit codes) has a sweep group beside
eval. The CI story is estimate, then decide:
export NEENS_BASE_URL="https://neens.example.com"
export NEENS_API_KEY="nk_live_..."
# 1. What will this cost? Writes nothing, spends nothing.
neens sweep preview \
--dataset-id ds_golden \
--arm "sonnet=conn_sonnet" \
--arm "mini=conn_mini" \
--arm "self-host=conn_selfhost" \
--pass-k 3 --json
# 2. Launch it and wait for every arm.
neens sweep start \
--name "monthly model sweep" \
--dataset-id ds_golden \
--version-label "$GIT_SHA" \
--arm "sonnet=conn_sonnet" \
--arm "mini=conn_mini" \
--arm "self-host=conn_selfhost" \
--pass-k 3 --budget-usd 10 --wait
# 3. Or poll one later.
neens sweep get msw-… --json
# 4. Print the verdict: which arm to ship, at what bar and cost, per agent.
neens sweep decide --sweep-id msw-… --bar 0.9Exit codes
- A sweep is informational by default. Arms that did not all pass do not fail your build — a sweep is a model-selection decision, not a release gate. Use eval gates and pre-prod evals to block a release.
- Pass
--require-all-armswhen you do want a non-zero exit unless every arm passed pass^k. - A
voidsweep always exits non-zero, with the void reason printed — a comparison that cannot be trusted must never look like a pass. neens sweep decidefollows the same shape: informational by default, non-zero only for a void or failed sweep — or with--require-winner. See its exit codes.
Drive it from a coding agent (MCP)
Three MCP tools expose the same flow to an agent:
| Tool | Does |
|---|---|
start_model_sweep | Launches a sweep (name, version_label, arms, plus the optional dataset_id / dataset_version_id / scenario_suite_id, pass_k, judge_deployment_ids, budget_usd) |
get_model_sweep | Reads one sweep back: header plus per-arm label, model, status, passed, greens, k, and run counts — compacted, never the full child-run list |
get_model_sweep_comparison | Reads the verdict: which arm to ship, the per-arm leaderboard with sample sizes and confidence intervals, and a verdict per agent |
How it works
Create pins the conditions
Neens resolves the frozen dataset version (from the suite, the explicit version, or the dataset’s golden version), snapshots the judge set, freezes each arm’s model off its endpoint connection, computes the estimate, checks it against the caps and the budget, and stores the conditions fingerprint.
The launch fans out N × k child runs
One background task creates arms × k ordinary pre-prod runs — each pointed at its arm’s endpoint,
all sharing the pinned conditions — and hands them to the dedicated pre-prod fleet. It does not
wait for them: a parent task that blocked on N×k children would hold a worker slot for hours and
starve live scoring.
If creating an arm’s runs fails, that arm is marked failed and the launch continues with the next one.
Aggregation is lazy and idempotent
Per-arm state is recomputed from the child runs — never cached as a claim. That happens on every
read of a sweep, and again on a background pass every few minutes, so a sweep converges even if
nobody is watching. Reconciliation never overwrites a cancelled sweep and never moves a sweep out
of void.
A run is green only if it passed AND had zero regressions
Each child run contributes one outcome: its pass rate over scored items against the platform
success threshold (0.90 by default), plus its regression count. greens counts the runs that
cleared both. pass^k is green only when greens == k.
An arm whose every run finished having scored nothing — a dead endpoint, an unreachable model —
is failed, not “0%”. We do not know how that model performs, and reporting 0% would claim we do.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| The launch is refused with a budget message | The estimate’s priced total is over the sweep’s budget or the platform ceiling | Lower passK, drop an arm, use a smaller golden version, or raise the budget |
The estimate says partial and the total looks too low | At least one arm’s model has no price | Set a rate in Settings → Model pricing, then preview again. The shown total is a floor |
An arm is failed while the others completed | That arm’s endpoint was unreachable or returned nothing usable | Check the arm’s error and the endpoint connection; the other arms’ results are still valid |
The sweep is void and shows no comparison | The arms did not run under identical conditions (e.g. a judge was redeployed mid-sweep) | Read the void reason, fix the drift, run a fresh sweep. A void sweep is intentionally not renderable |
An arm shows 2/3 and passed: false | The model passed some runs and failed others | That is the result: an unreliable pass is not a pass. See pass^k |
| The sweep finished but you still can’t say which model to ship | You’re reading the evidence, not the verdict | Press Compare arms — see Sweep decisions |