5種類のAIコーディングツール使用量を一元監視する:LLMを介さないマルチプロバイダUsage統合の設計と実装

Codex、Antigravity、Claude、Cursor、Kimiの使用量を、LLMを呼ばずに一元監視するMulticaの設計・実装とセキュリティ境界を解説。

Share

1. 背景:ツールごとのUsage画面巡回問題

AIコーディングツールの普及に伴い、複数のサービス(Codex, Antigravity, Claude Desktop, Cursor, Kimi等)を併用する開発環境が一般的となっています。しかし、各ツールはそれぞれ独自の使用量管理画面やリミット(5時間枠、週次枠、月次契約枠、従量課金等)を持っており、開発者は以下のような課題に直面していました。

  • 巡回コストの増大: 開発作業中に「あとどれくらい使えるか」を確認するため、複数のブラウザタブやクライアントUIを行き来する必要がある。
  • リミット突然到達リスク: セッション制限や週次制限の到達直前まで気づかず、作業の途中で突然コード生成が中断される。
  • 費用の視認性低下: 月次固定費と従量課金が混在し、開発チーム全体での利用コストの把握が困難。

これらの課題を解決するため、Multicaではデスクトップアプリケーション(Multica Desktop)上に5種類のAIコーディングツールの使用量を一元的に集約・表示する統合Usage監視機能を設計・実装しました。

2. 失敗事例:/quota 確認自体が4万トークン超を消費した罠

本機能を設計するにあたり、初期のアイディアとして「AI CLIツール(エージェント)に対してコマンドラインから /quota/usage といったプロンプトを送信し、その返答をパースする」という方式が検討されました。

しかし、プロンプト送信による監視方式を試行した際、以下のような致命的な問題が発生しました。

  • 監視処理自体によるトークン消費: クオータ状況を確認するために送信した1回の問い合わせにおいて、バックエンドでのシステムプロンプトの読み込みやコンテキスト構築が誘発され、4万トークン以上のトークン消費が発生しました。
  • 使用量を観測するために使用量を浪費する本末転倒: 制限値に近づいているか調べるリクエスト自体がクオータを大きく圧迫し、場合によっては監視リクエストによって制限値に到達してしまうリスクが浮き彫りとなりました。

この失敗に基づき、本機能のコア原理として「監視経路ではLLM(モデル呼び出し)を絶対に通さない」という鉄則が設定されました。

3. 設計原則:監視経路ではLLMを絶対に通さない

マルチプロバイダ監視システムを実現するにあたり、以下の3つの設計原則を確立しました。

  1. Zero LLM Invocation(ノン・プロンプト監視): モデル呼び出しやプロンプト送信を伴うインターフェース(エージェント起動やチャットAPI)は一切使用しない。ローカルの認証済みキャッシュ、RPC、管理用設定ファイル、軽量な状態取得APIのみを利用する。
  2. Local Credential Boundary(ローカル認証情報のカプセル化): 認証情報(APIキー、セッションToken、Cookie等)はローカルのdaemonプロセス内のみで消費し、ネットワーク越しのバックエンドやDB、UI層には一切送出・保持しない。
  3. Fault-Tolerant & Independent Collector(プロバイダ間の隔離): 各プロバイダの取得処理は完全な独立モジュール(Collector)として実装し、1つのプロバイダで取得失敗が発生しても、他のプロバイダの表示には一切影響を与えない。

4. 全体構成:Desktop daemon から Usage UI まで

本システムの全体コンポーネント構成を以下に示します。

マルチプロバイダUsage統合の全体構成図
  • Desktop Daemon: ローカル環境で常駐し、各ツールのローカルプロセスやローカルストレージと通信を担当。
  • Provider Collectors: 各ツールの固有ロジックを抽象化し、共通データモデルへ落とし込むモジュール群。
  • Multica Backend: 各Daemonから集約したクオータ情報を処理し、Workspace設定として永続化・配信。
  • Usage UI: クオータごとの使用率、残り期間、警告判定(Warning/Critical)をビジュアル表示。

5. 共通データモデル:異種クオータの正規化

