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?

5時間ブロック上限を超える前に止める ― ccusage × コストログで作るトークン番人

0
Posted at

前作「眠ったプラグインを自動で棚卸しする」でプラグイン整理の自動化を書きました。今回はその一段上流 ―― そもそもトークン枠を使い切る前に止める仕組みの話です。

Claude Code の5時間ブロックには出力トークンの上限があり、超えると次のブロックに入るまでレートリミットされます。問題は「超えた後に気づく」設計になっていること。autopilot や夜間バッチが裏で走っている環境では、朝起きたら全スロット SKIP 済み、という事態が現実に起きました。token-budget-advisor.sh はそれを「超える前に止める」ために書いたスクリプトです。

困りごと:制限は超えてから初めて見えた

launchd で複数の自動化ジョブが5時間ブロックをまたいで走ると、どのジョブがどれだけ消費したか追えません。claude -p の出力に token 数は出ないし、Claude Code の UI はセッション中しか見えない。残量を確認する手段がなかったため、上限はいつも事後通知でした。

必要だったのは3つです。

  1. 現在のアクティブブロックの出力トークン数をリアルタイムで取る
  2. warn(消費多め)と critical(あと少し)を数値で区別する
  3. dashboard と daily-brief の1行に収まる形で常時出し続ける

5時間ブロックのしきい値

スクリプト冒頭のコメントに実測ベースで決めたしきい値を書いてあります。

# しきい値:
#   5h block:  output > 800k  → warn (>1.2M で critical)
#   weekly:    cost  > $3000  → warn
#   sessions:  >5/day         → warn (集中作業の疑い)

800k を warn にしている理由は、ここを超えると重いモデルを長く走らせた場合に1〜2ターンで critical に達するからです。1.2M は「もう重い作業は次ブロックへ」の意思決定ライン。この2段で「警告を無視して走り続けた場合のダメージ」を小さくしています。

ccusage × cost-log.jsonl:二重ソース設計

トークン数の取得元を1つにするのは危険です。ccusage がパスに無い環境、blocks が空の初回ターン、jsonl の書き忘れ ―― どれかが欠けても番人が止まると、ガードが全滅します。そのため ccusage を正とし、cost-log.jsonl をフォールバックにした二重ソース構成にしています。

