GEMINI LABJP
OCT 2 — gemini-2.5-flash-image is scheduled to shut down on October 2. It is the original model the world came to know as Nano BananaCAREFUL — The replacement named in the official table, gemini-3.1-flash-image-preview, was itself retired on June 25. The live path is the GA gemini-3.1-flash-imageSEP 30 — gemini-omni-flash-preview shuts down on September 30. Move to gemini-omni-1.1-flash, which went GA on August 27RESOLUTION — gemini-omni-1.1-flash adds a resolution field to video_config, with 360p, 720p, 1080p and 4k to choose fromEARLIEST — Google notes that a shutdown date is the earliest possible date, not a fixed one. No need to panic, but no excuse to wait eitherSAMPLING — temperature, top_p and top_k were deprecated back on July 21. Turning the temperature down is a habit that stops working across generationsOCT 2 — gemini-2.5-flash-image is scheduled to shut down on October 2. It is the original model the world came to know as Nano BananaCAREFUL — The replacement named in the official table, gemini-3.1-flash-image-preview, was itself retired on June 25. The live path is the GA gemini-3.1-flash-imageSEP 30 — gemini-omni-flash-preview shuts down on September 30. Move to gemini-omni-1.1-flash, which went GA on August 27RESOLUTION — gemini-omni-1.1-flash adds a resolution field to video_config, with 360p, 720p, 1080p and 4k to choose fromEARLIEST — Google notes that a shutdown date is the earliest possible date, not a fixed one. No need to panic, but no excuse to wait eitherSAMPLING — temperature, top_p and top_k were deprecated back on July 21. Turning the temperature down is a habit that stops working across generations
Articles/API / SDK
API / SDK/2026-09-12Intermediate

gemini-2.5-flash-image stops on October 2, and the replacement the table names retired in June

gemini-2.5-flash-image shuts down on October 2, 2026, but the recommended replacement listed in the official table, gemini-3.1-flash-image-preview, was already retired on June 25. Here is the script I wrote to follow replacement chains to their end, and what it found across every row of the table.

Gemini API239image generation5model migration5deprecationsPython46

Early in September I went to replace a leftover gemini-2.5-flash-image in one of my verification scripts. The Deprecations page had a replacement listed right there in the row, so I copied that string and swapped the model ID constant.

The thing I swapped to, gemini-3.1-flash-image-preview, turned out to be listed a few tables up on the same page — retired on June 25.

Reading a single row would have moved me from a model that is about to stop to one that had already stopped. A migration target isn't the string in the row; it's wherever the chain ends. Here is how I check now, with the output I actually got.

The row for October 2 points at something that stopped on June 25

Three rows from the Gemini deprecations page, side by side.

Model IDReleasedShutdownReplacement listed in the table
gemini-2.5-flash-imageOct 2, 2025Oct 2, 2026gemini-3.1-flash-image-preview
gemini-3.1-flash-image-previewFeb 26, 2026Jun 25, 2026 (already gone)gemini-3.1-flash-image
gemini-3.1-flash-imageMay 28, 2026No shutdown date announced

The target you actually want is gemini-3.1-flash-image, the one without -preview. It went GA on May 28, 2026, and no shutdown date has been announced for it.

gemini-2.5-flash-image is the original model that spread under the name "Nano Banana," so it's probably the image endpoint that survives in the largest number of indie sample repos and blog posts. I don't use AI for the artwork or the assets I ship, but I did keep a small script around to poke at the endpoint and watch its behavior. Code like that — working, so nobody touches it — is exactly the code that misses a deadline.

One caveat belongs in every article about this page. Google states plainly that the dates in the table are the earliest possible dates a model might be retired, and that the exact date will be communicated in advance. So October 2 isn't a guarantee that things break. It also isn't a reason to treat it as soft.

Two columns tell you whether a target is alive

When I read a row now, I look at two things.

  1. Does the shutdown column say "No shutdown date announced"? If there's a date, that replacement will need replacing too.
  2. Does the model ID end in -preview? Preview models turn over on a much shorter cycle than GA ones.

gemini-3.1-flash-image-preview failed both tests. It had a date, and it was a preview.

