DEV Community

Casey Li
Casey Li

Posted on

When Free Inference Should Not Own the Contract

Free inference is a laboratory instrument. It is not a source of truth for APIs, schemas, or customer-facing shape. Teams that hand a complimentary model the right to finish a half-written contract ship invented headers, unsigned error objects, and agent traces that look successful while the interface has already drifted.

The damage is quiet. Tests still pass on fixtures the model just wrote. Reviewers read fluent Markdown and miss the field that never existed in production.

This article is a refusal guide. It treats free-tier models and shared free servers as a sketch lane, names the authority surfaces that must stay out of that lane, and encodes the decision as a small preflight that fails closed. The point is not to starve experiments. The point is to stop pattern-completion from minting the public shape of a system.

Agent write-ups keep circling the same bruise. A model fills gaps. It will invent X-Request-Id because many APIs have one, pick a timeout that looks professional, and name error codes after a popular gateway. Cheap generation makes that habit inexpensive. Inexpensive habits spread. When the generator is an unpinned free endpoint, the cost of a wrong assumption is paid later, in review and in the incident that looks like a product bug.

Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode provides free model access and a free server option that some operators use as an isolated lab. Those two availability claims are the only product facts used here. No quota, model name, hardware profile, uptime, or durability promise is implied. A disposable lane is useful because it can be thrown away. It is the wrong place to mint a contract.

Think of a spare key hanging on a hook labeled workshop. That key should open the bench, not the vault. Wire formats, authentication, retention, and anything another team will integrate against are the vault. Free inference is the workshop key. Mixing the two is how a sketch becomes an unofficial standard overnight.

Red flags appear before the first token is spent. A task that defines a wire format is already ineligible. So is a task that chooses a migration, drafts customer incident copy, or must replay bit-identically a month later. Shared free servers add a second class of refusal: if two jobs can collide on a working directory, an environment variable, or a port, the run is a sketch. It cannot be an authority run, no matter how tidy the prompt.

The better homes for that work are boring, which is the feature. Pin a snapshot you can name in a review comment when the output is only a proposal. Prefer JSON Schema and frozen OpenAPI when other services will integrate. Prefer deterministic scripts for lockfiles, license headers, and changelog numbers. The free lane still has honest work: prompt-shape probes, adversarial inputs against the gate below, and throwaway clients that never leave scratch/.

Exit criteria should be mechanical, not a vibe at the end of a long loop. If a path sits under contracts/, free inference never writes it. If a model is allowed to propose text, the file lands in scratch/ and a separate, non-model check is the only promotion path. If the working directory is not empty and owned by this job, the job aborts. If the object fails schema validation, the loop stops. It does not “fix” the schema to match the object. That last sentence is the whole guide in miniature.

The artifact below is a proposed gate. It is labeled a proposal because this article does not execute it against a live endpoint. Operators can run it locally as preflight before any agent is allowed to call a free model.

# authority_gate.py — proposed preflight, fail closed
from __future__ import annotations

import json
import os
from pathlib import Path

AUTHORITY_PREFIXES = (
    "contracts/",
    "openapi/",
    "schemas/",
    "migrations/",
    "public-status/",
)
SCRATCH_PREFIX = "scratch/"
LAB_ENV_MARKERS = ("FREE_INFERENCE", "LAB_SERVER")


class GateError(RuntimeError):
    pass


def classify(path: str) -> str:
    normalized = path.replace("\\", "/").lstrip("./")
    if normalized.startswith(AUTHORITY_PREFIXES):
        return "authority"
    if normalized.startswith(SCRATCH_PREFIX):
        return "scratch"
    return "unknown"


def assert_lab_isolation(env: dict[str, str] | None = None) -> None:
    env = env if env is not None else os.environ
    if not any(env.get(k) == "1" for k in LAB_ENV_MARKERS):
        raise GateError("lab markers missing; refusing free-inference routing")
    workdir = Path(env.get("LAB_WORKDIR", "")).resolve()
    if not workdir.is_dir():
        raise GateError("LAB_WORKDIR is not a directory")
    leftover = [p.name for p in workdir.iterdir() if p.name != ".keep"]
    if leftover:
        raise GateError(f"workdir not empty: {leftover!r}")


def assert_writable(path: str) -> None:
    kind = classify(path)
    if kind == "authority":
        raise GateError(f"authority surface is not writable by free inference: {path}")
    if kind == "unknown":
        raise GateError(f"unclassified path; refuse rather than guess: {path}")


