Claude Sonnet 5 が既定モデルになった発表を追いかけながら、自動処理のモデル指定を切り替えていた 7 月最初の朝のことです。切り替え自体は数行の変更で終わったのですが、あわせて更新した月次コストの試算シートを眺めていて、手が止まりました。この試算、9 月も正しいのだろうか、と。
Sonnet 5 の $2 / $10(百万トークンあたり入力 / 出力)は導入価格で、2026 年 8 月 31 日までです。9 月からは標準の $3 / $15 に戻ります。つまりいま作った試算は 2 か月後に確実にずれる、それも例外もエラーも出さずに静かにずれる、ということに気づきました。個人開発で複数の自動処理を細く長く回している身としては、コードの破損より請求の予測が黙って外れるほうが困ります。今回は、この「期限つき単価」を予測の仕組みに正面から組み込んだ設計を共有します。
何が、いつ、どれだけ変わるのか
まず事実関係を整理します。2026-06-30 に公開された Sonnet 5 は、次の価格構造を持っています。
| モデル | 適用期間 | 入力(/MTok) | 出力(/MTok) |
| claude-sonnet-5(導入価格) | 2026-06-30 〜 2026-08-31 | $2 | $10 |
| claude-sonnet-5(標準価格) | 2026-09-01 〜 | $3 | $15 |
| claude-opus-4-8 | 期限なし | $5 | $25 |
見てのとおり、入力も出力も同じ 1.5 倍です。この「両方とも 1.5 倍」という点は後で効いてきます。入出力の比率がどんなワークロードでも、Sonnet 5 に載っている部分のコストはちょうど 50% 増えるからです。
もうひとつ注意したいのは、切り替えの時刻境界です。8 月 31 日「まで」がどのタイムゾーンの日付で締まるのかは、請求システムの都合に依存します。予測の用途では厳密に当てる必要はなく、高い側に丸めるのが安全です。私は JST で 8 月 31 日の 0 時以降は標準価格が適用されるものとして見積もる、つまり悪いほうに 1 日ずらす前提を置いています。予測が実請求より少し高く出る分には意思決定を誤りませんが、逆は困るためです。
静的な単価表は 9 月に黙ってずれ始めます
問題の形を先に見せます。モデル単価をコードに直書きしている、よくある実装です。
// Before: 単価が「今日の値」で固定されている
const PRICING: Record<string, { inPerMTok: number; outPerMTok: number }> = {
"claude-sonnet-5": { inPerMTok: 2, outPerMTok: 10 }, // ← 導入価格を直書き
"claude-opus-4-8": { inPerMTok: 5, outPerMTok: 25 },
};
function estimateCost(model: string, inTok: number, outTok: number): number {
const p = PRICING[model];
if (!p) throw new Error(`unknown model: ${model}`);
return (inTok / 1e6) * p.inPerMTok + (outTok / 1e6) * p.outPerMTok;
}
このコードは 8 月 31 日までは正しく、9 月 1 日からは一律に安すぎる値を返します。例外は出ません。型も通ります。テストも緑のままです。ずれるのは請求書だけです。
どれくらいずれるかを、私自身の運用量で計算してみます。手元の自動処理群は Sonnet 系のモデルでおおむね 1 日あたり入力 6.0M・出力 0.9M トークンを消費しています。30 日換算で入力 180M・出力 27M です。
- 導入価格: 180 × $2 + 27 × $10 = $360 + $270 = 月 $630
- 標準価格: 180 × $3 + 27 × $15 = $540 + $405 = 月 $945
差は月 $315 です。そして直書きの単価表で 9 月を予測すると $630 と出ます。実請求 $945 に対して約 33% の過小評価です。3 割違う予測は、予測がないのとあまり変わりません。
単価に「有効期間」を持たせる
対策の本体は単純で、単価を「モデル → 値」の写像ではなく「モデル × 期間 → 値」の行の集まりとして持つことです。会計システムで実効日付(effective date)と呼ばれる、昔からある考え方をそのまま使います。
// After: 単価行に有効期間を持たせる
type PriceWindow = {
model: string;
inPerMTok: number;
outPerMTok: number;
validFrom: string; // ISO 日付・この日を含む
validUntil: string | null; // ISO 日付・この日を含まない。null は無期限
};
const PRICE_BOOK: PriceWindow[] = [
{ model: "claude-sonnet-5", inPerMTok: 2, outPerMTok: 10,
validFrom: "2026-06-30", validUntil: "2026-08-31" }, // 高い側へ1日丸めた導入価格
{ model: "claude-sonnet-5", inPerMTok: 3, outPerMTok: 15,
validFrom: "2026-08-31", validUntil: null }, // 標準価格
{ model: "claude-opus-4-8", inPerMTok: 5, outPerMTok: 25,
validFrom: "2026-06-30", validUntil: null },
];
function resolvePrice(model: string, onDate: string): PriceWindow {
const hits = PRICE_BOOK.filter(
(w) =>
w.model === model &&
w.validFrom <= onDate &&
(w.validUntil === null || onDate < w.validUntil)
);
if (hits.length === 0) {
// fail-closed: 単価が分からない日付のコストを 0 円扱いで通さない
throw new Error(`no price window: ${model} on ${onDate}`);
}
if (hits.length > 1) {
throw new Error(`overlapping price windows: ${model} on ${onDate}`);
}
return hits[0];
}
function estimateCostOn(
model: string, onDate: string, inTok: number, outTok: number
): number {
const p = resolvePrice(model, onDate);
return (inTok / 1e6) * p.inPerMTok + (outTok / 1e6) * p.outPerMTok;
}
設計判断は 3 つあります。
- 日付は ISO 文字列の辞書順比較で扱う。Date オブジェクトを経由するとタイムゾーンの解釈が入り込みます。日付粒度の判定に時刻は不要です。
- 該当行ゼロ件は例外にする。単価の分からない日付を黙って 0 円にすると、予測の穴は請求書が届くまで見えません。未知モデルを弾く発想については、以前書いた 新モデルが増えてもコスト集計をズラさない単価レジストリの設計 と同じ理由づけです。あちらは「知らないモデル」を弾く話でしたが、今回は「知っているモデルの知らない日付」を弾きます。
- 重複期間も例外にする。行の追加ミスで期間が重なると、フィルタの並び順という偶然で単価が決まってしまいます。決定的でない価格解決はデバッグのしようがありません。
月次予測に価格ステップを描き込む
単価が日付の関数になったので、予測は「日ごとに正しい単価を引いて積む」だけになります。
// 日次のトークン消費から月次コストを組み立てる
function forecastMonthly(
model: string,
dailyInTok: number,
dailyOutTok: number,
fromDate: string, // 例 "2026-07-01"
days: number
): Map<string, number> {
const byMonth = new Map<string, number>();
const cur = new Date(fromDate + "T00:00:00Z");
for (let i = 0; i < days; i++) {
const iso = cur.toISOString().slice(0, 10);
const cost = estimateCostOn(model, iso, dailyInTok, dailyOutTok);
const month = iso.slice(0, 7);
byMonth.set(month, (byMonth.get(month) ?? 0) + cost);
cur.setUTCDate(cur.getUTCDate() + 1);
}
return byMonth;
}
const f = forecastMonthly("claude-sonnet-5", 6_000_000, 900_000, "2026-07-01", 92);
// 2026-07 → $651.0(31日)
// 2026-08 → $661.5(30日分は導入価格・8/31 の1日分だけ標準価格: 30×$21 + 1×$31.5)
// 2026-09 → $945.0(30日 × $31.5)
出力を見ると、8 月の行にすでにステップの端がかかっているのが分かります(高い側へ丸めた前提のため、8/31 の 1 日分だけ標準単価)。ここが静的単価表との一番の違いで、予測のグラフに段差がそのまま現れるので、9 月の数字を見た瞬間に「これは値上がりではなく、割引の終了だ」と説明できます。予測の基本形そのものは 月次トークンコストの見積もり方 で書いた積み上げ方式と同じで、単価の解決だけが日付依存に変わっています。
なお、キャッシュ読み取りや Batches のような割引系の品目を使っている場合は、品目ごとに PriceWindow の行を分けてください。導入価格の倍率がすべての品目に一律で掛かる保証はないため、「モデル単位で 1.5 倍」と横着すると品目構成によっては再びずれます。
期限切れの取りこぼしは CI で先に捕まえる
実効日付方式には運用上の弱点が 1 つあります。後継行の入れ忘れです。validUntil で閉じた行の先に次の行がないと、その日付以降の予測が例外で止まります。止まってくれるのは fail-closed の設計どおりですが、止まるのが 9 月 1 日の朝では遅いので、先に検査します。
// 予測ホライズン全体で単価が引けることを CI で保証する
function assertCoverage(model: string, fromDate: string, horizonDays: number): void {
const cur = new Date(fromDate + "T00:00:00Z");
for (let i = 0; i < horizonDays; i++) {
const iso = cur.toISOString().slice(0, 10);
resolvePrice(model, iso); // 穴か重複があればここで例外
cur.setUTCDate(cur.getUTCDate() + 1);
}
}
// テスト: 全モデル × 今日から180日をカバーしていること
test("price book covers 180-day horizon", () => {
const today = new Date().toISOString().slice(0, 10);
for (const model of new Set(PRICE_BOOK.map((w) => w.model))) {
assertCoverage(model, today, 180);
}
});
180 日という数字は、四半期の予測を出すのに 90 日、その先の余白にもう 90 日、という程度の意味です。このテストを入れておくと「期限つきの価格告知を見た日に、終了行と後継行をペアで追加する」という運用が半ば強制されます。行を 1 本しか足していなければ、その日の CI が落ちるからです。値上げの告知だけでなく、今回のような割引の告知も、価格表の変更としては同じ形で扱えます。
導入価格で恒久判断をしないための線引き
仕組みの話はここまでで、最後は判断の話です。導入価格の期間中は Sonnet 5 が Opus 4.8 より入力・出力とも 60% 安く見えます。しかし 9 月からは 40% 安に縮みます。$2 対 $5 と $3 対 $5 では、比率の印象がかなり違います。
これが効くのは、リトライを見込んだ損益分岐です。ある処理を Opus から Sonnet に下げるとき、品質差をリトライで吸収するなら、Sonnet 側の実効コストは(1 + リトライ率)倍になります。導入価格の間は Opus の 40% の単価なので、リトライ率が 150% まで(つまり平均 2.5 回実行しても)Sonnet が勝ちます。標準価格では 60% の単価になるため、損益分岐は 67% まで下がります。7 月に「Sonnet で十分」と判断した根拠が、9 月には成立しないことがあるわけです。
そこで私は、判断を期限で 2 種類に分けています。
- 恒久判断は標準価格で計算する。既定モデルの選定、ルーティングの閾値、段階ごとのモデル割り当てといった構造の決定は、$3 / $15 を前提に損益を引きます。導入価格は「その判断がしばらく余計に得をする追い風」であって、判断の根拠にはしません。
- 前倒しできる一括処理は 8 月中に寄せる。過去ログの再分類、埋め込みの再生成、アーカイブ記事の一括要約のような「いつやってもよい大物」は、単価が 2/3 のうちに済ませます。私の手元では約 200M トークン入力相当の再処理を 8 月に予定していて、これだけで $400 と $600 の差になります。
- 切り替え済みの移行判断を 9 月の単価で再検証する。モデル移行そのものの段取りは モデル切り替え時のプロンプトキャッシュ再ウォーム設計 に書きましたが、移行の採算計算に導入価格を使っていた場合は、標準価格でもう一度引き直しておくと 9 月に慌てません。
- 9 月の初回請求で予測と突合する。実効日付の行が正しかったかは、結局のところ請求書としか照合できません。ここでずれたら、丸め方向か品目の分け方が間違っています。
まとめ — 8 月中にやっておくこと
やることは大きくありません。単価表に有効期間の列を足し、8/31 と 9/1 の 2 行を今日のうちに入れ、カバレッジ検査を CI に加える。そのうえで、恒久の設計判断は $3 / $15 で引き直し、前倒しできる重い処理だけ導入価格の窓に寄せる。これだけで、9 月 1 日の朝に予測が黙って 3 割ずれる未来は消えます。
割引には終了日があります。終了日を持てるデータ構造にしておくことが、割引を安心して使い切る条件だと考えています。同じように期限つきの告知と付き合っている方の参考になれば幸いです。