◉GEMINI LABEN
●ARGON — gemini-4-argon が 10/13 に GA。複雑なコーディング・エージェント・数学・科学向けです●DEEP RES — deep-research-pro-preview-12-2025 は 10/23 に停止、残り6日。移行先は deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 が GA(10/06)。gemini-3.1-flash-image は非推奨で停止日は未発表です●MCP — ツール結果が 8.5万トークンに膨らんだ事例を、ADK で 310 トークンに包んで抑えた報告が出ています●FLASH — gemini-3.7-flash と 3.5-flash は新しい Flash モデルに自動で回されます(10/08)●NEW — クラッシュ報告を毎回ゼロから読ませない — 既知バグ早見表を先に grep し…●ARGON — gemini-4-argon が 10/13 に GA。複雑なコーディング・エージェント・数学・科学向けです●DEEP RES — deep-research-pro-preview-12-2025 は 10/23 に停止、残り6日。移行先は deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 が GA(10/06)。gemini-3.1-flash-image は非推奨で停止日は未発表です●MCP — ツール結果が 8.5万トークンに膨らんだ事例を、ADK で 310 トークンに包んで抑えた報告が出ています●FLASH — gemini-3.7-flash と 3.5-flash は新しい Flash モデルに自動で回されます(10/08)●NEW — クラッシュ報告を毎回ゼロから読ませない — 既知バグ早見表を先に grep し…
記事一覧/最新情報
◎ 最新情報/2026-10-17中級

Flash の返事の調子が変わった朝、最初に見るのは自分のプロンプトではありません

モデル ID を書き換えていないのに応答の長さや形式が変わったとき、要求したモデルと実際に応答したモデルを記録して突き合わせる手順です。modelVersion を毎回残す 3 プロンプトの小さな確認スクリプトと、変化が出たあとの切り分けの順番をまとめました。

gemini-api288gemini-3.8-flash3modelVersion自動ルーティング回帰確認python107

Gemini API の changelog を読み返していて、10 月 8 日の項目で手が止まりました。gemini-3.7-flash と gemini-3.5-flash が非推奨になり、それぞれ新しい Flash へ自動で回される、という一行です。

自動で回される、ということは、コードの model を一文字も書き換えていなくても、返事を作っているモデルが入れ替わりうる、ということです。個人開発で、ストアのレビューへの返信文の下書きを Flash に任せている身としては、これを読んだ時点で少し落ち着かなくなりました。ある朝、返信の文末が妙に長くなっていたとして、私はまず自分のプロンプトを疑います。プロンプトを直し、温度を下げ、また直す——そうして半日を使ったあとで、実はモデルのほうが変わっていた、という順番だけは避けたいと思ったのです。

この記事は、その順番を逆にするための記録です。先にお伝えしておくと、自動で回された後に modelVersion へどの ID が入って返るのかを、私は自分の環境で確かめておりません。ですから以下は、確かめるための道具を先に置いておく話です。

最初にお伝えしたいのは「要求した ID」と「応答した ID」を別々に残すことです

generateContent のレスポンスには、実際に応答したモデルを示す modelVersion が入っています(Python の google-genai では response.model_version)。リクエストに書いた ID と、ここに入って返ってくる ID は、必ずしも同じ文字列とは限りません。

切り分けの考え方は、次の表のとおりです。

要求と応答の ID出力の変化先に疑う場所
一致しているありプロンプト・設定・入力データ・SDK の更新
一致していないありモデルの入れ替わり。プロンプトは触らない
一致していないなしいまは問題なし。ただし単価と上限は見直す
応答に modelVersion が無いあり記録できていない。ログの取り方を先に直す

この表の一行目と二行目を分けられるだけで、半日が三十分になります。逆に、ID を残していない日は、どの行にいるのかが分かりません。

3 プロンプトだけの確認スクリプト

毎日回す確認ですから、重くしないことを最優先にしました。入力は普段の用途に近い短いものを 3 本だけ、maxOutputTokens は 256、温度は 0 に固定します。見るのは文章の良し悪しではなく、次の 5 点です。

  • 要求した ID と応答した ID
  • 文字数
  • JSON として読めるか(コードフェンスを外したうえで)
  • finishReason
  • HTTP ステータス
