DEV Community

kongkong
kongkong

Posted on

The Model Is a Dependency. The Contract Is the Product.

The Model Is a Dependency. The Contract Is the Product.

Two weeks ago a friend's team swapped their LLM provider over a weekend because a new model scored higher on their internal eval. Then they spent three days fixing a schema mismatch that had nothing to do with reasoning quality. The old provider returned choices[0].message.content, while the new one wrapped the answer in a tool-call object. Every prompt template in the codebase started returning empty strings. Nobody had written a test against the response contract, because nobody believed the contract was the thing worth testing.

Every week another model lands with a benchmark crown and a quiet deprecation notice. Teams that treat the model as the product are permanently rebuilding the same integration. The current AI news cycle is mostly churn dressed up as progress, and everyone is building reasoning ledgers and agent orchestration layers right now. Most of them are skipping the layer that actually breaks in production, which is the contract between your app and the model. So here is the opinion I keep defending: the model is a dependency, and the contract is the product you actually ship. Write the contract before you pick the model.

I can demonstrate this with the workflow I now use for every AI feature I touch. The cheapest place to run it is MonkeyCode's open-source project with its free model access and free server option. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The whole exercise costs nothing except the hour it takes to write the contract first. It will save you the three days my friend's team just lost.

Here is the gateway I deploy to the free server, and notice what it does not contain. It accepts one request shape, calls a provider, and returns one response shape. Nowhere in the code does a hardcoded model name appear, because the provider URL and the model name are configuration values. The model is a dependency, so it lives in an environment variable rather than in your product logic.

# gateway.py — the contract is the product, the model is a dependency
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
import httpx, os

app = FastAPI()

class ChatRequest(BaseModel):
    messages: list[dict]
    max_tokens: int = Field(default=512, le=2048)

class ChatResponse(BaseModel):
    content: str
    provider: str
    degraded: bool = False

PROVIDERS = [
    {"name": "primary", "url": os.getenv("PRIMARY_URL"), "key": os.getenv("PRIMARY_KEY")},
    {"name": "fallback", "url": os.getenv("FALLBACK_URL"), "key": os.getenv("FALLBACK_KEY")},
]

@app.post("/v1/chat", response_model=ChatResponse)
async def chat(req: ChatRequest):
    for provider in PROVIDERS:
        try:
            async with httpx.AsyncClient(timeout=10) as client:
                r = await client.post(
                    provider["url"] + "/chat/completions",
                    headers={"Authorization": f"Bearer {provider['key']}"},
                    json={
                        "model": os.getenv(f"{provider['name'].upper()}_MODEL"),
                        "messages": req.messages,
                        "max_tokens": req.max_tokens,
                    },
                )
                r.raise_for_status()
                return ChatResponse(
                    content=r.json()["choices"][0]["message"]["content"],
                    provider=provider["name"],
                    degraded=(provider["name"] == "fallback"),
                )
        except Exception as exc:
            print(f"{provider['name']} failed: {exc}")
    raise HTTPException(status_code=503, detail="all providers failed")
Enter fullscreen mode Exit fullscreen mode

The gateway is deliberately boring, because the interesting part is the contract test suite that runs against it. I run this suite in CI on every pull request, and I run it locally against the free server. The free token allowance is the budget, and the contract is the thing under test. If the contract breaks, the suite breaks, and the model behind it is irrelevant.

# test_contract.py — if the contract is the product, test it like one
import time, httpx

GATEWAY = "http://localhost:8000/v1/chat"

def call(messages, max_tokens=512):
    started = time.time()
    response = httpx.post(GATEWAY, json={"messages": messages, "max_tokens": max_tokens}, timeout=30)
    return response, time.time() - started

def test_response_shape_is_stable():
    response, _ = call([{"role": "user", "content": "Reply with the single word: ok"}])
    assert response.status_code == 200
    body = response.json()
    assert set(body.keys()) == {"content", "provider", "degraded"}
    assert isinstance(body["content"], str) and body["content"]

def test_round_trip_stays_inside_budget():
    response, elapsed = call([{"role": "user", "content": "Say hello"}], max_tokens=16)
    assert elapsed < 15, f"round trip blew the budget: {elapsed:.1f}s"

def test_degraded_mode_still_returns_the_contract():
    # point PRIMARY_URL at a dead port and the gateway must fail over
    response, _ = call([{"role": "user", "content": "Still here?"}])
    assert response.status_code == 200
    assert response.json()["provider"] == "fallback"
Enter fullscreen mode Exit fullscreen mode

The first test catches the exact failure that cost my friend's team three days. A provider swap is only dangerous when the response shape changes underneath you, and this test makes that change visible in seconds. The second test catches the latency creep that benchmarks never show, because benchmarks measure the model while your users measure the round trip. The third test is the one most teams skip, and it is the reason the free tier matters. A free server is the cheapest place to point your primary provider at a dead endpoint and watch whether your contract survives. The reusable checklist is short: freeze the request shape, freeze the response shape, budget the round trip, and test the failover.

Deploying this is three commands, which is the other reason I keep pushing the free server option for staging work. You install the dependencies, start uvicorn, and hit the same URL your users will hit. The whole stack fits on a free server because the gateway has no state and no database.

pip install fastapi "uvicorn[standard]" httpx pydantic
uvicorn gateway:app --host 0.0.0.0 --port 8000
curl -s http://localhost:8000/v1/chat \
  -H 'Content-Type: application/json' \
  -d '{"messages":[{"role":"user","content":"Reply with ok"}],"max_tokens":16}'
Enter fullscreen mode Exit fullscreen mode

Notice what none of these tests do, because that is the actual argument. None of them assert a model name, none of them compare benchmark scores, and none of them care which provider answered. Your users cannot tell which model answered them, your tests should not care, and your product is the stable contract in between. How many of your current tests would survive a provider swap with zero changes?

Now the honest part, because every architectural opinion needs its boundary conditions. If your feature's value depends on a specific model's reasoning capability, a contract-first gateway will not save you. In that case the model is the product, and the contract is just plumbing. If you operate in a regulated industry, you need more than a normalized response shape. You need an audit trail of which model processed which request, and you should keep the provider name in every log line. And if your fallback heuristic is garbage, degraded mode is worse than a clear 503. Test the fallback with the same rigor as the primary provider. This gateway is also a minimal example, not a production service. It has no auth, no rate limiting, and no persistence, so add those before you point real traffic at it.

So here is my question, and I mean it as a design exercise rather than a rhetorical one. If your provider disappeared tomorrow, which layer of your stack would break first? Would the failure be a clean contract violation or a silent empty string? If the answer is your prompt templates or your response parsing, then the model was never your product, and the contract was. MonkeyCode's free tier gives you a server and a 10M-token allowance to run this experiment today. The only real cost is the hour it takes to write the contract before you pick the model.

Top comments (0)