Skip to content
← All posts
7 min read Dawid Skłodowski

Routing and caching across multiple model providers

Committing your app to a single LLM provider is a risk: a price change, an outage, or a better model elsewhere leaves you stuck. Treating providers as interchangeable behind a routing layer buys resilience and flexibility — but it has real costs. A deep-dive on multi-provider routing, where it pays off, and where it's premature.

By 2025 there isn’t one LLM provider that matters — there are several genuinely capable ones (OpenAI, Anthropic, Google, and a field of strong open models you can host or rent), and they leapfrog each other on quality, price, and speed constantly. That raises an architectural question that didn’t exist when there was effectively one option: should your app commit to a single provider, or treat providers as interchangeable behind a routing layer that can send each request to whichever one is best right now? Going multi-provider buys real things — resilience, flexibility, cost optimization — but it also adds real complexity, and like every architectural choice in this series it’s easy to adopt prematurely. This is a deep-dive on multi-provider routing and caching: what it gives you, what it costs, and how to decide whether you need it.

Why you’d want more than one provider

The motivations are concrete, and each is a real risk that single-provider commitment leaves you exposed to:

  • Resilience against outages. If your app depends entirely on one provider and that provider has an outage, your feature is down — full stop. With more than one provider available, a routing layer can fail over to an alternative when the primary is unavailable, turning a hard outage into a degraded-but- working state. For a feature your users depend on, that’s the difference between an incident and a non-event.
  • Cost optimization. Providers price differently, and prices change. The ability to route a given request to the cheapest provider that does it well — or to switch providers when one changes its pricing — is real money in an LLM-heavy app, and it builds directly on the model-tiering idea, now across vendors rather than just within one.
  • Using the best model per task. Different models genuinely have different strengths. One might be best at coding, another at long-context summarization, another at cheap classification. Routing each task to the provider that’s best at it lets you assemble a better overall system than any single provider gives you.
  • Avoiding lock-in. Building everything around one provider’s specific API and quirks makes switching expensive later. An abstraction layer keeps you free to move as the market shifts — and in a market moving this fast, that optionality has value.

These are the same reasons you’d avoid hard-coupling to any single critical vendor. The LLM market’s speed of change makes them sharper than usual, because the “best” and “cheapest” provider genuinely does change month to month.

The routing layer

The mechanism is an abstraction layer between your app and the providers — your code talks to your interface, and the layer decides which provider actually handles each call. The shape:

  • A common interface. Your application calls something like complete(prompt, options) against your own abstraction, not OpenAI’s or Anthropic’s SDK directly. Internally, the layer translates that to whichever provider it routes to. This is the unlock for everything else — your app is no longer written against a specific provider.
  • Routing logic. The layer decides where each request goes — by task type (send coding tasks here, classification there), by cost (cheapest capable provider), by availability (skip a provider that’s erroring), or by explicit configuration. The routing policy is where your multi-provider strategy actually lives.
  • Normalization of differences. Providers differ in API shape, message formats, token accounting, and features. The layer’s job is to paper over those differences so your app sees one consistent interface — which is precisely the work that makes this non-trivial.

You can build this yourself or use one of the libraries and gateway services that have emerged to do exactly this. Either way, the architectural idea is the same: a seam between your app and the providers, where routing decisions are made.

Caching across providers

Caching, which I covered as a cost pattern, becomes more valuable in a multi-provider setup, and the routing layer is the natural home for it. Because all requests flow through one seam, that seam is the perfect place to cache:

  • A provider-independent cache. Cache responses at the routing layer, keyed by the request, so a repeated request is served from cache regardless of which provider would have handled it. The cache sits above the providers, so its benefit doesn’t depend on routing staying constant.
  • Cache as another failover. If all providers are down or rate-limiting, a cache of recent responses can still serve repeated requests — a last line of resilience for the cases it covers.
  • Consistent cost savings. Centralizing caching at the routing layer means the savings apply uniformly no matter how routing changes underneath, which is cleaner than per-provider caching scattered through the app.

Putting the cache at the routing seam is the natural design: one place all requests pass through is exactly where a cache belongs.

The honest cost of going multi-provider

Now the skepticism this series insists on, because multi-provider routing is exactly the kind of architecture that’s tempting to build before you need it. The costs are real:

  • The abstraction layer is real complexity. Normalizing across providers that genuinely differ — in capabilities, in API shape, in how features like streaming or function calling work — is non-trivial ongoing work. The layer is a system you now build, test, and maintain.
  • You can lose provider-specific features. A lowest-common-denominator abstraction may not expose the best, newest capabilities of any single provider, because they don’t all have them. You sometimes trade the cutting edge of one provider for portability across several.
  • More moving parts to operate. Multiple provider integrations, credentials, and the routing logic itself are all things that can break and need monitoring. You’ve added operational surface in exchange for flexibility.
  • It’s premature for most apps starting out. Here’s the key honest point: if you’re building a new feature, committing to one good provider and writing clean, well-isolated code around it is usually the right first move. You get simplicity now, and if you later need multi-provider, the isolation makes adding it tractable.

The decision is the familiar match-the-tool-to-the-need one. The lightweight version — isolate your provider calls behind a thin internal interface from day one — is cheap and almost always worth it, because it keeps the door open. The full version — a real multi-provider routing layer with failover and cross-provider caching — is worth building when you have a concrete driver: a resilience requirement that a single provider’s outages would violate, cost pressure that cross-provider routing would meaningfully relieve, or a genuine need to use different models for different tasks. Build the seam early; build the full routing layer when a real need, not anticipation, calls for it.

Verdict

With several genuinely capable LLM providers leapfrogging each other on quality, price, and speed, treating providers as interchangeable behind a routing layer buys real things: resilience (fail over when a provider is down), cost optimization (route to the cheapest capable provider, switch as prices change), best-model-per-task routing, and freedom from lock-in in a fast-moving market. The mechanism is an abstraction layer — a common interface your app calls, routing logic that picks the provider, and normalization of their differences — and the routing seam is the natural home for a provider-independent cache that doubles as a last-resort failover. But the honest costs are real: the abstraction is genuine ongoing complexity, you can lose provider-specific features to a lowest-common- denominator interface, and you add operational surface — which makes a full multi-provider layer premature for most apps starting out. The right move splits the difference: isolate provider calls behind a thin internal interface from day one (cheap, keeps the door open) and build the full routing layer only when a concrete driver demands it — a real resilience requirement, real cost pressure, or a real best-model-per-task need. Build the seam early; build the machinery when the need is real, not anticipated.