GEMINI LABJP
VIDEO — Agentic video understanding reached 3.7 Flash, 3.6 Flash, and 3.5 Flash-Lite on September 1. The model navigates the timeline itself rather than sampling frames at a fixed rateTOKENS — Because it pulls transcripts, frames, or audio only when it needs them, Google measures up to 88% fewer tokens on long-form contentSCOPE — It works across both the Interactions and GenerateContent APIs. If you have costed out long-video work before, the assumptions have movedMUSIC — Lyria 3.5 entered public preview on September 3, generating full-length songs at 44.1 kHz stereoCONTROL — Lyria 3.5 accepts text and image inputs, with better musical coherence, more natural vocals, and finer control over duration and structureROBOTICS — gemini-robotics-er-2-streaming-preview is tuned for real-time streaming over the Live API, with function calling that blocks on physical robot actionsVIDEO — Agentic video understanding reached 3.7 Flash, 3.6 Flash, and 3.5 Flash-Lite on September 1. The model navigates the timeline itself rather than sampling frames at a fixed rateTOKENS — Because it pulls transcripts, frames, or audio only when it needs them, Google measures up to 88% fewer tokens on long-form contentSCOPE — It works across both the Interactions and GenerateContent APIs. If you have costed out long-video work before, the assumptions have movedMUSIC — Lyria 3.5 entered public preview on September 3, generating full-length songs at 44.1 kHz stereoCONTROL — Lyria 3.5 accepts text and image inputs, with better musical coherence, more natural vocals, and finer control over duration and structureROBOTICS — gemini-robotics-er-2-streaming-preview is tuned for real-time streaming over the Live API, with function calling that blocks on physical robot actions
Articles/Dev Tools
Dev Tools/2026-08-30Advanced

Why Shipped Clients Deserve a Refusal, Not a Silent Model Substitution

A model can retire, but the apps already on people's phones cannot. This is how I built a sunset ledger keyed on output contracts, and how I now back-date my own deadline from the version residue curve.

Gemini API234model deprecation3indie development22release engineeringHTTP

Premium Article

The crash report pointed at a fifteen-line function that turns a category name into a screen label. Not the network layer. Not the response decoder. The very last hop before rendering.

The cause was nowhere near that function. Our gateway had been quietly rewriting a retired model ID to its successor, and the successor had grown two extra values in its category enum.

Rewriting felt like the kind thing to do. Better to keep serving traffic than to hand a 410 to an app someone already installed. And it did keep serving traffic — that is exactly why nobody noticed.

"Mostly working" is the hardest state to detect

The shipped client recognized six categories. The successor returned eight. The two new ones show up in only a slice of real traffic. Here is the distribution, reproduced locally.

import json, random
 
V12 = {"nature","abstract","city","animal","art","minimal"}   # what the shipped app knows
V20 = list(V12) + ["japanese","texture"]                       # what the successor returns
 
def client_v12_parse(body):
    obj  = json.loads(body)                                    # layer 1: transport + JSON
    text = obj["candidates"][0]["content"]["parts"][0]["text"]
    rec  = json.loads(text)                                    # layer 2: structured output
    if rec["category"] not in V12:                             # layer 3: enum -> view model
        raise ValueError("unknown category: %s" % rec["category"])
    return rec["category"]
 
def make_response(category):
    inner = json.dumps({"category": category, "confidence": 0.9})
    return json.dumps({"candidates":[{"content":{"parts":[{"text": inner}]}}]})
 
random.seed(20260830)
sample = [random.choices(V20, weights=[22,18,14,12,10,10,9,5])[0] for _ in range(1000)]
 
ok = enum_err = 0
for c in sample:
    try:
        client_v12_parse(make_response(c)); ok += 1
    except ValueError:
        enum_err += 1
print(ok, enum_err)      # -> 849 151

849 of 1,000 succeed. Roughly 15% fail.

A total outage would have been easier to handle. At 15%, users experience it as "sometimes it just doesn't categorize." On my side, suspicion falls on whatever shipped most recently. What actually changed was a server-side rewrite rule, and not one line of app code had moved.

The model ID never appears where the exception does

The second problem is where the failure surfaces. Drop the same code and look at the frames.

import traceback, sys
try:
    client_v12_parse(make_response("japanese"))
except Exception:
    for i, f in enumerate(traceback.extract_tb(sys.exc_info()[2])):
        print("frame%d: %s() line %d" % (i, f.name, f.lineno))
# frame0: <module>()          line 24
# frame1: client_v12_parse()  line 12

In a real app there is a view-model builder in between, so the reported frame sits even closer to the UI. Along every path, the model ID you sent is absent from the stack.

Automated crash clustering does not rescue you here. The stack trace, as a string, carries no trace of the fact that a retired model was still being requested. When the cause sits on the API side, the reported location is always somewhere far away.

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 decide when a successor model may be swapped in silently based on the output contract rather than the model lineage, so you never inherit a failure whose cause is invisible after the sunset date
You will be able to back-date your own shipping deadline from a published sunset date, accounting for how slowly updates actually reach installed apps
You will be able to explain why silent substitution stays hidden, using a measurement where 849 of 1,000 responses pass through unharmed
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.

or
Unlock all articles with Membership →
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.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

Related Articles

Dev Tools2026-08-24
Gemini quietly drops %1$s from translations, and no reviewer catches it
Format specifiers go missing, turn full-width, or get duplicated when Gemini translates app strings. Here are the four failure modes I keep seeing in production, how to stop them at generation time, and a short check that catches the rest.
Dev Tools2026-08-22
The One Call I Refuse to Hand Gemini During a Phased Release
Day one of a phased release has nowhere near the sample size to tell a healthy build from a broken one. Here is the three-state rollout gate I use, and the narrow job I give Gemini inside it.
Dev Tools2026-08-17
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.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links