2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claudeくん!これ作って!~コスト可視化~

2
Posted at

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 枚で、ブラウザで開くとこうなります。

スクリーンショット 2026-10-31 21.23.58.png

上から、期間ごとの合計(今日、今週、今月、今年)、絞り込み、選んだ期間の合計、期間別コスト(モデル別の積み上げ)、トークン種別の内訳が並びます。
絞り込みは、期間(今日、今週、今月、今年、全期間)、粒度(時間、日、週、月、年)、プロジェクトの 3 つです。

その下に、モデル別、effort 別、呼び出し元別、プロジェクト別の金額と、セッションの一覧が続きます。
呼び出し元は、メイン、サブエージェント、advisor、記録外の 4 つに分けています。
advisor と記録外が何なのかは、後の節で説明します。

ステータスラインは、既存の 2 行目の末尾に、今日の金額と今のセッションの金額を足しました(数値はダミーです)。

スクリーンショット 2026-10-31 21.25.19.png

全体の構成

部品は次の 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 との差を「記録外」の呼び出しとして足すことにしました。

  1. セッションごと、モデルごとに、cost-state の金額からセッションログで計算した金額を引く。
  2. 差額は、同じセッション、同じモデルの呼び出しが多かった時間帯ほど多く割り振る(1 時間単位)。
  3. セッションログに対応する呼び出しがないモデル(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 で補正して、ダッシュボードとステータスラインに出すようにしたよ!

参考情報

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?