壁紙アプリのカテゴリ分類を任せているモデルの名前を、久しぶりに書き換えた朝のことでした。手が滑って古い ID を残したまま走らせてしまい、分類がまるごと空で返ってきたのです。
原因そのものは一分で分かりました。けれど、そのついでに開いた Gemini の廃止表で、手が止まりました。私が移行先にしようとしていたモデルの横に、すでに過ぎた日付が並んでいたのです。
推奨代替の欄は、いつでも生きているモデルを指しているものだと思い込んでおりました。そう読んでいたのは私だけではないかもしれません。その思い込みが、今回いちばん高くついた部分です。
停止日が書かれている行は、思ったより少ないのです
Gemini deprecations のページを、表として機械的に読み直しました。2026-09-15 更新版に載っている 74 行を、本日(2026-09-17)を基準に分類した結果が次の表です。
区分 行数 意味
停止日を経過済み 39 すでに呼べません
停止日が未来 4 日付で備えられます
停止日が未告知 31 日付で備えられません
未来の停止日を持つ行は、74 行のうち 4 行だけでした。内訳は次のとおりです。
モデル 停止日 本日からの残り
gemini-omni-flash-preview2026-09-30 13 日
gemini-2.5-flash-image2026-10-02 15 日
gemini-3.1-flash-lite2027-05-07 232 日
gemini-embedding-0012028-05-14 605 日
ここで、よく聞く言い回しを一つ訂正しておきます。「2.5 系は 10 月にまとめて消える」という説明を見かけますが、本日の表では gemini-2.5-flash・gemini-2.5-pro・gemini-2.5-flash-lite のいずれも「停止日の告知なし」です。10 月 2 日という日付が付いているのは画像生成の gemini-2.5-flash-image だけでした。
つまり、日付で身構えられる範囲は私たちが思っているよりずっと狭く、大半のモデルは「いつか」としか書かれていない のです。カレンダーに入れて安心するやり方が効くのは、74 行のうちの 4 行に対してだけでした。
推奨代替の欄を、そのまま写すと消えたモデルへ移ります
同じ表で、もう一段だけ踏み込みました。「推奨代替」に書かれたモデル ID を、同じ表の中で引き直して、その代替自身に停止日が付いていないかを見たのです。
結果は 5 件でした。推奨代替として案内されているモデルが、それ自体すでに停止日を過ぎている行 が 5 つあります。
元のモデル その停止日 推奨代替 代替の停止日
gemini-2.5-flash-image2026-10-02 gemini-3.1-flash-image-preview2026-06-25(経過済み)
imagen-3.0-generate-0022025-11-10 imagen-4.0-generate-0012026-08-17(経過済み)
imagen-4.0-generate-preview-06-062026-02-17 imagen-4.0-generate-0012026-08-17(経過済み)
imagen-4.0-ultra-generate-preview-06-062026-02-17 imagen-4.0-ultra-generate-0012026-08-17(経過済み)
gemini-robotics-er-1.5-preview2026-04-30 gemini-robotics-er-1.6-preview2026-08-31(経過済み)
一行目に注目していただきたいのです。15 日後に消える gemini-2.5-flash-image の移行先として案内されているのは gemini-3.1-flash-image-preview ですが、その preview 版はすでに 6 月 25 日で止まっております。表の推奨欄を素直に書き写した人は、消えたモデルへ移ることになります。
実際に生きている移り先は、preview が取れた GA 版の gemini-3.1-flash-image です。表の別の行にそう書いてあります——ただし、それは gemini-2.5-flash-image の行を見ているだけでは分かりません。
さらに、推奨代替そのものに未来の停止日が設定されている行が 6 件ありました。移った先も期限付きだった、という意味です。gemini-2.5-flash-image-preview の移行先が gemini-2.5-flash-image(10 月 2 日停止)である行などが、それにあたります。
推奨代替が表のどこにも存在しない行も 1 件ありました。gemini-robotics-er-1.6-preview の移行先として書かれた gemini-robotics-er-2-preview は、この表に行がありません。
推奨代替は「次にどこへ向かうか」の案内であって、「いま生きているか」の保証ではありません。 この区別がないまま移行計画を立てると、廃止のたびに同じ作業を二度することになります。
突き合わせを手でやらないための小さな監査スクリプト
一度気づいてしまえば、あとは毎回やるだけです。けれど毎回 74 行を目で追うのは続きません。私は自分の運用スクリプトに次の 40 行ほどを足しました。
使っているモデル ID を一行ずつ書いた models_in_use.txt と、廃止表から写した TSV を突き合わせて、危ない行だけを出します。
#!/usr/bin/env python3
"""使用中のモデル ID を、Gemini 廃止表と突き合わせて警告する。
deprecations.tsv の形式(タブ区切り・ヘッダなし):
model<TAB>shutdown(YYYY-MM-DD または空)<TAB>recommended_replacement(空可)
exit code は 0 = 問題なし / 1 = 要対応。CI に置くとそのまま落ちます。
"""
import sys
from datetime import date, timedelta
WARN_WINDOW = 90 # 何日先までを「まもなく」とみなすか
def load_table (path):
table = {}
with open (path, encoding = "utf-8" ) as f:
for line in f:
row = line.rstrip( " \n " ).split( " \t " )
if len (row) < 3 or not row[ 0 ].strip():
continue
model, shutdown, replacement = (c.strip() for c in row[: 3 ])
table[model] = (
date.fromisoformat(shutdown) if shutdown else None ,
replacement or None ,
)
return table
def audit (in_use, table, today):
"""戻り値は (深刻度, モデル, 一行メッセージ) のリスト。"""
findings = []
for model in in_use:
if model not in table:
# 表にない = 廃止対象として告知されていない、か、綴り間違い。
findings.append(( "WARN" , model, "廃止表に行がありません。ID の綴りを確認してください" ))
continue
shutdown, replacement = table[model]
if shutdown and shutdown <= today:
findings.append(( "FAIL" , model, f "停止日 { shutdown } を経過しています" ))
elif shutdown and shutdown - today <= timedelta( days = WARN_WINDOW ):
findings.append(( "FAIL" , model, f "停止日 { shutdown } (残り { (shutdown - today).days } 日)" ))
# ここが今回の本題。移行先そのものが生きているかを見る。
if replacement:
if replacement not in table:
findings.append(( "WARN" , model, f "推奨代替 { replacement } が廃止表にありません" ))
else :
rep_shutdown = table[replacement][ 0 ]
if rep_shutdown and rep_shutdown <= today:
findings.append(( "FAIL" , model, f "推奨代替 { replacement } は { rep_shutdown } に停止済みです" ))
elif rep_shutdown:
findings.append(( "WARN" , model, f "推奨代替 { replacement } にも停止日 { rep_shutdown } があります" ))
return findings
def main ():
table = load_table(sys.argv[ 1 ])
with open (sys.argv[ 2 ], encoding = "utf-8" ) as f:
in_use = [l.strip() for l in f if l.strip() and not l.startswith( "#" )]
findings = audit(in_use, table, date.today())
for level, model, message in findings:
print ( f " { level }\t{ model }\t{ message } " )
if not findings:
print ( "OK \t - \t 使用中のモデルに未対応の廃止はありません" )
sys.exit( 1 if any (l == "FAIL" for l, _, _ in findings) else 0 )
if __name__ == "__main__" :
main()
置き方は次の四手順です。
廃止表のページを開き、使っている系統の行をタブ区切りで deprecations.tsv に写します(本日時点で 74 行、うち写す価値があるのは自分が触る系統だけです)。
アプリとサーバのコードから grep -rhoE 'gemini-[0-9][a-z0-9.-]*' でモデル ID を抜き、重複を落として models_in_use.txt にします。
上のスクリプトを python3 audit_models.py deprecations.tsv models_in_use.txt の形で走らせます。
CI の週次ジョブに同じ一行を置き、exit code 1 で落ちるようにします。表の更新に気づく手段が、これで一つできます。
gemini-2.5-flash-image を一行だけ書いたファイルで走らせると、本日の日付ではこう出ます。
FAIL gemini-2.5-flash-image 停止日 2026-10-02(残り 15 日)
FAIL gemini-2.5-flash-image 推奨代替 gemini-3.1-flash-image-preview は 2026-06-25 に停止済みです
二行目が、目視では抜けていた行です。audit() を関数に切り出してあるのは、モデル一覧を API から取る作りに差し替えても、判定のロジックをそのまま使えるようにしておきたかったからでした。
なぜ「表にない ID」を FAIL ではなく WARN にしているのかも書き添えておきます。廃止表は廃止されたものしか載せませんので、現役のモデルは載っていないのが正常です——ここを FAIL にすると、毎日鳴り続ける警告になってしまいます。
停止日は「最も早い可能性のある日」であって、約束ではありません
表を読むときに見落としやすい注記が、ページの冒頭にあります。表に載っている停止日は「その日に止まる」ではなく、最も早い可能性のある日 だと明記されているのです。正確な停止日は事前に別途案内される、とも書かれています。
これを読んで、私は当初「では実際にはもっと先まで使えるのだろう」と受け取りました。**その受け取り方が、いちばん危ない読み方でした。**そう気づいたのは、フォーラムの報告をいくつか読んだあとのことでした。
開発者フォーラムには、告知された停止日より前に 404 が返るようになった、という報告が複数あります。gemini-2.5-flash と gemini-2.5-flash-lite が 7 月 9 日に「no longer available」を返し始めた件 や、gemini-2.5-pro が「新規ユーザーには提供しない」と返す件 が、その例です。
つまり日付は、遅れる側にも早まる側にも外れます。日付の前に静かに壊れることがあり、日付を過ぎても動き続けることがある——どちらに転んでも、日付だけを頼りにした計画は当てが外れます。
だから私は、移行の判断基準を日付から動作へ移しました。カレンダーに入れるのはやめませんが、それは作業を思い出すためであって、アプリを守るためではありません。アプリを守るのは、404 が返ってきたときの振る舞いのほうです。
アプリに焼くのは論理名だけにします
Rork で作ったアプリから直接 Gemini を呼んでいる場合、モデル ID はクライアントのコードの中にあります。そしてストアに出したあとのバイナリは、私の手ではもう書き換えられません。
ですので、アプリ側には「何をしたいか」だけを持たせて、「どのモデルを使うか」はサーバ側で解決するようにしております。API キーをアプリの外に出す話と同じ形で、モデル ID も外に出す、という整理です(この置き場所の考え方は そのAPIキー、アプリの中に入っていませんか — Rork の環境変数と Supabase Edge Function の使い分け に書きました)。
サーバ側は、論理名から候補列を返すだけの小さなものです。Cloudflare Workers でも Supabase Edge Function でも同じ形で書けます。
// 論理名 -> 実モデル ID の候補列。上から順に試します。
// ここだけを書き換えれば、出荷済みのアプリの挙動も変わります。
const MODEL_CHAINS : Record < string , string []> = {
"image.generate" : [
"gemini-3.1-flash-image" , // GA 版。廃止表に停止日なし(2026-09-17 時点)
"gemini-2.5-flash-image" , // 2026-10-02 停止。過渡期の保険としてのみ残します
],
"vision.classify" : [ "gemini-3.8-flash" , "gemini-3.6-flash" ],
};
type Resolved = { model : string ; chain : string [] };
export function resolveModel ( logical : string ) : Resolved {
const chain = MODEL_CHAINS [logical];
if ( ! chain || chain. length === 0 ) {
// 未知の論理名は握りつぶしません。設定ミスを本番で静かに通さないためです。
throw new Error ( `unknown logical model: ${ logical }` );
}
return { model: chain[ 0 ], chain };
}
export async function callWithFallback (
logical : string ,
body : unknown ,
apiKey : string ,
) : Promise < Response > {
const { chain } = resolveModel (logical);
let lastGone : Response | null = null ;
for ( const model of chain) {
const res = await fetch (
`https://generativelanguage.googleapis.com/v1beta/models/${ model }:generateContent` ,
{
method: "POST" ,
headers: { "content-type" : "application/json" , "x-goog-api-key" : apiKey },
body: JSON . stringify (body),
},
);
// 404 は「このモデルはもう無い」。次の候補へ落とします。
if (res.status === 404 ) {
lastGone = res;
console. warn ( `model gone: ${ model } (logical=${ logical })` );
continue ;
}
// それ以外は成功も失敗もそのまま返します。429/5xx はリトライ層の仕事です。
return res;
}
// 候補が尽きた = 設定の更新が追いついていない、という運用の失敗です。
console. error ( `all models gone for logical=${ logical }, chain=${ chain . join ( "," ) }` );
return lastGone ?? new Response ( "no model available" , { status: 503 });
}
候補列を配列にしているのは、停止日をまたぐ数日だけ二段構えにしたいからです。新しい GA 版を先頭に置き、古いほうを後ろに残しておけば、どちらかが先に落ちても画面は止まりません。そして落ち着いたら、古いほうの行を一つ消すだけで片付きます。
ログに model gone を出しているのは、この二段目が使われた事実を後から数えたいからでした。二段目が使われ始めた日が、実際の停止日です——表の日付ではなく。
404 はリトライしてはいけない種類のエラーです
ここでもう一つ、私が最初のうち混ぜてしまっていた線引きの話を書き残します。
ネットワーク越しの失敗は、リトライすれば直るものと、何度やっても直らないものに分かれます。廃止による 404 は後者です。にもかかわらず、私は当初これを汎用のリトライ層に流し込んでおりました。結果は芳しくありませんでした——指数バックオフが律儀に三回粘り、ユーザーは三倍長く待たされたうえで、同じ失敗を受け取ったのです。
いまは呼び出し側で、失敗を三つに分けています。
種類 代表的な応答 振る舞い
一時的 429 / 503 / タイムアウト バックオフして再試行します
恒久的(廃止) 404 / NOT_FOUND / no longer available 再試行せず、代替経路へ移ります
こちらの誤り 400 / 403 再試行せず、実装を直します
アプリ側のコードは、これだけです。
type Outcome =
| { kind : "ok" ; data : unknown }
| { kind : "retry" } // 呼び出し元がバックオフして再試行します
| { kind : "unavailable" } // 恒久的。ユーザーに代わりの手段を出します
| { kind : "bug" ; detail : string }; // 開発中に気づくべき誤りです
export async function askAi ( logical : string , payload : unknown ) : Promise < Outcome > {
const res = await fetch ( "/api/ai" , {
method: "POST" ,
headers: { "content-type" : "application/json" },
body: JSON . stringify ({ logical, payload }),
});
if (res.ok) return { kind: "ok" , data: await res. json () };
if (res.status === 429 || res.status >= 500 ) return { kind: "retry" };
if (res.status === 404 ) return { kind: "unavailable" };
return { kind: "bug" , detail: `${ res . status } ${ await res . text () }` };
}
unavailable のとき、画面に何を出すかは機能によって変えております。画像生成なら、その回だけ生成ボタンを隠して手動アップロードに寄せます。分類なら、前回の分類結果をそのまま見せて「更新できませんでした」と添えます。
やってはいけないのは、無言で空を返すことです。冒頭で私が踏んだのがまさにこれで、分類が空で返ってきても画面上は何事もなく、原因に気づくまでのあいだ、空のカテゴリが配られておりました。似た形の静かな失敗は OS の非推奨 API でも起きます(iOS 27 の新機能を Rork に頼むと、エラーにならないまま古い書き方へ戻ります にその例をまとめております)。
出荷済みのバージョンには、もう手が届きません
モデル ID をサーバへ出す設計に変えても、すでに配ってしまったバージョンは古いままです。ここだけは、あとから直せません。
個人開発でアプリを長く運用していますと、古いバージョンが思った以上に長く残ることが身に染みます。自動更新を切っている方、端末を買い替えていない方、通信量を気にして更新を溜めている方——どのアプリにも一定の割合でいらっしゃいます。
ですから、モデル ID をクライアントに焼いたまま出してしまったバージョンについては、次の三つのうちどれかを選ぶことになります。ひとつはサーバ側に同じ ID のプロキシ経路を残して延命すること、ひとつは強制更新の導線を出すこと、そしてもうひとつは、その機能だけを機能フラグで畳むことです。
私は三つ目を基本にしております。強制更新は最後の手段に取っておきたく、延命は「いつまで」を決めないと延び続けるからです。
モデル ID はアプリの外に置き、停止日ではなく 404 に備えます。 この一文だけは、次に新しいモデルへ乗り換えるときにも、先に思い出すようにしています。
なお、この作りにするとサーバ側の呼び出し回数が可視化され、費用の見え方も変わります。実行時の従量課金をどう切り分けるかは 思ったより早く残高が減る前に、Build クレジットと Cloud クレジットを分けて見る に分けて書きました。
今日やっておくこと
一つだけ選ぶとすれば、いま使っているモデル ID を一行ずつ書き出して、上の監査スクリプトに通してみることをお勧めします。10 分あれば終わります。
そこで FAIL が二行出た方は、私と同じところに立っています——停止日の行と、推奨代替の行の、両方です。
お読みいただきありがとうございました。移行の途中で見つかった「消えた移行先」があれば、私も同じ表を追いかけておりますので、どこかで共有していただけましたら嬉しく思います。