朝、報道で何度も見た日付が今日でした。手元のメモには「7/17、3.5 Pro」とだけ書いてあります。公式ドキュメントを開き、モデル一覧を引き、価格ページを確かめました。いずれにも gemini-3.5-pro はありません。Google からの発表も、この記事を書いている時点では確認できませんでした。
数日前に私は、公開を待つ間の準備として、手元のプロンプトのトークン数を測る話を書きました。あの時点では「7/17 に来る」と書きました。今日、その前提は外れています。外れたこと自体は珍しくありません。問題は、外れた前提をコードのどこに置いていたかです。
もし私が MAX_CONTEXT = 2_000_000 と定数に書き、モデルIDを gemini-3.5-pro と設定ファイルへ先置きしていたら、今日は本番が落ちていました。報道の数値は、記事に書く分には「報じられています」と添えれば済みます。設定に書いた瞬間、それは約束になります。
やることは三つです。噂の数値を設定から追い出し、上限をモデル自身から引き、超えたときに落とさず縮退させる。動くコードを置きますので、手元の呼び出しに一枚挟んでいただければ、次に日付が動いても何も起きません。
確認できることと、確認できないことを分ける
まず、今日時点で手元から確かめられる事実だけを並べます。ここを曖昧にしたまま設計に入ると、結局どこかに噂が混ざります。
| 項目 | 状態 | 確認方法 |
gemini-3.5-pro のモデルカード | 公開APIドキュメントに記載なし | ドキュメントのモデル一覧を参照 |
| 200万トークンのコンテキスト | 報道ベース・未確認 | 確かめる手段が現時点でない |
| 価格 | 報道ベース・未確認 | 価格ページに記載なし |
| 一般提供されている最新 | gemini-3.5-flash | models.list で確認可能 |
gemini-flash-latest の実体 | gemini-3.5-flash に切替済み | models.get で確認可能 |
表の右列を見ていただくと、境界がはっきりします。確認方法が「API を叩けば分かる」ものと、「誰かがそう言っている」ものが混在しているのです。前者だけを設定の根拠にする、というのがこの記事の全部です。
未発表のモデルIDを先置きすると何が起きるか
準備のつもりで、環境変数にモデルIDを先に入れておく。よくやります。私も一度やりました。結果は素直で、NOT_FOUND が返ってきて起動時のヘルスチェックが落ちました。
// Before: 報道由来の値を設定へ焼き込む
const MODEL = process.env.GEMINI_MODEL ?? "gemini-3.5-pro"; // まだ存在しない
const MAX_CONTEXT = 2_000_000; // 未確認の数値
async function ask(prompt) {
// MODEL が存在しなければ、ここで初めて 404 相当が返る
return ai.models.generateContent({ model: MODEL, contents: prompt });
}
このコードの厄介なところは、失敗が呼び出しの瞬間まで遅れることです。デプロイは通り、起動も通り、最初のユーザーのリクエストで落ちます。しかも MAX_CONTEXT は誰にも検証されないまま、以後ずっと入力の切り詰め判断に使われ続けます。存在しないモデルの、確認されていない上限を根拠に、実在するモデルへ入力を組み立てているわけです。
モデルIDのピン留めと既定への依存の使い分けは既定モデルの静かな入れ替わりを検知する設計で扱いました。ここで足りていなかったのは、まだ存在しないIDを書いてしまう方向です。
上限はモデル自身に聞く
Gemini API のモデル情報には、そのモデルが受け付ける入力トークンの上限が含まれています。models.get を一度引けば取れます。定数に書く理由はありません。
import { GoogleGenAI } from "@google/genai";
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
// 起動時に一度だけ引き、プロセス内に保持する
async function loadBudget(model) {
const info = await ai.models.get({ model });
return {
model: info.name, // 解決された実体
inputLimit: info.inputTokenLimit, // 公表されている上限
outputLimit: info.outputTokenLimit,
};
}
const budget = await loadBudget("gemini-flash-latest");
console.log(budget);
// => { model: 'models/gemini-3.5-flash', inputLimit: ..., outputLimit: ... }
gemini-flash-latest のようなエイリアスを渡すと、info.name に解決後の実体が入ります。エイリアスが何を指しているかを、推測ではなく戻り値で知れるわけです。ログに残しておくと、後で「いつ実体が入れ替わったか」を辿れます。
ここで組んでいるのは長さとコストの予算であって、機能の可否ではありません。thinking 指定や構造化出力が使えるかどうかを試し打ちで判定する話は起動時の能力検出レイヤーに分けてあります。両方を一枚の層に押し込むと、片方の失敗でもう片方が使えなくなります。軸を分けたまま並べるのが扱いやすいと感じています。
入力側は countTokens で測り、安全率を掛ける
上限が取れたら、入力がその中に収まるかを送信前に測ります。countTokens は課金されない見積もりで、送る前に規模を知るためのものです。
async function fits(model, contents, budget, headroom = 0.9) {
const { totalTokens } = await ai.models.countTokens({ model, contents });
const ceiling = Math.floor(budget.inputLimit * headroom);
return { totalTokens, ceiling, ok: totalTokens <= ceiling };
}
headroom を 0.9 に置いているのは、countTokens の見積もりと実際に課金されるトークンが完全に一致するとは限らないためです。ここが落とし穴で、システム指示・ツール定義・添付のメタデータが後から乗ります。測った本文だけを見て上限ぎりぎりを狙うと、たまに超えます。たまに超えるものは、本番運用では必ず超えます。安全率で回避しておくほうが、後から原因を探すより安く済みます。
私自身、個人開発の壁紙アプリで App Store 向けの説明文をまとめて生成している処理を、これで測り直しました。巨大なコンテキストが要ると思い込んでいた処理が、実際には数千トークンで収まっていました。安全率を 0.9 に置いても、上限の 1% にも届いていません。200万という数字を待つ前に、自分が今いくつ使っているかを知らなかったわけです。準備として最初にやるべきはここでした。
測り終えてから、私はこう決めました。上限に対する使用率が 10% を切っている処理には、新しいモデルが来ても触らない。大きな枠が効くのは全体を同時に参照する処理だけですから、そこに印を付けておけば、公開日に試す対象は自ずと絞れます。
超えたときに落とさない
測って超えていたら、例外を投げるのではなく縮退させます。分割・要約・降格の順に試し、どれも駄目なら初めて失敗させる、という連鎖です。段ごとに、狙いと適用条件を先に決めておきます。
第1段: 分割できる入力は割る
独立した単位の集まりなら、超過分を按分してチャンクに割り、個別に処理して結果を束ね直します。件数が増えるだけで、答えの質は落ちません。
第2段: 参照用の長文は圧縮する
分割できない長文が背景資料なら、先に要約して入れ直します。ここで一度だけ再帰し、二度目の要約はしません。要約の要約は、原文から遠ざかるだけでした。
第3段: どちらもできないなら失敗させる
分割も圧縮もできない入力を無言で切り詰めると、原因の見えない品質低下になります。ここは記録を残して落とします。
async function generateWithinBudget(contents, budget, opts = {}) {
const model = budget.model;
const check = await fits(model, contents, budget);
if (check.ok) {
return ai.models.generateContent({ model, contents });
}
// 1) 分割できる入力なら、チャンクに割って個別に処理する
if (opts.splittable) {
const chunks = splitByRatio(contents, check.totalTokens / check.ceiling);
const parts = [];
for (const c of chunks) parts.push(await generateWithinBudget(c, budget, opts));
return mergeParts(parts);
}
// 2) 参照用の長文なら、先に要約して圧縮する
if (opts.summarizable) {
const digest = await summarize(contents, budget);
return generateWithinBudget(digest, budget, { ...opts, summarizable: false });
}
// 3) どちらもできないなら、記録を残して明示的に失敗させる
throw new BudgetExceeded({
model,
needed: check.totalTokens,
ceiling: check.ceiling,
});
}
三段目で throw している点を、私は強く推奨します。静かに切り詰めて短い入力で答えてしまうと、出力の質だけが落ちて、原因が見えなくなります。切り詰めるなら切り詰めたと記録に残す。答えられないなら答えられないと返す。個人開発では特に、後から自分が調べる立場になりますので、ここで無言にしないほうが後の自分に親切です。
依存を一枚の層に閉じ込める考え方そのものはモデル提供終了に備える依存の隔離で扱っています。この予算層も、その層の内側に置くのが収まりが良いはずです。
存在しないモデルIDを CI で落とす
先置きを防ぐいちばん確実な方法は、設定に書いたモデルIDが本当に呼べるかを CI で確かめることです。20 行程度で足ります。
// scripts/check-models.mjs — CI で実行する
import { GoogleGenAI } from "@google/genai";
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
const declared = ["gemini-flash-latest", process.env.GEMINI_MODEL].filter(Boolean);
const available = new Set();
for await (const m of await ai.models.list()) available.add(m.name.replace("models/", ""));
let failed = false;
for (const id of declared) {
const ok = available.has(id);
console.log(`${ok ? "OK " : "MISS"} ${id}`);
if (!ok) failed = true;
}
process.exit(failed ? 1 : 0);
models.list はエイリアスも実体も返しますので、エイリアス指定のままでも通ります。逆に、報道を見て気の早い設定を入れた瞬間、CI が赤くなります。赤くなる場所を、本番の最初のリクエストから、プルリクエストの画面へ移しただけです。それだけのことですが、今日のような日に効きます。
実際に来た日に、最初に測ること
いつか gemini-3.5-pro が公開されたとして、その日にやることも決めておきます。上の層が入っていれば、順序は短く済みます。
| 順 | やること | 判断材料 |
| 1 | models.get で inputTokenLimit を引く | 報道の数値と一致するか。ここで初めて確認済みになる |
| 2 | 手元の代表プロンプトを countTokens で測る | 大きな枠が本当に要る処理か、分割で足りるか |
| 3 | 小さい入力で応答の質とレイテンシを見る | 既存モデルから乗り換える理由があるか |
| 4 | 入力を段階的に増やしてコストと速度を測る | 枠の大きさが自分の用途で価値になるか |
| 5 | golden set を通してから既定を切り替える | 出力の手触りが変わっていないか |
5 の切り替えゲートはエイリアス昇格時の golden set とロールバックに手順を残してあります。新しいモデルが来たときも、やることは同じです。
順序を見ていただくと分かる通り、1 と 2 は公開を待たなくても今日できます。現に gemini-3.5-flash に対しては今日引けます。待っている間にできることは、待っている対象そのものではなく、待ち方の側にありました。
まとめ
報道された日付を迎えて、何も来ませんでした。それでも手元は何も壊れていません。壊れなかったのは、上限を定数ではなくモデルから引き、モデルIDを CI で確かめているからです。逆に言えば、噂を設定に書いていたら、今日は落ちていました。
次の一手として、私はまず models.get を一度だけ呼んで inputTokenLimit をログに出すことをお勧めします。そこに出た数字が、今日あなたが根拠にしてよい唯一の上限です。定数に書いた数字と違っていたら、そこが直す場所です。
私もまだ、待つのが上手いとは言えません。日付が動くたびに気を取られます。それでも、動かないものを一つずつ手元に増やしていけば、次の延期は少し静かに迎えられるはずです。お読みいただきありがとうございました。