cd /news/machine-learning/nightly-drift-checks-catch-a-free-mo… · home topics machine-learning article
[ARTICLE · art-109947] src=dev.to ↗ pub= topic=machine-learning verified=true sentiment=· neutral

Nightly Drift Checks: Catch a Free Model's Behavior Change Before Your Users Do

A developer has created a nightly drift-check harness to detect behavior changes in free LLM endpoints, which can silently degrade without changelogs. The 90-line Python script runs core prompts against the endpoint, compares outputs to a baseline, and alerts teams to quality dips before users notice. The approach was demonstrated using MonkeyCode's free server, and the article was prepared as part of MonkeyCode's product outreach.

read6 min views2 publishedAug 25, 2026

Here's the conclusion up front: a free LLM endpoint is a moving target. You can't see the changes, but they're happening — model updates, quantization tweaks, server-side prompt rewrites. And your app will feel them, usually as a slow, invisible quality dip.

I've spent weeks on this account probing free LLM servers, caching tokens, and building evaluation harnesses. The pattern I keep seeing: teams pick a free tier, wire it in, and then never look at it again. They treat it like a static API. It isn't.

The fix is a nightly drift check. A small script that runs your most important prompts against the endpoint, compares the outputs to a baseline, and tells you when something changed. Not a benchmark. Not a one-time eval. A recurring alarm.

This post walks through a 90-line harness you can run tonight. I'll use MonkeyCode's free server as the reference endpoint — it's an open-source project with free model access, a free server option, and, as advertised at the time of writing, a 10M token grant. The exact numbers may move, so check the repo's README before you depend on them.

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

Let's be honest: free endpoints don't come with changelogs. The provider can swap the underlying model, adjust the temperature default, or add a safety filter without telling you. Your tests still pass. Your error rate stays flat. But the responses get a little shorter, a little more evasive, a little less useful.

Users notice before you do. They don't file bugs for 'the bot got dumber.' They just stop using it.

A drift check turns 'the bot got dumber' into a concrete signal: 'the pass rate on 12 core prompts dropped from 92% to 74% overnight.' That's something you can act on.

Don't test everything. Pick 10-20 prompts that represent the actual workload your app handles. For each prompt, define what 'good' looks like.

Prompt Expected behavior
'Summarize this article in 3 bullets' Output contains at least 3 bullet-like lines
'Extract the email address from this text' Output contains a regex-matchable email
'Explain recursion to a 10-year-old' Output contains the word 'function' or 'calls itself'
'Classify this review as positive or negative' Output contains 'positive' or 'negative'

The key is that expected behavior must be checkable without an LLM. Keywords, regexes, length limits, or simple heuristics. If you need another model to judge the output, you're adding a second drift source.

Here's the core script. It's designed to run in a cron job or GitHub Action, and it has two modes: --baseline

to record current behavior, and --check

to compare against the baseline.

#!/usr/bin/env python3
'''drift_harness.py — nightly drift detection for free LLM endpoints.'''

import argparse
import json
import re
import sys
import urllib.request
from datetime import date
from pathlib import Path

CASES = [
    {
        'name': 'summary_bullets',
        'prompt': 'Summarize this article in 3 bullet points:' + chr(10) + chr(10) +
                  'The new update adds dark mode, faster startup, and offline sync.',
        'checks': [
            ('has_3_lines', lambda out: len([l for l in out.split(chr(10)) if l.strip().startswith('-')]) >= 3),
        ],
    },
    {
        'name': 'extract_email',
        'prompt': 'Extract the email address from this text:' + chr(10) + chr(10) +
                  'Contact Jane at jane.doe@example.com for more info.',
        'checks': [
            ('has_email', lambda out: re.search(r'[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9.]+', out) is not None),
        ],
    },
    {
        'name': 'recursion_explainer',
        'prompt': 'Explain recursion to a 10-year-old in 2 sentences.',
        'checks': [
            ('mentions_function', lambda out: 'function' in out.lower() or 'calls itself' in out.lower()),
        ],
    },
    {
        'name': 'sentiment_classifier',
        'prompt': 'Classify this review as positive or negative: ' +
                  "'The app crashes constantly but the design is pretty.'",
        'checks': [
            ('has_sentiment', lambda out: 'positive' in out.lower() or 'negative' in out.lower()),
        ],
    },
]

