GuidesModel sweeps

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

WhereThe Agent model tab on the Cost & Quality page
Key API routesPOST /model-sweeps/preview, POST /model-sweeps, GET /model-sweeps, GET /model-sweeps/{id}, POST /model-sweeps/{id}/cancel, GET /model-sweeps/{id}/comparison
AuthThe 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
NeedsA dataset with a golden version, one Agent endpoint connection per model, and enabled judges
ProducesPer-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 onThe 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 sweepWhy
Dataset versionThe same frozen prompts, in the same frozen state, for every arm
Judge setThe same graders, at the same deployed versions
kThe same number of repeats per arm
Version labelThe same candidate label recorded on every captured trace
Gate policyThe 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

RefusalWhen
Over budgetThe 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 runsarms × k exceeds the platform ceiling of 40 child runs.
Too many arms / too high kAbove 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 failed with 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 from completed.

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
FieldRequiredMeaning
nameyes (create)Human name for the sweep
versionLabelyes (create)Candidate label recorded on every captured trace, identical across arms
armsyes[{label, agentConnectionId}] — one per model. Labels must be unique; two arms may not share a connection
datasetIdone ofThe golden dataset to replay
datasetVersionIdone ofPin an explicit frozen version instead of the golden one
scenarioSuiteIdone ofReplay a stress-test suite’s frozen synthetic version
passKnoRepeats per arm (default 3, clamped to the platform ceiling)
judgeDeploymentIdsnoJudges that score every arm; defaults to your agent’s Primary Score
budgetUsdno (create)Refuse the launch above this estimated spend
gate / baselineno (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.9

Exit 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-arms when you do want a non-zero exit unless every arm passed pass^k.
  • A void sweep always exits non-zero, with the void reason printed — a comparison that cannot be trusted must never look like a pass.
  • neens sweep decide follows 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:

ToolDoes
start_model_sweepLaunches 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_sweepReads 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_comparisonReads 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

SymptomCauseFix
The launch is refused with a budget messageThe estimate’s priced total is over the sweep’s budget or the platform ceilingLower passK, drop an arm, use a smaller golden version, or raise the budget
The estimate says partial and the total looks too lowAt least one arm’s model has no priceSet a rate in Settings → Model pricing, then preview again. The shown total is a floor
An arm is failed while the others completedThat arm’s endpoint was unreachable or returned nothing usableCheck the arm’s error and the endpoint connection; the other arms’ results are still valid
The sweep is void and shows no comparisonThe 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: falseThe model passed some runs and failed othersThat 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 shipYou’re reading the evidence, not the verdictPress Compare arms — see Sweep decisions