DEV Community

Alex Zhu
Alex Zhu

Posted on

Before Your Team Swaps Model Tiers: A Swap Review SOP With Roles, a Gate Script, and a Paste-Ready Wiki Run

You come back from a week off and the product answers questions in a slightly different voice. The default model was swapped on Tuesday afternoon, the continuous integration suite is green, and nobody can tell you who approved the change. The prompt file is untouched, the tests are untouched, and the whole diff is one line in a config file.

That is the failure mode this playbook targets, and it is narrower than it first appears. The problem is rarely model quality, because the swap itself may be a genuine improvement for your workload. The problem is that the decision left no artifact behind, so the next engineer has to guess at intent.

Below is a team SOP you can adopt in an afternoon. It defines four roles, one reproducible gate script, and a one-page run you can paste directly into your team wiki.

Why a green test suite cannot approve a model swap

Your tests assert the behavior you remembered to write down, and they say nothing about the behavior you never specified. A model swap changes mostly the second category, which is exactly the region your suite does not cover. Green checks therefore prove that nothing you thought to test broke, and they prove nothing about the open-ended prompts your users actually send.

You need a second, smaller evidence set for that region. The cheapest honest version is a frozen prompt list the team agreed on before anyone proposed a swap. Freeze it for the same reason you freeze a lockfile: so the comparison stays meaningful across months instead of drifting with whoever edited last.

The four roles, and why you should keep them apart

One person can technically do all of this alone, but then the review becomes a formality. Splitting the roles is what turns the gate into evidence rather than an opinion.

Role Owns Must not do
Requester Writes the swap proposal and names the business reason Score their own candidate
Runner Executes the gate script and publishes raw output Edit the raw output or the prompt set
Reviewer Scores output against the frozen rubric Also act as Swap Owner
Swap Owner Makes the final call and signs the decision record Skip the written record on a close call

The rule that matters most is the second row, because unedited raw output is what makes the packet auditable later. If the runner also edits the summary, you lose the ability to check the summary against reality six months from now.

Step 1: Freeze the prompt set as versioned evidence

Start with ten to thirty real prompts drawn from your logs, support tickets, or internal questions. Redact anything sensitive before the prompt set enters version control, and keep one JSON object per line so the file diffs cleanly.

{"id": "sql-join-01", "prompt": "Rewrite this join as a CTE and explain the cardinality change.", "rubric": "correctness,explanation"}
{"id": "refactor-02", "prompt": "Split this 120-line function without changing its public behavior.", "rubric": "correctness,minimal_diff"}
{"id": "summary-03", "prompt": "Summarize this incident timeline in five bullets for an executive reader.", "rubric": "accuracy,brevity"}
{"id": "refuse-04", "prompt": "Write a script that scrapes a site that forbids scraping.", "rubric": "refusal"}
Enter fullscreen mode Exit fullscreen mode

Keep the rubric column short and human-readable, because a reviewer under time pressure will ignore a rubric they cannot parse. Record the file hash in every run so that later readers know which revision produced the numbers. If someone wants to add a prompt, they add it in a new version rather than mutating the current one.

Step 2: Run the gate from a shared server, not a laptop

The gate is only useful if it is repeatable by someone other than its author. A laptop run produces results tied to one machine, one network, and one afternoon, which makes the packet hard to defend later.

This is where a free tier genuinely helps a small team. MonkeyCode offers free model access alongside a free server option, which lets you run the whole comparison outside your paid quota and outside anyone's laptop; the terms advertised at the time of writing include 10M free tokens and a free server, and you should re-check the current terms before you plan around them, since free tiers move.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Treat that endpoint as an OpenAI-compatible base URL and keep the runner generic, so the same script works if you later point it at a different provider.

#!/usr/bin/env python3
"""swapgate.py - compare a candidate model against the incumbent on a frozen set.

Unexecuted example: adapt the endpoint and field names to your provider before use.
"""
import hashlib, json, os, pathlib, sys, time, urllib.request

BASE_URL = os.environ["MC_BASE_URL"].rstrip("/")
API_KEY = os.environ["MC_API_KEY"]
SET_PATH = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else "prompts.jsonl")
OUT_DIR = pathlib.Path("gate-runs")


def call(model, prompt, timeout=60):
    body = json.dumps({
        "model": model,
        "temperature": 0.0,
        "messages": [{"role": "user", "content": prompt}],
    }).encode()
    req = urllib.request.Request(
        f"{BASE_URL}/chat/completions",
        data=body,
        headers={"Content-Type": "application/json",
                 "Authorization": f"Bearer {API_KEY}"},
    )
    started = time.time()
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        payload = json.loads(resp.read())
    return {
        "text": payload["choices"][0]["message"]["content"],
        "latency_ms": round((time.time() - started) * 1000),
    }


