DEV Community

Dakota Huang
Dakota Huang

Posted on

Five Gates to a Public Model Endpoint: A Deployment Tutorial

You can deploy a free-model proxy to a free server in five stages. Each stage ends with a gate. If a gate fails, you stop and fix. This loop turns "it works on my machine" into "it works from anywhere."

This walkthrough uses two free resources. A free model endpoint. A free server option. Both come from MonkeyCode.

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

What you need

  • A free model endpoint with a key. MonkeyCode provides one.
  • A free server option with SSH or console access. MonkeyCode provides one.
  • Python 3.11 or newer on your laptop.
  • curl and a terminal.

Why stage gates

Free endpoints fail quietly. Free servers cold-start. A proxy that works locally can break the moment it moves. Stage gates catch each failure at the cheapest point. You verify before you deploy. Not after.

Each gate is a command you can run. Each gate has a pass condition. If the condition fails, you stop. This is the opposite of deploy-and-pray.

Stage 1: Pin the contract

Write the contract before the code. Two shapes matter. The request you accept. The response you return. Keep both small.

A small contract is easy to test. It is also easy to change. A fat contract drags every consumer into every change.

# contract.py
from pydantic import BaseModel


class PromptIn(BaseModel):
    prompt: str
    max_tokens: int = 256


class CompletionOut(BaseModel):
    text: str
    model: str
    latency_ms: int
Enter fullscreen mode Exit fullscreen mode

Gate: import the module and build both models.

python -c "from contract import PromptIn, CompletionOut; print(PromptIn(prompt='x')); print(CompletionOut(text='y', model='m', latency_ms=1))"
Enter fullscreen mode Exit fullscreen mode

If that prints two objects, the contract is valid.

Stage 2: Build the proxy

The proxy has one job. Take a prompt. Forward it upstream. Normalize the response. Return your contract shape.

# proxy.py
import os
import time

import httpx
from fastapi import FastAPI
from contract import CompletionOut, PromptIn

app = FastAPI()

UPSTREAM_URL = os.environ["UPSTREAM_URL"]
UPSTREAM_KEY = os.environ["UPSTREAM_KEY"]
TIMEOUT = float(os.environ.get("TIMEOUT_SECONDS", "30"))


@app.get("/health")
def health():
    return {"status": "ok"}


@app.post("/complete", response_model=CompletionOut)
def complete(body: PromptIn):
    payload = {"prompt": body.prompt, "max_tokens": body.max_tokens}
    headers = {"Authorization": f"Bearer {UPSTREAM_KEY}"}
    started = time.monotonic()
    with httpx.Client(timeout=TIMEOUT) as client:
        resp = client.post(UPSTREAM_URL, json=payload, headers=headers)
        resp.raise_for_status()
        data = resp.json()
    latency_ms = int((time.monotonic() - started) * 1000)
    return CompletionOut(
        text=data["text"], model=data["model"], latency_ms=latency_ms
    )
Enter fullscreen mode Exit fullscreen mode

Save this as requirements.txt.

fastapi
uvicorn
httpx
Enter fullscreen mode Exit fullscreen mode

Upstream response shapes vary by provider. Replace data["text"] and data["model"] with your endpoint's real fields. Normalize in one place. Never scatter field names across the codebase.

Stage 3: Run the local gate

Start the proxy. Run three checks. Health, happy path, bad input.

export UPSTREAM_URL="..."   # your free endpoint URL
export UPSTREAM_KEY="..."    # your key
uvicorn proxy:app --port 8000
Enter fullscreen mode Exit fullscreen mode

In a second terminal:

curl -s http://localhost:8000/health
curl -s -X POST http://localhost:8000/complete -H "Content-Type: application/json" -d '{"prompt": "def add(a, b):", "max_tokens": 64}'
curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:8000/complete -H "Content-Type: application/json" -d '{"prompt": ""}'
Enter fullscreen mode Exit fullscreen mode

