GEMINI LABJP
NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Articles/API / SDK
API / SDK/2026-06-01Intermediate

Empty Output but finish_reason Is MAX_TOKENS on Gemini 2.5/3: Cause and Fix

Your prompt is just a few lines, yet a low maxOutputTokens on gemini-2.5-flash returns empty text with finish_reason MAX_TOKENS. The culprit is thinking tokens. Here are three fixes with working code.

gemini-api277troubleshooting82thinking7python104nodejs2

"My prompt is about ten lines, but response.text comes back empty — and when I check finish_reason, it says MAX_TOKENS. I never asked for a long answer." If you have started using the Gemini 2.5 or 3 thinking models, you may have tilted your head at exactly this.

I ran into it myself. In the app business I have run since 2014, I was writing a batch that classifies App Store and Google Play reviews with gemini-2.5-flash. Trying to optimize cost, I dialed maxOutputTokens down to 256 — and the visible text came back empty, costing me half a day to track down. The short version: the thing manufacturing those "empty" responses is the thinking tokens.

The symptom — you ask for a short answer and it stops at MAX_TOKENS

First, how to reproduce it. With a thinking model (gemini-2.5-flash, gemini-2.5-pro, the gemini-3-* family), set maxOutputTokens to something small (tens to a few hundred) and even a request that should produce a one-word answer returns this:

from google import genai
from google.genai import types
 
client = genai.Client(api_key="YOUR_API_KEY")
 
resp = client.models.generate_content(
    model="gemini-2.5-flash",
    contents="Answer the sentiment of this review in one word, positive or negative: 'Runs smoothly, very happy'",
    config=types.GenerateContentConfig(max_output_tokens=256),
)
 
print(repr(resp.text))                       # '' or None
print(resp.candidates[0].finish_reason)      # FinishReason.MAX_TOKENS

A request that returns a single word, stopped by MAX_TOKENS. It feels backwards, and that is where the thinking-model trap hides.

Why it happens — thinking tokens spend the output budget first

Since 2.5, Gemini's thinking models reason internally before producing a final answer. That reasoning consumes real tokens, and crucially, maxOutputTokens caps the sum of thinking tokens plus the visible answer tokens.

So with maxOutputTokens=256, if the model burns all 256 on thinking, there is zero budget left for the visible answer. The result: not a single text part lands in parts, the response is cut off with MAX_TOKENS, and response.text is empty. The response is not broken — it is a budget-allocation problem.

usage_metadata makes this obvious:

um = resp.usage_metadata
print("prompt:", um.prompt_token_count)
print("thoughts:", um.thoughts_token_count)     # pinned near maxOutputTokens
print("candidates:", um.candidates_token_count) # 0 or tiny

thoughts_token_count pinned at maxOutputTokens while candidates_token_count is 0 is the smoking gun that thinking ate the whole budget. The older non-thinking models (gemini-1.5-* and friends) never did this, so it catches a lot of people during migration.

Fix 1 — size maxOutputTokens to include the thinking

The most direct fix is to give the output cap enough room to cover the thinking tokens as well. On a thinking model you must not estimate maxOutputTokens from the answer length alone.

resp = client.models.generate_content(
    model="gemini-2.5-flash",
    contents="Answer the sentiment of this review in one word, positive or negative: 'Runs smoothly, very happy'",
    config=types.GenerateContentConfig(max_output_tokens=2048),  # room for thinking
)
print(resp.text)  # 'positive'

In my experience, if you use a thinking model even for short classification or extraction, keeping maxOutputTokens at 1024 minimum — 2048 to be safe — makes empty MAX_TOKENS responses all but disappear. Treat the output cap as a safety valve against runaway generation, not as a tight fit around your expected answer length.

Fix 2 — control the thinking itself with thinkingBudget

If the answer is reliably short, capping the thinking directly is better for cost too. thinking_config.thinking_budget sets an upper bound on thinking tokens.

resp = client.models.generate_content(
    model="gemini-2.5-flash",
    contents="Answer the sentiment of this review in one word, positive or negative: 'Runs smoothly, very happy'",
    config=types.GenerateContentConfig(
        max_output_tokens=256,
        thinking_config=types.ThinkingConfig(thinking_budget=0),  # disable thinking
    ),
)
print(resp.text)  # 'positive'

One caveat about model differences. gemini-2.5-flash and flash-lite let you fully turn off thinking with thinking_budget=0, but gemini-2.5-pro cannot disable thinking entirely and keeps a minimum budget (you generally need to specify 128 or more). Passing zero on Pro yields INVALID_ARGUMENT, so for Pro, prefer Fix 1 — widen the output cap — instead of trying to zero it out. Passing thinking_budget=-1 enables a dynamic mode where the model adjusts the thinking amount to the task.

Detecting empty responses and auto-recovering in production

In batches and long-running services, adding a layer that detects rather than swallows empty responses prevents nightly jobs from silently losing data. In my review-classification pipeline, this runs every night across apps totaling 50 million downloads, so robustness here ties directly to revenue stability.

def generate_with_guard(client, model, contents, max_tokens=2048):
    resp = client.models.generate_content(
        model=model,
        contents=contents,
        config=types.GenerateContentConfig(max_output_tokens=max_tokens),
    )
    fr = resp.candidates[0].finish_reason
    text = resp.text or ""
 
    if not text and str(fr) == "FinishReason.MAX_TOKENS":
        # assume thinking exhausted the budget; double the cap and retry once
        resp = client.models.generate_content(
            model=model,
            contents=contents,
            config=types.GenerateContentConfig(max_output_tokens=max_tokens * 2),
        )
        text = resp.text or ""
 
    if not text:
        raise RuntimeError(f"empty response persists: finish_reason={fr}")
    return text

Simply enforcing the order "read finish_reason before you touch the text" prevents most empty-response trouble. The idea is the same in the Node SDK (@google/genai): check response.candidates?.[0]?.finishReason and observe thinking spend via response.usageMetadata?.thoughtsTokenCount.

After migrating to a thinking model, the first thing to revisit is maxOutputTokens — start there. I hope this saves someone the half day it cost me.

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.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

API / SDK2026-05-30
Why Gemini 2.5 Pro Rejects thinkingBudget: 0 (and How to Fix It)
Setting thinkingBudget to 0 on Gemini 2.5 Pro returns a 400 INVALID_ARGUMENT error. Here is why the per-model thinking budget ranges differ, how to minimize thinking on Pro the right way, and when to switch to Flash, with Python and JavaScript examples.
API / SDK2026-06-21
Gemini API Implicit Caching Not Working — Troubleshooting Guide by Root Cause
Troubleshoot Gemini API implicit caching issues: cache not hitting, unexpectedly high costs, or low cache hit rates. Covers token thresholds, prompt structure, model version consistency, TTL expiry, and multimodal caching with code examples.
API / SDK2026-06-14
When Gemini API Cuts Your Response Off Mid-Sentence — Detecting finish_reason: MAX_TOKENS and Stitching the Continuation
Long-form generation that ends mid-sentence is usually finish_reason: MAX_TOKENS. This failure arrives as a quiet HTTP 200, no exception. Here is how to detect it, stitch a continuation to recover the full text, and avoid the thinking-token trap that makes it worse on 3.x models.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →