0
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?

Codex CLIの作業記録を数えたら、トークンの99%は入力で、その93%はキャッシュだった

0
Last updated at Posted at 2026-10-06

AIエージェントの費用というと、「たくさん書かせると高くなる」——つまり出力の量で決まると考えがちです。

Codex CLI が手元に残している作業記録を全部数えてみると、逆でした。99回分の実行で、トークンの99.2%は入力。しかもその93%はキャッシュからの読み直しで、AIが書いた出力は0.8%しかありませんでした。

この記事は、その数え方です。Codex CLI は、何もしなくても作業の記録を JSONL で残しています。これを読むだけで、どの道具を何回使い、どのコマンドを打ち、どれだけトークンを使ったかが分かります。

記録はどこにあるか

~/.codex/sessions/YYYY/MM/DD/rollout-<日時>-<id>.jsonl

1回の実行(セッション)が1ファイルです。1行に1レコードで、type と payload.type で種類が分かれます。今回使うのは次の4つだけです。

レコード 中身
session_meta(1行目) 作業ディレクトリ(cwd)など。どの仕事のセッションかを絞り込める
response_item / custom_tool_call 道具の呼び出し。中身は小さな JavaScript
response_item / custom_tool_call_output 道具の結果。call_id で呼び出しと結び付く
event_msg / token_count そのセッションのトークン使用量の累計

道具の呼び出しは「小さなJSコード」として残る

custom_tool_call の input には、たとえば次のようなコードが入っています。

const r = await tools.exec_command({cmd: "rg -n 'def route' src/app.py"});
text(await r.output());

1回の呼び出しの中で、複数の道具を続けて呼ぶこともあります(今回のデータでは最大5つ)。なので「先頭の単語」を数えると const が最多になってしまいます。tools.<名前>( と cmd: "..." を全部拾うのがコツです。

トークンは累計で入る

token_count の info.total_token_usage には、そのセッションの累計が入ります。

{"input_tokens": 32547, "cached_input_tokens": 0, "output_tokens": 139, "reasoning_output_tokens": 18, "total_tokens": 32686}

途中の値を足すと二重に数えるので、各セッションの最後の値だけを使います。

数えるスクリプト

外部ライブラリは使いません。引数に、作業ディレクトリに含まれる文字列を渡すと、その仕事のセッションだけを数えます。

codex_usage.py
#!/usr/bin/env python3
"""Codex CLI の作業記録(~/.codex/sessions/**/rollout-*.jsonl)を数える。

    python3 codex_usage.py [作業ディレクトリに含まれる文字列]
"""
import glob, json, os, re, statistics, sys
from collections import Counter

keep = sys.argv[1] if len(sys.argv) > 1 else ""
tools, commands, usage, sessions, broken = Counter(), Counter(), [], 0, 0
for path in sorted(glob.glob(os.path.expanduser("~/.codex/sessions/*/*/*/rollout-*.jsonl"))):
    with open(path, encoding="utf-8", errors="replace") as f:
        lines = f.read().splitlines()
    try:
        meta = json.loads(lines[0]).get("payload", {})      # 1行目は session_meta
    except (ValueError, IndexError):
        continue
    if keep not in str(meta.get("cwd", "")):
        continue
    sessions += 1
    last = None
    for line in lines:
        try:
            rec = json.loads(line)
        except ValueError:                                  # 書き込み途中などで壊れた行は飛ばす
            broken += 1
            continue
        p = rec.get("payload") or {}
        if p.get("type") == "custom_tool_call":             # 道具の呼び出し(中身は小さなJSコード)
            src = str(p.get("input") or "")
            tools.update(re.findall(r"tools\.(\w+)\s*\(", src))
            for m in re.findall(r'cmd\s*:\s*"((?:[^"\\]|\\.)*)"', src):
                words = [w for w in json.loads('"' + m + '"').split() if "=" not in w]
                if words:
                    commands[os.path.basename(words[0])] += 1
        if p.get("type") == "token_count" and p.get("info"):
            last = p["info"].get("total_token_usage")       # セッション内の累計。最後の値を使う
    if last:
        usage.append(last)

total = lambda k: sum(u.get(k, 0) for u in usage)
inp, cached, out = total("input_tokens"), total("cached_input_tokens"), total("output_tokens")
print(f"sessions: {sessions}  broken lines skipped: {broken}")
print(f"tools: {tools.most_common(6)}")
print(f"commands: {commands.most_common(8)}")
print(f"input {inp:,} (cached {cached:,} = {100*cached/max(1,inp):.0f}%)  output {out:,} = {100*out/max(1,inp+out):.1f}% of all")
print(f"per session median: input {statistics.median(u.get('input_tokens',0) for u in usage):,.0f}"
      f" / output {statistics.median(u.get('output_tokens',0) for u in usage):,.0f}")

結果(自分の環境、2026年8〜10月の99セッション)

sessions: 99  broken lines skipped: 87
tools: [('exec_command', 1065), ('apply_patch', 143), ('web__run', 79), ('write_stdin', 57), ('mcp__codex_apps__google_drive_fetch', 5), ('mcp__codex_apps__remote_desktop_commander_read_file', 1)]
commands: [('python3', 327), ('cat', 166), ('rg', 117), ('sed', 70), ('git', 55), ('python', 32), ('find', 31), ('pytest', 27)]
input 69,760,539 (cached 64,894,720 = 93%)  output 586,071 = 0.8% of all
per session median: input 559,265 / output 7,308

トークン:出力は0.8%

トークン
入力 69,760,539
うちキャッシュ 64,894,720(入力の93%)
出力 586,071(全体の0.8%)
1セッションの中央値 入力 559,265 / 出力 7,308

Codex は作業のたびに、指示・これまでのやり取り・読んだファイルの中身を読み直しています。だから入力が圧倒的に大きく、その大半がキャッシュに乗っています。費用や使用量の上限を考えるときに見るべきなのは、出力の長さより、1回のセッションで何を読み込ませているかでした。

道具:ほぼ「コマンドを打つ」

道具 回数
exec_command(シェルのコマンド) 1,065
apply_patch(ファイルの書き換え) 143
web__run(Web) 79
write_stdin(実行中のプロセスへの入力) 57

コマンド:多くは「読む」

コマンド 回数
python3 327
cat 166
rg 117
sed 70
git 55
python 32
find 31
pytest 27

cat・rg・sed・find を合わせると384回。書き換え(apply_patch 143回)の2倍以上、確認のために読んでいます。

ハマったところ

  • 壊れた行がある:書き込みの途中などで JSON として読めない行が、99セッション中で87行ありました。1行ずつ try で読み、飛ばして数を出しています。
  • 作業ディレクトリで絞る:Codex のデスクトップアプリなど、別の用途のセッションも同じ場所に保存されます。1行目の cwd で絞らないと、仕事以外の会話まで混ざります。
  • 形式は公式の仕様ではない:CLI の内部の記録なので、バージョンで変わる可能性があります。キーが無いときに落ちないように書いておくのが安全です。

まとめ

  • Codex CLI の作業記録は ~/.codex/sessions/ の JSONL。1行目の cwd で仕事ごとに絞れる
  • 道具の呼び出しは小さな JS。tools.<名前>( と cmd: "..." を全部拾う
  • トークンは token_count の累計の最後の値を使う
  • 自分の99セッションでは、トークンの99.2%が入力、その93%がキャッシュ、出力は0.8%

AIの使用量を減らしたいなら、まず「何を読ませているか」を数えるところからだと思います。

0
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
0
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?