My guess is that each row simply keeps the information it had when the deprecation was announced. The table is written row by row, and when a replacement later retires, the older row doesn't follow it. So it's less that the page is wrong and more that a row doesn't finish the thought. Following the chain is the reader's job.

A script that follows the chain to its end

Doing this by hand is how you miss one, so I wrote a short script. Give it a saved copy of the page and it walks the replacement column until it lands on a model with no announced shutdown date. Standard library only.

#!/usr/bin/env python3
"""Follow the 'recommended replacement' column until it lands on a live model.
 
    python3 resolve_replacement.py deprecations.md
    python3 resolve_replacement.py deprecations.md gemini-2.5-flash-image
"""
import re, sys
from datetime import date, datetime
 
TODAY = date.today()
# | `model-id` | release date | shutdown date | `replacement` |
ROW = re.compile(
    r"\|\s*`?([a-z0-9.\-]+)`?\s*\|"   # model ID
    r"[^|]*\|"                        # release date (unused)
    r"\s*([^|]*?)\s*\|"               # shutdown date
    r"\s*`?([a-z0-9.\-]*)`?\s*\|"     # recommended replacement
)
 
def parse_date(text):
    text = text.strip()
    if not text or "No shutdown" in text:
        return None                    # no date announced = alive for now
    for fmt in ("%B %d, %Y", "%b %d, %Y"):
        try:
            return datetime.strptime(text, fmt).date()
        except ValueError:
            continue
    return None
 
def load(path):
    table = {}
    with open(path, encoding="utf-8") as fh:
        for line in fh:
            m = ROW.search(line)
            if not m:
                continue
            model, shutdown_text, replacement = m.groups()
            if model in ("Model", "Preview models", "Deprecated models"):
                continue               # drop headers and section dividers
            table[model] = {
                "shutdown": parse_date(shutdown_text),
                "replacement": replacement.strip() or None,
            }
    return table
 
def resolve(table, model, seen=None):
    seen = (seen or []) + [model]
    row = table.get(model)
    if row is None:
        return model, seen, "unknown"          # ID not in the table
    if row["shutdown"] is None:
        return model, seen, "alive"            # end of the chain
    nxt = row["replacement"]
    if not nxt or nxt in seen:                 # empty target, or a loop
        return model, seen, "dead-end"
    return resolve(table, nxt, seen)
 
def status_of(table, model):
    row = table.get(model)
    if row is None:
        return "not in the table"
    if row["shutdown"] is None:
        return "no shutdown date"
    left = (row["shutdown"] - TODAY).days
    tail = " (already gone)" if left <= 0 else " (%d days left)" % left
    return "shutdown %s%s" % (row["shutdown"].isoformat(), tail)
 
def main():
    table = load(sys.argv[1])
    targets = sys.argv[2:] or sorted(
        m for m, r in table.items() if r["shutdown"] and r["shutdown"] > TODAY
    )
    for model in targets:
        listed = (table.get(model) or {}).get("replacement")
        final, path, kind = resolve(table, model)
        print("%-42s %s" % (model, status_of(table, model)))
        print("  -> actual target: %s (%s)" % (final, kind))
        if len(path) > 2:
            print("     chain: " + " -> ".join(path))
        if listed and listed != final:
            print("  !! listed target %s is %s" % (listed, status_of(table, listed)))
        print()
 
if __name__ == "__main__":
    sys.exit(main())

Pointed at the saved page, asking only about the row in question:

$ python3 resolve_replacement.py deprecations.md gemini-2.5-flash-image
gemini-2.5-flash-image                     shutdown 2026-10-02 (20 days left)
  -> actual target: gemini-3.1-flash-image (alive)
     chain: gemini-2.5-flash-image -> gemini-3.1-flash-image-preview -> gemini-3.1-flash-image
  !! listed target gemini-3.1-flash-image-preview is shutdown 2026-06-25 (already gone)

resolve() recurses because, as it turned out, not every chain is two links long.

Image models weren't the only ones chained

I ran the same script across every row. Among rows that have a shutdown date, 11 have a listed replacement that isn't where the chain ends, and 5 of those point at a model that has already stopped.

