ある朝、前夜の記事生成バッチのコストを見て手が止まりました。前日とほぼ同じ処理のはずが、請求だけが約2倍に膨らんでいたのです。
原因は処理の失敗でも、モデルの取り違えでもありませんでした。その晩だけ、蓄積した会話履歴と読み込んだファイル群で入力が20万トークンを越え、リクエスト全体が長コンテキスト単価に切り替わっていたのです。1トークンの差で単価が不連続に跳ねる、まさに「崖」でした。
個人開発で Dolice の4サイトの更新処理を毎晩モデルに任せている私にとって、この崖は放置できない性質のものでした。失敗ならログに残りますが、これは成功したまま静かに請求だけが増えます。気づく手がかりが請求書しかない、という怖さがありました。
その崖を送信の手前で検知して止める仕組みを、いま実際に運用しているコードのまま残しておきます。
なぜ20万トークンで請求が跳ねるのか
Claude Sonnet 5 はネイティブで1Mトークンのコンテキストを扱えます。ただし料金は一律ではありません。入力が20万トークンを超えたリクエストは、長コンテキスト単価という別の料金帯で課金されます。
重要なのは、これが「20万を超えた分だけ高くなる」段階的な加算ではない点です。私の理解では、境界を越えた瞬間にそのリクエスト全体 が長コンテキスト単価で計算されます。入力も出力も、20万1トークン目だけでなく全量が上位帯の単価になります。だからこそ、境界の直前と直後で総額が滑らかにではなく不連続に跳ねます。
具体的な単価の関係は次の通りです。導入価格の基準帯は公表値ですが、長コンテキスト帯の倍率は改定されることがあるため、必ず最新の料金ページで確認し、後述の設定値に反映してください。
入力トークン 入力単価(100万あたり) 出力単価(100万あたり) 備考
20万以下 基準帯(Sonnet 5 導入価格 $2) 基準帯($10) 〜2026-08-31 の導入価格
20万超 長コンテキスト帯(基準の約2倍が目安) 長コンテキスト帯(要確認) 境界超過でリクエスト全体が対象
私は入力側でおよそ2倍になる想定で設計していますが、正確な倍率は運用前に必ず確認し、コードにハードコードせず設定として持たせています。単価は変わりますが、「境界で全体が切り替わる」という構造は当面変わらないはずです。守るべきはこの構造の方です。
送信前に見積もる — count_tokens は課金対象外
崖を避ける鍵は、送信してから請求で気づくのではなく、送信する前に入力トークン数を知る ことです。ここで Token Counting API(count_tokens)が効いてきます。
この API には、無人運用にとって都合のよい性質が2つあります。ひとつは、count_tokens 自体は課金の対象外であること。もうひとつは、通常のメッセージ送信とは別枠で、レート制限を消費しないことです。つまり、毎リクエストの前に安全に呼べます。見積もりのために本番の予算やレート枠を削らずに済むわけです。
トークン数の一般的な把握についてはClaude API のトークン数を事前に把握してコストを最適化する方法 でも扱っていますが、ここでは「境界を越えるか越えないか」という一点の判定に絞って使います。
Python でのプリフライト実装
送信直前に呼ぶプリフライト関数です。入力トークンを見積もり、安全マージンを含めて3つの判定を返します。
import os
import anthropic
client = anthropic.Anthropic( api_key = os.environ[ "ANTHROPIC_API_KEY" ])
TIER_BOUNDARY = 200_000 # 長コンテキスト単価に切り替わる境界
SAFE_CEILING = 180_000 # 安全マージン。私はこの1割手前を上限に運用しています
MODEL = "claude-sonnet-5"
def count_input_tokens (system, messages, tools = None ):
"""送信前に入力トークン数を見積もる。
count_tokens は課金対象外で、レート制限も消費しません。"""
resp = client.messages.count_tokens(
model = MODEL ,
system = system,
messages = messages,
tools = tools or [],
)
return resp.input_tokens
def preflight (system, messages, tools = None ):
tokens = count_input_tokens(system, messages, tools)
if tokens <= SAFE_CEILING :
return "send" , tokens # そのまま送信
if tokens <= TIER_BOUNDARY :
return "warn" , tokens # 崖の直前。記録を残して送信
return "block" , tokens # 長コンテキスト単価に入る。要対処
decision, tokens = preflight(system_prompt, history)
print ( f "input= { tokens :, } decision= { decision } " )
# 期待する出力の例:
# input=176,320 decision=send
# input=204,880 decision=block
境界ちょうどの20万ではなく、18万を実務上の上限にしているのは理由があります。count_tokens の見積もりと、実際に課金されるトークン数は完全には一致しないからです。ツール定義やシステムプロンプト、プロンプトキャッシュの扱いによって数千トークンのずれが出ます。崖の縁ぎりぎりを攻めると、見積もりでは20万以下でも実送信で越えてしまいます。この1割のマージンは、その不確実性への保険です。
崖の手前で止めたあと、どう捌くか
block を返したとき、選択肢は主に3つあります。私は入力の性質で使い分けています。
1. 要約で畳んで境界の内側に戻す。 会話履歴が積み上がって膨らんだ場合に有効です。古いターンを安価なモデルで1つの要約にまとめ、直近のやり取りだけ原文で残します。
def compact_history (messages, keep_recent = 6 ):
"""古いターンを要約に畳んで境界の内側に戻す。
直近 keep_recent ターンは原文のまま残します。"""
if len (messages) <= keep_recent:
return messages
old, recent = messages[: - keep_recent], messages[ - keep_recent:]
rendered = " \n " .join( f ' { m[ "role" ] } : { m[ "content" ] } ' for m in old)
summary = client.messages.create(
model = "claude-haiku-4-5" , # 要約は安価なモデルで十分です
max_tokens = 1024 ,
messages = [{
"role" : "user" ,
"content" : "次の会話を、後続タスクに必要な事実だけ箇条書きで要約してください: \n " + rendered,
}],
).content[ 0 ].text
return [{ "role" : "user" , "content" : f "[これまでの要約] \n{ summary } " }] + recent
2. 分割して複数リクエストに散らす。 独立に処理できるファイル群を1リクエストに詰め込んでいた場合、それぞれを20万未満の塊に分けます。境界を跨ぐ1回の巨大リクエストより、境界内の複数リクエストの方が総額は安くなります。全体が上位単価になる崖を、そもそも踏まないからです。
3. 実行を先送りする。 その晩に必ず終える必要がないバッチなら、圧縮で品質を落とすより、翌日に人手で分割方針を見直す方が誠実な場合もあります。無理に境界内へ押し込めて要約しすぎ、肝心の文脈を失っては本末転倒です。
どの手を採るにせよ、境界超過は「異常」ではなく「設計で織り込むべき通常の分岐」だと捉えるようになりました。
TypeScript 版
Node で動かしているパイプライン向けに、同じ判定を TypeScript でも用意しています。
import Anthropic from "@anthropic-ai/sdk" ;
const client = new Anthropic ();
const TIER_BOUNDARY = 200_000 ;
const SAFE_CEILING = 180_000 ;
const MODEL = "claude-sonnet-5" ;
type Decision = "send" | "warn" | "block" ;
async function preflight (
system : string ,
messages : Anthropic . MessageParam [],
) : Promise <{ decision : Decision ; inputTokens : number }> {
const { input_tokens } = await client.messages. countTokens ({
model: MODEL ,
system,
messages,
});
let decision : Decision = "send" ;
if (input_tokens > TIER_BOUNDARY ) decision = "block" ;
else if (input_tokens > SAFE_CEILING ) decision = "warn" ;
return { decision, inputTokens: input_tokens };
}
// 使用例
const { decision , inputTokens } = await preflight (systemPrompt, history);
console. log ( `input=${ inputTokens . toLocaleString () } decision=${ decision }` );
if (decision === "block" ) {
throw new Error ( "長コンテキスト単価の境界を超過。圧縮または分割が必要です" );
}
このプリフライトは、日次トークン超過で処理を強制停止する予算サーキットブレーカーの設計 の上流に置くと相性がよいと感じています。サーキットブレーカーが「使いすぎたら止める」最後の砦なら、プリフライトは「単価の崖を踏まないよう手前で舵を切る」操舵に当たります。
実運用で見えた落とし穴
半月ほど運用して気づいた点を残します。
見積もりと実請求のずれは、主にツール定義とプロンプトキャッシュから来ます。ツールを多く渡すバッチほど、count_tokens の値より実送信が数千トークン大きくなりがちです。キャッシュヒット分は入力トークンとして数え方が変わるため、キャッシュを効かせている場合は「境界判定用の生のトークン数」と「実際の課金額」を混同しないよう分けて考える必要があります。プロンプトキャッシュ自体の効かせ方はプロンプトキャッシュで月額コストを半分にした実装メモ にまとめています。
もうひとつ。監視すべきは入力トークンの絶対値ではなく、日々の分布 です。ふだん12万前後で安定しているバッチが、ある晩だけ19万に伸びていれば、その翌週には境界を越えます。私は毎晩のプリフライト結果をログに残し、warn の出現頻度が上がってきたら圧縮ロジックを見直す、という運用にしています。崖は突然現れるのではなく、じわじわ近づいてくるものでした。
なお、Claude Code 側で1Mコンテキストを扱う判断についてはネイティブ1Mコンテキストをいつ使うか、コストで決める判断ルール で別途整理しています。API を直接叩く場合と、Claude Code 経由では、コスト管理の勘所が少し異なります。
次の一歩
まずは今動いている無人バッチの1本に、送信直前の count_tokens 呼び出しを1行足して、結果をログに出すところから始めてみてください。止める処理はまだ入れなくて構いません。数日分のトークン分布を眺めるだけで、自分のパイプラインが崖からどれくらい離れているかが見えてきます。
私自身、この計器を付けるまで、崖のふちを何度も気づかず通り過ぎていました。見えるようにするだけで、夜間の自動実行をずっと落ち着いて任せられるようになったと感じています。