Claude Media
Agent SDK Cost Trackingでトークン使用量を正確に集計する

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"を報告すると、そのレスポンスのトークンのリスト価格を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のレスポンスからトークンとコストを集計する方法を扱います。大量のプロンプトを安く処理したい場合は、Message Batches SDKの実装が参考になります。

全体の金額を知るにはどのフィールドを読むか

SDKは使用量を3つの単位で区切ります。

  • query()呼び出し: query()関数を1回呼ぶこと。中でClaudeが応答・ツール使用・結果取得・再応答を何段階も繰り返します。呼び出しの最後にresultメッセージが1つ出ます。ストリーミング入力モードだけは例外で、1回の呼び出しに複数のユーザーターンが乗り、ターンごとにresultが出ます
  • ステップ: query()内の1回のリクエスト・レスポンスの往復。各ステップはトークン使用量付きのassistantメッセージを生みます
  • セッション: resumeオプションでセッションIDを繋いだ一連の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()1回分の金額は、結果メッセージ(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())

単発のquery()は、エラー結果を流したあとに例外を投げます。エラー結果にもtotal_cost_usdは入っているため、上のコードではcatchより前にコストの出力が済んでいます。接続やプロセスの失敗ではresultメッセージ自体が出ません。

サブエージェントを使う構成では、usage / total_cost_usd / modelUsageの3フィールドで、サブエージェント分の扱いが異なります。

フィールドサブエージェント分の扱い
usageサブエージェント分の扱い含まない。最上位のエージェントループだけを数え、ネストが起きた時点で過小評価になる
total_cost_usdサブエージェント分の扱い含む。サブエージェントのリクエストも最上位ループと合算
modelUsage / model_usageサブエージェント分の扱い含む。モデル別の内訳付きで合算

ツリー全体のトークン集計にはmodelUsage(Python: model_usage)を使います。単発入力モードで、最終ターンの終わりにバックグラウンドのサブエージェントが動いている場合、Claude Codeはその完了を待ってから結果を出します。待つ上限は、連続で10分アイドルになった時点です(CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MSで変更でき、0なら上限なし)。結果のtotal_cost_usdとmodelUsageには待っている間の作業も入ります。

サブエージェントの深さ・同時実行数・金額の上限は、サブエージェントのcap設定の側で決めます。

二重計上と過小計上が起きる場面

同じtotal_cost_usdでも、呼び出し方によって指す範囲が変わります。合算の仕方を間違えると、金額は2倍にも半分にもなります。なおgetContextUsage()のtoken-countingリクエストはストリームに出ないため、この集計には乗りません。詳しくはgetContextUsage()の解説にあります。

セッションを再開したときは最新の結果だけを読む

resumeやcontinueで同じセッションを続けた呼び出しでは、結果がセッション全体の累計を報告します。Claude Codeは正常終了時にセッションの累計をトランスクリプトへ保存し、再開・フォーク時に復元するためです。

くらべる

呼び出し方で変わる合算の仕方

resume・continueなし

独立した呼び出し

各結果はその呼び出し単独の金額です。自分で足し合わせます。

resume・continueあり

同じセッションを再開した呼び出し

各結果に、セッションのそれまでの支出が入っています。足すと復元分を二重に数えるので、最新の結果だけを読みます。

v2.1.277より前は、SDKやclaude -pで再開したセッションの累計がゼロから始まり、各結果はその呼び出し分だけでした。古いバージョンを混在させている環境では、結果の読み方が変わる点に注意します。

独立した呼び出しを順に実行して合算するコードは次のとおりです。

let totalSpend = 0;
 
for (const prompt of prompts) {
  try {
    for await (const message of query({ prompt })) {
      if (message.type === "result") {
        totalSpend += message.total_cost_usd;
      }
    }
  } catch (error) {
    console.error(`呼び出しが失敗しました: ${error}`);
  }
}
console.log(`合計支出: $${totalSpend.toFixed(4)}`);

エラー結果のコストは例外の前に加算済みなので、catchでは次のプロンプトへ進むだけで済みます。

ストリーミング入力モードでは/clearが累計を区切る

ストリーミング入力モードでは、1回のquery()呼び出しに複数のユーザーターンが乗り、ターンごとに結果が出ます。フィールドごとの範囲は次のとおりです。

  • usage: そのターンだけ。しかもメインのエージェントループのみで、サブエージェント分は含まない
  • total_cost_usdとmodelUsage(Python: model_usage): その呼び出し全体の、そこまでの累計。セッションを再開した呼び出しなら復元済みの支出も含む

