DEV Community

Sam Rivera
Sam Rivera

Posted on

I Swapped My Side Project's Paid LLM Calls for Free Model Access — Here's the Canary Harness I Used First

My little CLI tool committer (it drafts commit messages from a diff, nothing fancy) costs me about $4–6 a month in API calls. That's not a lot, but it's the only recurring cost in the project, and it annoys me out of proportion to its size. So when I got access to free model usage through MonkeyCode — including a free server option so I didn't have to point my laptop at the thing 24/7 — I wanted to see if the free tier could take over the workload without me noticing a difference.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

What I did not do: flip the endpoint in production and hope. What I did instead: build a 60-line canary harness that replays my last 30 real diffs against both the paid baseline and the free candidate, scores the outputs, and tells me whether the swap is safe. That harness is the actual artifact of this post.

The constraint set

Solo project rules:

  • Budget: the whole experiment gets one evening and zero dollars.
  • Quality bar: the free model's commit messages must be usable, not impressive. I grade on "would I edit this less than 30 seconds?"
  • Rollback: one environment variable. If the canary fails, nothing ships.

The canary harness

I keep the last 30 real diffs (sanitized) in fixtures/diffs/. The harness runs each diff through both models with the identical prompt, then does a dumb-but-honest comparison: conventional-commit format compliance, length sanity, and whether the summary mentions the file that actually changed.

#!/usr/bin/env python3
"""canary.py — replay real diffs against baseline vs candidate model."""
import json, os, re, sys, time
from pathlib import Path
import httpx

PROMPT = """Write a conventional-commit message for this diff.
One line, <=72 chars, then an optional body. Diff:\n\n{diff}"""

ENDPOINTS = {
    "baseline": (os.environ["PAID_BASE_URL"], os.environ["PAID_MODEL"]),
    "candidate": (os.environ["FREE_BASE_URL"], os.environ["FREE_MODEL"]),
}

def call(base_url: str, model: str, diff: str) -> dict:
    t0 = time.time()
    r = httpx.post(
        f"{base_url}/chat/completions",
        json={"model": model,
              "messages": [{"role": "user", "content": PROMPT.format(diff=diff)}],
              "temperature": 0.2},
        timeout=60,
    )
    r.raise_for_status()
    return {"text": r.json()["choices"][0]["message"]["content"].strip(),
            "latency_s": round(time.time() - t0, 2)}

def grade(msg: str, diff: str) -> dict:
    first = msg.splitlines()[0] if msg else ""
    changed = set(re.findall(r"diff --git a/(\S+)", diff))
    stem_hit = any(Path(f).stem.lower() in msg.lower() for f in changed)
    return {
        "format_ok": bool(re.match(r"^(feat|fix|chore|docs|refactor|test)(\(.+\))?: .+", first)),
        "len_ok": 0 < len(first) <= 72,
        "mentions_file": stem_hit,
    }

def main():
    diffs = sorted(Path("fixtures/diffs").glob("*.diff"))
    report = []
    for d in diffs:
        diff = d.read_text()[:8000]  # hard cap, big diffs are a separate problem
        row = {"fixture": d.name}
        for name, (url, model) in ENDPOINTS.items():
            out = call(url, model, diff)
            row[name] = {**grade(out["text"], diff), "latency_s": out["latency_s"]}
        report.append(row)
        print(json.dumps(row))
    Path("canary_report.json").write_text(json.dumps(report, indent=2))
    fails = [r["fixture"] for r in report
             if not all(r["candidate"][k] for k in ("format_ok", "len_ok", "mentions_file"))]
    print(f"\ncandidate pass rate: {len(diffs)-len(fails)}/{len(diffs)}")
    sys.exit(1 if len(fails) > len(diffs) * 0.15 else 0)  # >15% fail = don't swap

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Run it:

export PAID_BASE_URL=... PAID_MODEL=...
export FREE_BASE_URL=...  FREE_MODEL=...
python canary.py && echo "swap approved"
Enter fullscreen mode Exit fullscreen mode

The 15% failure threshold is arbitrary but declared up front, which is the part people skip. Decide the abandonment criteria before you like the results.

What actually happened

Metric Paid baseline Free candidate
Pass rate (30 fixtures) 29/30 26/30
Median latency 1.8s 3.1s
Monthly cost at my volume ~$5 $0
Failures 1 (huge refactor diff) 4 (3 of them: vague chore: update stuff)

Two honest findings:

  1. The failure mode was boring, not catastrophic. The free model didn't hallucinate; it got lazy and wrote generic messages on multi-file diffs. Adding "name the most-changed file" to the prompt fixed 2 of the 4 failures on rerun. Prompt tuning beat model shopping, again.
  2. Latency doubled and it doesn't matter. This is a CLI I run maybe 15 times a day. If this were an interactive autocomplete, 3 seconds would be a dealbreaker and I'd have walked away.

One thing I did not benchmark: throughput, context window limits, or how the free access behaves under sustained load. My fixture set is 30 diffs from one repo written by one person. Treat my numbers as a method demo, not a verdict on anything.

The decision

Canary passed (13% fail rate after the prompt fix, under my 15% line). I shipped the swap behind a flag:

# ~/.config/committer/env
COMMITTER_BASE_URL=$FREE_BASE_URL   # flip back to $PAID_BASE_URL to roll back
Enter fullscreen mode Exit fullscreen mode

Two-week rule: if I hand-edit more than a third of the generated messages, I flip it back and write the postmortem. The free server option meant I also didn't have to keep anything running locally — for a tool I use across two machines, that removed the one bit of ops I was dreading. If you want to run the same experiment, the harness above works against any OpenAI-compatible endpoint; swap in whatever free access you have.

Who should not do this

  • Anything customer-facing. Free tiers come with no SLA, and "my commit message helper is down" is a very different email than "your data pipeline is down."
  • Latency-sensitive paths (completion, streaming chat). Reread finding #2.
  • Workloads where eval is expensive. My grading heuristic worked because commit messages are shallow. If you can't write a cheap pass/fail check, you can't canary, and you're just vibes-swapping.

If you've run a similar canary for a small tool: what did your pass/fail check look like? Mine (regex + filename mention) feels one step above a coin flip and I'd genuinely steal a better cheap heuristic for single-line generation tasks.

Top comments (0)