AIREITER

OpenRouter Prompt Cachingでキャッシュが当たらない理由

最終更新日: 2026-08-22 01:31:24

OpenRouterはダッシュボード公開時、プラットフォーム全体のキャッシュヒット率が82.8%だと報告しました(@OpenRouter)。一方、コミュニティではヒット率1%未満(@miolini)、想定の10〜32倍に膨らんだ請求額(r/openrouter)という声も目立ちます。OpenRouterのPrompt Cachingは入力コストを確かに下げられますが、効果を出すには4つの典型的な失敗を潰す必要があります。なかでも効くのは、連続するリクエストを同じウォーム状態のプロバイダーへ送り続けることです。まず押さえるべき制約として、プロバイダーの最低トークン数に届かないプロンプトは、どんな設定をしてもキャッシュされません。

OpenRouterで「キャッシュヒット」になる条件

Prompt Cachingは、プロバイダーがすでに処理した安定的なプロンプト先頭部分を再利用する仕組みです。同じ入力トークンを繰り返し送る際、通常の入力料金ではなく割引価格で課金されます。ただしキャッシュは、最初のリクエストを処理した特定のプロバイダーエンドポイントに紐付きます。プロンプトの構造だけでなく、どこへルーティングされるかが重要なのはこのためです。これは、ルーティング前に完全に同一のリクエストを無料で返すresponse cachingとは別レイヤーの機能です。

Prompt CachingResponse Caching
再利用するものあらゆるリクエストの安定した先頭部分バイト単位で同一のリクエスト(正規化済みbodyのSHA-256)
有効化方法大半は自動。Anthropic、Qwen、Geminiではcache_controlを使用X-OpenRouter-Cache: trueヘッダーまたはプリセット
料金キャッシュ済みトークンは入力料金の0.1〜0.5倍ヒットは無料、ミスは通常課金
保持期間通常3〜5分、最長1時間(Anthropic)既定300秒、1〜86,400秒の範囲
ヒットを妨げる要因先頭部分の変更、プロバイダー切替、最低トークン数未満JSONの変更、APIキーのローテーション、アカウントのZDR

Response Cachingが特に役立つのは、リトライ、ユニットテスト、エージェントワークフローでの完全に同一な呼び出しです。JSONのプロパティ順もキャッシュキーに含まれるため、シリアライズ時の小さな違いでもミスになります。プロバイダー側の仕組みについては、OpenRouterのPrompt Cachingガイドが基準となるドキュメントです。

OpenRouter Prompt Cachingのドキュメントページ

OpenRouter Prompt Cachingの料金体系をプロバイダー別に見る

キャッシュ読み取りはどのプロバイダーでも通常の入力料金より安価です。ただし、キャッシュを作成する書き込みには割増料金がかかる場合があります。Anthropicでは、既定の5分TTLで通常入力の1.25倍、1時間オプションでは2倍です。同じ先頭部分を十分な回数読み直して初めて書き込みコストを回収できるため、単発リクエストではキャッシュを使わないほうが安いことすらあります。OpenRouterが示した計算例では、Claude Sonnet 4.6のキャッシュ済み入力は$0.30/Mで、通常入力の$3.00/Mに対して大幅に低くなります。

同じ資料に基づく、プロバイダー別の書き込み・読み取り倍率は以下のとおりです。

プロバイダーキャッシュ書き込みキャッシュ読み取り備考
Anthropic1.25倍(5分)/ 2倍(1時間)0.1倍ブレークポイントごとにTTLを選択可能
OpenAI、GPT-5.6以前無料0.25〜0.5倍1,024トークンから自動適用
OpenAI GPT-5.6以降1.25倍0.25〜0.5倍明示的なブレークポイントに対応
Google Gemini無料0.25倍2.5以降は暗黙的に適用、TTLは約3〜5分
Grok無料0.25倍自動適用
Moonshot無料0.25倍自動適用
Groq無料0.5倍Kimi K2モデルのみ
DeepSeek1.0倍0.1倍書き込みは通常入力として課金
Alibaba Qwen1.25倍0.1倍明示的なcache_controlが必要
Z.AI無料約0.2倍キャッシュ保存は期間限定で無料と記載

