活用ガイドLOG 213約20分で読める

ClaudeとGeminiのプロンプトキャッシュ、なぜ当たらない? 揃える場所・選ぶAPIとモデル・外れる変更を仕様表と10手順で切り分ける【2026年10月版】

ClaudeもGeminiも、キャッシュの読込が0のまま。原因を探す前に、どのAPI・モデルで何が使えるか、保存点をどこに置くか、何を変えると外れるかを一枚ずつ確認できるよう、2026年10月26日の公式仕様を表にしました。自動・明示、最小入力、TTL、usageの見方、診断APIと10の手順、手元で試せる最小コードまで。

文・ニコ

同じ資料を何度も渡しているのに、cache_read_input_tokensがずっと0。GeminiのcachedContentTokenCountも出てこない。

キャッシュを入れたはずなのに、請求は下がらない。何が悪いんでしょう?

実は、原因の多くは「設定ミス」というより「前提のずれ」です。ClaudeとGeminiでは、保存の指示のしかたも、結果を確かめる場所も、そもそも使える機能もちがいます。同じ「キャッシュ」という名前でも、中身は別物なんですね。だから、古い解説や別のAPIの設定をそのまま持ってくると、気づかないうちに外れます。

この記事は当たらないキャッシュを表と手順で切り分けるための手引きです。

ぼくは2026年10月26日に、ClaudeとGeminiの公式ドキュメントを取り直して照合しました(URLは記事の最後にまとめています)。ただし、生成APIへの有料リクエストは送っていません。 記事のコードは「試し始めるための例」で、動作確認まではしていません。ヒット率や削減率の実測値も載せていません。公式に書いてあることと、ぼくの整理を分けて書きます。

先に結論

  • キャッシュするのは回答ではなく、入力の「先頭部分」です。 同じ質問の答えを使い回す仕組みではありません。新しい質問への出力は毎回生成されます。
  • 揃えるのは「同じモデル・同じAPIで、同じ先頭」です。 Claudeはtools → system → messagesの順。保存点までが一字一句同じである必要があります。
  • Claudeは固定資料の末尾に保存点を置きます。 自動キャッシュだけだと、末尾(毎回変わる質問)に保存点が付き、共通の資料が保存されないことがあります。
  • Geminiは暗黙と明示で別物です。 暗黙は設定不要でも当たる保証がありません。明示はgenerateContentで作る必要があり、InteractionsやGemini 4 Argon以降では使えません。
  • 最小入力はモデルごとに違います。 Claudeは512〜4,096 tokens、Gemini暗黙は2,048〜4,096 tokens。足りないと、エラーなしで黙ってキャッシュされません。
  • usageは「読込」「書込」「通常入力」を分けて見ます。 Claudeのinput_tokensは通常入力だけ。GeminiのpromptTokenCountはキャッシュ分を含みます。
  • 切り分けは外側から。 API・usage・対応可否 → 長さ・位置 → 中身の差 → 期限・環境の順に確かめます。

誰向けの記事?

ClaudeかGeminiのAPIで、同じ資料・システム指示・ツール定義を繰り返し渡している人向けです。自作のエージェント、社内ボット、資料への質問応答などですね。

APIを直接触らない非エンジニアの人は、「先頭が同じ部分だけが再利用される」「毎回変わる値は後ろへ置く」の2つを持ち帰れば十分です。エンジニアの人は、後半の表・手順・最小コードをそのまま使ってください。

ClaudeアプリやGeminiアプリ、Claude Codeの定額枠の話は扱いません。API課金のキャッシュだけです。usageを請求額に直す計算は、エージェントの費用の記事に任せます。

まず確認:このキャッシュは何を保存する?

ひとくちにキャッシュと言っても、何を使い回すのでしょう? 名前が似ているものが3つあります。

種類何を使い回す?この記事の対象
入力キャッシュ(プロンプトキャッシュ)同じ入力の先頭部分の処理結果○
回答キャッシュ同じ質問への、できあがった答え×
セマンティックキャッシュ意味が似た質問への、過去の答え×

入力キャッシュは、モデルが先頭部分を読み込んだ結果を取っておいて、次回は読み直しを省く仕組みです。だから、新しい質問に対する出力は毎回つくります。 割引されるのは、キャッシュを読んだ入力の部分だけです。

揃える場所:Claudeは「保存点までの先頭」

Claudeでは、cache_controlを付けた場所が保存点になります。保存されるのはそのブロック単体ではありません。tools → system → messagesの先頭から、保存点までの累積した部分です。

つまり、資料が同じでも、その前にあるツールやsystemが変われば、先頭から資料までが同じ内容にはなりません。

Claudeの入力はtools、system指示、固定資料、今回の質問の順に並ぶ。Aは末尾の質問に保存点があり、質問が変わると先頭が一致しない。Bは固定資料の末尾に明示の保存点を置き、2回目も資料まで読み込める。

自動キャッシュだけだと、共通資料が保存されないことがある

Claudeには、リクエストの最上位にcache_controlを付けるだけの「自動キャッシュ」があります。便利ですよね。ただ、保存点が付くのは最後の保存可能なブロックです。

独立した質問を投げる使い方だと、こうなります。

  • 1回目:固定資料 → 質問A。保存されるのは「資料+質問A」まで。
  • 2回目:固定資料 → 質問B。資料までは同じでも、そこに保存点がありません。

公式ドキュメントにも、この失敗例が載っています。このときの探索は「すでに書かれたエントリを探す」だけ。安定した部分を探して、保存してくれるわけではありません。

対策は固定資料の末尾に明示の保存点を置くことです。

{
  "model": "claude-sonnet-5-5",
  "max_tokens": 256,
  "system": [
    {"type": "text", "text": "固定の作業指示"},
    {
      "type": "text",
      "text": "十分に長い固定資料の本文",
      "cache_control": {"type": "ephemeral"}
    }
  ],
  "messages": [{"role": "user", "content": "今回だけ異なる質問"}]
}

これは構造の説明です。仮の短い文では最小長に届かないので、実際の資料で試してください。資料をsystemに置くのは、自分が管理する信頼できる資料のときだけにします。外から取ってきた文書なら、指示と区別してuserメッセージ側に置き、その固定資料の末尾に保存点を付ける形もあります。キャッシュのために指示の権限を変える必要はありません。

会話を足していく使い方には自動が合う

逆に、履歴を追記していく会話やエージェントのループには、自動キャッシュが合います。

  • 1回目:固定内容 → user1[保存点]
  • 2回目:固定内容 → user1 → assistant1 → user2[新しい保存点]。前回の保存点を探して読み込む。

ただし、20位置の探索範囲という制限があります。公式の説明では、保存点を含めて最大20位置までしかさかのぼりません。たとえば、前回の保存点はブロック15にあるとします。そこからブロック35まで伸びてしまうと、35〜16を調べても15には届きません。この場合は、15にも保存点を残します。明示の保存点は最大4つです。

なお、連続するtool_useの並びや、連続するtool_resultの並びは、それぞれ1位置として数えます。並列ツールの個数がそのまま位置数になるわけではありません。

Geminiの暗黙は、置き方の基本だけ

Geminiの暗黙キャッシュには、保存点の指定がありません。公式が挙げている改善策は2つだけです。

  • 大きくて共通の内容を、プロンプトの先頭に置く。
  • 似た先頭のリクエストを、短い時間で送る。

内部の並び順(Claudeのtools → system → messagesのような仕様)は公開されていません。ぼくたちにできるのは、自分が管理するsystem_instruction・tools・contentsの順序と値を安定させ、送ったあとにusageで確かめることです。

選ぶAPIとモデル:まず仕様表で見る

ここが一番まちがえやすいところです。同じ会社でも、APIが違えば使えるキャッシュが違います。

どのAPIで、どの方式が使える?

提供面入口キャッシュまわりのポイント
Claude API(Messages)POST /v1/messages自動・明示ともに使える。診断APIもここ
Gemini Developer API / generateContentPOST .../models/{model}:generateContent暗黙と、対応モデルの明示キャッシュ
Gemini Developer API / InteractionsPOST .../interactions暗黙のみ。明示は非対応
Google Cloud側のGeminiプロジェクト・ロケーション付きのAPI最小長や条件がDeveloper APIと違う
Bedrockなどから使うClaude各提供元のAPInativeの全設定・診断を前提にしない

Googleの資料では、新しく使い始めるならInteractionsを推奨し、generateContentをLegacyと呼んでいます。ただし、後者も引き続きサポートすると明記されています。「推奨のAPI」と「明示キャッシュを使えるAPI」は同じではありません。 明示キャッシュが必要なら、generateContentを選びます。

Interactionsのprevious_interaction_idは前の会話を取ってくるIDです。明示キャッシュのcachedContents/{id}とは別物で、置き換えられません。会話の履歴が保存されることと、キャッシュが読み込まれることも別の話です。

自動・明示、最小入力、TTL、一致条件、usage

4つの方式を、同じ項目で並べました。

項目Claude:自動Claude:明示保存点Gemini:暗黙Gemini:明示
有効化リクエスト最上位にcache_controlを追加個別ブロックにcache_control対応モデルで既定オンcachedContentsを作成して参照
使えるAPIMessagesMessagesInteractions、generateContentgenerateContentのみ
対応モデル現行のClaude同左原則Gemini 2.5以降Gemini 3・2.5。Gemini 4 Argon以降は非対応
保存される範囲最後の保存可能ブロックまで指定ブロックまでの先頭サービスが管理作成時のcontents・system・tools等
有効期間既定5分、ttl:"1h"で1時間同左固定TTLの保証値なし省略時1時間。作成時・更新時に指定
一致の条件保存済みの先頭と一致し、探索範囲に保存位置があるtools → system → messagesの先頭から保存点まで同一先頭を共通にし、近い時間に送る。ヒットは保証なし同じ名前を、同じモデルで参照。内容は変更不可
読込の見つけ方usage.cache_read_input_tokens同左Interactions:usage.total_cached_tokens/generateContent:usageMetadata.cachedContentTokenCount生成応答のusageMetadata.cachedContentTokenCount
初回通常は書込同左0でも異常ではない作成の成功と、その後の読込を別に確認
費用の注意書込にプレミアム(5分1.25倍・1時間2倍)同左ヒットした入力だけ割引。全体の削減率ではない読込に加えて、保存時間の課金がある

