Ask a CTO when they will deal with model-provider lock-in and the usual answer is later. Later means when pricing changes, when a model gets deprecated, when a better option appears and switching finally earns its cost. Until then lock-in sits with contract terms and migration budgets, a problem procurement will negotiate once it becomes real.
That framing puts the cost in the wrong place. Switching cost is set long before anyone decides to switch. It is set the first time application code calls a provider's API with nothing standing between them. Every prompt tuned to that model's quirks, every function-calling schema written against that provider's format, every retry policy and token-counting assumption baked in along the way accumulates for months. By the time a team is ready to switch, the coupling has become load-bearing.
None of this is specific to AI. Every major platform transition runs the same three phases. Teams build fast against whichever platform is dominant, because the value is immediate and the architectural risk is hard to see. The economic model underneath then changes: pricing shifts, a feature gets deprecated, a company gets acquired. Finally everyone pays the migration tax on coupling that was never meant to be permanent.

I have watched this happen twice, both times in infrastructure built to move enormous volumes of data through a system designed around one dominant way of doing it. The pattern held both times. Teams built directly against whichever platform was fastest and cheapest, because there was no reason not to. Years later the economics underneath shifted, and the coupling that had accumulated was not something a budget could clear. It had to be rebuilt.
AI infrastructure is on the same arc, earlier in it. Teams are building fast against whichever model provider is fastest and cheapest today. That is phase one, and the architectural risk is as hard to see as it was the last two times. Providers have already changed pricing and deprecated older models. Nothing yet has forced a wholesale migration the way an acquisition or a platform shutdown would. Phase two is still building. It has not arrived. The bill has not come due, and timing is the only open question.

None of this makes providers the villain. A provider optimizing for retention is ordinary, and plenty of infrastructure vendors build in switching costs the same way. Lock-in is the default outcome of skipping a step that every other dependency in your stack already gets: a boundary between your application and the thing underneath it that is allowed to change.
The boundary has a hard limit worth stating up front. It isolates transport and schema. It does not isolate model behavior. Putting an interface in front of an API call means your code does not know which provider it is talking to. It does not mean a prompt tuned against one model's response patterns behaves the same way against another. Those are separate problems, and the interface only solves one.
That limit is the argument for building the boundary, not against it. When transport and schema live behind an interface, the only thing exposed on a provider switch is model behavior. The work that remains is prompt adjustment and evaluation, and it is now scoped, visible, and testable in one place, not rediscovered at every call site. Behavior will never be portable across models. What the boundary buys is that behavior becomes the only variable you have to reason about during a switch.
That distinction points to what the interface is for. It defines what the application needs from a model: a completion, a structured tool call, a token count, a stream of partial results. Each adapter then translates that defined need onto a specific provider's contract, its function-calling format, its streaming behavior, its token accounting. This is the same discipline as a repository layer in front of a database. A team defines what the application needs from storage and lets the implementation underneath change, instead of writing raw SQL into application code and calling it a data strategy.

There is a well-known failure mode here worth heading off. Abstraction layers often flatten to the intersection of what every provider supports, which locks you out of a provider's newest capability the moment it ships. That happens when the interface is defined by what providers have in common. Define it by what your application needs, and the failure mode does not appear. If your application needs a capability only one provider offers, the interface exposes it and the other adapters declare that they do not support it. Adapters are allowed to be uneven. The contract is the set of needs your application actually has, not the greatest common divisor of the market.
A fair question at this point is why build the boundary at all when a gateway like LiteLLM or a routing service like OpenRouter already sits in front of multiple providers. For many teams that is the right first move, and it solves transport quickly. The catch is that a shared gateway defines the contract for you, around what it chose to support, which is the same greatest-common-divisor problem one layer out. A gateway can serve as one adapter behind your interface. It should not become the interface itself, because then an outside vendor decides what your application is allowed to need.
The interface is the fast part. Defining what your application needs from a provider is a bounded, well-understood piece of design work, and most teams can write it in a day. The first adapter is where real cost shows up, and that cost is not fixed. It scales with how much provider-specific behavior your application already leans on. A provider you use lightly, mostly plain completions, normalizes quickly. A provider whose tool-calling quirks or multi-turn state handling your application already depends on takes longer, because there is more behavior to translate and less to simply pass through.
Once the boundary exists, several things become possible. Changing which provider carries a workload becomes a configuration change, with the behavior and evaluation work scoped to prompts and measured in one place. Workloads can route to different models by task type or cost, cheaper models for simple calls and stronger ones for complex reasoning, without touching the calling code. New model versions can be tested in production behind the same interface, against the evaluation harness the boundary gives you a natural home for. New providers can be added without rewriting the logic that depends on them.
None of that is available without the interface. Routing by cost, testing versions safely, adding a provider without a rewrite, each depends on the boundary existing first. Skip it and each becomes its own one-off project, built and rebuilt every time, instead of a capability the architecture already has.
Teams that skipped the boundary did not make a mistake. They made the trade every team makes on every dependency: ship fast, abstract later. Under real deadlines that is usually the right call in the moment. The thing that trips teams up is timing. Later arrives on its own schedule.
The same lesson showed up both times before. Later arrived faster than anyone planned and cost more than anyone had budgeted. Nobody puts a number on architectural risk until the bill for ignoring it comes due, and newer tooling does not exempt AI infrastructure from that pattern.
Here is what that looks like in practice. The team that built the boundary eighteen months ago gets a pricing change or a deprecation notice, updates one adapter, and moves on with the week. The team that did not gets the same notice and opens a sprint. It happens again with the next shift, and the sprint does not get shorter. It gets longer, because everything built in the meantime sits on the same unguarded dependency.

The instinct after seeing that contrast is to over-correct and build adapters for every provider you might ever use. Do not. Build one interface and exactly one adapter, for the provider you are using today. The interface is what forces the useful thinking, defining precisely what your application needs from a model. Building against a provider you actually use is what gets that definition right. A hypothetical future provider teaches you nothing. The second adapter, when you need it, translates a contract you have seen instead of one you are guessing at.
Timing is the whole game. Move too early and you are designing against requirements you do not have yet. Wait too long and the design work becomes a rewrite. The window between those two is narrower than it looks, and it closes the moment you stop noticing it is open.
This is an infrastructure decision, and it is worth being precise about what it buys. The interface is fast to build. The first adapter costs exactly what the provider-specific behavior demands. The boundary isolates transport and schema and leaves model behavior as scoped, measurable work instead of a surprise. Skip it and that cost does not disappear. It arrives later as a rewrite, and it arrives every time.