エンジニアの思い立ったが吉日

このブログでは、「あ、これ面白い!」「明日から仕事で使えそう!」と感じたIT関連のニュースやサービスを、難しい言葉を使わずに分かりやすく紹介しています。ITに詳しくない方にも楽しんでもらえるような情報を発信していくので、ぜひ「継続的な情報収集」の場としてご活用ください。

Claude APIのトークン計算と実際の課金額が合わない問題と対策

「見積もりよりAPIコストが大幅に高い」「トークン数が予測できない」「キャッシュを使っているのにコストが減らない」「モデル別の料金計算が間違っていた」といった問題が多く報告されています。本記事では、Claudeのトークン計算の仕組みと課金額を正確に予測するための方法を解説します。

問題の症状

問題1:想定より課金が多い

月次の請求額が想定の2〜3倍になっており、原因がわからない。

問題2:トークン数の事前見積もりができない

プロンプトを送信する前にトークン数がわからないため、コスト予測ができない。

問題3:キャッシュ設定後もコストが変わらない

プロンプトキャッシングを設定したが、cache_read_input_tokensが0のまま。

問題4:モデル変更後に予期しない料金増加

高性能モデルに移行したら料金が数倍になった。


原因の解説

Claudeの課金構造

Claude APIの課金はトークン単位で計算されます。

課金対象のトークン:

トークン種別 説明 課金倍率(基準比)
input_tokens 通常の入力トークン 1.0x
cache_creation_input_tokens キャッシュ作成時の入力 1.25x(5分)/ 2.0x(1時間)
cache_read_input_tokens キャッシュから読み込んだ入力 0.1x(90%削減)
output_tokens 出力トークン 1.0x(入力の約3〜5倍の単価)

重要: 出力トークンは入力トークンより単価が高いモデルがほとんどです。


モデル別の料金(2026年3月時点)

モデル 入力(/MTok) 出力(/MTok) コンテキスト
Claude Opus 4.6 $5.00 $25.00 1M tokens
Claude Sonnet 4.6 $3.00 $15.00 1M tokens
Claude Haiku 4.5 $1.00 $5.00 200k tokens

バッチ処理(50%割引):

モデル 入力(/MTok) 出力(/MTok)
Claude Opus 4.6 $2.50 $12.50
Claude Sonnet 4.6 $1.50 $7.50
Claude Haiku 4.5 $0.50 $2.50

MTok = 100万トークン


コストが高くなりやすい原因

原因1:Extended Thinkingのトークンが見えていない

Extended Thinking有効時、思考トークンは入力・出力トークンとは別にカウントされます。Summarized Thinking(要約版思考)の場合、表示は短い要約ですが課金はフルの思考トークン数で行われます。

原因2:長い会話履歴の蓄積

マルチターン会話では、毎回すべての会話履歴を入力トークンとして送信します。会話が長くなるほど課金が増えます。

1ターン目:入力100トークン → 出力100トークン
2ターン目:入力300トークン(100+100+100) → 出力100トークン
3ターン目:入力600トークン(300+100+100) → 出力100トークン
...

原因3:画像のトークン数を過小評価

高解像度の画像は大量のトークンを消費します。

トークン計算式: tokens ≈ (width × height) / 750

例:
1000×1000px → 約1,333トークン
2000×2000px → 約5,333トークン
4000×4000px → 約21,333トークン(制限を超えて自動縮小)

原因4:JSONやコードブロックの出力が長い

Claudeにコードを生成させたり、JSONを出力させると出力トークンが多くなります。


解決策

ステップ1:Token Counting APIでリクエスト前にトークンを確認

Token Counting APIを使うと、APIリクエストを送信する前にトークン数を確認できます。

import anthropic

client = anthropic.Anthropic()

def count_tokens_before_request(
    model: str,
    system: str,
    messages: list,
    tools: list = None
) -> dict:
    """リクエスト前にトークン数を確認"""
    params = {
        "model": model,
        "system": system,
        "messages": messages
    }
    if tools:
        params["tools"] = tools

    token_count = client.messages.count_tokens(**params)

    return {
        "input_tokens": token_count.input_tokens,
        "estimated_cost_usd": calculate_cost(model, token_count.input_tokens, 0)
    }


def calculate_cost(model: str, input_tokens: int, output_tokens: int) -> float:
    """トークン数からコストを計算(USD)"""
    # モデル別の料金($/MTok)
    pricing = {
        "claude-opus-4-6": {"input": 5.0, "output": 25.0},
        "claude-sonnet-4-6": {"input": 3.0, "output": 15.0},
        "claude-haiku-4-5-20251001": {"input": 1.0, "output": 5.0},
    }

    model_price = pricing.get(model, {"input": 3.0, "output": 15.0})

    input_cost = (input_tokens / 1_000_000) * model_price["input"]
    output_cost = (output_tokens / 1_000_000) * model_price["output"]

    return input_cost + output_cost


# 使用例
system_prompt = "あなたは優秀なPythonエンジニアです。"
messages = [
    {"role": "user", "content": "FastAPIでRESTful APIを作成するサンプルコードを書いてください"}
]

count = count_tokens_before_request(
    model="claude-sonnet-4-6",
    system=system_prompt,
    messages=messages
)

print(f"予測入力トークン: {count['input_tokens']:,}")
print(f"予測コスト(入力のみ): ${count['estimated_cost_usd']:.4f}")

