Claude Code(とくにMaxプラン)のコストをDuckDBで集計する方法
はじめに
このポストのまとめでもある。
AnthropicのClaude Codeでは、通常は/cost
コマンドを実行するとコストが確認できる。しかし、月額課金のClaude Maxプラン(以降Maxプラン)を契約すると、API利用料は不要になる代わりに/cost
を実行してもどれだけコストが掛かったのかは分からない[1]。なのでMaxプランがどれだけお得かも分からない。
/logout
をすると一応わかるが、
(これはまだClaude 4発表前のスクショなのでClaude 3.7 Sonnetメイン)
毎回ログアウトするのは現実的ではないため、別の方法がないか探していた。
$HOME/.claude/projects
ディレクトリにはプロジェクト[2]ごとの利用履歴と思われる.jsonl
ファイルが大量にあった。この.jsonl
ファイルの一つを見てみるとcostUSDという項目を発見した。
cat ~/.claude/projects/<PROJECT>/<UUID>.jsonl | jq . | less
おそらくこのcostUSDを集計すればいいと思われるが、問題はディレクトリごとに大量に.jsonl
ファイルがあるのでどう集計したものか、と…。
DuckDBで集計する
ここで(ようやく本題)、DuckDBを利用する方法を思いついた。DuckDBは列指向で分析が得意なデータベースであり、複数のファイルを読み込む機能やJSONを読み込む機能がある。これらを組み合わせてディレクトリごとに大量にある.jsonl
ファイルの集計ができるかもしれないと考えた。.jsonl
だったのでこれが機能するか不明だったが、試しにDuckDBを起動してクエリを実行してみたところ、
(DuckDBで全.jsonlファイルのcostUSDを合計した様子)
どうやら集計できたようである。使用した環境はmacOS Sequoia(15.5)。
以下、集計クエリの例。
すべてのコストの合計
SELECT SUM(costUSD) FROM read_json('~/.claude/projects/*/*.jsonl')
;
プロジェクト(ディレクトリ)ごと、モデルごとの利用状況
SELECT regexp_replace(cwd, '^.*/', '') project
, message.model
, '$ ' || SUM(costUSD) total_cost
, SUM(message.usage.input_tokens) input_tokens
, SUM(message.usage.output_tokens) output_tokens
FROM read_json('~/.claude/projects/*/*.jsonl')
WHERE costUSD IS NOT NULL AND message.model IS NOT NULL AND message.model <> '<synthetic>'
GROUP BY GROUPING SETS ((project, message.model), ())
ORDER BY project ASC, message.model ASC
;
プロジェクト(ディレクトリ)別の利用状況
SELECT regexp_replace(cwd, '^.*/', '') AS project
, '$ ' || SUM(costUSD) AS total_cost
, SUM(message.usage.input_tokens) AS input_tokens
, SUM(message.usage.output_tokens) AS output_tokens
FROM read_json('~/.claude/projects/*/*.jsonl')
WHERE costUSD IS NOT NULL
GROUP BY ROLLUP (project)
ORDER BY project
;
日付別、プロジェクト(ディレクトリ)・モデルごとの利用状況
SELECT strftime(CAST(timestamp AS TIMESTAMP) + interval '9 hours', '%Y年%m月%d日') AS jst_date
, regexp_replace(cwd, '^.*/', '') AS project
, message.model
, '$ ' || SUM(costUSD) AS total_cost
, SUM(message.usage.input_tokens) AS input_tokens
, SUM(message.usage.output_tokens) AS output_tokens
FROM read_json('~/.claude/projects/*/*.jsonl')
WHERE costUSD IS NOT NULL AND message.model IS NOT NULL AND message.model <> '<synthetic>'
GROUP BY GROUPING SETS((jst_date, message.model, project), (message.model), ())
ORDER BY jst_date ASC, project ASC, message.model ASC
;
日付別、モデルごとの利用状況
SELECT strftime(CAST(timestamp AS TIMESTAMP) + interval '9 hours', '%Y年%m月%d日') AS jst_date
, message.model
, '$ ' || SUM(costUSD) AS total_cost
, SUM(message.usage.input_tokens) AS input_tokens
, SUM(message.usage.output_tokens) AS output_tokens
FROM read_json('~/.claude/projects/*/*.jsonl')
WHERE costUSD IS NOT NULL AND message.model IS NOT NULL AND message.model <> '<synthetic>'
GROUP BY GROUPING SETS((jst_date, message.model), (message.model), ())
ORDER BY jst_date ASC, message.model ASC
;
日付別の利用状況
SELECT strftime(CAST(timestamp AS TIMESTAMP) + interval '9 hours', '%Y年%m月%d日') AS jst_date
, '$ ' || SUM(costUSD) AS total_cost
, SUM(message.usage.input_tokens) AS input_tokens
, SUM(message.usage.output_tokens) AS output_tokens
FROM read_json('~/.claude/projects/*/*.jsonl')
WHERE costUSD IS NOT NULL AND message.model IS NOT NULL AND message.model <> '<synthetic>'
GROUP BY ROLLUP(jst_date)
ORDER BY jst_date ASC
;
おわりに
分析・集計するにはDuckDBはとても便利。CSVをインポートすることはSQLiteでもできるが、ディレクトリのサブディレクトリにある対象のファイルをグロブ(*)で掴んでSQLを書ける。これはSQLiteではできないはず。fsdir
でディレクトリ内はみれるが再帰的にサブディレクトリまで検索するのはできない認識で、やるならPythonと組み合わせたりかな、と。
また、-c
オプションを使うと、コマンドを実行してすぐに終了して、その結果が標準出力に出力される。なのでClaude Codeとパイプで繋げることができる。つまり、下記のようなことができる。
duckdb -c " SELECT strftime(CAST(timestamp AS TIMESTAMP) + interval '9 hours', '%Y年%m月%d日') AS jst_date
, message.model
, '$ ' || SUM(costUSD) AS total_cost
, SUM(message.usage.input_tokens) AS input_tokens
, SUM(message.usage.output_tokens) AS output_tokens
FROM read_json('~/.claude/projects/*/*.jsonl')
WHERE costUSD IS NOT NULL AND message.model IS NOT NULL AND message.model <> '<synthetic>'
GROUP BY GROUPING SETS((jst_date, message.model), (message.model), ())
ORDER BY jst_date ASC, message.model ASC
;" | claude -p "ここ一週間の使い方を分析して" --model sonnet
出力結果
この一週間(5月25日-31日)のClaude Code使用状況を分析します:
使用パターン分析
モデル選択の変化
- 5月23日: Sonnet 3.5 → Opus 4への移行開始
- 5月26日: Sonnet 4の導入開始
- 5月27日以降: 主にSonnet 4を使用、必要に応じてOpus 4を併用
日別使用量
最も集中的に使用した日:
- 5月27日: $94.91(Opus: $84.01 + Sonnet: $10.90)
- 5月26日: $49.39(Opus: $35.96 + Sonnet: $13.43)
- 5月25日: $57.47(Opus単体)
コスト効率
- Sonnet 4: 平均 $7.31/日(5月26-31日)
- Opus 4: 平均 $40.13/日(使用日のみ)
- Sonnet 4はOpus 4の約1/5のコストで高品質な結果を提供
使用傾向
- 高負荷日: 5月25-27日(新機能開発や複雑なタスク)
- 安定期: 5月28-31日(日常的な開発作業)
- トークン効率: Sonnet 4で入力トークン比率が向上
推奨事項
- 継続: Sonnet 4を主力モデルとして使用
- 使い分け: 特に複雑な推論が必要な場合のみOpus 4を使用
- コスト最適化: 現在の使用パターンは効率的
週間総コスト: $319.75(全期間の64%を一週間で使用)
今回はClaude Codeのコストを集計するだけなシンプルな集計だが、DuckDBで集計してそのままAIに分析させられるのはいいかもしれない。
あと、『DuckDB In Action』というPDFをDuckDBの開発元であるMotherDuckが出している。DuckDBの詳細について興味のある方ははこちらを。
おまけ
ChatGPTとやり取りしながら書いてもらった集計用のスクリプト。DuckDBを利用する前に集計してみた。
#!/usr/bin/env bash
# summarize_costUSD_by_project_date.sh (portable: BSD + GNU)
# -------------------------------------------------------------------
# 出力列: project date costUSD input_tokens cache_creation_input_tokens
# cache_read_input_tokens output_tokens
# 最終行: TOTAL <空白> Σ Σ Σ Σ Σ
# -------------------------------------------------------------------
set -euo pipefail
BASE="${1:-${HOME}/.claude/projects}" # projects ルート
BLANK=" " # TOTAL 行の日付列用の空白 1 文字
tmp=$(mktemp) ; trap 'rm -f "$tmp"' EXIT
# --- helper --------------------------------------------------------------
epoch_mtime() { # -> epoch秒
if stat -c %Y /dev/null &>/dev/null; then # GNU
stat -c %Y "$1"
else # BSD/macOS
stat -f %m "$1"
fi
}
ymd_from_epoch() { # epoch -> YYYY-MM-DD (Asia/Tokyo)
if date -d @0 +%F &>/dev/null; then # GNU
TZ=Asia/Tokyo date -d "@$1" +%F
else # BSD/macOS
TZ=Asia/Tokyo date -r "$1" +%F
fi
}
# ------------------------------------------------------------------------
# 1) project・date・5 指標を TSV で平坦化
find "$BASE" -type f -name '*.jsonl' -print0 |
while IFS= read -r -d '' f; do
proj=${f#*/projects/}; proj=${proj%%/*}
ymd=$(ymd_from_epoch "$(epoch_mtime "$f")")
jq -r '[.costUSD // 0,
.message.usage.input_tokens // 0,
.message.usage.cache_creation_input_tokens // 0,
.message.usage.cache_read_input_tokens // 0,
.message.usage.output_tokens // 0] | @tsv' "$f" |
awk -v p="$proj" -v d="$ymd" -v OFS='\t' '{print p, d, $0}'
done >"$tmp"
# 2) 集計 → ソート → ヘッダー+TOTAL 行
{
# ヘッダー
printf 'project\tdate\tcostUSD\tinput_tokens\tcache_creation_input_tokens\tcache_read_input_tokens\toutput_tokens\n'
# 集計(awk は POSIX だけで書く)
awk -F'\t' '
{ key=$1 FS $2
cost[key] += $3
inp [key] += $4 # ← 予約語 in を避け inp
cci [key] += $5
cri [key] += $6
out [key] += $7 }
END {
for (k in cost) {
split(k, a, FS)
printf "%s\t%s\t%.10g\t%.10g\t%.10g\t%.10g\t%.10g\n",
a[1], a[2], cost[k], inp[k], cci[k], cri[k], out[k]
}
}' "$tmp" |
sort -k1,1 -k2,2 |
awk -F'\t' -v blank="$BLANK" '{
print;
TC+=$3; TI+=$4; TCC+=$5; TCR+=$6; TO+=$7
}
END {
printf "TOTAL\t%s\t%.10g\t%.10g\t%.10g\t%.10g\t%.10g\n",
blank, TC, TI, TCC, TCR, TO
}' OFS='\t'
}
これを保存して実行権限をつけて
./claude_cost.sh | column -t -s$'\t'
とすると集計される。
ファイルが多くなると実行時間がかかるのでやっぱりDuckDBがいいね。
Discussion