●API — The Interactions API reaches general availability as the primary API for Gemini models and agents, with a stable schema●AGENTS — With GA, the Interactions API formally supports Managed Agents and background execution, with Gemini Omni coming soon●LIVE — The Live API and AI Studio add real-time speech-to-speech translation, auto-detecting 70+ languages while preserving intonation●OMNI — Gemini Omni Flash, a natively multimodal model, enters API public preview for building custom video workflows●SPARK — Gemini Spark, Google's personal agent, arrives on macOS to work across local files and Google Workspace on your behalf●PRO — Gemini 3.5 Pro is reported to be delayed to July 17, centered on a 2M-token context and a Deep Think Reasoning Layer●API — The Interactions API reaches general availability as the primary API for Gemini models and agents, with a stable schema●AGENTS — With GA, the Interactions API formally supports Managed Agents and background execution, with Gemini Omni coming soon●LIVE — The Live API and AI Studio add real-time speech-to-speech translation, auto-detecting 70+ languages while preserving intonation●OMNI — Gemini Omni Flash, a natively multimodal model, enters API public preview for building custom video workflows●SPARK — Gemini Spark, Google's personal agent, arrives on macOS to work across local files and Google Workspace on your behalf●PRO — Gemini 3.5 Pro is reported to be delayed to July 17, centered on a 2M-token context and a Deep Think Reasoning Layer
Images Made With a Retiring Model Can Never Be Made Again — Tracking Regenerability in a Ledger
When Gemini's image generation models shut down on August 17, the assets you made with them can no longer be reproduced the same way. Here is the ledger design and code I use to decide, before the deadline, which assets are regenerable and which must be frozen.
The call-site audit was done. Every script and month-end batch that touched a retiring model ID had been found. All that remained was swapping in the new GA model. Then I opened the asset directory for my wallpaper app.
Several hundred images sat there, all produced by the model that is going away. Some already shipped. Some queued for re-export at new device resolutions. And I could not answer a simple question about any of them: if I send this same prompt to the new model, will I get the same look back?
Migration was never about rewriting a model ID. It was about settling the regenerability of past assets before the shutdown date arrives.
Migrating your call sites does not migrate your assets
Image generation, though, differs from text generation in one decisive way. The output itself becomes a long-lived asset.
With text, a new model that produces a semantically equivalent answer from the same input causes no practical trouble. Images do not work that way. If I regenerate one image out of a five-image wallpaper series with a new model, the palette and brushwork will not line up. Put them side by side and the seam is visible immediately.
So the real shape of the problem looked like this.
Target
Before shutdown
After shutdown
Code call sites
Swap the model ID
Still fixable at any time
Existing assets
Determine regenerability
The means of determining it is gone
That bottom-right cell is the whole point. Once the old model is retired, you cannot call it. Which means you can no longer measure how closely a new model's output resembles the old one. That measurement is only possible before the deadline. The moment I understood that asymmetry, my priorities reordered themselves.
What I had failed to record
The first wall I hit was that my assets carried no record of which model or which prompt had produced them.
File names were sequential: wp_aurora_03.png. The generation script held prompts in variables, saved the image on success, and exited. Back then I assumed I could always make more. Recording felt pointless. I never imagined the model disappearing.
That regret became the design requirement. Some information exists only at the moment of generation. Miss it, and it cannot be reconstructed later.
Six fields turned out to be the minimum a provenance ledger needs.
Field
Why it matters
model_id
Primary signal for deprecation impact. Record the resolved model, never an alias like -latest
prompt / negative_prompt
The only input for regeneration. A one-character difference changes the look, so store it verbatim
params
Aspect ratio, image count, safety settings — anything that steers the output
output_sha256
Binds ledger rows to files. Survives renames and directory moves
embedding
The reference vector for drift measurement. The old model's trace, kept after the model is gone
generated_at
Separates drift caused by minor model updates over time
The fifth field is the keystone. After the shutdown you cannot invoke the old model, but the images it produced are still sitting on disk. Embed them with gemini-embedding-2, store the vectors, and you can keep measuring how far any new model's output drifts from the original look — long after the model itself is gone. What disappears is the model, not the evidence of what it made.
✦
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
✦A provenance ledger schema that captures model ID, prompt, and output hash, plus a backfill script for assets that were never recorded
✦Measuring old-vs-new model output drift with gemini-embedding-2 image embeddings, using cosine 0.86 as the machine-readable regenerable/frozen boundary
✦A two-layer pipeline that skips generation for frozen assets and routes only derivative work (resolution variants) through the new model
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.
Recording at generation time is the ideal. But several hundred images already existed without records. Rather than fight that, I decided to build the ledger from whatever could still be recovered.
I salvaged prompts where old scripts and Git history allowed it. Embeddings, on the other hand, can be computed today from the images alone. The script below walks the asset directory and seeds the ledger.
# build_ledger.py — seed a provenance ledger from existing assetsimport hashlibimport jsonimport sqlite3from pathlib import Pathfrom google import genaiclient = genai.Client() # reads GEMINI_API_KEY from the environmentASSETS = Path("assets/wallpapers")DB = Path("provenance.sqlite3")SCHEMA = """CREATE TABLE IF NOT EXISTS asset ( output_sha256 TEXT PRIMARY KEY, path TEXT NOT NULL, model_id TEXT, prompt TEXT, params_json TEXT, embedding BLOB, generated_at TEXT, verdict TEXT DEFAULT 'unjudged');"""def sha256_of(path: Path) -> str: h = hashlib.sha256() with path.open("rb") as f: for chunk in iter(lambda: f.read(1 << 20), b""): h.update(chunk) return h.hexdigest()def embed_image(path: Path) -> list[float]: """Embed an image with gemini-embedding-2, which shares a space with text.""" uploaded = client.files.upload(file=str(path)) res = client.models.embed_content( model="gemini-embedding-2", contents=uploaded, config={"task_type": "RETRIEVAL_DOCUMENT"}, ) return res.embeddings[0].valuesdef main() -> None: conn = sqlite3.connect(DB) conn.executescript(SCHEMA) # prompts salvaged from old scripts; missing entries stay None salvaged = json.loads(Path("salvaged_prompts.json").read_text(encoding="utf-8")) for path in sorted(ASSETS.glob("*.png")): digest = sha256_of(path) row = conn.execute( "SELECT 1 FROM asset WHERE output_sha256 = ?", (digest,) ).fetchone() if row: continue # idempotent: never embed the same image twice meta = salvaged.get(path.name, {}) vec = embed_image(path) conn.execute( "INSERT INTO asset (output_sha256, path, model_id, prompt, params_json," " embedding, generated_at) VALUES (?, ?, ?, ?, ?, ?, ?)", ( digest, str(path), meta.get("model_id"), meta.get("prompt"), json.dumps(meta.get("params", {}), ensure_ascii=False), json.dumps(vec).encode("utf-8"), meta.get("generated_at"), ), ) conn.commit() print(f"ledger += {path.name} ({digest[:12]})") conn.close()if __name__ == "__main__": main()
output_sha256 is the primary key so the ledger survives renames. My assets get shuffled between directories per distribution target; keying on path would rot the ledger within a day.
Idempotency is built in from the start for a practical reason. Embedding calls cost money proportional to image count. Seeding the ledger across my 587 images took roughly 9 minutes and cost well under a dollar. Cheap — but re-embedding everything each time I interrupt and rerun the script is pure waste.
Rows where no prompt could be salvaged keep prompt IS NULL. Those rows turn out to matter most.
Measuring regenerability — the cosine 0.86 boundary
With the ledger in place, judging is straightforward.
Select ledger rows where prompt IS NOT NULL
Send the same prompt and params to the replacement model
Embed the resulting image with gemini-embedding-2
Compute cosine similarity against the stored old-model embedding
Threshold it into regenerable / frozen and write the verdict back
# judge_regenerability.py — classify assets by drift from the originalimport jsonimport sqlite3import numpy as npfrom google import genaiclient = genai.Client()NEW_MODEL = "gemini-3-image" # the GA replacementTHRESHOLD = 0.86def cosine(a: np.ndarray, b: np.ndarray) -> float: return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b)))def regenerate(prompt: str, params: dict) -> bytes: res = client.models.generate_images( model=NEW_MODEL, prompt=prompt, config={ "number_of_images": 1, "aspect_ratio": params.get("aspect_ratio", "9:16"), }, ) return res.generated_images[0].image.image_bytesdef judge(conn: sqlite3.Connection) -> None: rows = conn.execute( "SELECT output_sha256, prompt, params_json, embedding FROM asset" " WHERE prompt IS NOT NULL AND verdict = 'unjudged'" ).fetchall() for digest, prompt, params_json, emb_blob in rows: old_vec = np.array(json.loads(emb_blob.decode("utf-8"))) image_bytes = regenerate(prompt, json.loads(params_json)) new_vec = np.array(embed_bytes(image_bytes)) score = cosine(old_vec, new_vec) verdict = "regenerable" if score >= THRESHOLD else "frozen" conn.execute( "UPDATE asset SET verdict = ? WHERE output_sha256 = ?", (verdict, digest) ) conn.commit() print(f"{digest[:12]} score={score:.3f} -> {verdict}") # No prompt means no input, which means no regeneration is possible conn.execute( "UPDATE asset SET verdict = 'frozen' WHERE prompt IS NULL AND verdict = 'unjudged'" ) conn.commit()
The 0.86 threshold did not fall from the sky. It came from the distribution I measured across the 412 wallpapers whose prompts I could recover.
Cosine band
Count
What my eyes said
0.92 and above
131
Indistinguishable in context. Safe to mix within a series
0.86 – 0.92
168
Details differ, palette and composition hold. Fine as a single swap
0.78 – 0.86
84
A different picture. Cannot live in the same series
Below 0.78
29
Even the subject shifts. Concentrated in highly abstract prompts
My visual judgment broke sharply between 0.86 and 0.78. I placed the line at 0.86 because it matched my actual shipping constraint: can this image sit inside an existing series? Your number will differ with the character of your assets. Do not borrow my threshold — measure the distribution on your own library first. For reference, judging all 412 took about 38 minutes and a few dollars of regeneration cost. That is a cheap premium on an insurance policy you cannot buy after the deadline.
Then there are the 175 images with no surviving prompt. No similarity check is needed. They are frozen by definition — no model can reconstruct an input that no longer exists. Without a ledger, those 175 would have quietly stayed in the pipeline as assets I believed I could rebuild.
A two-layer pipeline that never regenerates frozen assets
Once judged, assets split into two kinds. What you build next is where the real work lives.
I chose to separate generation from derivation.
Layer
Input
Model used
Frozen assets
Generation
Prompt
New GA model
Never runs (excluded by ledger verdict)
Derivation
Existing image file
None (pure image processing)
Runs normally
When a new device resolution appears, my habit was to regenerate from the original prompt at the new size, because a generative model recovers detail more naturally than an upscale would. My exact-resolution export pipeline assumes exactly that.
Frozen assets close that road. So the derivation layer gives up on generation and falls back to image processing: high-quality resize, crop if needed, no model in the loop. Detail suffers slightly. But a broken series hurts my app far more than a slightly softer image does. That trade-off holds because visual consistency is the core value of a wallpaper app. For a one-off hero image, I would regenerate without hesitating.
The branch costs one ledger lookup.
def build_variant(digest: str, target_size: tuple[int, int], conn) -> Path: (verdict, path, prompt, params_json) = conn.execute( "SELECT verdict, path, prompt, params_json FROM asset WHERE output_sha256 = ?", (digest,), ).fetchone() if verdict == "regenerable": # generation layer: rebuild at the target aspect with the new model params = json.loads(params_json) params["aspect_ratio"] = to_aspect_ratio(target_size) return save(regenerate(prompt, params), digest, target_size) # derivation layer: no model, just image processing on the existing file return resize_without_model(Path(path), target_size, digest)
One conditional. But for that conditional to be meaningful, the judging had to finish before the shutdown. Finish it, and the deadline passes as an ordinary day on the calendar.
Field notes the documentation will not give you
A few things I only learned by running this.
Storing an alias makes the ledger worthless. Write -latest into model_id and you will never recover which model actually produced a given image. Pull the resolved model name from the generation response instead. Three of my rows carried alias names, and I had to drop them from judging entirely.
Mismatched task_type skews the similarity. Embed the old asset as RETRIEVAL_DOCUMENT and the new output as RETRIEVAL_QUERY, and cosine similarity drops measurably even for identical images. For comparison work, both sides must use the same task type. This shares a root with how task_type mismatch quietly degrades retrieval recall — I redid an entire judging run because of it.
Do not run the judging close to the deadline. That is 412 regeneration requests, submitted during a window when every other migrating team is hammering the same quota. I split mine across three days, roughly 150 per run. A single all-in batch a week before the shutdown is a plan that breaks.
frozen is not a failure. Early on I burned time salvaging prompts to shrink the frozen count. But an asset you know cannot be regenerated is perfectly manageable. What is dangerous is believing an asset is regenerable, reaching the deadline, and letting the pipeline silently emit a different picture.
What has to be settled before the deadline
You can fix a model ID after the shutdown. Something breaks, you patch it. But regenerability can only be measured while the old model is still alive. That asymmetry decides everything about ordering.
If you run a pipeline that depends on image generation, try exactly one image today. Pick an asset, send its prompt to the new GA model, and put the two side by side. Ask yourself whether they could share a series. Work backward from that answer, and the shape of your ledger becomes obvious.
I am still mid-migration myself. What to do with those 175 frozen images — rebuild them in a future refresh, or keep them as they are — remains unsettled. But knowing precisely which 175 they are, versus not knowing, changes every move available to me next. That, more than anything, is why I am glad I built the ledger.
Thank you for reading.
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.