ここでひとつ、読み違えやすいのが「自動」です。「自動」という日本語が、ClaudeとGeminiで別の意味なんです。 Claudeの自動は「保存点を置く場所を自動にする設定」。Geminiの暗黙は「サービスがこっそり再利用すること」です。同じ感覚で設計すると、前提がずれてしまいます。

最小入力:短いと、黙ってキャッシュされない

最小入力に届かないと、cache_controlを付けてもエラーなしでキャッシュされません。ここは見落としやすいところ。モデルごとに確認します。

Claude(公式ガイドの代表的なモデル)

最小tokensモデル
512Opus 5.5 / Opus 5 / Sonnet 5.5 / Haiku 5.5 / Fable 5.1 / Fable 5 / Mythos 5.1 / Mythos 5
1,024Opus 4.8 / Sonnet 5 / Sonnet 4.6 / Sonnet 4.5
2,048Opus 4.7 / Mythos Preview
4,096Opus 4.6 / Opus 4.5 / Haiku 4.5

Sonnet 4.5のようにdeprecated(非推奨)のモデルも表に載っています。「表にある」ことと「いま新規に使える」ことは別なので、モデルページで確かめてください。

Gemini Developer APIの暗黙キャッシュ

モデル最小tokens
Gemini 4 Argon / 3.8 Flash / 3.6 Flash / 3.1 Pro Preview4,096
Gemini 2.5 Flash / 2.5 Pro2,048

注意が3つあります。

  1. この表は暗黙キャッシュの表です。 明示キャッシュを作るときの下限は、現行ガイドに全モデル共通の数字がありません。暗黙の値を明示の保証値として使わず、作成APIの応答で確かめます。
  2. Cloud側は条件が違います。 Google Cloudの資料には、3.8 Flashの暗黙キャッシュが6,144 tokensという別の条件が載っています。Developer APIの4,096と混ぜると、切り分けを誤ります。
  3. Gemini 4 Argon以降のエージェントには、さらに別のルールがあります。 新しいリクエストで追加されるトークンが2,050以上なら、前回のリクエスト全体をキャッシュします。それより少ないと、前回の末尾2,050 tokensを通常料金でもう一度処理します。出力・思考・関数の結果もこの追加量に数えます。4,096は「最小入力」、2,050は「会話更新のしきい値」で、別物です。

文字数からトークン数を決めつけるのもやめましょう。日本語、JSONスキーマ、PDF、画像は、文字数からの換算がとくに外れやすい入力です。事前に数えるなら、Claudeは/v1/messages/count_tokens、GeminiはcountTokensを使います。ただし、数えられるのは「長さ」だけで、ヒットの確認ではありません。

古い解説にも注意です。2025年の動画に出てくる「Flashは1,024 tokens」「75%」や、Gemini 1.5時代のコードは、現行のモデルにそのまま当てはまりません。

外れる変更:何を変えると、どこまで失う?

先頭が一致しなくなる変更は、思ったより多いです。Claudeの公式表をもとに、「どの層が外れるか」を整理しました。

Claude

変更外れる範囲実装で見るところ
モデルの切り替えそのモデルのキャッシュ(モデルごとに別)ルーター、フォールバック、A/Bの割り当て
ツールの名前・説明・スキーマ・順序tools以降(ほぼ全部)列挙順、スキーマのシリアライズ、SDKの自動追加
systemに入れた時刻・ユーザーIDその位置以降テンプレートへの差し込み
Web検索・citationsのオン/オフsystemとmessagesフラグ、サーバーツールの設定
speed(fast/standard)の切り替えsystemとmessages速度設定
tool_choiceの変更messages全呼び出しで同じ値か
画像の有無messages同じ文字でも構成が変わっていないか
thinkingのモード・budget、output_config.effortmessages。tools・systemはモデル次第「出力の設定だから無関係」と決めない
過去のmessagesの編集・削除・並べ替え変更した位置以降履歴の要約や圧縮、tool resultの整形し直し
保存点より後ろの質問だけの変更その質問だけ新規処理。前方は再利用の候補保存点までが本当に同じか

これは「すべての既存エントリが消える」という意味ではなく、そのリクエストで先頭の一致を失う範囲を示しています。

ひとつ意外なのが、thinkingとeffortです。出力に関する設定に見えるのに、キャッシュにも関係するの? と思いませんか。実際はプロンプトに組み込まれるので、変えるとmessagesの一致が外れます。明示的にモデルの既定値と同じにした場合は、省略と同じ扱いで外れません。

