7月2日に Claude Sonnet 5 が各プランの既定モデルになり、導入価格は100万トークンあたり入力 $2・出力 $10 と発表されました。Opus 4.8($5/$25)と比べると入出力とも約40%、導入価格なら約60%安い計算です。私自身、運営しているブログ群の夜間バッチをすぐに切り替えて、翌朝の日次コスト台帳を楽しみに開きました。
ところが下がり幅は約18%。単価は6割引きなのに、です。usage ログを突き合わせて分かったのは、ツールを回すタスクでターン数が中央値5から7に増えており、その2ターンが入力トークンを想像以上に膨らませていたことでした。単価の比較表だけを見てモデル移行の損益を判断すると、この現象は最後まで見えません。
ここでは「タスク当たり実効コスト」という物差しを立てて、消費プロファイルの記録、新旧モデルの並走計測、損益分岐ターン数の逆算までを一続きの設計として組んでいきます。個人開発の少人数運用でも入れられる小さな仕掛けですが、移行判断の精度は目に見えて変わります。
タスク単価は「単価ベクトル × 消費ベクトル」で決まります
モデルの請求は、単価そのものではなく、単価と消費量の内積で決まります。
成分 単価側 消費側 入力 $/MTok(入力) タスク完了までに送った入力トークン総量 出力 $/MTok(出力) 生成トークン総量 キャッシュ読取 入力より大幅に安い読取単価 キャッシュにヒットした入力分 リトライ — 失敗して再実行した分の全成分
単価ベクトルはモデルを替えた瞬間に切り替わりますが、消費ベクトルも同時に変わります。Sonnet 5 は計画とツール利用が強化された「エージェント的」なモデルとされており、実際に使うと、同じタスクでもツールを呼ぶ回数や出力の長さが Opus 4.8 と同じにはなりません。増える系統もあれば減る系統もあります。つまり移行の損益は、単価表からは原理的に読めない、というのが出発点です。
ターン数は入力トークンを二乗で膨らませます
ツールループの各ターンでは、会話履歴全体を入力として送り直します。システムプロンプトと初期コンテキストを S、1往復ごとに増える履歴(tool_result と直前の出力)を d とすると、n ターンのタスクの入力総量はおよそ次のようになります。
入力総量 ≈ n×S + d×(0 + 1 + ... + (n-1)) = n×S + d×n(n-1)/2
第2項が n の二乗で効きます。S = 3,000、d = 1,200(tool_result 800 + 直前出力 400)、1ターンの出力 400 トークンという、私のリンク検証エージェントに近い形状で実額を並べます。
モデルと単価 4ターン 6ターン Opus 4.8・4ターン比 Opus 4.8($5/$25) $0.136 $0.240 基準 / +76% Sonnet 5 導入価格($2/$10) $0.054 $0.096 -60% / -29% Sonnet 5 標準価格($3/$15) $0.082 $0.144 -40% / +6%
同じ4ターンで走れば、割引率は単価どおり60%・40%です。しかし移行後にターンが2つ増えるだけで、導入価格でも節約は29%まで縮み、9月1日以降の標準価格では Opus 4.8 より6%高くなります。「40%安いモデルに替えたのに請求が増えた」は、この形状のタスクでは普通に起こる算術です。プロンプトキャッシュを併用すれば二乗の傾きは緩みますが、キャッシュはモデル単位で分離されるため移行直後はヒットが期待できません。この点はモデル切替とプロンプトキャッシュ再ウォームの設計 で扱ったとおりです。
消費ベクトルを記録する最小の計測レイヤー
判断の材料は response.usage に全部あります。足りないのは「タスク単位で束ねる」ことだけです。ターンごとの usage をタスクIDで積み上げます。
// consumption.ts — タスク単位の消費ベクトルを積み上げる
export interface ConsumptionVector {
taskId : string ;
model : string ;
turns : number ; // API呼び出し回数(ツール往復を含む)
inputTokens : number ; // 非キャッシュ入力
outputTokens : number ;
cacheReadTokens : number ; // cache_read_input_tokens
cacheWriteTokens : number ; // cache_creation_input_tokens
retries : number ; // 失敗→再実行した回数
completed : boolean ; // 成功ゲートを通過したか
}
export function emptyVector ( taskId : string , model : string ) : ConsumptionVector {
return { taskId, model, turns: 0 , inputTokens: 0 , outputTokens: 0 ,
cacheReadTokens: 0 , cacheWriteTokens: 0 , retries: 0 , completed: false };
}
// 1ターンぶんの usage を加算する。usage が欠けたレスポンスは
// 「消費不明」として扱い、後段の集計から除外できるよう印を残す
export function addTurn ( v : ConsumptionVector , usage : {
input_tokens : number ; output_tokens : number ;
cache_read_input_tokens ?: number ; cache_creation_input_tokens ?: number ;
}) : ConsumptionVector {
return {
... v,
turns: v.turns + 1 ,
inputTokens: v.inputTokens + usage.input_tokens,
outputTokens: v.outputTokens + usage.output_tokens,
cacheReadTokens: v.cacheReadTokens + (usage.cache_read_input_tokens ?? 0 ),
cacheWriteTokens: v.cacheWriteTokens + (usage.cache_creation_input_tokens ?? 0 ),
};
}
ここで一つだけ運用上の癖を足しています。リトライは「別のタスク」ではなく同じ taskId に積むことです。失敗して打ち直した分も、そのタスクを完了させるために支払ったコストだからです。リトライを別勘定にすると、失敗しやすいモデルほど安く見えるという逆立ちした集計になります。
同じタスクを両モデルで走らせる並走ハーネス
次に、同一のタスク仕様を新旧モデルで走らせて消費ベクトルを対で取ります。ポイントは、成功ゲート(そのタスクが本当に完了したかの判定関数)を挟むことです。速くて安くても完了していなければ、それはコストであって成果ではありません。
// paired-run.ts — 新旧モデルの並走計測
import { ConsumptionVector, emptyVector, addTurn } from "./consumption" ;
interface TaskSpec {
id : string ;
run : ( model : string , onUsage : ( u : any ) => void ) => Promise < unknown >;
succeeded : ( result : unknown ) => boolean ; // 成功ゲート
maxRetries : number ;
}
export async function measureOnce (
spec : TaskSpec , model : string
) : Promise < ConsumptionVector > {
let v = emptyVector (spec.id, model);
for ( let attempt = 0 ; attempt <= spec.maxRetries; attempt ++ ) {
if (attempt > 0 ) v = { ... v, retries: v.retries + 1 };
try {
const result = await spec. run (model, ( usage ) => { v = addTurn (v, usage); });
if (spec. succeeded (result)) return { ... v, completed: true };
} catch { /* 失敗はリトライ勘定に積んで継続 */ }
}
return v; // completed: false のまま返し、集計側で分けて扱う
}
// 同じ仕様を両モデルで N 回ずつ。到着順の偏りを避けるため交互に流す
export async function pairedRun (
spec : TaskSpec , models : [ string , string ], n : number
) : Promise < ConsumptionVector []> {
const out : ConsumptionVector [] = [];
for ( let i = 0 ; i < n; i ++ ) {
for ( const m of models) out. push ( await measureOnce (spec, m));
}
return out;
}
回数は系統ごとに10回も走らせれば傾向が見えます。私は夜間バッチの実タスクからサンプリングして流しました。合成ベンチマークではなく自分の本番タスクで測るのが肝心で、ここを省くと「ベンチでは勝ったのに請求で負ける」が起こります。
損益分岐ターン数を逆算しておきます
並走の結果を待つ間に、理論側の防衛線も引けます。「新モデルは何ターンまで増えても旧モデルより安いか」は、タスク形状(S, d, 出力/ターン)と両モデルの単価だけで解けます。
// break-even.ts — 移行を許容できる増分ターン数の逆算
interface Price { inPerMTok : number ; outPerMTok : number ; }
interface Shape { base : number ; growthPerTurn : number ; outPerTurn : number ; }
function taskCost ( p : Price , s : Shape , turns : number ) : number {
const input = turns * s.base + s.growthPerTurn * (turns * (turns - 1 )) / 2 ;
const output = turns * s.outPerTurn;
return (input * p.inPerMTok + output * p.outPerMTok) / 1e6 ;
}
// 旧モデル baselineTurns のコストを上限に、新モデルが並ぶターン数を探す
export function breakEvenTurns (
oldP : Price , newP : Price , s : Shape , baselineTurns : number
) : number {
const ceiling = taskCost (oldP, s, baselineTurns);
let lo = baselineTurns, hi = baselineTurns * 4 ;
for ( let i = 0 ; i < 40 ; i ++ ) {
const mid = (lo + hi) / 2 ;
taskCost (newP, s, mid) < ceiling ? (lo = mid) : (hi = mid);
}
return lo;
}
const shape = { base: 3000 , growthPerTurn: 1200 , outPerTurn: 400 };
const opus = { inPerMTok: 5 , outPerMTok: 25 };
console. log ( breakEvenTurns (opus, { inPerMTok: 2 , outPerMTok: 10 }, shape, 4 )); // ≈ 7.6
console. log ( breakEvenTurns (opus, { inPerMTok: 3 , outPerMTok: 15 }, shape, 4 )); // ≈ 5.7
この形状なら、導入価格の間は約7.6ターンまで、標準価格に戻ると約5.7ターンまでが許容ラインです。逆に言うと、標準価格下で中央値が6ターンを超える系統は、単価が40%安くても移行で損をします。導入価格は8月31日までなので、判断は必ず標準価格側でも計算しておきます。この「恒久判断は標準単価で」という線引きは導入価格の期限を織り込むコスト予測 と同じ考え方です。
1週間並走して、系統ごとに答えが割れました
実際に3系統を1週間並走させた結果です。母数は各系統とも本番タスクからのサンプリングで、金額はタスク当たり実効コスト(リトライ込み・未完了は除外して別掲)です。
タスク系統 ターン中央値(Opus→Sonnet 5) タスク単価の変化 判断 記事下書き生成(単発・ツールなし) 1 → 1 -59% 即移行 内部リンク検証(ツールループ) 5 → 7 -18% プロンプト改修後に移行 タグ分類(短出力バッチ) 1 → 1(出力 -22%) -63% 即移行
単発系はほぼ単価どおりに下がりました。消費ベクトルが変わらないなら、割引は素直に効きます。問題のリンク検証は、Sonnet 5 が丁寧に検証手順を細分化する傾向があり、ターンが増えていました。ここは「1回の応答で最大3件まとめて検証し、往復は5回以内」と上限を仕様に明記する改修を入れたところ、中央値は5ターンに収まり、タスク単価は-41%まで改善しました。モデルの計画性が上がった分、こちらが上限を言語化しないと丁寧側に振れる、というのが今回いちばんの学びです。
計測の落とし穴を先に潰しておきます
実装よりも集計設計でつまずきやすいので、私が踏んだ順に残しておきます。
未完了タスクを平均に混ぜない — completed: false の消費は「完了単価」とは別の指標(無駄撃ち率)として掲示します。混ぜると失敗の多いモデルが安く見えます。
キャッシュ成分を入力と合算しない — 移行直後はキャッシュが冷えているため、初週の実効コストは定常状態より高めに出ます。cacheReadTokens を分けて持っておくと、温まった後の再計算ができます。
max_tokens とリトライ方針は両モデルで揃える — 片方だけ上限や再試行条件が違うと、モデル差ではなく設定差を測ることになります。
効果測定の窓を跨いで単価を変えない — 8月31日の導入価格終了を窓の途中に挟むと、消費の変化と単価の変化が混ざります。窓は価格が一定の期間で切ります。
まとめ — 最初の一歩は「ターン数の分布を出す」ことです
移行判断の前に、いま動いているタスク系統ごとに usage ログからターン数の中央値と分布をまず出してみてください。それだけで、単価表の割引がそのまま効く系統(1〜2ターン)と、消費プロファイル次第で逆転し得る系統(5ターン超)が仕分けできます。並走ハーネスを流すのは、その次で十分です。私自身、この順番にしてから移行の意思決定が半日で終わるようになりました。