9月の頭に単価表を貼り替えようとして、手が止まりました。入力も出力も、数字がひとつも動いていなかったのです。
動いていたのは単価ではなく、掛け算のほうでした。私はコスト集計のヘルパーに cacheRead = input * 0.1 と書いており、その 0.1 を「モデルによらない定数」だと思い込んでいました。単価表を見に行く習慣はあっても、倍率を見に行く習慣がなかったのです。
Lab のサイト群では、長めの運用ガイドラインを毎ターン読み直させる作りの処理をいくつも回しております。その手のワークロードほど今回の変更が効くはずで、逆にほとんど効かない形もあるはずだ——そう考えて、貼り替える前に自分で計算してみました。
変わったのは単価ではなく、読み取りの倍率です
プロンプトキャッシュの料金は、ベース入力単価に対する倍率で決まります。ここが今回動きました。
| キャッシュ操作 | 倍率 | 有効期間 |
|---|---|---|
| 5 分キャッシュ書き込み | ベース入力の 1.25 倍 | 5 分 |
| 1 時間キャッシュ書き込み | ベース入力の 2 倍 | 1 時間 |
| キャッシュ読み取り(ヒット) | ベース入力の 0.1 倍 Fable 5.1 と Mythos 5.1 のみ 0.025 倍 | 直前の書き込みと同じ |
つまり Fable 5.1 のキャッシュ読み取りは 100 万トークンあたり $1.00 から $0.25 になりました。ベース入力の $10、出力の $50、書き込みの $12.50 と $20 はそのままです。倍率の例外は現時点でこの 2 モデルだけで、Opus 5 も Sonnet 5 も Haiku 4.5 も 0.1 倍のままです。数字の出どころは Claude Platform の料金ページ にあります。
ここを「Fable 5.1 は 75% 安い」と要約してしまうと、読んだ人の見積もりが壊れます。安くなったのは請求書のうち cache read の行だけ です。
自分の構成でいくら下がるかを、先に計算します
見積もりに要るのは、レスポンスの usage に入っている 4 つの数字だけです。推測ではなく、直近のリクエストから実際の内訳を取ってきます。
# usage の例(1 リクエスト分)
usage = {
"input_tokens": 2000, # キャッシュに乗らなかった新しい入力
"cache_read_input_tokens": 60000, # キャッシュから読んだ分
"cache_creation_input_tokens": 0, # 今回書き込んだ分
"output_tokens": 1500,
}
def request_cost(usage, base_in, base_out, read_mult, write_mult=1.25):
"""1 リクエストの費用をドルで返します。単価は 100 万トークンあたり。"""
return (
usage["input_tokens"] * base_in
+ usage["cache_read_input_tokens"] * base_in * read_mult
+ usage["cache_creation_input_tokens"] * base_in * write_mult
+ usage["output_tokens"] * base_out
) / 1_000_000
before = request_cost(usage, 10.0, 50.0, read_mult=0.1)
after = request_cost(usage, 10.0, 50.0, read_mult=0.025)
print(f"{before:.6f} -> {after:.6f} ({(before - after) / before:.1%} 減)")
# 0.155000 -> 0.110000 (29.0% 減)倍率を引数にしてあるところが要点です。ここを定数で埋めてしまうと、モデルを切り替えた瞬間に見積もりが静かにずれます。落ちてくれないぶん、たちが悪いのです。
効き方が 10 倍違った三つの形
同じ関数に、性質の違う 3 つのリクエストを通してみました。単価は Fable 系の $10 / $50 で揃えてあります。
| ワークロードの形 | cache read / 新規入力 / 出力 | 0.1 倍 | 0.025 倍 | 削減 |
|---|---|---|---|---|
| 長い前提を毎ターン読み直すエージェント | 60,000 / 2,000 / 1,500 | $0.1550 | $0.1100 | 29.0% |
| 分類・要約のバッチ | 20,000 / 500 / 300 | $0.0400 | $0.0250 | 37.5% |
| 短い前提で長文を書かせる生成 | 8,000 / 1,000 / 4,000 | $0.2180 | $0.2120 | 2.8% |
いちばん上といちばん下で、削減幅がおよそ 10 倍違います。割引率はどちらも同じ 75% です。
差を作っているのは、cache read が総額に占める割合でした。エージェント型では 38.7%、バッチでは 50.0%、生成型では 3.7% です。出力トークンが主役の構成では、入力側をいくら削っても請求書はほとんど動きません。
割引率を決めるのは単価表ですが、請求書を決めるのは自分のトークンの内訳です。 発表の数字をそのまま自分の削減幅として期待していた最初の見積もりは、私の場合、生成寄りの処理でまるで届きませんでした。
倍率を定数で持っているコードを、表に移します
直し方は難しくありません。単価と一緒に倍率もモデル単位で持たせるだけです。
PRICING = {
# base_in, base_out, cache_read_mult(100 万トークンあたりのドル)
"claude-fable-5-1": (10.0, 50.0, 0.025),
"claude-fable-5": (10.0, 50.0, 0.1),
"claude-opus-5": (5.0, 25.0, 0.1),
"claude-sonnet-5": (2.0, 10.0, 0.1),
"claude-haiku-4-5-20251001": (1.0, 5.0, 0.1),
}
def cost_for(model, usage):
if model not in PRICING:
# 未知のモデルを 0 円で素通りさせないこと
raise ValueError(f"単価未登録のモデルです: {model}")
base_in, base_out, read_mult = PRICING[model]
return request_cost(usage, base_in, base_out, read_mult)自分のリポジトリで危ないところを探すなら、この一行で足ります。
grep -rn "0\.1\s*\*\|\* 0\.1\b" --include='*.py' --include='*.ts' src/ | grep -i cache私は「単価は変わるが倍率は変わらない」と思い込んでいたぶん、単価表だけを外部化して倍率をコードに残していました。いまは 単価と倍率を同じ表に置き、片方だけ更新できない形 にしています。
5 分と 1 時間の選び方は、これでは変わりません
ここは意外でした。読み取りが 4 分の 1 になったのだから、TTL の判断も動くだろうと考えたのです。
呼び出しの間隔が 5 分を超えて 1 時間以内に収まる場合、5 分キャッシュは毎回書き直しになります。1 時間あたりの呼び出し回数を K として損益が並ぶ点を解くと、こうなります。
| 読み取り倍率 | 分岐点 K(1 時間あたりの呼び出し回数) | 結論 |
|---|---|---|
| 0.1 倍 | 1.65 回 | 2 回以上なら 1 時間 TTL が有利 |
| 0.025 倍 | 1.61 回 | 2 回以上なら 1 時間 TTL が有利 |
分岐点はほとんど動きません。書き込みの倍率(1.25 倍と 2 倍)が支配的だからです。同じく「キャッシュを入れて元が取れるか」も、どちらの倍率でも読み取り 1 回で回収できます。
読み取りの単価だけを見て TTL を引き直そうとしたのは、私の勇み足だったのかもしれません。安くなった行と、設計を決めている行は別だったのです。
読み取りが安くなったのは、すでにキャッシュが効いている構成の請求を薄くする変化であって、キャッシュ設計そのものを引き直す理由にはなりませんでした。ヒット率が伸びていない状態でモデルだけ替えても、効果は出ません。設計の側から入るなら、プロンプトキャッシュで月額コストを半分にした実装メモ にヒット率の測り方をまとめてあります。
次の一歩
まずは直近 1 日ぶんのレスポンスから cache_read_input_tokens を合計し、それが総コストの何割を占めているかだけ出してみてください。その割合に 0.75 を掛けた数字が、モデルを移したときに期待できる上限です。
私はこの一手間を惜しんで、貼り替えたあとに「思ったより変わらない」と首をかしげるところでした。読んでくださってありがとうございます。少しでも見積もりの手戻りが減れば嬉しく思います。