OpenRouterのチュートリアルでは、10,000トークンの繰り返し部分を6ターン使うケースを比較しています。キャッシュなしなら単一ターンの6.0倍、Anthropicの5分キャッシュとスティッキールーティングを使うと1.75倍、書き込み無料かつ読み取り0.25倍のプロバイダーでは2.25倍です。この試算には、増加するメッセージと出力トークンは含まれません。

4種類のキャッシュ構成における6ターン・10,000トークンの相対入力コスト

Anthropicは書き込みが高価でも、2ターン目以降の読み取りが0.1倍になるため、6ターンでは有利になります。ターンが増えるほど差は広がります。ただし、各ターンの間に5分TTLが切れると事情は逆です。毎回1.25倍の書き込みを払い直すことになり、6ターンで7.5倍となって、キャッシュなしより高くなります。一方、書き込み無料のプロバイダーなら、入力料金1.0倍でキャッシュなしの6.0倍と同額にとどまります。

調査の前に確認したい、ヒットを示す3つの数値

OpenRouterのレスポンスでは、usageオブジェクト内のcached_tokens、cache_write_tokens、cache_discountが判定材料になります。各フィールドの意味はOpenRouterのキャッシュガイドに記載されています。設定を変える前にこの3項目を読めば、本当のキャッシュミスなのか、料金の想定違いなのかを切り分けられます。cached_tokensが0より大きければウォームキャッシュにヒットしています。Activityダッシュボードがどう表示していても、0ならヒットしていません。

"usage": {
  "prompt_tokens": 10339,
  "prompt_tokens_details": {
    "cached_tokens": 10318,
    "cache_write_tokens": 0
  }
}

このレスポンスのヒット率は99.8%です。10,339個のプロンプトトークンのうち10,318個がキャッシュから供給されています。cache_write_tokensは最初にキャッシュを生成したリクエストで現れます。cache_discountは節約額を示しますが、Anthropicの書き込みでは負になることがあります。1.25倍の書き込みプレミアムは実コストであり、後続の読み取りで回収するものだからです。これらの数値はActivityの生成詳細画面からも、/api/v1/generationからも取得できます。Activity上での場所はActivityダッシュボードの解説を参照してください。

判断基準にすべきなのはUIではなく生のメタデータです。SillyTavernのあるユーザーは、ログを直接確認するまで存在しないキャッシュ問題を追い続けていました。

「OpenRouterの生メタデータには、はっきりnative_tokens_cached: 0、usage_cache: nullと出ている。」— u/HauntingWeakness

この3つの数値が何日見てもゼロなら、以下の4つの失敗パターンのどれかがキャッシュを食い潰しています。

ウォームキャッシュが効かなくなる4つのパターン

OpenRouterのドキュメントとコミュニティの報告を見ると、ヒット率低下の主因は4つに集約されます。最低トークン数未満、ターン間でのTTL切れ、先頭部分の変更、そしてプロバイダーの切り替わりです。ログ上の兆候も、対処法もそれぞれ異なります。

1. プロンプトが最低トークン数に届いていない

Prompt Cachingに対応するプロバイダーには、モデルごとに最低トークン数があります。たとえば900トークンのシステムプロンプトは、どのClaudeモデルでもキャッシュされません。また、無意味な文章を足して閾値を越えさせる方法は明確に推奨されていません。OpenRouterのチュートリアルも「無理にキャッシュさせるため、フィラーテキストでリクエストを水増ししないこと」としています。カタログ内の必要量には最大4倍の開きがあります。

モデルファミリー別のキャッシュ可能な最小プロンプトサイズ

