Claude Code 学习站

Agent SDK 成本与用量跟踪指南

介绍 Claude Agent SDK 中如何跟踪 token 用量、估算成本、处理流式模式与会话崩溃场景,以及配置 1 小时提示缓存 TTL。

本页目录15
AI 摘要 · 已核查整理于 2026-07-24原文:Track cost and usage(Anthropic)Claude Agent SDK成本跟踪Token用量提示缓存
要点速览
  • total_cost_usd 和 costUSD 是客户端本地估算值,并非权威账单数据,权威计费请用 Usage and Cost API 或 Claude Console
  • 并行工具调用时多条 assistant 消息共享同一个消息 id,需按 id 去重后再累加 input_tokens,避免重复计数
  • assistant 消息上的 output_tokens 只是 message_start 时的占位值,真实输出 token 数要从 result 消息的 usage 或 modelUsage 中读取
  • 当 agent 派生 subagent 时,usage 字段只统计顶层主循环,会漏算子代理消耗;total_cost_usd 和 modelUsage/model_usage 才包含子代理请求
  • 流式输入模式下一次 query() 调用可能包含多个用户回合,每个回合各自产生一个 result 消息,应读取最新 result 而非逐条相加,且 /clear、/reset、/new 会重置累计值
  • 设置环境变量 ENABLE_PROMPT_CACHING_1H 可将默认 5 分钟的缓存 TTL 延长到 1 小时,但写入价格更高

本文是对官方 Agent SDK 某页的中文整理,完整与最新内容以原文为准:https://code.claude.com/docs/en/agent-sdk/cost-tracking

Claude Agent SDK 会为每次与 Claude 的交互提供详细的 token 用量信息。本指南说明如何正确跟踪用量、理解成本报告,尤其是在涉及并行工具调用和多步对话的场景下。

完整 API 文档参见 TypeScript SDK referencePython SDK reference

警告total_cost_usdcostUSD 字段是客户端本地估算值,并非权威账单数据。SDK 是根据构建时内置的价格表在本地计算得出的,因此在以下情况下可能与实际账单存在偏差:

  • 价格发生变化
  • 已安装的 SDK 版本不认识某个模型
  • 适用了客户端无法建模的计费规则

这些字段仅用于开发阶段的洞察和粗略预算估算。若需要权威账单数据,请使用 Usage and Cost APIClaude Console 的 Usage 页面。不要据此向终端用户计费或触发财务决策。

理解 Token 用量

TypeScript 和 Python SDK 暴露的是同一份用量数据,只是字段命名不同:

  • TypeScript:在每条 assistant 消息上提供逐步 token 明细(message.message.idmessage.message.usage),在 result 消息上通过 modelUsage 提供按模型划分的成本,并在 result 消息上提供累计总量。
  • Python:在每条 assistant 消息上以 message.usagemessage.message_id 提供逐步 token 明细,在 result 消息上通过 model_usage 提供按模型划分的成本,累计总量为 result 消息上的 total_cost_usd

两个 SDK 使用相同的底层成本模型,暴露相同粒度的信息,区别只在于字段命名和逐步用量的嵌套位置。

成本跟踪依赖于理解 SDK 对用量数据的作用域划分:

  • query() 调用:SDK query() 函数的一次调用。一次调用中可能包含多个步骤:Claude 回复、使用工具、获得工具结果、再次回复。每次调用结束时产生一条 result 消息,但在流式输入模式下例外——此时一次 query() 调用可携带多个用户回合,每个回合各自产生一条 result 消息。
  • 步骤(Step):一次 query() 调用内的单次请求/响应循环。每个步骤会产生带有 token 用量的 assistant 消息。
  • 会话(Session):通过 resume 选项以同一会话 ID 关联起来的一系列 query() 调用。会话内每次 query() 调用各自独立报告其成本。

