「あと何%使えるか」を1枚にした — 複数AIエージェントのクォータ計器を作った話
この記事について
複数のAIエージェント(CLI)を並行して使っていると、各エージェントのトークン消費量を並べて見ても、「次の仕事をどこへ振るか」の判断材料にはならない。知りたいのは消費量そのものではなく、**「あとどれくらい使えるか」と「今どのエージェントへ仕事を振れるか」**だった。
そこで各CLI(Claude Code の /usage、Codex の /status、Antigravity の /usage)を疑似端末経由で叩き、残量%・リセット時刻・消費速度・枯渇予測を1枚のダッシュボードへ統合した。この記事は、その実装と、実装中に踏んだ落とし穴の記録である。
何を計測しているか
各エージェントのCLIは対話コマンドしか提供していない。API経由で使用量を取れるサービスは少なく、あっても課金体系がAPIとサブスクで別枠になっていることが多いため、サブスク画面の表示を一次情報として信じる設計にした。
| エージェント | 取得コマンド | 優先スコープ |
|---|---|---|
| Claude Code | /usage | week / weekly |
| Codex | /status | week / weekly |
| Antigravity | /usage | week / 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 で解決 |
| 2 | TUIの端末問い合わせに応答が要る | カーソル位置問い合わせ(\x1b[6n)や背景色問い合わせ(OSC 10/11)に応答しないと描画が止まる | それぞれに最小限の標準応答を返す(例: カーソル位置は \x1b[40;1R を固定で返す) |
| 3 | MCP起動中は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: 初稿公開