I'm Masaki Hirokawa, an artist and indie developer. I've been building mobile apps on my own since 2014. The other day, while wiring an in-app tagging feature for one of my wallpaper apps to Gemini 2.5 Pro, I wanted to shave off some latency and set thinkingBudget to 0. The whole generation call died with a 400 error. The exact same code worked fine on 2.5 Flash, so my first guess was an SDK bug.
It wasn't a bug. Gemini 2.5 Pro simply does not allow thinking to be turned off completely. The same thinking-budget setting accepts different value ranges depending on the model. Here's how to tell the symptom apart and write it correctly so you don't lose an afternoon to it like I almost did.
The symptom: thinkingBudget: 0 returns 400 INVALID_ARGUMENT
Here is the reproduction. In the Python SDK (google-genai), setting the thinking budget to 0 on Pro fails:
from google import genai
from google.genai import types
client = genai.Client(api_key="YOUR_GEMINI_API_KEY")
# Works on 2.5 Flash, but returns 400 on 2.5 Pro
resp = client.models.generate_content(
model="gemini-2.5-pro",
contents="Give me 5 tags that fit this description.",
config=types.GenerateContentConfig(
thinking_config=types.ThinkingConfig(thinking_budget=0), # Pro rejects 0
),
)
print(resp.text)The error you get back looks roughly like this:
google.genai.errors.ClientError: 400 INVALID_ARGUMENT
{
"error": {
"code": 400,
"message": "Budget 0 is invalid. Expected a value in the range [128, 32768], or -1 (DYNAMIC) for models/gemini-2.5-pro.",
"status": "INVALID_ARGUMENT"
}
}
The key detail is that the message explicitly says 0 is invalid and that it expects [128, 32768] or -1. The API parsed your request correctly and rejected the value itself. This is not a malformed request or a network problem.
Why only Pro rejects 0 — the per-model thinking budget ranges
The Gemini 2.5 family lets you tune how many tokens go toward internal "thinking" via thinkingBudget. The accepted range, however, differs by model. As of this writing (May 2026), it breaks down roughly like this:
- gemini-2.5-pro: 128 to 32,768 tokens, or -1 (dynamic allocation). Thinking cannot be fully disabled (0 is not allowed). Because it is built for hard reasoning, at least 128 tokens always go to thinking.
- gemini-2.5-flash: 0 to 24,576 tokens. Passing 0 disables thinking entirely.
- gemini-2.5-flash-lite: does not think by default. You can enable thinking by giving it a budget.
If you don't know about this difference and "reuse the setting that worked on Flash for Pro," you hit exactly this error. That's what happened to me: I prototyped on Flash, then swapped in Pro for smarter output in production, and it broke the moment I switched models. When you change models, you have to change the thinking-budget assumptions along with them.
Note that -1 means dynamic allocation, where the model decides how much thinking to spend based on task difficulty. If you just want a safe default on Pro, pass -1 instead of 0.
Fix 1: minimize thinking on Pro the right way
If you want to stay on Pro but trim the thinking overhead as much as possible, pass the minimum value of 128 instead of 0:
resp = client.models.generate_content(
model="gemini-2.5-pro",
contents="Give me 5 tags that fit this description.",
config=types.GenerateContentConfig(
thinking_config=types.ThinkingConfig(thinking_budget=128), # smallest allowed budget
),
)
print(resp.text)
print("thinking tokens:", resp.usage_metadata.thoughts_token_count)usage_metadata.thoughts_token_count tells you how many tokens were actually spent on thinking. Whether 128 is enough for your output quality is something you should judge from this value and from the actual responses. For lightweight work like tagging or short classification, I saw almost no perceptible quality drop at 128.
On the other hand, capping summarization consistency or multi-step reasoning at 128 can make the conclusions sloppy. Whenever you trim thinking for cost or speed, check a few output samples to confirm quality hasn't degraded.
Fix 2: switch to 2.5 Flash when speed and cost come first
Honestly, the moment you feel like you want to turn thinking off completely, that task is often overkill for Pro. Tagging, short classification, and reshaping text into a fixed format are all handled well by Flash with thinking disabled.
resp = client.models.generate_content(
model="gemini-2.5-flash",
contents="Give me 5 tags that fit this description.",
config=types.GenerateContentConfig(
thinking_config=types.ThinkingConfig(thinking_budget=0), # Flash allows 0
),
)
print(resp.text)I eventually moved my wallpaper app's tagging from Pro (128) to Flash (thinking off). Output quality was practically identical for my use, responses came back one or two seconds faster, and the token bill dropped by exactly the thinking portion that disappeared. Since I run these apps solo on mostly AdMob revenue, small savings like this add up.
As a rule of thumb: if you want speed and low cost over raw smarts, use Flash (thinking off if you like); if you need hard reasoning or code generation accuracy, use Pro (128 or higher, or -1).
A field-name trap in the migrated SDK: thinkingConfig
Around the same error, there's a second thing that trips people up: getting the field name wrong. In the new unified SDK (google-genai / @google/genai), the naming convention differs by language.
In JavaScript / TypeScript it is camelCase:
import { GoogleGenAI } from "@google/genai";
const ai = new GoogleGenAI({ apiKey: "YOUR_GEMINI_API_KEY" });
const res = await ai.models.generateContent({
model: "gemini-2.5-flash",
contents: "Give me 5 tags that fit this description.",
config: {
thinkingConfig: { thinkingBudget: 0 }, // camelCase in JS
},
});
console.log(res.text);There are two common mistakes. One is placing thinkingConfig at the top level instead of inside config, which makes the setting silently ignored. The other is mixing in the syntax of the old SDK (@google/generative-ai). If a setting doesn't seem to take effect, first check that thinkingConfig sits directly under config. Also remember that Python uses snake_case (thinking_config / thinking_budget) while REST uses camelCase (thinkingConfig / thinkingBudget) — the spelling changes by layer.
Prevention: keep per-model settings in one place
The real fix against recurrence is a single table that maps a model name to its thinking budget. If you stop hard-coding 0 at each call site, swapping models won't trigger this error again.
# Map each model to an allowed thinking budget
THINKING_BUDGET = {
"gemini-2.5-pro": 128, # 0 not allowed; clamp to the minimum
"gemini-2.5-flash": 0, # 0 disables thinking
"gemini-2.5-flash-lite": 0, # no thinking by default
}
def make_config(model: str):
budget = THINKING_BUDGET.get(model, -1) # unknown models fall back to dynamic (-1)
return types.GenerateContentConfig(
thinking_config=types.ThinkingConfig(thinking_budget=budget),
)
resp = client.models.generate_content(
model="gemini-2.5-pro",
contents="Give me 5 tags that fit this description.",
config=make_config("gemini-2.5-pro"),
)Falling back to -1 (dynamic) for unknown model names means a newly released model won't crash with a 400 even if you forget to add it. I call Gemini through a shared library across several apps, so keeping this in one place saves me from patching every call site whenever a model updates.
The next time you see this error, read the Expected a value in the range [...] part of the message first, and check whether the model you're using allows 0 (the Flash family) or not (Pro). That single check resolves most cases.
Reference: Gemini API official docs — Thinking