ツールを動的に追加するときには、例外があります。公式のtool searchのdefer_loadingは、見つけた定義を履歴の中に追加することで、先頭のツールの並びを保つ設計です。毎回tools配列を作り直す方式とは違います。導入するときは、対応モデルと制約を別に確認してください。

Gemini(明示キャッシュ)

変更・状態判断次にすること
今回の質問だけを変えるキャッシュはそのまま参照できる資料を重複して送らず、usageを見る
資料・system・tools・toolConfigを変えたいこれらは作成後に変更できない新しい版を作り直し、新しい名前を使う
作成時と違うモデルを指定同じキャッシュを別モデルで使えないそのモデル用に別に作る
cachedContentの指定を忘れた明示では参照していない暗黙のヒットと混同せず、送信bodyを確認
期限切れ・削除済み・別の提供面有効な参照にならないGETで存在と期限を確認
Fileをアップロードしただけキャッシュを作ったことにはならないfiles/...とcachedContents/...を区別

「資料は同じだから、systemだけ差し替えて1つのキャッシュを使おう」という設計はできません。要約役と監査役でsystemが違うなら、組み合わせごとにキャッシュを作ります。

公式SDKのissueには、生成時にもsystem_instructionやツールを重ねて指定すると400になった、という2025年の報告があります。現行のモデルで再確認はしていませんが、設定は保存側にまとめ、生成時には重複して指定しない形が安全です。

時間・並列・サーバーツールの落とし穴(Claude)

  • TTLはリクエストの開始時刻から測ります。 応答が終わってから5分ではありません。長い生成のあとに少し待つと、残りの時間が短いことがあります。読込のたびに期限は更新されます。
  • TTLを混ぜるなら、長い方を先に。 1時間の保存点を前、5分の保存点を後ろに置きます。自動キャッシュは4枠のうち1枠を使うので、明示の保存点が4つあるところに足すと400です。最後の明示点と自動点でTTLが違う場合も400になります。
  • 新しいエントリは、最初の応答が始まってから使えます。 冷えた状態で一斉に送っても、すべてのリクエストで読み込めるとは限りません。まず1件を完了させてから、次を送ります。
  • Batchは、ヒットが「できれば」の扱いです。 順序も同時実行も固定でないからです。配置の良し悪しは、通常のリクエストで確かめてからにします。
  • Web検索などのサーバーツールが走ると、内部のtool resultに5分の保存点が自動で付きます。 自分の保存点を全部1時間にしていても、ephemeral_5m_input_tokensが出るのはこのためかもしれません。

usageの読み方:保存・読込・不明を見分ける

観測の入口もAPIごとに違います。

APIと表現読込書込全入力の出し方
Claude(REST/SDK)usage.cache_read_input_tokensusage.cache_creation_input_tokens(TTL別はusage.cache_creation)input_tokens + 書込 + 読込
Gemini generateContent(REST)usageMetadata.cachedContentTokenCount明示:作成応答のusageMetadata.totalTokenCountpromptTokenCount(読込を含む)
Gemini generateContent(Python SDK)usage_metadata.cached_content_token_countcache.usage_metadata.total_token_count同上。snake_caseとcamelCaseを取り違えない
Gemini Interactionsusage.total_cached_tokens(明示は使わない)APIごとの定義で扱う

同じ呼び出しを、2社のusageで見るとこうなります。

全入力12,870トークンの呼び出し。Claudeはinput_tokens 70、書込800、読込12,000の3つを足して全入力。Geminiはpromptトークン数12,870が全入力で、読込12,000を含む。

Claudeの例(説明用の仮の数字です)

{
  "input_tokens": 70,
  "cache_creation_input_tokens": 800,
  "cache_read_input_tokens": 12000,
  "cache_creation": {
    "ephemeral_5m_input_tokens": 800,
    "ephemeral_1h_input_tokens": 0
  }
}

12,000 tokensを読み、新しく800を保存し、通常入力は70です。「書込が0じゃないからミス」ではありません。会話が伸びれば、読込と書込は同時に出ます。全入力は12,870なので、入力の読込率は12,000 ÷ 12,870。これは入力トークンの割合で、請求の削減率ではありません。

Geminiの例(同じく仮の数字)

{
  "usageMetadata": {
    "promptTokenCount": 12870,
    "cachedContentTokenCount": 12000,
    "candidatesTokenCount": 300
  }
}

Geminiでは、promptTokenCountの中にキャッシュ分も含まれます。Claudeのように足してはいけません。

もうひとつの罠が0と欠落を同じにしてしまうことです。応答に項目がない。SDKでNoneになっている。中継が落としている。明示的に0が返っている。この4つは別の状態です。最初は生のusageオブジェクトをそのまま保存し、項目があったかどうかも記録します。ダッシュボードに読込が出ないときも、元のJSONを見ます。ストリーミングの場合は、リクエストID単位で1つの観測値にまとめます。

10手順で切り分ける

