「あと何%使えるか」を1枚にした — 複数AIエージェントのクォータ計器を作った話

  • #AI活用
  • #Claude Code
  • #Codex
  • #Antigravity
  • #ダッシュボード
  • #開発環境
「あと何%使えるか」を1枚にした — 複数AIエージェントのクォータ計器を作った話

この記事について

複数のAIエージェント(CLI)を並行して使っていると、各エージェントのトークン消費量を並べて見ても、「次の仕事をどこへ振るか」の判断材料にはならない。知りたいのは消費量そのものではなく、**「あとどれくらい使えるか」と「今どのエージェントへ仕事を振れるか」**だった。

そこで各CLI(Claude Code の /usage、Codex の /status、Antigravity の /usage)を疑似端末経由で叩き、残量%・リセット時刻・消費速度・枯渇予測を1枚のダッシュボードへ統合した。この記事は、その実装と、実装中に踏んだ落とし穴の記録である。

何を計測しているか

各エージェントのCLIは対話コマンドしか提供していない。API経由で使用量を取れるサービスは少なく、あっても課金体系がAPIとサブスクで別枠になっていることが多いため、サブスク画面の表示を一次情報として信じる設計にした。

エージェント取得コマンド優先スコープ
Claude Code/usageweek / weekly
Codex/statusweek / weekly
Antigravity/usageweek / quota / limit
COLLECTORS = {
    "claude-code": {"program": "claude", "args": [], "slash": "/usage", "prefer": ("week", "weekly")},
    "codex": {"program": "codex", "args": ["--no-alt-screen"], "slash": "/status", "prefer": ("week", "weekly"), "repeat": 1},
    "antigravity": {"program": "agy", "args": [], "slash": "/usage", "prefer": ("week", "quota", "limit")},
}

静的なページからはCLIを実行できないため、構成は認証付きのPOST API(/api/quota/refresh)がローカルのCLIを疑似端末で叩く形にした。ダッシュボード側の「↻ 全クォータ更新」ボタンを押すと、このAPIが3本のCLIを順に起動し、結果を集計して返す。

疑似端末で叩く理由と、踏んだ5つの落とし穴

対話CLIをスクリプトから動かすだけなら subprocess でも足りそうに見えるが、実際にはTUI(Ratatuiベースの画面)を前提にしたCLIが多く、パイプ経由の標準入出力では起動すらしない。そのため pty.openpty() で疑似端末を渡し、人間が操作しているように振る舞わせる必要があった。ここで5つの落とし穴を順に潰した。

#落とし穴症状対策
1非ログインシェルではCLIが見えないlaunchd経由の実行では PATH が最小構成で、claude/codex/agy が見つからないPATH~/.local/bin /opt/homebrew/bin /usr/local/bin を明示的に足してから shutil.which で解決
2TUIの端末問い合わせに応答が要るカーソル位置問い合わせ(\x1b[6n)や背景色問い合わせ(OSC 10/11)に応答しないと描画が止まるそれぞれに最小限の標準応答を返す(例: カーソル位置は \x1b[40;1R を固定で返す)
3MCP起動中はEnterが落ちるCodexは起動直後、内部のMCPサーバ接続待ちで最初のEnterキーだけ無視することがあるスラッシュコマンド送信後 1.5秒経っても確定していなければ、追加でEnterを1回送る
4ランダムな初期プロンプトを「準備完了」判定に使えないCodexの起動時サンプルプロンプトは毎回変わるため、特定文字列の一致では待てない画面下部のステータス行(モデル名+default·のパターン)を正規表現でマッチさせ、準備完了の印にする
5コンテキスト残量を契約クォータと取り違えかねない出力中に「コンテキストウィンドウの残量%」と「契約クォータの残量%」が両方出ることがあり、前者を拾うと誤記録になる行に context / コンテキスト を含む場合は候補から除外する
if b"\x1b[6n" in data:
    os.write(master, b"\x1b[40;1R")
if b"\x1b]10;?" in data:
    os.write(master, b"\x1b]10;rgb:ffff/ffff/ffff\x1b\\")

枯渇予測とバーン率

単発の残量%だけでは「今日中に使い切るか」が分からない。同じリセット窓での過去の観測値と比較し、1日あたりの消費速度(バーン率)から枯渇予測時刻を出す。

elapsed_days = (observed_at - old_at).total_seconds() / 86400
consumed = old_pct - current_pct
if elapsed_days > 0 and consumed > 0:
    candidates.append((old_at, consumed / elapsed_days))

ステータスは4段階で判定する。

ステータス条件表示
critical残量10%以下、またはリセット前に枯渇する予測残量わずか/リセット前に枯渇
warning残量30%以下注意
healthyそれ以外余裕あり
stale観測から24時間以上経過要更新(ステータスを上書き)

stale は他の判定より優先される。24時間前の残量%を「今の状態」として使い続けると、実際にはとっくに枯渇している枠へ仕事を振ってしまうためだ。

実測値

導入時点の実測はこうだった(観測時点のスナップショットであり、常にこの値ではない)。

  • Claude Code: 残5%
  • Codex: 残60%
  • Antigravity: 残98%

この3値を見て初めて、「Claudeには新規タスクを振らず、Codexか Antigravityへ回す」という判断が一目でできるようになった。個別のCLIを開いて /usage を打つ手間がゼロになったこと自体より、3つの数字を横に並べて比較できるようになったことが効いている。

限界

  • サブスク枠の残量は正確なトークンログから逆算できない。各CLIの画面表示を信じる設計であり、表示自体が不正確ならこの計器も不正確になる
  • stale(24時間超)の間は判断材料として使えない。定期実行かボタン操作での再取得が前提
  • TUIのレイアウトが変わればパース側(正規表現・キーワードマッチ)も追従が必要になる。UIの内部実装に依存した設計であるため、CLI側のアップデートで壊れる可能性は残る

更新履歴

  • 2026-08-02: 初稿公開