●NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaply●OMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflows●AGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactions●MEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuously●THROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and region●DEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20●NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaply●OMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflows●AGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactions●MEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuously●THROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and region●DEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Routing Between Local Gemma 4 and the Gemini API Cut My Bill from ¥32,000 to ¥9,000 — A Production Hybrid Router Design
How I cut a ¥32,000/month Gemini API bill to the ¥9,000 range with hybrid inference: routing design, a full Python router, production pitfalls, and how Gemma 4 arriving on the Gemini API in July 2026 changes the decision.
When my monthly Gemini API bill crossed ¥32,000 (roughly $215), I started asking a question I should have asked sooner: does every request really need the top-tier model?
The answer was no — not by a long shot.
After analyzing two weeks of request logs, I found that roughly 70% of my app's API calls were simple classification tasks, short summaries, and template-based generations. None of them required Gemini 2.5 Pro's full reasoning power. They just needed "good enough" quality — fast.
Gemma 4 had just landed in Ollama. I gave it a shot on my MacBook Pro. Two weeks later, my API costs were sitting at ¥9,200.
This article walks through the hybrid inference architecture I settled on — the design rationale, the full Python implementation, and the production pitfalls you'll want to know before shipping.
Why Hybrid Inference Works
The core insight is that real-world AI application traffic is deeply heterogeneous.
After analyzing thousands of requests, I found they cluster into roughly three buckets:
Bucket A (~40–50%): Simple tasks
Text classification (sentiment, category, language)
Short summaries under 200 words
Form validation and structured data extraction
Templated response generation
Bucket B (~30–40%): Moderate tasks
FAQ generation and Q&A
Short code explanations and review
Data transformation with clear schema
Bucket C (~15–20%): Complex tasks
Multi-step reasoning and analysis
Long-form summarization and translation
Code generation and debugging
Multimodal processing (images, audio, video)
Buckets A and B are where Gemma 4 shines. For most of these tasks, the quality difference between a well-tuned local model and Gemini 2.5 Flash is negligible to the end user. Only Bucket C genuinely needs cloud inference.
# Install Ollamacurl -fsSL https://ollama.ai/install.sh | sh# Pull Gemma 4 (27B model, ~16GB download)ollama pull gemma4:27b# Test it worksollama run gemma4:27b "What is the capital of France?"# Start the API server (default port: 11434)ollama serve
Ollama exposes an OpenAI-compatible REST API, which means you can reuse virtually any existing OpenAI SDK code with minimal changes. This compatibility is a major practical advantage when integrating into existing codebases.
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
✦Real routing ratios and cost data from 8,000 requests/month in production (¥32,000 → ¥9,000 range)
✦A production-ready Python hybrid router with fallback, timeouts, and stats logging
✦How Gemma 4 arriving on the Gemini API in July 2026 — plus unrestricted-key rejection — changes the architecture decision
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 router is the heart of the hybrid architecture. It decides, for each incoming request, whether to call Gemini API or handle the request locally with Gemma 4.
Here's the routing logic I landed on after several iterations:
Route to Gemma 4 (local) when:
Prompt is under 1,500 characters
Task is classification, judgment, or short generation
No high originality or complex reasoning required
No multimodal inputs present
Route to Gemini API when (priority rules):
Input contains images, audio, or video
Task involves reasoning, analysis, or code generation
Prompt exceeds 1,500 characters
Caller explicitly requests force_gemini=True
These thresholds aren't universal. You'll want to calibrate them against your own traffic patterns — but they work well as a starting point for text-heavy applications.
Full Python Implementation
Here's the complete router class as deployed in production:
# hybrid_router.pyimport asyncioimport osimport timefrom enum import Enumfrom dataclasses import dataclassfrom typing import Optionalimport google.generativeai as genaifrom openai import AsyncOpenAIfrom dotenv import load_dotenvload_dotenv()class ModelTarget(Enum): GEMINI = "gemini" GEMMA_LOCAL = "gemma_local"@dataclassclass RoutingDecision: target: ModelTarget reason: str estimated_cost_usd: floatclass HybridRouter: """ Automatically routes requests between Gemini API and Gemma 4 local inference. Optimizes cost, quality, and latency based on request characteristics. """ GEMINI_COSTS = { "gemini-2.5-pro": {"input": 1.25, "output": 10.0}, "gemini-2.5-flash": {"input": 0.15, "output": 0.60}, "gemini-2.0-flash": {"input": 0.10, "output": 0.40}, } LOCAL_THRESHOLD_CHARS = 1500 COMPLEX_TASK_KEYWORDS = [ "write code", "implement", "debug", "analyze", "reasoning", "compare", "evaluate", "explain why", "step by step", "コードを書いて", "実装して", "デバッグ", "分析して", "推論", ] def __init__( self, gemini_model: str = "gemini-2.5-flash", gemma_model: str = None, ): genai.configure(api_key=os.getenv("GEMINI_API_KEY")) self.gemini_client = genai.GenerativeModel(gemini_model) self.gemini_model_name = gemini_model self.gemma_client = AsyncOpenAI( base_url=f"{os.getenv('OLLAMA_BASE_URL', 'http://localhost:11434')}/v1", api_key="ollama", ) self.gemma_model = gemma_model or os.getenv("GEMMA_MODEL", "gemma4:27b") self.stats = {"gemini_calls": 0, "local_calls": 0, "total_cost_usd": 0.0} def _decide_route(self, prompt: str, force_gemini: bool = False) -> RoutingDecision: if force_gemini: return RoutingDecision( target=ModelTarget.GEMINI, reason="caller explicitly requested Gemini", estimated_cost_usd=self._estimate_cost(prompt) ) # Multimodal signals if any(tag in prompt for tag in ["[IMAGE]", "[AUDIO]", "[VIDEO]"]): return RoutingDecision( target=ModelTarget.GEMINI, reason="multimodal input detected", estimated_cost_usd=self._estimate_cost(prompt) ) # Complexity check prompt_len = len(prompt) is_complex = any(kw in prompt.lower() for kw in self.COMPLEX_TASK_KEYWORDS) if is_complex and prompt_len > 300: return RoutingDecision( target=ModelTarget.GEMINI, reason=f"complex task keywords detected (len={prompt_len})", estimated_cost_usd=self._estimate_cost(prompt) ) # Length threshold if prompt_len >= self.LOCAL_THRESHOLD_CHARS: return RoutingDecision( target=ModelTarget.GEMINI, reason=f"prompt exceeds local threshold ({prompt_len} chars)", estimated_cost_usd=self._estimate_cost(prompt) ) return RoutingDecision( target=ModelTarget.GEMMA_LOCAL, reason=f"short, simple task (len={prompt_len})", estimated_cost_usd=0.0 ) def _estimate_cost(self, prompt: str) -> float: model_costs = self.GEMINI_COSTS.get( self.gemini_model_name, self.GEMINI_COSTS["gemini-2.5-flash"] ) # Rough estimate: 0.5 tokens per character (Japanese-heavy apps) input_tokens = len(prompt) * 0.5 output_tokens = 500 return round( (input_tokens / 1_000_000) * model_costs["input"] + (output_tokens / 1_000_000) * model_costs["output"], 6 ) async def generate( self, prompt: str, system_instruction: str = "", force_gemini: bool = False, temperature: float = 0.7, ) -> tuple[str, RoutingDecision]: """ Routes the request and generates a response. Returns: Tuple of (generated text, RoutingDecision) """ decision = self._decide_route(prompt, force_gemini) start_time = time.time() try: if decision.target == ModelTarget.GEMMA_LOCAL: messages = [] if system_instruction: messages.append({"role": "system", "content": system_instruction}) messages.append({"role": "user", "content": prompt}) response = await self.gemma_client.chat.completions.create( model=self.gemma_model, messages=messages, temperature=temperature, max_tokens=2048, ) result = response.choices[0].message.content self.stats["local_calls"] += 1 else: full_prompt = f"{system_instruction}\n\n{prompt}" if system_instruction else prompt response = await asyncio.to_thread( self.gemini_client.generate_content, full_prompt, generation_config=genai.types.GenerationConfig( temperature=temperature, max_output_tokens=2048, ) ) result = response.text self.stats["gemini_calls"] += 1 self.stats["total_cost_usd"] += decision.estimated_cost_usd except Exception as e: # Automatic fallback: local failure → Gemini API if decision.target == ModelTarget.GEMMA_LOCAL: print(f"⚠️ Local model failed, falling back to Gemini API: {e}") fallback_decision = RoutingDecision( target=ModelTarget.GEMINI, reason="local model failed — fallback", estimated_cost_usd=self._estimate_cost(prompt) ) full_prompt = f"{system_instruction}\n\n{prompt}" if system_instruction else prompt response = await asyncio.to_thread( self.gemini_client.generate_content, full_prompt ) result = response.text self.stats["gemini_calls"] += 1 self.stats["total_cost_usd"] += fallback_decision.estimated_cost_usd return result, fallback_decision else: raise elapsed = time.time() - start_time print(f"[{decision.target.value}] {elapsed:.2f}s | {decision.reason}") return result, decision def get_stats(self) -> dict: total = self.stats["gemini_calls"] + self.stats["local_calls"] local_ratio = self.stats["local_calls"] / total if total > 0 else 0 return { **self.stats, "total_calls": total, "local_ratio": f"{local_ratio:.1%}", }# Usage exampleasync def main(): router = HybridRouter(gemini_model="gemini-2.5-flash") # Simple task → automatically routes to Gemma 4 (local) text, decision = await router.generate( prompt="Classify this text as Positive, Negative, or Neutral: 'The meeting ended earlier than expected.'", system_instruction="You are a text classifier. Output only the classification label.", ) print(f"Classification: {text}") # Expected output: Neutral # Route: gemma_local (cost: $0.000000) # Complex task → automatically routes to Gemini API text, decision = await router.generate( prompt="Implement an async HTTP client in Python with timeout handling, retry logic with exponential backoff, and structured error handling suitable for production use.", ) print(f"Code generated: {len(text)} chars | Route: {decision.target.value}") # Route: gemini (complex task keywords detected) print(router.get_stats())if __name__ == "__main__": asyncio.run(main())
The automatic fallback from local to Gemini API is critical for production reliability. Your service shouldn't go down because Ollama restarted.
Three Pitfalls That Will Bite You in Production
Pitfall #1: Model Cold Start Latency
Ollama unloads models from memory after an idle period (default: 5 minutes). When the next request arrives, Gemma 4's 27B model takes 8–12 seconds to reload. For a user-facing application, that's an unacceptable experience.
Fix: Keep the model warm by setting OLLAMA_KEEP_ALIVE=-1:
OLLAMA_KEEP_ALIVE=-1 ollama serve
Or run a background warmup script that pings the model every few minutes:
# warmup.py — Keeps Gemma 4 resident in memoryimport asyncioimport httpxasync def keep_warm(): while True: try: async with httpx.AsyncClient() as client: await client.post( "http://localhost:11434/api/generate", json={"model": "gemma4:27b", "prompt": "ping", "stream": False}, timeout=30.0 ) print("Warmup ping sent") except Exception as e: print(f"Warmup failed: {e}") await asyncio.sleep(270) # Every 4.5 minutesasyncio.run(keep_warm())
Pitfall #2: Silent Quality Degradation
Routing purely on character count can send quality-sensitive tasks to the local model without you noticing. A 60-character prompt like "Is this Japanese phrase grammatically correct?" looks simple by length, but actually requires nuanced language understanding.
My solution: use force_gemini=True on endpoints where quality is non-negotiable.
# For customer-facing responses, always use Gemini APIresult, _ = await router.generate( prompt=user_input, force_gemini=True, # Quality-critical endpoint)
This is a more surgical approach than trying to enumerate every edge case in the routing logic.
Pitfall #3: Mac vs. Linux Server Performance Gap
Local inference performance differs dramatically by hardware. On a MacBook Pro M3 Max, Gemma 4 27B runs at roughly 25–35 tokens/second — fast enough for synchronous requests. On a CPU-only Linux server (no GPU), that drops to 2–4 tokens/second. That's not usable for real-time applications.
If your production server doesn't have a GPU, the hybrid approach changes: instead of cloud vs. local, your split becomes Gemini 2.5 Pro vs. Gemini 2.5 Flash (or 2.0 Flash). The routing logic stays identical — only the "local" target changes to a cheaper cloud model.
Here are the actual results from two weeks of production traffic on my service:
Before (Gemini 2.5 Pro only)
Monthly requests: ~8,000
Average prompt length: ~600 characters
Monthly cost: ¥28,000–¥32,000
After (hybrid architecture)
Gemini API calls: 28% of traffic (~2,240 requests)
Gemma 4 local: 72% of traffic (~5,760 requests)
Monthly cost: ¥7,500–¥9,500
Cost reduction: ~70%
Note that I also downgraded from Gemini 2.5 Pro to 2.5 Flash for the cloud calls — the combination of routing and model downgrade drove most of the savings. The key insight: once you're only sending complex tasks to the cloud, 2.5 Flash handles them well. Perceived quality from users stayed the same.
If your application repeatedly uses large system prompts or reference documents, combining hybrid routing with Context Caching reduces costs further.
# context_cache_router.py (excerpt)import google.generativeai as genaidef create_cached_context(system_doc: str, expiry_minutes: int = 60): """ Cache a large system document to avoid re-billing input tokens on every request. Most effective for documents over 32K tokens. """ cache = genai.caching.CachedContent.create( model="models/gemini-2.5-flash-001", contents=[system_doc], ttl=f"{expiry_minutes * 60}s", ) return cachedef get_model_with_cache(cache): """Return a model instance that uses the cached context.""" return genai.GenerativeModel.from_cached_content(cache)
The cache TTL determines how long the content stays resident. For a 1-hour context window with frequent queries, the cached token cost is typically 75% lower than re-sending the document each time.
If you don't have local GPU hardware, you can still run Gemma 4 on cloud infrastructure at lower cost than premium Gemini models:
Google Cloud Run + Ollama: Package Ollama into a container image and deploy on Cloud Run with GPU instances. Works well for workloads with variable traffic since Cloud Run scales to zero.
Vertex AI Endpoints: Deploy Gemma 4 as a managed endpoint on Vertex AI. Higher base cost but no infrastructure management required.
Hugging Face Inference Endpoints: Gemma 4 is available on Hugging Face; you can deploy a dedicated endpoint and use the Inference API.
For most individual developers, starting with local inference and migrating to cloud when you need horizontal scale is the practical path. The router code changes minimally — you just swap the Ollama endpoint URL for a cloud endpoint URL.
July 2026 Update: Gemma 4 Is Now Available Directly on the Gemini API
One premise of this article has shifted since I first published it. As of July 2026, Gemma 4 is available through AI Studio and the Gemini API itself — and Gemini 3.5 Flash, positioned for agentic and coding workloads, has reached general availability.
Local Ollama is no longer the only home for your lightweight tasks. With the same API key, you can now route simple requests to API-hosted Gemma 4 and complex ones to Gemini 3.5 Flash. The router's classification logic stays untouched; only the destinations change.
As an indie developer running automated pipelines against the Gemini API every day, I welcome the third option. Here is how the trade-offs break down:
Setup
Best for
Watch out for
Local Gemma 4 (Ollama)
High request volume, data you want to keep on-device, a dev machine that is always on
Startup time, thermals, power; does not scale horizontally
API-hosted Gemma 4
No infrastructure to babysit; absorbs Mac/Linux environment differences
Metered billing — it is no longer "effectively free"
Hybrid (this article)
Cost-first setups where you already own local hardware
Implementation and monitoring cost of fallback paths
The deciding metric is monthly requests × average tokens. At my volume (8,000 requests a month, ~600 characters each), keeping local inference remained the cheapest option. At around 1,000 requests a month, though, moving everything to API-hosted Gemma 4 and retiring the router entirely is the lighter operation — not owning code is also a form of optimization.
One operational change to note: since June 19, 2026, requests from unrestricted API keys are rejected. The more roles your key plays in a hybrid setup, the more important it becomes to configure application and API restrictions on it. I have added this to the checklist below.
Production Deployment Checklist
Before shipping the hybrid router to production, verify the following:
Infrastructure
Ollama port (11434) is firewall-protected (not exposed to public internet)
Your Gemini API key has application and API restrictions configured (requests from unrestricted keys are rejected as of June 2026)
Model files are on persistent storage that survives container restarts
CPU/GPU resource limits are configured so Ollama doesn't starve other services
Code Quality
Fallback from local → Gemini API is implemented and tested
Timeouts are set appropriately (local: 30s, API: 10s recommended)
All routing decisions are logged with reason and model target
Observability
# routing_monitor.py — Log routing statistics hourlyimport loggingimport asynciologger = logging.getLogger(__name__)async def log_routing_stats(router: HybridRouter, interval_seconds: int = 3600): """Emit routing statistics every hour for cost monitoring.""" while True: await asyncio.sleep(interval_seconds) stats = router.get_stats() logger.info( f"[HybridRouter] " f"Gemini: {stats['gemini_calls']} | " f"Local: {stats['local_calls']} | " f"Local ratio: {stats['local_ratio']} | " f"Cost: ${stats['total_cost_usd']:.4f}" ) # Alert if local ratio drops below 50% (possible misconfiguration) if stats["total_calls"] > 100: local_ratio_num = stats["local_calls"] / stats["total_calls"] if local_ratio_num < 0.5: logger.warning( f"⚠️ Local inference ratio below 50% — review routing thresholds" )
Your Next Step
The hybrid inference approach isn't about replacing Gemini API — it's about using it precisely where it adds the most value.
The best starting point: analyze one week of your application's request logs. Ask yourself honestly: how many of these requests actually needed the top model? What percentage were simple classification, short generation, or templated responses?
If that number is above 40%, you have a strong case for hybrid inference. Install Ollama, pull Gemma 4, and test the router on your five most common request types. The cost math will speak for itself.
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.