ModelReplacement in the tableEnd of the chainHops
gemini-2.5-flash-imagegemini-3.1-flash-image-preview (gone)gemini-3.1-flash-image2
gemini-2.5-flash-image-previewgemini-2.5-flash-image (Oct 2)gemini-3.1-flash-image3
gemini-2.0-flash-preview-image-generationgemini-2.5-flash-image (Oct 2)gemini-3.1-flash-image3
imagen-3.0-generate-002imagen-4.0-generate-001 (gone)gemini-3.1-flash-image2
gemini-2.0-flash-litegemini-3.1-flash-lite (May 7, 2027)gemini-3.5-flash-lite2
gemini-robotics-er-1.5-previewgemini-robotics-er-1.6-preview (gone)gemini-robotics-er-2-preview2

Two of the image chains are three hops long. Start from gemini-2.0-flash-preview-image-generation in an old sample and follow it honestly: you pass through gemini-2.5-flash-image before you reach gemini-3.1-flash-image. Stop one step early and you have landed on the model that goes away in twenty days.

I should admit the first version of the script reported 16 rows, not 11. Five were noise. I was comparing rows whose shutdown column was empty — models with no retirement planned at all — and some of those, like gemini-3-flash-preview, carry a replacement anyway.

Adding one condition, "only look at rows that have a shutdown date," brought it down to 11. The condition I added to shrink the result was worth more, later, than the code that made it run at all. I left a comment explaining why the count dropped, which is the part that will help when the page's layout changes.

Ask the endpoint before you trust the page

Documentation records announcements. What actually answers is the endpoint. So the migration script starts with one check that the ID I'm about to ship is really being served.

from google import genai
 
client = genai.Client(api_key="YOUR_API_KEY")
 
def assert_served(model_id: str) -> None:
    """Fail immediately on an ID that models.list doesn't return."""
    served = {m.name.removeprefix("models/") for m in client.models.list()}
    if model_id not in served:
        near = sorted(s for s in served if s.startswith(model_id.split("-preview")[0]))
        raise SystemExit(
            "%s is not served. Closest IDs: %s" % (model_id, ", ".join(near) or "none")
        )
 
TARGET = "gemini-3.1-flash-image"   # confirmed by following the chain
assert_served(TARGET)               # past this line, it's just a constant swap

It prints the nearby IDs so a -preview mixup is visible at a glance. I made that mixup, which is why those two lines exist.

If your model IDs live in one place, the change itself is a constant swap. If they don't, that's the earlier job. And it's worth keeping the two clocks separate: an SDK method going deprecated and a model being retired are different deadlines, which I wrote about in why generate_images and the model retirement don't share a date.

One thing to do before October 2

Just one. Search your code and your notes for gemini-2.5-flash-image, and if anything matches, resolve the target by following the chain rather than copying the row. The end of that chain is gemini-3.1-flash-image.

In date order: gemini-omni-flash-preview on September 30, gemini-2.5-flash-image on October 2. After those two there's a long gap until gemini-3.1-flash-lite on May 7, 2027. I put the September 30 one into a single deadline table in the sunset inventory for gemini-omni-flash-preview.

What I took from this is a small rule I now follow: when I read a replacement, I open that row too. It looks like duplicated work, and it costs far less than discovering in production that I migrated onto something already switched off.

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 $15 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-08-15
generate_images Survives Until 2027. Your Image Generation Still Stops on August 17
google-genai 2.18.1 still ships generate_images, and the SDK deprecation notice points at 2027. The imagen-4.0 models, meanwhile, shut down on August 17. Here is what those two deadlines actually mean, measured on my own machine.
API / SDK2026-06-11
Gemini 3.2 API Developer Guide — Correct Model IDs, Migration from 3.1, and Production Checklist
A practical guide to calling Gemini 3.2 via the API: correct model IDs, what changed from Gemini 3.1, Python and TypeScript code examples, and a production migration checklist.
API / SDK2026-09-07
The day Lyria 3.5 landed, I changed how my audio folders are laid out
When Lyria 3.5 brought full-length generation, my generated takes were sitting in the same folder as the tracks I had chosen by hand. Here is the forty-line ledger gate that draws the line by hash, not by filename.
📚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