Fintech

How to Build a Reusable Harness for Vendor Model Tests

Antonio Evans9 min read

A clean desk with a folder labeled Fixtures, a terminal window running a script that prints a score table, and a printed comparison report with three columns of numbers.
Key points

Key points

  1. 1A reusable harness starts with fixtures: a fixed set of test inputs and expected outputs stored in version control so every model sees the same cases.
  2. 2Scoring must live in its own script, separate from the code that calls a model, so every vendor's output passes through identical metrics.
  3. 3A comparison report that shows all runs side by side is what turns a one-off test into a decision tool that survives new vendor claims.
  4. 4Treat your test set as a maintained asset: version it, guard it against leakage, and keep past reports so you can see change over time.

Why a one-off test is not enough

Most teams evaluate a model once, usually right before a purchase decision or a launch. A prompt is written, a handful of examples are run by hand, a few outputs are pasted into a document, and a judgment gets made. The problem is that this judgment is not repeatable. When the vendor releases an update, or when a second vendor makes a similar claim, the team has no baseline to compare against and often no record of exactly which inputs produced the original numbers.

Regulated industries already treat this as a discipline rather than an errand. Model risk management guidance from the OCC describes validation as including an evaluation of conceptual soundness, ongoing monitoring, and outcomes analysis, and it explicitly expects ongoing monitoring to be part of a model's lifecycle, not a single event (OCC Comptroller's Handbook). Model validation consultancies make the same point: a validation that only checks that files reconcile and column headings look right will miss errors that persist across periods, and performance can deteriorate over time through environmental change, technology drift, or staff turnover (Darling Consulting Group).

Vendor models make this harder, not easier. Because the vendor controls the code and formulas, a validator often can only hypothesize cause and effect from the documentation, which is why comprehensive validation of black-box vendor models is problematic and why ongoing, interim outcomes analysis matters more (RiskSpan). You cannot inspect the weights. You can only control the inputs, apply consistent metrics, and compare results run over run. That is exactly what a harness gives you.

The decision rule is simple: if a test result will ever be compared to another result, it needs a harness. If the answer will only ever be yes or no, once, for one model, you can skip it. In practice most vendor evaluations get revisited, so the harness pays for itself the second time you run it.

Designing fixtures that survive contact with reality

A fixture, in software testing, is the fixed environment and known data a test runs against so that results are repeatable (Stack Overflow). For model testing, the fixture is a directory of cases: an input, the expected or reference output, and metadata that tells the scorer how to judge it.

Keep fixtures in version control with the harness code. Each case is one file or one row, with a stable identifier. When you change a fixture, the change is visible in the diff, which is the point: silently editing a test case after seeing a model fail is the model-testing equivalent of moving the goalposts.

Design principles that hold up:

  • Broad coverage beats volume. A few hundred well-chosen cases across the failure modes that matter (edge cases, ambiguous inputs, long inputs, inputs in other languages or formats) tell you more than thousands of near-duplicates.
  • Disaggregate by slice. Model evaluation guidance recommends measuring performance across meaningful slices such as regions, languages, device types, or content categories so you can see where performance breaks down (Label Studio). Store the slice label on every fixture row.
  • Separate the stable regression set from the exploratory set. Keep a frozen subset that never changes so runs stay comparable, and a larger evolving set for diagnosis (Label Studio).
  • Record provenance. Each case should note where it came from and whether it is synthetic or real, so a reviewer can weight it appropriately.

Here is a workable fixture layout:

harness/
  fixtures/
    cases.jsonl          # one case per line, stable IDs
    regression_ids.txt    # the frozen subset
  run.py                 # calls the model, writes raw outputs
  score.py               # reads raw outputs, applies metrics
  report.py              # builds the comparison table
  runs/
    2024-11-vendor-a/
      raw.jsonl
      scores.json
      report.md

Each line in cases.jsonl carries an id, the input payload, the expected output or a rubric reference, the slice label, and a provenance note. Keep the input payload as data, not as a template string buried in code, so you can diff two versions of the same case later.

Separating data collection from scoring

The single most important architectural decision is to split the harness into two stages. Stage one calls the model and writes raw outputs to disk. Stage two reads those outputs and applies metrics. They never run in the same process, and they never share state.

Why this matters: if scoring is entangled with the model call, any change to the scoring code changes the meaning of every past run. If the model call fails midway, you cannot tell whether the numbers you got are real or artifacts of a retry. If you want to re-score an old run with a new metric, you cannot, because the outputs were never persisted in a form you can reprocess.

Persisting raw outputs also lets you audit disagreements. When two runs produce different scores, you open the raw files, find the case IDs where they diverge, and read the actual outputs side by side. That is the difference between "vendor B scored higher" and "vendor B handled 31 of 34 ambiguous date inputs correctly while vendor A fell back to a default."

