I was reading through the model IDs in my code when I noticed a new row in the deprecations table. The October 6 changelog says only that gemini-3.1-flash-image is deprecated, with no shutdown date announced. Oddly, a missing date is harder to plan around than a bad one.
My starting point: when a deprecation has no date, decide by which conditions let you move today, not by when the model will stop. Everything below comes from the official docs as I read them on October 8, 2026. The numbers are the docs' numbers, not mine.
Facts first
gemini-nano-banana-2.1became GA on October 6.gemini-3.1-flash-imageis deprecated and points togemini-nano-banana-2.1. The deprecations table says "No shutdown date announced."- The same page notes that a listed date is the earliest a model might be retired. When a date appears, treat it as your deadline.
- An older note of mine said October 29. The current table has no such date, so if you see it elsewhere, check the official table.
Three models, side by side
| Aspect | gemini-nano-banana-2.1 | gemini-3.1-flash-lite-image | gemini-3.1-flash-image |
|---|---|---|---|
| Status | GA (recommended) | Available | Deprecated, no date |
| Resolution | 1K / 2K / 4K | 1K only | 512px / 1K / 2K / 4K |
| Reference images | 10 object, 4 character | 14 object (no character figure listed) | 10 object, 4 character |
| Google Search grounding | Yes (image search too) | No | Yes (image search too) |
| Thinking levels | minimal / medium / high (default medium) | minimal / high | minimal / high (default minimal) |
Building the table surprised me: 512px output exists only on the deprecated model. If a thumbnail path asks for 512px, you cannot simply swap the ID. You would generate at 1K and downscale, or find another remedy.
Make the choice reproducible
A table is easy to misread, so I encoded it. This script needs no API key and runs on plain Python.
MODELS = {
"gemini-nano-banana-2.1": {"sizes": {"1K", "2K", "4K"}, "objects": 10, "characters": 4, "grounding": True, "thinking": {"minimal", "medium", "high"}, "status": "GA"},
"gemini-3.1-flash-lite-image": {"sizes": {"1K"}, "objects": 14, "characters": 0, "grounding": False, "thinking": {"minimal", "high"}, "status": "stable"},
"gemini-3.1-flash-image": {"sizes": {"512", "1K", "2K", "4K"}, "objects": 10, "characters": 4, "grounding": True, "thinking": {"minimal", "high"}, "status": "deprecated"},
}
def pick(size="1K", objects=0, characters=0, grounding=False, thinking=None):
out = []
for name, m in MODELS.items():
why = []
if size not in m["sizes"]: why.append(f"{size} unsupported")
if objects > m["objects"]: why.append(f"max {m['objects']} object refs")
if characters > m["characters"]: why.append(f"max {m['characters']} character refs")
if grounding and not m["grounding"]: why.append("no search grounding")
if thinking and thinking not in m["thinking"]: why.append(f"thinking={thinking} unsupported")
if m["status"] == "deprecated": why.append("deprecated (no date)")
out.append((name, why))
return outI ran it against four use cases. The output (Japanese labels in my original run, translated here):
[A 1K bulk wallpapers, no references]
OK gemini-nano-banana-2.1
OK gemini-3.1-flash-lite-image
NG gemini-3.1-flash-image - deprecated (no date)
[B 4K, 2 character refs, search on]
OK gemini-nano-banana-2.1
NG gemini-3.1-flash-lite-image - 4K unsupported / max 0 character refs / no search grounding
NG gemini-3.1-flash-image - deprecated (no date)
[C 512px thumbnail]
NG gemini-nano-banana-2.1 - 512 unsupported
NG gemini-3.1-flash-lite-image - 512 unsupported
NG gemini-3.1-flash-image - deprecated (no date)
[D 12 references, 1K]
NG gemini-nano-banana-2.1 - max 10 object refs
OK gemini-3.1-flash-lite-image
NG gemini-3.1-flash-image - max 10 object refs / deprecated (no date)
Case A passes on two models, so it becomes a quality-versus-cost call; the cost design for batch images with Nano Banana 2 Lite covers that side. Case B leaves only nano-banana-2.1. Case C has no valid target at all.
The call shape in the official docs
The current image generation page shows interactions.create, not generate_content:
import base64
from google import genai
client = genai.Client()
interaction = client.interactions.create(
model="gemini-nano-banana-2.1",
input="Create a picture of a nano banana dish in a fancy restaurant with a Gemini theme",
)
with open("generated_image.png", "wb") as f:
f.write(base64.b64decode(interaction.output_image.data))Output options go in response_format={"type": "image", "aspect_ratio": "16:9", "image_size": "2K"}. The docs say image_size needs an uppercase K and rejects "1k". Thinking goes in generation_config={"thinking_level": "high"}. I copied this block from the docs and did not call the API myself, so try one image with your own key first.
Swapping the ID does not give you the same behavior
One more difference is easy to miss. The docs say Gemini 3 image models always think (it cannot be turned off in the API), may generate up to two interim images, and bill thinking tokens. The default level is medium on nano-banana-2.1 and minimal on 3.1-flash-image.
So if you swap only the ID and leave thinking unset, latency and cost can look different because of the defaults alone. I have more than once swapped a model expecting the same quality for less, then puzzled over the billing breakdown later. On day one of a migration I now set thinking_level explicitly to the old default (minimal) before comparing, so a difference can be traced to either the model or the setting.
The order I would follow
- Search your code for
gemini-3.1-flash-imageand list the resolution, reference count, and search use at each call site. - Feed those conditions to the script and treat every OK as a candidate.
- Set aside any call site that fails on every candidate, such as 512px or 12-plus references, and decide whether to change the spec or hold until a date appears.
Even when you hold, keep the model ID in one switchable place so the day a date is announced is a small job. I wrote about moving IDs into configuration in a pipeline that survives image model migrations and deprecations.
While no date exists, the only preparation worth doing is the inventory of conditions. When the table updates next, I will start from my own NG rows.