●FLASH — Gemini 3.5 Flash is now generally available, billed as the most intelligent model for agentic and coding tasks●TIER — New tiers like 3.1 Pro and 3.1 Flash-Lite are rolling into apps, cloud products, and business tools●PIXEL — The June Pixel Drop adds Gemini music generation, AI video and music creation, and screen-recording reactions●OMNI — Gemini Omni (creation), 3 Deep Think (reasoning), and Deep Research (automation) all advance in parallel●LIVE — Gemini Live's real-time interaction is expanding across Android, Search, YouTube, and connected Google apps●ULTRA — Google AI Ultra offers top model access, Deep Research, Veo 3 video, and a 1M-token context window●FLASH — Gemini 3.5 Flash is now generally available, billed as the most intelligent model for agentic and coding tasks●TIER — New tiers like 3.1 Pro and 3.1 Flash-Lite are rolling into apps, cloud products, and business tools●PIXEL — The June Pixel Drop adds Gemini music generation, AI video and music creation, and screen-recording reactions●OMNI — Gemini Omni (creation), 3 Deep Think (reasoning), and Deep Research (automation) all advance in parallel●LIVE — Gemini Live's real-time interaction is expanding across Android, Search, YouTube, and connected Google apps●ULTRA — Google AI Ultra offers top model access, Deep Research, Veo 3 video, and a 1M-token context window
Catching Gemini Model Deprecations in CI Before They Bite
Build a small guard that scans your codebase for hardcoded Gemini model IDs, cross-checks shutdown deadlines, and turns CI red before a model quietly disappears.
On June 25, gemini-3.1-flash-image-preview and gemini-3-pro-image-preview shut down. When I read that line on the deprecations page, my first problem was not the migration steps. It was that I could not immediately say which of my repositories still referenced those two model IDs.
Running four technical blogs (Dolice Labs) and a set of mobile apps as an indie developer, I scatter model IDs across an OGP image-generation script, sample code inside articles, and a wallpaper batch job in the app. One stale ID left in a single place starts returning 404 on the shutdown date — and that is the scariest kind of breakage. The error fires, but somewhere I did not expect.
So I built a small guard: it walks the codebase, collects every model ID, and reports the ones near their shutdown date with days remaining, right inside CI. Building the "did I forget to migrate?" detector before doing the migration itself turns out to be the calmer order of operations. Let's build that guard together in working Python.
Why missed deprecations hit indie developers harder
On a large team, these deadlines land somewhere: a dependency dashboard, an SRE runbook, a ticket. Solo, that record tends to live in your own memory — and memory is least reliable three months later, right before a release.
The second reason is that a Gemini model ID is just a string. Unlike a broken pip resolution that fails the build instantly, a soon-to-be-removed model keeps working until the shutdown date. Tests still pass. That is exactly why it pays to convert the shutdown date from "a calendar entry" into "a CI pass/fail." You may forget to check the calendar; CI checks every single time.
When I went through the gemini-2.0-flash wind-down in May 2026, the migration itself was a one-line swap. Hunting down where to swap took far longer, and I still missed one spot. Wishing CI had caught that miss is where this whole idea started.
The shape of the guard — three parts
The guard is three parts. Keeping their roles separate makes each one easy to swap later.
Part
Role
In / Out
Scanner
Collect every model-ID reference in the repo
source tree → reference list
Registry
Hold the planned shutdown date per model
YAML/dict → deadline map
Evaluator
Compute days left, verify existence, decide the exit code
refs + deadlines + models.list → pass/fail
The hard question is where the shutdown date comes from. The honest answer: you cannot reliably get it from the API.models.list returns whether a model exists and its description, but not a machine-readable "this turns off on date X." So keep the shutdown date in your own small registry, and use models.list only to confirm whether an ID is still alive. That division of labor is what keeps the tool from falling over in practice.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Stop a production pipeline from silently breaking on a model shutdown you forgot was coming, starting today
✦Copy a working pattern that pairs models.list existence checks with a deadline registry you maintain by hand
✦Drop the same script into GitHub Actions and a daily cron, with warnings and failures keyed to days remaining
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Start by gathering references. A regex catches gemini-* strings. To cover comments and Markdown sample code too, keep the extension list broad.
# scan_models.py — collect Gemini model-ID references across the repoimport refrom pathlib import Path# matches gemini-2.5-flash / gemini-3-pro-image-preview / models/gemini-...MODEL_RE = re.compile(r"(?:models/)?(gemini-[a-z0-9][a-z0-9.\-]*)")SCAN_EXT = {".py", ".ts", ".tsx", ".js", ".mjs", ".mdx", ".md", ".yaml", ".yml"}SKIP_DIRS = {"node_modules", ".git", ".next", "dist", "build"}def scan(root: str) -> dict[str, list[str]]: """model ID -> list of file paths where it appears""" found: dict[str, list[str]] = {} for path in Path(root).rglob("*"): if not path.is_file() or path.suffix not in SCAN_EXT: continue if any(part in SKIP_DIRS for part in path.parts): continue text = path.read_text(encoding="utf-8", errors="ignore") for m in set(MODEL_RE.findall(text)): found.setdefault(m, []).append(str(path)) return foundif __name__ == "__main__": import json, sys result = scan(sys.argv[1] if len(sys.argv) > 1 else ".") print(json.dumps(result, indent=2, ensure_ascii=False))
The detail that matters is set(MODEL_RE.findall(text)), which collapses duplicates within one file before counting. Without it, an article that writes the same ID twenty times reads as "twenty references" and drowns the signal. Expected output looks like this:
Now you can see that the soon-to-shut gemini-3-pro-image-preview survives only in scripts/wallpaper_batch.py. Same result as a manual grep, but the difference is that it is now structured data ready to cross-check against deadlines.
Step 2: Confirm each ID is still alive
Next, ask models.list whether each collected ID is actually still served. An ID that no longer exists is either already gone or a typo — and both are worth catching.
# live_models.py — current model IDs via the google-genai SDKfrom google import genaidef live_model_ids(api_key: str) -> set[str]: client = genai.Client(api_key=api_key) ids = set() for m in client.models.list(): # name arrives as "models/gemini-2.5-flash" ids.add(m.name.removeprefix("models/")) return idsif __name__ == "__main__": import os live = live_model_ids(os.environ["YOUR_API_KEY"]) print(f"served models: {len(live)}")
m.name.removeprefix("models/") normalizes the API form to the prefix-less form the scanner produces. Skip that alignment and you lose an afternoon to a quiet bug where "the same model" refuses to match. I lost one once.
Step 3: Cross-check days remaining and fail CI
The evaluator brings together your registry, the scan results, and the live model set. The registry is a small dict you update by hand from the deprecations page. It is the single source of truth for shutdown dates.
# guard.py — reconcile deadlines, references, and existence into an exit codeimport sysfrom datetime import date# update by hand from the deprecations page; shutdown date (UTC) as ISOSHUTDOWN_REGISTRY = { "gemini-3.1-flash-image-preview": "2026-06-25", "gemini-3-pro-image-preview": "2026-06-25", "gemini-2.0-flash": "2026-09-24",}WARN_DAYS = 30 # warn (yellow) at or below thisFAIL_DAYS = 7 # fail (red) at or below thisdef evaluate(found: dict[str, list[str]], live: set[str], today: date): problems = [] for model, files in found.items(): deadline = SHUTDOWN_REGISTRY.get(model) gone = model not in live days = None if deadline: days = (date.fromisoformat(deadline) - today).days if gone: problems.append(("FAIL", f"{model} is not in the served list (gone/typo)", files)) elif days is not None and days <= FAIL_DAYS: problems.append(("FAIL", f"{model} shuts down in {days} days", files)) elif days is not None and days <= WARN_DAYS: problems.append(("WARN", f"{model} shuts down in {days} days", files)) return problemsdef main(found, live): problems = evaluate(found, live, date.today()) exit_code = 0 for level, msg, files in sorted(problems, reverse=True): mark = "FAIL" if level == "FAIL" else "WARN" print(f"[{mark}] {msg}") for f in files: print(f" - {f}") if level == "FAIL": exit_code = 1 if not problems: print("OK: no near-deadline or removed model references") sys.exit(exit_code)
The two thresholds, WARN_DAYS and FAIL_DAYS, exist for a reason. With a single threshold, the moment you notice it is already red, and migration mutates into an emergency. Warning yellow 30 days out and turning red only at 7 days lets you schedule the migration as calm, ordinary work. After moving to this two-stage scheme, the guard flagged my image-model migration yellow 11 days before shutdown, and I swapped it without any rush.
The evaluator prints something like:
[FAIL] gemini-3-pro-image-preview shuts down in 5 days - scripts/wallpaper_batch.py[WARN] gemini-2.0-flash shuts down in 96 days - scripts/ogp.py
Wiring it into GitHub Actions and a daily cron
Once the guard works, put it where it runs even when you forget. I run it twice: on every push, and once each morning. The push run blocks merges with a red; the morning cron exists to catch the yellow stage.
run_guard.py is a thin entry point that just calls the three parts above. When a push surfaces a FAIL (exit code 1), that PR goes red and cannot merge. The moment you introduce a near-deadline model ID, it stops — exactly the intended behavior.
The reason I do not rely on cron alone is that I want to close the path by which a new stale ID enters. The morning check watches what already exists, but code that adds an old ID at noon sails through until the next morning. The push trigger stops it at the moment of introduction.
Where it tripped me up, and what made it work
My first version treated every ID the scanner found as a migration target, including historical mentions in articles ("I used to use gemini-1.5-pro"). That is not realistic. So I changed it to only evaluate models that appear in the registry, and merely list IDs outside it as information. An ID with no shutdown date has no basis for being stopped.
The other thing that mattered was folding the registry update into actual work. Once a week, open the deprecations page and add new deadlines to SHUTDOWN_REGISTRY. Whether those five minutes happen decides whether the guard stays useful. A mechanism without the habit that feeds it quietly rots. Keeping it tended by hand is, in the end, the shortest path.
A time-bound deprecation is, fundamentally, less about the migration steps and more about making "when and where it bites" visible. The same structure works beyond Gemini — API key rotation deadlines, certificate expiry. Swap out SHUTDOWN_REGISTRY and you have a different deadline watchdog.
Start by running scan_models.py at the root of your own repo once, and count how many distinct model IDs are scattered there. Knowing that number is where this guard begins to work.
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.