Claudeくん!これ作って!~コスト可視化~
Claude Code の細かい tips 記事です。
今回は、Claude Code の利用料を手元で見張る話です。
Claude Enterprise は、シート料金とは別に、使ったトークンを標準の API 料金で払う形になりました。
従来のシートは新規契約では提供されず、既存の契約は次回の更新時に切り替わります。
従量課金になると、Opus を effort 高めで回し続けたセッションがいくらだったのかが気になります。
管理者向けには、メンバーごとの当月の利用額を見る画面や、ユーザー別、モデル別の利用額のレポートがあります。
ただ、どちらも管理者向けで、セッションや effort の単位までは分かれていません。
Claude Code の /usage の Session ブロックにも金額は出ますが、対象は今のセッションだけで、/clear すると 0 に戻ります。
そこで、Claude Code が手元に残しているセッションログから金額を計算して、ダッシュボードとステータスラインへ出すことにしました。
記事3行概要
- Claude Code のセッションログ(JSONL)にはリクエストごとのトークン数が残っているので、API の定価を掛ければ日別、モデル別、セッション別のコストを出せるよ!
- ただしログだけで計算すると、ログに残らない裏側の呼び出しの分が漏れて、Claude Code 自身の計算より 9% ほど安く出たよ!
- 漏れた分はセッション終了時に記録される cost-state で補正して、ダッシュボードとステータスラインに出すようにしたよ!
作りたいもの
見たいものは次のとおりです。
| 見たいもの | 内容 |
|---|---|
| 期間 | 日次、週次、月次、年次(ついでに時間単位も) |
| モデルと effort | Opus、Sonnet などのモデル別と、effort 別 |
| セッション | セッション名つきで、どのセッションがいくらだったか |
| トークンの種類 | 入力、出力、キャッシュの書き込みと読み込み |
| 更新頻度 | 1 時間ごとくらいで十分 |
| ステータスライン | 今日の合計金額を常に出す |
作ったもの
ダッシュボードは HTML 1 枚で、ブラウザで開くとこうなります。
上から、期間ごとの合計(今日、今週、今月、今年)、絞り込み、選んだ期間の合計、期間別コスト(モデル別の積み上げ)、トークン種別の内訳が並びます。
絞り込みは、期間(今日、今週、今月、今年、全期間)、粒度(時間、日、週、月、年)、プロジェクトの 3 つです。
その下に、モデル別、effort 別、呼び出し元別、プロジェクト別の金額と、セッションの一覧が続きます。
呼び出し元は、メイン、サブエージェント、advisor、記録外の 4 つに分けています。
advisor と記録外が何なのかは、後の節で説明します。
ステータスラインは、既存の 2 行目の末尾に、今日の金額と今のセッションの金額を足しました(数値はダミーです)。
全体の構成
部品は次の 4 つです。
| 部品 | 役割 |
|---|---|
~/.claude/projects/**/*.jsonl |
Claude Code が書くセッションログ。集計の元データ |
claude_cost.py |
セッションログを取り込んで SQLite に貯め、today.json と dashboard.html を書く |
| launchd |
claude_cost.py を 5 分ごとに実行する。HTML は前回から 55 分以上たっていたら作り直す |
statusline.sh |
today.json から今日の金額を読み、ステータスラインに出す |
claude_cost.py は Python の標準ライブラリだけで書いたので、macOS 付属の /usr/bin/python3でそのまま動きます。
Claude Code には、OpenTelemetry でトークン数やコストを外部へ送る機能もあります。
ただ、受け口(コレクター)を別に立てる必要があり、有効にする前の利用分は取れません。
セッションログなら、追加の設定なしで手元に過去 30 日分が残っていて、effort やセッション名も同じファイルに入っています。
そこで今回は、セッションログを元データにしました。
この選択の代償として、ログに残らない呼び出しの分が漏れます(後述)。
セッションログから金額を出す
ログのどこに何があるか
Claude Code は、セッションごとの会話を ~/.claude/projects/<作業ディレクトリ由来の名前>/<セッションID>.jsonl に、1 行 1 レコードの JSON で書いています。
サブエージェントの会話は、同じ場所の <セッションID>/subagents/ に別ファイルで入ります。
サブエージェントのファイルでも、各行の sessionId は親セッションの ID でした。
集計に使うのは次のレコードです。
| レコード | 使うフィールド | 用途 |
|---|---|---|
type: "assistant" |
message.model、message.usage、perTurnEffort、timestamp、cwd
|
トークン数、モデル、effort、日時、プロジェクト |
type: "custom-title" |
customTitle |
/rename で付けたセッション名 |
type: "ai-title" |
aiTitle |
自動で付くセッション名 |
type: "cost-state" |
totalCostUSD、modelUsage
|
Claude Code 自身が計算したセッションの金額(補正に使う) |
このファイルの形式は公開された仕様ではないので、Claude Code のバージョンが上がると変わる可能性があります。
手元で確認したのは v2.1.316 です。
また、/rename の名前が custom-title として記録されることは、手元のログに例がなかったので、Claude Code 本体に含まれる文字列から推定しています。
message.usage は次のような形です(抜粋)。
"usage": {
"input_tokens": 2,
"cache_creation_input_tokens": 13143,
"cache_read_input_tokens": 13506,
"output_tokens": 562,
"cache_creation": {
"ephemeral_5m_input_tokens": 0,
"ephemeral_1h_input_tokens": 13143
},
"speed": "standard",
"inference_geo": "global"
}
トークンの種類と、課金上の扱いは次のとおりです。
-
入力:
input_tokens。キャッシュを使わずに読ませた入力。 -
キャッシュ書き込み:
cache_creation_input_tokens。キャッシュの保持時間(5 分か 1 時間か)で単価が違い、内訳はcache_creationに入っている。 -
キャッシュ読み込み:
cache_read_input_tokens。キャッシュから読んだ入力。 -
出力:
output_tokens。thinking の分も出力として課金され、この値に含まれている。
単価
単価は Anthropic の料金ページの値を、スクリプトに表として持たせました(2026-10-31 時点、USD / 100 万トークン)。
| モデル | 入力 | キャッシュ書き込み(5 分) | キャッシュ書き込み(1 時間) | キャッシュ読み込み | 出力 |
|---|---|---|---|---|---|
| Opus 5.5 | $4 | $5 | $8 | $0.20 | $20 |
| Sonnet 5.5 | $2 | $2.50 | $4 | $0.10 | $10 |
| Haiku 5.5(プロンプト 10 万トークン以下) | $0.10 | $0.125 | $0.20 | $0.01 | $0.50 |
| Haiku 5.5(プロンプト 10 万トークン超) | $0.50 | $0.625 | $1 | $0.05 | $2.50 |
キャッシュ読み込みは多くのモデルで入力の 0.1 倍ですが、Opus 5.5 と Sonnet 5.5 は 0.05 倍です。
Haiku 5.5 は、キャッシュの読み書きを含めたプロンプト全体が 10 万トークンを超えると、すべての単価が上がります。
ほかに、fast mode は Opus 5.5 で 2 倍、米国内での推論を指定すると 1.1 倍、Web 検索は 1,000 回で $10 かかります。
1 リクエストの金額は、トークン数に単価を掛けて 100 万で割るだけです。
def row_cost(model, inp, cw5, cw1h, cr, out, web, speed, geo):
"""1 リクエスト分の金額を (入力, キャッシュ書き込み, キャッシュ読み込み, 出力, Web検索) で返す。"""
p = PRICES.get(model) # (入力, 書き込み5分, 書き込み1時間, 読み込み, 出力)
if p is None:
return None
lc = LONG_CONTEXT.get(model) # Haiku 5.5 の 10 万トークン超
if lc and (inp + cw5 + cw1h + cr) > lc[0]:
p = lc[1]
mult = 1.0
if speed == "fast":
mult *= FAST_MULT.get(model, 1.0)
if geo == "us":
mult *= US_GEO_MULT
return (
inp * p[0] / 1e6 * mult,
(cw5 * p[1] + cw1h * p[2]) / 1e6 * mult,
cr * p[3] / 1e6 * mult,
out * p[4] / 1e6 * mult,
web * WEB_SEARCH_USD,
)
モデル名は claude-opus-5-5[1m] のように末尾に付くことがあるので、[...] を取り除いてから単価を引いています。
単価表にないモデルは 0 円扱いにせず、ダッシュボードに「単価表にないモデルがある」と警告を出します。
同じメッセージが何行も記録される
1 回の API 応答は、テキスト、thinking、ツール呼び出しといった content block ごとに別の行として書かれます。
どの行にも同じ usage が付いているので、そのまま足すと 2 倍、3 倍に数えてしまいます。
message.id と requestId の組で重複を除き、同じ組の最後の行だけを使いました。
ファイルをまたいで同じ組が出てきた場合も、最初に取り込んだ方だけを数えます。
advisor の分はトップレベルの usage に入っていない
advisor(作業中のモデルが、より強いモデルに方針を相談する機能)を使うと、1 つの応答の中で複数回のモデル呼び出しが起きます。
その内訳は usage.iterations に入っています。
"usage": {
"input_tokens": 4,
"output_tokens": 903,
"cache_read_input_tokens": 189921,
"cache_creation_input_tokens": 2987,
"iterations": [
{ "type": "message", "input_tokens": 2, "output_tokens": 117,
"cache_read_input_tokens": 94449, "cache_creation_input_tokens": 1023 },
{ "type": "advisor_message", "model": "claude-opus-5-5",
"input_tokens": 88099, "output_tokens": 17180,
"cache_read_input_tokens": 0, "cache_creation_input_tokens": 0 },
{ "type": "message", "input_tokens": 2, "output_tokens": 786,
"cache_read_input_tokens": 95472, "cache_creation_input_tokens": 1964 }
]
}
トップレベルの usage は、type: "message" の iteration だけを足した値でした(手元の 6,929 件すべてで一致)。
advisor_message の分はトップレベルに入っていないので、トップレベルだけを集計すると advisor の金額がまるごと漏れます。
advisor の iteration には model が入っているので、そのモデルの単価で別に計算しています。
手元の 10 月分では、advisor が金額全体の 16.6% を占めていました。
もう 1 つ、トップレベルの cache_creation(5 分と 1 時間の内訳)は、最初の iteration の値しか反映していない行がありました(295 件)。
キャッシュ書き込みは 5 分と 1 時間で単価が違うので、iteration ごとに計算しています。
ログに残らない呼び出しがある
計算が合っているかを確かめるため、Claude Code 自身の計算と比べました。
Claude Code はセッションの終了時に、そのセッションの累計金額を cost-state というレコードでセッションログに書いています。
{"type":"cost-state","sessionId":"...","totalCostUSD":...,"startTime":...,"totalDuration":...,
"modelUsage":{"claude-opus-5-5[1m]":{"inputTokens":...,"outputTokens":...,"cacheReadInputTokens":...,
"cacheCreationInputTokens":...,"costUSD":...}, ...}}
cost-state があるセッション 41 件で比べた結果が次の表です。
| 比較 | 結果 |
|---|---|
| 合計 | セッションログだけの計算は、cost-state より 9.13% 少ない |
| セッション単位 | 0.03% から 36.25% 少ない。多い側に外れたセッションはない |
ずれが小さいセッションでは、メインの Sonnet 5.5 の分が小数第 4 位まで一致し、残りの差は Haiku 5.5 の分だけでした。
単価と計算式は合っていて、ずれの原因はセッションログに残らない呼び出しです。
モデル別にトークン数を比べると、足りない分は主に次の 3 種類でした。
- Haiku 5.5 の小さな呼び出し(セッション名の自動生成などに使われていると思われる)
- メインのモデルの呼び出しで、キャッシュ読み込みが多く、出力が少ないもの
- Opus 5.5 の、キャッシュを使わない入力(advisor の呼び出しと同じ形で、ログに残っている advisor の分より多かった)
コスト管理のドキュメントには、会話とは別にトークンを使う機能として、待機中のセッションの要約(session recap)とアイドル時の compaction、応答の後に次のプロンプトを提案する機能(prompt suggestions)が挙がっています。
このうちプロンプトの提案は「会話のキャッシュを使うので、ほとんどがキャッシュ読み込みで、出力は数トークン」と説明されていて、2 つ目の形と合います。
ただし、どの呼び出しがどの機能のもので、いくら使ったかまでは確かめていません。
セッションログの api-request レコードも、メインスレッドとサブエージェントの分しかありませんでした。
cost-state で補正する
ずれはすべて少ない側だったので、cost-state との差を「記録外」の呼び出しとして足すことにしました。
- セッションごと、モデルごとに、cost-state の金額からセッションログで計算した金額を引く。
- 差額は、同じセッション、同じモデルの呼び出しが多かった時間帯ほど多く割り振る(1 時間単位)。
- セッションログに対応する呼び出しがないモデル(Haiku 5.5 など)の分は、セッションの終了時刻に置く。
同じセッションに cost-state が複数ある場合は、最大値を使います。
手元で cost-state が 2 つあったセッション(1 件)では、2 つ目が開始時刻を引き継いだ累計値になっていたためです。
補正後は、41 セッションすべてで cost-state と一致しました。
手元の 10 月分では、記録外が金額全体の 8.7% でした。
この補正には限界があります。
cost-state が書かれるのはセッションの終了時なので、実行中のセッションは補正できません。
今日の金額は、今動いているセッションの裏側の呼び出しの分だけ少なく出ます。
30 日で消えるログを貯めておく
Claude Code は、cleanupPeriodDays(既定 30 日)より古いセッションログを起動時に削除します。
年次で見るには、消える前に取り込んで残しておく必要があります。
cleanupPeriodDays を伸ばす手もありますが、セッションログには会話の中身(読んだファイルやコマンドの出力)が平文で入っています。
長く残すのは避けて、取り込んだ結果のトークン数だけを SQLite(~/.claude/cost-dashboard/usage.db)に保存しています。
金額は表示のたびに単価表から計算するので、単価が変わったときは単価表を直せば過去の分も計算し直せます。
取り込みは、ファイルのサイズと更新時刻を覚えておき、変わったファイルだけを読み直します。
手元の 89 ファイル(約 165MB)の初回の取り込みは 1 秒かかりませんでした。
ダッシュボード
集計結果を HTML に埋め込み、グラフは JavaScript で SVG を描いています。
外部のライブラリや CDN は使っていないので、ブラウザで file:// のまま開けます。
10 分ごとに自動で再読み込みするので、開いたままにしておけば、1 時間ごとの更新が反映されます。
集計の単位は次のとおりです。
- 日付:ログの時刻は UTC なので、日本時間に直してから日、週、月、年に振り分ける。週は月曜始まり。
-
プロジェクト:作業ディレクトリ(
cwd)の末尾のフォルダ名。 -
effort:
perTurnEffort。advisor と記録外は effort を持たないので、別の行にまとめる。
モデルの色は、全期間の金額が多い順に固定で割り当てています。
絞り込みのたびに順位で色を振り直すと、同じモデルの色が画面ごとに変わってしまうためです。
使ってみると、手元の 10 月分では、入力トークンの 95.9% がキャッシュ読み込みでした。
それでも金額で見るとキャッシュ読み込みは 33.9% で、キャッシュ書き込み(27.9%)、出力(23.5%)、入力(14.8%)と続きます。
キャッシュ書き込みの 9 割は 1 時間の保持時間(入力の 2 倍の単価)で書かれていました。
ステータスラインに今日の金額を出す
既存のステータスラインは 2 行構成で、2 行目にトークン数の日次、週次、月次の累計を出していました。
その 2 行目の末尾に、次の 2 つを足しました。
-
Today:
today.jsonの今日の金額。終了したセッションは補正込み。最大 5 分遅れる。 -
Session:ステータスラインの入力に含まれる
cost.total_cost_usd。Claude Code がクライアント側で定価から計算した推定値で、/clearで 0 に戻る。
追加したのは次の部分です(抜粋)。
# Cost: Session = Claude Code's own estimate, Today = cost-dashboard (updated every 5 min by launchd)
session_cost=$(sanitize_num "$(echo "$input" | jq -r '.cost.total_cost_usd // 0')")
COST_TODAY_FILE="$CLAUDE_DIR/cost-dashboard/today.json"
today_cost=""
if [ -f "$COST_TODAY_FILE" ]; then
today_cost=$(jq -r --arg d "$(date +%F)" 'if .date == $d then .today else empty end' "$COST_TODAY_FILE" 2>/dev/null)
[[ "$today_cost" =~ ^[0-9]+\.?[0-9]*$ ]] || today_cost=""
fi
ステータスラインの側では集計をせず、today.json を読むだけにしています。
ステータスラインのスクリプトは会話が更新されるたびに(300 ミリ秒の間引きつきで)実行され、実行中に次の更新が来ると途中で止められます。
そこで毎回 Python を起動してログを読むのは重いと判断しました。
手元では、追加後のスクリプト全体の実行時間は 0.16 秒でした。
today.json の日付が今日でなければ、金額の代わりに -- を出します。
日付が変わった直後や、定期実行が止まっているときに、前日の金額を今日の金額として出さないためです。
定期実行
定期実行は macOS の launchd に任せます。
~/Library/LaunchAgents/local.claude-cost-dashboard.plist を次の内容で作ります(<ユーザー名> は自分のものに置き換えます)。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>local.claude-cost-dashboard</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/python3</string>
<string>-I</string>
<string>/Users/<ユーザー名>/.claude/cost-dashboard/claude_cost.py</string>
<string>update</string>
<string>--html-every</string>
<string>3300</string>
</array>
<key>StartInterval</key><integer>300</integer>
<key>RunAtLoad</key><true/>
<key>ProcessType</key><string>Background</string>
<key>StandardOutPath</key><string>/Users/<ユーザー名>/.claude/cost-dashboard/launchd.log</string>
<key>StandardErrorPath</key><string>/Users/<ユーザー名>/.claude/cost-dashboard/launchd.log</string>
</dict>
</plist>
登録、すぐに 1 回実行、停止は次のコマンドです。
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/local.claude-cost-dashboard.plist
launchctl kickstart -k gui/$(id -u)/local.claude-cost-dashboard
launchctl bootout gui/$(id -u)/local.claude-cost-dashboard
5 分ごとに起動し、取り込みと today.json の更新は毎回、HTML の作り直しは前回から 55 分(3300 秒)以上たっているときだけ行います。
Python は /usr/bin/python3 を絶対パスで指定しています。
launchd から起動したプロセスにはシェルで設定した PATH が入らないことと、asdf などのバージョン管理ツールの python3 は、バージョンを決めていないディレクトリから呼ぶとエラーになるためです。
登録してから、launchctl print の実行回数と終了コード、launchd.log、today.json の更新時刻で動作を確かめました。
| 確認したこと | 結果 |
|---|---|
| 登録直後 |
RunAtLoad で 1 回目が実行され、終了コード 0。today.json が更新された |
| 5 分後 | 2 回目が実行され、today.json の更新時刻が 301 秒進んだ。終了コード 0 で、ログにエラーはなし |
| HTML(最初の 2 回) | 前回の生成から 55 分たっていなかったので、作り直さなかった(設定どおり) |
| HTML(7 回目) | 前回の生成から 3557 秒(約 59 分)後に作り直された。終了コード 0 |
5 分ごとの起動で 55 分のしきい値を判定するので、HTML の更新間隔は 55 分から 60 分のあいだになります。
結論
Claude Code のセッションログにはリクエストごとのトークン数が残っているので、定価を掛ければ、日別、モデル別、effort 別、セッション別のコストを手元で出せました。
ただし、トップレベルの usage だけを足すと advisor の分が漏れ、セッションログだけで計算すると裏側の呼び出しの分(手元では 9% ほど)も漏れます。
終了したセッションは cost-state で補正できますが、実行中のセッションは補正できません。
どの金額も定価での推定なので、請求額の確認には管理画面の数字を使います。
記事3行概要(再掲)
- Claude Code のセッションログ(JSONL)にはリクエストごとのトークン数が残っているので、API の定価を掛ければ日別、モデル別、セッション別のコストを出せるよ!
- ただしログだけで計算すると、ログに残らない裏側の呼び出しの分が漏れて、Claude Code 自身の計算より 9% ほど安く出たよ!
- 漏れた分はセッション終了時に記録される cost-state で補正して、ダッシュボードとステータスラインに出すようにしたよ!

