先週、自分の手元で恥ずかしい取りこぼしを見つけました。ある壁紙アプリの、月に一度しか走らないサムネイル生成スクリプトの中で、すでに後継へ移したはずの古い画像モデルがまだ指定されていたのです。普段触るコードは移行済みでしたが、CI も通らない単発スクリプトは誰の目にも触れず、停止日を過ぎれば静かに 404 を返すだけの状態でした。
6月は Gemini まわりの停止・廃止が立て続けに来る時期です。CLI と Code Assist の個人向け提供が止まり、画像のプレビューモデル 2 種も間もなく停止します。移行ガイドは丁寧に書かれていますが、ガイドが教えてくれるのは「何が止まるか」であって、「自分のどのファイルの何行目が止まるか」ではありません。後者を人間の記憶と grep に任せている限り、めったに触らないコードの中の指定は必ず取りこぼします。
私は技術ブログを 4 つと、iOS / Android の自作アプリをいくつか、個人開発で並行して運用しています。それぞれが少しずつ違うモデルをピン留めしているので、停止のたびに全リポジトリを手で確認するのは現実的ではありません。そこで、古いモデル指定を CI で機械的に弾く小さな仕組みを組みました。以下では、その廃止レジストリと走査スクリプトを、実際に動く形で共有します。
なぜ「移行ガイドを読む」だけでは足りないのか
移行作業を手で進めると、たいてい次の 3 つで漏れます。
ひとつ目は、停止日がばらけていることです。6/18 に止まるもの、6/25 に止まるもの、来月のものが混在すると、人間は「いちばん近い締切」しか意識に残りません。残りの締切は、その日が来てから思い出します。
ふたつ目は、参照箇所が散らばっていることです。アクティブなアプリ本体は移行しても、ドキュメントのコード例、過去のサンプル、CI を通らない運用スクリプト、.env.example のコメントなどに古いモデル ID が残ります。これらは普段のテストを通らないので、壊れたことに気づくのはユーザーからの問い合わせか、クラッシュレポートが来てからです。
みっつ目は、モデル ID が文字列であることです。型もスキーマもないただの文字列なので、コンパイラもリンタも何も言ってくれません。"gemini-3.1-flash-image-preview" と書いてあっても、それが 8 日後に消えることはコードのどこにも書かれていません。
この 3 つはすべて「機械が得意で人間が苦手な」種類の作業です。だから機械にやらせます。
まず「廃止レジストリ」を 1 ファイルにまとめる
最初にやるのは、止まるモデルの正本リストを 1 つの JSON にまとめることです。ここが唯一の真実の置き場所になります。各エントリには、モデル ID、停止日、後継、出典をひもづけます。
{
"$schema_note" : "停止予定の Gemini モデルの正本。停止日は YYYY-MM-DD(UTC基準で保守的に判定)。" ,
"deprecations" : [
{
"model" : "gemini-3.1-flash-image-preview" ,
"shutdown" : "2026-06-25" ,
"replacement" : "gemini-3.1-flash-image" ,
"note" : "プレビュー版の画像生成。GA 版へ。" ,
"source" : "ai.google.dev/gemini-api/docs/deprecations"
},
{
"model" : "gemini-3-pro-image-preview" ,
"shutdown" : "2026-06-25" ,
"replacement" : "gemini-3.1-pro-image" ,
"note" : "プレビュー版の画像生成。GA 版へ。" ,
"source" : "ai.google.dev/gemini-api/docs/deprecations"
},
{
"model" : "gemini-2.0-flash" ,
"shutdown" : "2026-09-30" ,
"replacement" : "gemini-3.5-flash" ,
"note" : "順次デフォルトが 3.5 Flash へ移行。" ,
"source" : "ai.google.dev/gemini-api/docs/changelog"
}
]
}
ここで大事なのは、停止日を必ず添えることです。後で「残り日数」で警告の強さを変えるための材料になります。停止日が分からないモデルは、暫定で近めの日付を入れて保守的に倒しておきます。レジストリは公式の廃止ページを正本とし、新しい停止が発表されたらこのファイルだけを更新します。スクリプト側には日付を直書きしません。
このレジストリは各リポジトリにコピーするのではなく、共通の場所に 1 つ置いて参照するのが理想です。私は 4 サイト分を 1 ファイルで共有しています。停止情報が増えたときに 1 箇所だけ直せば、全リポジトリの判定が同時に新しくなります。
リポジトリを走査して参照箇所を洗い出す
次に、レジストリを読み込んでリポジトリ全体を走査し、古いモデル ID が書かれた箇所を行単位で報告するスクリプトを書きます。Python だけで完結します。
#!/usr/bin/env python3
"""廃止予定の Gemini モデル指定をリポジトリから検出する。
残り日数に応じて warning(情報)か error(CIを落とす)かを切り替える。"""
import json
import os
import re
import sys
from datetime import date, datetime
# 残りこの日数を切ったら error として扱う(CIを失敗させる)
FAIL_WITHIN_DAYS = 30
# 走査対象の拡張子
SCAN_EXT = { ".py" , ".ts" , ".tsx" , ".js" , ".jsx" , ".mjs" , ".json" , ".yaml" , ".yml" , ".md" , ".mdx" , ".env" , ".sh" }
# 走査から除外するディレクトリ
SKIP_DIRS = { ".git" , "node_modules" , ".next" , "dist" , "build" , "vendor" , ".venv" }
def load_registry (path):
with open (path, encoding = "utf-8" ) as f:
data = json.load(f)
items = []
for d in data[ "deprecations" ]:
shutdown = datetime.strptime(d[ "shutdown" ], "%Y-%m- %d " ).date()
# 単語境界で囲み、'gemini-2.0-flash' が 'gemini-2.0-flash-lite' に
# 誤マッチしないようにする
pattern = re.compile( r " (?<! [\w .- ] ) " + re.escape(d[ "model" ]) + r " (?! [\w - ] ) " )
items.append({ ** d, "shutdown_date" : shutdown, "pattern" : pattern})
return items
def iter_files (root):
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS ]
for name in filenames:
ext = os.path.splitext(name)[ 1 ].lower()
if ext in SCAN_EXT or name.startswith( ".env" ):
yield os.path.join(dirpath, name)
def scan (root, registry, today):
hits = []
for path in iter_files(root):
try :
with open (path, encoding = "utf-8" , errors = "ignore" ) as f:
lines = f.readlines()
except OSError :
continue
for lineno, line in enumerate (lines, 1 ):
for item in registry:
if item[ "pattern" ].search(line):
remaining = (item[ "shutdown_date" ] - today).days
hits.append({
"path" : os.path.relpath(path, root),
"line" : lineno,
"model" : item[ "model" ],
"replacement" : item[ "replacement" ],
"remaining" : remaining,
"text" : line.strip()[: 120 ],
})
return hits
def main ():
registry_path = os.environ.get( "DEPRECATION_REGISTRY" , "deprecations.json" )
root = sys.argv[ 1 ] if len (sys.argv) > 1 else "."
today = date.today()
registry = load_registry(registry_path)
hits = scan(root, registry, today)
if not hits:
print ( "OK: 廃止予定のモデル指定は見つかりませんでした。" )
return 0
# 残り日数が少ない順に並べる
hits.sort( key =lambda h: h[ "remaining" ])
has_error = False
for h in hits:
level = "ERROR" if h[ "remaining" ] <= FAIL_WITHIN_DAYS else "WARN"
if level == "ERROR" :
has_error = True
when = f "残り { h[ 'remaining' ] } 日" if h[ "remaining" ] >= 0 else f " { abs (h[ 'remaining' ]) } 日超過"
print ( f "[ { level } ] { h[ 'path' ] } : { h[ 'line' ] } "
f " { h[ 'model' ] } → { h[ 'replacement' ] } ( { when } )" )
print ( f " { h[ 'text' ] } " )
print ( f " \n 検出 { len (hits) } 件 / error 閾値 { FAIL_WITHIN_DAYS } 日以内" )
return 1 if has_error else 0
if __name__ == "__main__" :
sys.exit(main())
走らせると、こういう出力になります。
[ERROR] scripts/make_thumbnail.py:42 gemini-3.1-flash-image-preview → gemini-3.1-flash-image (残り8日)
model="gemini-3.1-flash-image-preview",
[WARN] docs/sample.md:88 gemini-2.0-flash → gemini-3.5-flash (残り105日)
response = client.generate(model="gemini-2.0-flash", ...)
検出 2 件 / error 閾値 30 日以内
ここで効いてくるのが、冒頭でレジストリに停止日を入れておいたことです。残り 8 日のものは ERROR として CI を落とし、残り 105 日のものは WARN にとどめて気づきだけ与えます。すべてを一律に fail にしないのは、まだ猶予のある移行で開発を止めたくないからです。締切が近づくにつれて、同じ参照が自動的に WARN から ERROR に格上げされていきます。
単語境界の罠 ― ここが実装の肝です
このスクリプトでいちばん気をつけたのは、正規表現の境界です。素朴に "gemini-2.0-flash" in line と書くと、まだ生きている gemini-2.0-flash-lite まで巻き込んで誤検出します。逆に \b(単語境界)だけに頼ると、モデル ID に含まれるピリオドやハイフンの扱いがエンジンごとに揺れて、期待どおりに止まりません。
そこで、前後を「単語文字でもピリオドでもハイフンでもない」位置に限定しています。
# 前: 直前が \w . - のいずれでもない / 後: 直後が \w - のいずれでもない
pattern = re.compile( r " (?<! [\w .- ] ) " + re.escape(d[ "model" ]) + r " (?! [\w - ] ) " )
re.escape を必ずかけるのも忘れてはいけません。モデル ID にはピリオドが含まれるので、エスケープしないと . が「任意の 1 文字」として効いてしまい、これも誤検出のもとになります。私は最初これを忘れて、gemini-3x1-flash のような実在しない文字列にまでマッチして首をかしげました。
誤検出を 0 に近づけることは、この種のツールでは機能そのものより重要です。狼少年になった瞬間にチームは出力を読まなくなり、ツールは無いのと同じになります。
CI に組み込む
あとは CI から呼ぶだけです。GitHub Actions なら数行で済みます。
name : gemini-deprecation-guard
on : [ push , pull_request ]
jobs :
guard :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v4
- uses : actions/setup-python@v5
with :
python-version : "3.12"
- name : 廃止モデル検知
env :
DEPRECATION_REGISTRY : ci/deprecations.json
run : python ci/check_deprecated_models.py .
ローカルで先に弾きたければ、pre-commit のローカルフックにも同じスクリプトを刺せます。
# .pre-commit-config.yaml
- repo : local
hooks :
- id : gemini-deprecation-guard
name : Gemini 廃止モデル検知
entry : python ci/check_deprecated_models.py
language : system
pass_filenames : false
私は CI を「最後の砦」、pre-commit を「日常の気づき」として両方に入れることを推奨します。pre-commit は猶予のある WARN でも手元で見えるので、締切がまだ遠いうちに自然と直すきっかけになります。
レジストリの更新を半自動にする
レジストリを手で保守し続けると、結局そこが新しい停止情報を取りこぼします。そこで、Gemini API のモデル一覧と突き合わせて、レジストリに載っていないのに API 側で非推奨フラグが立っているモデルを洗い出す補助スクリプトも置いています。
import os
from google import genai
client = genai.Client( api_key = os.environ[ "YOUR_GEMINI_API_KEY" ])
# API が返すモデル一覧を取得し、説明文に deprecation の手がかりがあるものを拾う
for m in client.models.list():
name = m.name.split( "/" )[ - 1 ]
desc = ( getattr (m, "description" , "" ) or "" ).lower()
if "deprecat" in desc or "preview" in name:
print ( f "要確認: { name } — { desc[: 80 ] } " )
これは自動でレジストリを書き換えるためのものではなく、「人間が公式の廃止ページを見に行くべきモデル」を毎日のバッチで知らせるための気づき役です。最終的な停止日と後継の確定は、必ず公式ドキュメントを正本にします。API のメタデータだけで停止日を機械決定すると、誤った日付で CI を落としかねないからです。
API の堅牢な扱い方そのものについては、Gemini API のモデル非推奨と移行の進め方 も合わせて読むと、レジストリに載せる「後継」をどう選ぶかの判断がしやすくなります。
実際に回して見えた落とし穴
しばらく運用して気づいたことをいくつか共有します。
文字列を連結してモデル ID を組み立てているコードは、この走査では拾えません。"gemini-3.1-" + variant のような書き方は静的検査の死角です。対処として、モデル ID は必ず 1 つの定数ファイルに集約し、各所からはその定数を参照する規約にしました。走査対象が定数ファイル 1 枚に絞れるので、死角が消えます。
ドキュメントやブログ記事の中の古いモデル ID も、容赦なくヒットします。これは正しい挙動です。読者がコピーして動かなくなるくらいなら、記事側も直すべきだからです。ただし「これは歴史的な記録なので残す」と判断した箇所には、行末に # deprecation-guard: allow のような印を付けてスキップできるようにしておくと運用が楽になります。許可リストは小さく保ち、付けた理由を必ずコメントで残すのが私の決めごとです。
停止日の判定は UTC とローカルのどちらで切るかで 1 日ずれます。私は保守的に、UTC で停止日の前日には ERROR に上がるよう、閾値に少し余裕を持たせています。締切ぎりぎりで時差に泣くより、1 日早く直すほうが精神衛生上もよほど健全でした。
まず 1 リポジトリで動かしてみる
仕組み自体は小さいので、いきなり全リポジトリに広げる必要はありません。まずは停止が近いモデルを 1 つだけレジストリに書き、いちばん移行が不安なリポジトリのルートで python check_deprecated_models.py . を 1 回走らせてみてください。自分でも忘れていた参照が 1 件でも出てくれば、この仕組みは元を取ります。そこから対象拡張子とレジストリを少しずつ太らせていけば十分です。
私自身、最初に走らせたときに冒頭の単発スクリプトが引っかかって、ようやく安心して停止日を迎えられました。締切に追われる移行を、締切に追われない移行に変えるための小さな投資として、同じ課題を持つ方の役に立てば嬉しいです。