各AIツールによってリミットの単位や期間の概念が異なります。5時間枠、7日間枠、契約サイクル(月次)枠、および従量課金残高を共通のフォーマットに正規化するため、Go言語バックエンドにおいて RuntimeCapacity および RuntimeQuota モデルを定義しました。

共通構造体(Go型定義の構造概念)

type RuntimeCapacityResult struct {
    Provider          string              `json:"provider"`
    Automatic         bool                `json:"automatic"`
    UsedPercent       *float64            `json:"used_percent,omitempty"`
    WindowMinutes     *int64              `json:"window_minutes,omitempty"`
    ResetsAt          *time.Time          `json:"resets_at,omitempty"`
    PlanType          string              `json:"plan_type,omitempty"`
    Remaining         *float64            `json:"remaining,omitempty"`
    Unit              string              `json:"unit,omitempty"`
    UnavailableReason string              `json:"unavailable_reason,omitempty"`
    FetchedAt         time.Time           `json:"fetched_at"`
    Quotas            []RuntimeQuotaResult `json:"quotas,omitempty"`
}

type RuntimeQuotaResult struct {
    ID            string     `json:"id"`
    Label         string     `json:"label"`
    Group         string     `json:"group,omitempty"`
    UsedPercent   *float64   `json:"used_percent,omitempty"`
    WindowMinutes *int64     `json:"window_minutes,omitempty"`
    ResetsAt      *time.Time `json:"resets_at,omitempty"`
    Remaining     *float64   `json:"remaining,omitempty"`
    Limit         *float64   `json:"limit,omitempty"`
    Unit          string     `json:"unit,omitempty"`
}

単一のプロバイダ内に複数のウィンドウ(例: 5時間制限と7日間制限)が存在する場合、Quotas 配列へ各ウィンドウの情報を格納し、カード全体の代表値として最も大きい使用率を UsedPercent に設定します。

6. Provider別実装の詳細

各プロバイダの利用状況取得メカニズムは、以下の通りモデルを呼び出さない軽量な手段に特化しています。

