Agent SDK Cost Trackingでトークン使用量を正確に集計する
Claude Agent SDKのtotal_cost_usdとmodelUsageの範囲・重複計算の落とし穴・サブエージェント分の扱いを実装コード付きで説明します。
Agent SDK Cost Trackingは何を測るものか
Agent SDKはquery()の各やり取りごとに、トークン使用量とコスト見積もりを返します。ただしこのtotal_cost_usdとcostUSDはクライアント側の見積もりであり、正式な請求データではありません。SDKはビルド時に同梱された価格表(またはmodelPricingテーブルが有効な場合はそちら)を使ってローカルで計算しているだけで、価格改定・インストール済みSDKが認識していないモデル・クライアントが再現できない請求ルールがあると、実際の請求額とずれます。
SDKがモデル化している請求ルールの1つがデータレジデンシー料金です。レスポンスの usage が inference_geo: "us" を報告すると、SDKはそのレスポンスのトークンのリスト価格を1.1倍します(Web検索などのリクエスト単位の追加料金は対象外)。この計算にはTypeScript Agent SDK v0.3.239以降、またはPython Agent SDK v0.2.144以降が必要です。
これらの値は開発時の目安や概算の予算管理には使えますが、正式な請求にはUsage and Cost APIかClaude ConsoleのUsageページを使う必要があります。これらの値だけを根拠にエンドユーザーへ課金したり、財務上の意思決定をしたりする用途には向きません。
Claude Codeをチームで運用する際の組織アナリティクスや支出上限の設定はClaude Codeのコスト管理で扱っています。本記事は、自分のアプリケーションがAgent SDKのAPIレスポンスからトークンとコストをどう集計するかに絞ります。
トークン使用量の3つのスコープ
SDKは使用量を「query()呼び出し」「ステップ」「セッション」の3つの単位で区切っています。
query()呼び出し: SDKのquery()関数を1回呼ぶこと。1回の呼び出しの中でClaudeが応答・ツール使用・結果取得・再応答を何段階も繰り返すことがあります。各呼び出しは最後に1つのresultメッセージを出します(例外はストリーミング入力モードで、1回のquery()呼び出しに複数のユーザーターンが乗り、ターンごとにresultメッセージが出ます)- ステップ:
query()呼び出し内の1回のリクエスト・レスポンスの往復。各ステップはトークン使用量付きのassistantメッセージを生成します - セッション:
resumeオプションでセッションIDを繋いだ一連のquery()呼び出し。セッション内の各query()呼び出しは、それぞれ独立にコストを報告します
TypeScriptとPythonでフィールド名は異なりますが、粒度は同じです。TypeScriptはassistantメッセージごとのmessage.message.idとmessage.message.usage、結果メッセージのmodelUsageと累計値を提供します。Pythonは同じ情報をmessage.usage / message.message_id(assistantメッセージ)、model_usageとtotal_cost_usd(結果メッセージ)として提供します。
クエリ全体のコストを取得する
query()呼び出しの終わりに出る結果メッセージ(TypeScript: SDKResultMessage、Python: ResultMessage)には、その呼び出し内の全ステップを合算したtotal_cost_usdが入ります。Pythonではこのフィールドがoptional型なので、読む前にNoneでないか確認します。成功・エラーどちらの結果にもこの値は含まれますが、セッションがクラッシュした場合の最終結果はゼロになることがあります。
import { query } from "@anthropic-ai/claude-agent-sdk";
try {
for await (const message of query({ prompt: "このプロジェクトを要約して" })) {
if (message.type === "result") {
console.log(`合計コスト: $${message.total_cost_usd}`);
}
}
} catch (error) {
console.error(`セッションがエラーで終了しました: ${error}`);
}from claude_agent_sdk import query, ResultMessage
import asyncio
async def main():
try:
async for message in query(prompt="このプロジェクトを要約して"):
if isinstance(message, ResultMessage):
print(f"合計コスト: ${message.total_cost_usd or 0}")
except Exception as error:
print(f"セッションがエラーで終了しました: {error}")
asyncio.run(main())サブエージェントを使う構成では、usage / total_cost_usd / modelUsageの3フィールドで、サブエージェント分の扱いが異なります。
| フィールド | サブエージェント分の扱い |
|---|---|
usage | サブエージェント分の扱い含まない。最上位のエージェントループだけを数え、ネストが起きた時点で過小評価になる |
total_cost_usd | サブエージェント分の扱い含む。サブエージェントのリクエストも最上位ループと合算 |
modelUsage / model_usage | サブエージェント分の扱い含む。モデル別の内訳付きで合算 |
サブエージェントを含むツリー全体のトークン集計にはmodelUsage(Python: model_usage)を使います。usageはサブエージェントが関わった時点で使えなくなります。サブエージェントの深さ・同時実行数・使う金額の上限はSub-agentsのcap設定で制御できます。
ストリーミング入力モードでの読み方
ストリーミング入力モードでは、1回のquery()呼び出しに複数のユーザーターンが乗り、ターンごとに結果メッセージが出ます。フィールドが指す範囲が変わる点に注意が必要です。
usage: そのターンだけを対象にし、しかもメインのエージェントループのみ(サブエージェントの分は含まない)total_cost_usdとmodelUsage(Python:model_usage): その呼び出し全体の、そこまでの累計
アプリが/clear /reset /newのいずれも送らない限り、呼び出し全体のコストは複数の結果を合算するのではなく、最新の結果を読むのが正解です。累計はこの3コマンドのいずれかを送るたびにリセットされ、query()呼び出しの中ではそれ以外に累計をリセットする要因はありません。
TypeScriptではリセットのたびにSDKConversationResetMessageが、Pythonでも同様にConversationResetMessageがストリームに流れるため、ストリームからリセットを検知できます。ただしPython SDK v0.2.137より前では、このメッセージがイテレータから欠落するため、自分が送った/clearターンから自分で数える必要があります。maxBudgetUsd(Python: max_budget_usd)も同じ累計と比較されるため、/clearを送ると予算もリセットされます。
ステップ単位・モデル単位で内訳を取る
ステップ単位の使用量
各assistantメッセージには、idとusage(トークン数)を持つBetaMessageがネストされています。Claudeが1つのターンで複数のツールを並列に使うと、そのターンの全メッセージが同じIDを共有します。同じIDを2回数えないよう、IDで重複排除する必要があります。
const seenIds = new Set<string>();
let totalInputTokens = 0;
let resultOutputTokens = 0;
for await (const message of query({ prompt: "このプロジェクトを要約して" })) {
if (message.type === "assistant" && !message.parent_tool_use_id) {
const msgId = message.message.id;
if (!seenIds.has(msgId)) {
seenIds.add(msgId);
totalInputTokens += message.message.usage.input_tokens;
}
}
if (message.type === "result") {
resultOutputTokens = message.usage.output_tokens;
}
}なぜ出力トークンがプレースホルダーになるかというと、Claude Codeは応答が始まった時点(message_start)でAPIが報告した使用量から各assistantメッセージを組み立てるためです。1つのAPIレスポンスから複数のassistantメッセージが生まれても、そのすべてが同じプレースホルダーを持ちます。実際の出力数はレスポンス終了時にAPIが報告し、Claude Codeがそれを結果メッセージに追加します。応答中の出力トークンの増え方をリアルタイムで見たい場合は、includePartialMessages(Python: include_partial_messages)を設定し、message_deltaストリームイベントからusageを読みます。
モデル単位の内訳
結果メッセージのmodelUsageは、モデル名ごとのトークン数とコストのマップです。サブエージェントにHaiku、メインエージェントにOpusのように複数モデルを使い分けている場合に、どこにトークンが流れているかを見るのに役立ちます。
for await (const message of query({ prompt: "このプロジェクトを要約して" })) {
if (message.type !== "result") continue;
for (const [modelName, usage] of Object.entries(message.modelUsage)) {
console.log(`${modelName}: $${usage.costUSD.toFixed(4)}`);
console.log(` 入力: ${usage.inputTokens} / 出力: ${usage.outputTokens}`);
console.log(` キャッシュ読み取り: ${usage.cacheReadInputTokens}`);
}
}各エントリのcostBasisは、そのモデルの直近リクエストをどの価格表で計算したかを示します。listはリスト価格、managedはmodelPricingテーブル、unknownはどちらにも一致しなかった場合です。このフィールドにはClaude Code v2.1.246以降が必要です。
複数回のquery()呼び出しを合算する
SDKはセッション単位の合計値を提供しません。多ターンの会話や複数ユーザーにまたがってquery()を何度も呼ぶアプリでは、自分で合算する必要があります。
let totalSpend = 0;
for (const prompt of prompts) {
for await (const message of query({ prompt })) {
if (message.type === "result") {
totalSpend += message.total_cost_usd;
}
}
}
console.log(`合計支出: $${totalSpend.toFixed(4)}`);エラー・キャッシュ・クラッシュへの対処
失敗した会話でもコストは発生している
成功・エラーどちらの結果メッセージにもusageとtotal_cost_usdは含まれます(Pythonでは両方optional型)。会話が途中で失敗しても、失敗するまでのトークンは消費済みです。一部のエラー結果ではusageが実際より少なく報告されます。
- セッションクラッシュ後の
error_during_execution: すべてのコストフィールドがゼロになることがある error_max_budget_usd:usageは予算を超えたレスポンス分を含まないが、total_cost_usdとmodelUsageには含まれる
選べるなら、usageではなくtotal_cost_usdかmodelUsageから集計します。
セッションクラッシュ後に集計を復元する
Claude Codeのプロセスがクラッシュすると、最終的にerror_during_executionの結果を出して終了します(単発・ストリーミング入力モードどちらでも同様)。この最終結果はusage total_cost_usd modelUsageがすべてゼロになっている場合があるため、その手前の結果から呼び出し全体の集計を復元します。
- クラッシュ直前のターンの結果を使います。ストリーミング入力モードでは、呼び出し開始または直前の
/clearからの累計を保持しています。単発呼び出しでこれより前の結果が無い場合、クラッシュが1ターン目で起きた場合、直前のターンが/clear自体だった場合はこの手が使えないため、手順2に進みます - assistantメッセージの
usageを、APIレスポンスごとに1回だけ数えて自分で合算します。単発モードでは全件、ストリーミング入力モードでは直近の結果以降の分だけを合算します。これでメインループの入力・キャッシュトークンは復元できますが、サブエージェント分・出力トークン・USD換算コストは復元できません
キャッシュトークンを追跡する
Agent SDKは繰り返し使う内容のコストを抑えるため、自動でプロンプトキャッシュを使います。自分で設定する必要はありません。usageオブジェクトにはキャッシュ追跡用の2フィールドがあります。
cache_creation_input_tokens: 新しいキャッシュエントリの作成に使ったトークン(通常の入力トークンより高い単価)cache_read_input_tokens: 既存のキャッシュエントリから読み取ったトークン(割引単価)
キャッシュによる節約額を把握するには、これらをinput_tokensとは別に追跡します。
プロンプトキャッシュのTTLを1時間に延ばす
自分のターンは、Claude Codeがインラインで実行する補助リクエストと一緒に「メイン会話のTTLバケット」に入ります。APIキー認証、またはAmazon Bedrock・Google Cloud・Microsoft Foundry経由で実行する場合、既定のキャッシュ書き込みTTLは5分です。同じシステムプロンプトとコンテキストに対して、5分より間隔の空いた短いセッションを何度も実行するワークロードでは、セッションの間にキャッシュが切れて毎回フル料金の入力コストがかかります。
キャッシュ書き込みのTTLを1時間にするには、環境変数ENABLE_PROMPT_CACHING_1Hを設定します。
const options = {
env: {
...process.env,
CLAUDE_CODE_USE_BEDROCK: "1",
ENABLE_PROMPT_CACHING_1H: "1",
},
};1時間TTLのキャッシュ書き込みは5分TTLより高い単価で課金されるため、これは「書き込みコストを上げてキャッシュ読み取りを増やす」というトレードオフです。メイン会話用のCLAUDE_CODE_PROMPT_CACHE_TTLとサブエージェント用のCLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTLという環境変数を使えば、バケットごとに5mか1hを個別に指定でき、こちらがENABLE_PROMPT_CACHING_1Hより優先されます。
まとめ
Agent SDKのtotal_cost_usdはあくまでクライアント側の見積もりで、正式な請求はUsage and Cost APIかClaude Consoleを使います。集計時は、サブエージェント分を含めたいならusageではなくtotal_cost_usdかmodelUsageを使う、ステップ単位のoutput_tokensはプレースホルダーなので結果メッセージから読む、ストリーミング入力モードでは結果を合算せず最新の値を読む、という3点を押さえれば、SDKが返す値を正しく解釈できます。セッションがクラッシュしても、直前の結果とassistantメッセージのusageから集計の大部分は復元できます。
よくある質問
TypeScriptとPythonでフィールド名はどう対応しますか
TypeScriptのmessage.message.usage / message.message.idはPythonのmessage.usage / message.message_idに、TypeScriptのmodelUsageはPythonのmodel_usageに、total_cost_usdは両SDKで共通のフィールド名に対応します。粒度は同じで、ネストの位置と命名規則だけが異なります。
サブエージェント1つ1つのコストを個別に確認できますか
SDKはサブエージェント単位の内訳を返しません。modelUsageはモデル別の合計であり、total_cost_usdにはサブエージェント分が合算されますが、「どのサブエージェントがいくら使ったか」を分離する内蔵の手段はありません。個別に見たい場合は、サブエージェントごとに別々のquery()呼び出しとして実行する構成にする必要があります。
サブスクリプションプランでも1時間TTLのキャッシュは使えますか
Claudeのサブスクリプションでプランに含まれる利用量の範囲内であれば、ENABLE_PROMPT_CACHING_1Hを設定しなくても、自分のターンと一部の補助リクエストで1時間TTLが適用されます。プランの追加利用枠を使い始めると、Claude Codeはそれらのターンを5分TTLへ切り替えます。
maxBudgetUsdに達すると何が起きますか
新しいサブエージェントの起動をBudget limit reachedで拒否し、実行中のバックグラウンドサブエージェントを停止し、クエリをerror_max_budget_usdという結果サブタイプで終了させます。この上限はtotal_cost_usdと比較されるため、サブエージェントのリクエストも上限にカウントされます。