アプリが/clear /reset /newを送らない限り、呼び出し全体の金額は最新の結果を読めば足ります。結果を足してはいけません。

この3コマンドのどれかを送ると累計は最初からやり直しです。query()の中で累計を戻すのは、この3つだけです。送った場合の結果は3種類に分かれます。

  • /clearのターン自身の結果: リセット後に走った分だけで、新しいsession_idが付く
  • 以降の結果: リセットの時点から数え直した値
  • 各/clearの直前の結果: 前回のリセット以降のターンの総額

呼び出し全体の総額は、「各/clearの直前の結果」を全部足し、最後の結果を加えて出します。それ以外の結果は後続の結果に置き換わるので、/clearのターン自身の結果も含めて足しません。

TypeScriptではリセットのたびにSDKConversationResetMessageが流れ、PythonもConversationResetMessageを出すので、ストリームから検知できます。ただしPython SDK v0.2.137より前はこのメッセージがイテレータから欠落します。その場合は、自分が送った/clearのターンから数えます。

金額の上限は呼び出し自身の支出だけを数える

maxBudgetUsd(Python: max_budget_usd)は、その呼び出し自身の支出だけを上限と比べます。再開したセッションから復元された累計は数えません。/clearを送ると予算もやり直しになります。

上限に届くと、Claude Codeは3つのことをします。新しいサブエージェントの起動をBudget limit reachedで拒否し、実行中のバックグラウンドのサブエージェントを止め、クエリをerror_max_budget_usdのサブタイプで終えます。サブエージェントのリクエストも上限に数えられます。この動作にはClaude Code v2.1.217以降が必要です。ストリーミング入力では、上限に達したあとの同じ会話の後続メッセージもerror_max_budget_usdで終わります。

失敗した会話でもコストは発生している

成功・エラーのどちらの結果にもusageとtotal_cost_usdが入ります(Pythonでは両方optional型)。会話が途中で失敗しても、そこまでのトークンは消費済みです。一部のエラー結果ではusageが実際より少なく出ます。

  • セッションクラッシュ後のerror_during_execution: すべてのコストフィールドがゼロになることがある
  • error_max_budget_usd: usageは予算を超えたレスポンス分を含まないが、total_cost_usdとmodelUsageには含まれる

選べるなら、usageではなくtotal_cost_usdかmodelUsageから集計します。

ステップ単位・モデル単位で内訳を取る

ステップ単位の使用量