(1) Codex

  • 取得手段: Codex CLIの app-server サブコマンドを stdio モード (--listen stdio://) で起動し、JSON-RPCプロトコルで account/rateLimits/read リクエストを送信。
  • 特徴: LLMを起動せず、内部の認証済みセッションから直接レートリミット情報を読み取る。
  • 対応枠: 週次リミット (primary) および 5時間リミット (secondary)。5時間枠が返らない環境では恣意的に値を補完せず、返却された枠のみを正規化する。

(2) Antigravity

  • 取得手段: ローカルで起動中の Antigravity Language Server に対し、クオータサマリ取得用のRPC(RetrieveUserQuotaSummary)をプロセス間通信で呼び出す。
  • 特徴: チャットCLI (agy) を起動せず、クオータ情報のサマリのみを取得。
  • 対応枠: Geminiモデル群およびClaude/GPTモデル群それぞれについて、5時間枠と週次枠(計4クオータ)を取得。

(3) Claude Desktop

  • 取得手段: Claude Desktop アプリケーションがローカルに保持する使用履歴キャッシュファイル plan-usage-history.json を直接リード。
  • 特徴: ネットワーク通信やCLI呼び出しを一切行わず、ファイル読み取りのみで完結。
  • 対応枠: 7日間利用率 (sd: seven_day) および 5時間利用率 (fh: five_hour)。

(4) Cursor

  • 取得手段: Cursorクライアントがローカルに保持する認証済みセッションを再利用し、ダッシュボードと同等の使用量サマリを取得。
  • 特徴: エージェントやモデル呼び出しを一切行わず、閲覧用の集計値のみを読み取る。
  • 対応枠: 契約サイクル月次枠における API利用率 (api)、Auto利用率 (auto)、Total利用率 (total)。

(5) Kimi API

  • 取得手段: ローカルに設定済みのAPIキーを用いて、Moonshot公式の残高確認API (/users/me/balance) を呼び出し。
  • 特徴: 公式で提供されている請求管理APIを利用。
  • 対応枠: USD単位の利用可能残高 (available_balance)。

Provider別取得経路の比較表

provider 取得元 取得手段 期間の粒度 取得できる値 取得できない値 既知の制約
Codex Codex app-server stdio JSON-RPC (account/rateLimits/read) 5時間 / 週次 期間別使用率(%)、リセット日時、Plan種別 具体的トークン数値 5時間枠が応答に含まれない環境がある
Antigravity Language Server ローカルRPC (RetrieveUserQuotaSummary) 5時間 / 週次 モデル群別(Gemini, Claude/GPT)の残割合、リセット時間 具体的メッセージ件数 Language Serverプロセスが起動中である必要がある
Claude Claude Desktop キャッシュファイル (plan-usage-history.json) 5時間 / 7日間 7日間使用率(%)、5時間使用率(%)、最終更新日時 各プロンプトごとの消費詳細 Claude Desktopアプリのローカルキャッシュ依存
Cursor ローカルセッション ダッシュボード用使用量サマリ取得 契約月次 API / Auto / Total 使用率(%)、請求サイクル開始・終了日 週次・日次などの細分化データ 公式なusage取得APIではなく、将来の仕様変更の影響を受ける可能性がある
Kimi Moonshot API HTTPS API (/users/me/balance) リアルタイム残高 利用可能残高(USD) 期間別パーセンテージ制限 無料枠・定額制ではなく従量課金残高のみ

7. セキュリティ境界:Credential Trust Boundary 設計

ローカルの認証情報やキーチェーン、セッションCookieを利用して監視を行うため、認証情報が外部やUI層へ漏洩しない厳格なセキュリティ境界を設けています。

認証情報をローカル境界内に閉じ込める設計図

セキュリティ仕様のポイント

  1. ローカルプロセス内での消化: APIキー、JWT、Cookie、CSRFトークンは Collector モジュール内でのみ一時的に使用され、関数の戻り値(RuntimeCapacityResult)には一切含まれません。
  2. DB・ログ・UIからの排除: バックエンドデータベースへ保存される workspace.settings やアプリケーションログ、フロントエンドへのREST API応答には、認証情報が絶対に乗らないようスキーマレベルで隔離されています。
  3. 匿名化コード例(Collectorでの認証処理パターン):
// 匿名化疑似コード:ローカルセッションを利用した安全なデータ取得
func FetchProviderCapacity(ctx context.Context) (NormalizedCapacity, error) {
    // 1. ローカルのセッション情報を取得(ローカルファイル/プロセス限定)
    token, err := readLocalSessionToken()
    if err != nil {
        return NormalizedCapacity{}, err
    }

    // 2. 軽量エンドポイントへリクエスト(認証情報はヘッダまたはCookie等に適用し、戻り値には含めない)
    req, _ := http.NewRequestWithContext(ctx, "GET", providerEndpoint, nil)
    applyAuthentication(req, token)

    resp, err := httpClient.Do(req)
    if err != nil || resp.StatusCode != 200 {
        return NormalizedCapacity{}, fmt.Errorf("failed to fetch capacity")
    }
    defer resp.Body.Close()

    // 3. レスポンスから使用率・リセット時間のみを抽出し、Tokenは破棄
    var data ProviderUsageResponse
    json.NewDecoder(resp.Body).Decode(&data)

    // 4. 認証情報を含まない正規化オブジェクトのみを返却
    return NormalizedCapacity{
        UsedPercent: data.UsagePercent,
        ResetsAt:    data.ResetTimestamp,
    }, nil
}

8. UXと警告判定仕様

ユーザーが直感的に状態を把握できるよう、ダッシュボードのUIおよび警告判定に以下の仕様を導入しました。

閾値判定ロジック

各プロバイダカードの警告ステータス(alertLevel)は、そのプロバイダが持つすべてのクオータ(例: 5時間枠、週次枠)の最大使用率(highestUsage)に基づいて判定されます。

  • Normal: 最大使用率 < 70%
  • Warning: 最大使用率 ≧ 70% かつ < 80%
  • Critical: 最大使用率 ≧ 80%
// 閾値判定のフロントエンド実装例
export function calculateAlertLevel(quotas: ToolQuotaUsage[], primaryUsed: number | null): "normal" | "warning" | "critical" {
  const quotaPercents = quotas.map(q => q.used_percent).filter((v): v is number => typeof v === 'number');
  const allValues = primaryUsed !== null ? [primaryUsed, ...quotaPercents] : quotaPercents;

  const highestUsage = allValues.length > 0 ? Math.max(...allValues) : 0;

  if (highestUsage >= 80) return "critical";
  if (highestUsage >= 70) return "warning";
  return "normal";
}

UX配慮事項

  • 独立更新と部分失敗の許容: Promise.all 等で全プロバイダを並列取得しますが、特定の1プロバイダが認証切れやネットワークエラーで失敗した場合でも、他のプロバイダのカードは正常に更新表示されます。失敗したカードのみ「未接続・取得失敗」アイコンと理由を表示します。
  • リセット日時の表示: リセット日時が提供されるクオータについては、ユーザーのローカルタイムゾーンに合わせて「○時間後にリセット」「M/D HH:mm」形式で表示し、計画的な利用を支援します。

9. 実装中に遭遇した問題と障害対応

開発および実機検証の過程で遭遇した3つの主要課題とその解決策です。

(1) 古いBackend Schemaとの互換性問題

  • 問題: 新たに導入したマルチクオータ構造(Quotas 配列)に対応していない古いバックエンドが、データのデシリアライズに失敗する現象が発生。
  • 対応: バックエンドの RuntimeCapacity 受信ハンドラおよびレスポンススキーマを更新し、単一 UsedPercent と複数 Quotas の両方を後方互換性を保ちながら受容・変換する構造に修正。

(2) 重複Daemon存在時の古いRuntime誤選択

  • 問題: 開発環境や複数端末で過去の検証用 Daemon がオフライン/スリープ状態で残留していた際、バックエンドが古い Daemon のキャパシティ結果を優先して取得してしまう問題が発生。
  • 対応: プロバイダごとの Runtime 選択ロジックにおいて、オンライン(status == "online")状態であり、かつ last_seen_at(最終ハートビート時刻)が最も新しい Runtime を動的に選択するように修正 (fd0dd44)。

(3) Cursorの期間差(週次 vs 月次契約サイクル)

  • 問題: 他の多くのツールが5時間枠や週次枠を提供するのに対し、Cursorはユーザーの契約更新日(例: 毎月15日など)に依存する月次サイクルであった。
  • 対応: UI上で一律に「週次」と表記せず、Cursorについては「API usage」「Auto usage」「Total usage」という名目とともに、契約終了日(billingCycleEnd)を基準としたウィンドウ表記に個別対応。

10. テスト戦略と実機検証

システムの品質と安定性を保つため、多層的なテストを実施しました。

  • Go Collector ユニットテスト: 各プロバイダのレスポンスパース、異常系(HTTP 4xx/5xx、JSON崩れ)のテストケースを作成し全件 Pass を確認。
  • フロントエンド型チェック & ロジックテスト: TypeScript による型検証および normalizeToolUsage による 70%/80% 閾値判定テスト(6件)を含む自動テストを実施。
  • 実機検証: macOS (arm64) 環境にて実際にビルド・パッケージングを行い、リアルな実アカウント環境にて以下のような各種クオータおよび警告表示(CodexのWarning判定等)が意図通りに動作することを確認。

11. 制約事項と今後の展望

本実装には以下の既知の制約事項が存在します。

  • 非公開/ローカルインターフェース変更リスク: Antigravity や Cursor の取得経路はローカルプロセスや非公開ダッシュボードAPIに依存しているため、ツール側のメジャーアップデート等により取得仕様が変更される可能性があります。
  • 認証の自動更新非対応: 各ローカルツールでログインが切れた場合、Multica側から再ログインを促すことはできますが、自動で再認証を行うことはできません。
  • Codexの5時間枠不在パターン: アカウントの状態やCodex CLIのバージョンによって secondary(5時間枠)が返されないケースが存在します。システム側ではこれを正常な挙動として扱い、存在する枠のみを表示します。

※ 本記事は、社内の実装記録に基づいて執筆した技術記録です。