OpenRouterのプロバイダーノートによると、Claude Opus 4.5〜4.8およびHaiku 4.5では、何かをキャッシュする前に4,096トークンが必要です。Sonnet 4/4.5/4.6とOpus 4/4.1は1,024トークン、Gemini 2.5 Proは4,096トークン、Gemini 2.5 Flashは1,024トークン、OpenAIモデルも1,024トークンからキャッシュされます。短いプロンプトをOpus 4.8へ送るワークロードは、構造的にキャッシュ不可能です。静的な情報、たとえばツールスキーマ、参照ドキュメント、few-shot例を1つの先頭部分にまとめるか、より低い閾値のモデルへ切り替える必要があります。

2. ターンの間にキャッシュが失効している

Anthropicの既定キャッシュは5分間保持されます。1時間TTLは書き込みが2倍になります。Geminiの暗黙キャッシュは約3〜5分で、重要なのは読み取りでタイマーがリセットされないことです。これはOpenRouterのチュートリアルでも明記されています。同一プロバイダーに送り続けるスティッキーセッションも、10分間操作がなければ切れます。呼び出しの間に5〜6分考えるエージェントループは、これらの時間枠をすべて超えやすくなります。

「OpenRouterはモデルのテストには素晴らしい。しかし本番エージェントにはひそかにひどい。厄介な真実は、実ワークロードではキャッシュが実質ゼロになることだ。」— @ran_cohenn。5〜6分間隔のエージェントではスティッキーアフィニティが失効し、完全なキャッシュミスと高額なキャッシュ書き込みに着地すると説明

セッションが1時間以内に継続するなら、Anthropicの1時間TTL・2倍書き込みは、5分ごとに1.25倍を書き直すより有利です。ただしユーザーの間隔が20分なら、用意されているどのTTLでも維持できません。キャッシュが役立つのは、短時間に連続するターンの中だけです。

3. プロンプトの先頭が変わっている

OpenRouterの既定の会話キーは、最初のsystemメッセージと最初の非systemメッセージのハッシュから作られます。プロンプトの冒頭が変われば、その時点以降のキャッシュは無効です。よくある原因は、systemプロンプトより前にRAGコンテキストを挿入すること、最初のメッセージにタイムスタンプやリクエストIDを埋め込むこと、ツール定義を呼び出しごとに作り直すこと、フロントエンドのチャットアプリが履歴の途中へメッセージを差し込むことです。

「プロンプトの先頭にある何かが常に変化していると、キャッシュミス率は上がる。」— u/Exact_Law_6489

変更を加えているのは、自分で書いたコードとは限りません。「Claude Codeがキャッシュヒットの問題を起こしていると分かった。ツールを注入する方法が原因だと思う」とu/askchrisは報告しています。Geminiにはさらに2つの注意点があります。OpenRouterが使うのは送信したcache_controlブレークポイントのうち最後のものだけです。また、system instructionは変更不可のキャッシュ対象として扱われます。動的な情報はsystemプロンプトの後ろに追加せず、後続のuserメッセージへ移す必要があります。対策は共通です。静的なsystemプロンプト、ツールスキーマ、参照資料を先に置き、リクエストごとに変わる情報を最後に置きます。

4. コールド状態のプロバイダーへ振り分けられた

OpenRouterは70以上のプロバイダーへルーティングします(同社チュートリアルより)。そしてPrompt Cacheは、それを書き込んだエンドポイントにローカルなものです。スティッキールーティングは後続リクエストをウォーム状態のプロバイダーへ戻しますが、それが機能するのは、そのプロバイダーのキャッシュ読み取りが通常入力より安い場合に限られます。また、手動でprovider.orderを指定するとスティッキー設定は完全に上書きされます。プロバイダーエラーでもピンは解除されます。

この問題に関するコミュニティのデータははっきりしています。

  • @bruceforaiは、同じモデル名でもプロバイダーによってキャッシュヒット率が95.3%から0%まで変わることを測定しました。一部のサードパーティー製キャッシュ料金は公式料金の10倍でした。
  • @Bryan_1269は、OpenRouter経由のGLM 5.2では極めて低いヒット率だった一方、同一プロンプトをFireworksへ直接送ると85%超になったと報告しています。
  • @mioliniはOpenRouter経由のルーティングについて、「キャッシュヒット率は本当に悪く、1%未満のようだ」と述べています。

