来年ぶんの費用を見直していた夜、表計算の隅に手で打ち込んだ「0.75」という数字を、しばらく眺めておりました。個人開発で動かしているバッチはどれも小さいのですが、毎日走る本数だけは増えておりまして、月末の請求はいつも予想の少し外側に着地します。
そのとき私が考えていたのは、こういうことでした。新しい Flash に上げなければ、この単価のままでいられる——。
念のためにと思って移行のドキュメントを開き直し、手が止まりました。読み違えていたのです。
導入価格の終わりは、モデルの世代についていません
Google の「What's new in Gemini 3.8 Flash 」には、価格についてこう書かれています。導入価格は 100万トークンあたり入力 0.75 ドル・出力 3.75 ドルで、2026年12月31日まで 。標準価格の入力 1.50 ドル・出力 7.50 ドルは2027年1月1日から 適用されます。
私が読み違えていたのはここから先でした。同じページの Pricing の節には、この導入価格が Gemini 3.8 Flash・Gemini 3.7 Flash・Gemini 3.6 Flash に対して適用されると書かれています。つまり期限は、モデルの世代ではなく暦のほうに付いています 。
古い世代に据え置いても、その日は同じように来ます。「乗り換えなければ単価は変わらない」という私の前提は、はじめから成り立っていませんでした。
これは意地悪な仕様ではなく、単に私の思い込みでした。単価の数字だけを見て、その数字がいつまでのものかを確かめなかったのが原因だったのかもしれません。ただ、思い込みのまま数字を置いた表計算は、来年の一月に静かに嘘をつきます。表計算は警告を出してくれません。
見積もりが壊れるのは、価格を「数字」として書いたときです
私は長いあいだ、費用の見積もりを表計算で作っておりました。単価のセルに手で 0.75 と打ち、トークン数を掛けて、月額を出しておりました。それで十分だと思っていたのです。
うまくいかなくなったのは、使うモデルが増えてからでした。壁紙アプリ側の分類バッチと、サイト運用側の翻訳の下ごしらえで別のモデルを使い始めた月に、シートの単価が何のものだったのか分からなくなりました。セルには数字しか残っていないからです。いつの価格なのか、どのモデルのものなのか、誰が更新したのか——どれも数字の中には入っていません。
最初のうちは、シートにメモ欄を足して凌ごうとしました。結果は芳しくありませんでした。メモは更新されないまま古びていき、数字だけが独り歩きします。
いまは線引きを変えました。価格は数字ではなく、いつからいつまで有効かを添えた記録として書き残します。 手で打った 0.75 を疑えるようにしておくこと。それだけで、来年の一月をまたぐ計算が自分の手に戻ってきます。
以下は、その考え方をいちばん小さな形にした一本です。Python と、標準ライブラリだけで動きます。
期限つきの価格表をつくります
はじめに、価格そのものをファイルに出します。ここで大事なのは金額ではなく、from と to を必ず持たせるところです。
{
"currency" : "USD" ,
"per_tokens" : 1000000 ,
"rows" : [
{
"model" : "gemini-3.8-flash" ,
"from" : "2026-09-02" , "to" : "2026-12-31" ,
"input" : 0.75 , "output" : 3.75 ,
"note" : "導入価格"
},
{
"model" : "gemini-3.8-flash" ,
"from" : "2027-01-01" , "to" : null ,
"input" : 1.50 , "output" : 7.50 ,
"note" : "2027-01-01 からの標準価格"
},
{
"model" : "gemini-3.7-flash" ,
"from" : "2026-08-13" , "to" : "2026-12-31" ,
"input" : 0.75 , "output" : 3.75 ,
"note" : "導入価格。期限は 3.8 と同じ"
},
{
"model" : "gemini-3.7-flash" ,
"from" : "2027-01-01" , "to" : null ,
"input" : 1.50 , "output" : 7.50 ,
"note" : "2027-01-01 からの標準価格"
}
]
}
to が null の行は「今のところ終わりが告知されていない」という意味にしています。終わりが決まったら、その日を書き込むだけで済みます。
3.7 Flash の行を見ていただくと、私の読み違いがそのまま形になっています。世代は古いのに、期限は 3.8 と同じ日です。表にしてしまえば、思い込みの入り込む隙がなくなります。
なお per_tokens を 100万として外に出しておくと、単位の勘違いが起きません。私は以前、100万あたりの単価を1,000トークンあたりの単価と取り違えて、月額を千倍で見積もったことがあります。単位を数式に埋めず、データ側に置いておくのがいちばん安全でした。
日付を引数に取る見積もりを書きます
次に、価格表を読んで合計を出す小さなスクリプトです。ここでの要点は、「いつの価格で計算するか」を引数として外から渡す ことです。今日の日付を暗黙の前提にしないだけで、来年の姿が同じコードから出てきます。
import json, sys
from datetime import date
def load_prices (path):
raw = json.load( open (path, encoding = "utf-8" ))
unit = raw[ "per_tokens" ] # 100万トークンあたりの単価をトークン単価へ
rows = []
for r in raw[ "rows" ]:
rows.append({
"model" : r[ "model" ],
"start" : date.fromisoformat(r[ "from" ]),
"end" : date.fromisoformat(r[ "to" ]) if r[ "to" ] else date.max,
"input" : r[ "input" ] / unit,
"output" : r[ "output" ] / unit,
})
return rows
class PriceMissing ( Exception ):
"""その日付を覆う価格行が無い。黙って 0 円にしないための例外。"""
def price_on (rows, model, on):
for r in rows:
if r[ "model" ] == model and r[ "start" ] <= on <= r[ "end" ]:
return r
raise PriceMissing( f " { model } has no price row covering { on } " )
def total (ledger, rows, on):
per_model = {}
for line in open (ledger, encoding = "utf-8" ):
rec = json.loads(line)
p = price_on(rows, rec[ "model" ], on)
cost = rec[ "input_tokens" ] * p[ "input" ] + rec[ "output_tokens" ] * p[ "output" ]
per_model[rec[ "model" ]] = per_model.get(rec[ "model" ], 0.0 ) + cost
return per_model
def main ():
rows = load_prices( "prices.json" )
days = [date.fromisoformat(d) for d in sys.argv[ 1 :]] or [date.today()]
tables = {d: total( "usage.jsonl" , rows, d) for d in days}
models = sorted ({m for t in tables.values() for m in t})
head = "model" .ljust( 20 ) + "" .join( str (d).rjust( 14 ) for d in days)
print (head)
print ( "-" * len (head))
for m in models:
print (m.ljust( 20 ) + "" .join( f "$ { tables[d].get(m, 0 ) :.4f } " .rjust( 14 ) for d in days))
print ( "-" * len (head))
print ( "TOTAL" .ljust( 20 ) + "" .join( f "$ { sum (tables[d].values()) :.4f } " .rjust( 14 ) for d in days))
main()
入力になる usage.jsonl は、一日一行の素朴な台帳です。この記事では、手元に置いたサンプルの五日ぶんで動かします。
{ "day" : "2026-09-01" , "model" : "gemini-3.8-flash" , "input_tokens" : 412000 , "output_tokens" : 38000 }
{ "day" : "2026-09-02" , "model" : "gemini-3.8-flash" , "input_tokens" : 455000 , "output_tokens" : 41000 }
{ "day" : "2026-09-03" , "model" : "gemini-3.7-flash" , "input_tokens" : 690000 , "output_tokens" : 12000 }
{ "day" : "2026-09-04" , "model" : "gemini-3.8-flash" , "input_tokens" : 398000 , "output_tokens" : 36000 }
{ "day" : "2026-09-05" , "model" : "gemini-3.7-flash" , "input_tokens" : 705000 , "output_tokens" : 11000 }
二段で出すと、判断がその場で終わります
引数に二つの日付を渡します。片方は今日、もう片方は値上げのあとの適当な一日です。
python3 estimate.py 2026-09-08 2027-01-15
私の手元で実際に走らせると、こう出ました。
model 2026-09-08 2027-01-15
------------------------------------------------
gemini-3.7-flash $1.1325 $2.2650
gemini-3.8-flash $1.3800 $2.7600
------------------------------------------------
TOTAL $2.5125 $5.0250
サンプルの台帳ですから、金額そのものに意味はありません。意味があるのは並びのほうです。3.7 Flash の列も、3.8 Flash の列と同じ倍率で伸びています。据え置きは避難所ではない、ということが、説明ではなく表として目に入ります。
私はこの形にしてから、モデルの乗り換えを迷う時間が短くなりました。二つの列の差は、どちらの世代を選んでも同じだからです。そうなると比べるべきものは単価ではなく、一件あたりに使うトークンの量と、やり直しの回数のほうへ移ります。
出力側の単価は入力側の5倍です。この比を体で覚えておくと、長い応答を返させる設計にどれだけの重みがあるのかが分かってきます。私の場合、出力を短く保つ工夫のほうが、モデルを一段下げるよりも効きました。応答の長さを自分で決められる仕事が多い場合は、まず出力側から削ることをお勧めします。
価格表に穴があるとき、黙って0円にしないでください
初めて作る見積もりでいちばん深い落とし穴は、計算が失敗することではなく、失敗しないまま間違った数字を返すこと です。エラーとして表に出てくれる問題は、いずれ気づけます。回避したいのは、静かなほうです。
たとえば 3.7 Flash の2027年以降の行を、うっかり書き忘れたとします。よくある実装なら、該当する価格が見つからないので 0 を返し、合計はそれらしい数字のまま出てきます。安く見えた見積もりは疑われにくく、気づくのは請求書が届いたあとです。
先ほどのコードでは、そこを例外にしています。行を一つ削って走らせると、こうなります。
__main__.PriceMissing: gemini-3.7-flash has no price row covering 2027-01-15
終了コードは 1 です。定期実行に載せているなら、この一行で止まってくれます。
価格行が見つからないときの扱い その場で起きること あとで起きること
0 として合計に足す 見積もりは出る 請求が届くまで誤りに気づけない
その行を読み飛ばす 合計が少し小さくなる どの行が消えたか追えない
例外にして止める 見積もりは出ない 価格表を直せば、その日から正しくなる
止まることを嫌って握り潰す実装を、私は何度か書いてしまいました。いまは逆にしています。金額に関わる欠落は、黙って埋めずに、必ず声を上げさせます。
実際に使ったトークン数を台帳に落とします
ここまではサンプルの台帳で動かしてきました。最後に、本物の数字を入れる口をつくります。応答に含まれる利用状況のメタデータから、入出力のトークン数をそのまま追記するだけです。
import json
from datetime import date
from google import genai
client = genai.Client() # 環境変数 GEMINI_API_KEY を読みます
MODEL = "gemini-3.8-flash" # モデル ID は一箇所に置きます
def ask (prompt: str , ledger: str = "usage.jsonl" ) -> str :
interaction = client.interactions.create( model = MODEL , input = prompt)
usage = getattr (interaction, "usage" , None )
if usage is None :
# 取れないときは推測で埋めず、取れなかったことを記録します
record = { "day" : str (date.today()), "model" : MODEL , "usage" : "missing" }
else :
record = {
"day" : str (date.today()),
"model" : MODEL ,
"input_tokens" : getattr (usage, "input_tokens" , 0 ),
"output_tokens" : getattr (usage, "output_tokens" , 0 ),
}
with open (ledger, "a" , encoding = "utf-8" ) as f:
f.write(json.dumps(record, ensure_ascii = False ) + " \n " )
return interaction.output_text
トークン数が取れなかったときに 0 を書かないところが、地味ですが効きます。0 は「使わなかった」と読めてしまい、あとから見分けがつきません。取れなかったという事実のほうを残しておけば、月末に照らし合わせたときに「ここは数えられていない」と分かります。
呼び出し前の見積もりと実際の請求がずれる原因については、Gemini API の count_tokens 推定と実課金トークン数がズレる5つの原因 にまとめてあります。応答から受け取った実績値を台帳に積むこの形なら、そのズレの大部分は最初から入り込みません。
値上げの日までに、手元でできること
期限が暦に付いている以上、こちらにできるのは単価を選ぶことではなく、使う量を選ぶことです。公式のドキュメントには、判断の材料が二つ書かれています。
一つは、3.8 Flash が難しい多段の作業ほど設計上たくさんのトークンを使うこと。そして日常的な作業では reasoning の効きを下げられること。もう一つは、3.7 Flash がいまも完全にサポートされていることです。
「新しいほうが常に良い」でも「古いほうが常に安い」でもありません。私は、分類のように答えの短い仕事は効きを下げた設定に寄せ、まとめや翻訳の下ごしらえのように出力が伸びる仕事だけを丁寧に見るようにしています。
移行のときに見落としやすい点 3.8 Flash での扱い
temperature / top_p / top_k生成設定から外します
thinking_budgetthinking_level に置き換えます(minimal は非対応)
candidate_countGemini 3 以降では使いません
モデル ID を一行差し替えるだけで済むと思っていると、この辺りで足を取られます。私が価格表を外に出したのと同じ理由で、モデル ID も設定の一箇所に置いておくと、差し替えの影響範囲がその場で見渡せます。
今日できることは一つです。いま持っている見積もりのシートかコードを開いて、単価の隣に from と to を書き足してみてください。数字が「いつのものか」を持った瞬間から、来年の一月は、驚く日ではなく確かめる日に変わります。
拙い手順にお付き合いいただき、ありがとうございました。私自身もまだ、月末になるたびに台帳を眺め直している最中です。