Most "it worked on my machine" failures in a free-tier AI gateway are configuration-parity bugs, not model bugs, and a twelve-line fingerprint plus four checks will surface them before the first prompt is sent. This workshop teaches that habit in 90 minutes: students write one config file, run one preflight script on two machines, and debug a deliberately broken deployment. The artifact they submit is two JSON reports that must agree on a config hash and disagree only where the environment is supposed to differ.
The failure this workshop prevents
A hosted small server and a laptop rarely disagree about Python. They disagree about three things, and each one produces an error message that points at the wrong layer.
-
Bind addresses. A laptop defaults to
127.0.0.1, while a hosted box must bind0.0.0.0or the service is unreachable from outside the machine. -
Environment presence. An API key exported in
~/.zshrcexists on the laptop and nowhere else, so auth fails only after deploy. - Egress shape. Some hosts block, proxy, or redirect outbound HTTPS to the model endpoint, so a request that answers instantly locally times out remotely.
None of these are model problems, and none of them require reading provider documentation to detect. The workshop replaces guesswork with a report that names the failing check and the exact host and port involved.
What students build
-
gateway.toml— a single source of truth for host, port, endpoint URL, model id, and token caps. -
preflight.py— a stdlib-only script that validates environment variables, required keys, port availability, and outbound reachability. -
preflight.local.jsonandpreflight.server.json— the two reports compared at the end. - One real request routed through the deployed gateway, with its latency recorded by hand.
The 90-minute timetable
| Minutes | Segment | Output |
|---|---|---|
| 0–10 | Framing and repository clone | Everyone has the same starting tree |
| 10–25 | Write gateway.toml from the blank template |
Config file committed |
| 25–40 | Write and run preflight.py locally |
preflight.local.json |
| 40–60 | Deploy the gateway and run the same script remotely | preflight.server.json |
| 60–75 | Break parity on purpose and diagnose | A written diagnosis per team |
| 75–85 | Send one real request and record latency | A two-line log excerpt |
| 85–90 | Debrief on fingerprint equality | Shared board of findings |
Step 1: Write the config file (15 min)
The template is intentionally boring. Every value that differs between machines lives in one place, so the fingerprint changes only when a human changes it on purpose.
[server]
host = "0.0.0.0"
port = 8080
max_inflight = 4
[model]
base_url = "https://api.example.invalid/v1" # replace with your provider base URL
name = "your-model-id"
timeout_s = 30
max_output_tokens = 256
[budget]
request_token_cap = 1024
Instruct students to keep secrets out of this file. The API key comes from the environment, and the preflight script checks presence only, never the value.
Step 2: Write the preflight script (15 min)
Give students the skeleton and let them fill the four check functions. The point of the exercise is that every check returns the same shape, so the report is machine-comparable.
#!/usr/bin/env python3
"""preflight.py - prove two machines agree before the first prompt."""
import hashlib
import json
import os
import socket
import sys
import urllib.error
import urllib.parse
import urllib.request
REQUIRED_ENV = ["GATEWAY_API_KEY"]
REQUIRED_KEYS = [("server", "host"), ("server", "port"),
("model", "base_url"), ("model", "name")]
def load_config(path):
import tomllib # python 3.11+
with open(path, "rb") as fh:
return tomllib.load(fh)
def fingerprint(cfg):
payload = json.dumps(cfg, sort_keys=True).encode()
return hashlib.sha256(payload).hexdigest()[:12]
def check_env():
missing = [k for k in REQUIRED_ENV if not os.environ.get(k)]
return {"name": "env", "ok": not missing, "missing": missing}
def check_config(cfg):
missing = [f"{s}.{k}" for s, k in REQUIRED_KEYS if not cfg.get(s, {}).get(k)]
return {"name": "config", "ok": not missing, "missing": missing}
def check_port(cfg):
host = cfg.get("server", {}).get("host", "0.0.0.0")
port = int(cfg.get("server", {}).get("port", 8080))
bind_host = "127.0.0.1" if host in ("0.0.0.0", "::") else host
try:
with socket.socket() as sock:
sock.bind((bind_host, port))
return {"name": "port", "ok": True, "value": f"{bind_host}:{port}"}
except OSError as exc:
return {"name": "port", "ok": False,
"value": f"{bind_host}:{port}", "error": exc.strerror}
def check_egress(cfg):
url = cfg.get("model", {}).get("base_url", "")
host = urllib.parse.urlparse(url).netloc
try:
with urllib.request.urlopen(urllib.request.Request(url), timeout=5) as resp:
return {"name": "egress", "ok": True, "host": host, "status": resp.status}
except urllib.error.HTTPError as exc:
# 401 or 404 still proves the path is open; auth is a separate concern
return {"name": "egress", "ok": True, "host": host, "status": exc.code}
except Exception as exc:
return {"name": "egress", "ok": False, "host": host,
"error": type(exc).__name__}
def main():
cfg = load_config(sys.argv[1] if len(sys.argv) > 1 else "gateway.toml")
checks = [check_env(), check_config(cfg), check_port(cfg), check_egress(cfg)]
report = {"fingerprint": fingerprint(cfg), "checks": checks,
"ok": all(c["ok"] for c in checks)}
print(json.dumps(report))
raise SystemExit(0 if report["ok"] else 1)
if __name__ == "__main__":
main()
Run it identically on both machines and capture the output: python3 preflight.py | tee preflight.local.json.
Step 3: Run it on the free server (20 min)
For the deploy half, teams need a host and a model endpoint. A free server option and free model access remove the budget negotiation from the exercise, so students spend their time on parity instead of procurement. The operator states that the free tier includes 10 million tokens of model access, and that a free server option is available; treat those numbers as a starting budget rather than a guarantee, because quotas and hardware are subject to change and are not audited in this workshop. Disclosure: This article was prepared as part of MonkeyCode's product outreach.
The deploy sequence is deliberately short. Copy the config, set the key as an environment variable on the host, run the same script, and compare fingerprints. If fingerprint differs between the two reports, a human edited a file during transit and the team must reconcile before debugging anything else.
Worked example: reading a failed report
One team's server-side report returned exit code 1 with this body, trimmed for length:
{"fingerprint":"9c1a7f0d3b2e",
"checks":[{"name":"env","ok":true,"missing":[]},
{"name":"config","ok":true,"missing":[]},
{"name":"port","ok":false,"value":"127.0.0.1:8080","error":"Address already in use"},
{"name":"egress","ok":true,"host":"api.example.invalid","status":404}],
"ok":false}
Three facts follow directly from the report. The fingerprint matches the laptop's, so the config is identical and the config hash is not the culprit. Port 8080 is already occupied on the host, which is why the gateway never started and the deploy log looked empty. Egress returns 404, which means DNS and TCP are fine and the path is open, so there is nothing to fix at the network layer.
The fix is a one-line config change to port 8081, a re-run, and an updated fingerprint recorded in both reports. The teaching moment is that the error message from the process manager never mentioned the port, while the preflight did.
Step 4: Break parity on purpose (15 min)
Each team applies one mutation from a sealed list and predicts which check fails before running anything. Predictions are written on the whiteboard, then tested.
- Change
hostto127.0.0.1on the server only. - Move
GATEWAY_API_KEYfrom the host environment intogateway.toml. - Replace the endpoint hostname with a non-resolving name.
- Set
portto a value below 1024 to see the permission error surface.
Most teams get two of four right. The recurring miss is mutation 2, because a hardcoded key passes the config check and fails the env check only if the script enforces separation, which is why the exercise exists.
What the report does not prove
- It does not validate model output quality, prompt behavior, or token accounting.
- A reachable endpoint with status 404 is counted as
ok: true; authentication is out of scope. - Fingerprints cover the config file only, not installed package versions or system libraries.
- Nothing here measures throughput, so it is not a load test and should not be sold as one.
Skip this approach if your deployment already runs behind a platform that injects configuration at boot, if your secrets live in a manager that forbids environment variables, or if you need production-grade tracing on day one. The workshop is for teams whose first deploy keeps failing for reasons nobody can name.
Debrief questions
- Which check caught a real problem in your setup, and which one was pure ceremony?
- What else belongs in the fingerprint without making it unstable across machines?
- How would you extend the report so a CI job can block a deploy on a failing check?
Keep both JSON reports in the repository next to the config. If your team needs a key and a host to run the exercise, the free model access and free server option are a reasonable place to start, and the reports will tell you the truth about whether the two machines agree.
Top comments (0)