●ROBOTICS — The ER 1.6 preview that shut down on August 31 does have a successor. Gemini Robotics ER 2 is in public preview, in both standard and streaming variants●VIDEO — ER 2 judges success and failure from live video rather than still snapshots, which is what lets it catch spills, slips, and misalignments while a task is still running●DEADLINE — Next up is September 30, when gemini-omni-flash-preview is retired. The target is gemini-omni-1.1-flash, GA since August 27, and there are now under four weeks left●APIKEY — Every remaining standard API key, restricted ones included, stops working during September. The replacement is an auth key bound to a Google Cloud service account●PRICE — Gemini 3.7 Flash keeps its introductory $0.75/$3.75 per 1M through December 31, then moves to $1.50/$7.50 on January 1, 2027. Any estimate crossing the year needs both figures●AUDIO — Gemini 3.5 Transcribe handles language detection across 85+ languages, speaker diarization, word-level timestamps, and custom vocabulary biasing of up to 1,000 terms●ROBOTICS — The ER 1.6 preview that shut down on August 31 does have a successor. Gemini Robotics ER 2 is in public preview, in both standard and streaming variants●VIDEO — ER 2 judges success and failure from live video rather than still snapshots, which is what lets it catch spills, slips, and misalignments while a task is still running●DEADLINE — Next up is September 30, when gemini-omni-flash-preview is retired. The target is gemini-omni-1.1-flash, GA since August 27, and there are now under four weeks left●APIKEY — Every remaining standard API key, restricted ones included, stops working during September. The replacement is an auth key bound to a Google Cloud service account●PRICE — Gemini 3.7 Flash keeps its introductory $0.75/$3.75 per 1M through December 31, then moves to $1.50/$7.50 on January 1, 2027. Any estimate crossing the year needs both figures●AUDIO — Gemini 3.5 Transcribe handles language detection across 85+ languages, speaker diarization, word-level timestamps, and custom vocabulary biasing of up to 1,000 terms
An agreement-rate gate approves the model swap, then your category shares move
Swapping the model behind an image classification batch can pass a golden-set agreement gate while category shares quietly shift. I measured the sample sizes each check really needs and rebuilt the gate as a paired comparison.
The week after I promoted a new classification model in my wallpaper app, the categories sitting near the top of the browse list had shifted a little.
No individual label was wrong. Looking at images one by one, every assignment was defensible. Yet the landscape shelf had thickened, and the small categories at the bottom had thinned out.
The promotion decision had been made with an agreement rate against a golden set — the share of images where the new model produced the same label as the old one. That check passed. It passed, and the shares moved anyway.
What follows is how I pinned down the reason with numbers, and how I rebuilt the gate so the shift shows up before promotion rather than after. The statistical figures come from a simulation I ran locally over a skewed 30-category distribution built to resemble the real one. The operational problem is from the classification batch I run for a wallpaper app I develop on my own. It ships on both the App Store and Google Play, and the browse ordering on each is assembled from the same classification output, so checking only one side proves nothing — an assumption that shapes everything below.
What a 92% agreement rate was hiding
I started by confirming that agreement and composition are genuinely different quantities.
Thirty categories, with a Zipf-like skew (19.8% for the top one, 11.1% second, 7.8% third, 32.7% for the bottom twenty combined). On top of that I imposed a specific confusion pattern: the new model returns the same label 92% of the time, and of the 8% that disagree, roughly 70% land in one of the top three categories. Errors are not scattered uniformly — they get absorbed by the dominant categories. Running 200,000 items gave this:
Category
Old model
New model
Relative change
1st
19.78%
21.18%
+7.1%
2nd
11.06%
11.87%
+7.3%
3rd
7.80%
8.40%
+7.7%
4th
6.13%
5.71%
-6.9%
Bottom 20 combined
32.73%
31.72%
-3.1%
Effective agreement came out at 91.9%, with a total variation distance of 2.81%.
A gate that only reads agreement will wave this through if the threshold sits at 90%. Meanwhile the top categories have gained 7% and the bottom twenty have lost 3% between them. A few percent is exactly the range where browse ordering and the "new arrivals" shelf start to look different.
Agreement counts how many labels changed. Composition depends on where the changed ones went. Obvious once stated, but folding both into a single gate is how the distinction got lost.
Watching production output finds it far too late
The natural follow-up is to monitor the output distribution after promotion. This is where my expectation was furthest off.
Assume a chi-square goodness-of-fit test over the 30 shares (29 degrees of freedom, 5% significance, critical value 42.557). Against the sub-2% TVD drift above, the power came out like this:
Items used for the decision
Power
500
9.5%
1,200
19.2%
2,000
39.8%
4,000
79.2%
For a batch handling a few hundred images a day, accumulating 4,000 takes one to two weeks. Throughout that window power sits near 20%, which means most of the "nothing to report" readouts were reporting a shortage of data rather than an absence of drift.
I assumed narrowing to a single category would fix it, so I measured that too:
Items
Top category (19.78%→21.18%)
Third category (7.80%→8.40%)
1,800
31.4%
16.7%
3,000
47.6%
24.0%
6,000
76.8%
41.8%
15,000
—
76.3%
Better than the omnibus test, but the order of magnitude does not change. Watching a 7.8% category takes 15,000 items.
The reason lies in where the variance comes from. When you observe the production distribution, most of the wobble comes from which images arrived that day. The model's contribution is a thin layer on top of that, buried under input variation, and the only lever you have is piling up more items.
✦
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 confirm that category shares have not moved before you promote a new classification model, even when the agreement gate is green
✦You will be able to choose between watching production output and running a paired new-versus-old comparison, based on the sample size each one actually requires
✦You will understand why a 30-category goodness-of-fit test still reaches only 79% power at 4,000 items, and redesign your monitoring around test granularity
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.
Feeding identical input to both models cuts the requirement fivefold
If input variation is the problem, hold the input fixed. Send the same image to both models, and for each image subtract "was this the old label" from "is this the new label." The value is -1, 0, or +1, and the 92% that agree all contribute exactly 0.
Only the disagreeing images generate variance, and the input skew cancels in the subtraction. Measuring power again against the same drift:
Items
Paired (identical input)
Unpaired (production observation)
500
40.5%
—
800
58.8%
—
1,200
74.7%
about 25%
6,000
—
76.8%
Catching the top-category shift with 75% probability takes 6,000 items by production observation and 1,200 by paired comparison — roughly a fifth. Even the third category at 7.8% reaches 64.9% power at 2,500 items.
At 300 images a day, the paired check needs four days' worth of images. What had been three weeks of watching after promotion became four days before it.
Implementing the promotion check
Here is the shape I actually use, with dependencies stripped back. The classification call is the same one the normal batch makes; the only difference is sending each image twice and lining the answers up.
import os, json, mathfrom collections import Counterfrom google import genaifrom google.genai import typesCATEGORIES = ["landscape", "night-sky", "flower", "animal", "abstract"] # 30 in practiceOLD_MODEL = "gemini-3.5-flash"NEW_MODEL = "gemini-3.7-flash"client = genai.Client(api_key=os.environ.get("GEMINI_API_KEY", "YOUR_API_KEY"))SCHEMA = types.Schema( type=types.Type.OBJECT, required=["category"], properties={"category": types.Schema(type=types.Type.STRING, enum=CATEGORIES)},)def classify(model: str, image_bytes: bytes) -> str | None: """One image, one category. Failures return None and the image forms no pair.""" try: res = client.models.generate_content( model=model, contents=[ types.Part.from_bytes(data=image_bytes, mime_type="image/jpeg"), "Choose exactly one category that best fits this wallpaper.", ], config=types.GenerateContentConfig( response_mime_type="application/json", response_schema=SCHEMA, ), ) return json.loads(res.text)["category"] except Exception as exc: # a half-successful pair is unusable, so drop it print(f"[warn] {model}: {type(exc).__name__}: {exc}") return None
And the decision side — a two-sided test on the mean paired difference, run per category.
Z_975 = 1.959964 # two-sided 5%def paired_report(pairs: list[tuple[str, str]]) -> list[dict]: """pairs holds (old_label, new_label). Drop unpaired images before calling.""" n = len(pairs) if n < 200: raise ValueError(f"{n} pairs is not enough to decide (200 minimum)") old_share = Counter(o for o, _ in pairs) rows = [] for cat in CATEGORIES: d = [(1 if nw == cat else 0) - (1 if od == cat else 0) for od, nw in pairs] mean = sum(d) / n var = sum((x - mean) ** 2 for x in d) / (n - 1) se = math.sqrt(var / n) if var > 0 else 0.0 base = old_share[cat] / n rows.append({ "category": cat, "old_share": base, "delta": mean, # absolute change in share "relative": mean / base if base else 0.0, "significant": se > 0 and abs(mean) / se > Z_975, "ci_half_width": Z_975 * se, }) return rows
The var > 0 branch turned out to be necessary, and it was the first trap I hit. In small categories neither model may pick the label even once, every difference is 0, and the standard error collapses to zero, taking the division with it as a ZeroDivisionError. The workaround is to pass the row with significant set to False instead of raising — the right reading, since no difference was observed.
One more thing surfaced only in production: how to treat images that failed to form a pair. Counting an image where only one model returned a 500 as "no change" dilutes the difference on days with more failures. Unpaired images are now excluded from the decision, and if the exclusion rate passes 5% the whole decision is deferred to the next day.
Two thresholds, one relative and one absolute
Stopping on significance alone means that as sample size grows you start catching shifts too small to matter. In practice I use two layers on top of it:
The difference is significant (significant from the harness above)
The relative change exceeds 5% (abs(relative) > 0.05)
That change amounts to at least 30 images per day (abs(delta) * daily_volume >= 30)
The third condition exists because an 8% relative move in a category holding 1.1% of the catalog shifts only a handful of images a day, and nothing visible changes. Back when the relative threshold stood alone, noise in small categories turned the gate red regularly, and each time I looked at it by hand and concluded it was fine. When the judgment is the same every time, it belongs in the condition.
Where to draw the stop line is a product question, not a statistical one. Not delegating that part to the test is, I think, why this version survived contact with the actual schedule.
If you are processing over ten thousand images a day, production observation reaches usable power within days, and building the paired harness may not repay the effort. At a few hundred to a few thousand images, with the promotion call resting on one person, I would run the paired comparison the day before the switch. Reversing a decision costs far less before promotion than after it.
What I removed when this went into production
I kept monitoring the production output distribution in parallel at first, then dropped it. Once the power figures were in front of me, a green lamp there carried no information, and a monitor that cannot fail is more dangerous than no monitor at all.
What stayed is a small step that writes the 1,200 pairs used for the decision, the timestamp, and both model IDs into a single file. It lets me reconstruct which way I went on a given day. The same check runs every time a model generation moves, so keeping the image set reusable gives successive decisions a common basis for comparison.
If an agreement rate is currently deciding your model swaps, send 500 images through both models at the next changeover and print the per-category share difference. Even at 92% agreement, a 7% move in the top categories will be visible right there.
I spent a long time watching whether the labels were correct, and missed that the distribution of correct labels had changed. If this saves someone the same detour, that is enough.
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.