ここからが本題です。読込が0のときに、外側(呼び方)から内側(中身)へ確かめていきます。

不発の切り分け10手順。1から3は何を呼んでいるか、4から6は条件を満たすか、7と8は中身が同じか、9と10は環境かを確かめる。
順確かめること判定・対処
1実際のhost、API名、モデルID、SDKの版、利用tierClaude/Gemini、Developer/Cloud、Interactions/generateContentを特定。ルーターの表向きのモデル名で判断しない
2正しいusageを読めているか生JSONの親フィールドから確認。欠落と0を分ける
3そのモデル・APIが、その方式に対応するかInteractions/Argon以降に明示を求めていたら、方式を変える。Claudeは設定が最終bodyに載っているかも確認
4保存対象の長さ資料・tools・systemを対象モデルで数える。Claudeは保存点まで。全体の長さや文字数で代用しない
5初回作成と再利用を分けたかClaudeは書込量、Gemini明示は作成応答を確認。そのあとの同条件の送信で読込を見る
6保存位置・参照名が正しいかClaudeは固定部分の末尾に保存点があるか、20位置の探索で過去の点に届くか。Gemini明示は名前をbodyに渡しているか
7保存済みの先頭が同一か時刻、ID、資料の版、改行、ツール順、スキーマ、過去履歴の「最初の差分」を探す
8見た目以外の設定が変わっていないかモデル、thinking、effort、tool_choice、画像の有無、betaヘッダー、speed、system差し込みを固定
9TTL・並列・スコープ最初の応答の前に並列送信していないか。リクエスト開始の間隔、組織・workspace、Cloudならproject/location、キャッシュの期限を照合
10中継・SDKとサービス側を分けられるか同じ提供元のnativeの最小リクエストで比べる。nativeで読めて中継で読めないなら、変換後のbodyとusage欠落を疑う。nativeでも読めないなら、条件とrequest IDをそろえて提供元に相談

手順7で気をつけたいのは、比べる範囲です。「JSON全体の文字列が違うから必ずミス」とは限りません。質問や通信ヘッダーが変わるのは正常です。比べるのは、保存した先頭と、プロンプトに影響する設定です。逆に、キーを並べ替えた正規化JSONが同じでも、必ずヒットするとは言えません。手元で作ったfingerprintは、提供元の内部のキーそのものではないからです。

観測パターンから、最初の1手を選ぶ

10手順を全部やる前に、いまの症状から調べ始める場所を選べます。

観測最初に疑うもの注意点
Claude:書込0、読込0保存点なし、最小長不足、対象にならない末尾まず「保存できた」ことを確認する
Claude:毎回、書込が大きく読込0変わる末尾だけに保存点、先頭の差分、期限切れ設定があるだけでは解決しない
Claude:読込も書込もある部分ヒットと、履歴の追加保存会話が伸びれば正常でも起きる
Claude:間隔が短くても、ときどき0ルーティング、モデル、workspace、可用性時間だけで実装のバグと決めない
Gemini暗黙:初回だけ0冷えた状態正常な可能性。2回目以降を近い時間で観測
Gemini暗黙:毎回0共通の先頭が短い、先頭が毎回変わる、モデル・API・tier、観測ミス入力が同じでもヒットの保証はない
Gemini明示:作成が400モデル非対応、入力が短い、body不備エラー理由を保存。推測した最小値で上書きしない
Gemini明示:参照時にエラー期限、名前、モデル、認証先、重複した設定暗黙のミスとは別の障害
Gemini明示:200だがcached量が0・不明usageのフィールド違い、参照の脱落、中継キャッシュが存在するだけで成功にしない
ヒット率は高いのに費用が下がらない再利用が少ない、書込・保存時間、通常入力と出力の割合配置の検証を終えてから、費用の集計へ

Claudeの診断APIを使う

Claudeには、「前回から、入力のどこが変わったか」を教えてくれる診断の機能があります。最初のリクエストからdiagnosticsを付けておかないと、あとから比較できません。

{"diagnostics": {"previous_message_id": null}}

2回目以降はnullのところに前の応答のidを入れます。公式の現行ガイドでは、cache-diagnosis-2026-04-07のbetaヘッダーはもう不要で、付けても従来どおり動くと書かれています。

応答のdiagnostics.cache_miss_reasonはこう読みます。

値意味
model_changedモデルが変わった(ルーター・A/B・フォールバックなど)
system_changedsystemが違う。時刻やリクエストIDの差し込みが典型
tools_changedツールの追加・削除・並べ替え、スキーマのシリアライズの揺れ
messages_changed過去のmessagesを書き換えた・並べ替えた・削った
previous_message_not_found前回の記録が見つからない。入力が変わった証拠ではない
unavailable診断できない。tool_choice・thinking・effortなど、他の設定の違いもここに入る
null(cache_miss_reason自体)比較がまだ終わっていない。結論は出さない
diagnostics: null差分が見つからなかった

診断は「リクエストが変わったか」、usageは「キャッシュが当たったか」を教えてくれます。両方を見ると、次の1手がわかります。

