7月1日の朝、Claude Sonnet 5 の導入価格(100万入力トークンあたり $2、出力 $10)の告知を確認して、夜間バッチのモデルを Opus 4.8 から段階的に移す検討を始めました。
私自身、複数サイト向けの定期バッチをプロンプトキャッシュ前提で回しております。まず試しに1系統だけ Sonnet 5 へ向けたところ、その晩の実行ログで cache_read_input_tokens がゼロに落ち、代わりに cache_creation_input_tokens が全実行に立っていました。単価が6割下がったはずの晩に、その系統の請求はむしろ増えていた。この「切替初日の逆転現象」が、本稿の出発点です。
プロンプトキャッシュはモデルごとに別の世界です。ここを設計に織り込まずに移行すると、安くするための切替がしばらくの間だけ高くつきます。順に整理してまいります。
切替の朝に何が起きるか — キャッシュはモデル単位で分離される
Anthropic のプロンプトキャッシュは、キャッシュされたプレフィックスがモデルごとに独立しています。claude-opus-4-8 で温めたプレフィックスは、claude-sonnet-5 へのリクエストからは一切参照されません。組織・モデル・プレフィックス内容の組が一致して初めてヒットします。
料金の構造も思い出しておきます。キャッシュ書き込みは基本入力単価の 1.25 倍(5分 TTL の場合。1時間 TTL は 2 倍)、キャッシュ読み取りは 0.1 倍です。つまり切替初日は、全系統のプレフィックスが「0.1 倍で読めていたもの」から「1.25 倍で書き直すもの」に変わります。キャッシュされていた部分だけ見れば、単価はその瞬間 12.5 倍。
具体的な数字にします。8,000 トークンのプレフィックスを共有する系統で、Sonnet 5 の導入価格を使う場合を考えます。
- ウォームな実行 — 読み取り 8,000 × $0.2/MTok = 約 $0.0016
- コールドな実行 — 書き込み 8,000 × $2.5/MTok = 約 $0.02
1回あたりの差は小さく見えますが、系統が 10 個、1日の実行が数百回の規模になると、移行のやり方次第でこの差が数日から数週間続きます。逆に言えば、コールドライトは本来「系統ごとに 1 回」で済むはずのものです。何回払うかは移行戦略が決めます。
割合ベースの段階移行がキャッシュ経済を二重に壊す理由
モデル移行の定石として「まず 10%、様子を見て 50%、最後に 100%」というリクエスト単位の割合分割を考えたくなります。品質リスクの管理としては合理的ですが、プロンプトキャッシュの観点では最悪の分割です。壊れ方が二つあります。
一つ目は、二重のコールドライト。 割合分割ではすべてのプレフィックスが両方のモデルに流れます。プレフィックスが 12 種類あれば、コールドライトは 12 回ではなく 24 回。さらに TTL が切れるたびに、両側で再ウォームが発生し続けます。
二つ目が本命で、TTL 飢餓です。 5分 TTL は「使うたびに延長」されますが、延長されるのはそのモデル側のキャッシュだけです。4分間隔で回る監視系のタスクを考えます。100% 片側なら、2回目以降は常に TTL 内でヒットし続けます。これを 50/50 に分割すると、各モデルから見た平均到着間隔は 8 分に伸び、5分 TTL を毎回超えます。結果、両側でほぼ全実行がコールドライトになる。ヒット率 100% だった系統が、分割した瞬間に双方 0% 近くへ落ちます。
まとめると、こうなります。
| 移行戦略 | コールドライト回数 | ヒット率への影響 | 品質検証 |
|---|
| 一括切替 | 系統ごとに1回 | 切替直後のみ低下 | 全系統を同時に賭ける |
| 割合分割(リクエスト単位) | 全系統 × 両モデル、TTL切れごとに再発 | 疎な系統は両側ほぼ0%に低下 | 細かく制御できる |
| コホート・カットオーバー(系統単位) | 移行した系統ごとに1回 | 移行済み・未移行とも維持 | 系統単位で段階検証できる |
割合分割の「品質検証のしやすさ」だけを残し、キャッシュの壊れ方を避けるのが、次のコホート・カットオーバーです。
タスク系統ごとに切り替える — コホート・カットオーバーの設計
原則は一つだけです。同じプレフィックスを共有する実行群(タスク系統)を、決して二つのモデルに割らない。 移行の単位をリクエストではなく系統にします。
設定はこれだけの構造で足ります。
// cutover.ts — タスク系統単位のモデル解決
type ModelId = "claude-opus-4-8" | "claude-sonnet-5";
interface TaskFamily {
id: string; // 例: "nightly-digest", "log-triage"
prefixTokens: number; // 共有プレフィックスの概算トークン数
runsPerDay: number;
avgIntervalMin: number; // 実行間隔の目安(TTL判断に使う)
model: ModelId; // 現在の所属モデル
cutoverAt?: string; // ISO日付。これ以降は newModel に切替
newModel?: ModelId;
}
export function resolveModel(f: TaskFamily, now = new Date()): ModelId {
if (f.cutoverAt && f.newModel && now >= new Date(f.cutoverAt)) {
return f.newModel;
}
return f.model;
}
// ガード: 同一系統が同日に2モデルへ割れる設定を機械的に拒否する
export function assertNoSplit(families: TaskFamily[]): void {
for (const f of families) {
if (f.cutoverAt && !f.newModel) {
throw new Error(`${f.id}: cutoverAt があるのに newModel が未指定です`);
}
}
}
呼び出し側は model: resolveModel(family) を渡すだけです。ポイントは、切替日時を系統ごとに持たせて、移行の順番を設計変数にすることにあります。
順番の決め方について、私はこの基準を好みます。
- プレフィックス再利用の少ない系統から移す — 1日数回しか走らない系統は、もともとキャッシュの恩恵が薄く、コールドライトの損失も小さい。品質の観察台として最適です
- 実行間隔が TTL を超えている系統を先に — どうせ毎回コールドの系統は、移行しても失うものがありません
- 再利用の濃い基幹系統は最後に — 数日分の観察結果が溜まってから、確信を持って移します
もう一つ、切替当日の朝に効く小技として、プレウォーム実行を仕込みます。バッチ窓の直前に、本文を最小限にしたリクエストを新モデルへ1本だけ投げ、プレフィックスを書き込んでおく。初回バッチの先頭がコールドライトの遅延を踏まずに済みます。
// prewarm.ts — 切替日の初回バッチ前に1回だけ実行する
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
export async function prewarmPrefix(model: ModelId, systemPrompt: string) {
await client.messages.create({
model,
max_tokens: 1,
system: [
{ type: "text", text: systemPrompt, cache_control: { type: "ephemeral" } },
],
messages: [{ role: "user", content: "ok" }],
});
}
なお、キャッシュ可能な最小プレフィックス長(Sonnet / Opus 系で 1,024 トークン)を下回る系統は、そもそもこの議論の外です。移行順を考える前に、系統一覧から除外しておきます。
usage ブロックでヒット率を系統別に測る
移行判断は感覚ではなく、レスポンスの usage ブロックで行います。見るフィールドは三つです。
// cache-metrics.ts — 系統×モデル別のヒット率集計
interface RunUsage {
familyId: string;
model: string;
inputTokens: number; // usage.input_tokens
cacheRead: number; // usage.cache_read_input_tokens
cacheWrite: number; // usage.cache_creation_input_tokens
}
export function hitRate(runs: RunUsage[]): Map<string, number> {
const acc = new Map<string, { read: number; total: number }>();
for (const r of runs) {
const key = `${r.familyId}/${r.model}`;
const cur = acc.get(key) ?? { read: 0, total: 0 };
cur.read += r.cacheRead;
cur.total += r.cacheRead + r.cacheWrite;
acc.set(key, cur);
}
return new Map(
[...acc].map(([k, v]) => [k, v.total === 0 ? 0 : v.read / v.total])
);
}
私の運用では、この集計を日次ログに落として「系統×モデル」の行が二重化していないかを見ています。同じ familyId が同日に2モデル分の行を持っていたら、それは系統の割れが起きているサインです。resolveModel を通さない直書きのリクエストが紛れ込んだときに、この表で捕まえられます。切替後2〜3日はヒット率が移行前の水準(実測では 90% 前後)へ戻ることを確認してから、次のコホートに進みます。
移行コストシミュレータ — 8月31日の導入価格期限から逆算する
Sonnet 5 の導入価格は 2026-08-31 までで、以降は $3 / $15 に戻ります。キャッシュ書き込み(×1.25)も読み取り(×0.1)も基本入力単価の倍率なので、移行検証そのものが 8月中は約33%安いことになります。検証を先送りする理由が一つ減る、という見方をしております。
戦略比較は手計算では追い切れないので、簡単なシミュレータで済ませます。
// simulate.ts — 移行戦略ごとの30日コスト概算
interface Pricing { inPerMTok: number; outPerMTok: number }
const SONNET5_INTRO: Pricing = { inPerMTok: 2, outPerMTok: 10 };
const OPUS48: Pricing = { inPerMTok: 5, outPerMTok: 25 };
interface Family { prefix: number; fresh: number; out: number; runs: number; warmRatio: number }
function dailyCost(f: Family, p: Pricing, coldWritesPerDay: number): number {
const warmRuns = f.runs * f.warmRatio;
const read = (warmRuns * f.prefix / 1e6) * p.inPerMTok * 0.1;
const write = (coldWritesPerDay * f.prefix / 1e6) * p.inPerMTok * 1.25;
const fresh = (f.runs * f.fresh / 1e6) * p.inPerMTok;
const out = (f.runs * f.out / 1e6) * p.outPerMTok;
return read + write + fresh + out;
}
// 例: 8kプレフィックス・2k入力・1.5k出力・1日40回・平常時ヒット率90%
const fam: Family = { prefix: 8000, fresh: 2000, out: 1500, runs: 40, warmRatio: 0.9 };
console.log("Opus 4.8 継続 :", dailyCost(fam, OPUS48, 4).toFixed(3));
console.log("Sonnet 5 一括切替:", dailyCost(fam, SONNET5_INTRO, 4).toFixed(3));
// 割合分割: 両モデルで warmRatio が落ち、コールドライトが両側で増える
console.log(
"50/50 分割 :",
(
dailyCost({ ...fam, runs: 20, warmRatio: 0.3 }, OPUS48, 14) +
dailyCost({ ...fam, runs: 20, warmRatio: 0.3 }, SONNET5_INTRO, 14)
).toFixed(3)
);
手元の系統でこの概算を回すと、Opus 4.8 継続が日額 $1.90 前後、Sonnet 5 へのコホート一括切替が $0.76 前後まで下がる一方、50/50 のリクエスト分割は $1.44 前後で止まりました。分割は「安いモデルを半分使っているのに、継続比で 24% しか下がらない」わけです。しかも品質観察は両側で薄まります。数字で見ると、割合分割を選ぶ理由がほぼ残りません。
公式ドキュメントの倍率表からは読み取れない運用知見
最後に、実際に切り替えてみて分かった細部を残しておきます。
切替週だけ 1 時間 TTL を検討する。 1h TTL の書き込みは基本単価の 2 倍で、5分 TTL(1.25 倍)より割高です。ただし実行間隔が 5〜60 分の系統では、5分 TTL だと毎回コールドライト(1.25 倍を毎回)、1h TTL なら最初の 2 倍だけで以降 0.1 倍が続きます。実行が 1 時間に 2 回以上あるなら 1h TTL が勝ちます。移行の観察期間だけ実行頻度を落としている系統には、特に効きます。
ロールバックの費用は小さい。判断を鈍らせない。 切替から数分で旧モデル側のキャッシュは TTL 切れで消えているので、「戻すと再ウォームがもったいない」という感覚は錯覚です。戻す費用は系統ごとにコールドライト 1 回分だけ。ロールバックのトリガーは費用ではなく、出力品質の回帰(既存のゴールデンデータセット比較など)に置くべきです。
モデル既定値に乗らず、必ず明示指定する。 6月30日以降、プラットフォーム側の既定モデルが Sonnet 5 へ動いています。モデル未指定の箇所が残っていると、こちらの移行計画と無関係に系統が割れます。個人開発の規模でも、モデル ID は設定ファイル一箇所(前述の TaskFamily 定義)に集約し、リクエスト構築コードには文字列リテラルを書かない運用を勧めます。
次のアクションとしては、お手元の実行ログから cache_read_input_tokens と cache_creation_input_tokens を系統別に集計し、「実行間隔が TTL を超えている系統」を一つ選んで最初のコホートにしてみてください。失うキャッシュがない系統から始めれば、切替の観察はただの得になります。
切替の朝を静かに越えるための一助になれば嬉しく思います。