OpenTelemetryの入力トークンをGenAI規約に合わせるClaude Codeの換算
Claude Codeのinput_tokensはキャッシュの読み書きを含みません。GenAI規約のgen_ai.usage.input_tokensに合わせる足し算と、キャッシュ属性の対応名を示します。
Claude Codeが出力するinput_tokensは、キャッシュから読んだトークンも、キャッシュに書いたトークンも含みません。OpenTelemetryのGenAI規約が想定するgen_ai.usage.input_tokensは、その両方を含む合計です。同じ「入力トークン」でも指す範囲が違うので、そのまま並べると数字が合いません。
規約に合わせた値は、input_tokensとcache_read_tokensとcache_creation_tokensの3つを足せば得られます。この記事は、その足し算をどこに入れるかと、キャッシュ属性の対応名を示します。利用量とコストの全体を可視化する設定はClaude CodeのOpenTelemetryで利用量とコストを可視化が扱い、ここは入力トークンの換算に絞ります。
なぜClaude Codeの入力トークンは小さく見えるのか
Claude Codeは、APIレスポンスのusageブロックにある入力トークン数をそのまま出力します。そのためキャッシュの読み書きは別枠に分かれ、input_tokensには入りません。対象は次の2つです。
claude_code.llm_requestスパンとapi_requestイベントのinput_tokensclaude_code.token.usageメトリクスの"input"タイプ
この分け方はAPI側の仕様に由来します。プロンプトキャッシュのドキュメントでは、input_tokensを「キャッシュから読まれず、キャッシュ作成にも使われなかったトークン」、つまり最後のキャッシュブレークポイントより後ろの部分と説明しています。リクエスト全体の入力量は次の式です。
total_input_tokens = cache_read_input_tokens + cache_creation_input_tokens + input_tokensキャッシュがよく効いているセッションほど、input_tokensは全体のごく一部になります。ダッシュボードでinput_tokensだけを見ると、実際に送った量を大きく過小評価します。キャッシュが効く仕組みはプロンプトキャッシュと利用上限の関係にまとめています。
規約に合わせるには3種類を足す
Claude Codeはgen_ai.usage.*属性を付けません。GenAI規約側ではgen_ai.usage.input_tokensにキャッシュの読み書きを含めるべきとされているため、規約どおりの値は自分で作ります。足す対象は、データの取り出し口によって名前が変わります。
| 取り出し口 | 足す項目 |
|---|---|
スパンまたはapi_requestイベント | 足す項目input_tokens + cache_read_tokens + cache_creation_tokens |
claude_code.token.usageメトリクス | 足す項目"input" + "cacheRead" + "cacheCreation" の各タイプ |
メトリクスはtype属性で系列が分かれており、名前はcacheRead・cacheCreationのようにキャメルケースです。スパンやイベントのcache_read_tokens・cache_creation_tokensとは表記が違う点に注意してください。
具体例で確かめる
1リクエストの値が次のとおりだったとします(数値は説明用の例です)。
| 項目 | 値 |
|---|---|
input_tokens | 値120 |
cache_read_tokens | 値48,000 |
cache_creation_tokens | 値3,000 |
規約に合わせた入力トークンは、120 + 48,000 + 3,000 = 51,120です。input_tokensだけを見ると120なので、約426分の1に見えます。ここからcache_read_tokensを合計で割ると、そのリクエストの読み取り比率も出せます。48,000 ÷ 51,120で約93.9%です。
同じ数値を、書き込みを5分キャッシュ、読み取りを0.1倍の単価として基本入力トークン換算すると、120 + 48,000 × 0.1 + 3,000 × 1.25 = 8,670です。トークン数の合計51,120とは約5.9倍ずれます。入力量の比較には合算値、費用の見積もりには単価別の計算と、使う値を分ける理由がここにあります。
キャッシュ属性の対応名
規約は合計とは別に、キャッシュの読み取りと書き込みの専用属性も定めています。Claude Codeの属性名との対応は次のとおりです。
| Claude Codeの属性 | GenAI規約の属性 |
|---|---|
cache_read_tokens | GenAI規約の属性gen_ai.usage.cache_read.input_tokens |
cache_creation_tokens | GenAI規約の属性gen_ai.usage.cache_write.input_tokens |
書き込み側の名前には揺れがあります。規約の古い版では、キャッシュ書き込みの属性がgen_ai.usage.cache_creation.input_tokensでした。現行のcache_writeと旧名のcache_creationが混在するので、送り先のバックエンドがどちらの名前を期待するかに合わせます。Claude Code自身の属性名はcache_creation_tokensのままで、旧名とも新名とも一致しません。
すでに付いているGenAI属性と、付いていない属性
gen_ai.usage.*は付きませんが、GenAI規約の別の属性はスパンやイベントに付いています。
| 付く場所 | 属性 | 中身 |
|---|---|---|
claude_code.llm_requestスパン | 属性gen_ai.system | 中身常にanthropic |
| 同上 | 属性gen_ai.request.model | 中身modelと同じ値 |
| 同上 | 属性gen_ai.response.id | 中身request_idと同じ値 |
| 同上 | 属性gen_ai.response.finish_reasons | 中身stop_reasonを文字列配列にしたもの |
| ツール実行のスパン | 属性gen_ai.tool.call.id | 中身tool_use_idと同じ値 |
リトライごとに、gen_ai.request.attemptというスパンイベントも記録されます。つまりモデル名や終了理由はGenAI規約の名前で取れるのに、トークン数だけが規約の外にあります。バックエンドがGenAI用のビューをgen_ai.usage.*から作る場合、このビューだけトークン列が空になります。
換算をどこで行うか
換算の置き場所は、集計ツールの能力に合わせて3通りから選べます。
換算を入れる場所の候補
クエリ時に足す
PromQLやSQLで3種類を合算します。収集側を変えずに済みますが、ダッシュボードごとに同じ式を書きます。
コレクターで属性を足す
OpenTelemetry Collectorの変換処理で、スパンに
gen_ai.usage.input_tokensを計算して付けます。バックエンドは規約どおりの属性を受け取れます。バックエンドの派生フィールド
バックエンドが算出フィールドを持つなら、そこで3つの属性を足します。
クエリ時に足す例
メトリクスをPrometheus形式で受ける場合、typeラベルで3種類を選んで合計します。メトリクス名はエクスポーターの設定で変わるため、次は形だけを示した例です。
sum(
increase(claude_code_token_usage_total{type=~"input|cacheRead|cacheCreation"}[1h])
)typeを正規表現で3つに絞る点が要点です。outputは規約では別の属性なので、合計に入れません。
コレクターで属性を足す例
スパンに合計を付ける場合は、Collectorのtransformプロセッサで3つの属性を足します。以下は書き方の一例です。llm_requestスパンに限定し、3つの属性が揃っているときだけ計算するよう存在確認も入れています。
processors:
transform/genai:
trace_statements:
- context: span
statements:
- >
set(attributes["gen_ai.usage.input_tokens"],
attributes["input_tokens"]
+ attributes["cache_read_tokens"]
+ attributes["cache_creation_tokens"])
where attributes["span.type"] == "llm_request"
and attributes["input_tokens"] != nil
and attributes["cache_read_tokens"] != nil
and attributes["cache_creation_tokens"] != nil
- set(attributes["gen_ai.usage.cache_read.input_tokens"],
attributes["cache_read_tokens"])
where attributes["span.type"] == "llm_request"
- set(attributes["gen_ai.usage.cache_write.input_tokens"],
attributes["cache_creation_tokens"])
where attributes["span.type"] == "llm_request"絞り込みにはspan.type属性を使っています。どのスパンも名前に対応するspan.typeを持つためです。値の綴りはconsoleエクスポーターで確かめてから条件を書いてください。ツール実行やフックのスパンにはトークン属性が載らないので、条件なしで足すと欠損値の計算になります。
api_requestイベントに付ける例
ベータ機能に頼らず、ログとして届くapi_requestイベントに合計を付ける方法もあります。イベントにはinput_tokens・cache_read_tokens・cache_creation_tokensが同じ名前の属性で載っています。Collectorではlog_statementsに書き、コンテキストはlogにします。
processors:
transform/genai_logs:
log_statements:
- context: log
statements:
- >
set(attributes["gen_ai.usage.input_tokens"],
attributes["input_tokens"]
+ attributes["cache_read_tokens"]
+ attributes["cache_creation_tokens"])
where attributes["event.name"] == "api_request"
and attributes["input_tokens"] != nil
and attributes["cache_read_tokens"] != nil
and attributes["cache_creation_tokens"] != nil絞り込みはevent.nameがapi_requestのイベントだけにします。api_errorなど他のイベントにはトークン属性が載らないためです。属性の型が文字列で届く経路では足し算が連結になることがあるので、送り先に着いた値を一度目視してから本番に回してください。
取り出し口ごとの落とし穴
メトリクス経路を使う最小設定
claude_code.token.usageを受けるには、テレメトリを有効にしてメトリクスのエクスポーターを選びます。イベントも使うならOTEL_LOGS_EXPORTERも指定します。
export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_METRICS_EXPORTER=prometheus # otlp / console / none も選べる
export OTEL_LOGS_EXPORTER=otlp # api_requestイベントを使う場合
export OTEL_METRIC_EXPORT_INTERVAL=10000 # 既定は60000msメトリクスの出力間隔は既定で60秒、ログは5秒です。動作確認では間隔を短くすると、換算した値が表示されるまでの待ちが減ります。Prometheusに取り込んだあとは、typeラベルにinput・output・cacheRead・cacheCreationの4値が並んでいるかを確かめてから、先のクエリを使います。
スパンはベータ機能で、既定では出ない
トレースは既定で無効です。スパンから換算するには、CLAUDE_CODE_ENABLE_TELEMETRY=1とCLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1の両方を設定し、OTEL_TRACES_EXPORTERで送り先を選びます。ENABLE_ENHANCED_TELEMETRY_BETAという名前も受け付けられます。トークン数だけが目的なら、ベータ機能に頼らずメトリクスとapi_requestイベントで足りる場合もあります。
export CLAUDE_CODE_ENABLE_TELEMETRY=1
export CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1
export OTEL_TRACES_EXPORTER=console # まずconsoleで属性名を目視するconsoleエクスポーターに出すと、input_tokens・cache_read_tokens・cache_creation_tokensがスパンに載っていることを、コレクターを立てる前に確かめられます。
メトリクスと属性で表記が違う
先に触れたとおり、メトリクスのtype値はcacheRead・cacheCreation、スパンとイベントの属性はcache_read_tokens・cache_creation_tokensです。メトリクス側の式を属性名で書くと、系列が0件になります。
合計に入れるのは入力側だけ
output_tokensは規約でも別の項目です。入力の合算に混ぜません。コストの確認には、トークン数ではなくメトリクスやイベントに付くコスト項目を使います。コスト側の見方はClaude CodeのOpenTelemetryで利用量とコストを可視化で扱っています。
換算した値で何が確かめられるか
規約に沿った合計入力とキャッシュ読み取りが揃うと、リクエスト単位で読み取り比率が出せます。キャッシュが急に効かなくなった場合、cache_read_tokensが減り、cache_creation_tokensが増えるという形で現れます。有効期間の短縮で起きた事例はClaude CodeのキャッシュTTLが5分に短縮された問題に、手元の端末で比率を見る方法はstatuslineでprompt_cacheを表示するにあります。
テレメトリ側で見る利点は、組織全体を同じ物差しで並べられることです。input_tokensだけで比較すると、キャッシュがよく効く利用者ほど小さく見えます。合算してから比べれば、利用者ごとの実際の入力量が揃います。
まとめ
Claude Codeのinput_tokensは、APIのusageブロックと同じ意味で、キャッシュの読み書きを含みません。GenAI規約のgen_ai.usage.input_tokensに合わせるには、スパンやイベントならinput_tokens・cache_read_tokens・cache_creation_tokens、メトリクスならinput・cacheRead・cacheCreationを足します。キャッシュ書き込みの属性は新名がcache_write、旧名がcache_creationなので、送り先に合わせて選びます。
よくある質問
将来、Claude Codeがgen_ai.usage.*を付けるようになったら二重計上になりますか
属性が追加されても、既存のinput_tokensの意味が変わるとは限りません。コレクターでgen_ai.usage.input_tokensを上書きする設定は、属性の有無を確かめてから入れます。公式ドキュメントには、gen_ai.usage.*を付けないとだけ書かれています。
料金の計算にも、この合算値を使えますか
使えません。プロンプトキャッシュのドキュメントでは、5分キャッシュの書き込みが基本入力単価の1.25倍、1時間キャッシュの書き込みが2倍、読み取りが0.1倍とされています(読み取りは一部モデルで別の倍率です)。3種類を足した値に一律の単価を掛けると、この倍率の差がそのまま誤差になります。金額の確認には、api_requestイベントのcost_usdのようなコスト項目を使ってください。