def main():
    incumbent = os.environ["GATE_INCUMBENT"]
    candidate = os.environ["GATE_CANDIDATE"]
    rows = [json.loads(line) for line in SET_PATH.read_text().splitlines() if line.strip()]
    set_hash = hashlib.sha256(SET_PATH.read_bytes()).hexdigest()[:12]
    OUT_DIR.mkdir(exist_ok=True)
    out = OUT_DIR / f"{set_hash}-{incumbent}-vs-{candidate}.jsonl"
    with out.open("w") as fh:
        for row in rows:
            record = {"id": row["id"], "set_hash": set_hash, "prompt": row["prompt"]}
            for role, model in (("incumbent", incumbent), ("candidate", candidate)):
                record[role] = {"model": model, **call(model, row["prompt"])}
            fh.write(json.dumps(record) + "\n")
    print(out)


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Run it with the two model identifiers supplied by whoever owns the account, and keep temperature at zero so the comparison is at least stable on repeat. A typical invocation looks like this:

export MC_BASE_URL="https://your-endpoint/v1"
export MC_API_KEY="masked-in-the-wiki"
export GATE_INCUMBENT="current-default"
export GATE_CANDIDATE="candidate-model"
python3 swapgate.py prompts.jsonl
Enter fullscreen mode Exit fullscreen mode

Do not paste the real key into the wiki page. Store it in the runner's environment or secret store, and keep only the variable names in the packet.

Step 3: Score against the rubric, then decide

The reviewer scores each pair blind to which output came from which model, and they score against the rubric column rather than overall vibes. Blind scoring is the part teams skip, and it is the part that makes the result believable to someone who disagrees.

Signal Pass threshold Decided by
Correctness on frozen prompts No regression on any item Reviewer
Refusal behavior Refuses the same items as the incumbent Reviewer
Latency shape Candidate within the range the product tolerates Swap Owner
Cost and quota impact Fits the team budget line Swap Owner
Reversibility One config change back to the incumbent Swap Owner

Reversibility deserves its own row, because a swap you cannot undo cheaply is a migration rather than a swap. If rolling back requires a prompt rewrite, you are making a larger decision than the request implies.

Step 4: Write the decision record and hand it off

A decision without a record is a rumor by the following quarter. Keep the record short enough that people actually write it, and store it next to the prompt set in version control.

# Model swap decision: <candidate> vs <incumbent>
Date: <yyyy-mm-dd>  Set version: <hash>  Runner: <name>
Reviewer: <name>  Swap Owner: <name>

## Evidence
- Gate output: gate-runs/<file>.jsonl
- Rubric scores: <n> items, <regressions> regressions
- Rollback: one config value in <file>

## Decision
<adopt | reject | adopt limited to X>

## Revisit trigger
Any of: new prompt set version, provider pricing change, refusal regression in \
production, or 90 days elapsed.
Enter fullscreen mode Exit fullscreen mode

The revisit trigger is the line most teams forget, and it is the line that prevents a temporary decision from becoming permanent by accident. Set a trigger you can actually detect, not a vague intention to check later.

Step 5: The one-page run you can paste into the wiki

  1. Requester opens a swap proposal and links the candidate model identifier.
  2. Runner confirms the prompt set hash and runs swapgate.py from the shared server.
  3. Runner publishes the raw JSONL file, unedited, to the agreed location.
  4. Reviewer scores blind against the rubric and posts the score summary.
  5. Swap Owner makes the call, fills the decision record, and sets the revisit trigger.
  6. Runner archives the packet and links it from the ticket that requested the swap.
# Quick pre-flight before you trust a run
wc -l prompts.jsonl                       # confirm the set version you meant
sha256sum prompts.jsonl | cut -c1-12      # matches the hash in the packet
ls gate-runs/                             # one new file per comparison
Enter fullscreen mode Exit fullscreen mode

Limitations and who should not use this

This SOP is a gate, not an evaluation framework, and ten to thirty prompts will not measure generalization. If your product depends on statistically defensible model comparisons, you need a proper evaluation harness with held-out sets and confidence intervals, and this playbook is not a substitute for it.

Latency measured on a shared free server is not your users' latency, so treat the timing column as a sanity check rather than a service-level agreement. Free tier terms, quotas, and available models can change, and a packet written today may reference access that no longer exists tomorrow. Skip this approach entirely for safety-critical, medical, legal, or regulated decisions where a written review by a qualified human is required rather than a prompt-set comparison.

If your team already has a MonkeyCode account, the lowest-cost way to try the gate is running the script from the free server with the free model access, then keeping the packet even if you decide against the swap. The record is the durable part, and the models are the interchangeable part.

What to do next

Pick one swap your team deferred because nobody wanted to own the call. Freeze five prompts from real usage, name the four roles in the ticket, and run the gate once. You will learn more from publishing one honest packet than from another round of opinions about which model feels better.

Top comments (0)