●PRO35 — Gemini 3.5 Pro is reported to target July 17, though the date, context window, and pricing all remain unconfirmed by Google●REBUILD — The model is said to be a ground-up rebuild after the 2.5 Pro architecture was scrapped over structural failures in recursive tool-calling and SVG generation●FLASH35 — Gemini 3.5 Flash is the newest broadly available release, tuned for faster, lower-cost coding, agents, multimodal, and long-document work●SAATHI — ATL Saathi, built on Gemini 3.5 Flash, begins piloting in 100 Indian schools as a 24/7 planning and training assistant for Tinkering Lab educators●SEA — In Southeast Asia, nearly 70% of prompts arrive in native languages — Vietnam 89%, Thailand 87%, Indonesia 84% — and 3 in 4 requests come from mobile●SHEETS — From July 15, AI Expanded Access licenses raise the usage limits for Fill with Gemini and the AI function in Sheets●PRO35 — Gemini 3.5 Pro is reported to target July 17, though the date, context window, and pricing all remain unconfirmed by Google●REBUILD — The model is said to be a ground-up rebuild after the 2.5 Pro architecture was scrapped over structural failures in recursive tool-calling and SVG generation●FLASH35 — Gemini 3.5 Flash is the newest broadly available release, tuned for faster, lower-cost coding, agents, multimodal, and long-document work●SAATHI — ATL Saathi, built on Gemini 3.5 Flash, begins piloting in 100 Indian schools as a 24/7 planning and training assistant for Tinkering Lab educators●SEA — In Southeast Asia, nearly 70% of prompts arrive in native languages — Vietnam 89%, Thailand 87%, Indonesia 84% — and 3 in 4 requests come from mobile●SHEETS — From July 15, AI Expanded Access licenses raise the usage limits for Fill with Gemini and the AI function in Sheets
Designing a Multi-LLM Failover Architecture Around Gemini API: Production Redundancy Patterns That Actually Hold
A production-grade pattern for putting Gemini API at the core of your stack while keeping Claude and GPT-4o as fallbacks — router, adapters, circuit breakers, and observability, all written in Python you can paste straight into your service.
import { Callout } from '@/components/ui/callout';
About six months after I shipped a Gemini-powered feature into production, Google had a regional incident in the middle of the night. The frontend was up, the database was healthy, and yet every Gemini call was timing out. Support tickets started piling in: "the AI feature is the only thing that's broken."
Sitting in front of my terminal that night, I learned a lesson the hard way: a production app that depends on a single LLM provider cannot ride out provider-level outages. Unless you have a contractual SLA backing you, an upstream incident at OpenAI, Anthropic, or Google takes your feature down with it.
This article is the architecture I rebuilt afterwards — a multi-LLM failover stack with Gemini API at the core and Claude / GPT-4o as fallbacks. It's not theoretical. It's the structure I've been running in production since.
Why think about multi-LLM redundancy at all
Let me start with what I'm not arguing. I'm not saying "don't pick a primary provider." Gemini API has the strongest balance of price, performance, and multimodal support that I've seen, and I haven't moved my main workload off of it. The point is that picking a primary and planning for the day it fails are two different conversations.
In production, I've watched LLM-related incidents fall into three buckets.
Provider-side outages. Google, OpenAI, and Anthropic each take a few multi-hour incidents per year. New model launches especially tend to come with rough edges. Their public status pages are honest, and they show this clearly.
Hitting your own rate limits. Bursty traffic or a sudden user spike can push your project past its RPM/TPM allocation. Quota increases take time to approve, and you can't always afford to wait.
Deliberate cost-driven routing. Sending hard reasoning tasks to Gemini 2.5 Pro, light tasks to Gemini 2.5 Flash, and overflow to Claude Haiku is a real way to cut your monthly LLM bill by 30 to 50%. Cost-aware routing isn't exotic — it's pragmatic.
Multi-LLM redundancy gives you a single mechanism that handles all three. The first time you build it, the indirection feels like overhead. Once it's in place, daily operations get simpler, not more complex.
The overall shape — router plus adapter
The structure I run in production breaks into a few clean layers.
Application code calls a single abstract API: "ask the LLM."
Router layer picks a provider based on health, cost, and task profile.
Adapter layer normalizes each provider's SDK behind one interface.
Observability layer records every call: provider, latency, success rate.
Circuit breakers trip a provider into an "open" state after consecutive failures.
Forcing every call through "app → router → adapter → provider" means the application never has to know which provider answered. That's the trick that makes this scalable. Adding a new provider later is one new adapter file plus one line in the router config.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Move from a single-provider setup to a redundant Gemini-first stack without changing your application code
✦Get a copy-pasteable Python implementation of the router + adapter pattern with Claude and GPT-4o as fallbacks
✦Ship circuit breakers, health checks, and observability that keep your app running on one wing during a provider outage
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
The crucial detail is that every response carries provider and model. When something goes wrong at 3 a.m., being able to answer "which provider responded to that user, and how long did it take?" is what makes the system operable.
The Claude and OpenAI adapters follow the same shape. The discipline is to keep every provider-specific SDK call inside its adapter, and to have the rest of the codebase speak only in LLMRequest and LLMResponse.
Circuit breakers — don't keep calling a provider that's dead
Before building the router, set up circuit breakers. Without them, the failure mode looks like this: Gemini is down → call Gemini → wait for the timeout → finally fall back to Claude. Your users feel every retry. With breakers in place, after a few consecutive failures the router stops calling the dead provider entirely for a cooldown window.
# llm/breaker.pyimport timefrom dataclasses import dataclassfrom enum import Enumclass BreakerState(Enum): CLOSED = "closed" # normal OPEN = "open" # tripped, do not call HALF_OPEN = "half_open" # one trial call allowed@dataclassclass CircuitBreaker: failure_threshold: int = 3 recovery_timeout_sec: float = 30 state: BreakerState = BreakerState.CLOSED failure_count: int = 0 opened_at: float = 0.0 def can_attempt(self) -> bool: if self.state == BreakerState.CLOSED: return True if self.state == BreakerState.OPEN: if time.time() - self.opened_at >= self.recovery_timeout_sec: self.state = BreakerState.HALF_OPEN return True return False return True # HALF_OPEN def record_success(self) -> None: self.state = BreakerState.CLOSED self.failure_count = 0 def record_failure(self) -> None: self.failure_count += 1 if self.failure_count >= self.failure_threshold: self.state = BreakerState.OPEN self.opened_at = time.time()
Three states, three transitions. The math is unsexy, but the impact on your average latency during an outage is enormous.
The router — pick a provider, fall back if it fails
The router holds an ordered list of adapters and a circuit breaker per adapter. I've kept the policy intentionally simple in production: try the primary, fall through to the next on failure or open breaker, raise if all fail.
# llm/router.pyimport loggingfrom .base import LLMAdapter, LLMRequest, LLMResponsefrom .breaker import CircuitBreakerlogger = logging.getLogger(__name__)class LLMRouter: def __init__(self, adapters: list[LLMAdapter]): self._adapters = adapters self._breakers = {a.name: CircuitBreaker() for a in adapters} async def complete(self, request: LLMRequest) -> LLMResponse: last_error: Exception | None = None for adapter in self._adapters: breaker = self._breakers[adapter.name] if not breaker.can_attempt(): logger.warning( "skip provider=%s reason=breaker_open", adapter.name, ) continue try: response = await adapter.complete(request) breaker.record_success() return response except Exception as e: breaker.record_failure() logger.error( "provider_failed provider=%s err=%s", adapter.name, e, ) last_error = e continue raise RuntimeError( f"all providers failed, last_error={last_error}" )
Wiring it up:
# main.pyfrom llm.router import LLMRouterfrom llm.gemini_adapter import GeminiAdapterfrom llm.claude_adapter import ClaudeAdapterrouter = LLMRouter([ GeminiAdapter(model="gemini-2.5-pro"), ClaudeAdapter(model="claude-haiku-4-5-20251001"),])response = await router.complete(LLMRequest( system="You are a careful, friendly assistant.", user="Confirm the failover path is working.",))print(f"Reply: {response.text}")print(f"Provider: {response.provider}, cost: ${response.cost_usd:.4f}")# Expected output:# Reply: The failover path is functioning ...# Provider: gemini, cost: $0.0023
Why I stick with a simple ordered fallback list
I tried weighted random and weighted round-robin first. They look smarter on paper. They were a pain in operations.
First, when something breaks, you want to be able to answer "which provider is serving traffic right now?" instantly. Weighted distribution leaves you in a permanent fog of "roughly half Gemini, roughly half Claude," which makes reproducing provider-specific bugs harder than it should be.
Second, your cost story is easier to read. If you're budgeting on the assumption that 100% of calls go to Gemini, you can treat fallback usage as "extra spend during incidents" — a clean signal you can monitor.
Third, prompt tuning concentrates on one provider. With multi-provider distribution, you're optimizing prompts against a moving target. Picking a primary and tuning prompts hard for it, while treating fallbacks as a safety net, gives you better long-term quality.
Provider-specific prompt tweaks
You can't paste the same prompt into every provider and expect identical quality. Gemini wants system_instruction cleanly separated from the user content, Claude uses system plus messages, and OpenAI uses role-based messages. Even "respond with JSON only" is phrased differently for best results across these models.
I keep that variance inside the adapters with small helpers:
# llm/prompt_templates.pyGEMINI_JSON_HINT = ( "Respond with JSON only. " "No code fences, no preamble, no closing remarks.")CLAUDE_JSON_HINT = ( "Respond with valid JSON only. " "Do not include any prose before or after the JSON.")def to_gemini_prompt(system: str, user: str, expects_json: bool): if expects_json: system = system + "\n\n" + GEMINI_JSON_HINT return system, userdef to_claude_prompt(system: str, user: str, expects_json: bool): if expects_json: user = user + "\n\n" + CLAUDE_JSON_HINT return system, user
Add expects_json (or whatever metadata you need) to LLMRequest and let each adapter translate it into its provider's preferred phrasing. The application keeps speaking the abstract dialect.
Observability — make the system legible during incidents
A redundant system without observability is not actually easier to operate. The bare minimum I push into Cloud Logging:
I pipe this into Looker Studio so I can see, at a glance, the daily fallback rate and the estimated extra cost during any incident window. That single dashboard has paid back the work of building it many times over.
Three traps that bite first-time multi-LLM stacks
1. The fallback's context window doesn't fit the prompt
Gemini 2.5 Pro takes 1M tokens. Claude Haiku 4.5 takes 200K. GPT-4o takes 128K. If you've been routinely sending long prompts to Gemini, a naïve fallback to Claude will simply error on input length.
The fix is to filter the candidate adapters in the router by estimated token count:
def select_capable(adapters, estimated_tokens: int): capacity = {"gemini": 1_000_000, "claude": 200_000, "openai": 128_000} return [ a for a in adapters if estimated_tokens < capacity.get(a.name, 32_000) * 0.8 ]
I lived through one late-night incident where a long-prompt path failed across the entire fallback chain because no provider could hold the input. Adding the filter ahead of time made that class of incident disappear.
2. JSON stability varies by provider
Gemini becomes very stable when you combine response_mime_type="application/json" with a response_schema. Claude and GPT-4o, without explicit "no prose, JSON only" instructions, still occasionally prepend a sentence of prose.
Always wrap parsing in a tolerant layer:
import jsonimport redef safe_parse_json(text: str): try: return json.loads(text) except json.JSONDecodeError: m = re.search(r"```(?:json)?\s*(\{.*?\}|\[.*?\])\s*```", text, re.DOTALL) if m: return json.loads(m.group(1)) m = re.search(r"\{.*\}|\[.*\]", text, re.DOTALL) if m: return json.loads(m.group(0)) raise
3. temperature=0 and max_tokens don't mean the same thing everywhere
temperature=0 is not perfectly deterministic on Gemini; you may need to combine it with a seed for reproducibility. And max_tokens is the output budget on Gemini, but on some OpenAI models it has historically referred to input plus output.
Don't trust your assumption that "the same parameters mean the same thing across providers." Normalize that semantic in your adapter layer so the rest of the system sees consistent behavior.
Cost-aware routing — when to break the simple fallback rule
I said earlier that I prefer ordered fallback over weighted distribution. There's one case where I bend that rule: cost-tiered routing within a single workload.
Here's the pattern. Some user-facing tasks are obviously cheap — short Q&A, classification, intent extraction — and some are obviously expensive — multi-step reasoning, long-document summarization, code generation. Sending everything to Gemini 2.5 Pro means paying Pro prices for tasks that Gemini 2.5 Flash or Claude Haiku could handle for a fraction of the cost.
The trick is to make this a task type decision, not a per-request lottery. I tag each call site with a task profile, and the router maps that profile to a different ordered list of adapters.
You retain the simplicity of ordered fallback within each profile, but the per-profile routing lets you express "send classification to Flash, send hard reasoning to Pro" without spreading model strings throughout your codebase.
In one of my services, this pattern alone cut the monthly LLM bill by roughly 40%. The split was about 70% of calls being cheap classification work that didn't need a Pro-class model. Once I made that split visible at the architecture layer, I could measure it, monitor it, and tune it.
What real outage telemetry looks like
When the next incident does happen, what does the log stream actually show? Here's a redacted slice from one of mine, so you can pattern-match against your own when the moment comes.
2026-04-09T03:14:22Z INFO llm_call_success provider=gemini fallback_count=0 latency_ms=812.42026-04-09T03:15:08Z INFO llm_call_success provider=gemini fallback_count=0 latency_ms=945.12026-04-09T03:15:51Z ERROR provider_failed provider=gemini err=DeadlineExceeded2026-04-09T03:15:53Z INFO llm_call_success provider=claude fallback_count=1 latency_ms=1342.82026-04-09T03:16:20Z ERROR provider_failed provider=gemini err=DeadlineExceeded2026-04-09T03:16:22Z INFO llm_call_success provider=claude fallback_count=1 latency_ms=1289.42026-04-09T03:16:51Z ERROR provider_failed provider=gemini err=ServerError(503)2026-04-09T03:16:51Z WARN breaker_open provider=gemini cooldown_sec=302026-04-09T03:16:53Z INFO llm_call_success provider=claude fallback_count=1 latency_ms=1310.72026-04-09T03:17:23Z INFO llm_call_success provider=claude fallback_count=1 latency_ms=1265.0...2026-04-09T03:21:52Z INFO breaker_half_open provider=gemini2026-04-09T03:21:54Z INFO llm_call_success provider=gemini fallback_count=0 latency_ms=901.32026-04-09T03:21:54Z INFO breaker_closed provider=gemini
You can read the incident in plain language: Gemini started failing at 03:15:51, the breaker tripped at 03:16:51 after three consecutive failures, traffic ran cleanly on Claude for the next four minutes (latency about 400 ms higher, because Haiku is slower than Pro for this prompt shape), and the breaker reset on its first successful trial call at 03:21:54. From a user-facing perspective there were almost no visible failures — only the request that happened to be in flight at the exact moment the breaker tripped saw a couple of seconds of extra latency.
That's the operational target. Not "no incidents," because incidents are inevitable at the provider level. The target is "incidents that you only notice in the logs."
A small note on testing failover paths
The hardest part of operating a redundant system is keeping the fallback path tested. Code that runs only during incidents is, by definition, code that's almost never executed. I run two safeguards.
The first is a synthetic monthly drill: I configure the router for a "drill mode" that fails the primary adapter for a fixed test prompt, on a schedule, in production. If the drill ever stops returning a Claude response with fallback_count=1, I get paged. This catches credential rot, library upgrades that broke the adapter, and quota issues, all before a real outage exposes them.
The second is a unit test that injects a dummy adapter that always raises, places it first in the chain, and asserts that the router falls through to a stub second adapter. It's a tiny test and it catches a surprising number of refactors that would otherwise quietly break failover.
Tiny, but it has paid off every time someone — including me — has refactored the router and quietly broken the fallback semantics.
Closing — your first concrete step
Redundancy architecture sounds expensive before you build it. In practice the code is small. Once the adapter abstraction exists, adding a fallback provider is one new file.
If your service depends on Gemini API alone today, my honest suggestion is to start with the adapter layer only. Refactor your call sites to go through llm.complete(request). That single change opens the door to every other improvement in this article. The first time I did this for a real service, it took half an afternoon.
The redundancy itself is the easy part once the foundation exists. Generate a Claude key, write a 30-line adapter, and your service stops dying on one wing during the next incident.
Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.