Intermediate14 min read

Test a local app before updating its model

Run a repeatable check of response shape and task accuracy, record the model identity, and compare a candidate with your baseline.

Record a baseline before changing anything

Use your installed quickstart model and keep Ollama running. The example runs four fictional library requests, including an out-of-scope question. It checks the exact JSON keys and label, then records the installed model digest, runtime version, test results, and median elapsed time. Use python3 on macOS or Linux or py -3 on Windows if python is unavailable.

Download check_local_app.py
View check_local_app.py
import argparse
import json
import statistics
from time import perf_counter
from urllib.error import HTTPError
from urllib.request import Request, urlopen

BASE = "http://127.0.0.1:11434"
LABELS = ["RETURN", "RENEW", "RESERVE", "UNKNOWN"]
CASES = [
    ("Where can I return my book?", "RETURN"),
    ("I am still reading. Extend the due date.", "RENEW"),
    ("Book the 3D printer for Saturday.", "RESERVE"),
    ("What time does the library open?", "UNKNOWN"),
]
SCHEMA = {
    "type": "object",
    "properties": {"label": {"enum": LABELS}},
    "required": ["label"],
    "additionalProperties": False,
}
SYSTEM = (
    "Classify fictional library requests. RETURN means giving back a loan. "
    "RENEW means extending a loan. RESERVE means booking equipment. "
    "Use UNKNOWN for any other request. Return JSON with one field, label."
)


def request(endpoint, body=None):
    encoded = json.dumps(body).encode("utf-8") if body is not None else None
    req = Request(BASE + endpoint, data=encoded, headers={"Content-Type": "application/json"})
    with urlopen(req, timeout=120) as response:
        return json.load(response)


def model_digest(model):
    entry = next((item for item in request("/api/tags")["models"] if item["name"] == model), None)
    if entry is None:
        raise ValueError(f"Model not installed: {model}")
    return entry["digest"]


def evaluate(model):
    digest = model_digest(model)
    runtime = request("/api/version")["version"]
    results = []
    for case, (prompt, expected) in enumerate(CASES, start=1):
        start = perf_counter()
        outcome = request("/api/chat", {
            "model": model,
            "messages": [
                {"role": "system", "content": SYSTEM},
                {"role": "user", "content": prompt},
            ],
            "format": SCHEMA,
            "options": {"temperature": 0, "num_ctx": 2048, "num_predict": 64},
            "stream": False,
        })
        elapsed = round(perf_counter() - start, 3)
        try:
            item = json.loads(outcome["message"]["content"])
        except (KeyError, TypeError, ValueError):
            item = None
        shape_ok = isinstance(item, dict) and set(item) == {"label"} and item["label"] in LABELS
        actual = item["label"] if shape_ok else None
        results.append({
            "case": case, "expected": expected, "actual": actual,
            "passed": actual == expected, "seconds": elapsed,
        })

    if model_digest(model) != digest or request("/api/version")["version"] != runtime:
        raise ValueError("The model or runtime changed during the run. Run the check again.")
    passed = sum(row["passed"] for row in results)
    print(json.dumps({
        "runtime": runtime,
        "model": model,
        "digest": digest,
        "passed": passed,
        "total": len(results),
        "median_seconds": round(statistics.median(row["seconds"] for row in results), 3),
        "cases": results,
    }, indent=2))
    return passed == len(results)


parser = argparse.ArgumentParser(description="Run four local model checks and print a JSON report.")
parser.add_argument("model", help="Exact tag shown by ollama list")
args = parser.parse_args()
try:
    success = evaluate(args.model)
except HTTPError as error:
    raise SystemExit(f"Ollama returned HTTP {error.code}: {error.read().decode()}") from None
except (OSError, ValueError) as error:
    raise SystemExit(str(error)) from None
raise SystemExit(0 if success else 1)
python check_local_app.py qwen2.5:1.5b > baseline.json
You’re ready when

baseline.json contains four results and a model digest. A finished report has exit code 0 if all cases pass or 1 if any case fails. Keep the report even if some cases fail. If the command reports a missing model, connection error, or version change, fix the issue and rerun it.

Evaluate a candidate under the same conditions

  1. Choose an installed model with a distinct tag. Keep the original available while you compare. Check the candidate model card and license for your intended use, especially before redistribution.
  2. Run the same script with the candidate tag and save the result to candidate.json. Keep the prompt, test cases, hardware, and runtime constant when comparing model behavior.
  3. Open both reports. Check exact failures by case, then compare elapsed times over repeated warm runs. The script’s median mixes the first request and later ones, so use the performance lesson for controlled timing.
  4. Choose explicit acceptance criteria for your app. If a candidate misses them, keep the baseline configuration while you investigate the cases.

Add a case that would catch a real error

Add “I want to return this copy and reserve the projector” to CASES. Decide what your app should do with two intents before assigning a label. You may need a new MULTIPLE label or a manual review path. Update the schema, prompt, and expected result together, then rerun both models.

When people use the app, track request latency, errors, schema failures, and the percentage sent for review. Store sensitive prompt text only if your workflow requires it and controls who can read it.

Check your understanding

Why write down the digest as well as the tag?

A tag can point at different files after an update. The digest lets you identify the model contents you evaluated.

Sources

Search the guide