消息流程

  1. 每个步骤产生 assistant 消息:Claude 回复时会发送一条或多条 assistant 消息。在 TypeScript 中,每条 assistant 消息包含一个嵌套的 BetaMessage(通过 message.message 访问),带有 id 和一个含 token 计数(input_tokensoutput_tokens)的 usage 对象。在 Python 中,AssistantMessage dataclass 直接通过 message.usagemessage.message_id 暴露相同数据。当 Claude 在一个回合内使用多个工具时,该回合内的所有消息共享同一个 id,因此需按 id 去重以避免重复计数。
  2. result 消息提供累计估算值query() 调用完成时,SDK 会发出一条带有 total_cost_usd 和累计 usage 的 result 消息,在 TypeScript 中类型为 SDKResultMessage,在 Python 中类型为 ResultMessage。如果你进行多次 query() 调用(例如多轮会话),每个 result 只反映该次调用自身的成本。如果只需要估算总额,可以忽略逐步用量,只读取这一个值。在流式输入模式下,每个回合各自产生一条 result 消息,具体读取方式见下一节。

在流式输入模式下跟踪成本

流式输入模式下,一次 query() 调用携带多个用户回合,每个回合各自产生一条 result 消息。result 字段的统计范围有所不同:

  • usage:仅覆盖该回合,且只统计主 agent 循环,不含它运行的任何子代理。
  • total_cost_usdmodelUsage(Python 中为 model_usage:携带到目前为止整次调用的累计总量。

在应用从未发送 /clear/reset/new 的一次调用中,应读取最新的 result 来获取调用总量,而不是把各个 result 相加。

这些累计总量会在应用每次发送上述三条命令之一时重新归零,在一次 query() 调用内部,除此之外没有其他因素会重置它们。以下三种 result 与统计相关:

  • /clear 回合自身的 result:只覆盖重置以来发生的内容,并携带一个新的 session_id
  • 之后的每个 result:从该次重置开始继续计数。
  • 每次 /clear 之前的最后一个 result:保存自上次重置以来各回合的总量。

要统计整次调用的总量,应将每次 /clear 之前的最后一个 result 与调用的最终 result 相加;其他所有 result(包括 /clear 回合自身的 result)都会被后续的 result 覆盖取代。

在 TypeScript 中,SDK 还会在每次重置时发出一条 SDKConversationResetMessage,因此可以从消息流中检测重置事件;Python 中同样会发出一条 ConversationResetMessage。在 Python SDK v0.2.137 之前,Python 的迭代器会丢弃该消息,因此在那些版本上需要自行根据应用发送的 /clear 回合来统计重置次数。

maxBudgetUsd(Python 中为 max_budget_usd)是与同一个累计总量做比较的,因此 /clear 同样会重置预算计数。

获取一次查询的总成本

result 消息(TypeScript 中类型为 SDKResultMessage,Python 中类型为 ResultMessage)标志着一次 query() 调用的 agent 循环结束。它包含 total_cost_usd,即该次调用中所有步骤的累计估算成本。在 Python 中该字段类型是可选的(optional),读取前需先检查它是否为 None。成功和错误的 result 都会携带该字段,不过会话崩溃后的最终 result 可能携带的是被清零的值。

如果你通过会话进行多次 query() 调用,每个 result 只反映该次调用自身的成本。在流式输入模式下,请按照上一节所述方式读取调用总量。

当 agent 派生子代理(subagents)时,以下三个 result 级字段对子代理活动的统计方式有所不同。请使用 modelUsage(Python 中为 model_usage)来统计整棵调用树的 token;一旦出现嵌套,usage 字段就会低估实际消耗。

字段对子代理活动的统计
usage不包含。只统计顶层 agent 循环,子代理内消耗的 token 不会被计入
total_cost_usd包含。将子代理请求与顶层循环一并统计
modelUsage / model_usage包含。将子代理请求与顶层循环一并统计,并按模型划分

以下示例遍历一次 query() 调用的消息流,并在 result 消息到达时打印总成本:

import { query } from "@anthropic-ai/claude-agent-sdk";

try {
  for await (const message of query({ prompt: "Summarize this project" })) {
    if (message.type === "result") {
      console.log(`Total cost: $${message.total_cost_usd}`);
    }
  }
} catch (error) {
  // A single-shot query() throws after yielding an error result. If the
  // failure was an error result, it still carried total_cost_usd and the
  // branch above has already run; connection or process failures yield
  // no result message.
  console.error(`Session ended with an error: ${error}`);
}
from claude_agent_sdk import query, ResultMessage
import asyncio


async def main():
    try:
        async for message in query(prompt="Summarize this project"):
            if isinstance(message, ResultMessage):
                print(f"Total cost: ${message.total_cost_usd or 0}")
    except Exception as error:
        # A single-shot query() raises after yielding an error result. If the
        # failure was an error result, the branch above has already run;
        # connection or process failures yield no result message.
        print(f"Session ended with an error: {error}")


asyncio.run(main())

要限制子代理能对 total_cost_usd 增加多少,可以在 query 上设置深度、并发和花费限制

跟踪逐步与逐模型用量

本节示例使用 TypeScript 字段名。在 Python 中,对应字段为逐步用量的 AssistantMessage.usageAssistantMessage.message_id,以及按模型划分的 ResultMessage.model_usage

跟踪逐步用量

每条 assistant 消息包含一个嵌套的 BetaMessage(通过 message.message 访问),带有 id 和含 token 计数的 usage 对象。当 Claude 并行使用工具时,多条消息会共享同一个 id 且用量数据相同。应记录已计数过的 id 并跳过重复项,以避免统计结果虚高。

警告:去重后的逐步值对于 input 和 cache token 是准确的。逐步 output_tokens 只是一个占位值,因此应从 result 消息中读取输出 token

以下示例在所有步骤上累加 input token,每个唯一的主循环消息 id 只计一次并跳过子代理消息,输出 token 总量则从 result 消息(覆盖主循环)中读取:

import { query } from "@anthropic-ai/claude-agent-sdk";

const seenIds = new Set<string>();
let totalInputTokens = 0;
let resultOutputTokens = 0;

try {
  for await (const message of query({ prompt: "Summarize this project" })) {
    if (message.type === "assistant" && !message.parent_tool_use_id) {
      const msgId = message.message.id;

      // Parallel tool calls share the same ID, only count once
      if (!seenIds.has(msgId)) {
        seenIds.add(msgId);
        totalInputTokens += message.message.usage.input_tokens;
      }
    }
    if (message.type === "result") {
      // Per-step output_tokens is a placeholder; the result message
      // carries the accumulated output total.
      resultOutputTokens = message.usage.output_tokens;
    }
  }
} catch (error) {
  // A single-shot query() throws after yielding an error result, so the
  // input total below still reflects the steps that ran before the failure.
  console.error(`Session ended with an error: ${error}`);
}

console.log(`Steps: ${seenIds.size}`);
console.log(`Input tokens: ${totalInputTokens}`);
console.log(`Output tokens: ${resultOutputTokens}`);

按模型拆分用量

result 消息包含 modelUsage,即模型名称到逐模型 token 计数和成本的映射。当你运行多个模型(例如子代理使用 Haiku、主代理使用 Opus)并想了解 token 消耗去向时,这个字段很有用。

以下示例运行一次查询,并打印每个所用模型的成本和 token 明细:

import { query } from "@anthropic-ai/claude-agent-sdk";

try {
  for await (const message of query({ prompt: "Summarize this project" })) {
    if (message.type !== "result") continue;

    for (const [modelName, usage] of Object.entries(message.modelUsage)) {
      console.log(`${modelName}: $${usage.costUSD.toFixed(4)}`);
      console.log(`  Input tokens: ${usage.inputTokens}`);
      console.log(`  Output tokens: ${usage.outputTokens}`);
      console.log(`  Cache read: ${usage.cacheReadInputTokens}`);
      console.log(`  Cache creation: ${usage.cacheCreationInputTokens}`);
    }
  }
} catch (error) {
  // A single-shot query() throws after yielding an error result. If the
  // failure was an error result, the per-model breakdown above has already
  // printed; connection or process failures yield no result message.
  console.error(`Session ended with an error: ${error}`);
}

modelUsage 中每个模型条目包含的字段:

字段说明
costUSD该模型的估算成本(美元)
inputTokens输入 token 数
outputTokens输出 token 数
cacheReadInputTokens从缓存读取的 token 数
cacheCreationInputTokens用于创建缓存的 token 数

累计多次调用的成本

每次 query() 调用返回各自的 total_cost_usd。SDK 不提供会话级别的总量,因此如果应用要进行多次 query() 调用(例如多轮会话或跨不同用户),需要自行累加总量。流式输入模式下,请按上文所述方式读取每次调用的总量。对于以崩溃结束的调用,参见下文「会话崩溃后恢复统计」。

以下示例依次运行两次 query() 调用,将每次调用的 total_cost_usd 加到累计总量中,并打印每次调用及合计的成本:

import { query } from "@anthropic-ai/claude-agent-sdk";

// Track cumulative cost across multiple query() calls
let totalSpend = 0;

const prompts = [
  "Read the files in src/ and summarize the architecture",
  "List all exported functions in src/auth.ts"
];

for (const prompt of prompts) {
  try {
    for await (const message of query({ prompt })) {
      if (message.type === "result") {
        totalSpend += message.total_cost_usd;
        console.log(`This call: $${message.total_cost_usd}`);
      }
    }
  } catch (error) {
    // A single-shot query() throws after yielding an error result. If the
    // failure was an error result, this call's cost was already counted;
    // connection or process failures yield no result message. Continue
    // with the next prompt.
    console.error(`Call failed: ${error}`);
  }
}

console.log(`Total spend: $${totalSpend.toFixed(4)}`);
from claude_agent_sdk import query, ResultMessage
import asyncio


async def main():
    # Track cumulative cost across multiple query() calls
    total_spend = 0.0

    prompts = [
        "Read the files in src/ and summarize the architecture",
        "List all exported functions in src/auth.ts",
    ]

    for prompt in prompts:
        try:
            async for message in query(prompt=prompt):
                if isinstance(message, ResultMessage):
                    cost = message.total_cost_usd or 0
                    total_spend += cost
                    print(f"This call: ${cost}")
        except Exception as error:
            # A single-shot query() raises after yielding an error result. If
            # the failure was an error result, this call's cost was already
            # counted; connection or process failures yield no result message.
            # Continue with the next prompt.
            print(f"Call failed: {error}")

    print(f"Total spend: ${total_spend:.4f}")


asyncio.run(main())

处理错误、缓存与输出 token 计数

为了准确跟踪成本,需要考虑 assistant 消息上的占位输出计数、失败对话所消耗的 token,以及缓存 token 的计价方式。

从 result 消息读取输出 token

Claude Code 是根据响应开始时 API 报告的用量来构建每条 assistant 消息的,因此消息的 output_tokens 只是 API 在 message_start 时报告的计数——也就是在响应实际生成之前的值。一次 API 响应可能产生多条 assistant 消息,而每一条都携带这同一个占位值。

API 会在响应结束时报告真实的输出计数,Claude Code 会把它加到 result 消息中。应从 result 的 usage 中读取输出 token,或从 modelUsage 中读取按模型划分的明细。

若想在响应流式生成过程中实时观察输出计数的增长,可设置 includePartialMessages(Python 中为 include_partial_messages),并从每个 message_delta 流事件中读取 usage,该事件在 TypeScript 中类型为 SDKPartialAssistantMessage,在 Python 中类型为 StreamEvent

跟踪失败对话的成本

成功和错误的 result 消息都包含 usagetotal_cost_usd;在 Python 中这两个字段都是可选类型,读取前需检查它们是否为 None

如果对话中途失败,在失败之前所消耗的 token 依然是真实消耗。应从每一条 result 消息中读取成本数据,无论其 subtypesuccess 还是某个错误子类型。在部分错误 result 上,usage 报告的数值会低于实际调用消耗:

  • 会话崩溃后的 error_during_execution:所有成本字段都可能被清零。
  • error_max_budget_usdusage 不包含导致超出预算的那次响应,而 total_cost_usdmodelUsage 包含它。

在可以选择的情况下,应优先根据 total_cost_usdmodelUsage 来统计,而不是 usage

会话崩溃后恢复统计

当 Claude Code 进程崩溃时,无论是单次模式还是流式输入模式,都会发出一条最终的 error_during_execution result 后退出。该 result 可能携带被清零的 usagetotal_cost_usdmodelUsage,因此需要从崩溃之前到达的消息中恢复该次调用的统计。步骤 1 只要存在更早的 result 就能恢复完整统计;步骤 2 的兜底方案只能恢复主循环的 input 和 cache token。

  1. 使用崩溃前一个回合的 result。在流式输入模式下,它保存的是自调用开始或自上次 /clear 以来的累计总量。如果出现以下情况,该 result 无法帮到你,请转到步骤 2:
    • 该调用是单次模式,不存在更早的 result;
    • 崩溃发生在第一个回合;
    • 崩溃前的那个回合恰好就是 /clear 本身,其 result 只覆盖了这次重置。
  2. 改为对 assistant 消息上的 usage 求和,每个 API 响应只计一次,做法与「跟踪逐步用量」示例相同。单次模式下对全部消息求和;流式输入模式下只对最后一个 result 之后到达的消息求和。这样能得到主循环的 input 和 cache token。子代理的用量无法用这种方式恢复,输出 token 和美元成本也不能,因为逐步 output_tokens 只是一个占位值

跟踪缓存 token

Agent SDK 会自动使用提示缓存(prompt caching)来降低重复内容的成本,无需自行配置缓存。usage 对象包含两个额外的缓存跟踪字段:

字段说明
cache_creation_input_tokens用于创建新缓存条目的 token 数(计价高于标准 input token)
cache_read_input_tokens从已有缓存条目中读取的 token 数(计价低于标准 input token)

应将它们与 input_tokens 分开跟踪,以了解缓存带来的节省。在 TypeScript 中,这些字段定义在 Usage 对象上;在 Python 中,它们是 ResultMessage.usage 这个 dict 里的键(例如 message.usage.get("cache_read_input_tokens", 0))。

将提示缓存 TTL 延长到一小时

当使用 API key 认证,或运行在 Amazon Bedrock、Google Cloud 的 Agent Platform、Microsoft Foundry 或 Claude Platform on AWS 上时,SDK 写入的缓存条目默认 TTL 为 5 分钟。如果你的工作负载是针对同一个 system prompt 和上下文频繁运行许多短会话,且会话之间的间隔超过 5 分钟,缓存就会在会话之间过期,每个新会话都要按完整 input 价格计费。

要为缓存写入请求 1 小时 TTL,可设置 ENABLE_PROMPT_CACHING_1H 环境变量。你可以在 shell 或容器环境中导出它,也可以通过 options.env 传入。

以下示例为运行在 Amazon Bedrock 上的 agent 启用 1 小时 TTL。由于它设置了 CLAUDE_CODE_USE_BEDROCK,需要具备可用的 AWS 凭证(参见 Amazon Bedrock),否则查询会失败。

from claude_agent_sdk import ClaudeAgentOptions, query
import asyncio


async def main():
    options = ClaudeAgentOptions(
        env={
            "CLAUDE_CODE_USE_BEDROCK": "1",
            "ENABLE_PROMPT_CACHING_1H": "1",
        },
    )

    async for message in query(prompt="Summarize this project", options=options):
        print(message)


asyncio.run(main())
import { query } from "@anthropic-ai/claude-agent-sdk";

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

for await (const message of query({ prompt: "Summarize this project", options })) {
  console.log(message);
}

1 小时 TTL 的缓存写入价格高于 5 分钟写入,因此启用该选项相当于用更高的写入成本换取更多的缓存命中。详情参见提示缓存价格说明。使用 Claude 订阅版且在包含额度范围内的用户会自动获得 1 小时 TTL,无需设置该变量。当消耗的是额外使用额度(usage credits)时,除非设置了 ENABLE_PROMPT_CACHING_1H,SDK 会降级为 5 分钟 TTL。

相关文档