def validate_proposal(schema_path: Path, payload: dict) -> None:
    schema = json.loads(schema_path.read_text())
    required = schema.get("required", [])
    missing = [key for key in required if key not in payload]
    if missing:
        raise GateError(f"proposal missing required keys {missing}; stop, do not edit schema")
    extras = [key for key in payload if key not in schema.get("properties", {})]
    if extras:
        raise GateError(f"proposal invented keys {extras}; stop, do not widen schema")


def route(task: dict, env: dict[str, str] | None = None) -> str:
    """Return 'lab' or raise. Never returns 'authority'."""
    assert_lab_isolation(env)
    for path in task.get("writes", []):
        assert_writable(path)
    if task.get("needs_replay") or task.get("customer_visible"):
        raise GateError("replay or customer-visible work is not a lab task")
    return "lab"
Enter fullscreen mode Exit fullscreen mode

A tiny contract file makes the refusal visible. The schema is the vault. The model is not allowed to enlarge it.

{
  "type": "object",
  "properties": {
    "error_code": {"type": "string"},
    "retryable": {"type": "boolean"}
  },
  "required": ["error_code", "retryable"],
  "additionalProperties": false
}
Enter fullscreen mode Exit fullscreen mode
# test_authority_gate.py — proposed tests
import json
from pathlib import Path

import pytest

from authority_gate import GateError, route, validate_proposal


def test_refuses_contract_write(tmp_path):
    env = {"FREE_INFERENCE": "1", "LAB_WORKDIR": str(tmp_path)}
    task = {"writes": ["contracts/errors.json"], "needs_replay": False}
    with pytest.raises(GateError, match="authority surface"):
        route(task, env)


def test_refuses_invented_keys(tmp_path):
    schema = tmp_path / "errors.schema.json"
    schema.write_text(json.dumps({
        "properties": {"error_code": {}, "retryable": {}},
        "required": ["error_code", "retryable"],
    }))
    payload = {"error_code": "upstream_timeout", "retryable": True, "hint": "try later"}
    with pytest.raises(GateError, match="invented keys"):
        validate_proposal(schema, payload)


def test_allows_scratch_only(tmp_path):
    env = {"FREE_INFERENCE": "1", "LAB_WORKDIR": str(tmp_path)}
    task = {"writes": ["scratch/probe-client.md"], "needs_replay": False}
    assert route(task, env) == "lab"
Enter fullscreen mode Exit fullscreen mode

Shell wrapping keeps the same rule outside Python. The commands are local checks, not a claim about any hosted fleet.

export FREE_INFERENCE=1
export LAB_WORKDIR="$(pwd)/.lab-empty"
mkdir -p "$LAB_WORKDIR"
# fail if the bench is dirty
[ -z "$(ls -A "$LAB_WORKDIR" | grep -v '^\.keep$')" ] || exit 2
python -m pytest test_authority_gate.py -q
# promotion is a human or pinned step, never the free model
install -m 0644 scratch/proposal.json /tmp/held-proposal.json
# contracts/ stays untouched unless this diff is empty
git diff --exit-code -- contracts openapi schemas migrations
Enter fullscreen mode Exit fullscreen mode

Promotion is the exit. A proposal that survives schema validation still does not write the contract. A reviewer, or a pinned model whose snapshot id is recorded in the pull request, copies fields that already exist. New keys require a schema change in a separate commit with no free-inference author. If that sounds slow, it is slower than explaining a made-up header to every downstream client.

Limitations are sharp. The gate is syntactic. It cannot see a prompt that smuggles a contract change inside a “small refactor.” It cannot prove a free server is empty beyond the working directory it was given. It does not replace security review, license review, or an audit log. Teams that need retention, residency, or a named model snapshot should not use this lane for the work that must satisfy those rules. Teams that want one model for every task should not use this guide as a way to keep using the complimentary endpoint everywhere with extra if-statements.

Who should ignore this approach is equally concrete. Do not adopt the gate if the real need is a production agent with an SLA. Do not adopt it as a privacy control; isolation of a lab directory is not a confidentiality boundary. Do not adopt it if the repository has no contracts/ tree and every file is already treated as scratch. In that case the missing artifact is the contract, not a router around a free model.

The core conclusion does not change after the code. Free inference may sketch. It may not own the shape other systems will trust. Keep authority files under humans, schemas, and pinned snapshots. Leave the complimentary lane for work that can be deleted without a meeting.

Top comments (0)