●SUNSET — The image generation models shut down today, August 17: imagen-4.0-generate-001, ultra, fast, and the Gemini 3 Image family, and calls after that fail with a hard error●SCALE — Gemini crossed one billion monthly active users on August 11●ASSISTANT — Starting September 4, Gemini replaces Google Assistant on Android and Wear OS, a rollout expected to take several weeks and one you cannot reverse on a device●DEVICES — The change covers Android phones and tablets, Wear OS watches, Assistant-enabled headphones, and phone-projected Android Auto; cars with Google built-in keep working●SPARK — Since August 13, Gemini 3.7 Flash has powered Gemini Spark for AI Pro and Ultra subscribers across more than 160 countries●PRICE — Gemini 3.7 Flash carries introductory pricing of $0.75 per million input tokens and $3.75 output through December 31, moving to $1.50 and $7.50 after that●SUNSET — The image generation models shut down today, August 17: imagen-4.0-generate-001, ultra, fast, and the Gemini 3 Image family, and calls after that fail with a hard error●SCALE — Gemini crossed one billion monthly active users on August 11●ASSISTANT — Starting September 4, Gemini replaces Google Assistant on Android and Wear OS, a rollout expected to take several weeks and one you cannot reverse on a device●DEVICES — The change covers Android phones and tablets, Wear OS watches, Assistant-enabled headphones, and phone-projected Android Auto; cars with Google built-in keep working●SPARK — Since August 13, Gemini 3.7 Flash has powered Gemini Spark for AI Pro and Ultra subscribers across more than 160 countries●PRICE — Gemini 3.7 Flash carries introductory pricing of $0.75 per million input tokens and $3.75 output through December 31, moving to $1.50 and $7.50 after that
Your Shipped App Still Remembers the Retired Model
Finishing the server-side migration is only half of a model retirement. The older builds of your app still hold the old model name, and once the cutoff passes, rolling back stops being a recovery option. Here is how I moved model resolution onto the server.
On the morning of the cutoff, every server-side call had already been switched over. I still felt uneasy, and the reason was specific: the older builds of my apps sitting on the App Store and Google Play were still holding the old model name.
The wallpaper apps I run as an indie developer lean on Gemini for image classification and description text. The calls themselves live on the server, but there was a period when early builds carried a client-side config file that said which model backed which feature.
When the preview image models were retired at the end of June, a piece of that leftover surfaced. My server logs showed a thin but steady trickle of requests carrying a model name that, in theory, nobody was sending anymore.
A retirement date is not the day your migration finishes. It is the day your oldest still-supported client stops working. Miss that distinction and your preparation is only half done.
Deploying does not rewrite the binaries already out there
If you only run a web service, model retirement is structurally simple. You deploy, and the next request takes the new path. Nobody walks the old one.
Add a mobile app and that assumption breaks. The binary lives on the user's device, and you have no way to rewrite it. Updates happen on the user's schedule and the store's.
Layer
Time until a change takes effect
Against the cutoff
Server code
Immediately on deploy
Can be done by the day
Remote config
Minutes, or next launch
Can still be done on the day
App binary
Review, phased release, user update
Will not be in place by the day
Only the third row is outside your control. The moment you bake a value with an expiry date into that layer, the cutoff stops being yours to manage.
Once I understood that shape, I started peeling proper nouns out of the client config one at a time. The work is unglamorous, but it reads better as a single idea: move values that expire into places you can still edit.
The moment the cutoff passes, rollback stops being recovery
The second thing that is easy to miss is that a retirement date inverts the meaning of a rollback.
During a migration we naturally keep an escape route. Revert the environment variable. Redeploy the previous revision. Halt the phased release so the old build stays put. Normally these are all correct moves.
Past the cutoff, the model at the end of the old path no longer exists. Rolling back is no longer a return to a known-good state; it is choosing a state that is guaranteed to fail. Calls after retirement stop with a hard error rather than a deprecation warning, so there is no window in which this shows up as partial degradation. And in the middle of an incident, that is exactly the button your hand reaches for.
Starting about a week before the date, I began checking three things concretely:
Which point in the deploy history is the first one that resurrects a call to the retired model
Blocking rollbacks past that revision in the deployment settings themselves
Where the "previous app version" left behind by pausing a phased release actually lands when it reaches Gemini
The third one was the awkward one. Pausing an iOS phased release only stops the rollout; people who already updated do not go back, and people who have not updated stay where they are. So the pause neither reverts everyone nor advances everyone. It freezes two populations in place. If the older population's path dies at the cutoff, freezing it prolongs the outage rather than ending it.
✦
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
✦You will be able to estimate how long a value baked into a shipped binary keeps affecting your operations
✦You will be able to decide for yourself where the line sits between the client and the server when Gemini API model resolution moves server-side
✦You will be able to judge, before you press it, whether a rollback across a retirement date is safe
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.
There is really only one direction to move in. The client says what it wants done; the server decides which model does it.
Ship capability names, not model names
The client sends something like image.describe or wallpaper.classify. No model name is included at all. Here is the minimal Swift shape:
struct AIRequest: Encodable { let capability: String // a capability name such as "wallpaper.classify" let assetId: String let locale: String}func classify(assetId: String) async throws -> Classification { var req = URLRequest(url: URL(string: "https://api.example.com/v1/ai")!) req.httpMethod = "POST" req.setValue("application/json", forHTTPHeaderField: "Content-Type") // Send the client version. Never send a model name. req.setValue(Bundle.main.appVersion, forHTTPHeaderField: "X-App-Version") req.httpBody = try JSONEncoder().encode( AIRequest(capability: "wallpaper.classify", assetId: assetId, locale: Locale.current.identifier) ) let (data, resp) = try await URLSession.shared.data(for: req) guard let http = resp as? HTTPURLResponse, http.statusCode == 200 else { // Degrade one feature quietly. Do not take the whole screen down. throw AIError.unavailable } return try JSONDecoder().decode(Classification.self, from: data)}
The version header is the part that pays off later. It lets you count which builds are still alive using nothing but server logs.
Keep exactly one resolution table on the server
Confine every appearance of a model name to a single place. When the next deprecation notice lands, whether the edit is one file or forty decides how calm your cutoff day is.
import osfrom google import genaiclient = genai.Client(api_key=os.environ["GEMINI_API_KEY"])# Model names appear here and nowhere elseCAPABILITY_MODELS = { "wallpaper.classify": "gemini-3.7-flash", "image.describe": "gemini-3.7-flash", "image.generate": "gemini-3.1-flash-image",}# Capability names older clients might still sendCAPABILITY_ALIASES = { "image.imagen4": "image.generate", # sent by builds up to v2.3.0}class UnknownCapability(Exception): passdef resolve_model(capability: str) -> str: key = CAPABILITY_ALIASES.get(capability, capability) model = CAPABILITY_MODELS.get(key) if model is None: # Fail loudly. Guessing a default hides the incident from you. raise UnknownCapability(capability) return modeldef run(capability: str, parts: list) -> str: model = resolve_model(capability) resp = client.models.generate_content(model=model, contents=parts) return resp.text
The deliberate choice here is refusing to route unknown capabilities to a default model. A well-meaning fallback conceals the existence of old clients. Failing instead fills your logs with UnknownCapability, and by the end of the day you know exactly which builds were left behind.
If you are still moving code off generate_images(), the arguments do not map one-to-one onto generate_content(). I wrote that part up separately in the argument mapping notes.
Refuse model names that arrive from the client
The most dangerous thing to carry into a cutoff is a request schema that still lets a client name a model. You cannot change the string an old build sends.
FORBIDDEN_FIELDS = ("model", "model_name", "engine")def sanitize(payload: dict) -> dict: # Drop client-supplied model hints, but record them before dropping leaked = [f for f in FORBIDDEN_FIELDS if f in payload] if leaked: logger.warning( "client sent model hint: fields=%s version=%s", leaked, payload.get("_app_version", "unknown"), ) for f in leaked: payload.pop(f) return payload
Record, then drop. The volume of that warning line is a decent proxy for how many old clients are still out there.
The compatibility branch is the first thing to break
This is where my expectation was most clearly wrong.
My instinct was to thicken the compatibility layer for the sake of old clients: requests from older builds go to the old model, newer builds go to the new one. Perfectly reasonable on its face.
Read it again with a cutoff in mind and the branch works against you. The code you wrote for compatibility is the one piece guaranteed to fail on the retirement date. Worse, it only executes for requests from old clients, so your staging environment never touches it. Production alone goes red, on the day.
So I stopped thickening it. Requests from old builds now flow to the new model like everything else. The output shape may shift enough that an old build renders something slightly off, but a cosmetic defect is a lighter failure than a hard one.
Approach
Before the cutoff
After the cutoff
Branch old clients to the old model
Rendering matches exactly
That branch, and only that branch, fails
Route every client to the new model
Minor rendering differences
Nothing fails
When the thing you are trying to stay compatible with has an expiry date, compatibility work makes you less safe. That inversion did not land for me until I had lived through one retirement.
A forced update will not arrive in time
Forcing an update is the obvious way to retire old builds, and I considered it once.
The arithmetic does not work. Build the release that carries the forced-update gate, clear review, run the phased rollout, wait for people to actually update. In my experience as a solo developer, you should budget several weeks between announcement and meaningful adoption. That competes head-on with the window a deprecation notice gives you.
A forced update also locks out people who cannot update. A meaningful share of my users are still on older devices, and bricking their app over a scheduling problem of mine was not a trade I was willing to make.
The order that actually works looks like this:
Absorb model resolution on the server and route old clients to the new model — this must be finished before the date
Build per-feature degradation so a failure costs one feature, not the whole screen
Treat forced updates as next month's cleanup, not as a cutoff-day weapon
Reframing the forced update from "today's tool" to "next release's tidying" made the decision considerably easier.
Three things to look at before the date
On the morning itself, what I read is logs, not code. Three checks, in order:
Where model names appear. Search the whole repository and confirm nothing outside the resolution table names a model — config files, tests, notebooks, and sample snippets in documentation included
Client version distribution. Aggregate the X-App-Version header over the last week. As long as old numbers show up, the old path is alive
How far back a rollback reaches. Open the actual diffs and confirm which revision is the last safe one to return to
Start collecting the second one the day the deprecation notice lands. Adding it on the cutoff day leaves you with no history to compare against.
For the inventory step itself — finding every place you call the retired model from — I wrote up the procedure in the shutdown audit notes. This article picks up after that, with the layer you cannot rewrite yourself.
One thing to do today
Open a single real request coming from your client and look at what is inside it. If a model name is in there, that value now has an expiry date attached.
It took me one full retirement cycle to arrive at this line. Do not put values that expire in places you cannot edit. Written down it sounds obvious, and that is exactly why it survives, quietly, in a corner of a config file.
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.