A weekend side project that depends on free model access has an ordering problem. Most people wire the endpoint first, get one good response, and then discover that the demo is only as reliable as a service they do not control.
The fix is unglamorous: build the demo path against recorded fixtures first, and treat the live call as an optional upgrade. The demo stays deterministic, the AI dependency becomes swappable, and the live path gets tested deliberately instead of accidentally.
The failure this prevents
A demo has one job — to show a single end-to-end path working. That path usually contains three parts you control (input, prompt construction, output rendering) and one you do not (the model response).
When the uncontrolled part sits in the middle of the demo, every rehearsal is a fresh experiment. Prompt tweaks change output length, output length changes your rendering, and a slow or refused call changes the story you tell on stage.
Fixtures move the model to the edge of the system. You record responses once, commit them, and then develop everything else against a stable byte stream.
Scope cut: three features out, one in
The point of the cut is to make the fixture surface small enough to record in an afternoon. This table is the decision rule I would apply to an illustrative weekend build — not a record of a build that happened.
| Candidate | Verdict | Reason |
|---|---|---|
| Streaming output | Cut | Adds partial-chunk fixtures and renderer state you cannot fake cheaply |
| Multi-model comparison | Cut | Doubles the fixture matrix for a demo that needs one path |
| Retry and backoff policy | Defer | Belongs to the live mode, which the demo does not use |
| One prompt, one schema, one renderer | Keep | Everything the demo needs, nothing it does not |
Write the cut list down before writing code. A cut list you can point at is what stops a weekend build from turning into a two-week build with a broken demo.
Artifact: a three-mode client (illustrative)
The following Python is illustrative and was not executed for this post. It has no dependencies beyond the standard library, and it deliberately omits endpoint details — those come from whatever provider you use.
Prerequisites: Python 3.10+, a fixtures/ directory in the repo, and a prompt constant you are willing to treat as a cache key.
# illustrative — not executed as part of this post
import hashlib
from pathlib import Path
FIXTURES = Path("fixtures")
def cache_key(prompt: str, model_id: str) -> str:
# model_id is part of the key so a model swap invalidates old fixtures
raw = f"{model_id}\x00{prompt}".encode("utf-8")
return hashlib.sha256(raw).hexdigest()[:16]
class ModelClient:
def __init__(self, mode: str, model_id: str):
if mode not in {"replay", "record", "live"}:
raise ValueError(f"unknown mode: {mode}")
self.mode = mode
self.model_id = model_id
FIXTURES.mkdir(exist_ok=True)
def complete(self, prompt: str) -> str:
path = FIXTURES / f"{cache_key(prompt, self.model_id)}.txt"
if self.mode == "replay":
if not path.exists():
raise FileNotFoundError(
f"no fixture {path.name}; record it first"
)
return path.read_text(encoding="utf-8")
if self.mode == "record" and path.exists():
return path.read_text(encoding="utf-8")
text = self._call_provider(prompt) # your endpoint call
if self.mode in {"record", "live"}:
path.write_text(text, encoding="utf-8")
return text
def _call_provider(self, prompt: str) -> str:
raise NotImplementedError("wire your provider here")
Two design notes matter more than the code.
- The model identifier is inside the cache key. When you change models, old fixtures become unreachable instead of silently wrong.
-
recordreuses an existing fixture. This makes re-running the recorder idempotent, so a crashed recording session does not cost you the fixtures you already have.
The check that matters
A fixture directory with no check is just a pile of text. The check below proves the demo path does not touch the network and does not vary between runs.
# illustrative test
def test_replay_is_deterministic_and_offline():
client = ModelClient(mode="replay", model_id="demo-model")
first = client.complete(PROMPT)
second = client.complete(PROMPT)
assert first == second
assert first.strip() != ""
For the non-Python part of the demo, the equivalent check is a diff. Run the demo twice against --mode=replay, write both outputs to disk, and diff them. Any diff is either a real bug or an unrecorded nondeterminism in your own code — both worth knowing before a demo.
What this check does not tell you: whether the live model produces anything like the fixture. Recorded output is a fixed artifact, not evidence of model behaviour today.
Where the free access fits
Replay mode needs no network at all, which makes it the right default for development. The live path is where free model access and a free server option become useful: record mode needs a reachable endpoint, and a hosted demo needs somewhere to run.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
MonkeyCode is described by its operator as an open-source project offering free model access and a free server option. Those are operator-supplied availability claims, and quotas, model names, and terms of this kind change frequently — this post deliberately does not pin a token figure or a hardware spec, because a number copied into an article ages badly. Check the current terms on the project page before planning a build around them.
Nothing above depends on those terms being true. The fixture layer works the same whether live calls come from a free tier, a paid key, or a local model.
Failure modes this does not catch
- Latency and streaming. Replay returns instantly. Timeouts, partial chunks, and slow first tokens only appear in live mode.
- Limit behaviour. Rate limits, refusals, and quota errors are invisible to fixtures by construction.
- Prompt rot. Change the prompt and every fixture miss. That is correct behaviour, but it means prompt iteration invalidates the cache.
- Quality drift. A fixture recorded from an older model can look fine while current outputs differ in format.
- False confidence. A green replay suite is not a passing integration test, and should not be described as one.
Who should skip this
The approach is a poor fit for projects whose subject is model behaviour — evaluation harnesses, prompt benchmarks, latency work. Those need live calls as the primary artifact.
It is also thin value for a one-off script that will run once. Fixtures pay off when you rehearse the same path repeatedly, which is exactly the side-project demo case.
A closing rule
Keep the fixtures in the repository, keep replay as the default mode, and make the live run something you invoke on purpose. If you try this flow against the free model access, record the fixtures on day one — the demo then survives an endpoint hiccup, a quota change, or a laptop on conference Wi-Fi.
Top comments (0)