import json
import os
import time
import urllib.error
import urllib.request
 
BASE = "https://generativelanguage.googleapis.com/v1beta"
REQUESTED = os.environ.get("GEMINI_MODEL", "gemini-3.7-flash")  # 自分のコードに書いているモデル ID
LOG = "canary_log.jsonl"
 
# 普段の用途に近い短い入力を 3 本だけ。長さと形式が見えれば十分です。
PROMPTS = {
    "reply_draft": "星 2 のレビュー「広告が多い」に、丁寧な返信を 3 文で書いてください。",
    "json_shape": 'JSON だけで答えてください。キーは "tone" と "reason"。文面: 「ありがとう、助かりました」',
    "short_answer": "壁紙アプリの通知は何時に送ると開封されやすいですか。1 文で。",
}
 
 
def call(prompt, key):
    body = json.dumps({
        "contents": [{"parts": [{"text": prompt}]}],
        "generationConfig": {"maxOutputTokens": 256, "temperature": 0},
    }).encode()
    req = urllib.request.Request(
        f"{BASE}/models/{REQUESTED}:generateContent",
        data=body, method="POST",
        headers={"Content-Type": "application/json", "x-goog-api-key": key},
    )
    try:
        with urllib.request.urlopen(req, timeout=30) as r:
            return r.status, json.load(r)
    except urllib.error.HTTPError as e:
        return e.code, {"error": e.read().decode()[:300]}
 
 
def summarize(label, status, resp):
    """レスポンス 1 件を、後で突き合わせる 1 行に畳む。"""
    cand = (resp.get("candidates") or [{}])[0]
    parts = (cand.get("content") or {}).get("parts") or [{}]
    text = parts[0].get("text", "")
    served = resp.get("modelVersion", "(none)")  # 実際に応答したモデル
    try:
        json.loads(text.strip().removeprefix("```json").removesuffix("```").strip())
        json_ok = True
    except ValueError:
        json_ok = False
    return {
        "ts": time.strftime("%Y-%m-%dT%H:%M:%S"),
        "label": label, "status": status,
        "requested": REQUESTED, "served": served,
        "routed": served != REQUESTED and served != "(none)",
        "chars": len(text), "json_ok": json_ok,
        "finish": cand.get("finishReason", "(none)"),
    }
 
 
def last_two_runs(path=LOG):
    rows = [json.loads(l) for l in open(path, encoding="utf-8")]
    labels = sorted({r["label"] for r in rows})
    out = {}
    for lb in labels:
        mine = [r for r in rows if r["label"] == lb]
        if len(mine) >= 2:
            out[lb] = (mine[-2], mine[-1])
    return out
 
 
def report(path=LOG):
    for lb, (a, b) in last_two_runs(path).items():
        notes = []
        if a["served"] != b["served"]:
            notes.append(f"served {a['served']} -> {b['served']}")
        if a["json_ok"] != b["json_ok"]:
            notes.append(f"json_ok {a['json_ok']} -> {b['json_ok']}")
        if a["chars"] and abs(b["chars"] - a["chars"]) / a["chars"] > 0.5:
            notes.append(f"chars {a['chars']} -> {b['chars']}")
        print(f"{lb:<13}", "; ".join(notes) if notes else "変化なし")
 
 
if __name__ == "__main__":
    key = os.environ["GEMINI_API_KEY"]
    with open(LOG, "a", encoding="utf-8") as f:
        for label, prompt in PROMPTS.items():
            status, resp = call(prompt, key)
            row = summarize(label, status, resp)
            f.write(json.dumps(row, ensure_ascii=False) + "\n")
            print(row["label"], row["status"], row["requested"], "->", row["served"])
    report()

キーは環境変数 GEMINI_API_KEY から読み、コマンドラインには渡しません。GEMINI_MODEL には、自分のコードに書いているモデル ID をそのまま入れてください。リトライは入れていません。確認用の呼び出しが 429 や 503 で失敗した日は、失敗した事実がそのまま status に残るほうが後で役に立つからです。

