【Python】Claude Code・Codexのトークン消費量を確認するツールを作ってみた

【前編】Claude Code・Codexのトークン消費量を確認するツールを作ってみた

公開: 更新:
CodeCampが提供するDX人材育成が可能なプログラミングやITが学べる公開講座

Claude CodeやCodexでは利用状況を確認できても、どのプロンプトが何トークン使ったのか、入力・出力・キャッシュ・推論のどこで大きく消費しているのか、などはわかりにくいことがあります。そこで、実行ごとのトークン消費量をPythonで受け取り、ローカル画面へ記録するツールを作りました。

掲載コードを順番にコピーし、Claude CodeまたはCodexを1回計測した後、普段使っているプロンプトを3回実行することによって、現在のトークン消費量の基準値と内訳を確認できます。ブラウザのClaude・ChatGPTのChat画面、設定前に終えた会話、などは対象外となるのでご注意ください。

※動画は主にコードの解説がメインとなっております

【Python】Claude Code・Codexのトークン消費量を確認するツールを作ってみた
動画の内容をテキストで確認する

オープニング。Claude Code・Codexのトークン消費量を実行ごとに確認するツールを作ります。概要紹介。

計測できるClaude Code・Codexの実行範囲を理解するFastAPI・SQLite・OTLPの役割と使い方を学ぶ 実行ごとのトークン消費量を記録する要件を整理する 入力・出力・キャッシュ・推論の内訳を確認する 具体的にやること。

GET /でトークン分析ダッシュボードのHTMLを返すPOST /v1/logsでOTLPのJSONまたはProtobufログを受信するClaude Codeはprompt.id、Codexはrun IDで実行を分ける 入力・出力・キャッシュ・推論を同じ画面へ表示する 欠損値を0にせずcoverageと一緒に確認する 実装環境・必須アプリ。

OS:Windows 11 Pro Python:3.13.3シェル:PowerShell 5.1必須アプリ:コードエディター、Webブラウザ、ターミナル パッケージ:pip、fastapi、uvicorn、opentelemetry-proto、python-multipart FastAPI・SQLite・OTLPとは。

FastAPI:FastAPIはPythonの関数へHTTPのルートを割り当ててWeb画面とAPIを同時に提供できるWebフレームワークSQLite:SQLiteはPython標準ライブラリのsqlite3モジュールから直接操作できる軽量データベース トークン消費量を確認する際の要点。

通常のClaude・ChatGPTではなくClaude Code・Codexの実行を対象にするClaude Codeは通常入力・キャッシュ・出力を合計するCodexはキャッシュと推論を内数として二重加算しない 取得できない値はunknownとしてcoverageへ反映する トークン可視化ダッシュボードの要件定義。

ブラウザで/を開くとトークン分析ダッシュボードが表示されるfixture投入ボタンを押すとknown-totalとcoverageが更新されるClaude CodeとCodexをruntime別に確認できる ボトルネック順位表に分析単位と合計トークンが表示される 明細表に入力・出力・キャッシュ・推論が表示される /api/overviewから同じ集計結果をJSONで取得できる PREVIEW: 完成したトークン可視化ダッシュボードを操作。

入力・送信・結果表示を実際のブラウザ画面で確認します。UPLOAD: 実際の入力データでアプリを操作。JSONとProtobufのfixtureを取り込み、coverage・ボトルネック順位・改善前後の中央値比較が表示されることを確認できた 確認1/4: fixtureを投入してダッシュボードを確認する。

JSON+Protobufのfixtureを投入し、ダッシュボードに表示されるトークン合計とfield coverageの数値を確認します。確認2/4: ボトルネック順位表の1位を確認する。fixture投入後のボトルネック順位表で、最もトークンを消費したprompt単位が1位に表示されることを確認します。

確認3/4: 改善前後の中央値比較を確認する。before/after variantを持つClaudeのデータから、中央値と削減率が比較表に表示されることを確認します。確認4/4: APIエンドポイントで集計結果を取得する。

/api/overviewエンドポイントを呼び出し、JSONレスポンスに含まれるdetectedとknown_totalの値を確認します。学習内容のまとめ。

@app.get("/")でダッシュボードのHTMLを返すsqlite3.connect(":memory:", check_same_thread=False)でスレッド間共有のインメモリDBを開く _sourceでservice.nameとscope名からproviderとruntimeを判定するprompt本文を保存せず属性のみ保持するsafe mode小さく実行確認しながら完成状態まで段階的に組み立てる エンディング。

Python研修はCodeCampでご確認ください。

FastAPI・SQLite・OTLPとは

今回のツールは、Claude CodeとCodexが終了時に出す使用量を受け取り、同じ形式へ整えて表示するローカルWebアプリです。まず始めに、使用しているフレームワークなどについて、簡単に解説していきます。

FastAPIの役割

FastAPIは、PythonでWeb画面とデータ受信口を作るフレームワークです。このツールでは、管理画面や使用量の受信、集計結果の出力を1つにまとめます。

SQLite・OTLPの役割

SQLiteは、Pythonに標準搭載された小さなデータベースです。OTLPは観測データを運ぶ共通形式で、Claude CodeとCodexの項目名が異なっても同じ確認処理へ渡せます。

必要なアプリを確認する

Windows PowerShellを開き、使うサービスの確認コマンドを1行ずつ実行します。Claude Codeだけ測る場合はClaude Code、Codexだけを測る場合はNode.jsとCodexだけ準備すれば問題ありません。見つからないアプリは、記事末尾の公式ページから導入してください。

python --version
claude --version
node --version
codex --version

作業フォルダを作る

次の1行をPowerShellへ貼り付けます。デスクトップにTokenScopeフォルダが作られ、その場所へ移動できれば準備完了です。この後も同じPowerShellで続けます。次の「環境を構築する」にある4行は必須です。最後に環境構築OKと表示されてから、ファイルの保存へ進んでください。

New-Item -ItemType Directory -Path "$env:USERPROFILE\Desktop\TokenScope" -Force | Out-Null; Set-Location "$env:USERPROFILE\Desktop\TokenScope"; Get-Location

Python・FastAPIで開発する場合の環境構築

この記事のセットアップ手順と掲載コードは、Windows 11 Pro、PowerShell 5.1、Python 3.13.3で動作確認しています。仮想環境を有効化せず、その中のPythonを直接指定するため、以下のコマンドはPowerShellとコマンドプロンプト(cmd)の両方で使えます。

python -m venv .venv
.\.venv\Scripts\python.exe -m pip install --upgrade pip
.\.venv\Scripts\python.exe -m pip install fastapi uvicorn opentelemetry-proto python-multipart
.\.venv\Scripts\python.exe -c "import fastapi, uvicorn, opentelemetry.proto; print('環境構築OK')"

macOS・Linuxでは、仮想環境内のPythonパスが異なります。今回の動作確認環境とは異なるため、以下は環境差分を補う参考手順です。

python3 -m venv .venv
./.venv/bin/python -m pip install --upgrade pip
./.venv/bin/python -m pip install fastapi uvicorn opentelemetry-proto python-multipart
./.venv/bin/python -c "import fastapi, uvicorn, opentelemetry.proto; print('環境構築OK')"
  • 画面フォームの受信にStarletteがpython-multipartを使うため、fastapiと一緒に入れてください
  • SQLiteは:memory:のため、管理画面を停止すると取り込み済みデータは消えます
  • 4行を上から順に実行し、最後に環境構築OKと表示されるまで、ファイル保存や管理画面の起動へ進まないでください
  • 記事の主手順はWindows PowerShell専用です。環境構築の4行はcmdでも使えます