OpenRouterの公式見解は、ピン留め自体は維持されるというものです。「モデルまたはプロバイダーでキャッシュされると、キャッシュが期限切れになるまでそこへピン留めされる」(@OpenRouter)。これはドキュメントの説明とも一致します。つまり、管理すべきなのはピン留めの有無というより、プロバイダー間のばらつきです。

cache_controlはどこに置くべきか。途中で消えるケースにも注意

OpenRouter上のAnthropicモデルには、2つのキャッシュ指定方法があります。1つは会話が伸びるにつれて自動で進むトップレベルのcache_controlオブジェクトで、OpenRouterはマルチターンチャットではこちらを推奨しています。もう1つは個別のコンテンツブロックに明示的なブレークポイントを付ける方法です。最大4個まで置けるため、ツールスキーマ、RAGドキュメント、CSVダンプ、キャラクターカードのような大きな固定素材に向いています。トップレベル形式はAnthropic native、Vertex、Azure、Bedrockで使えます。Bedrock APIがトップレベルフィールドを受け付けないため、OpenRouterが末尾のブレークポイントへ変換します。TTLを明示するにはResponsesではなく、Chat CompletionsまたはAnthropic Messages APIを使う必要があります。

{
  "role": "system",
  "content": [
    {
      "type": "text",
      "text": "<20k tokens of tool schemas and reference docs>",
      "cache_control": { "type": "ephemeral", "ttl": "1h" }
    }
  ]
}

OpenAIは仕組みが異なります。1,024トークンから自動でキャッシュされ、明示的なprompt_cache_breakpointマーカーが使えるのはGPT-5.6以降だけです。input_textまたはtextブロックに設定し、TTLを指定する場合の最短値は30分です。

プロバイダーノートによれば、OpenRouterは各社の記法を変換します。Anthropicのcache_controlマーカーはOpenAIのブレークポイントに、OpenAIのブレークポイントは既定5分のAnthropicマーカーに変換されます。ただしTTL値は引き継がれません。Qwenでは明示的なcache_controlマーカーが必要で、キャッシュ期間は5分です。対応するのも一部モデルだけです。qwen3-max、qwen-plus、qwen3-coder-plusなどが対象で、qwen3.5-plus-02-15のようなスナップショットは対象外です。

見落とされやすい失敗もあります。アプリとOpenRouterの間にあるクライアントやゲートウェイが、非標準フィールドを転送前に削除してしまうケースです。

「ゲートウェイの背後でAnthropic Prompt Cachingがゼロに落ちるのは、通常マーシャリングのバグだ。……cache_controlマーカーがOpenRouterへ転送される前に黙って削られている。スキーマ拡張を落としたままではプロバイダーを抽象化できない。」— @SiddharthInk_

マーカーが届いているか確認してください。Activityの生成詳細で生のリクエストメタデータを確認するか、途中に何も挟まないcurlでテストリクエストを1件送ります。メッセージを1つの巨大な塊に平坦化するツールでは、どれほど正しくブレークポイントを置いても意味がありません。OpenRouterのexamples repoには、マーカーを保ったまま使えるTypeScript、Vercel AI SDK、Effectの実行可能なサンプルがあります。

session_idとprovider.orderでプロバイダーを固定する

ルーティングで最も強力なのは安定したセッションIDです。session_idを指定すると、最初に成功したリクエストを処理したプロバイダーへ、後続リクエストを固定できます。まだキャッシュヒットを観測していない段階から有効です。指定しない場合、スティッキー状態が始まるのは最初のキャッシュヒット検出後になります。既定の識別子は最初のsystemメッセージと最初の非systemメッセージのハッシュなので、先頭部分が変われば知らないうちに別の値へ切り替わります。これは失敗パターン3そのものです。詳細はOpenRouterのルーティングドキュメントを参照してください。

{
  "model": "anthropic/claude-sonnet-4.6",
  "session_id": "user-8801-thread-3",
  "messages": [ ... ]
}

