The model you build on today is not the model you will run in a year. New versions ship, old ones get deprecated on a fixed timeline, prices move, and every few months a different provider leaps ahead on the exact task you care about. None of that is under your control. What is under your control is whether a model change is a config edit or a migration project.
A model-agnostic architecture is the difference between those two outcomes. It will not make swapping free, and anyone who tells you it does has not done it. But done well it turns "we are locked to one vendor's price list and roadmap" into "we can move when moving is worth it," which is a form of leverage worth designing for.
Why this is not premature optimization
It is tempting to wire straight to one provider's SDK and worry about portability later. The problem is that "later" arrives on someone else's schedule. A model you depend on gets deprecated with a few months of notice. A price change turns a healthy margin into a thin one overnight. A competitor ships a model that is twice as good on your hardest task. If your system is threaded through one vendor's specifics, each of those events is a scramble. The point of the boundary is not elegance. It is not being held hostage by a roadmap you do not own.
Put a boundary between your system and the model
The pattern is old and boring, which is why it works: ports and adapters. Define one internal interface for "talk to a model," and let your application code depend only on that interface. Behind it, an adapter per provider translates to and from that vendor's actual API.
Your interface should express what your system needs in neutral terms: a request carries the messages, the tools available, and the generation parameters; a response carries the text or the structured result and the token usage. Application code calls generate(request) and never imports a vendor SDK directly. Swapping a provider then means writing or selecting an adapter, not touching the code that reasons about your product.
What to abstract
Four things belong behind the boundary, because they are exactly what differs between providers:
- Endpoint, auth, and model identity. The base URL, the credential, and the model name are configuration, not code. They should be swappable without a deploy where possible.
- Request and response shape. Message roles, the tool-call format, and how results come back differ across providers. The adapter normalizes them to your internal shape.
- Token accounting. Each provider reports usage differently. Normalize it so your cost and budgeting logic does not care who served the request.
- Streaming. How tokens stream back varies. Hide that behind one streaming interface so consumers see the same thing regardless of provider.
Get these four right and the mechanical part of a swap becomes small.
What leaks through, honestly
Here is the part most portability write-ups skip. An abstraction boundary hides the API. It does not hide the behavior, and behavior is where the real coupling lives.
Prompts are the big one. A prompt tuned for one model is rarely optimal for another. The same instructions can produce tighter output on one model and rambling on another, so a swap almost always means a round of prompt re-tuning, not just a config change. Tool-calling reliability differs too, and a format that one model follows faithfully another will occasionally mangle. Context limits differ, which interacts with how you budget input and is its own design question covered in the context window is an architecture constraint. Structured-output support and how gracefully a model streams partial results vary as well, which matters if you rely on streaming structured output. And raw quality and latency differ per task in ways no interface can paper over.
So the boundary is necessary but not sufficient. It makes the plumbing swappable. It does not make the behavior identical, and pretending otherwise is how a "quick model swap" turns into a week of chasing subtle regressions.
Keep prompts and model choice as data, not code
Because prompts and model selection are the parts you will change most, treat them as configuration rather than hard-coded constants. Externalize your prompts and your per-task model choices so that trying a new model, or pointing a task at a cheaper one, is a data change you can review and roll back, not a code path you have to rewrite. This is also what makes per-task routing possible, sending routine steps to a small model and hard ones to a premium model is just model selection expressed as config, and it depends on the same boundary.
Eval before you swap, every time
The load-bearing discipline is this: never swap a model on reputation or a benchmark someone else ran. Run your own evaluation suite on the candidate against the tasks you actually care about, and compare quality, cost, and latency on your workload. A model that tops a public leaderboard can still be worse for your specific job, and the only way to know is to measure it on your job.
A swap without an eval is a silent quality change shipped to production and discovered later as a mysterious uptick in bad output. The abstraction boundary is what makes running that comparison cheap: point the adapter at the new model, run the suite, read the numbers. If you cannot do that in an afternoon, the boundary is not doing its job.
The honest limitation
Provider-agnostic design lowers the cost of a swap. It does not make swaps free, and it can be overdone. If you abstract so aggressively that your interface is the lowest common denominator of every provider, you give up the features that make the best models worth using: a provider-specific caching mechanism, a superior tool-calling mode, a longer context window. Sometimes coupling to one of those is the right call, made deliberately, because the feature is worth more than the portability.
The honest target is not zero coupling. It is coupling you chose on purpose, concentrated behind a boundary, so that when a model is deprecated or repriced or outclassed you can respond in days instead of quarters. Draw the line where a swap is genuinely plausible, and do not pay for flexibility you will never use.
This is a stance we build on at Loopsfinity: you bring your own model account, and the platform is designed so the model behind a task is a choice you make and can change, not a decision baked into the product. The specifics of how we keep that boundary are our own work, but the principle is not, and it is the one that keeps any agent system from being quietly owned by a single vendor's roadmap. It is one piece of the larger picture in AI agent architecture.