このツールでできることと必要なもの

計測したい文章をprompt.txtへ保存し、計測用PythonからClaude CodeまたはCodexを起動します。終了時の使用量だけを管理画面へ送り、実行ごとの合計と内訳を確認します。

このセクションの用語

トークン数と使用率
トークン数はモデルが処理した文章や文脈の量です。使用率は契約やモデルなども含めてサービス側が計算する利用状況で、このツールのトークン数だけからは算出できません。
計測用Pythonがprompt.txtをCLIへ渡し、終了時の使用量だけを管理画面へ送ります prompt.txtに測りたい文章を保存measure_prompt.pyが読み込むClaude Code/Codexを起動して回答終了時の使用量を取り出すローカル管理画面へ記録合計と内訳を確認
計測用Pythonがprompt.txtをCLIへ渡し、終了時の使用量だけを管理画面へ送ります
項目 内容
できること これから実行するプロンプトごとのトークン数を記録
測れないもの 設定前に終えた会話、通常のClaude・ChatGPTのChat画面
必要なもの Windows、PowerShell、Python、Claude CodeまたはCodex
作るファイル tokenscope.py、measure_prompt.py、prompt.txt
初回のゴール 1件の計測結果が管理画面へ表示されること
データ保存 管理画面を止めると消える一時保存
POINT

計測範囲:これから実行する分だけ

過去会話:終了済みの数値は復元不可

使用率:トークン数とは別の指標

Claude Code・Codexのトークン確認要件

取得できるかどうかは、実行方法と使用量の出力経路で決まります。管理画面を起動しただけでアプリ内の会話を自動監視することはできません。

PythonがClaude CodeまたはCodexを子プロセスとして起動し、その1回の終了時情報を受け取る方法に限定している為、確実に測りたい既存プロンプトはprompt.txtへ保存して再実行します。

実行場所・画面 実行環境 今回の扱い
Claude Desktop・Codeタブ Claude Code Pythonランチャーからの実行を計測
Claude Desktop・Chat/Cowork Claude 今回の対象外
ChatGPTデスクトップ・Codex Codex Pythonランチャーからの実行を計測
ChatGPTデスクトップ・Chat/Work ChatGPT 今回の対象外
Claude Code/Codex CLI Claude Code/Codex 終了時情報を取得

デスクトップアプリから実行した場合

CodeタブやCodex画面を手動で操作した会話は、Pythonランチャーの子実行ではありません。この記事と同じ方法で測る場合は、使ったプロンプトをprompt.txtへ貼り付け、後述のコマンドから再実行してください。

実装方針

safe modeが保存しないのは、管理画面のデータベース内のプロンプト本文です。prompt.txtはPCに残り、実行時にはClaude CodeまたはCodexへ送信されます。機密情報を含む文章は入力しないでください。

安全に記録するための条件は、次のとおりです。

  • prompt本文を保存せず属性のみ保持するsafe mode
  • sqlite3.connect(":memory:", check_same_thread=False)でインメモリDBを共有する
  • threading.Lock()でINSERTとDELETEを排他制御する
  • ingestがtry/exceptで不正なOTLPを捕捉しinvalid_otlpを返す
  • 未知providerや欠損トークンをNULLのまま0にせず区別する

トークン消費量を正しく確認する重要ポイント

同じ実行を正しく比べるには、Claude CodeとCodexで異なる識別子と内訳のルールを揃える必要があります。取得できない値は0にせず、unknownとして残します。

このセクションの用語

coverage(取得率)
検出した実行のうち、必要なトークン合計を取得できた割合です。3件すべて取得できれば3/3、つまり100%です。

Claude CodeとCodexの分析単位

Claude Codeはprompt.id、Codexはlauncher_runまたはrun IDを分析単位にします。識別子が無いログを別の実行へ推測で結合すると、重いプロンプトを誤認するため集計対象から外します。

実行環境 分析単位 対応精度
Claude Code prompt.id exact_provider
Codex launcher_run・run ID exact_launcher

サービスごとに異なるトークン計算

Claude Codeでは通常入力、キャッシュ読み取り、キャッシュ作成、出力を合算します。補助モデルが記録された場合は、主モデルと分けます。Codexではキャッシュが入力の内数、推論が出力の内数なので、合計へ二重に足しません。

モデルが異なると固定コンテキストも変わるため、別モデルの生トークン数だけで優劣を決めず、同じ実行環境の内訳を確認します。

欠損値を0にしない

トークン属性が無い実行を0として集計すると、軽いプロンプトに見えてしまいます。unknownとcoverageを一緒に表示し、本当に少ないのか、取得できなかっただけかを分けます。

Pythonでトークン消費量を確認するツールの完成コード

完成コードは1つのPythonファイルです。FastAPIで使用量を受け取り、許可した識別子と数値だけをSQLiteへ保存します。コードを理解しなくても、次の手順で全文を保存できます。

実装を確認する際の中心点は次のとおりです。

  • JSONとProtobufをOTLP型へデコード
  • プロンプト本文を保存しないsafe mode
  • prompt.idとrun IDで実行を相関
  • provider別の包含規則で二重計上を防止
  • NULLとcoverageを併記
  • 同一条件の中央値で改善前後を比較

tokenscope.pyとして保存する

次の1行で空のメモ帳を開きます。そのまま、この節の末尾にある折り畳みから全文をコピーし、貼り付けて保存してください。

notepad .\tokenscope.py

保存は次の順番で進めます。

  1. PowerShellで上の1行を実行して空のメモ帳を開く
  2. メモ帳を開いたまま下の折り畳みまで進む
  3. 「コードをコピー」でtokenscope.py全文をコピーする
  4. メモ帳へ貼り付けてCtrl+Sで保存する
  5. 名前がtokenscope.py.txtではないことを確認する