診断読込読み方
null多い想定どおり。先頭は安定し、当たっている
null少ない・0リクエストは一致しているのに、エントリが残っていない。間隔を詰めるか、1時間TTLを検討
*_changed少ない・0変わった。typeが示す原因を直す
*_changed多いまれ。後ろで差分が出たが、前方の保存点は当たった。直す価値はあるが影響は小さい

最初のリクエスト(previous_message_id: null)は、診断が必ずnullで、読込も通常は0です。これは異常ではありません。cache_missed_input_tokensはバイト長からの推定値で、請求のトークン数ではありません。

ただし、比較の記録は短い時間で失効します。同じ組織・workspaceでないと見つからない、という制約もあります。2回の応答ヘッダーでanthropic-workspace-idを比べるのも有効です。BedrockやGoogle managedのClaudeでは、この診断は使えません。

手元で試す:最小の検証コード

仕様を読んだら、手を動かして確かめましょう。最初は信頼できる長文1つ、モデル1つ、API1つで始めます。水増しの文章で閾値を超えても、本番の効果の証明にはなりません。現実の用途に近い資料を使います。

以下はPython 3とrequestsを使う独自の検証例です。ぼくは実行していません。構文のチェックだけ通しています。実行すると有料のAPI呼び出しが発生し、Gemini明示では保存時間の課金も発生します。使うモデルIDは調査日のもので、実行前に利用可能かを確かめてください。

python3 -m pip install requests
# ANTHROPIC_API_KEY / GEMINI_API_KEYは環境変数で設定する
# corpus.txtに、自分が扱う固定資料(UTF-8)を置く

共通のヘルパーをcache_probe_common.pyとして保存します。エラーが出たらその場で止めて、自動リトライで有料呼び出しを増やしません。

import json
import time
import requests

def send(method, url, headers, body=None):
    started = time.perf_counter()
    response = requests.request(
        method, url, headers=headers, json=body, timeout=180
    )
    elapsed = time.perf_counter() - started
    response.raise_for_status()
    data = response.json() if response.content else {}
    print(json.dumps({
        "http_status": response.status_code,
        "elapsed_http_s": round(elapsed, 3),
        "request_id": response.headers.get("request-id"),
        "workspace_id": response.headers.get("anthropic-workspace-id"),
    }, ensure_ascii=False))
    return data

elapsed_http_sは応答全体の時間で、最初の出力までの時間(TTFT)ではありません。速度を比べるなら、ストリーミングで別に測ります。

Claude:固定資料に保存点を置いて、2回質問する

claude_cache_probe.pyです。資料が短いときは、モデルの最小長を満たす実際の資料に差し替えてください。

import copy
import json
import os
from pathlib import Path
from cache_probe_common import send

model = "claude-sonnet-5-5"
headers = {
    "x-api-key": os.environ["ANTHROPIC_API_KEY"],
    "anthropic-version": "2023-06-01",
    "Content-Type": "application/json",
}
system = [{
    "type": "text",
    "text": "以下の信頼済み資料を参照して答えてください。\n"
            + Path("corpus.txt").read_text(encoding="utf-8"),
    "cache_control": {"type": "ephemeral"},
}]
tools = [{
    "name": "lookup_section",
    "description": "資料内の節を探す。今回の検証では実行しない。",
    "input_schema": {
        "type": "object",
        "properties": {"section": {"type": "string"}},
        "required": ["section"],
    },
}]
previous_id = None
for question in ["資料の目的を短く教えて。", "資料の制約を短く教えて。"]:
    body = {
        "model": model,
        "max_tokens": 256,
        "tools": copy.deepcopy(tools),
        "tool_choice": {"type": "none"},
        "system": copy.deepcopy(system),
        "messages": [{"role": "user", "content": question}],
        "diagnostics": {"previous_message_id": previous_id},
    }
    data = send("POST", "https://api.anthropic.com/v1/messages", headers, body)
    print(json.dumps({
        "id": data.get("id"), "model": data.get("model"),
        "usage": data.get("usage"),
        "diagnostics": data.get("diagnostics"),
    }, ensure_ascii=False))
    previous_id = data["id"]

見るのは1回目の書込量と、2回目の読込量です。すでにキャッシュがあれば、1回目から読込になることもあります。今回は独立した質問なので、診断が質問の末尾の差分を報告することがあります。それでも、資料の保存点が読めていれば目的は達成です。書込も読込も0なら、資料の長さと、実際に送ったbodyを先に見ます。

Gemini明示:作成・参照・期限確認・削除

gemini_explicit_probe.pyです。長い固定資料をキャッシュに入れ、それを参照しながら違う2つの質問を送ります。作成に必要な下限はモデルごとに違うので、4,096を保証値としてコードに埋め込まず、エラーと最新の仕様で確かめます。

import json
import os
from pathlib import Path
from cache_probe_common import send

