Yeda AI Tips · #035

Swap The Model, Not The App

A new model ships almost every week — cheaper, faster, better at your exact task. If adopting it means rewriting code across five call sites, you'll never actually do it; the switch will keep losing to "not this sprint." The fix is architectural: put every model call behind one interface, and make the model itself a config value instead of a hardcoded dependency.

The problem: vendor code baked into every call site

The natural way to add an AI feature is to import the provider's SDK directly wherever you need a completion — one call site in the chat handler, another in the summarizer, another in the classifier. Each one imports the vendor's client, references the vendor's model-name string, and shapes its request in that vendor's specific format.

This works fine until you need to change providers — the model you started with got more expensive, a competitor shipped something clearly better for your workload, or you want a cheaper model for low-stakes calls and a stronger one for the hard ones. Now the change isn't one line, it's a search across the codebase for every place that imports that SDK, followed by matching each call site's parameters and response-shape assumptions to the new provider's API.

The fix: one interface, model as config

Your application code never talks to a provider's SDK directly. It talks to a single internal function or interface — call it getCompletion(), generateText(), whatever fits your codebase — and that function is the only place a provider SDK is ever imported. The model itself becomes a parameter, or a config value, passed into that one function.

# Every call site in your app looks the same regardless of provider:
result = ai.generateText({
  model: config.MODEL,   # e.g. "provider-a/model-name"
  prompt: "...",
})

# Swapping providers is a one-line change:
config.MODEL = "provider-b/model-name"

Because every call site goes through the same interface, a new model — even from a different vendor entirely — only requires updating that one config value and, if needed, the interface's internal implementation. No call site changes. No deep refactor.

Why this is safe, not just convenient

The safety comes from where the boundary sits. Your application logic — prompts, business rules, retry/error handling — lives entirely on your side of the interface. The interface's only job is translating a generic "generate text/object given this model and this prompt" request into whatever shape the underlying provider expects, and translating the response back. Your app never learns the vendor's request format; it only knows the interface's format, which doesn't change when the vendor does.

This also makes testing easier: you can mock the interface without needing a live API key for every provider you might ever use.

How to set it up

  1. Pick or write one interface function that every model call routes through — a thin wrapper you write, or a unified-interface library that already normalizes multiple providers behind a common call shape.
  2. Move the model identifier into config — an environment variable or settings file, not a string literal scattered across call sites.
  3. Audit for direct SDK imports outside that one interface module. Every direct import is a future migration cost.
  4. Keep provider-specific quirks inside the interface, not leaked into app code — token-limit conventions, streaming formats, provider-specific error types.

Power tricks

Resources

Building an AI feature? Yeda AI designs, audits, and ships production LLM systems.

Talk to us · Read the blog