"""TokenScope: Claude Code/CodexのOTLP Logsを安全に可視化する教材アプリ。"""
    
    import json
    import sqlite3
    import statistics
    import threading
    from html import escape
    
    from fastapi import FastAPI, Request
    from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
    from google.protobuf import json_format
    from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import (
        ExportLogsServiceRequest,
    )
    
    
    _LOCK = threading.Lock()
    _DB = sqlite3.connect(":memory:", check_same_thread=False)
    _DB.row_factory = sqlite3.Row
    _DB.execute(
        """
        CREATE TABLE IF NOT EXISTS llm_calls (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            provider TEXT NOT NULL,
            runtime TEXT NOT NULL,
            model TEXT,
            analysis_unit TEXT NOT NULL,
            unit_kind TEXT NOT NULL,
            correlation_quality TEXT NOT NULL,
            scenario TEXT,
            variant TEXT,
            input_tokens INTEGER,
            output_tokens INTEGER,
            cache_read_tokens INTEGER,
            cache_create_tokens INTEGER,
            reasoning_tokens INTEGER,
            accounted_total_tokens INTEGER,
            token_semantics TEXT NOT NULL
        )
        """
    )
    _DB.commit()
    
    
    def _any_value(value):
        if not isinstance(value, dict):
            return None
        for key in ("stringValue", "intValue", "doubleValue", "boolValue"):
            if key in value:
                return value[key]
        return None
    
    
    def _attributes(items):
        result = {}
        for item in items or []:
            key = item.get("key")
            if isinstance(key, str) and key in {
                "service.name", "provider", "model", "prompt.id", "launcher_run",
                "run.id", "input_tokens", "output_tokens", "cache_read_input_tokens",
                "cache_creation_input_tokens", "reasoning_output_tokens",
                "tokenscope.scenario", "tokenscope.variant",
            }:
                result[key] = _any_value(item.get("value"))
        return result
    
    
    def _token(value):
        if value is None or isinstance(value, bool):
            return None
        try:
            parsed = int(value)
        except (TypeError, ValueError):
            return None
        return parsed if parsed >= 0 else None
    
    
    def _source(service_name, scope_name):
        value = f"{service_name or ''} {scope_name or ''}".lower()
        if "claude" in value or "anthropic" in value:
            return "anthropic", "claude-code"
        if "codex" in value or "openai" in value:
            return "openai", "codex"
        return "unknown", "unknown"
    
    
    def _semantics(provider, attrs):
        input_tokens = _token(attrs.get("input_tokens"))
        output_tokens = _token(attrs.get("output_tokens"))
        cache_read = _token(attrs.get("cache_read_input_tokens"))
        cache_create = _token(attrs.get("cache_creation_input_tokens"))
        reasoning = _token(attrs.get("reasoning_output_tokens"))
        if provider == "anthropic":
            parts = [input_tokens, cache_read or 0, cache_create or 0]
            input_total = sum(parts) if input_tokens is not None else None
            total = input_total + output_tokens if None not in (input_total, output_tokens) else None
            return input_total, output_tokens, cache_read, cache_create, reasoning, total, "anthropic-v1"
        if provider == "openai":
            invalid_cache = cache_read is not None and (
                input_tokens is None or cache_read > input_tokens
            )
            invalid_reasoning = reasoning is not None and (
                output_tokens is None or reasoning > output_tokens
            )
            total = None if invalid_cache or invalid_reasoning or None in (
                input_tokens, output_tokens
            ) else input_tokens + output_tokens
            return input_tokens, output_tokens, cache_read, None, reasoning, total, "openai-v1"
        return input_tokens, output_tokens, cache_read, cache_create, reasoning, None, "unknown"
    
    
    def normalize_otlp(data):
        calls = []
        for resource_log in data.get("resourceLogs", []):
            resource_attrs = _attributes(
                resource_log.get("resource", {}).get("attributes", [])
            )
            for scope_log in resource_log.get("scopeLogs", []):
                scope_name = scope_log.get("scope", {}).get("name")
                provider, runtime = _source(resource_attrs.get("service.name"), scope_name)
                for record in scope_log.get("logRecords", []):
                    attrs = {**resource_attrs, **_attributes(record.get("attributes", []))}
                    if provider == "anthropic" and attrs.get("prompt.id"):
                        unit, kind, quality = str(attrs["prompt.id"]), "prompt", "exact_provider"
                    elif provider == "openai" and (
                        attrs.get("launcher_run") or attrs.get("run.id")
                    ):
                        unit = str(attrs.get("launcher_run") or attrs.get("run.id"))
                        kind, quality = "launcher_run", "exact_launcher"
                    else:
                        continue
                    values = _semantics(provider, attrs)
                    calls.append({
                        "provider": provider,
                        "runtime": runtime,
                        "model": attrs.get("model"),
                        "analysis_unit": unit,
                        "unit_kind": kind,
                        "correlation_quality": quality,
                        "scenario": attrs.get("tokenscope.scenario"),
                        "variant": attrs.get("tokenscope.variant"),
                        "input_tokens": values[0],
                        "output_tokens": values[1],
                        "cache_read_tokens": values[2],
                        "cache_create_tokens": values[3],
                        "reasoning_tokens": values[4],
                        "accounted_total_tokens": values[5],
                        "token_semantics": values[6],
                    })
        return calls
    
    
    def decode_otlp(body, content_type):
        request = ExportLogsServiceRequest()
        if "json" in content_type:
            json_format.Parse(body.decode("utf-8"), request)
        elif "protobuf" in content_type or "octet-stream" in content_type:
            request.ParseFromString(body)
        else:
            raise ValueError("unsupported_content_type")
        return json_format.MessageToDict(request)
    
    
    def ingest(body, content_type):
        try:
            calls = normalize_otlp(decode_otlp(body, content_type))
        except Exception:
            return {"ok": False, "ingested": 0, "error": "invalid_otlp"}
        columns = tuple(calls[0].keys()) if calls else ()
        if calls:
            placeholders = ",".join("?" for _ in columns)
            with _LOCK:
                _DB.executemany(
                    f"INSERT INTO llm_calls ({','.join(columns)}) VALUES ({placeholders})",
                    [[call[column] for column in columns] for call in calls],
                )
                _DB.commit()
        return {"ok": True, "ingested": len(calls)}
    
    
    def _fixture(service, records):
        return {"resourceLogs": [{
            "resource": {"attributes": [{
                "key": "service.name", "value": {"stringValue": service}
            }]},
            "scopeLogs": [{"scope": {"name": service}, "logRecords": records}],
        }]}
    
    
    def _record(unit_key, unit, model, input_tokens, output_tokens, variant=None, **extra):
        values = {
            unit_key: unit,
            "model": model,
            "input_tokens": input_tokens,
            "output_tokens": output_tokens,
            "tokenscope.scenario": "article-body",
            "tokenscope.variant": variant,
            **extra,
        }
        attributes = []
        for key, value in values.items():
            if value is None:
                continue
            field = "intValue" if isinstance(value, int) else "stringValue"
            attributes.append({"key": key, "value": {field: str(value)}})
        return {"attributes": attributes}
    
    
    CLAUDE_FIXTURE = _fixture("claude-code", [
        _record("prompt.id", "prompt-before-a", "claude-opus-4-8", 18000, 4000,
                "before", cache_read_input_tokens=2000),
        _record("prompt.id", "prompt-before-b", "claude-opus-4-8", 17600, 4000,
                "before", cache_read_input_tokens=1800),
        _record("prompt.id", "prompt-before-c", "claude-opus-4-8", 19000, 4000,
                "before", cache_read_input_tokens=2200),
        _record("prompt.id", "prompt-after-a", "claude-opus-4-8", 11000, 3000,
                "after", cache_read_input_tokens=1600),
        _record("prompt.id", "prompt-after-b", "claude-opus-4-8", 10900, 3000,
                "after", cache_read_input_tokens=1500),
        _record("prompt.id", "prompt-after-c", "claude-opus-4-8", 11500, 3000,
                "after", cache_read_input_tokens=1700),
    ])
    CODEX_FIXTURE = _fixture("codex", [
        _record("launcher_run", "run-codex-001", "gpt-5.6-sol", 7200, 1800,
                cache_read_input_tokens=1200, reasoning_output_tokens=600),
        _record("launcher_run", "run-codex-missing", "gpt-5.6-sol", None, None),
    ])
    
    
    def load_fixtures():
        with _LOCK:
            _DB.execute("DELETE FROM llm_calls")
            _DB.commit()
        claude_body = json.dumps(CLAUDE_FIXTURE).encode("utf-8")
        proto = ExportLogsServiceRequest()
        json_format.ParseDict(CODEX_FIXTURE, proto)
        first = ingest(claude_body, "application/json")
        second = ingest(proto.SerializeToString(), "application/x-protobuf")
        return first["ingested"] + second["ingested"]
    
    
    def overview():
        with _LOCK:
            rows = _DB.execute(
                "SELECT provider, runtime, model, analysis_unit, unit_kind, "
                "correlation_quality, scenario, variant, input_tokens, output_tokens, "
                "cache_read_tokens, reasoning_tokens, accounted_total_tokens, token_semantics "
                "FROM llm_calls ORDER BY id"
            ).fetchall()
        calls = [dict(row) for row in rows]
        known = [row["accounted_total_tokens"] for row in calls
                 if row["accounted_total_tokens"] is not None]
        coverage = len(known) / len(calls) if calls else 0
        return {"calls": calls, "known_total": sum(known), "known": len(known),
                "detected": len(calls), "coverage": coverage}
    
    
    def bottlenecks(calls):
        groups = {}
        for call in calls:
            if call["accounted_total_tokens"] is None:
                continue
            key = (call["provider"], call["runtime"], call["model"], call["token_semantics"])
            groups.setdefault(key, []).append(call)
        ranked = []
        for key, items in groups.items():
            for rank, call in enumerate(
                sorted(items, key=lambda row: row["accounted_total_tokens"], reverse=True), 1
            ):
                ranked.append({**call, "rank": rank, "stratum": " / ".join(key)})
        return ranked
    
    
    def comparisons(calls):
        groups = {}
        for call in calls:
            if call["accounted_total_tokens"] is None or call["variant"] not in {
                "before", "after"
            }:
                continue
            key = (call["scenario"], call["provider"], call["runtime"],
                   call["model"], call["token_semantics"])
            groups.setdefault(key, {"before": [], "after": []})[call["variant"]].append(
                call["accounted_total_tokens"]
            )
        result = []
        for key, arms in groups.items():
            if not arms["before"] or not arms["after"]:
                continue
            before = statistics.median(arms["before"])
            after = statistics.median(arms["after"])
            reduction = 100 * (before - after) / before if before else None
            result.append({"scenario": key[0], "stratum": " / ".join(key[1:]),
                           "before": before, "after": after, "reduction": reduction,
                           "before_n": len(arms["before"]), "after_n": len(arms["after"]),
                           "p95": None})
        return result
    
    
    def _value(value):
        return "—" if value is None else escape(f"{value:,}" if isinstance(value, int) else str(value))
    
    
    def render_page():
        data = overview()
        coverage = f"{data['known']} / {data['detected']} ({data['coverage']:.0%})"
        rank_rows = "".join(
            f"<tr><td>{row['rank']}</td><td>{escape(row['stratum'])}</td>"
            f"<td>{escape(row['analysis_unit'])}</td><td>{row['unit_kind']}</td>"
            f"<td>{row['correlation_quality']}</td><td class='num'>{row['accounted_total_tokens']:,}</td></tr>"
            for row in bottlenecks(data["calls"])
        )
        comparison_rows = "".join(
            f"<tr><td>{escape(row['scenario'])}</td><td>{escape(row['stratum'])}</td>"
            f"<td>{row['before']:,.0f} (n={row['before_n']})</td>"
            f"<td>{row['after']:,.0f} (n={row['after_n']})</td>"
            f"<td>{row['reduction']:.1f}%</td><td>N/A (n&lt;20)</td></tr>"
            for row in comparisons(data["calls"])
        )
        detail_rows = "".join(
            f"<tr><td>{row['runtime']}</td><td>{escape(row['analysis_unit'])}</td>"
            f"<td>{row['unit_kind']} / {row['correlation_quality']}</td>"
            f"<td class='num'>{_value(row['input_tokens'])}</td>"
            f"<td class='num'>{_value(row['output_tokens'])}</td>"
            f"<td class='num'>{_value(row['cache_read_tokens'])}</td>"
            f"<td class='num'>{_value(row['reasoning_tokens'])}</td>"
            f"<td class='num'>{_value(row['accounted_total_tokens'])}</td></tr>"
            for row in data["calls"]
        )
        empty = "<p class='empty'>fixtureを投入すると分析結果が表示されます。</p>"
        return PAGE.format(
            total=f"{data['known_total']:,}", coverage=coverage,
            rank_rows=rank_rows or "<tr><td colspan='6'>未集計</td></tr>",
            comparison_rows=comparison_rows or "<tr><td colspan='6'>比較データなし</td></tr>",
            detail_rows=detail_rows or "<tr><td colspan='8'>未集計</td></tr>",
            empty=empty if not data["calls"] else "",
        )
    
    
    PAGE = """<!doctype html><html lang="ja"><head><meta charset="utf-8">
    <title>TokenScope</title><style>
    body{{font-family:system-ui,sans-serif;margin:0;background:#f4f7fb;color:#14213d}}
    main{{max-width:1120px;margin:auto;padding:28px}}header{{display:flex;justify-content:space-between;align-items:center}}
    h1{{margin:0;font-size:28px}}h2{{margin-top:28px;font-size:20px}}button{{background:#1769e0;color:white;border:0;border-radius:6px;padding:10px 18px;font-weight:700}}
    .stats{{display:grid;grid-template-columns:repeat(2,1fr);gap:14px;margin-top:20px}}.card{{background:white;border:1px solid #d8e1ee;border-radius:10px;padding:18px}}
    .label{{color:#60708a;font-size:13px}}.metric{{font-size:25px;font-weight:800;margin-top:4px}}.note{{color:#60708a;font-size:13px}}table{{width:100%;border-collapse:collapse;background:white;font-size:13px}}
    th,td{{border-bottom:1px solid #e1e7f0;padding:9px;text-align:left}}th{{background:#eaf1fb}}td.num{{text-align:right}}.empty{{padding:12px;background:#fff7db;border-left:4px solid #d49a00}}
    </style></head><body><main><header><div><h1>TokenScope</h1><p>Claude Code / Codexのトークンを、取得確度とcoverage付きで確認します。</p></div>
    <form method="post" action="/fixtures"><button id="load-fixtures" type="submit">JSON+Protobuf fixtureを投入</button></form></header>{empty}
    <div class="stats"><div class="card"><div class="label">観測できたトークン合計</div><div id="known-total" class="metric">{total}</div><div class="note">provider別の包含規則を反映</div></div>
    <div class="card"><div class="label">field coverage</div><div id="coverage" class="metric">{coverage}</div><div class="note">未着・欠損は0にせずNULL</div></div></div>
    <h2 id="bottlenecks">ボトルネック順位</h2><table id="bottleneck-table"><thead><tr><th>順位</th><th>同一stratum</th><th>分析単位</th><th>粒度</th><th>相関品質</th><th>tokens</th></tr></thead><tbody>{rank_rows}</tbody></table>
    <h2 id="comparison">改善前後の中央値比較</h2><table id="comparison-table"><thead><tr><th>scenario</th><th>同一stratum</th><th>before中央値</th><th>after中央値</th><th>削減率</th><th>p95</th></tr></thead><tbody>{comparison_rows}</tbody></table>
    <h2 id="details">取得明細</h2><table id="detail-table"><thead><tr><th>runtime</th><th>分析単位</th><th>粒度 / 確度</th><th>input</th><th>output</th><th>cache read</th><th>reasoning</th><th>accounted total</th></tr></thead><tbody>{detail_rows}</tbody></table>
    </main></body></html>"""
    
    
    app = FastAPI(title="TokenScope")
    ROUTES = ["/", "/fixtures", "/v1/logs", "/api/overview"]
    
    
    @app.get("/", response_class=HTMLResponse)
    def dashboard():
        return HTMLResponse(render_page())
    
    
    @app.post("/fixtures")
    def fixtures():
        load_fixtures()
        return RedirectResponse("/#bottlenecks", status_code=303)
    
    
    @app.post("/v1/logs")
    async def otlp_logs(request: Request):
        result = ingest(await request.body(), request.headers.get("content-type", ""))
        return JSONResponse(result, status_code=200 if result["ok"] else 400)
    
    
    @app.get("/api/overview")
    def api_overview():
        data = overview()
        data["bottlenecks"] = bottlenecks(data["calls"])
        data["comparisons"] = comparisons(data["calls"])
        return data
    