def call_endpoint(prompt, endpoint, api_key=None):
    '''Call an OpenAI-compatible chat completions endpoint.'''
    payload = {
        'model': 'default',
        'messages': [{'role': 'user', 'content': prompt}],
        'temperature': 0.2,
    }
    headers = {'Content-Type': 'application/json'}
    if api_key:
        headers['Authorization'] = 'Bearer ' + api_key
    req = urllib.request.Request(
        endpoint, data=json.dumps(payload).encode(), headers=headers
    )
    with urllib.request.urlopen(req, timeout=30) as resp:
        data = json.loads(resp.read().decode())
    return data['choices'][0]['message']['content']

def run_case(case, endpoint, api_key=None):
    '''Run a single case, return (passed, output, details).'''
    try:
        output = call_endpoint(case['prompt'], endpoint, api_key)
    except Exception as e:
        return False, '', 'endpoint error: ' + str(e)
    failures = []
    for name, check in case['checks']:
        if not check(output):
            failures.append(name)
    if failures:
        return False, output, 'failed checks: ' + str(failures)
    return True, output, 'ok'

def main():
    parser = argparse.ArgumentParser(description='Nightly LLM drift checker')
    parser.add_argument('--endpoint', required=True, help='OpenAI-compatible endpoint URL')
    parser.add_argument('--api-key', default=None, help='Optional API key')
    parser.add_argument('--baseline', action='store_true', help='Record baseline results')
    parser.add_argument('--threshold', type=float, default=0.8, help='Min pass rate before alert')
    args = parser.parse_args()

    results = []
    for case in CASES:
        passed, output, details = run_case(case, args.endpoint, args.api_key)
        results.append({'name': case['name'], 'passed': passed, 'details': details})
        status = 'PASS' if passed else 'FAIL'
        print('  ' + status + '  ' + case['name'] + ': ' + details)

    pass_rate = sum(1 for r in results if r['passed']) / len(results)
    print('Pass rate: {:.0%}'.format(pass_rate))

    if args.baseline:
        baseline_path = Path('baseline_' + date.today().isoformat() + '.json')
        baseline_path.write_text(json.dumps(results, indent=2))
        print('Baseline written to ' + str(baseline_path))
        return 0

    baseline_files = sorted(Path('.').glob('baseline_*.json'))
    if not baseline_files:
        print('No baseline found. Run with --baseline first.')
        return 2
    baseline = json.loads(baseline_files[-1].read_text())
    baseline_pass = sum(1 for r in baseline if r['passed']) / len(baseline)

    print('Baseline pass rate: {:.0%}'.format(baseline_pass))
    if pass_rate < args.threshold:
        print('ALERT: pass rate dropped below {:.0%}!'.format(args.threshold))
        return 1
    if pass_rate < baseline_pass - 0.1:
        print('ALERT: pass rate dropped {:.0%} vs baseline!'.format(baseline_pass - pass_rate))
        return 1
    print('OK: no significant drift detected.')
    return 0

if __name__ == '__main__':
    sys.exit(main())

Add a cron job:

0 2 * * * cd /path/to/project && python3 drift_harness.py --endpoint "$MONKEYCODE_ENDPOINT" --api-key "$MONKEYCODE_KEY" --threshold 0.8

Or a GitHub Action that runs nightly and opens an issue on failure.

The alert means something changed. Now what?

The point isn't to prevent drift. It's to know about it on day one, not day thirty.

This harness checks surface-level behavior, not semantic quality. A model can pass all your keyword checks while getting subtly worse at reasoning. That's fine — this is an early-warning system, not a full evaluation.

Your test cases will go stale. As your app evolves, your prompts change. Update the CASES list regularly, and re-baseline after intentional changes.

Free endpoints can be flaky. A single timeout will count as a failure. Run the harness a few times before trusting a single alert. Or add retries to call_endpoint

.

Skip the drift check if:

Free LLM tiers are a trade: cost for control. You don't get to see the model updates, the config changes, the silent tweaks. But you can build your own early-warning system.

Set up a nightly drift check this weekend. Ten prompts, one script, one cron job. When the model changes — and it will — you'll be the first to know, not the last.

And if you're looking for a free endpoint to practice on, MonkeyCode's free server is a reasonable place to start. The 10M token grant and free server are what the project advertises today. Check the README for current numbers, then point this harness at it and see what you learn.

── more in #machine-learning 4 stories · sorted by recency
── more on @monkeycode 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/nightly-drift-checks…] indexed:0 read:6min 2026-08-25 ·