各assistantメッセージには、idとusage(トークン数)を持つBetaMessageがネストされています。1つのターンでClaudeが複数のツールを並列に使うと、そのターンの全メッセージが同じ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が各assistantメッセージを、応答の開始時(message_start)にAPIが報告した使用量から組み立てるためです。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}`);
    console.log(`  キャッシュ作成: ${usage.cacheCreationInputTokens}`);
  }
}

各エントリのcostBasisは、そのモデルの直近リクエストをどの価格表で計算したかを示します。listはリスト価格、managedはmodelPricingテーブル、unknownはどちらにも一致しなかった場合です。このフィールドにはClaude Code v2.1.246以降が必要です。

セッションクラッシュ後に集計を復元する

Claude Codeのプロセスがクラッシュすると、最終的にerror_during_executionの結果を出して終了します。単発・ストリーミング入力のどちらでも同じです。この最終結果はusage total_cost_usd modelUsageがゼロの場合があるので、その手前に届いた値から集計を復元します。

手順

クラッシュ後の復元手順

  1. 1

    直前のターンの結果を使う

    ストリーミング入力モードなら、直前の結果が呼び出し開始(または直前の/clear)からの累計を持っています。使えないのは、単発呼び出しで前の結果が無い場合、クラッシュが1ターン目だった場合、直前のターンが/clear自体だった場合です。その場合は手順2へ進みます。

  2. 2

    assistantメッセージのusageを合算する

    APIレスポンスごとに1回だけ数えます。単発モードでは全件、ストリーミング入力モードでは直近の結果以降の分です。メインループの入力・キャッシュトークンは復元できますが、サブエージェント分・出力トークン・USD換算のコストは復元できません。

キャッシュトークンとTTLを追跡する

Agent SDKは、繰り返し使う内容のコストを抑えるため、自動でプロンプトキャッシュを使います。設定は不要です。usageオブジェクトにはキャッシュ追跡用の2フィールドがあります。

  • cache_creation_input_tokens: 新しいキャッシュエントリの作成に使ったトークン(通常の入力トークンより高い単価)
  • cache_read_input_tokens: 既存のキャッシュエントリから読み取ったトークン(割引単価)

節約額を把握するには、これらをinput_tokensとは別に追跡します。TypeScriptではUsage型のフィールドです。PythonではResultMessage.usageの辞書のキーなので、message.usage.get("cache_read_input_tokens", 0)のように読みます。

プロンプトキャッシュのTTLを1時間に延ばす

自分のターンは、Claude Codeがインラインで実行する補助リクエストと一緒に「メイン会話のTTLバケット」に入ります。サブエージェントなど会話の外のリクエストは、別のTTL制御を持ちます。

APIキー認証のとき、またはAmazon Bedrock・Google Cloud・Microsoft Foundry・Claude Platform on AWSで動かすときの既定のキャッシュ書き込みTTLは5分です。同じシステムプロンプトとコンテキストに対し、5分より長い間隔を空けて短いセッションを何度も走らせるワークロードでは、キャッシュが切れて毎回フル料金の入力コストがかかります。

TTLを1時間にするには、環境変数ENABLE_PROMPT_CACHING_1Hを設定します。次の例はBedrock上のエージェントです。CLAUDE_CODE_USE_BEDROCKを設定するので、AWSの認証情報が必要です。

const options = {
  env: {
    ...process.env,
    CLAUDE_CODE_USE_BEDROCK: "1",
    ENABLE_PROMPT_CACHING_1H: "1",
  },
};

1時間TTLのキャッシュ書き込みは5分TTLより高い単価で課金されます。書き込みコストを上げて、キャッシュ読み取りを増やす取引です。

バケットごとに分けたいときは、メイン会話にCLAUDE_CODE_PROMPT_CACHE_TTL(設定キーはpromptCacheTtl)、それ以外にCLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL(設定キーはsubagentPromptCacheTtl)を使います。どちらも5mか1hを取り、ENABLE_PROMPT_CACHING_1Hより優先されます。

Claudeのサブスクリプションで、プランに含まれる利用量の範囲内なら、この変数なしでも自分のターンと一部の補助リクエストに1時間TTLがかかります。追加利用枠(usage credits)を使い始めると、Claude Codeはそれらを5分TTLへ切り替えます。promptCacheTtlを1hにすれば、追加利用枠を使っている間もメイン会話の1時間キャッシュが保たれます。

CLIで同じ値を取り出す

SDKのquery()は、内部でClaude Codeのプロセスを動かしています。CLIにも、--printと組み合わせて使う同じ種類のフラグがあります。v2.1.287のclaude --helpには、集計に関わる次のフラグが載っています。

claude --version
claude --help | grep -A2 -E "max-budget-usd|include-partial-messages|forward-subagent-text|output-format"

出力から集計に関わる部分を抜き出すと、次のとおりです。

2.1.287 (Claude Code)
  --forward-subagent-text               Forward subagent text and thinking
                                        blocks as assistant/user messages with
                                        parent_tool_use_id set (only works with
                                        --print and --output-format=stream-json)
  --include-partial-messages            Include partial message chunks as they
                                        arrive (only works with --print and
                                        --output-format=stream-json)
  --max-budget-usd <amount>             Maximum dollar amount to spend on API
                                        calls (only works with --print)
  --output-format <format>              Output format (only works with --print):
                                        "text" (default), "json" (single
                                        result), or "stream-json" (realtime

読み取れるのは3点です。--max-budget-usdフラグはmaxBudgetUsdに当たる上限で、--printでしか効きません。--include-partial-messagesは、前述のmessage_deltaイベントを流すスイッチに当たります。--forward-subagent-textを付けると、サブエージェントの文章がassistantメッセージとして流れ、parent_tool_use_idで本体と見分けられます。

--output-formatのjsonは結果を1つだけ返し、stream-jsonは途中のメッセージも流します。

まとめ

読むフィールドは、サブエージェントを含めたいかどうかで決まります。含めたいならtotal_cost_usdかmodelUsage、メインループだけならusageです。足し算は、再開したセッションと/clearを挟むストリーミング入力では最新の結果、独立した呼び出しなら自分で加算、と使い分けます。

よくある質問

サブエージェント1つ1つのコストを個別に見られますか

結果メッセージの3フィールド(usage / total_cost_usd / modelUsage)には、サブエージェント別の内訳がありません。modelUsageはモデル別の合計で、total_cost_usdはサブエージェント分を合算した値です。サブエージェントの文章や思考はforwardSubagentTextオプションで流せ、複数のメッセージに付くparent_tool_use_idで本体と区別できます。サブエージェント別の金額を内蔵の集計で出す仕組みは、結果メッセージには含まれていません。

この記事を共有:XはてブLinkedIn