An assistant product has an odd relationship with its own core. The thing users experience — the voice, the judgment, the capability — is produced by a language model, and the model is the one component we neither built nor control. Models are versioned, priced, rate-limited, deprecated and superseded on someone else's schedule. Hardwire one and the product's economics and behaviour are hostage to a vendor's roadmap. So the position we keep coming back to is that the model is the swappable part: the product is the loop around it — the tools, the memory, the guardrails, the billing — and which model answers a given message should be a routing decision, changeable without the user noticing anything but the answer.
Today that position stopped being theoretical, because today we exercised it hard enough to find its limits. This entry is written at the end of a single Saturday in which the routing layer changed three times, a cheap tier was handed across a provider boundary and taken back, and one piece of state was moved because the day proved it was pinned to the wrong thing. The churn was not indecision. It was the cheapest possible way to learn which parts of "swappable" are real.
The morning: making a swap observable before making it possible
The unglamorous work came first, and it is the part that made the rest of the day survivable. Usage and the model that produced it are now recorded per turn, not per conversation, and the pricing table became a dated store — each model's rates keyed by model and time window. The cost of a turn stopped being a figure reconstructed afterwards from a monthly total: which model ran, and what it cost, is now a fact recorded alongside the turn itself.
None of that routes anything. All of it is the precondition for routing: a system that can change models mid-conversation but only records cost per conversation cannot tell you what any routing decision did. Before the first swap happened, every turn's model became a fact attached to that turn — so by the time the afternoon's experiments ran, each one left a priced audit trail rather than an impression.
The afternoon: three tiers, and a guest in the cheap seat
Then the routing itself. A classifier runs before each turn and sorts it into one of three tiers — simple, default, complex. It is deliberately dumb: keyword tests (a message mentioning email or calendar implies tool work, so route it up), a word-count floor and ceiling, an image forces the top tier, and prior tool use in the thread sets a floor. Complex turns route to a mid-class model; the default is the fast one; and the simple tier — greetings, one-liners, trivial yes/no — was pointed somewhere more interesting: a small, cheap model from a different provider entirely.
The logic was sound. A greeting does not need frontier capability; another vendor's small model answers it for a fraction of the price; the abstraction to translate between provider APIs — message shapes, tool calls, streaming — existed and worked. Over the afternoon the cheap seat's occupant changed twice more, one small model swapped for a newer sibling, chasing the best cost-per-greeting. Three tiers, a provider boundary running through them, one assistant.
The evening: why the guest was asked to leave
By ten that night the cross-provider tier was gone — the simple tier now routes to a fast model, and the threshold for escalating to the mid tier was raised while we were in there. The commit that removed it records the reasons, and they are worth more than the experiment cost:
The voice changed mid-conversation. Models from different providers have noticeably different styles, and a thread that crossed the boundary read like two people sharing a keyboard. The user sees one assistant. The routing is invisible — which means any stylistic difference between tiers is not perceived as a routing artifact; it is perceived as the assistant being off today. A conversation is a single performance, and the audience attributes everything in it to one performer.
Tool behaviour drifted. The guest model was less reliable at driving our tool surface — the same definitions, translated faithfully, called more erratically. And it was weaker at implicit multi-step context: the follow-up message that makes no sense without the three turns before it, which is most messages in a real thread.
The general lesson is the one this entry exists to record: models are interchangeable at the API level and not at the conversation level. The translation layer was never the problem — messages, tools and streams all converted cleanly. What does not translate is everything the API contract is silent about: register, tone, tool-calling temperament, how much of the unstated thread a model carries forward. A routing layer that treats models as fungible compute will keep passing every functional test while the product develops a personality disorder. Swappable is real — we swapped four times today and nothing broke — but the unit of swap is the whole conversation's tenant, not this turn's supplier.
What survived the day
Every specific decision made today is provisional, and the things that are not provisional are the ones the churn proved out. The seam — one point in the loop where a turn's tier is decided, with the whole thread in scope, before any provider is constructed — survived four routing changes without moving. The observability — per-turn model facts, dated rate rows, a priced trail behind every routing decision — is what let four changes happen in one day with confidence instead of anxiety. And the user-facing posture held throughout: what the interface promises is capability, not components. The product sells depth — a tier is a commitment about how much thinking a question gets, not about which engine delivers it. That posture is not cosmetic. It is the thing that makes the model swappable in practice: a product whose interface promises a named engine has signed away the right to improve it, because every improvement then arrives as a renegotiation about something the user was never really buying.
The discipline, as a checklist, for anyone building on models they do not control: meter per turn against the model that actually ran; key your rates by model and date, because prices move under you; keep the routing decision at one seam with the whole conversation in scope; make the interface a promise about capability rather than about components; and test any cross-model route at the conversation level — voice, tool temperament, implicit context — before trusting the API-level tests that will all pass.
Postscript, August 2026. The day's verdicts aged in both directions, instructively. The keyword classifier was throttled within weeks — the mid-tier branch disabled pending a cost/quality evaluation, the classifier itself demoted to log-only — and in July the approach was formally judged unreliable and superseded by an accepted design that closes the loop on this entry's own logic: a cheap model deciding which expensive one answers. The router reads a digest of the thread — recent turns, which tiers they ran at, whether tools fired — never the transcript, escalates rather than ping-pongs, and picks capability levels, never models, so billing, the promise a tier makes and the interface all hold unchanged. The user-facing half shipped as exactly the tiers this day sketched: three named depth levels, priced as credit multipliers, mapped to models server-side. One economic fact, discovered later, retroactively justified the evening's caution about per-turn swapping: prompt caches are per-model, so a single mid-thread model switch reprocesses the entire conversation at full input price — on a long thread, roughly ten times that turn's cached cost before the answer even starts. The strongest argument against routing churn turned out to be in the billing data. And the cross-provider abstraction the afternoon exercised still exists, translation layer and all — deliberately unwired, kept as the escape hatch the swappable-part position requires — today's routing is a choice we keep making, not a lock-in we inherited.