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"}
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()
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
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.
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
- Requester opens a swap proposal and links the candidate model identifier.
- Runner confirms the prompt set hash and runs
swapgate.pyfrom the shared server. - Runner publishes the raw JSONL file, unedited, to the agreed location.
- Reviewer scores blind against the rubric and posts the score summary.
- Swap Owner makes the call, fills the decision record, and sets the revisit trigger.
- 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
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)