On the afternoon of October 7, I reopened the Gemini API deprecations table and a familiar ID stopped me.
gemini-3.1-flash-image. It differs by one suffix from gemini-3.1-flash-image-preview, the ID I named as a replacement in an earlier post. Its row now showed a shutdown date of October 29 and the next replacement, gemini-nano-banana-2.1.
The system I had moved to has its own end date. Obvious in hindsight, yet I had quietly assumed that once I migrated, I was done.
So this is not a post about trying a new model. It is about knowing where your setup calls an image model, and deciding the cutover day and the way back before the deadline decides for you. That alone removes most of the last-day scramble.
The Destination Had a Date Too
I once wrote about auditing model shutdown dates yourself (The "October 16" Date Missing from the Official Table — Auditing Gemini Model Shutdowns Yourself). Here are the ledger rows from then, plus one I added today.
| Model ID | Shutdown | Replacement |
|---|---|---|
gemini-2.5-flash-image | 2026-10-02 | gemini-3.1-flash-image-preview |
gemini-3.1-flash-image | 2026-10-29 | gemini-nano-banana-2.1 |
I copied the second row by hand from the table, so please compare spellings against the official page before trusting my ledger.
One trap deserves a plain statement. IDs with and without -preview are separate rows in the table. If your ledger says gemini-3.1-flash-image but the code still holds gemini-3.1-flash-image-preview, a naive match walks right past it. Check whether the -preview ID carries its own date before adding it.
Scan the Repo and Match the Ledger in One Pass
Image calls rarely live in code alone. Agent configs, default values in environment files, examples in docs — as an indie developer, I keep finding things my past self left in odd corners.
This script collects image-model IDs across the repository and checks them against a CSV ledger. The point: it treats the suffix as part of the ID.
#!/usr/bin/env python3
"""Find image-model IDs in a repo and match them against a shutdown ledger."""
import csv, re, sys, datetime as dt
from pathlib import Path
LEDGER = Path(sys.argv[2] if len(sys.argv) > 2 else "image_models.csv")
ROOT = Path(sys.argv[1] if len(sys.argv) > 1 else ".")
SKIP = {".git", "node_modules", ".venv", "dist", ".next"}
EXT = {".py", ".ts", ".tsx", ".js", ".mjs", ".json", ".yaml", ".yml", ".toml", ".env", ".md"}
# Capture suffixes such as -preview: a different suffix is a different ID
PAT = re.compile(r"gemini-[0-9][0-9.]*-[a-z-]*image[a-z0-9-]*")
def load_ledger():
rows = {}
with LEDGER.open(encoding="utf-8") as f:
for r in csv.DictReader(f):
rows[r["model_id"]] = r
return rows
def main():
ledger, today = load_ledger(), dt.date.today()
hits = {}
for p in ROOT.rglob("*"):
if not p.is_file() or SKIP & set(p.parts) or p.suffix not in EXT:
continue
for n, line in enumerate(p.read_text(encoding="utf-8", errors="ignore").splitlines(), 1):
for m in PAT.findall(line):
hits.setdefault(m, []).append(f"{p.relative_to(ROOT)}:{n}")
for mid, places in sorted(hits.items()):
r = ledger.get(mid)
if r is None:
print(f"? {mid} not in ledger (check the official table) {', '.join(places)}")
continue
left = (dt.date.fromisoformat(r["shutdown"]) - today).days if r["shutdown"] else None
tail = "no date" if left is None else f"{left} days left"
print(f"{'!' if left is not None and left < 30 else ' '} {mid} {tail} -> {r['replacement'] or '-'} {', '.join(places)}")
main()The ledger is the table above as CSV:
model_id,shutdown,replacement
gemini-3.1-flash-image,2026-10-29,gemini-nano-banana-2.1
gemini-2.5-flash-image,2026-10-02,gemini-3.1-flash-image-previewI ran it on a small test repository — one hit in a config file, one in Python, one in TypeScript with the -preview suffix. On October 7 it printed:
! gemini-3.1-flash-image 22 days left -> gemini-nano-banana-2.1 conf/agent.yaml:1, src/thumb.py:1
? gemini-3.1-flash-image-preview not in ledger (check the official table) src/other.ts:1
! means under 30 days. ? means the ledger cannot answer — and those are the lines that need a human.
Split the Switch: One Function, One Way Back
With three call sites, editing all three is the slow route. Write one function that returns the model ID, and give it the cutover date.
import os, datetime as dt
# Ledger: primary model, fallback, and planned cutover day
PLAN = {
"primary": "gemini-3.1-flash-image",
"next": "gemini-nano-banana-2.1",
"cutover": dt.date(2026, 10, 20), # nine days before the 10/29 deadline
"shutdown": dt.date(2026, 10, 29),
}
def pick_model(today=None):
today = today or dt.date.today()
forced = os.environ.get("IMAGE_MODEL") # manual override wins
if forced:
return forced, "env"
if today >= PLAN["shutdown"]:
return PLAN["next"], "after-shutdown"
if today >= PLAN["cutover"]:
return PLAN["next"], "cutover"
return PLAN["primary"], "primary"
def generate(prompt, call, today=None):
"""call(model, prompt) is your own image call. On failure, fall back to the old model."""
model, why = pick_model(today)
try:
return call(model, prompt), model
except Exception as e:
if model == PLAN["next"] and today_before_shutdown(today):
print(f"[warn] {model} failed ({e}) -> falling back to {PLAN['primary']}")
return call(PLAN["primary"], prompt), PLAN["primary"]
raise
def today_before_shutdown(today=None):
return (today or dt.date.today()) < PLAN["shutdown"]Feeding it dates, with a dummy call instead of the real API:
2026-10-07 ('gemini-3.1-flash-image', 'primary')
2026-10-20 ('gemini-nano-banana-2.1', 'cutover')
2026-10-29 ('gemini-nano-banana-2.1', 'after-shutdown')
[warn] gemini-nano-banana-2.1 failed (quota) -> falling back to gemini-3.1-flash-image
Why it is written this way:
- The cutover sits nine days before the deadline so any problem with the new model surfaces while the old one still works.
- The environment override comes first, so "put it back" never requires touching code.
- The fallback only exists until the day before shutdown. After that, there is nothing to fall back to.
This is a test script, not a verified production API call. Fill in call — client setup and arguments — to match your own stack.
Image Models Break by Eye
Swapping a text model can be half-checked by whether the JSON keeps its shape. Images do not work that way. The same prompt can shift composition, color, or how text lands on the canvas.
My own line, especially near a deadline:
Do not let the new model's output reach production until I have looked at a few images with my own eyes.
Before the cutover, I pick five of my usual prompts and view old and new results side by side — size, margins, broken lettering. Looking is faster than measuring here.
What to Settle Before October 29
| Date | Task |
|---|---|
| By 10/10 | Run the scan; verify every ? line against the official table |
| By 10/15 | Compare old and new models on the same prompts, by eye |
| 10/20 | Cutover day — new model on, fallback still in place |
| 10/29 | Shutdown — drop the env override and tidy the fallback code |
Your first step can be today: run the scan once. Just learning that you have a single call site is a relief.
A destination with its own end date is something I expect to meet again. When it happens, I will add one more line to the ledger — a small habit I intend to keep.