跟踪成本与用量
在 Agent SDK 里跟踪 token 用量、估算成本并配置提示缓存:用量范围(query 调用、步骤、会话)、流式输入模式、总成本、按步骤和按模型的用量、跨调用累计、失败与崩溃后的恢复、缓存 token 与一小时 TTL。
Claude Agent SDK 为与 Claude 的每次交互提供详细的 token 用量信息。本指南讲如何正确跟踪用量并理解成本报告,尤其是在处理并行工具使用和多步骤对话时(完整的 API 文档见 TypeScript SDK 参考和 Python SDK 参考)。
警告: total_cost_usd 和 costUSD 字段是客户端估算,不是权威的计费数据。SDK 在本地根据构建时捆绑的价格表计算它们,除非有 modelPricing 表生效。它们可能在这些情况下与你实际被计费的金额产生偏差:价格变化;安装的 SDK 版本不认识某个模型;适用了客户端无法建模的计费规则。SDK 确实会建模的一条计费规则是数据驻留定价:当响应的 usage 报告 inference_geo: "us" 时,SDK 把该响应 token 的标价乘以 1.1;每请求的费用(如网页搜索)不乘(需要 TypeScript Agent SDK v0.3.239 及以上,或 Python Agent SDK v0.2.144 及以上)。把这些字段用于开发时的洞察和近似预算;要权威的计费,用 Usage and Cost API 或 Claude Console 的 Usage 页面。不要据这些字段向最终用户计费或触发财务决定。
理解 token 用量
TypeScript 和 Python SDK 以不同的字段名暴露相同的用量数据:
- TypeScript 在每条助手消息上提供按步骤的 token 拆分(
message.message.id、message.message.usage),通过结果消息上的modelUsage提供按模型的成本,并在结果消息上提供累计总数。 - Python 在每条助手消息上以
message.usage和message.message_id提供按步骤的 token 拆分,通过结果消息上的model_usage提供按模型的成本,并在结果消息上以total_cost_usd提供累计总数。
两个 SDK 使用相同的底层成本模型并暴露相同的粒度;差别在于字段命名以及按步骤用量嵌套在哪里。成本跟踪取决于理解 SDK 如何限定用量数据的范围:
query()调用:SDK 的query()函数的一次调用。单次调用可以涉及多个步骤:Claude 响应、使用工具、得到结果、再次响应。每次调用在结束时产生一条result消息,流式输入模式除外:那里一次query()调用携带多个用户轮次,每个轮次发出自己的result消息。- 步骤:
query()调用内的单个请求/响应周期。每个步骤产生带 token 用量的助手消息。 - 会话:通过
resume选项以会话 ID 链接起来的一系列query()调用。被恢复调用的结果报告整个会话的花费,而不只是该调用自己的(总数如何延续见「跨多次调用累计成本」)。
流程:每个步骤产生助手消息——Claude 响应时发送一条或多条助手消息。在 TypeScript 里,每条助手消息含嵌套的 BetaMessage(经 message.message 访问),带 id 和含 token 计数(input_tokens、output_tokens)的 usage 对象;在 Python 里,AssistantMessage dataclass 直接通过 message.usage 和 message.message_id 暴露相同的数据。Claude 在一个轮次里使用多个工具时,该轮次的所有消息共享同一个 ID,所以要按 ID 去重以避免重复计数。结果消息提供累计估算——query() 调用完成时,SDK 发出带 total_cost_usd 和累计 usage 的结果消息(TypeScript 里类型为 SDKResultMessage,Python 里为 ResultMessage)。如果你只需要估算的总数,可以忽略按步骤的用量,读这一个值。如果你做了多次独立的 query() 调用,每个结果只反映该次调用的成本;恢复会话的调用还会计入会话先前的花费。流式输入模式下每个轮次发出自己的结果消息(在该模式下如何读取调用总数,见下一节)。
在流式输入模式下跟踪成本
在流式输入模式下,一次 query() 调用携带多个用户轮次,每个轮次发出自己的结果消息。结果字段的范围不同:
usage:只涵盖该轮次,且在其中只涵盖主智能体循环,不含它运行的任何子智能体。total_cost_usd和modelUsage(Python 里是model_usage):携带到目前为止整个调用的累计总数,加上调用恢复会话时还原的任何花费。
在你的应用从不发送 /clear、/reset 或 /new 的调用里,读最新的结果得到调用总数,而不是跨结果求和。每次你的应用发送这三个命令之一,累计总数就重新开始;在 query() 调用内部,没有其他任何东西会重置它们。三个结果对你的核算有意义:/clear 轮次自己的结果——只涵盖重置以来运行的内容,并携带新的 session_id;之后的每个结果——从该重置起继续计数;每次 /clear 之前的最后一个结果——持有自上一次重置以来这些轮次的总数。要得出整个调用的总数,把每次 /clear 之前的最后一个结果加到调用的最终结果上;其他每个结果(包括 /clear 轮次自己的)都被之后的某个结果取代。在 TypeScript 里,SDK 还在每次重置时发出 SDKConversationResetMessage,所以你能从流里检测重置;在 Python 里,SDK 同样发出 ConversationResetMessage(Python SDK v0.2.137 之前,Python 迭代器丢弃该消息,所以在那些版本上要根据你的应用发送的 /clear 轮次自己统计重置)。maxBudgetUsd(TypeScript)或 max_budget_usd(Python)只统计该调用自己的花费:从恢复的会话还原的总数不计入它,/clear 会让预算重新开始。
得到一次查询的总成本
结果消息(TypeScript 里类型为 SDKResultMessage,Python 里为 ResultMessage)标志着一次 query() 调用的智能体循环的结束。它包含 total_cost_usd,即该调用里所有步骤累计的估算成本;恢复会话的调用还会计入会话先前的花费。读取该值时有两个注意事项:在 Python 里该字段类型是可选的,所以读取之前要检查它不是 None;成功和错误结果都携带它,不过会话崩溃的最终结果可能把它归零。
三个结果级字段在智能体派生子智能体时统计的内容不同。要做整棵树的 token 核算,用 modelUsage(Python 里是 model_usage);一旦出现嵌套,usage 字段就会少算。
| 字段 | 子智能体活动 |
|---|---|
usage | 排除。只统计顶层智能体循环,所以子智能体内部消耗的 token 不会加进来 |
total_cost_usd | 包含。把子智能体请求与顶层循环一起统计 |
modelUsage / model_usage | 包含。把子智能体请求与顶层循环一起统计,按模型拆分 |
在单消息输入模式下,当后台子智能体在最终轮次结束时仍在运行,Claude Code 会等它们(最多到「退出时的后台任务」所述的上限)再发出结果;结果的 total_cost_usd、duration_api_ms 以及 modelUsage(Python 里是 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) {
// 单次 query() 在产出错误结果之后抛出。如果失败是错误结果,
// 它仍然携带 total_cost_usd,上面的分支已经运行过了;
// 连接或进程失败不产生结果消息。
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:
# 单次 query() 在产出错误结果之后抛出。如果失败是错误结果,
# 上面的分支已经运行过了;连接或进程失败不产生结果消息。
print(f"Session ended with an error: {error}")
asyncio.run(main())要限制子智能体能给 total_cost_usd 增加多少,在查询上设置深度、并发和花费限制。
跟踪按步骤和按模型的用量
本节的例子使用 TypeScript 字段名;在 Python 里,对应的字段是按步骤用量的 AssistantMessage.usage 和 AssistantMessage.message_id,以及按模型拆分的 ResultMessage.model_usage。
跟踪按步骤的用量
每条助手消息含嵌套的 BetaMessage(经 message.message 访问),带 id 和含 token 计数的 usage 对象。Claude 并行使用工具时,多条消息共享同一个 id 和相同的用量数据;要记录你已经统计过的 ID 并跳过重复项,避免总数虚高。注意:去重后的按步骤值对输入和缓存 token 是准确的;按步骤的 output_tokens 是占位符,所以要从结果消息读取输出 token。下面的例子累计所有步骤的输入 token,每个唯一的主循环消息 ID 只计一次并跳过子智能体消息,并从结果消息(它涵盖主循环)读取输出总数:
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;
// 并行工具调用共享同一个 ID,只计一次
if (!seenIds.has(msgId)) {
seenIds.add(msgId);
totalInputTokens += message.message.usage.input_tokens;
}
}
if (message.type === "result") {
// 按步骤的 output_tokens 是占位符;结果消息
// 携带累计的输出总数。
resultOutputTokens = message.usage.output_tokens;
}
}
} catch (error) {
// 单次 query() 在产出错误结果之后抛出,所以下面的
// 输入总数仍然反映失败之前运行的步骤。
console.error(`Session ended with an error: ${error}`);
}
console.log(`Steps: ${seenIds.size}`);
console.log(`Input tokens: ${totalInputTokens}`);
console.log(`Output tokens: ${resultOutputTokens}`);按模型拆分用量
结果消息包含 modelUsage,一个从模型名到按模型 token 计数和成本的映射。当你运行多个模型(例如子智能体用 Haiku、主智能体用 Opus)并想看 token 花在哪里时很有用。每个条目的 costBasis 说明哪个价格表为该模型最近一次请求定价:list 为标价,managed 为 modelPricing 表,两者都不匹配该模型 ID 时为 unknown(需要 Claude Code v2.1.246 及以上)。下面的例子运行一个查询,并打印所用每个模型的成本和 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) {
// 单次 query() 在产出错误结果之后抛出。如果失败是错误结果,
// 上面的按模型拆分已经打印了;连接或进程失败不产生结果消息。
console.error(`Session ended with an error: ${error}`);
}跨多次调用累计成本
每次 query() 调用在它的结果上返回 total_cost_usd。怎么合并这些值取决于这些调用是否共享会话:独立的调用(没有 resume 或 continue 选项)——每个结果只涵盖它自己的调用,所以要自己把总数相加,如下面的例子所做;恢复同一个会话的调用——Claude Code 在进程正常退出时把会话的总数保存到它的记录里,并在之后的调用恢复或分叉该会话时还原它们;每个结果已经包含会话先前的花费,所以读最新的结果得到会话总数,把结果求和会重复计算还原的花费(v2.1.277 之前,通过 SDK 或 claude -p 恢复的会话把总数从零开始,所以每次调用的结果只涵盖该调用)。在流式输入模式下,按「在流式输入模式下跟踪成本」读取每个调用的总数;以崩溃结束的调用见「会话崩溃之后恢复总数」。下面的例子顺序运行两次 query() 调用,把每次调用的 total_cost_usd 加到运行总数里,并打印每次调用和合并后的成本:
import { query } from "@anthropic-ai/claude-agent-sdk";
// 跨多次 query() 调用跟踪累计成本
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) {
// 单次 query() 在产出错误结果之后抛出。如果失败是错误结果,
// 这次调用的成本已经被计入;连接或进程失败不产生结果消息。
// 继续下一个提示。
console.error(`Call failed: ${error}`);
}
}
console.log(`Total spend: $${totalSpend.toFixed(4)}`);from claude_agent_sdk import query, ResultMessage
import asyncio
async def main():
# 跨多次 query() 调用跟踪累计成本
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:
# 单次 query() 在产出错误结果之后抛出。如果失败是错误结果,
# 这次调用的成本已经被计入;连接或进程失败不产生结果消息。
# 继续下一个提示。
print(f"Call failed: {error}")
print(f"Total spend: ${total_spend:.4f}")
asyncio.run(main())处理错误、缓存和输出 token 计数
为了准确的成本跟踪,要考虑助手消息上的占位输出计数、失败的对话消耗的 token,以及缓存 token 的定价。
从结果消息读取输出 token
Claude Code 用 API 在响应开始时报告的用量构建每条助手消息,所以消息的 output_tokens 只是 API 在 message_start 时(响应生成之前)已报告的计数。一个 API 响应可以产生多条助手消息,每一条都携带同样的占位符。API 在响应结束时报告真实的输出计数,Claude Code 把它加到结果消息里。要从结果的 usage 读取输出 token,或从 modelUsage 读取按模型的拆分。要在响应流式输出时观察它的输出计数增长,设置 includePartialMessages(Python 里是 include_partial_messages),并从每个 message_delta 流事件读取 usage(TypeScript 里类型为 SDKPartialAssistantMessage,Python 里为 StreamEvent)。
跟踪失败对话的成本
成功和错误的结果消息都包含 usage 和 total_cost_usd;在 Python 里两个字段的类型都是可选的,所以读取之前要检查它们不是 None。如果对话在中途失败,你仍然消耗了直到失败点的 token。要从每条结果消息读取成本数据,不论它的 subtype 是 success 还是某个错误 subtype。在某些错误结果上,usage 报告的比该调用花费的少:会话崩溃之后的 error_during_execution——每个成本字段都可能被归零;error_max_budget_usd——usage 不含越过预算的那个响应,而 total_cost_usd 和 modelUsage 包含它。有选择时,按 total_cost_usd 或 modelUsage 而不是 usage 来核算。
会话崩溃之后恢复总数
Claude Code 进程崩溃时,它发出最终的 error_during_execution 结果并退出,单次和流式输入模式都一样。该结果可能携带归零的 usage、total_cost_usd 和 modelUsage,所以要从它之前到达的内容恢复调用的总数。只要存在更早的结果,第 1 步就能恢复完整的总数;第 2 步的回退只能恢复主循环的输入和缓存 token。
- 使用崩溃之前那个轮次的结果。在流式输入模式下,它持有「在流式输入模式下跟踪成本」所述的累计总数。当那个结果帮不上忙时改走第 2 步:调用是单次的,所以不存在更早的结果;崩溃发生在第一个轮次;崩溃之前的轮次就是
/clear本身,所以它的结果只涵盖重置。 - 改为对助手消息上的
usage求和,每个 API 响应只计一次,如「跟踪按步骤的用量」示例所做。单次模式下,对所有助手消息求和;流式输入模式下,对最后一个结果之后到达的求和。这给你主循环的输入和缓存 token;子智能体的用量无法这样恢复,输出 token 和美元成本也不行,因为按步骤的output_tokens是占位符。
跟踪缓存 token
Agent SDK 自动使用提示缓存来降低重复内容的成本,你不需要自己配置缓存。usage 对象包含两个额外的缓存跟踪字段:cache_creation_input_tokens——用于创建新缓存条目的 token(按比标准输入 token 更高的费率计费);cache_read_input_tokens——从现有缓存条目读取的 token(按降低的费率计费)。要把它们与 input_tokens 分开跟踪,以了解缓存节省了多少。在 TypeScript 里,这些字段在 Usage 对象上有类型;在 Python 里,它们作为键出现在 ResultMessage.usage 字典里(例如 message.usage.get("cache_read_input_tokens", 0))。
把提示缓存 TTL 延长到一小时
你自己的轮次落在主对话 TTL 桶里,连同 Claude Code 与之内联运行的辅助请求;Claude Code 在该对话之外发出的请求(如子智能体)有单独的 TTL 控制。当你用 API key 认证,或运行在 Amazon Bedrock、Google Cloud Agent Platform、Microsoft Foundry 或 Claude Platform on AWS 上时,你自己轮次的缓存条目默认使用 5 分钟 TTL。如果你的工作负载对同一个系统提示和上下文运行许多短会话,会话之间的间隔超过 5 分钟,缓存就会在会话之间过期,每个新会话都要付全额输入价格。要请求对缓存写入使用 1 小时 TTL,设 ENABLE_PROMPT_CACHING_1H 环境变量;你可以在 shell 或容器环境里导出它,也可以通过 options.env 传入。下面的例子为运行在 Amazon Bedrock 上的智能体启用 1 小时 TTL;因为它设了 CLAUDE_CODE_USE_BEDROCK,所以需要 Amazon Bedrock 可用的 AWS 凭据,没有它们查询会失败。
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 订阅里、在你套餐包含的用量之内,你不设这个变量就能在自己的轮次上(以及 Claude Code 在旁边发出的一些辅助请求上)得到 1 小时 TTL,一旦你开始使用用量额度,Claude Code 会把这些轮次降到 5 分钟 TTL。ENABLE_PROMPT_CACHING_1H 对两个桶里的每个请求都要求 1 小时 TTL。要分别为每个桶选择 TTL,改用这些控制;每个接受 5m 或 1h,并优先于 ENABLE_PROMPT_CACHING_1H:主对话——CLAUDE_CODE_PROMPT_CACHE_TTL 环境变量,或 promptCacheTtl 设置;其他一切——CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL 环境变量,或 subagentPromptCacheTtl 设置。把 promptCacheTtl 设为 1h 会在你使用用量额度时仍让主对话保持 1 小时缓存(完整的优先级顺序见「自己选择 TTL」)。
相关文档
- TypeScript SDK 参考:完整的 API 文档
- SDK 概览:SDK 入门
- SDK 权限:管理工具权限