base = "https://generativelanguage.googleapis.com/v1beta"
model = "models/gemini-3.8-flash"
headers = {
    "x-goog-api-key": os.environ["GEMINI_API_KEY"],
    "Content-Type": "application/json",
}
fixed = Path("corpus.txt").read_text(encoding="utf-8")
cache = send("POST", f"{base}/cachedContents", headers, {
    "model": model,
    "displayName": "cache-probe-fixed-corpus",
    "systemInstruction": {"parts": [{"text": "資料に基づき簡潔に答える。"}]},
    "tools": [{"functionDeclarations": [{
        "name": "lookup_section",
        "description": "資料内の節を探す。今回の検証では実行しない。",
        "parameters": {"type": "OBJECT", "properties": {
            "section": {"type": "STRING"}
        }, "required": ["section"]},
    }]}],
    "toolConfig": {"functionCallingConfig": {"mode": "NONE"}},
    "contents": [{"role": "user", "parts": [{"text": fixed}]}],
    "ttl": "300s",
})
name = cache["name"]
print(json.dumps({
    "name": name, "model": cache.get("model"),
    "expireTime": cache.get("expireTime"),
    "usageMetadata": cache.get("usageMetadata"),
}, ensure_ascii=False))
try:
    for question in ["資料の目的を短く教えて。", "資料の制約を短く教えて。"]:
        data = send("POST", f"{base}/{model}:generateContent", headers, {
            "cachedContent": name,
            "contents": [{"role": "user", "parts": [{"text": question}]}],
            "generationConfig": {"maxOutputTokens": 256},
        })
        print(json.dumps({"usageMetadata": data.get("usageMetadata")},
                         ensure_ascii=False))
    current = send("GET", f"{base}/{name}", headers)
    print(json.dumps({"expireTime": current.get("expireTime")},
                     ensure_ascii=False))
finally:
    send("DELETE", f"{base}/{name}", headers)

作成応答のトークン数と、2回の生成応答のcachedContentTokenCountを別々に確認します。途中で失敗したら、出力したnameで削除できたかを確かめてください。保存期間を延ばすなら、PATCH /v1beta/{name}に{"ttl":"3600s"}のようなbodyを送ります。資料を変える更新ではなく、期限の変更です。

Gemini暗黙:同じ先頭を続けて送る

gemini_implicit_probe.pyです。明示のnameは付けず、固定資料を各回の先頭に置きます。この例はツールとsystemを省いているので、明示との費用・速度の直接比較には使えません。2回目も0になりえるので、「必ずヒット」を前提にしたassertは書きません。

import json
import os
from pathlib import Path
from cache_probe_common import send

base = "https://generativelanguage.googleapis.com/v1beta"
model = "models/gemini-3.8-flash"
headers = {"x-goog-api-key": os.environ["GEMINI_API_KEY"],
           "Content-Type": "application/json"}
fixed = Path("corpus.txt").read_text(encoding="utf-8")
for question in ["資料の目的を短く教えて。", "資料の制約を短く教えて。"]:
    data = send("POST", f"{base}/{model}:generateContent", headers, {
        "contents": [{"role": "user", "parts": [
            {"text": fixed}, {"text": question}
        ]}],
        "generationConfig": {"maxOutputTokens": 256},
    })
    usage = data.get("usageMetadata")
    print(json.dumps({"usageMetadata": usage}, ensure_ascii=False))

暗黙の最小長は3.8 Flashで4,096 tokensです。全体が長くても、共通の先頭が短ければ同じ比較になりません。事前にcountTokensで資料の長さを見ておきます。Interactionsを使っているなら、固定の先頭をinputにそろえるか、同じ設定でprevious_interaction_idをつないで、観測先をusage.total_cached_tokensに変えます。

1つずつ変えて、原因を確かめる

読込が当たったら、次は「何を変えると外れるか」。1変数ずつ変えて確かめます。複数を同時に変えると、原因がわからなくなります。

条件変えるものClaudeでの期待Geminiでの期待
A何も変えず、近い時間に再送読込の候補暗黙は候補。明示は同じ名前の読込を確認
B保存点より後ろの質問だけ変更固定部分を読み、質問は新規同じ先頭か同じ名前を使える
C固定資料の先頭1文字を変更その位置以降の一致を失う暗黙は共通の先頭を失う。明示は別の版を作る
Dtoolsの並び順だけ変更tools以降の一致を失いうる暗黙は同じ条件と見なされない。明示は新しい構成で作る
E毎回の時刻を、固定部分の先頭に入れる先頭から一致を失う暗黙の先頭の共通性を失う
F時刻を、保存点の後ろ・資料の後ろへ移す固定部分は維持される候補暗黙の候補が増える。保証ではない
Gモデルだけ切り替え別モデルのキャッシュ扱い明示は同じ名前を別モデルに使えない
H既定TTLを超える間隔で送る読込を失う候補(開始時刻が基準)暗黙に固定期限を当てはめない。明示はexpireTimeで判断
I冷えた状態で同時に送信書込が終わる前を疑う暗黙は保証なし。明示は作成の完了後に参照
J過去のtool resultを毎回整形し直すその位置から先頭が変わりうる暗黙は過去の先頭が変わりうる

