에이전트가 문장을 작성하고 두 개의 도구를 호출하는 단계는 하나의 API 응답입니다. 트랜스크립트에 기록되는 Claude Code에서는 이 하나의 응답이 세 줄일 수 있으며, 각 줄에는 응답의 전체 usage 객체가 포함됩니다. 이 줄들을 합산하면 동일한 토큰을 세 번 계산하게 됩니다.
이 수치들은 공개되어 있습니다. 2026년 8월 Claude Code 이슈 트래커에 게시된 세션에서, cache_creation_input_tokens를 단순 합산한 값은 1,075,420이었습니다. 각 message.id를 한 번씩 계산하면 560,751로, 이는 1.92배의 과다 계산이며, 캐시 읽기는 1.59배 초과되었습니다 (issue #87303). Anthropic은 이 이슈를 문서화된 동작(documented behaviour)과 같은 날 폐쇄했습니다. 따라서 비용 스크립트가 줄을 합산한다면, 문제는 스크립트에 있습니다.
자신의 트랜스크립트를 양방향으로 합산하는 간단한 Node 스크립트를 통해 데이터에서 차이를 확인할 수 있습니다.
하나의 응답, 여러 줄
Claude Code는 각 세션을 ~/.claude/projects/<project>/<session-id>.jsonl에 JSONL 형태로 저장합니다. 여기서 메시지, 도구 사용 또는 메타데이터당 한 줄의 JSON 객체가 기록됩니다 (sessions docs).
하나의 API 응답이 여러 콘텐츠 블록(예: thinking 블록, 일부 text, 하나 이상의 tool_use 호출)을 포함할 때, Claude Code는 블록당 하나의 assistant 항목을 작성합니다. #87303 보고서는 각 항목이 자체 uuid를 가지며 단일 콘텐츠 블록을 가지고 있고, 응답의 message.id와 바이트 단위로 동일한 usage 객체를 공유한다고 설명합니다. 이슈 예시를 간소화하면 다음과 같습니다:
assistant uuid=70abebf3 content=[text]
assistant uuid=2c277f40 content=[tool_use]
assistant uuid=99c159e5 content=[tool_use]
...
해당 세션에서 521개의 사용량 기록이 323개의 고유 메시지 ID를 커버했으며, 보고자가 진행한 10개의 자동화된 세션 각각에 중복이 있었고, 그 수는 각 세션당 13개에서 86개 사이였습니다 (#87303).
이 패턴은 해당 보고서보다 오래되었습니다. Claude Code 1.0.96에 대해 2025년 8월에 개설된 Issue #6805는 stream-json 출력에서 동일한 분할을 설명했습니다: 하나의 메시지가 thinking, text 및 tool_use 블록으로 세 개의 이벤트로 방출되며, 각각 전체 사용량(full usage)을 가집니다. 해당 보고자는 한 메시지 ID의 cache_read_input_tokens 값이 세션 파일에 8번 나타났다고 말했습니다. 이 문제는 2026년 2월에 유지 관리자 답변 없이 폐쇄되었습니다.
Anthropic의 규칙: 각 ID를 단 한 번만 계산하기
#87303을 폐쇄하며, 한 유지 관리자는
첫째, 출력 토큰(output tokens)에 관한 것입니다. '단계별로 중복 제거된 값(deduplicated per-step values)'이 입력 및 캐시 토큰에는 정확하다고 나와 있지만, 단계별 output_tokens는 플레이스홀더입니다. 이는 API가 응답을 생성하기 전에 message_start 시점에 보고했던 카운트입니다. 실제 출력 카운트는 SDK의 최종 결과 메시지(cost tracking)에서 확인할 수 있습니다. 해당 구절은 SDK의 메시지 스트림에 관한 것이지만, 유지보수자(maintainer)가 닫은 #87303에서는 '트랜스크립트 파일과 stream-json 출력은 동일한 메시지 스트림'이라고 작성했으므로, 플레이스홀더는 디스크에서도 적용됩니다(#87303). Rust 사용량 CLI인 ccstats조차도, 그들의 회계 참고 사항(accounting notes)에 따르면 하나의 message.id에 대한 스트리밍 항목이 각각 사용량을 업데이트할 수 있기 때문에, stop_reason을 가진 항목을 유지합니다. 출력 숫자보다는 중복 제거된 입력 및 캐시 숫자를 더 신뢰하세요.
둘째, Claude Code가 계산하는 모든 달러 금액은 번들 가격표에서 나온 클라이언트 측 추정치입니다. 실제로 청구되는 금액에 대해서는 가이드가 Usage and Cost API 또는 콘솔의 사용량 페이지(cost tracking)를 참조하도록 안내합니다. 토큰이 어디에 쓰였는지 확인하려면 트랜스크립트 합계를 사용하고, 달러 금액을 언급하기 전에 Usage and Cost API를 확인하세요.
양방향으로 자체 트랜스크립트 합산하기
이 스크립트는 하나의 트랜스크립트를 읽고 모든 사용량 라인을 합산한 다음, message.id당 하나의 항목으로 다시 합산합니다. 이 스크립트는 중복을 유지할 규칙으로 stop_reason이 있는 것을 우선하며, 그렇지 않으면 가장 최신 항목을 따릅니다. message.id가 없는 항목은 #87303 리포터가 신뢰성 있게 중복 제거할 수 없다고 언급했기 때문에 그들의 uuid로 대체됩니다. 또한 다음 섹션의 이유로 하나의 requestId 아래에서 확인된 메시지 ID의 개수도 계산합니다.
// usage-sum.mjs: node usage-sum.mjs <session.jsonl>
import { readFileSync } from "node:fs";
...
가장 최근 세션에 대해 현재 Node.js로 실행해 보세요:
node usage-sum.mjs "$(ls -t ~/.claude/projects/*/*.jsonl | head -1)"
제가 직접 이 스크립트를 실행한 것은 아닙니다. 사용량 차이는 에이전트가 한 단계에서 얼마나 자주, 그리고 병렬로 생각하고 도구를 호출하는지에 따라 달라집니다. 두 열이 일치하면 해당 세션은 분할 응답이 없었다는 의미입니다. output_tokens 행은 중복 확인용으로만 읽으세요: Anthropic에 따르면 이는 중복 제거 후에도 자리 표시자이며, 실제 출력 횟수는 결과 메시지 또는 claude -p --output-format json에서 확인할 수 있습니다.
셸 환경을 유지하고 싶다면, jq가 하나의 필드를 처리할 수 있습니다. 첫 번째 명령어는 단순 합산이며, 두 번째 명령어는 ID당 하나의 항목을 유지합니다:
F=$(ls -t ~/.claude/projects/*/*.jsonl | head -1)
jq -s 'map(select(.type == "assistant" and .message.usage"))\n | map(.message.usage.cache_read_input_tokens // 0) | add' "$F"
...
각 그룹의 첫 번째 항목을 사용하는 것은 입력 및 캐시 필드에 대해 괜찮습니다. #87303이 중복 간에 바이트 단위로 동일함을 발견했기 때문입니다. 두 가지 제한 사항이 있습니다: group_by는 ID가 없는 모든 항목을 하나의 그룹으로 묶고, jq -s는 파싱할 수 없는 첫 번째 줄에서 중단하는데, Node 스크립트는 이 부분을 건너뜁니다.
두 스크립트 모두 또 다른 제한 사항을 공유합니다. sessions 문서는 항목 형식이 “Claude Code 내부적이며 버전별로 변경되므로, 이 파일을 직접 파싱하는 스크립트는 어떤 릴리스에서도 작동하지 않을 수 있다”고 명시하고 있습니다 (sessions docs). 스크립트 실행의 경우, claude -p --output-format json을 사용하면 구조화된 출력으로 사용량과 비용을 확인할 수 있습니다.
토큰이 두 번 계산되는 네 가지 추가 방법
응답 분할(Split responses)은 일반적인 사례입니다. 파일 전체 또는 실행 전체에 걸쳐 합산할 때 더 많은 경우가 발생합니다.
브랜치는 기록을 복사합니다. /branch와 --fork-session은 현재 시점까지의 대화 내용을 담는 트랜스크립트를 가진 새 세션을 생성합니다 (sessions docs). 프로젝트 폴더 내 모든 파일을 합산하면 복사된 부분이 브랜치당 한 번씩 계산됩니다. 파일별이 아닌, message.id를 기준으로 모든 파일에 걸쳐 중복을 제거해야 합니다. ccstats 노트는 프로젝트 및 서브 에이전트 파일 전반의 복사에 대해서도 같은 점을 지적합니다.
압축(Compaction)은 기록을 다시 작성할 수 있습니다. Issue #66947에서는 각 압축 또는 연속 실행이 이전 항목들을 동일한 uuid와 타임스탬프를 가진 트랜스크립트에 다시 작성하는 것처럼 보인다고 보고했습니다. 한 메시지가 하나의 파일에서 다섯 번 나타난 경우도 있었습니다. Anthropic은 이를 확인하지 않았으며, 봇은 계획되지 않은 것으로 종결했습니다. ID를 키로 사용하면 어느 쪽이든 이 문제를 해결할 수 있습니다.
재개된 총합에는 이미 과거가 포함되어 있습니다. Claude Code는 세션의 총합을 정상적으로 종료할 때 트랜스크립트에 저장하고, 나중에 호출하여 재개될 때 이를 복원합니다. 따라서 재개된 호출의 각 결과는
하나의 보고서가 부하 상태에서 ID 중복을 보고합니다. Issue #64880는 여전히 열려 있으며, 서버 부하 상태에서 동일한 message.id를 가진 여섯 개의 개별적인 순차 응답을 보고합니다. 이 보고자는 나중에 일반 부하 창을 확인했고, 하나의 requestId당 두 개 이상의 message.id가 없다는 것을 발견했습니다. 이것이 스크립트의 split 라인이 확인하는 내용입니다. 만약 0보다 큰 값이 출력된다면, 해당 파일에 대해 message.id와 requestId를 키로 사용하십시오.
지켜보는 동안 증가하는 비용 카운터
이것을 잘못 이해하기는 쉽습니다. 심지어 지출을 모니터링하는 데 사용하는 도구에서도 마찬가지입니다. 저는 Hermes IDE를 구축했으니, 이 점을 염두에 두시기 바랍니다. 그 2.2.9 릴리스는 2026년 10월 12일에 게시되었으며, 동일한 계열의 버그를 수정했습니다:
세션별로 Hermes는 에이전트가 보고하는 비용 또는 트랜스크립트를 기반으로 추정된 비용을 토큰 합계와 함께 보여줍니다 (README). 다운로드하는 것은 무료이며, BSL 1.1에 따라 소스 코드가 공개되어 있고 macOS, Windows 및 Linux에서 실행됩니다. Anthropic과 제휴하지 않습니다. 만약 위 스크립트를 옆에서 실행했는데 숫자가 일치하지 않는다면, 두 출력 결과를 모두 포함하여 이슈를 열어주세요.
유용했다면
예상치 못한 LLM 청구서 고지서는 아무도 확인하지 않은 숫자로 시작되는 경우가 많습니다. 잘못된 곳에서 읽은 사용량 필드이거나, 같은 응답이 두 번 계산되었을 수 있습니다. _AI That Answers_는 제 시리즈의 첫 번째 책이며, Node.js용 TypeScript로 첫 호출부터 프롬프트와 구조화된 출력까지 토큰 비용을 다룹니다. 만약 사용 기록에 대한 중복 제거(dedup) 스크립트를 방금 작성했다면, 해당 필드들이 얼마의 비용이 발생하는지 설명하는 책입니다.
AI That Answers — AI in TypeScript
이는 AI in TypeScript 시리즈의 1권으로, 첫 LLM 호출부터 프로덕션 환경의 에이전트에 이르기까지 다섯 권의 책을 다룹니다: 전체 시리즈. 그리고 만약 에이전트 옆에서 세션별 지출 비용을 확인하고 싶다면, Hermes IDE가 무료입니다.