0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude API の count_tokens でリクエスト前にトークン数を数える実装手順 — 課金額とズレる・レート制限が別枠・tools 分の加算漏れ、3つのハマりどころ【2026】

0
Posted at

はじめに / 対象と前提

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 だけ渡して systemtools を渡し忘れると大幅に過小カウントになる(これが一番踏みやすい)

手順 / 動かし方

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:systemtools を渡し忘れて過小カウント

最初に踏んだのがこれ。messages だけ数えて「まだ余裕がある」と判断していたら、本番リクエストで想定より多く課金されていた。原因は単純で、本番には長い system プロンプトとツール定義 5 個分の JSON スキーマが付いていたのに、カウント時には渡していなかったから。ツール定義はスキーマ全体がトークン化されるので、ツールが多いアプリだと数千トークン規模で見積もりが狂う。

回避策:リクエスト組み立て関数を 1 つにまとめ、count_tokensmessages.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 で数える
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?