# ccusage が居れば 5h block の output token を取る (transcript 計算より公式)
CC_OUTPUT_TOK=""
CC_COST_5H=""
if command -v ccusage >/dev/null 2>&1; then
  CC_JSON=$(ccusage blocks --json 2>/dev/null || true)
  if [ -n "$CC_JSON" ]; then
    EXTRACTED=$(printf '%s' "$CC_JSON" | python3 -c "
import sys, json
try:
    d = json.load(sys.stdin)
    active = [b for b in d.get('blocks', []) if b.get('isActive')]
    if active:
        b = active[0]
        tc = b.get('tokenCounts', {}) or {}
        out = int(tc.get('outputTokens', 0))
        cost = float(b.get('costUSD', 0))
        print(f'{out}|{cost}')
    else:
        print('|')
except Exception:
    print('|')
" 2>/dev/null || echo "|")
    CC_OUTPUT_TOK="${EXTRACTED%|*}"
    CC_COST_5H="${EXTRACTED#*|}"
  fi
fi

ccusage blocks --jsonblocks[] 配列を返し、isActive: true のブロックが現在の5時間ウィンドウです。tokenCounts.outputTokens を取り出して判定に使います。

Python 集計側では ccusage の値が有効ならそちらを採用し、cost-log.jsonl との差分を source_diff_pct として記録します。

# ccusage の値が有効ならそちらを優先 (transcript 計算より信頼できる)
own_out_5h = out_5h
if cc_out is not None and cc_out > 0:
    out_5h = cc_out
if cc_cost is not None and cc_cost > 0:
    cost_5h = cc_cost

# 比較 (検証用)
diff_pct = None
if cc_out is not None and own_out_5h > 0:
    diff_pct = round(abs(cc_out - own_out_5h) / max(cc_out, own_out_5h) * 100, 1)

source_diff_pct が大きい(5%以上)なら cost-log.jsonl の集計ロジックにズレがある合図です。番人自身の精度を番人が検証できる構造になっています。

判定と --short 出力

判定ロジックは3ステータスです。

THRESH_5H_WARN     = 800_000      # output tokens
THRESH_5H_CRIT     = 1_200_000
THRESH_WEEK_WARN   = 3000         # USD

if out_5h >= THRESH_5H_CRIT:
    s5 = "critical"
elif out_5h >= THRESH_5H_WARN:
    s5 = "warn"
else:
    s5 = "ok"

加えて「直近3日の平均セッション数が5/day超」を burst フラグで検出します。短期集中作業で枠を消耗しているパターンは weekly cost が増える前兆なので、別軸で警告を出します。

--short モードの出力がこれです。

🟢 OK (5h:234k tok $1.2 / 7d:$45)
🟡 burst (5h:821k tok $4.1 / 7d:$89)
🔴 cap-near (5h:1234k tok $6.7 / 7d:$122)

コード内では _short キーに格納されています。

"_short": f"{icon} {label} (5h:{out_5h/1000:.0f}k tok ${cost_5h:.1f} / 7d:${cost_7d:.0f})",

dashboard と daily-brief への統合

dashboard.sh の Cost セクションはこうなっています。

echo "## 💰 Cost (7d)"
~/.claude/scripts/cost-summary.sh --short
echo "  budget: $(~/.claude/scripts/token-budget-advisor.sh --short)"

cost-summary.sh --short$45.20 / 31 sess (7d) のような累積コストを返し、その直下に budget: として5時間ブロックの現在地を並べます。dashboard を開くたびにコストと残量が両方見えるのがポイントです。

daily-brief.shrun_to 60 というタイムアウトラッパーで cost-summary.sh 7 を呼び出し、10行分を brief のコストセクションに差し込みます。

echo "## 💰 cost(直近 7d)"
run_to 60 ~/.claude/scripts/cost-summary.sh 7 2>&1 | head -10

run_togtimeout --kill-after=15 秒数 のラッパーで、集計スクリプトがハングしてもブリーフ全体を殺さない設計になっています。

token-budget-advisor.sh を呼ぶ dashboard.sh 側には run_to を被せていません。advisor 自体が fail-open で exit 0 を返す設計のため、ハングが起きにくいからです。ただし python3 が詰まるコーナーケースは残るので、今後 run_to 10 を被せる予定です。

fail-open 設計の理由

スクリプト先頭に set -u はありますが -e は外してあります。

set -u  # -e は外す: fail-open 方針

fail-open ヘルパがこうです。

fail_open() {
  if [ "$MODE" = "--short" ]; then
    echo "⚫ n/a"
  else
    printf '{"5h_status":"unknown","weekly_status":"unknown","advice":"%s"}\n' "${1:-no data}"
  fi
  exit 0
}

[ -f "$LOG" ] || fail_open "cost-log.jsonl not found"

dashboard に組み込まれているスクリプトが exit 1 を返すと、dashboard 生成スクリプト自体が -o pipefail の連鎖で止まります。番人は「番人自身が落ちてダッシュボードを止める」より「⚫ n/a を出して素通りさせる」方が安全です。コストの確認手段が失われるよりも、⚫ n/a を見て「今日は手で確認しよう」と気づける方が運用としてましです。

ccusage のインストール

npm install -g ccusage
# または
npx ccusage blocks --json

ccusage blocks は5時間ブロック単位の使用量を JSON で返します。--json を付けないとターミナル向けの装飾出力になるので注意。isActive: true のブロックが存在しない場合(ブロック切れ直後)は空配列が返り、スクリプトは自動でフォールバックします。

踏んだ落とし穴

  • ccusage なしで走らせると cost-log の session 重複カウントがある → 最新行のみ採用するため (session_id, transcript) をキーにして最終行を取り直すロジックを追加した(スクリプト内 latest = {} の二周目ループ)
  • ラベル(🟡)だけ見て残量がマイナスでも素通りした → autopilot 側で数値チェックを別途追加(REMAINING=$((800000 - BLOCK_OUT))<= 0 を止める)
  • ccusage が nvm 管理で launchd の PATH に入っていないcommand -v ccusage >/dev/null 2>&1 の分岐で graceful degradation、フォールバックの jsonl 集計が引き継ぐ
  • --short⚫ n/a を autopilot が「問題なし」と誤読した → autopilot 側のチェックを「🔴 または critical 含む」の OR に変え、 もスキップ対象に追加
  • by_day の集計に session_id 去重をしていなかった → 同一日に複数行ある session が日別カウントを水増しし、burst が誤発火した。sess_7d_by_day で set 去重するよう修正

まとめ

  • 5時間ブロックの出力トークンしきい値は warn: 800k / critical: 1.2M
  • 取得元は ccusage 優先 + cost-log.jsonl フォールバックの二重ソース。source_diff_pct で精度を自己検証
  • --short モードが 🟢/🟡/🔴 の1行サマリを返し、dashboard と daily-brief に差し込む
  • fail-open(exit 0 + ⚫ n/a)で番人がダッシュボードを殺さない設計
  • ラベル判定だけでなく、数値で残量を再計算して止めるのが最終ガード

次回は、このしきい値を超えそうになった時に autopilot が自動でモデルを軽いものに切り替える仕組み ―― effort 可変とモデルルーティングの連携を書く予定です。


Lily@bokuwalily)― 個人開発者。Claude Code で自動化基盤を組みながら、iOSアプリやWebサービスを量産しています

皆さんの ❤️ やシェアが励みになります!

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?