An LLM can be free, fast, and still flip a classification when the only change is a capital letter. That is enough to break an automation before it is useful.
Expected output after two files and one command:
$ python fake_endpoint.py &
$ python canary.py
base APPROVE PASS
case REJECT FAIL expected APPROVE
space APPROVE PASS
suffix APPROVE PASS
Learning question: for your task, which input changes are safe, and does the endpoint hold still when you apply them?
What we are testing
This is not an accuracy benchmark. A prompt-invariance canary only checks one thing: when a prompt changes in a way that should not change the output, the output stays the same. If casing, spacing, or punctuation flips a label, an agent that feeds generated text into this endpoint will fail in ways that are hard to trace later.
A canary is useful because running it is cheap. It is one small loop, not a dataset.
What you need
- Python 3.10 or newer
- Two terminal windows
- No third-party packages
MonkeyCode offers free model access and a free server option, which make repeated small calls and a hosted rerun point cheap. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The demo below needs neither, so you can reproduce the failure with only Python.
Step 1: build a tiny fake endpoint
The fake endpoint is intentionally brittle. It returns APPROVE only when the exact lowercase substring approve if urgent appears in the last user message. A capital A makes it return REJECT.
Create fake_endpoint.py:
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
RULE = 'approve if urgent'
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get('Content-Length', 0))
body = json.loads(self.rfile.read(length) or b'{}')
content = body.get('messages', [{}])[-1].get('content', '')
label = 'APPROVE' if RULE in content else 'REJECT'
payload = json.dumps({'choices': [{'message': {'content': label}}]}).encode()
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', str(len(payload)))
self.end_headers()
self.wfile.write(payload)
def log_message(self, format, *args):
pass
if __name__ == '__main__':
HTTPServer(('127.0.0.1', 8765), Handler).serve_forever()
Run it in the first terminal and leave it running:
python fake_endpoint.py
Step 2: write the canary client
Create canary.py:
import json
import urllib.request
URL = 'http://127.0.0.1:8765/v1/chat/completions'
PAIRS = [
('base', 'Reply APPROVE or REJECT for: approve if urgent'),
('case', 'Reply APPROVE or REJECT for: Approve if urgent'),
('space', 'Reply APPROVE or REJECT for: approve if urgent '),
('suffix', 'Reply APPROVE or REJECT for: approve if urgent.'),
]
def call(prompt):
data = json.dumps({'messages': [{'role': 'user', 'content': prompt}]}).encode()
req = urllib.request.Request(URL, data=data, method='POST',
headers={'Content-Type': 'application/json'})
with urllib.request.urlopen(req, timeout=5) as response:
body = json.loads(response.read())
return body['choices'][0]['message']['content'].strip()
for name, prompt in PAIRS:
label = call(prompt)
status = 'PASS' if label == 'APPROVE' else 'FAIL expected APPROVE'
print(f'{name:<8} {label:<10} {status}')
In the second terminal:
python canary.py
You should see:
base APPROVE PASS
case REJECT FAIL expected APPROVE
space APPROVE PASS
suffix APPROVE PASS
The one failing fixture
The only change in case is approve became Approve. The fake endpoint treats it as a different input and returns REJECT. That is the exact class of break you want to catch before it reaches an agent or a downstream API call.
What to do when a fixture fails
- Write down the transformation that failed.
- Decide whether that transformation should be semantically identical for your task.
- If yes, fix the prompt, the output contract, or the parser.
- Add a regression fixture so the same break cannot return silently.
Common mistakes
- Testing only one prompt and calling the endpoint stable.
- Comparing raw strings without trimming whitespace or fixing casing.
- Confusing invariance with correctness: our fake endpoint can be stable for some changes and still be wrong about the task.
- Assuming a free endpoint will keep the same behavior when the provider changes models.
Limitations
- This canary only catches the transformations you list.
- It does not measure accuracy, latency, cost, or calibration.
- It does not prove a real provider is stable over time.
- It is not a replacement for an eval set or human review in safety-critical settings.
Who should not use this approach
- Teams that need a full accuracy benchmark across many examples.
- Tasks where incorrect labels are expensive or dangerous; add human review.
- Anyone who expects a free model endpoint to be permanent or semantically identical across upgrades.
What you should understand after completing this
A model call is stable only within a contract you define. Before automation, you need to say which input variations are safe and test them explicitly. The cheapest endpoint is not the one with the lowest price per token; it is the one that does not flip your labels on inputs you already decided are equivalent.
Try adding a probe that changes urgent to expedited. Decide first whether that should keep the label, then compare the result.
Top comments (0)