A minimal two-stage harness in Python looks like this:

# run.py: call the model, persist raw outputs, do nothing else
import json, hashlib

def run(model_call, cases, out_path):
    with open(out_path, "w") as f:
        for case in cases:
            out = model_call(case["input"])          # the only model-dependent line
            f.write(json.dumps({
                "id": case["id"],
                "slice": case["slice"],
                "input_hash": hashlib.sha256(
                    case["input"].encode()).hexdigest()[:12],
                "output": out,
            }) + "\n")

# score.py: read raw outputs, apply metrics, know nothing about the model
import json

def score(raw_path, fixtures, out_path):
    expected = {c["id"]: c for c in fixtures}
    results = []
    for line in open(raw_path):
        row = json.loads(line)
        case = expected[row["id"]]
        results.append({
            "id": row["id"],
            "slice": row["slice"],
            "pass": judge(row["output"], case["expected"], case["rubric"]),
        })
    with open(out_path, "w") as f:
        json.dump(results, f)

The judge function is where you put exact match, structured comparison, or a rubric-based check. Keep it deterministic where you can. If you must use an LLM as a judge, record the judge model and prompt alongside the scores, because a change to the judge changes every historical score.

Keeping runs comparable over time

Comparability is fragile. A harness that produces a score you cannot compare to last quarter's score is only slightly better than a one-off test. Protect comparability with four habits.

First, freeze the fixture set per run. Record a hash of cases.jsonl in every scores file. If the hash changed between two runs, the comparison is qualified, and the report should say so.

Second, control for data leakage. Evaluation data must be kept strictly separate from anything the model was trained or tuned on, because overlap invalidates the results (LlamaIndex). With vendor models, you usually cannot verify training data, so the practical defense is to keep some fixtures private and never share them with the vendor, even during setup. If a vendor suddenly aces a case it failed before a release, check whether that case was visible to them.

Third, pin the run conditions. Record the date, the endpoint or version identifier the vendor exposes, sampling settings, and any system prompt used. A vendor that changes a default temperature between releases has changed your test conditions, not just their model.

Fourth, do not tune against your final set. In classical evaluation, once a test set has been used to adjust a model, its metrics are no longer unbiased and it can no longer serve as a true test (Alteryx Community). The harness version of this is: if you revise prompts or post-processing to fix a failure you saw in the frozen regression set, move that case into the exploratory set and accept that the frozen set has been compromised for future comparisons.

A useful maintenance cadence:

TriggerActionEffect on comparability
New vendor claim or releaseRun full harness, file a reportAdds a new run, no change to history
New failure mode discoveredAdd cases to exploratory setNone, frozen set untouched
Metric definition changeBump metric version, re-score old raw outputsHistorical scores recomputed, version recorded
Fixture revised after a failureMove case to exploratory set, note in changelogFrozen set hash changes, flag the break
QuarterlyRe-run frozen set, file a report even with no changesDetects silent vendor drift

That last row is where a harness earns its keep. A vendor that silently changes behavior between releases will show up as a score movement on a set of cases nobody touched.

Turning scores into a comparison report

A report is not a dump of numbers. It answers three questions: which model won on which slices, where the gap is largest, and whether the difference is bigger than the noise in your test set. Structure the report with a headline table, then per-slice tables, then a short list of the cases where models diverged most sharply.

The headline table should show every run you want to compare, with the metric version and fixture hash in the header so nobody compares two incompatible numbers by accident.

Model runFixture hashCasesPass rateWeakest sliceMedian latencyRaw output file
Vendor A, run 1a1b2c341281%Multilingual (61%)1.2 sruns/vendor-a-1/raw.jsonl
Vendor B, run 1a1b2c341278%Ambiguous input (54%)0.9 sruns/vendor-b-1/raw.jsonl
Vendor A, run 2a1b2c341283%Multilingual (64%)1.1 sruns/vendor-a-2/raw.jsonl

The numbers above are illustrative, not benchmark results. The point is the shape: same fixture set, same metric version, side by side, with the weakest slice surfaced because that is where a decision usually turns.

Two report conventions make the output usable by people who were not there when the test ran. Always show the denominator, because a 61% pass rate on 18 cases is a very different claim from 61% on 180. And always show the divergence list, sorted by how many runs got a case right versus wrong, because the cases where all vendors fail tell you your fixtures are testing something hard, and the cases where exactly one vendor succeeds are where the real signal lives.

What the first search result usually gets wrong

The tempting shortcut is to reuse a public benchmark. Public benchmarks are useful for orientation, but they can diverge from production reality because domain shift, adversarial inputs, and new user behaviors create failure modes a curated test set never captures (Label Studio). The same talk on out-of-distribution evaluation makes the point formally: a held-out test set only gives you a rigorous guarantee if the new data really comes from the same distribution as the old data (ComputerVisionFoundation).

