●OMNI — Gemini Omni Flash, a natively multimodal model, enters API public preview for building custom video workflows●NANO — Nano Banana 2 Lite arrives as the fastest and most cost-efficient Gemini image model yet●FLASH — Gemini 3.5 Flash reaches general availability with sustained frontier performance on agentic and coding tasks●AGENTS — Managed Agents enter public preview in the Gemini API, running autonomous, stateful agents in isolated Google-hosted Linux sandboxes●MEMORY — The Memory Bank IngestEvents API is GA, decoupling event ingestion from memory generation for continuous streaming●THROUGHPUT — Provisioned throughput now accepts up to seven pending model orders for the same model and region●OMNI — Gemini Omni Flash, a natively multimodal model, enters API public preview for building custom video workflows●NANO — Nano Banana 2 Lite arrives as the fastest and most cost-efficient Gemini image model yet●FLASH — Gemini 3.5 Flash reaches general availability with sustained frontier performance on agentic and coding tasks●AGENTS — Managed Agents enter public preview in the Gemini API, running autonomous, stateful agents in isolated Google-hosted Linux sandboxes●MEMORY — The Memory Bank IngestEvents API is GA, decoupling event ingestion from memory generation for continuous streaming●THROUGHPUT — Provisioned throughput now accepts up to seven pending model orders for the same model and region
Spend Deep Reasoning Only Where It's Needed: Per-Request thinking_level Routing in Gemini
Running every request at high thinking_level bloats latency and cost; forcing low drops accuracy on hard questions. This walks through a router that picks Gemini 3.x thinking_level per request from an inexpensive difficulty estimate, keeping p95 latency inside a mobile budget while reserving deep reasoning for the questions that need it — with measured numbers and working code.
The first feedback on a small advice feature I added to an app wasn't about accuracy. It was: "sometimes it takes forever to come back." The logs showed the response times split into two clusters. Most requests returned in one to two seconds, while a handful took eight or nine.
The odd part was that the slow requests weren't always the hard ones. A light query like "any wallpapers in a similar color?" would have the model deliberating just as long as a genuinely multi-step question. Because I had pinned every request to a single deep setting, easy questions were paying the same reasoning tax as hard ones.
Gemini 3.x lets you set thinking_level per request. Instead of using one fixed value, you vary it with the difficulty of the request: shallow and fast for simple questions, deep only for the ones that earn it. That single change held the median wait time steady while cutting the total amount of deliberation sharply. Below is how I built the router, with the numbers I measured along the way.
Why "all high" and "all low" both break
A fixed thinking_level hurts in either direction. Pin it to high and simple questions over-deliberate, stretching latency and inflating output tokens (which include thinking tokens) so the bill climbs. Pin it to low and you get fast and cheap, but answers to multi-step questions turn shallow and sometimes wrong.
Here are the numbers I measured. I ran a representative 100 requests for one feature through the same prompt and input, varying only thinking_level, and recorded the median completion time (not first token) and the median billed output tokens. The model was gemini-flash-latest (the 3.5 Flash line).
thinking_level
latency p50
latency p95
median output tokens
easy-question accuracy
hard-question accuracy
low
~1.3s
~2.1s
~210
98%
71%
medium
~3.4s
~5.2s
~540
98%
86%
high
~6.8s
~9.1s
~1,180
99%
92%
What the table says is that on easy questions, low and high land at almost the same accuracy while latency spreads about 5x and output tokens about 5.6x. Processing an easy question at high throws away both the wait time and the money. Run a hard question at low, though, and accuracy falls to 71% — missing close to three in ten. That is not a feature you want to put in front of paying members.
The answer is not to pin either extreme. Tell the difficulty apart, route simple ones to low and only hard ones to high, and take the middle at medium. As long as the routing is accurate enough, the median wait time stays at low while hard-question accuracy climbs toward high.
Telling requests apart by difficulty
The heart of routing is the part that, the moment a request arrives, cheaply estimates how much thinking it needs. Putting an expensive judgment here would defeat the purpose, so I made it two-stage: a zero-cost heuristic sorts the bulk, and only the ambiguous ones get a model in the loop.
The heuristic reads difficulty off the shape of the input. As an indie developer, the observations I lean on collapse into three:
Input length and structure. Short single-fact lookups skew low. Requests with several conditions, or phrasing like "compare" or "and explain why," skew high.
The shape of the expected answer. A one-word or one-value answer is low. Anything asking for steps, reasoning, or trade-offs needs deeper thinking.
The cost of being wrong. Anything touching the purchase flow or steering a user's action gets bumped to medium or above, even if slower. Here, not missing beats being fast.
These are not perfect. A short-but-hard question ("which is the better deal, A or B?" is multi-step in ten characters) slips through. That is exactly why only the gray zone the heuristic can't call gets handed to the next stage.
✦
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
✦A two-stage difficulty estimate that maps each request to low / medium / high thinking_level and keeps p95 latency inside a fixed budget
✦Measured response time and output tokens per thinking_level, plus how to back the ceiling out from a mobile wait-time budget
✦How mis-routing a hard question to low fails silently, and the cheap signals plus a Flash pre-pass that catch it
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.
Here is a minimal router in Python that unites the heuristic and the model judgment. The trick is to keep the part that maps a difficulty decision to a tier separate from the part that injects thinking_level into the actual request, so you can tune thresholds later.
# router.py — pick thinking_level from a request's difficultyfrom google import genaifrom google.genai import typesclient = genai.Client()# minimal hints that suggest multi-step reasoning (to reduce misses)_HARD_HINTS = ("compare", "which", "why", "reason", "steps", "design", "trade-off", "vs", "recommend", "choose")def heuristic_tier(text: str, sensitive: bool) -> str | None: """Zero-cost first pass. Returns None to defer to the model.""" if sensitive: return "medium" # raise the floor where misses hurt n = len(text) hard = any(h in text.lower() for h in _HARD_HINTS) if n <= 40 and not hard: return "low" # short and simple -> shallow, fast if hard or n >= 200: return "high" # multi-step or long -> deep return None # gray zone -> defer to modeldef model_tier(text: str) -> str: """Let Flash judge difficulty itself, but only for the gray zone.""" r = client.models.generate_content( model="gemini-flash-latest", contents=f"Answer with one word (low/medium/high): how much " f"reasoning depth does this question need? Question: {text}", config=types.GenerateContentConfig( thinking_config=types.ThinkingConfig(thinking_level="low"), max_output_tokens=4, ), ) out = (r.text or "").strip().lower() return out if out in ("low", "medium", "high") else "medium"def route(text: str, sensitive: bool = False) -> str: return heuristic_tier(text, sensitive) or model_tier(text)def answer(text: str, sensitive: bool = False) -> str: level = route(text, sensitive) r = client.models.generate_content( model="gemini-flash-latest", contents=text, config=types.GenerateContentConfig( thinking_config=types.ThinkingConfig(thinking_level=level), ), ) return r.text
model_tier runs only when heuristic_tier returns None. Because that pre-pass itself runs at thinking_level="low" with max_output_tokens=4, its cost stays small next to a real answer. Constraining the judgment to a single word matters: if you let the gray-zone estimate deliberate, you undo the very latency you set out to cut.
Backing the ceiling out of a mobile wait-time budget
Once routing works, you set the ceiling on how deep a hard question may go by working backward from perceived speed. Skip this and a rare hard question pushes up p95 until the whole feature reads as "the app that sometimes freezes."
For interactive features I treat a p95 of five seconds to completion as a working target. Against the table above, high's p95 of ~9.1s blows the budget. So I capped ordinary hard questions at medium (p95 ~5.2s) and opened high by hand only to requests that are both high-stakes and rare. Put plainly: allow each level only in the proportion where its p95 doesn't break the overall budget.
There is a cost ceiling too. Raising thinking_level loads thinking tokens onto the output side, so leaning on high spikes the per-request unit cost. If you expose the AI feature to free users, the test is whether AdMob revenue covers the per-request cost. In my case I pinned free-tier requests to low by default and reserved medium-and-up for actions initiated by premium members. Both speed and unit cost land on the same one-page table of who gets which level, and when.
Catching mis-routes
This router's weak spot is dropping a hard question to low. It's faster, but the answer goes shallow and reads as off-target to the user. The nasty part is that a mis-route throws no exception: the call completes normally and simply returns a lower-quality answer, so without monitoring you never notice.
The detection I run in production is two cheap signals together. One records requests that were handled at low yet produced output longer than expected — if a supposedly shallow question needed a long answer, the difficulty read was probably wrong. The other ties a lightweight "was this helpful?" tap right after the answer to the level it was routed at. If low-routed requests skew toward "not helpful," that's the cue to revisit the heuristic threshold or _HARD_HINTS.
I also fix the order of response. First raise the share of gray-zone requests sent to the model judge; if a pattern still slips through, add exactly one word for it to _HARD_HINTS. Making the heuristic too clever makes the pre-pass heavy, so I only add words I have evidence actually missed.
Whether the Flash pre-pass pays off
The core of the two-stage design is deferring only the gray zone to a model. The obvious worry is whether the extra judgment call costs more than it saves. Measuring it settled the question.
In my feature the heuristic alone settled about 70% of requests, leaving roughly 30% for the model judge. The judgment cost on that 30% is a tiny max_output_tokens=4 call — a fraction of a real answer's cost. Meanwhile, having that stage cut the mistake of running easy questions at high, shrinking the total volume of deliberation. The small cost of judging came out smaller than the wasted deep-thinking it avoided.
The pre-pass doesn't always pay, though. In a feature where nearly every request sits at the same difficulty, there's little to route, and only the router's complexity remains. There, drop the extra stage and run one fixed thinking_level. Routing earns its keep where difficulty is bimodal. The thinking_level API itself is documented in the Gemini API docs; check the defaults on your own model before you set the ceiling.
Before you ship it
If an interactive AI feature is suffering from uneven wait times, the first step is to run a few dozen representative requests at low, medium, and high, and measure response time and accuracy on your own feature once. My numbers are only true for my inputs; change the feature and the break points move. Once you have your own figures, where to draw the ceiling stops being a guess.
Boiled down, designing the routing was the work of putting "which requests, and which users, pay for speed versus depth" onto one page. I'm still tuning it, but just dropping the fixed value and holding that one page made the feature feel visibly steadier. Thanks for reading.
Share
Thank You for Reading
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.