「一部の画像生成モデルが8月17日で提供終了」という告知を見たとき、私が最初にしたのは移行先を選ぶことではありませんでした。手を止めて考えたのは、そもそも自分のどのコードが、いつ、どのモデルを呼んでいるのか、正確には把握できていない、という事実のほうでした。
個人開発で壁紙アプリ向けの画像を生成するパイプラインをいくつか回していると、モデル ID は思っている以上にあちこちに散らばります。数か月前に書いた検証用スクリプト、月末だけ動くバッチ、設定ファイルに埋めた文字列。移行先の GA モデルを1つ決めても、呼び出し箇所を取りこぼせば、8月17日の朝に止まるのは「気づいていなかったその1本」です。だから移行の前に、まず棚卸しをしました。その手順を共有します。
grep だけでは漏れる — 呼び出し箇所の3つの盲点
最初に思いつくのは、リポジトリ全体を対象にモデル名で検索することです。これは有効ですが、完全ではありません。ソースコードの文字列検索だけを頼りにすると、次の3つが抜け落ちます。
ひとつめは、モデル ID を環境変数や設定ファイル、データベースから読み込んでいる箇所です。コードには os.environ["IMAGE_MODEL"] としか書かれておらず、実際の値はデプロイ環境にしか存在しません。ふたつめは、クライアントから引数で渡されるケースです。API のラッパーを作っていると、モデル名を呼び出し側から受け取る設計になっていることがあり、静的解析では実際にどの値が来ているか分かりません。みっつめは、すでに動いていないと思い込んでいる古いジョブです。無効化したつもりのスケジュールが実は生きていた、というのは廃止対応でいちばん怖いパターンです。
つまり、静的なコード検索は「呼んでいる可能性のある場所」を教えてくれますが、「実際にいま呼ばれているか」までは教えてくれません。この2つを別々に集める、というのがこの記事の要点です。
実行ログから「実際に呼ばれているモデル」を集計する
静的な grep の弱点を埋めてくれるのが、リクエストログです。API 呼び出しのたびにモデル ID とタイムスタンプを記録していれば、そこから「どのモデルが、直近いつ、何回呼ばれたか」を機械的に集計できます。まだログを取っていない場合でも、呼び出しを一枚のラッパーに通しているなら、そこに1行足すだけで今日から記録を始められます。
まず、既存の呼び出しをラップして最小限の記録を残す例です。
import json, time, pathlib
LOG_PATH = pathlib.Path("logs/model_calls.jsonl")
LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
def call_image_model(client, model_id: str, prompt: str, *, caller: str):
"""画像生成呼び出しのラッパー。model_id と呼び出し元を記録してから実行する。"""
record = {
"ts": time.time(),
"model": model_id,
"caller": caller, # 例: "wallpaper_batch.generate" のような呼び出し元の識別子
}
with LOG_PATH.open("a", encoding="utf-8") as f:
f.write(json.dumps(record, ensure_ascii=False) + "\n")
return client.models.generate_images(model=model_id, prompt=prompt)caller を必ず渡すのがポイントです。あとで「どのモデルが多いか」だけでなく「そのモデルをどの処理が呼んでいるか」まで一気に辿れるようになります。
次に、溜まったログを集計します。廃止対象のモデル ID をひとまとめにして、それぞれの呼び出し回数・最終呼び出し日・呼び出し元を出します。
import json, pathlib
from collections import defaultdict
from datetime import datetime, timezone
# 8月17日に停止するモデル ID を列挙する(公式の廃止一覧を確認して更新してください)
RETIRING = {
"gemini-3.1-flash-image-preview",
"gemini-3-pro-image-preview",
}
stats = defaultdict(lambda: {"count": 0, "last_ts": 0.0, "callers": set()})
for line in pathlib.Path("logs/model_calls.jsonl").read_text(encoding="utf-8").splitlines():
rec = json.loads(line)
if rec["model"] not in RETIRING:
continue
s = stats[rec["model"]]
s["count"] += 1
s["last_ts"] = max(s["last_ts"], rec["ts"])
s["callers"].add(rec.get("caller", "unknown"))
for model, s in sorted(stats.items(), key=lambda kv: kv[1]["count"], reverse=True):
last = datetime.fromtimestamp(s["last_ts"], tz=timezone.utc).strftime("%Y-%m-%d")
callers = ", ".join(sorted(s["callers"]))
print(f"{model}: {s['count']}回 / 最終 {last} / 呼び出し元: {callers}")このスクリプトを回すと、廃止対象のモデルごとに「まだ生きているか」「どれくらいの頻度か」「誰が呼んでいるか」がひと目で分かります。呼び出し回数がゼロなら、コード上に残っていても移行の緊急度は下がります。逆に、grep では見落としていた呼び出し元がここで初めて名前で出てくることがあります。私自身、無効化したと思い込んでいた検証用バッチが最終呼び出し3日前で顔を出し、静かに肝を冷やしました。個人開発では、こうした「忘れられた1本」を自分で見つけるほかありません。
洗い出した呼び出し箇所に移行優先度を付ける
棚卸しの結果が出たら、すべてを同時に移行しようとせず、優先度を付けます。判断材料は「呼び出し量」「直近に呼ばれているか」「自動で無人実行されるか」の3つです。とくに無人で動くジョブは、止まっても誰もすぐには気づかないため、量が少なくても優先度を上げます。
| 呼び出し箇所の性質 | 移行の優先度 | 理由 |
|---|---|---|
| 無人の定期バッチで、直近も呼ばれている | 最優先 | 止まっても即座に気づけず、影響が静かに広がる |
| ユーザー操作起点で呼び出し量が多い | 高 | 停止が体験の劣化に直結する |
| 手動実行の検証・実験スクリプト | 中 | 止まっても実害は小さいが、放置すると後で混乱する |
| ログ上で呼び出し回数がゼロ | 低(ただし削除は検討) | 実質使われていない。コードから消す好機でもある |
優先度の高いものから移行先の GA モデルに差し替え、切り替えたら同じログ集計をもう一度回します。廃止対象モデルの呼び出しが新しい記録に現れなくなっていれば、その経路の移行は完了です。この「切り替え後にログで確認する」ひと手間を省かないことが、8月17日を静かに越えるいちばんの近道だと感じています。
移行先モデルの選定と、切り替え時に確認するコード差分については、Gemini の画像生成 preview モデルが 6月25日に停止します — GA 版への移行で確認したコード差分と検証手順にまとめてあります。停止の朝に慌てないためのパイプライン側の備えはプレビュー画像モデル停止の朝に学んだこと — Gemini 画像モデル GA 移行と廃止に強いパイプライン設計が参考になります。
モデル ID を一箇所にまとめておく
今回の棚卸しでいちばん学んだのは、モデル ID がコードのあちこちに直接書かれているほど、廃止のたびに探索が発生する、ということでした。次の廃止に備えるなら、モデル ID は定数モジュールか設定に集約し、呼び出し側は名前で参照する形にしておくと、次回は1箇所の変更で済みます。既定モデルが黙って上がってしまう事故を含めた、モデル ID の固定と変更検知の考え方はGemini APIの『デフォルトモデルが上がる』を事故にしない — モデルIDの固定と既定変更の検知設計で掘り下げています。
まず今日できる具体的な一歩として、画像生成の呼び出しに caller 付きのログを1行足すところから始めてみてください。8月17日までに、あなたのコードのどこが本当に移行を必要としているのかが、推測ではなくデータで見えてきます。お読みいただきありがとうございました。