measure_prompt.pyとprompt.txtを保存する

tokenscope.pyと同じTokenScopeフォルダへ、計測用コードと計測したい文章を保存します。3ファイルは必ず同じ場所へ揃えてください。

measure_prompt.pyとして保存する

最初の1行で空のメモ帳を開き、次の折り畳みから全文をコピーします。メモ帳へ貼り付け、Ctrl+Sで保存してください。

notepad .\measure_prompt.py
"""既存プロンプトをClaude CodeまたはCodexで実行し、usageを記録する。"""
    
    from __future__ import annotations
    
    import argparse
    import hashlib
    import json
    import os
    import shutil
    import subprocess
    import sys
    import time
    import urllib.request
    import uuid
    from datetime import datetime
    from pathlib import Path
    from typing import Any
    
    
    DEFAULT_ENDPOINT = "http://127.0.0.1:8877/v1/logs"
    DEFAULT_CODEX_PACKAGE = "@openai/codex@0.146.0"
    
    
    def _command(name: str) -> str:
        candidates = [f"{name}.cmd", name] if os.name == "nt" else [name]
        for candidate in candidates:
            found = shutil.which(candidate)
            if found:
                return found
        raise RuntimeError(f"コマンドが見つかりません: {name}")
    
    
    def _run(command: list[str], prompt: str, cwd: Path, timeout: int,
             env: dict[str, str] | None = None) -> subprocess.CompletedProcess[str]:
        completed = subprocess.run(
            command,
            input=prompt,
            cwd=cwd,
            capture_output=True,
            text=True,
            encoding="utf-8",
            errors="replace",
            timeout=timeout,
            check=False,
            env=env,
        )
        if completed.returncode != 0:
            tail = completed.stderr[-1200:].strip()
            raise RuntimeError(f"LLMの実行に失敗しました(exit={completed.returncode})\n{tail}")
        return completed
    
    
    def _claude(prompt: str, cwd: Path, model: str | None, effort: str | None,
                profile: Path | None) -> tuple[Any, str, dict[str, int]]:
        command = [
            _command("claude"),
            "--print",
            "--output-format",
            "json",
            "--tools",
            "",
            "--permission-mode",
            "dontAsk",
            "--no-session-persistence",
        ]
        if model:
            command.extend(["--model", model])
        if effort:
            command.extend(["--effort", effort])
        env = os.environ.copy()
        if profile is not None:
            for name in ("ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "CLAUDE_CONFIG_DIR"):
                env.pop(name, None)
            env["CLAUDE_CONFIG_DIR"] = str(profile.resolve())
        completed = _run(command, prompt, cwd, timeout=300, env=env)
        payload = json.loads(completed.stdout)
        raw_usage = payload.get("usage") or {}
        input_tokens = int(raw_usage.get("input_tokens") or 0)
        cache_read = int(raw_usage.get("cache_read_input_tokens") or 0)
        cache_create = int(raw_usage.get("cache_creation_input_tokens") or 0)
        output_tokens = int(raw_usage.get("output_tokens") or 0)
        usage = {
            "input_tokens": input_tokens,
            "cached_input_tokens": cache_read,
            "cache_write_input_tokens": cache_create,
            "output_tokens": output_tokens,
            "reasoning_output_tokens": 0,
            "observed_total_tokens": input_tokens + cache_read + cache_create + output_tokens,
        }
        return payload, str(payload.get("result") or ""), usage
    
    
    def _codex_command() -> list[str]:
        # Windows版デスクトップアプリ内のcodex.exeを直接起動できない環境でも、
        # 検証済みのnpmパッケージへフォールバックできるようにします。
        codex_cmd = shutil.which("codex.cmd")
        if codex_cmd:
            return [codex_cmd]
        if os.name != "nt":
            codex = shutil.which("codex")
            if codex:
                return [codex]
        return [_command("npx"), "-y", DEFAULT_CODEX_PACKAGE]
    
    
    def _codex(prompt: str, cwd: Path, model: str | None, effort: str | None) -> tuple[Any, str, dict[str, int]]:
        command = [
            *_codex_command(),
            "exec",
            "-",
            "--json",
            "--ephemeral",
            "--sandbox",
            "read-only",
            "--skip-git-repo-check",
            "--cd",
            str(cwd),
        ]
        if model:
            command.extend(["--model", model])
        if effort:
            command.extend(["--config", f'model_reasoning_effort="{effort}"'])
        completed = _run(command, prompt, cwd, timeout=420)
        events = [json.loads(line) for line in completed.stdout.splitlines() if line.strip()]
        answer = ""
        raw_usage: dict[str, Any] = {}
        for event in events:
            item = event.get("item")
            if event.get("type") == "item.completed" and isinstance(item, dict):
                if item.get("type") == "agent_message":
                    answer = str(item.get("text") or "")
            if event.get("type") == "turn.completed":
                raw_usage = event.get("usage") or {}
        if not raw_usage:
            raise RuntimeError("Codexのturn.completed.usageが見つかりません")
        input_tokens = int(raw_usage.get("input_tokens") or 0)
        output_tokens = int(raw_usage.get("output_tokens") or 0)
        usage = {
            "input_tokens": input_tokens,
            "cached_input_tokens": int(raw_usage.get("cached_input_tokens") or 0),
            "cache_write_input_tokens": int(raw_usage.get("cache_write_input_tokens") or 0),
            "output_tokens": output_tokens,
            "reasoning_output_tokens": int(raw_usage.get("reasoning_output_tokens") or 0),
            "observed_total_tokens": input_tokens + output_tokens,
        }
        return events, answer, usage
    
    
    def _attribute(key: str, value: str | int) -> dict[str, Any]:
        field = "intValue" if isinstance(value, int) else "stringValue"
        return {"key": key, "value": {field: str(value)}}
    
    
    def _otlp(runtime: str, model: str, effort: str, run_id: str,
              scenario: str, variant: str, usage: dict[str, int]) -> dict[str, Any]:
        unit_key = "prompt.id" if runtime == "claude-code" else "launcher_run"
        attributes = [
            _attribute(unit_key, run_id),
            _attribute("model", model),
            _attribute("input_tokens", usage["input_tokens"]),
            _attribute("output_tokens", usage["output_tokens"]),
            _attribute("tokenscope.scenario", scenario),
            _attribute("tokenscope.variant", variant),
        ]
        if usage["cached_input_tokens"]:
            attributes.append(_attribute("cache_read_input_tokens", usage["cached_input_tokens"]))
        if runtime == "claude-code" and usage["cache_write_input_tokens"]:
            attributes.append(_attribute(
                "cache_creation_input_tokens", usage["cache_write_input_tokens"]
            ))
        if runtime == "codex" and usage["reasoning_output_tokens"]:
            attributes.append(_attribute(
                "reasoning_output_tokens", usage["reasoning_output_tokens"]
            ))
        return {
            "resourceLogs": [{
                "resource": {"attributes": [_attribute("service.name", runtime)]},
                "scopeLogs": [{
                    "scope": {"name": runtime},
                    "logRecords": [{"attributes": attributes}],
                }],
            }],
        }
    
    
    def _post(endpoint: str, payload: dict[str, Any]) -> dict[str, Any]:
        request = urllib.request.Request(
            endpoint,
            data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
            headers={"Content-Type": "application/json"},
            method="POST",
        )
        with urllib.request.urlopen(request, timeout=15) as response:
            return json.loads(response.read().decode("utf-8"))
    
    
    def main() -> int:
        parser = argparse.ArgumentParser(
            description="prompt.txtをClaude CodeまたはCodexで実行し、usageを記録します"
        )
        parser.add_argument("runtime", choices=("claude-code", "codex"))
        parser.add_argument("prompt_file", type=Path)
        parser.add_argument("--model")
        parser.add_argument("--effort", choices=("low", "medium", "high", "xhigh", "max"))
        parser.add_argument("--claude-profile", type=Path)
        parser.add_argument("--scenario", default="existing-prompt")
        parser.add_argument("--variant", default="baseline")
        parser.add_argument("--endpoint", default=DEFAULT_ENDPOINT)
        parser.add_argument("--cwd", type=Path, default=Path.cwd())
        parser.add_argument("--output-dir", type=Path, default=Path("measurement-output"))
        parser.add_argument(
            "--save-response",
            action="store_true",
            help="回答本文と生のCLI出力も結果JSONへ保存します",
        )
        args = parser.parse_args()
    
        prompt_path = args.prompt_file.resolve()
        cwd = args.cwd.resolve()
        prompt = prompt_path.read_text(encoding="utf-8")
        if not prompt.strip():
            raise ValueError("プロンプトファイルが空です")
    
        runtime = args.runtime
        model = args.model or "default"
        effort = args.effort or "default"
        run_id = f"run-{runtime}-{uuid.uuid4().hex[:12]}"
        started = time.monotonic()
        print(f"[1/3] {prompt_path.name}を{runtime}で実行します", flush=True)
        if runtime == "claude-code":
            raw, answer, usage = _claude(
                prompt, cwd, args.model, args.effort, args.claude_profile
            )
        else:
            raw, answer, usage = _codex(prompt, cwd, args.model, args.effort)
        elapsed_ms = round((time.monotonic() - started) * 1000)
    
        print("[2/3] usageを取得しました", flush=True)
        print(json.dumps(usage, ensure_ascii=False, indent=2), flush=True)
        ingest = _post(
            args.endpoint,
            _otlp(runtime, model, effort, run_id, args.scenario, args.variant, usage),
        )
        print(f"[3/3] 管理画面へ記録しました: {ingest}", flush=True)
    
        args.output_dir.mkdir(parents=True, exist_ok=True)
        stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
        output_path = args.output_dir / f"{stamp}-{runtime}.json"
        result = {
            "runtime": runtime,
            "model": model,
            "effort": effort,
            "run_id": run_id,
            "scenario": args.scenario,
            "variant": args.variant,
            "prompt_file_name": prompt_path.name,
            "prompt_sha256": hashlib.sha256(prompt.encode("utf-8")).hexdigest(),
            "prompt_chars": len(prompt),
            "usage": usage,
            "elapsed_ms": elapsed_ms,
            "ingest": ingest,
        }
        if args.save_response:
            result["answer"] = answer
            result["raw"] = raw
        output_path.write_text(
            json.dumps(result, ensure_ascii=False, indent=2) + "\n",
            encoding="utf-8",
        )
        print(f"結果ファイル: {output_path.resolve()}")
        return 0
    
    
    if __name__ == "__main__":
        sys.exit(main())
    

prompt.txtとして保存する

次の1行でメモ帳を開き、計測したい既存プロンプトを貼り付けてCtrl+Sで保存します。まず動作だけ確かめる場合は、下のサンプル文をそのまま使えます。

notepad .\prompt.txt
Pythonの仮想環境を、プログラミング未経験者向けに200文字以内で説明してください。

3つのファイルと仮想環境を確認する

次の1行を実行し、4項目のExistsがすべてTrueになれば準備完了です。Falseがある場合は先へ進まず、ファイル名または環境構築を確認します。末尾が.py.txtになっている場合は、メモ帳で名前を付け直してください。

@('.\tokenscope.py','.\measure_prompt.py','.\prompt.txt','.\.venv\Scripts\python.exe') | ForEach-Object { [PSCustomObject]@{Path=$_; Exists=(Test-Path $_)} }
確認する項目 役割
tokenscope.py ローカル管理画面
measure_prompt.py LLMの起動と使用量取得
prompt.txt 計測する文章
.venv\Scripts\python.exe 記事専用のPython仮想環境

venvの実行を忘れる場合が多いので、まだ実行をしていない場合は、Python・FastAPIで開発する場合の環境構築に掲載されているコードを実行してください。

Claude Code・Codexのトークン消費量を1回確認する

Pythonがprompt.txtを読み、Claude CodeまたはCodexを起動し、終了時情報から使用量を取り出して管理画面へ送ります。LLMを先に手動で起動する手順ではありません。

工程 成功の目印
準備確認 3ファイルと仮想環境のExistsがすべてTrue
起動前確認 起動準備OKと表示される
管理画面起動 Uvicorn runningと表示される
ブラウザ表示 トークン分析ダッシュボードが開く
LLM実行 [1/3]プロンプト実行開始が表示される
使用量取得 [2/3] usageを取得しましたと表示される
記録完了 [3/3]管理画面へ記録しましたと表示される
初回の画面 ボトルネック欄に1行表示される

ターミナル1で管理画面を起動する

最初に「起動前の確認」を実行します。起動準備OKと表示された場合だけ、次の起動コマンドへ進んでください。Uvicorn runningと表示されたら成功です。この画面は計測が終わるまで閉じず、そのまま待機させます。

Set-Location "$env:USERPROFILE\Desktop\TokenScope"; .\.venv\Scripts\python.exe -c "import fastapi, uvicorn, opentelemetry.proto; print('起動準備OK')"
Set-Location "$env:USERPROFILE\Desktop\TokenScope"; .\.venv\Scripts\python.exe -m uvicorn tokenscope:app --host 127.0.0.1 --port 8877

ターミナル2で1回計測する

新しいPowerShellを開き、最初に管理画面を表示します。その後、Claude Code用かCodex用のどちらか1行だけを実行してください。

Start-Process "http://127.0.0.1:8877"
Set-Location "$env:USERPROFILE\Desktop\TokenScope"; .\.venv\Scripts\python.exe .\measure_prompt.py claude-code .\prompt.txt --scenario first-check --variant current
Set-Location "$env:USERPROFILE\Desktop\TokenScope"; .\.venv\Scripts\python.exe .\measure_prompt.py codex .\prompt.txt --scenario first-check --variant current

1回目の結果を確認する

[3/3]まで表示されたらブラウザを更新します。ボトルネック欄に1行増えていれば成功です。初回は比較用データが無いため、比較表が空でも失敗ではありません。

PythonからClaude Codeを実行し、終了時の使用量をローカル管理画面へ記録する無音デモ
PythonからClaude Codeを実行し、終了時の使用量をローカル管理画面へ記録する無音デモ
サンプルログの取込後にClaude CodeとCodexの集計カード、実行ID、入出力を確認した画面
サンプルログの取込後にClaude CodeとCodexの集計カード、実行ID、入出力を確認した画面
さらに詳しく学べるPython研修の詳細はこちら

普段使っているプロンプトを3回計測する

動作確認が終わったら、prompt.txtを普段使っている文章へ置き換えます。同じ文章を同じサービスで3回続けて実行し、単発のばらつきではなく現在の基準値を残します。

このセクションの用語

中央値
3回の結果を小さい順に並べた中央の値です。極端に多い1回や少ない1回の影響を受けにくいため、現在の基準値に使います。

計測する既存プロンプトを用意する

次の1行でprompt.txtを開き、初回確認用の文章を削除して、普段使っているプロンプトへ置き換えます。毎回同じ内容を使うため、編集後にCtrl+Sで保存してください。

notepad .\prompt.txt

Claude CodeまたはCodexで3回実行する

使っているサービス側の1行だけを実行します。両方を比べる必要はありません。3回分の回答が生成されるため、サブスクの残量を確認してから実行してください。

Set-Location "$env:USERPROFILE\Desktop\TokenScope"; 1..3 | ForEach-Object { .\.venv\Scripts\python.exe .\measure_prompt.py claude-code .\prompt.txt --scenario current-prompt-check --variant current }
Set-Location "$env:USERPROFILE\Desktop\TokenScope"; 1..3 | ForEach-Object { .\.venv\Scripts\python.exe .\measure_prompt.py codex .\prompt.txt --scenario current-prompt-check --variant current }

直近3回の基準値を表示する

3回が終わった直後に次の1行を実行します。直近3件の合計トークンを並べ、最小値、中央値、最大値をPowerShellへ表示します。数値が3つ出れば実験完了です。

$values = @(Get-ChildItem .\measurement-output\*.json | Sort-Object LastWriteTime -Descending | Select-Object -First 3 | ForEach-Object { (Get-Content $_.FullName -Raw | ConvertFrom-Json).usage.observed_total_tokens } | Sort-Object); [pscustomobject]@{ Minimum=$values[0]; Median=$values[1]; Maximum=$values[2] }

管理画面で3件を確認する

ブラウザを更新し、ボトルネック欄に今回の3件が追加されたことを確認します。この記事では現在値を測るだけなので、前後比較の表が空でも正常です。

ブラウザで取込処理を実行し、プロンプト単位の明細が表示されることを確認した画面
ブラウザで取込処理を実行し、プロンプト単位の明細が表示されることを確認した画面

トークン消費量の内訳を確認する

合計だけでは、ユーザーが書いた文章、会話コンテキスト、回答、内部推論のどこが大きいか分かりません。管理画面と結果JSONを見て、次に調べる場所を切り分けます。

多い項目 主に含まれるもの 確認する場所
入力トークン プロンプト、会話履歴、指示ファイル、読み込んだ資料 prompt.txt以外に渡した文脈
キャッシュ 再利用された入力、今回新しく保存した入力 初回と2・3回目の差
出力トークン 画面へ返された回答 指定した出力形式と回答の長さ
推論分 Codexの出力に含まれる内部推論 出力トークンの内訳
unknown 取得できなかった項目 coverageと終了時情報

入力トークンが多い場合

prompt.txtの文字数だけでなく、会話履歴、AGENTS.md、skills、ツール定義、読み込んだファイルも入力へ含まれます。まず3回とも同じ条件で実行できたかを確認します。

出力トークンが多い場合

回答の長さや出力形式が影響します。回答が途中で失敗していないことを確認し、3回で出力トークンが大きく変わる場合は、指示の範囲が曖昧でないかを調べます。

キャッシュ・推論を読む

Claude Codeのキャッシュ読み取り・作成は入力の構成要素として合計します。Codexのキャッシュ済み入力は入力トークンの内数、推論分は出力トークンの内数なので、もう一度足しません。

現在の基準値を残す

一律に『何トークン以下なら正常』とは決められません。今回の3回の中央値と範囲を、そのプロンプト・実行環境・モデルにおける現在値として記録します。

さらに詳しく学べるPython研修の詳細はこちら

トークン消費量を確認できない場合の対処

止まった場所に合わせて対処します。コマンドが動かない問題と、表示された数字が不自然な問題を分けると原因を見つけやすくなります。

今回の仮想環境エラーを直す

「.\.venv\Scripts\python.exeは認識されません」と表示された場合は、TokenScopeフォルダ内に仮想環境が作られていません。Uvicornの問題ではないため、次の復旧用コードをPowerShellへまとめて貼り付けます。最後に復旧完了と表示されたら、ターミナル1の起動前確認へ戻ってください。

$ErrorActionPreference = 'Stop'
Set-Location "$env:USERPROFILE\Desktop\TokenScope"
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install --upgrade pip
.\.venv\Scripts\python.exe -m pip install fastapi uvicorn opentelemetry-proto python-multipart
.\.venv\Scripts\python.exe -c "import fastapi, uvicorn, opentelemetry.proto; print('復旧完了')"

動かないとき

画面のエラー文に近い行を探し、右端の対処を1つずつ実行してください。PowerShellに >> が出た場合は、Ctrl+Cで止めてから記事の1行版をコピーし直します。

症状 主な原因 対処
pythonが見つからない Python未導入・再起動前 公式ページから導入しPowerShellを開き直す
.venvのpythonが認識されない 環境構築の4行を未実行・作業場所が違う 上の復旧用コードを実行
No module named パッケージ未導入 環境構築のpip installを再実行
8877番を使用中 前回の管理画面が動作中 前回のPowerShellでCtrl+Cを押す
接続を拒否された 管理画面が停止中 ターミナル1の起動結果を確認
claudeが無い Claude Code未導入 公式導入ページから準備・ログイン
npxが無い Node.js未導入 Node.jsを導入しPowerShellを開き直す
>>で入力が止まる 引用符・改行が未完了 Ctrl+C後に1行版をコピー
文字化けする UTF-8以外で保存 メモ帳でUTF-8保存し直す
利用上限エラー サブスク枠の不足 リセット後に必要な回数だけ実行

数字がおかしいとき

数字が少なすぎる場合はcoverage、多すぎる場合は二重加算と重複記録を先に確認します。トークン数とサブスク使用率は同じ指標ではありません。

症状 主な原因 確認方法
終了直後の実行が無い 終了時のログflush前に停止 正常終了を待ってからJSON・OTLPを確認
合計が過大 キャッシュ・推論を二重加算 provider別の包含規則を確認
同じrunが複数回入る 再送ログを重複保存 prompt.id・run IDで重複排除
coverageが低い トークン属性または相関IDが欠損 0埋めせずNULLの行を確認
モデル別に分かれない model・effortの記録漏れ 起動引数と実測provenanceを保存
使用率と一致しない サブスク上限と観測トークンを混同 公式残量と相対比較を分けて確認

Claude Code・Codexのトークン確認まとめ

Windows PowerShellで3ファイルを同じフォルダへ保存し、管理画面を起動してから、別のPowerShellでClaude CodeまたはCodexを実行すれば、プロンプトごとの使用量を記録できます。

最初はサンプルを1回実行し、次に普段のプロンプトを3回測ります。最小値・中央値・最大値と、入力・キャッシュ・出力・推論の内訳を確認できれば、現在の状態を判断するための基準値が完成です。

このツールが示すトークン数は、サービス側の使用率そのものではありません。通常のChat画面や過去の会話を含めず、Pythonランチャーから実行したClaude Code・Codexだけを同じ条件で確認してください。

さらに詳しく学べるPython研修の詳細はこちら

参考にした一次情報

  1. ^ Python公式ダウンロード. https://www.python.org/downloads/, (参照26-08-05).
  2. ^ Claude Code公式インストール手順. https://code.claude.com/docs/en/installation, (参照26-08-05).
  3. ^ Claude Code monitoring usage (OpenTelemetry). https://code.claude.com/docs/en/monitoring-usage, (参照26-08-05).
  4. ^ Node.js公式ダウンロード. https://nodejs.org/en/download, (参照26-08-05).
  5. ^ Codex CLI公式ガイド. https://learn.chatgpt.com/docs/codex/cli, (参照26-08-05).
  6. ^ OpenTelemetry Protocol (OTLP) Specification. https://opentelemetry.io/docs/specs/otlp/, (参照26-08-05).
  7. ^ FastAPI公式ドキュメント. https://fastapi.tiangolo.com/, (参照26-08-05).

※内容は執筆時点のものです。ライブラリやサイトの仕様は変わる可能性があるため、公式ドキュメントもあわせてご確認ください。

法人向け研修のご案内

研修を導入しても、現場で成果につながらない。
特定の担当者や特定の分野の知識だけに依存し、事業全体の接続が見えていない。
SEOや広告について、担当者任せになっており、投資判断の根拠が社内で共有されていない。
DXを進めたいが、部署ごとに知識や活用レベルがバラバラで、組織としての方針を整理したい。
こうした課題を感じている企業では、ツールの使い方だけでなく、業務設計、基礎知識、部門横断で成果を接続する力を含めた人材育成が重要です。

こうした課題を感じている企業では、ツールの使い方だけではなく、業務設計、基礎知識、部門横断で成果を接続する力を含めた人材育成が重要です。

企業が抱えやすい課題 研修で整理すべきテーマ
担当領域ごとに知識が閉じてしまっている マーケティング・開発・PM・経営がそれぞれの判断軸を理解し合える土台を作る。
SEOや広告の投資判断が担当者任せになっている ROASや付加価値を数字で説明できる基礎知識と検証力を育てる。
社員ごとのITリテラシーに差がある 基礎知識、情報整理、データの見方、ツール活用の土台を揃える。
一部のエースや担当者に事業判断が依存している 全社で仮説検証と撤退基準を共有できる体制を設計する。
AIやデータを事業成果に結びつけられない ゴール設計、業務分解、検証指標、改善サイクルを設計する。

専門性を、成果につながる接続力へ。

CodeCampでは、企業のAI活用方針や育成目標に合わせ、生成AI活用、DX人材育成、ITリテラシー、データ分析、Web制作・開発領域まで、実務に合わせた研修設計をご提案しています。

専門性を「深める」だけで終わらせず、部門を越えて成果につなげるために。
まずは、自社に必要な学習領域と活用ステップを整理するところから始めてみてはいかがでしょうか。

執筆:コードキャンプ株式会社 マーケティング部 木守健介

ブログに戻る

コメントを残す

コメントは公開前に承認される必要があることにご注意ください。

企業・法人向けのIT・プログラミング・生成AI研修を探す、比較する - IT・プログラミングを知って学べるコネクトメディア CodeCampが提供するDX人材育成が可能なプログラミングやITが学べる公開講座 - IT・プログラミングを知って学べるコネクトメディア コードキャンプが提供する無料で学べるプログラミングスクール講座 - IT・プログラミングを知って学べるコネクトメディア コードキャンプDX人材育成研修 - IT・プログラミングを知って学べるコネクトメディア 3.5日の研修で、年間1,600時間の削減効果が見込まれる。東京きらぼしフィナンシャルグループのDX人材育成事例 - IT・プログラミングを知って学べるコネクトメディア 配属3ヶ月で30%の生産性向上を実現するいよぎんコンピュータサービスの新人研修に迫る - IT・プログラミングを知って学べるコネクトメディア 金融業界の業務効率化を加速するニッセイアセットマネジメントの生成AI×GAS活用研修事例 - IT・プログラミングを知って学べるコネクトメディア 【製造業のDX人材育成事例】デジタル人材の即戦力化を実現する、日本ガイシ株式会社の異動者向オンボーディング研修 - ITやプログラミングを知って学べるコネクトメディア フューチャーアーキテクト株式会社が実現した新入社員向けIT研修プログラムでタスクフォース制度が主体的な学びと成長を生み出す - IT・プログラミングを知って学べるコネクトメディア コードキャンプDX人材育成研修 - IT・プログラミングを知って学べるコネクトメディア コードキャンプIT・プログラミング研修事例/【IT新入社員研修】オンラインとオフラインの最適バランスを実現したFutureOneの導入事例 - IT・プログラミングを知って学べるコネクトメディア コードキャンプIT・プログラミング研修事例/【新入社員研修】柔軟なハイブリッド型Java研修で実現した新卒20名の成長と成果|サークレイス株式会社 - ITやプログラミングを知って学べるコネクトメディア コードキャンプIT・プログラミング研修事例/現場により近いところにデジタルを根付かせるDX基礎講座研修|株式会社ブリヂストン - ITやプログラミングを知って学べるコネクトメディア コードキャンプIT・プログラミング研修事例/業務の効率化・DX推進に向けたIT人材育成への第一歩|株式会社カナエ - ITやプログラミングを知って学べるコネクトメディア 企業・法人向けのIT・プログラミング研修 - ITやプログラミングを知って学べるコネクトメディア

新着記事

対象者別で探す

子供(小学生・中学生・高校生)向け
プログラミング教室検索する

子供(小学生・中学生・高校生)がロボットやプログラミング言語を学ぶことができるオフラインからオンラインスクールを検索、比較することが可能です。

子供(小学生・中学生・高校生)
プログラミング教室検索する

ITやプログラムなどの
最新情報を検索する

日々、新しいITやプログラミング言語の情報が流れていきますが、特定の情報を時系列でニュースやコラムを確認することができます。

ITやプログラムなどの
最新情報を検索する