A prompt works in one client and misbehaves in another. Nobody saved the request. That is the whole bug: you compared prompts, not the actual bytes on the wire.
Request parity means the same fields, the same values, the same resolved defaults, from the same host. This FAQ lists claims developers repeat, a cheap check for each, and a corrected mental model.
Everything below is a method. The code is illustrative and was not executed for this article.
Q1: "Same prompt means same request."
Usually false. Clients inject defaults you never typed.
- A default system prompt.
- A default
temperature. - A default
max_tokens. - A default
stopsequence or response format. - Streaming on in one client, off in the other.
Cheap check: dump the full payload each client sends, then hash it. Two hashes, two requests.
Corrected model: the prompt is one field out of many. Compare the object, not the string.
Q2: "temperature: 0 makes it deterministic."
Not something to assume. Determinism is a property you measure, not a field you set.
Many providers do not promise byte-identical output across calls. Batching, routing, and version changes can move results. Check your provider's documentation instead of trusting folklore.
Cheap check: send one fingerprinted request N times. Count distinct normalized outputs.
Corrected model: treat 0 as "low variance". If your test asserts exact text, that assumption needs evidence.
Q3: "My laptop and the server send the same thing."
Code can be identical while the environment is not.
- Different client library version.
- Different resolved endpoint host.
- A proxy variable set only on one side.
- A different default timeout or retry wrapper.
Cheap check: fingerprint the environment too. Record version, host, and the names of proxy variables. Never record their values.
Corrected model: parity belongs to the pair (client, host), not to the repository.
Q4: "HTTP 200 means the request was honored."
A status code describes transport. It says nothing about semantics.
Servers may ignore unknown fields, rename parameters, or truncate output at a default token limit. Your JSON looked fine. The work was not done.
Cheap check: vary one field at a time and watch whether behavior moves. If max_tokens changes output length predictably, that field is live. If nothing changes, it may be dropped.
Corrected model: verify fields behaviorally, not by hoping the docs and the server agree.
Q5: "One green run proves parity."
One run proves one run. Parity is a rate with a threshold you chose in advance.
Cheap check: run the probe, compute agreement, list mismatches, then apply a stop rule.
Corrected model: decide the gate before you look at results. Otherwise you will rationalize whatever you see.
The artifact: a small fingerprint probe
Prerequisites: Python 3, an endpoint, a key, a place to run it that matches production. Keep it to a handful of calls and check your provider's current limits first.
# parity_probe.py — illustrative sketch. Not executed for this article.
import hashlib, json, os, time, urllib.request
ENDPOINT = os.environ["MODEL_ENDPOINT"] # full URL from env, never hardcode
MODEL = os.environ["MODEL_ID"]
API_KEY = os.environ["MODEL_API_KEY"]
PAYLOAD = {
"model": MODEL,
"temperature": 0,
"max_tokens": 64,
"messages": [
{"role": "system", "content": "Reply with one JSON object only."},
{"role": "user", "content": "Return the object {\"ok\": true}."},
],
}
def fingerprint(obj):
canonical = json.dumps(obj, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(canonical.encode()).hexdigest()[:16]
def env_fingerprint():
proxy_names = sorted(k for k in os.environ if k.lower().endswith("_proxy"))
return {
"python": os.sys.version.split()[0],
"host": ENDPOINT.split("/")[2],
"proxy_vars": proxy_names, # names only, never values
"request_fp": fingerprint(PAYLOAD),
}
def send_once():
body = json.dumps(PAYLOAD).encode()
req = urllib.request.Request(
ENDPOINT,
data=body,
headers={"Content-Type": "application/json",
"Authorization": f"Bearer {API_KEY}"},
)
start = time.time()
with urllib.request.urlopen(req, timeout=30) as resp:
raw = resp.read().decode()
status = resp.status
return {"status": status,
"ms": round((time.time() - start) * 1000),
"env": env_fingerprint(),
"response": raw}
if __name__ == "__main__":
for _ in range(5): # N is a choice; justify it, don't guess
print(json.dumps(send_once()))
Adapt the response parsing to your provider's shape. Treat the response field as untrusted text until you parse it. Redact before you store anything, and never log keys or user content.
The probe should run where the job runs. That is the point of Q3. If you do not have a spare host, a free server option is enough for a short run like this.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. Free model access and a free server option are offered by the product; confirm current limits and policies in its own documentation before you rely on them.
Reading the output
Group runs by request_fp, then compare normalized response text.
- Same fingerprint, different text. Sampling or server-side variance. Record the rate. Stop asserting exact text.
- Different fingerprints across hosts. Client or environment drift. Fix that first, before comparing outputs at all.
- Same fingerprint, same text, one host only. You measured one host. Repeat on the other.
| Situation | Cheap check | Corrected model |
|---|---|---|
| Playground vs script | hash the payloads | two clients, two requests |
| Laptop vs server | env fingerprint diff | parity is per host pair |
| Flaky exact-text test | 5-run agreement count | gate on rate, or assert schema |
| "Param ignored?" | vary one field | verify behaviorally |
Pick the gate before running: for exact-text assertions, any disagreement is a failing gate. For schema assertions, a single valid shape is often enough.
Limits and who should skip this
- The probe measures one endpoint pair at one moment. It is not a guarantee.
- It cannot see server-side routing or model version changes.
- Five runs can hide rare differences. Choose N from your risk, not from habit.
- Free tiers may idle, restart, or cap usage. Confirm the current policy rather than assuming a long run survives.
- Do not send production data to a free tier without checking its retention terms.
- Skip this if you need guaranteed determinism. Use schema validation and constrained output instead, then test the schema.
- Skip it if you cannot store fingerprints safely. A leaky probe creates the incident it was meant to prevent.
If you have been comparing prompts, switch to comparing requests. The hash is cheap, and it settles arguments fast.
Top comments (0)