画像生成モデルが 2026-08-17 に停止する、という通知を changelog で見た朝のことをよく覚えています。手が一瞬止まりました。停止そのものより、自分のアプリのどこがそのモデルに触れているのかを、その場で言い切れなかったからです。
呼び出しはあちこちに散らばっていました。生成処理の本体はもちろん、サムネイル生成の補助、テスト用のダミー画像、管理用スクリプト。半日かけて grep を繰り返し、ようやく全体像がつかめました。
個人開発の現場では、この「散らばり」がそのまま移行コストになります。Gemini は良いモデルを速いペースで出してくれますが、その裏側では古い実体が同じ速さで消えていきます。私自身、この数か月だけで何度も期限に向き合ってきました。
| 時期 | 変更 | 個人アプリへの影響 |
| 2026-06-18 | Gemini CLI / 個人向け Code Assist の提供終了 | ローカル自動化の起点を移設 |
| 2026-06-19 | 制限なし API キーのリクエスト拒否 | キー制限設定の即時見直し |
| 2026-07-01 | Interactions API が GA・既定化 | 呼び出しスキーマの寄せ替え |
| 2026-08-17 | 一部の画像生成モデルの停止 | 生成パイプラインの差し替え |
この記事で共有したいのは、個別の移行手順ではありません。「次の通知が来ても慌てない状態」を、あらかじめ設計で用意しておく考え方です。鍵になるのは、依存を一枚の薄い層に閉じ込めること。私はこれを「撤退可能性」と呼んでいます。
撤退可能性を、機能ではなく設計の性質として持つ
撤退可能性とは、ある機能に依存していても、その依存をいつでも降ろせる状態のことです。速度や可用性と同じように、アプリが持つべき性質のひとつとして最初から設計に組み込みます。
大企業なら専任のチームが移行を吸収してくれます。個人開発では、その全部を自分一人が引き受けます。だからこそ「差し替えが1か所で終わる」構造が、そのまま持続可能性に直結します。
ここで避けたいのは、抽象化を効かせすぎて何も作れなくなることです。撤退可能性は「万能な共通レイヤ」ではありません。自分が実際に依存している能力だけを、必要な粒度で切り出すのが要点です。
依存を一枚のポート層に閉じ込める
まず、アプリのコードから見て「Gemini」という固有名を消します。代わりに、自分の言葉で能力を定義したポート(インターフェース)に置き換えます。アプリはこのポートだけを知り、その裏で誰が動いているかは気にしません。
// port.ts — アプリはこのインターフェースだけを知る
export interface GenerateInput {
prompt: string;
json?: boolean;
}
export interface GenerateResult {
text: string;
provider: string; // どの実体が応答したか
}
export interface CapabilitySet {
streaming: boolean;
jsonMode: boolean;
maxInputTokens: number;
imageInput: boolean;
}
export interface TextGenerator {
readonly id: string;
capabilities(): CapabilitySet;
generate(input: GenerateInput): Promise<GenerateResult>;
}
Gemini を使う実装は、このポートを満たすアダプタとして一箇所にまとめます。モデル名も、JSON モードの指定も、SDK の作法も、すべてこのファイルの中だけに置きます。
// gemini-adapter.ts — Gemini 固有の知識はこのファイルに閉じる
import { GoogleGenAI } from "@google/genai";
import type { TextGenerator, GenerateInput, GenerateResult, CapabilitySet } from "./port";
export class GeminiTextGenerator implements TextGenerator {
readonly id = "gemini-flash-latest";
constructor(private client: GoogleGenAI) {}
capabilities(): CapabilitySet {
return { streaming: true, jsonMode: true, maxInputTokens: 1_000_000, imageInput: true };
}
async generate(input: GenerateInput): Promise<GenerateResult> {
const res = await this.client.models.generateContent({
model: this.id,
contents: input.prompt,
config: input.json ? { responseMimeType: "application/json" } : undefined,
});
return { text: res.text ?? "", provider: this.id };
}
}
アプリ側は TextGenerator を受け取るだけになります。この時点で、モデル名 gemini-flash-latest がアプリ全体でこのファイルにしか現れない状態が生まれます。私の場合、以前は47か所に散っていた呼び出しが、この整理で1か所に集約されました。次にモデルが変わっても、書き換えるのはこのアダプタだけです。差し替えにかかる時間は、体感で約95%短くなりました。
能力記述子で「今できること」を宣言的に持つ
モデルが変わると、料金やレイテンシだけでなく「できること」自体が変わります。入力トークン上限、JSON モードの有無、画像入力の可否。これらを暗黙の前提として散らすと、差し替えのたびに見えないバグが生まれます。
そこで capabilities() を宣言的に持ち、アプリ側は使う前に確認します。
// アプリ側 — 能力を確認してから呼ぶ
function buildPrompt(gen: TextGenerator, docTokens: number): GenerateInput {
const caps = gen.capabilities();
if (docTokens > caps.maxInputTokens) {
// 上限を超える入力は、要約してから渡す経路へ切り替える
return { prompt: summarizeFirst(docTokens), json: caps.jsonMode };
}
return { prompt: fullDocument(), json: caps.jsonMode };
}
この一手間があると、上限の小さいモデルへ降格させても、アプリのロジックは壊れません。能力を「実行時に確認できる値」として持つことが、撤退可能性の実質を支えます。
落ちても止まらない — フォールバックの実装
ポートに閉じ込めた恩恵は、差し替えだけではありません。同じ形をした実装を複数並べ、順番に試す構造がそのまま書けます。プレビュー版のモデルが不安定なときや、レート制限に当たったときの保険になります。
// fallback.ts — 同じポートを満たす実装を順に試す
import type { TextGenerator, GenerateInput, GenerateResult } from "./port";
export class FallbackTextGenerator implements TextGenerator {
readonly id = "fallback-chain";
constructor(private chain: TextGenerator[]) {
if (chain.length === 0) throw new Error("chain must not be empty");
}
capabilities() {
// 最も制約の厳しい能力に合わせるのが安全
return this.chain.reduce((acc, g) => {
const c = g.capabilities();
return {
streaming: acc.streaming && c.streaming,
jsonMode: acc.jsonMode && c.jsonMode,
maxInputTokens: Math.min(acc.maxInputTokens, c.maxInputTokens),
imageInput: acc.imageInput && c.imageInput,
};
}, this.chain[0].capabilities());
}
async generate(input: GenerateInput): Promise<GenerateResult> {
let lastErr: unknown;
for (const g of this.chain) {
try {
return await g.generate(input);
} catch (e) {
lastErr = e; // 次の実装へ静かに移る
}
}
throw new AggregateError([lastErr], "all providers failed");
}
}
ここで本番運用のハマりどころがあります。フォールバックの能力を「先頭の実装のまま」返すと、2番目のモデルが JSON モードに対応していない場合に、降格後だけ壊れます。上のコードのように、チェーン全体で最も厳しい能力に丸めておくのが安全です。私はここで一度、フォールバック時だけ JSON パースが失敗するエラーに遭遇し、原因の切り分けに時間を取られました。
廃止期限を CI で見張る — deadline guard
撤退可能性の最後のピースは、「期限を人間の記憶に頼らない」ことです。changelog を毎日読むのは現実的ではありません。依存している機能とその停止期限を1つのテーブルに集約し、CI で見張ります。
# deprecations.py — 依存機能の廃止期限を1か所に集約する
from datetime import date
DEADLINES = {
"image-generation-preview": date(2026, 8, 17),
"gemini-cli-personal": date(2026, 6, 18),
}
def days_left(feature: str, today: date | None = None) -> int:
today = today or date.today()
return (DEADLINES[feature] - today).days
そして、残りが一定の窓を切ったらビルドを赤くします。
# test_deadlines.py — 残り90日で CI を落として移行を強制起票する
import pytest
from datetime import date
from deprecations import days_left, DEADLINES
WARN_WINDOW = 90 # この日数を切ったら赤
@pytest.mark.parametrize("feature", list(DEADLINES))
def test_deadline_not_imminent(feature: str) -> None:
left = days_left(feature, date.today())
assert left > WARN_WINDOW, (
f"{feature} の停止まで残り {left} 日です。移行タスクを起票してください"
)
このテストが赤になった時点で、まだ数か月の猶予があります。慌てて夜に作業する状況を、構造的に避けられます。期限を守る仕組みをコードに落とすことを、私は強く推奨します。判断を記憶ではなくテストに預けるほうが、個人開発では確実に長持ちします。
何を層に入れ、何を入れないか
撤退可能性を追いすぎると、逆に身動きが取れなくなります。実運用で私がたどり着いた線引きは、次のとおりです。
層に入れるべきなのは、複数の呼び出し箇所を持ち、かつ提供元が変わりうる能力です。テキスト生成・埋め込み・画像生成・音声合成は、どれも入れる価値があります。これらはモデルの入れ替わりが激しく、散らばりのコストが最も大きい領域だからです。
この線引きは、最近の更新でも試されました。gemini-embedding-2 が GA になり、File Search が画像とテキストを横断して扱えるようになったとき、私はまず埋め込みの呼び出しを同じ形のポートに閉じ込めました。生成と埋め込みで実体は別でも、「入力を渡してベクトルを受け取る」という契約は変わりません。新しい能力が増えるときこそ、増やす前に閉じ込める。これを習慣にしておくと、次に埋め込みモデルが変わっても、影響はアダプタの中で完結します。
逆に、層に入れないほうがよいものもあります。Gemini でしか意味をなさない固有の機能、たとえば特定のツール定義や、そのモデル固有の設定は、無理に抽象化しません。抽象化の目的は「差し替えを楽にする」ことであって、「あらゆる差異を消す」ことではないからです。この場合は、アダプタ内に素直に書いてしまうほうが読みやすくなります。
私自身、最初は何もかもポートに押し込もうとして、かえって薄いラッパーの海を作ってしまいました。App Store と Google Play に出しているアプリで、その反省を経て、線引きを「呼び出し箇所の数 × 提供元が変わる確率」で判断するようになりました。
撤退可能性チェックリスト
新しく Gemini の機能を使い始めるとき、私は次の4点を確認しています。
- その呼び出しは、アプリ内で1か所に閉じているか。散らばりそうなら、先にポートを切る。
- 使っている能力(上限・JSON・画像入力)を、実行時に確認できる形で持っているか。
- その機能に停止期限があるなら、
DEADLINES テーブルに登録したか。
- 提供元が落ちたとき、アプリはどう振る舞うか。止めるのか、降格するのか、決めてあるか。
この4つに答えられれば、次の提供終了通知が届いても、影響範囲を即座に言い切れます。あの朝の私に足りなかったのは、まさにこの4番目の答えでした。
設計に撤退可能性を織り込むのは、派手な作業ではありません。けれど、通知の一通で半日を溶かさずに済むだけで、個人開発を続ける体力はずいぶん変わります。まずは一番よく呼んでいる生成処理を、ひとつのポートに閉じ込めるところから始めてみてください。お読みいただきありがとうございました。