仕様面では、session_idはリクエストbodyまたはx-session-idヘッダーに指定します。両方にある場合はbodyが優先されます。最大256文字で、どちらもなければOpenRouterはOpenAI形式のprompt_cache_keyへフォールバックします。

ドキュメントには2つ注意点があります。プロバイダーエラーが起きるとピンは解除されます。また、Batch APIの各行は並行かつ順不同で実行されるため、ある行が書き込んだキャッシュを次の行が参照できません。バッチ間で"ttl": "1h"の先頭部分を共有するか、先に同期リクエストを1件送ってウォームアップしてください。なお、Auto Routerが解決済みモデルをベストエフォートで再利用する仕組みは、Auto Routerガイドで解説しています。

ピン留めだけでは足りない場合は、使うプロバイダーを明示的に絞り込む方法があります。

「私が見つけた解決策は、優先順位順に使用するプロバイダーの優先リストを設定することだ。」— u/nabil9506

キャッシュ読み取りが安いプロバイダーを2〜3社に絞ったprovider.orderリストは、フェイルオーバーの広さと引き換えにキャッシュの局所性を高めます。エージェントワークロードでは妥当なトレードオフです。u/welcome_to_milliwaysは手動設定の負担を「ORのかなり根本的な欠陥」と呼んでいます。評価はともかく、現時点でこれがOpenRouterの運用上の前提です。

ルーター経由のキャッシュを諦めるべき場面

OpenRouter経由のPrompt Cachingが採算に合わなくなる状況は、見分けやすく3つあります。プロンプトがモデルの最低トークン数へ届かない場合、セッション間隔が利用可能なすべてのTTLより長い場合、そして書き込みプレミアムを割引読み取りで回収できない単発リクエストです。加えて4つ目として、cache_controlがルーターへ届く前に削除される、変更不能なツールを使っている場合もあります。@grapeotが指摘するように、ゲートウェイ層でキャッシュが失敗すると、ルーティング手数料など比較にならないほど、コスト差は1桁規模に達します。

キャッシュが重要なワークロードで、ここまでの対策がどれも使えないなら、ルーターよりも単一の固定アップストリームが向いています。キャッシュの挙動が決定的になり、ピン留めを管理する必要もありません。プロバイダーの切り替わりを防げない場合は、Anthropic自身のキャッシュを利用するClaude APIの直接エンドポイントが素直な逃げ道です。

アカウントレベルのZero Data RetentionではResponse Cachingが完全に無効になります。ZDR環境でのPrompt Cachingについては、暗黙的キャッシュがデータ保持に該当するかを扱ったOpenRouterの分析記事を確認してください。

キャッシュを復旧させるための修正順

手戻りを抑えて節約効果を取り戻すには、測定から始め、プロンプト、ルーティング、TTLの順で下流へ進むのが効率的です。

#実施すること判断できること
1実際のリクエストを数件取り、cached_tokensとcache_discountを確認するヒット率の問題か、料金想定の問題か
2プロンプトサイズをモデルの最低トークン数と比較する何をしてもキャッシュできないケースを最初に除外する
3先頭部分を固定する。静的なsystemプロンプト、スキーマ、資料を先に置き、タイムスタンプとRAGを後ろに置く気付きにくい無効化パターンを排除する
4会話内の全リクエストでsession_idを渡す最初のヒット後ではなく、1ターン目からプロバイダーを固定する
5provider.orderを、キャッシュ読み取りが安い2〜3社に設定するプロバイダー間のドリフトを防ぐ
6"ttl": "1h"(Anthropic)を追加するか、長時間セッションでは書き込み無料のプロバイダーへ切り替えるターン間での失効に対処する

手順1〜3はコード側で制御できる失敗原因を取り除きます。手順4〜6まで進めると、ヒット率82.8%という見出しと、1%未満という報告の差も説明できます。関連情報として、キャッシュ済みトークンが請求額にどう反映されるかはOpenRouter料金ガイド、モデル固定の挙動はAuto Routerガイド、ヒット率を継続監視する方法はActivityダッシュボードガイドを参照してください。