十月に入って、来年の予算を見直す時期になりました。ストアレビューの返信文を下書きさせているスプレッドシートを開いたとき、私はそのシートが「いくらかかっているのか」を、一度も数字で見たことがないと気づきました。
動いてはいるのです。セルの関数を引くと返信の下書きが並び、手でそれを直して送っておりました。その繰り返しを、もう何か月も続けておりました。けれど請求の画面を眺めても、どの数字がこのシートのものなのかは分かりません。
しかも Gemini Flash の導入価格には、年末に期限があります。値上げのあとに慌てて試算するのではなく、いまのうちに、シートの中だけで月額を二段で出せるようにしておきたいと思いました。
そのために書いた Apps Script を、そのまま載せます。呼び出しのたびにトークン数を台帳タブへ一行残し、価格表を日付つきで持ち、今日の価格と値上げ後の価格で月額を並べて出します。
先にお伝えしておく前提
導入価格の期限や単価の中身は、ここでは繰り返し書きません。2026年12月31日で導入価格が終わり、2027年1月1日から標準価格になること、そしてそれがモデルの世代を問わず適用されることは、別の記事で一次情報から確かめて書きました。
そちらは Python と標準ライブラリで書いています。こちらは、スプレッドシートから呼んでいる方が、シートの外へ出ずに済ませる版です。
一つだけ、私自身が確かめられていない点があります。年が替わる瞬間は、どのタイムゾーンの日付で切り替わるのでしょうか。先の記事を書いたときに読んだ範囲では、時刻までは書かれていませんでした。そのため、後で触れるとおり、12月31日と1月1日の二日間を「余裕を見る帯」として扱います。
台帳タブに、呼び出しごとの一行を残します
見積もりの材料は、一回ごとの呼び出しが使ったトークン数です。API の応答には usageMetadata が付いており、入力側は promptTokenCount、出力側は candidatesTokenCount に入っています。考える工程を持つモデルでは thoughtsTokenCount も付きます。私はこれを出力側に足して数えています。
まず、台帳用のタブを一つ作ります。シート名は ledger、一行目の見出しは次の五つです。
| 列 | 見出し | 入る値 |
|---|---|---|
| A | day | 呼び出した日(YYYY-MM-DD の文字列) |
| B | model | 呼び出したモデル ID |
| C | in_tokens | 入力トークン数 |
| D | out_tokens | 出力トークン数(考えたぶんを含む) |
| E | note | 用途のメモ(任意) |
日付を文字列にしているのは、意図があってのことです。Apps Script の Date はスクリプトのタイムゾーン設定の影響を受けます。一日のずれが月額には響きませんが、価格の切り替え日の判定には響きます。文字列のまま大小を比べれば、その心配が消えます。
呼び出しの側は、次のように包みます。
function callGemini_(model, prompt, note) {
const key = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
const url = 'https://generativelanguage.googleapis.com/v1beta/models/' +
model + ':generateContent';
const res = UrlFetchApp.fetch(url, {
method: 'post',
contentType: 'application/json',
headers: { 'x-goog-api-key': key },
payload: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] }),
muteHttpExceptions: true,
});
if (res.getResponseCode() !== 200) {
throw new Error('Gemini ' + res.getResponseCode() + ': ' +
res.getContentText().slice(0, 200));
}
const body = JSON.parse(res.getContentText());
const u = body.usageMetadata || {};
const out = (u.candidatesTokenCount || 0) + (u.thoughtsTokenCount || 0);
const day = Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy-MM-dd');
SpreadsheetApp.getActive().getSheetByName('ledger')
.appendRow([day, model, u.promptTokenCount || 0, out, note || '']);
return body.candidates[0].content.parts[0].text;
}API キーはコードに書かず、スクリプトのプロパティに置いています(YOUR_API_KEY の部分を、ご自身のキーで設定してください)。台帳に残すのは数字だけで、プロンプトの本文は入れません。顧客の文面をうっかり台帳へ写してしまわないための、小さな線引きです。
価格表は、日付つきで持ちます
次が、見積もりの心臓部です。単価を数字のまま埋め込まず、いつからいつまで有効かを添えて持ちます。
// 100万トークンあたりの USD。from / to は "YYYY-MM-DD"、to が null は終了未告知
const PRICES = [
{ model: 'gemini-3.8-flash', from: '2026-09-02', to: '2026-12-31', input: 0.75, output: 3.75 },
{ model: 'gemini-3.8-flash', from: '2027-01-01', to: null, input: 1.50, output: 7.50 },
{ model: 'gemini-3.7-flash', from: '2026-08-13', to: '2026-12-31', input: 0.75, output: 3.75 },
{ model: 'gemini-3.7-flash', from: '2027-01-01', to: null, input: 1.50, output: 7.50 },
];
const PER = 1000000;
function priceOn_(model, day) {
for (const r of PRICES) {
if (r.model === model && r.from <= day && (r.to === null || day <= r.to)) return r;
}
throw new Error('価格行がありません: ' + model + ' @ ' + day);
}
function costOf_(row, day) {
const p = priceOn_(row.model, day);
return (row.inTok * p.input + row.outTok * p.output) / PER;
}表の値は、私が公式ページで確認した時点のものです。ご自身で使うときは、必ず最新の公式の価格表と見比べてから上書きしてください。
priceOn_ は、該当する行がなければ例外を投げます。ここは 0 を返さないようにしました。価格表の書き忘れが、もっともらしい安い数字として出てくるのが、見積もりのいちばん怖い失敗だからです。
台帳から、30日ぶんの月額を二段で出します
台帳を読み、直近の一日あたりの平均を出し、30日ぶんに引き伸ばします。そのとき「どの日の価格で計算するか」を引数で渡します。
function projectMonth_(rows, day) {
const days = new Set(rows.map(r => r.day));
const perModel = {};
for (const r of rows) perModel[r.model] = (perModel[r.model] || 0) + costOf_(r, day);
let total = 0;
for (const m in perModel) {
perModel[m] = perModel[m] / days.size * 30;
total += perModel[m];
}
return { perModel: perModel, total: total };
}
function estimate() {
const ss = SpreadsheetApp.getActive();
const values = ss.getSheetByName('ledger').getDataRange().getValues().slice(1);
const rows = values.map(v => ({
day: String(v[0]), model: v[1], inTok: Number(v[2]), outTok: Number(v[3]),
}));
const sheet = ss.getSheetByName('estimate') || ss.insertSheet('estimate');
sheet.clear();
sheet.appendRow(['基準日', 'モデル', '30日あたり(USD)']);
['2026-12-31', '2027-01-01'].forEach(function (day) {
const r = projectMonth_(rows, day);
for (const m in r.perModel) sheet.appendRow([day, m, Math.round(r.perModel[m] * 100) / 100]);
sheet.appendRow([day, '合計', Math.round(r.total * 100) / 100]);
});
}基準日に 12月31日と 1月1日の二つを入れてあるのは、先ほどの帯のためです。切り替えの境目がどちらに寄っても、見積もりが二つの数字のどちらかに収まります。
手元で動かした出力
台帳ロジックの部分(projectMonth_ まで)だけを取り出し、七日ぶんのサンプルを入れて Node.js で動かしました。サンプルは、3.8 Flash を使う日と 3.7 Flash を使う日が混ざった、架空の台帳です。
2026-12-31 gemini-3.8-flash $8.21 | gemini-3.7-flash $6.19 TOTAL $14.39
2027-01-01 gemini-3.8-flash $16.41 | gemini-3.7-flash $12.38 TOTAL $28.79金額そのものに意味はなく、見ていただきたいのは並びです。1月1日側は、どちらのモデルも 12月31日側のちょうど2倍になっています。入力も出力も単価が2倍になるので、台帳の中身が同じなら月額も2倍になる、という当たり前の結果が、こうして数字で出てきます。
世代を古いものに据え置いても、この2倍は避けられません。避けられるのは、呼び出しの量を減らす工夫のほうです。見積もりを二段で出したあとの私は、モデルを替える相談よりも先に、出力を短くできないかを考えるようになりました。出力の単価は入力の5倍だからです。
価格表に穴があるとき
試しに 3.6 Flash の行が無い状態で、その日の価格を引いてみます。
ERR 価格行がありません: gemini-3.6-flash @ 2026-10-03シート上では、estimate の実行がここで止まり、実行ログに同じ文言が残ります。止まってくれることが大切です。台帳に新しいモデル名が現れたのに価格表が古いまま、という状況を、一番早く教えてくれるからです。
最初に試す順番
一度に全部を入れる必要はありません。私が手を付けた順に書きます。
ledgerタブを作り、見出しの五つを一行目に置きます。- 既存の呼び出しを
callGemini_で包み、一日ぶん動かして行が増えることだけを確かめます。 - 価格表を、ご自身が使っているモデルの行だけで作ります。
- 一週間ぶん貯まってから
estimateを実行します。
台帳が数日ぶんしかないうちは、平均が偏るのかもしれません。休日の多い週だけで30日ぶんに伸ばすと、実際より安く出てしまうからです。
数字を出す前に、その数字がいつの価格で、どの期間の量から出たのかを一行で残しておくこと。 この線引きを、私はいまも守るようにしております。
まずは ledger タブを作るところから、始めていただければと思います。