はじめに / 対象と前提
Claude API を使ったアプリで「リクエストを投げてみたらコンテキスト超過で 400 が返ってきた」「月末に請求を見てトークン消費に驚いた」という経験がある人向けの記事。送信前に正確なトークン数を知る手段として、公式の Token Counting API(POST /v1/messages/count_tokens)を実装する手順と、自分が実際にハマった 3 点をまとめる。
- 想定読者:Claude API(Messages API)を Python から叩いたことがある人
- 環境:Python 3.13 / anthropic SDK 1.x / モデルは
claude-opus-5 - tiktoken で代用しようとしている人ほど読んでほしい(後述するがズレる)
TL;DR
-
client.messages.count_tokens()に本番リクエストと同じペイロードを渡すと、課金ゼロでinput_tokensの見積もりが返る - ただし返り値は「推定値」。実際の課金トークンと数トークン〜数十トークンずれることがある前提で使う
-
messagesだけ渡してsystemとtoolsを渡し忘れると大幅に過小カウントになる(これが一番踏みやすい)
手順 / 動かし方
1. 最小構成で数える
エンドポイントは Messages API とほぼ同じ形のペイロードを受け取る。違いは max_tokens が不要なことと、推論が走らない(=無料)こと。
from anthropic import Anthropic
client = Anthropic() # ANTHROPIC_API_KEY を環境変数から読む
resp = client.messages.count_tokens(
model="claude-opus-5",
messages=[{"role": "user", "content": "この文章のトークン数を数えたい"}],
)
print(resp.input_tokens)
実行結果:
21
トークン数はモデル依存なので、model には実際に推論で使うモデル ID を渡すこと。別モデルの数字を流用すると平気で 1〜3 割ずれる。
2. 本番と同じペイロードで数える(system / tools 込み)
実際のアプリでは system プロンプトやツール定義が付く。数えるときも同じものを全部渡す。
tools = [{
"name": "get_weather",
"description": "指定した都市の現在の天気を返す",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
}]
resp = client.messages.count_tokens(
model="claude-opus-5",
system="あなたは天気案内アシスタントです。",
tools=tools,
messages=[{"role": "user", "content": "大阪の天気は?"}],
)
print(resp.input_tokens) # tools の JSON スキーマ分も含んだ数字が返る
3. 送信前ガードとして組み込む
自分は「コンテキスト上限の 9 割を超えたら古い履歴を落とす」というガードに使っている。
MAX_CONTEXT = 1_000_000 # claude-opus-5 の入力上限
SAFETY_RATIO = 0.9
def fits(messages, system, tools) -> bool:
n = client.messages.count_tokens(
model="claude-opus-5",
system=system, tools=tools, messages=messages,
).input_tokens
return n < MAX_CONTEXT * SAFETY_RATIO
会話履歴を送る直前にこれを通し、False なら先頭側の古いターンを削って再チェックする、という単純なループで「いきなり 400」は消えた。
ハマりどころ
その1:system と tools を渡し忘れて過小カウント
最初に踏んだのがこれ。messages だけ数えて「まだ余裕がある」と判断していたら、本番リクエストで想定より多く課金されていた。原因は単純で、本番には長い system プロンプトとツール定義 5 個分の JSON スキーマが付いていたのに、カウント時には渡していなかったから。ツール定義はスキーマ全体がトークン化されるので、ツールが多いアプリだと数千トークン規模で見積もりが狂う。
回避策:リクエスト組み立て関数を 1 つにまとめ、count_tokens と messages.create が同じ dict を共有する構造にする。「数える用」と「送る用」でペイロードを別々に組むと必ずいつか乖離する。
その2:課金明細の input_tokens と完全一致しない
count_tokens の返り値は公式に「推定値(estimate)」で、実際のレスポンスの usage.input_tokens と数トークンずれることがある。自分の観測ではズレはごく小さいが、ゼロではない。「見積もり == 課金額」を前提にした厳密なコスト計算やテストのアサーションを書くと、たまに落ちる不安定なテストになる。
回避策:見積もりは閾値判定にだけ使い、コスト集計はレスポンスの usage(実測値)から取る。テストで比較するなら完全一致ではなく誤差許容(±数十トークン)で書く。
その3:無料だがレート制限は別枠で存在する
count_tokens は課金されないが、リクエスト数のレート制限は Messages API とは別枠で存在する。自分は履歴管理のループで 1 ターンごとに何度も呼ぶ実装にしていたら、推論本体より先にカウント側が 429 を返してきた。エラーはこう返る:
anthropic.RateLimitError: Error code: 429 - {'type': 'error',
'error': {'type': 'rate_limit_error', 'message': 'Number of request tokens has exceeded your per-minute rate limit'}}
回避策:毎ターン全履歴を数え直さない。前回のカウント結果に差分(新しいターン分だけを単独で数えた値)を足し込むキャッシュ方式にすると、呼び出し回数がターンあたり 1 回で済む。SDK は 429 を自動リトライ(既定 2 回)するが、それに頼ると今度はレイテンシが悪化する。
背景・補足:tiktoken で代用してはいけない
検索すると「tiktoken でざっくり数える」記事が出てくるが、tiktoken は OpenAI のトークナイザで、Claude とは分かち書きが別物。典型的な英文で 15〜20% 程度の過小カウント、コードや日本語ではさらに大きくずれる。また Claude 同士でもモデル世代でトークナイザが変わることがあり(旧世代→現行世代で同じテキストが約 1〜1.35 倍になるケースを確認)、「昔測った値」を新モデルにそのまま使うのも危険。数えたいモデルの ID で count_tokens を叩くのが唯一正確な方法。
まとめ
-
count_tokensは無料でリクエスト前のトークン数見積もりができる。max_tokens不要以外は本番と同じペイロードを渡す -
system/toolsの渡し忘れが最頻の事故。ペイロード組み立てを一本化して防ぐ - 返り値は推定値。閾値判定に使い、課金集計は
usageの実測値で - 無料でもレート制限は別枠。差分カウントで呼び出し回数を減らす
- tiktoken での代用は不正確。モデル ID を指定して公式 API で数える