Claude APIのthinking_tokensで思考トークンを切り分ける
usage.output_tokens_details.thinking_tokensは、課金対象のoutput_tokensのうち内部推論に使われた分を返す内数です。要約表示でも生の量を返す点、ずれ方、集計コードを扱います。
usage.output_tokens_details.thinking_tokensは、レスポンスのoutput_tokensのうち、モデルが内部推論に使った分を返すフィールドです。課金の正はoutput_tokensのままで、thinking_tokensはその内訳を見るための読み取り専用の数字にあたります。思考にどれだけ払ったかを、レスポンスだけで切り分けられます。
thinking_tokensは何の内数か
output_tokensは思考と本文を合わせた出力トークンの総数で、請求の根拠もこの値です。output_tokens_detailsはその分解で、現在はthinking_tokensだけが入っています。
usageの3つの数字の関係
output_tokens
総数
課金の正。思考と本文を含む
thinking_tokens
内数
内部推論に使った分。output_tokens以下
差分
近似
output_tokens - thinking_tokens が非推論分
thinking_tokensには、思考ブロックの区切りトークンも含まれます。値はoutput_tokensを超えないと定義されています。引き算した残りは、本文やツール呼び出しなど推論以外の出力の近似です。「近似」と書かれているのは、次の節で触れる数え方の事情があるためです。
output_tokens_detailsの型は「オブジェクトまたはnull」です。フィールドが無いレスポンスを想定して、読む側でnullを許容しておきます。
レスポンスのどこを見るか
非ストリーミングでは、usageの直下に並びます。次は形を示す例で、数値は説明用の仮の値です。
{
"usage": {
"input_tokens": 2095,
"output_tokens": 4200,
"output_tokens_details": {
"thinking_tokens": 3100
}
}
}この例なら、4,200トークンの出力のうち3,100トークンが推論で、残り1,100トークンが本文などの推論以外です。思考が出力の約74%を占めている、と一目で分かります。
APIリファレンスのレスポンス例では、output_tokensが503でthinking_tokensが0の形で載っています。思考を使わなかった呼び出しでは、thinking_tokensの既定値は0です。
要約表示でも生の推論量が返る
ここが一番誤解されやすい点です。display: "summarized"を指定すると、レスポンスのthinkingブロックには思考の要約が入ります。thinking_tokensが数えるのは、この要約ではなく、モデルが実際に生成した生の推論です。
思考の課金も同じ考え方です。要約のトークンではなく、元のリクエストで生成された思考トークン全体が課金されます。レスポンスに見える文章を数えても、請求されるoutput_tokensとは一致しません。
同じ呼び出しで数が合わない理由
レスポンスの thinking テキスト
要約された思考です。display: "omitted"なら空文字で、数えるものがありません。
thinking_tokens
要約前の生の推論の量です。表示の設定を変えても、同じ推論なら同じ規模の数字になります。
displayの既定値が"omitted"のモデルでは、thinkingが空のままthinking_tokensだけが大きい、という状態が普通に起きます。空の理由はthinkingのdisplay: summarizedとはで扱っています。思考が空でも課金は発生しており、その大きさを確かめる手段がthinking_tokensです。
数トークンずれる理由
thinking_tokensは、生の推論テキストを再トークナイズして数えた値です。そのため、モデルが実際に生成したトークン数と、小さな差が出ることがあります。
この差は数トークン規模のものとして説明されています。1リクエスト単位の厳密な照合には向きませんが、思考の比率を見る監視や、budget_tokensの調整の判断には支障のない精度です。請求額と突き合わせるときは、常にoutput_tokensを使います。
ストリーミングでは最後のmessage_deltaだけに載る
ストリーミング時は、この内訳が最終のmessage_deltaイベントにだけ現れます。途中のイベントでは取れません。
message_deltaのusageは累積値です。イベントごとに足し算すると二重に数えることになるので、最後の1件を採ります。
import anthropic
client = anthropic.Anthropic()
usage = None
stream = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=16000,
thinking={"type": "adaptive", "display": "summarized"},
messages=[{"role": "user", "content": "素数判定のアルゴリズムを比較して"}],
stream=True,
)
for event in stream:
if event.type == "message_delta":
usage = event.usage # 累積値。最後の1件が確定値ループを抜けた時点のusageが、最終のmessage_deltaのものです。event.usageにoutput_tokens_detailsが付いているかは、次の集計関数が吸収します。
集計用の関数を1つ作る
非ストリーミングのresponse.usageも、ストリーミングのevent.usageも、同じ関数で分解できるようにします。
def split_output(usage) -> dict:
total = usage.output_tokens
details = getattr(usage, "output_tokens_details", None)
thinking = getattr(details, "thinking_tokens", 0) if details else 0
thinking = min(thinking, total) # 内数なので総数を超えさせない
return {
"output_tokens": total,
"thinking_tokens": thinking,
"other_tokens": total - thinking, # 非推論分の近似
"thinking_ratio": thinking / total if total else 0.0,
}ポイントは3つあります。output_tokens_detailsが無い・nullの場合は0として扱うこと。minで総数を超えないよう丸めること。差分はother_tokensのように「近似」と分かる名前にすることです。
ログに書くときは、output_tokensの列とthinking_tokensの列を別々に持たせます。比率は後から計算できるので、生の2列を残しておきます。
row = split_output(usage)
print(
f"model={model} out={row['output_tokens']} "
f"think={row['thinking_tokens']} ratio={row['thinking_ratio']:.0%}"
)数字をどう使うか
thinking_tokensが使える場面は、思考にかけた量を調整する判断です。
budget_tokensを設定している呼び出しでは、予算に対して実際のthinking_tokensがどこまで使われたかを見る。budget_tokensは厳密な上限ではなく目標値で、Claudeが予算を使い切る前に推論をやめることもあるmax_tokensで出力が途切れる場合、思考もmax_tokensに含まれるため、thinking_tokensが大きければ本文に回る枠が削られている。max_tokensが思考で食われているかどうかは、この数字で判断できるeffortを変えたのに思考量が変わらない、という疑いは、変更前後のthinking_tokensを並べると数字で確かめられる。原因の切り分けはeffortを変えてもthinkingの深さが変わらないときにまとめている
budget_tokensの制約は、最小1,024トークンでmax_tokens未満です。小さいタスクは下限付近から始め、複雑なタスクは16,000トークン以上から始めて調整する、という目安が示されています。調整のたびにthinking_tokensの実績を見れば、予算が余っているのか、足りずに頭打ちなのかが分かります。思考が32,000トークンを超える用途は、ネットワーク上の問題を避けるためバッチ処理が勧められています。
ツール呼び出しを挟む長いやり取りでは、レスポンスのusage.iterationsに、サンプリング単位のエントリが並びます。その各エントリにもoutput_tokens_detailsが定義されています。ターン全体の合計は上位のusageで、どの反復で思考が膨らんだかはiterations側で追う、という使い分けになります。
iterationsを集計に使うときは、エントリのtypeを見ます。messageのエントリはモデルのサンプリング1回分で、サーバー側のツール実行ループの各ターンがここに並びます。compactionのエントリは、コンテキストを要約する処理そのものの使用量です。この分は上位のusageのトークン数に含まれません。上位のusageとiterationsの合計が一致しなくても、compactionがあればその分の差で説明がつきます。
反復ごとのthinking_tokensを時系列で並べると、ツールの結果を受けた後に推論が増える箇所が見えます。ターン全体の値だけでは平均化されて見えない動きなので、思考が重いと感じる長いエージェントの調査では、この粒度まで下りる価値があります。
curlで1回だけ確かめる
集計コードを組む前に、手元で形を見ておくと安心です。次はjqでusageだけを取り出す例です。
curl -s https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "claude-sonnet-5-5",
"max_tokens": 16000,
"thinking": {"type": "adaptive", "display": "omitted"},
"messages": [{"role": "user", "content": "17の3乗は?"}]
}' | jq '{usage, thinking_text: [.content[] | select(.type=="thinking") | .thinking]}'display: "omitted"にしているので、thinking_textは空文字の配列になり、usage.output_tokens_details.thinking_tokensだけに推論量が現れます。要約を返す設定に変えて同じ質問を投げても、thinking_tokensは同じ規模のままです。表示の設定と推論量が別物だと、自分の目で確かめられます。
thinkingのパラメータを付けない呼び出しでthinking_tokensが0になるのは、Opus 4.8・4.7・4.6やSonnet 4.6のように思考が既定でオフのモデルです。Sonnet 5.5・Opus 5.5・Haiku 5.5など思考が既定でオンのモデルは、パラメータを省いても思考が走るため、この例のclaude-sonnet-5-5でも0にはなりません。思考を使わなかった呼び出しでは0で、0という値自体はフィールドの既定値です。
症状から数字の読み方を引く
output_tokensとthinking_tokensの組み合わせから、次に調べる場所が決まります。
| 見えている状態 | 数字の読み方 | 次に見る場所 |
|---|---|---|
thinkingが空、thinking_tokensが大きい | 数字の読み方推論は走っており、表示だけ省かれている | 次に見る場所displayの設定 |
thinking_tokensが0、output_tokensは大きい | 数字の読み方本文など推論以外が長い | 次に見る場所max_tokensと出力の長さ |
stop_reasonがmax_tokens、thinking_tokensが大半 | 数字の読み方思考が出力枠を使い切り、本文が途切れた | 次に見る場所max_tokensの引き上げ、effort |
変更前後でthinking_tokensがほぼ同じ | 数字の読み方設定変更が思考量に効いていない | 次に見る場所effort・budget_tokensの指定経路 |
請求とoutput_tokensの合計が合わない | 数字の読み方thinking_tokensを別料金として足していないか | 次に見る場所集計式 |
2行目の「thinking_tokensが0」は、思考がオフだったのか、思考が短く済んだのかを区別しません。区別したいときは、リクエストに載せたthinkingの設定をログに一緒に残します。
日次で思考の比率を集計する
1回ごとの数字は揺れるので、運用では集計して傾向を見ます。先ほどのsplit_outputの結果を貯める想定で、モデルごとの比率を出す関数は次のとおりです。
from collections import defaultdict
def thinking_share(rows: list[dict]) -> dict[str, float]:
"""rows: model と split_output の結果を持つ辞書のリスト"""
total = defaultdict(int)
think = defaultdict(int)
for r in rows:
total[r["model"]] += r["output_tokens"]
think[r["model"]] += r["thinking_tokens"]
return {m: think[m] / total[m] for m in total if total[m]}比率は「トークンの合計どうしの割り算」で出します。呼び出しごとの比率を平均すると、短い呼び出しの影響が過大になります。個々の呼び出しが数トークンずれる件は、合計に対しては無視できる大きさです。
effortやbudget_tokensを変えた日の前後でこの比率を比べれば、変更が思考量に効いたかどうかを、感覚ではなく数字で判断できます。MAX_THINKING_TOKENSで予算を固定している場合の考え方はMAX_THINKING_TOKENSの解説にあります。
取り違えやすい点
落とし穴
thinking_tokensを足して請求額を出してしまう
thinking_tokensはoutput_tokensの内数なので、両者を足すと思考を二重に数えます。請求の計算はoutput_tokensだけで行い、thinking_tokensは内訳の表示に限ります。
次の3点も、集計でつまずきやすい箇所です。
- 要約テキストの長さから
thinking_tokensを推定しない。要約と生の推論は別物で、display: "omitted"では要約が空になる - ストリーミングの途中イベントに内訳を期待しない。最終の
message_deltaを待つ output_tokens - thinking_tokensを厳密な本文トークン数として請求と突き合わせない。再トークナイズに由来する差が含まれる
thinkingの設定を変えるとプロンプトキャッシュの効き方も変わります。思考の量とあわせて見るなら、thinking設定とキャッシュヒット率の記事が手がかりになります。
まとめ
thinking_tokensは、課金の正であるoutput_tokensの内訳を、表示設定に関係なく生の推論量で返す数字です。請求はoutput_tokens、思考の比率の監視はthinking_tokensと役割を分ければ、二重計上は起きません。ストリーミングでは最後のmessage_deltaだけを読み、nullと総数超えをコード側で丸めておけば、集計関数は十数行で済みます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。
関連する記事
Anthropic をもっと見る →Invalid signature in thinking blockの原因と直し方 — 会話の前提が変わる400
effort cannot change when thinking is disabledの400エラーと直し方
adaptive thinking blockが出ない理由 — 一部のターンで起きる正常な挙動
thinkingのdisplay: summarizedとは — フィールドが空文字になる理由
thinking blocks cannot be modifiedエラー(400)の原因と直し方
Claude APIのtool useエージェントループを5段階で自作する — is_errorまで