Expect 200, a JSON completion, and 422. The happy path proves the upstream works. The bad input proves validation works. If any check fails, fix it locally. Deploying a broken proxy multiplies the debugging cost.

Stage 4: Deploy to the free server

Copy the project to the host. Install dependencies. Start the service. Expose the port.

Exact commands depend on your host. The pattern is what matters.

# from your laptop
rsync -az --exclude '.git' --exclude '__pycache__' ./ user@host:/srv/model-proxy/

# on the host
cd /srv/model-proxy
pip install -r requirements.txt
export UPSTREAM_URL="..." UPSTREAM_KEY="..."
nohup uvicorn proxy:app --host 0.0.0.0 --port 8000 > proxy.log 2>&1 &
Enter fullscreen mode Exit fullscreen mode

Replace user@host with your server's address. Set environment variables from the host's secret store. Never commit keys to the repo. A leaked key is a revoked key.

Gate: confirm the process is listening.

curl -s http://localhost:8000/health   # run on the host
Enter fullscreen mode Exit fullscreen mode

Stage 5: Smoke test from the public internet

Local checks prove nothing about the public path. DNS, firewalls, and cold starts sit between your laptop and the server. Test the public URL directly.

The script below runs three gates. Health, completion shape, input validation. It exits non-zero if any gate fails. That makes it usable in CI or a cron job.

# smoke_test.py
import sys
import time

import httpx

BASE_URL = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:8000"
TIMEOUT = 30.0
failures = 0


def check(name, condition, detail=""):
    global failures
    mark = "PASS" if condition else "FAIL"
    print(f"[{mark}] {name} {detail}")
    if not condition:
        failures += 1


started = time.monotonic()
r = httpx.get(f"{BASE_URL}/health", timeout=TIMEOUT)
check("health returns 200", r.status_code == 200, f"(got {r.status_code})")
check("health answers in < 5s", time.monotonic() - started < 5.0)

r = httpx.post(
    f"{BASE_URL}/complete",
    json={"prompt": "def add(a, b):", "max_tokens": 64},
    timeout=TIMEOUT,
)
check("complete returns 200", r.status_code == 200, f"(got {r.status_code})")
if r.status_code == 200:
    data = r.json()
    check("response has text", isinstance(data.get("text"), str) and data["text"])
    check("response has latency_ms", isinstance(data.get("latency_ms"), int))

r = httpx.post(f"{BASE_URL}/complete", json={"prompt": ""}, timeout=TIMEOUT)
check("empty prompt rejected", r.status_code == 422, f"(got {r.status_code})")

if failures:
    print(f"{failures} gate(s) failed.")
    sys.exit(1)
print("All gates passed.")
Enter fullscreen mode Exit fullscreen mode

Run it against the public URL:

python smoke_test.py https://your-proxy.example.com
Enter fullscreen mode Exit fullscreen mode

Expect five PASS lines. The first cold call may be slow. That is normal on free servers. The health gate allows a five-second budget.

Limitations

This loop targets prototypes and internal tools. Free servers are best-effort infrastructure. They have no SLA. They share IPs. They can pause without notice. Free model endpoints rate-limit and fail quietly. The proxy adds a timeout and a shape check. It does not add retries, auth, or rate limiting.

Add those as separate layers. Do not bolt them onto this proxy. Each layer needs its own tests. Each layer needs its own failure mode.

Who should not use this

Skip this approach for production traffic. Free servers cannot promise uptime. Skip it for sensitive data. Free endpoints should not see private payloads. Skip it for sub-second latency. Cold starts will miss your budget.

The sweet spot is a demo, a prototype, or an internal tool with tolerant users. If that is your situation, the loop will save you time.

The takeaway

Five stages. Five gates. Each gate is a command or a script. The total cost is one afternoon. The payoff is a public endpoint that fails loudly.

Copy the files, adapt the contract, and run the gates. The whole loop is small enough to own.

Top comments (0)