2 回目以降の実行では、末尾の report() が、前回との差だけを 1 ラベル 1 行で出します。形式が崩れた日、文字数が半分以下か 1.5 倍以上になった日、応答した ID が変わった日が、そこで目に入ります。

手元の疑似レスポンスで、差の出方だけ確かめました

実際の API を叩いたわけではありません。要求を gemini-3.7-flash、応答を gemini-3.8-flash に見立てた疑似レスポンスを 2 件作り、summarize と report に通しただけです。

json_shape    served gemini-3.7-flash -> gemini-3.8-flash; chars 25 -> 87

1 行目の served の差で「モデルが入れ替わった日」が、続く chars の差で「出力の長さが動いた日」が読み取れます。この出力は私が組み立てた疑似データの結果ですので、実際のモデルの挙動を示すものではない点は、念のため書き添えておきます。

変化が出た日の、切り分けの順番

report() に何かが出た日は、次の順に見ています。

  1. 応答した ID が変わっていないか。 変わっていたら、プロンプトには触らず、changelog の該当日の項目を開きます。
  2. finishReason が STOP 以外になっていないか。 MAX_TOKENS なら、モデルではなく出力上限の問題です。
  3. JSON が読めなくなっていないか。 コードフェンスの有無だけで落ちている場合が多く、パーサー側で外す処理を足せば済みます。
  4. 文字数が動いただけか。 指示書に語数の上限を書く前に、前日の出力を 1 件だけ並べて読みます。

読み比べるときは、固定の質問集を使った比較の記事も参考になります。3.8 Flash と 3.7 Flash のどちらに寄せるかを、手元の 20 問で決めていますでは、同じ入力で二つの世代を走らせて判断するところまでを書きました。この記事の確認スクリプトは、その前段にあたる「そもそも入れ替わったのか」を知るための道具です。

応答した ID は、費用の記録にも使えます

応答した ID を残しておくと、副産物として、いつからどのモデルの単価で請求が動いたのかも後から辿れます。リクエスト単位でトークンを記録する実装はGemini API の usageMetadata をリクエスト単位で記録してコストを按分するに書いていますので、あわせてご覧ください。

非推奨や停止の通知を受けたときの移行の進め方は、Gemini API のモデル廃止にどう備えるかに整理してあります。自動で回される種類の変更は、こうした移行作業を「やらされる」側から「あとで気づく」側に寄せてしまうので、確認を仕組みにしておく意味が大きいのだと思います。

私が引いた線

今回の整理で、はっきりした線引きが一つできました。

出力が変わったら、プロンプトを直す前に、どのモデルが答えたのかを確かめる。

確認スクリプトは、cron で朝に 1 回だけ走らせるつもりでおります。夜中に通知を飛ばす仕組みにはしません。見るのは、コーヒーを淹れたあとの自分の目です。

まずは今日、上のスクリプトを一度だけ手動で実行して、canary_log.jsonl に 3 行が残ることを確かめてください。変化のない日の記録が 1 日分あるだけで、後日の比較の土台になります。

シェア

お読みいただきありがとうございます

Gemini Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

◎ 最新情報2026-09-15
3.8 Flash と 3.7 Flash のどちらに寄せるかを、手元の20問で決めています
モデル ID を新しい世代へ書き換えたあと、同じ入力で答えが割れて見えることがあります。20問の質問集を1本だけ用意して、二つの世代を同じ条件で走らせ、充足率と自己一致率で判断するまでの手順をお伝えします。
◈ API / SDK2026-10-16
models は 200 なのに generateContent だけ 403 になる日 — 切り分けの順番を先に決めておく
モデル一覧の取得は 200 で通るのに、生成リクエストだけが 403 permission_denied で止まるときの切り分け手順です。キー・プロジェクト・請求の順に確かめる小さな診断スクリプトと、リトライしてはいけない理由、サポートへ渡す材料の揃え方をまとめました。
◈ API / SDK2026-09-04
ループ音源のプチノイズは、Gemini に聴かせる前に波形の端で切り分けられます
環境音のループ再生で折り返しだけがプチッと鳴る。音声理解に丸ごと投げる前に、波形の端の段差を内部の変化量で正規化してふるい分ける手順と、配布形式への変換で継ぎ目が作り直される落とし穴をまとめました。