ステップ2:実際のAPI使用量を追跡する

import anthropic
from dataclasses import dataclass, field
from typing import List

client = anthropic.Anthropic()

@dataclass
class TokenTracker:
    """API使用量トラッカー"""
    input_tokens: int = 0
    output_tokens: int = 0
    cache_creation_tokens: int = 0
    cache_read_tokens: int = 0
    requests: int = 0

    def add(self, usage):
        self.input_tokens += usage.input_tokens
        self.output_tokens += usage.output_tokens
        self.cache_creation_tokens += getattr(usage, 'cache_creation_input_tokens', 0)
        self.cache_read_tokens += getattr(usage, 'cache_read_input_tokens', 0)
        self.requests += 1

    def calculate_cost(self, model: str = "claude-sonnet-4-6") -> dict:
        pricing = {
            "claude-opus-4-6": {"input": 5.0, "cache_write": 6.25, "cache_read": 0.5, "output": 25.0},
            "claude-sonnet-4-6": {"input": 3.0, "cache_write": 3.75, "cache_read": 0.3, "output": 15.0},
            "claude-haiku-4-5-20251001": {"input": 1.0, "cache_write": 1.25, "cache_read": 0.1, "output": 5.0},
        }

        p = pricing.get(model, pricing["claude-sonnet-4-6"])
        M = 1_000_000

        input_cost = (self.input_tokens / M) * p["input"]
        cache_write_cost = (self.cache_creation_tokens / M) * p["cache_write"]
        cache_read_cost = (self.cache_read_tokens / M) * p["cache_read"]
        output_cost = (self.output_tokens / M) * p["output"]
        total = input_cost + cache_write_cost + cache_read_cost + output_cost

        # キャッシュ節約額(キャッシュなしで全部入力した場合との比較)
        savings = (self.cache_read_tokens / M) * (p["input"] - p["cache_read"])

        return {
            "input_cost": input_cost,
            "cache_write_cost": cache_write_cost,
            "cache_read_cost": cache_read_cost,
            "output_cost": output_cost,
            "total_cost": total,
            "cache_savings": savings
        }

    def summary(self, model: str = "claude-sonnet-4-6"):
        costs = self.calculate_cost(model)
        print(f"=== トークン使用量サマリー ===")
        print(f"リクエスト数: {self.requests:,}")
        print(f"入力トークン: {self.input_tokens:,}")
        print(f"出力トークン: {self.output_tokens:,}")
        print(f"キャッシュ書き込み: {self.cache_creation_tokens:,}")
        print(f"キャッシュ読み込み: {self.cache_read_tokens:,}")
        print(f"\n=== コスト内訳 ===")
        print(f"入力コスト: ${costs['input_cost']:.4f}")
        print(f"キャッシュ書き込みコスト: ${costs['cache_write_cost']:.4f}")
        print(f"キャッシュ読み込みコスト: ${costs['cache_read_cost']:.4f}")
        print(f"出力コスト: ${costs['output_cost']:.4f}")
        print(f"合計: ${costs['total_cost']:.4f}")
        print(f"キャッシュによる節約: ${costs['cache_savings']:.4f}")


# 使用例
tracker = TokenTracker()

for prompt in prompts:
    response = client.messages.create(...)
    tracker.add(response.usage)

tracker.summary("claude-sonnet-4-6")

ステップ3:コスト削減のための最適化

# 1. 長い会話履歴の圧縮
def compress_conversation(messages: list, max_context_tokens: int = 50000) -> list:
    """会話が長くなりすぎたら古い部分を要約"""
    total_tokens = sum(estimate_tokens(m) for m in messages)

    if total_tokens > max_context_tokens:
        # 古いメッセージを要約
        old_messages = messages[:-4]  # 最新4件以外
        summary_response = client.messages.create(
            model="claude-haiku-4-5-20251001",  # 安いモデルで要約
            max_tokens=500,
            messages=[
                {"role": "user", "content": f"以下の会話を簡潔に要約してください:\n{str(old_messages)}"}
            ]
        )
        summary = summary_response.content[0].text
        return [{"role": "user", "content": f"[会話履歴の要約]:{summary}"}] + messages[-4:]

    return messages

# 2. 出力トークンを制限する
response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=500,  # 必要最小限に設定(デフォルト値に任せない)
    messages=[...]
)

# 3. タスクに応じてモデルを使い分ける
def select_model(task_complexity: str) -> str:
    """タスクの複雑さに応じてモデルを選択"""
    models = {
        "simple": "claude-haiku-4-5-20251001",   # 単純な分類・要約
        "medium": "claude-sonnet-4-6",           # 一般的なタスク
        "complex": "claude-opus-4-6"             # 複雑な推論・コード生成
    }
    return models.get(task_complexity, "claude-sonnet-4-6")

コスト最適化のチェックリスト

最適化手法 削減効果 実装難易度
Batch API使用 50%削減 低(非同期でOKなら)
プロンプトキャッシング 最大90%削減(キャッシュ部分)
安いモデルを使う(Haiku) 70〜80%削減
出力トークンの最小化 10〜30%削減
会話履歴の圧縮 20〜50%削減
画像の最適化(リサイズ) 画像依存

参照URL

engineer-kichizitsu.net

engineer-kichizitsu.net

engineer-kichizitsu.net

engineer-kichizitsu.net

engineer-kichizitsu.net

当サイトは、アフィリエイト広告を使用しています。