この表は仕様から導いた検証の仮説です。ぼくがAPIで全ケースを実測した結果ではありません。固定資料の版を変えるCのあとに、AやBへ旧版を混ぜないように注意してください。

記録票を残す

1リクエスト1行で、次の項目を残すと、あとから診断しやすくなります。

取得日時UTC / request_id / provider / host / API / SDK・adapterの版
要求model / 応答model・version / tier / project・workspace・location
資料のversion・SHA256 / toolsの順序・スキーマのhash / system hash / 履歴のversion
保存点の位置・TTL / cache name・expireTime / 前回リクエスト開始との差
生のusage / cached項目の有無 / 書込量 / 読込量 / 通常入力
diagnostics / HTTPの結果 / 完了理由 / 全体時間 / TTFTを測ったか

APIキーや機密資料の本文は、共有のログに出さないでください。本文を保存できないときは、版・順序・fingerprint・最初に違った位置を残しておくと、診断しやすくなります。

判定は3段階に分けます。

段階合格の根拠
再利用を確認正しいAPIのusageで、期待した固定部分に対応する読込を見た。作成・期限・設定と矛盾しない
費用の減少を確認書込、保存時間、通常入力、出力も含めた同条件の比較で減った
実務で有効品質・完了率・運用の手間を崩さず、本番に近い再利用回数と間隔でも効果がある

「割引率」と「全体の削減率」を分ける

最後に、費用の話を少しだけ。キャッシュの割引率と、アプリ全体の削減率は別物です。

Claudeは、5分の書込が通常入力の1.25倍、1時間の書込が2倍です。読込の倍率はモデルによって違い、現行の料金表ではSonnet 5.5とOpus 5.5が0.05倍、Fable 5.1などが0.025倍、その他の標準が0.1倍です。「Claudeはぜんぶ90%引き」とは言えません。

同じ固定部分P tokensを、同じTTLの期間内にn回送るとします。1回書いて残りがぜんぶヒットし、ほかの条件も同じなら、固定部分の費用は次のとおりです。

  • キャッシュあり:P × [書込単価 + (n−1) × 読込単価]
  • キャッシュなし:P × n × 入力単価

これは条件つきの計算式で、ヒット率を予測する式ではありません。失敗した書込や期限切れが増えれば、別の計算になります。

Gemini明示では保存時間の費用も考えます。調査日の3.8 Flash(Standard・有料)の単価は、入力が$0.75、cached入力が$0.075、保存が$0.50(いずれも百万tokensあたり。保存は1時間あたり)で、2026年末までの期間条件が付いていました。この単価なら、百万tokensを1時間保存すると$0.50。読込1回あたりの差額は$0.675です。再利用が少ないまま長く保存すると、読込単価が安くても得とは限りません。

公式の「cost saving guarantee」という表現も、「対応した参照入力の割引」の説明として読みます。自分のアプリの総請求が必ず下がる、という約束ではありません。

この記事で確かめたこと・確かめていないこと

確かめたこと

  • 2026年10月26日に、Claude・Geminiの公式ドキュメントを取得し、最小入力・TTL・保存点・診断・usage・対応API/モデルを突き合わせた。Claudeの最小長・自動キャッシュの探索範囲・無効化の表・診断の値、Geminiの暗黙の最小入力・Argon以降の2,050・明示の非対応範囲は、公開直前にもう一度原文で見直した。
  • 記事のPythonコードは構文として解析できることを確認した。

確かめていないこと

  • 各APIへの有料リクエストと、実際のキャッシュ作成・ヒット率・TTL経過・並列の挙動。
  • コードのAPI接続・モデルへのアクセス・実際の応答。
  • Gemini明示キャッシュの、モデルごとの最小長。現行ページに数字が見つからなかったので、不明のままにしています。
  • BedrockやCloudの全地域・全モデルの組み合わせ、中継サービスやハーネスごとの挙動。
  • 実際の請求書、回答の品質、削減率。

モデル名、最小長、料金、期限は更新されます。実行する日の公式ページと、自分のAPIの応答を優先してください。

今日からやること

  1. 読込が0なら、手順1〜3(呼んでいるAPI、usageの読み方、方式の対応)を、生のJSONで確かめる。
  2. 手順4〜6で、保存対象の長さをcount_tokens/countTokensで数え、固定資料の末尾に保存点(Claude)か、参照する名前(Gemini明示)があるか見る。
  3. Claudeなら、最初のリクエストからdiagnosticsを付けて、cache_miss_reasonを見る。
  4. 毎回変わる値(時刻・ID・質問)を、固定部分の後ろへ動かす。
  5. 当たったら、費用の計算に進む。

当たらない原因は、たいてい地味です。先頭の1行の時刻、ツールの並び順、短すぎる資料。表を上から順に確かめれば、見つかります。

参考にした一次資料

いずれも2026年10月26日に取得しました。