「サブスクで足りているのか、APIキーに切り替えたほうが安いのか、判断材料がない」
——この感覚は正しいです。Claude Code の画面に出る使用量は、月単位でもモデル別でもキャッシュ込みでもなく、自分で集計しないと答えが出ません。この記事では、手元に残っているセッションログ(JSONL)から日別・モデル別のトークンを集計し、単価表を掛けて「API従量課金だったらいくらか」を出すスクリプトを作ります。
なぜログから集計するのか
Claude Code は会話の記録を ~/.claude/projects/<プロジェクトのスラッグ>/*.jsonl に1行1イベントで保存します。assistant の行には message.usage があり、入力・出力・キャッシュ書き込み・キャッシュ読み取りのトークン数が入っています。外部サービスに送らず、ローカルだけで完結します。
注意点が2つあります。
- 同じ
message.idの行がストリーミングの途中経過として複数回出ることがあります。そのまま足すと二重計上になるので、message.idで重複を除きます。 - 単価はこの記事にハードコードしません。改定されるので、公式の料金ページを見て
prices.jsonに自分で書きます。
Step 1 単価表を用意する
数値は公式の料金ページで確認して埋めます(単位は USD / 100万トークン)。以下は形だけで、値は仮です。
{
"claude-sonnet-5-5": { "input": 0, "output": 0, "cacheWrite": 0, "cacheRead": 0 },
"claude-opus-5-5": { "input": 0, "output": 0, "cacheWrite": 0, "cacheRead": 0 }
}
値が 0 のままのモデルは、集計時に「単価未設定」として警告します。黙って 0 円にしないためです。
Step 2 JSONL を読んで重複を除く
// usage-report.ts
import { readdirSync, readFileSync, statSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
type Usage = {
input_tokens?: number;
output_tokens?: number;
cache_creation_input_tokens?: number;
cache_read_input_tokens?: number;
};
type Row = { day: string; model: string; usage: Usage };
function* walk(dir: string): Generator<string> {
for (const name of readdirSync(dir)) {
const p = join(dir, name);
if (statSync(p).isDirectory()) yield* walk(p);
else if (p.endsWith(".jsonl")) yield p;
}
}
export function load(root = join(homedir(), ".claude", "projects")): Row[] {
// 同一 message.id は最後に現れた行(= 最終の usage)だけを採る
const byId = new Map<string, Row>();
for (const file of walk(root)) {
for (const line of readFileSync(file, "utf8").split("\n")) {
if (!line.trim()) continue;
let ev: any;
try {
ev = JSON.parse(line);
} catch {
continue; // 書き込み途中の行は捨てる
}
const msg = ev?.message;
if (ev?.type !== "assistant" || !msg?.usage || !msg?.id) continue;
byId.set(msg.id, {
// JST の日付に揃える
day: new Date(new Date(ev.timestamp).getTime() + 9 * 3600e3)
.toISOString()
.slice(0, 10),
model: msg.model ?? "unknown",
usage: msg.usage,
});
}
}
return [...byId.values()];
}
Step 3 単価を掛けて日別・モデル別に出す
// 続き
import prices from "./prices.json" with { type: "json" };
const M = 1_000_000;
export function report(rows: Row[]) {
const acc = new Map<string, { usd: number; read: number; fresh: number }>();
const missing = new Set<string>();
for (const { day, model, usage: u } of rows) {
const p = (prices as Record<string, Record<string, number>>)[model];
if (!p || p.input === 0) {
missing.add(model);
continue;
}
const fresh = (u.input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
const read = u.cache_read_input_tokens ?? 0;
const usd =
((u.input_tokens ?? 0) * p.input +
(u.output_tokens ?? 0) * p.output +
(u.cache_creation_input_tokens ?? 0) * p.cacheWrite +
read * p.cacheRead) / M;
const key = `${day}\t${model}`;
const cur = acc.get(key) ?? { usd: 0, read: 0, fresh: 0 };
acc.set(key, { usd: cur.usd + usd, read: cur.read + read, fresh: cur.fresh + fresh });
}
for (const [key, v] of [...acc].sort()) {
const hit = v.read / Math.max(1, v.read + v.fresh);
console.log(`${key}\t$${v.usd.toFixed(2)}\tcache読取率 ${(hit * 100).toFixed(0)}%`);
}
if (missing.size) console.warn("単価未設定(集計から除外):", [...missing].join(", "));
const total = [...acc.values()].reduce((s, v) => s + v.usd, 0);
return total;
}
if (import.meta.url === `file://${process.argv[1]}`) {
const total = report(load());
const plan = Number(process.env.PLAN_USD ?? 0); // 契約中のプラン月額(USD)
console.log(`\nAPI換算の合計: $${total.toFixed(2)}`);
if (plan > 0) {
console.log(total > plan ? "サブスクのほうが安い" : "API従量課金のほうが安い");
}
}
実行は次のとおりです。
PLAN_USD=100 pnpm dlx tsx usage-report.ts
読み方の落とし穴
-
期間を揃える。 ログには全期間が入るので、月額と比べるなら対象月だけに絞ってから合計を出します。Step 3 の出力を
grep '^2026-10'で月ごとに切れば足ります。 - cache読取率が低い日は高くつく。 読み取りは入力より安い単価なので、読取率が下がった日は同じ作業でも換算額が跳ねます。CLAUDE.md を頻繁に書き換えた日などが該当します。
- サブスクには利用上限があります。 この比較は金額だけです。上限で作業が止まる影響は入っていないので、損益分岐の一要素として扱ってください。
-
ログが消えると集計も消える。
~/.claude/projectsの保持期間設定やクリーンアップで古い分が無くなります。月次で集計結果だけ別に残しておくと安全です。
まとめ
- セッションログの
message.usageをmessage.idで重複除去して足せば、ローカルだけでモデル別・日別のトークンが出ます。 - 単価はコードに埋め込まず
prices.jsonに分け、未設定は警告して除外すると、改定時に黙って狂いません。 - 「API換算額 vs プラン月額」を月単位で並べると、サブスク継続かAPI切り替えかを自分の使用実績で判断できます。
次の一手として、このスクリプトを月初に cron で回し、結果を1行ずつ CSV に追記しておくのがおすすめです。ログが消えても推移が残ります。