A second shortcut is to trust the vendor's model card. Model cards are genuinely useful: they document intended use, evaluation data, disaggregated performance, and known caveats, and the original proposal defined nine standard sections including quantitative analyses broken down by relevant factors (Ultralytics). The IAPP notes that model cards have no mandatory or established requirements, which is exactly why they are an input to your evaluation rather than a substitute for it (IAPP). The Snowflake guide makes the same distinction: a model card records where a model was evaluated, on which populations, and where it is likely to underperform, and it is a versioned artifact tied to a specific model version (Snowflake). Use it to choose which slices your fixtures must cover, then measure those slices yourself.

A third shortcut is single-score reporting. A single headline score hides failures concentrated in one subgroup, which is the argument for disaggregated evaluation in the first place (Ultralytics). Your report should never show one number without at least the per-slice breakdown underneath it.

When to run the harness, and how to keep it alive

Run the harness on four occasions: before a purchase decision, when a vendor announces a change to a model you depend on, when a failure in production suggests a gap in your fixtures, and on a fixed calendar cadence so silent drift gets caught. Each run produces a report filed next to the raw outputs, and the reports accumulate into a history that answers the question nobody asks at purchase time but everyone asks later: is this model still as good as it was when we chose it.

The operational details that keep a harness from decaying:

  • One command per stage. If running the harness takes a wiki page of instructions, it will not run. run.py, score.py, and report.py should each be a single command with defaults that point at the right directories.
  • Store raw outputs, always. They are the only artifact you cannot regenerate if a vendor retires a model version.
  • Version the metrics. A metric change is a release. Label it, and re-score prior raw outputs so the history stays coherent.
  • Keep a changelog next to the fixtures. Every added case gets a line: what failure mode it covers and why.
  • Assign an owner. A harness with no owner becomes a directory of stale JSON within two quarters.

If your test set spans several modalities (text prompts, image inputs, audio clips), the collection stage is where vendor APIs differ most, and that is the step a unified API removes. Quanta Inference exposes text, image, video, audio, and embedding models through one OpenAI-compatible key priced in US dollars, which means the collection script stays the same when you swap vendors and the only thing that changes is the model identifier.

The bigger organizational step is deciding what to do with the reports. A harness that produces a comparison table and no decision rule is still a research project. Write the rule down before the first run: for example, a vendor must beat the incumbent on the frozen set overall and not fall below it on any single high-priority slice. That rule is what turns the harness from a tool for testing vendors into a tool for defending the choice.

FAQ

FAQ

How many test cases does a harness need?

Enough to cover each failure mode and slice you care about with more than a handful of cases per slice. A few hundred well-chosen cases with a frozen regression subset is a workable starting point for most teams; the right number is driven by slice coverage, not by a round number, because disaggregated evaluation is what exposes where a model breaks down (Ultralytics).

Can I use a public benchmark instead of building fixtures?

You can use one for orientation, but not as your decision evidence. Benchmarks can diverge from production reality because domain shift and adversarial inputs create failure modes a curated set never captures (Label Studio). Build fixtures from your own data and use public benchmarks to spot slices you forgot to cover.

What if the vendor changes the model between my runs?

That is exactly the case a harness is built for. Record the version identifier the vendor exposes, run the frozen set again, and compare against the previous report. If the fixture hash changed, treat the comparison as qualified and say so in the report. A score movement on untouched cases is your evidence of a behavior change.

Should I use an LLM to score the outputs?

Only when the output cannot be judged deterministically, and only with the judge model and prompt recorded alongside the scores. Label Studio recommends complementing automated metrics with structured review for factuality and safety, and using human review on targeted slices and edge cases rather than reviewing everything (Label Studio). If the judge changes, re-score the old raw outputs so the history stays comparable.

How do I stop the harness from becoming stale?

Freeze a regression subset, keep an evolving exploratory set, file a report on a fixed cadence even when nothing changed, and keep a changelog next to the fixtures with an owner. Model performance can deteriorate over time through environmental change and technology drift, which is why ongoing monitoring rather than a single validation is the expectation in model risk management (Darling Consulting Group). If your team wants help scoping the first version of the harness around the workflows you actually run, a short discovery call is usually faster than reading more guides.

Sources

Sources

  1. Model Risk Management, Comptroller's Handbook
  2. 4 Questions You're Not Asking About Model Validators, But ...
  3. 4 Questions to Ask When Determining Model Validation ...
  4. ui testing - What are fixtures in programming?
  5. How to Evaluate AI Models Effectively
  6. What is Model Evaluation Datasets?
  7. Why the Data Used to Evaluate your Model Matters
  8. Evaluating Models Beyond the Textbook - Out-of-distribution ...
  9. Model Card: Sections, Examples, and Uses
  10. 5 things to know about AI model cards
  11. What Is a Model Card? Sections, Templates & Best Practices
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles