Skip to main content

The Model Is a Component: Designing for the Swap

· 9 min read
CatalEx Engineering
The team building CatalEx
CatalEx Engineering · Published July 13, 2026 · 09:00 UTC

Every AI platform says it is model-agnostic. Almost none of them are. Not because the teams are lying — because agnosticism isn't a property you declare in a README. It's a seam in the code, and a seam that nothing routinely crosses is a seam that has already rusted shut. You just don't find out until the day you try to move it.

We ended an earlier post by saying the model is a component you swap. That's the easy half. The hard half is the follow-up question every engineer asks immediately: fine, how? What does the code have to look like for that sentence to be true a year from now, under a team that has shipped a hundred features since anyone last thought about it?

Here is the test we've landed on, and it's blunter than most architecture advice. A swappable dependency is one where something in your normal workflow already swaps it. Not a test that could be written. Not an adapter that could be added. Something that crosses the seam today, on a Tuesday, without anyone deciding to. If the only time you'd exercise the seam is the day you need it, you don't have a seam. You have a plan. Plans don't compile.

This post walks four seams in our system, what keeps each one alive, and — for one of them — an admission about where the architecture is ahead of the deployment.

Seam 1: the model behind an alias

All inference goes through a model proxy. Nothing in our application code requests a vendor model by name. Callers request an alias: a default alias, and a cheaper-tier alias for work that doesn't need the good stuff. The alias resolves to a real model in configuration, not in code.

That much is unremarkable — it's the indirection everyone writes. The part that makes it real is the routing table. Local development routes the same alias to a locally-run model while deployed environments route it somewhere else entirely. Every engineer, every day, on every branch, runs the system with the alias pointed at a genuinely different provider than production uses. Nobody has to remember to test the indirection. You cannot get your work done without exercising it.

That's the whole difference between a seam and a wish. The indirection isn't kept honest by discipline or by a lint rule. It's kept honest by the fact that the local loop would break the instant someone hard-coded a model, and it would break for the person who did it, in the first minute.

The consequence shows up in the schema. There is no model column on an agent. An agent is a definition — instructions, tools, KPIs — and deliberately not a model binding. Model choice is a service-level concern, resolved at dispatch, not a property an agent carries around. The reasoning is mechanical rather than philosophical: if every agent row named a model, "swap the model" would be a data migration across customer rows instead of a config change. The seam would technically exist and be practically unusable, which is the most expensive kind of abstraction to own.

The honest part. The architecture is model-agnostic. The deployment today is effectively single-model — both aliases currently resolve to the same model. The seam is maintained and it is exercised, but we are not running a heterogeneous fleet behind it, and saying otherwise would be exactly the unearned agnosticism this post is about. What we've bought is optionality, and the price of optionality is that you keep paying for it before you use it.

Seam 2: the sandbox behind a Protocol

Agents run code in a box. The orchestrator has never met one. It depends only on a Protocol — an interface — and three adapters satisfy it: a fake provider used locally and in CI, a microVM provider that is the real one, and a confidential-computing provider aimed at stricter deployments.

That third one deserves a flag before it does any work in this argument: it is aspirational. The Protocol is written for it. The adapter is not built. We are not shipping confidential computing today, and it appears here only because of what it did to the interface.

Which was this: the Protocol was designed against the harder provider so all three satisfy one contract. Write your interface against the most constrained implementation — including one that doesn't exist yet — and the easy cases fall out of it for free. Write it against the easy case and the hard one will not fit later, no matter how much you want it to. The unbuilt adapter earned its keep by being a constraint during design, which is a legitimate use for a thing that doesn't exist.

The mechanism that keeps it from degrading is capability declaration. Adapters advertise what they offer; the orchestrator branches on the capability, never on the provider:

@dataclass(frozen=True)
class Capabilities:
sudo: bool # can the agent take root inside the box?
scratch_disk: bool # writable disk beyond the process?
resumable: bool # can a box be suspended and woken later?


class SandboxProvider(Protocol):
"""A place an agent can run. Nothing above this line knows which one."""

def capabilities(self) -> Capabilities: ...

async def create(self, spec: BoxSpec) -> BoxHandle: ...
async def exec(self, box: BoxHandle, cmd: Command) -> ExecResult: ...
async def destroy(self, box: BoxHandle) -> None: ...

if provider == "x" is how a seam dies. It always starts as one line, in one place, for one good reason, and then it's forty lines in nine files and the abstraction is now decoration around a switch statement. Branching on caps.resumable instead means a new adapter answers a question rather than joining a conditional.

The Protocol also documents what must not cross it. For every adapter, the forbidden leaks are written down: the box's only external egress is the tool gate and the model proxy; tool results follow the preview-plus-reference contract; delivery and box lifecycle stay behind the interface. Naming what must not pass through an interface is rarer than naming what must, and it's more useful — leaks are how abstractions rot, and they rot from the inside, by addition, one convenient exception at a time.

And the fake provider deserves its own beat. The entire system runs in CI with no external sandbox at all. That's not a testing convenience; it's the strongest available proof the seam holds. An abstraction you can't run without is not an abstraction — it's a wrapper. Every green CI run is a swap of the most infrastructure-heavy dependency we have, performed by nobody, hundreds of times a week.

Seam 3: integrations behind a uniform shape

Two sourcing paths exist for third-party tools: sometimes we wrap a vendor's API ourselves, sometimes we proxy a remote server someone else runs. These are peer-equal behind the same registry shape, and the line that matters is that agents can't tell them apart at call time. The agent-facing contract doesn't leak the sourcing decision — which means the sourcing decision can be revisited without touching a single agent definition.

The registry is convention, not registration. Every vendor is a directory whose name is its slug, with a fixed file layout inside; discovery walks one level down and finds it. Adding a vendor is "write a new package + decorate", not "edit a shared elif chain". A central registry file is two bad things at once: a merge conflict and a coupling point. A naming convention is neither, and it can't drift from itself.

Metering follows the same instinct. A tool is metered if its canonical name isn't in the local-tools set — vendor-agnostic by construction, because there is no per-vendor list anywhere for anyone to forget to update. A canonicalizer collapses the two surface spellings of the same tool into one identity, so the accounting doesn't care which call style the model happened to use. And the tool catalog itself is data — in the database, read at runtime — not code. The general shape: when correctness depends on a list staying current, delete the list.

Seam 4: the seam has to move while traffic flows

The last one is smaller and travels further. A dispatch-shape change shipped consumer-first: the client was taught to speak both the old and the new response shapes before any environment switched. Once the consumer is bilingual, the two deploys can land in either order, and nothing is holding its breath.

An interface change is two deploys, and the consumer goes first. That's the entire technique. It's worth saying out loud because the seam metaphor is usually static — a line in the architecture — when in practice a seam is also a thing you must be able to move without stopping the world. A boundary you can only change during a maintenance window is a boundary you'll stop changing.

The four seams, and how each one would rust

SeamWhat it abstractsWhat crosses it routinelyHow it would rust
Model aliasWhich model actually serves a requestLocal dev routes the alias to a local runner; deployed routes elsewhereA model column on the agent row
Sandbox ProtocolWhere agent code executesCI runs the whole system on the fake providerif provider == "microvm"
Integration registryWhether we wrapped it or proxy itEvery vendor added the same way — a directory, a decoratorA central elif chain; a per-vendor metering list
Dispatch shapeThe runtime↔client response contractConsumers taught both shapes before the switchChanges that require both sides to land at once

Read the third column first. That's the only one that's load-bearing. The first two columns describe every model-agnostic architecture diagram ever drawn, including the ones belonging to teams who'd need three weeks to change a model.

What this actually costs

Model-agnosticism is not a property you announce. It's a maintenance cost you agree to pay, forever, in exchange for never being trapped. You pay it in a Protocol that's harder to write than the concrete class would have been. You pay it in a fake provider that must be kept faithful. You pay it in a capability flag where a provider check would have taken ten seconds. You pay it on days when nothing is being swapped, which is most days, which is the point.

The way to know whether you're actually paying it is simple, and you can run it on your own system this afternoon: name the last time something crossed the seam. Not a plan to. Not a test that would. An actual crossing, with a date.

If you can't name one, the seam is decorative. The model isn't a component you swap — it's a component you married.


Written by CatalEx Engineering. We build the AI operating layer for AI-native companies — one platform to build, deploy, and run AI agents in production. More at catalex.co.