OpenRouter отчитался о среднем cache hit rate по платформе в 82,8% (@OpenRouter). Но в обсуждениях пользователи показывают совсем другие цифры: менее 1% попаданий в кэш (@miolini) и счета, превышающие ожидания в 10–32 раза (r/openrouter). Кэширование промптов в OpenRouter действительно снижает стоимость входных токенов — но только если устранить четыре типовые причины промахов. Главный рычаг здесь — отправлять последовательные запросы одному и тому же прогретому провайдеру. И важное ограничение: если промпт не дотягивает до минимального порога токенов у провайдера, кэш не заработает ни при каких настройках.
Что OpenRouter считает попаданием в кэш
Кэширование промптов позволяет повторно использовать уже обработанный провайдером неизменный префикс запроса. Повторяющиеся входные токены тарифицируются со скидкой, а не по полной цене. Кэш существует на конкретном endpoint провайдера, который обработал первый запрос, поэтому маршрутизация здесь не менее важна, чем структура промпта. Не путайте этот механизм с кэшированием ответов: оно бесплатно возвращает результат только для полностью идентичного запроса, ещё до этапа роутинга.
| Кэширование промптов | Кэширование ответов | |
|---|---|---|
| Что переиспользуется | Стабильный префикс любого запроса | Побайтно идентичный запрос (SHA-256 нормализованного тела) |
| Как включить | В основном автоматически; cache_control для Anthropic, Qwen, Gemini | Заголовок X-OpenRouter-Cache: true или preset |
| Стоимость | Кэшированные токены по 0,1–0,5x от цены input | Попадания бесплатны, промахи тарифицируются обычно |
| Срок жизни | Обычно 3–5 мин, до 1 ч у Anthropic | По умолчанию 300 с, диапазон 1–86 400 с |
| Что ломает | Изменение префикса, смена провайдера, минимум токенов | Любое изменение JSON, ротация API-ключа, ZDR аккаунта |
Кэширование ответов особенно удобно для повторных попыток, unit-тестов и идентичных вызовов в агентных сценариях. Но учитывайте: порядок свойств в JSON входит в ключ кэша, поэтому даже безобидное изменение сериализации даст промах. Базовые принципы работы кэша на стороне провайдера описаны в руководстве OpenRouter по prompt caching:
Во сколько обходится prompt caching у разных провайдеров
Во всех случаях чтение из кэша стоит лишь часть обычной цены input. Но создание кэша иногда дороже стандартного ввода: у Anthropic это 1,25x при TTL 5 минут и 2x для варианта на 1 час. Экономия появляется, когда один префикс достаточно часто читается и окупает первоначальную запись. Для разового запроса кэширование может обойтись дороже, чем работа без него. По расчётам самого OpenRouter, у Claude Sonnet 4.6 кэшированный input стоит $0.30/M вместо $3.00/M для новых токенов.
Множители для записи и чтения кэша у провайдеров из того же источника:
| Провайдер | Запись в кэш | Чтение из кэша | Примечания |
|---|---|---|---|
| Anthropic | 1,25x (5 мин) / 2x (1 ч) | 0,1x | TTL можно выбрать для каждой точки останова |
| OpenAI, до GPT-5.6 | Бесплатно | 0,25–0,5x | Автоматически от 1 024 токенов |
| OpenAI GPT-5.6+ | 1,25x | 0,25–0,5x | Теперь поддерживаются явные точки останова |
| Google Gemini | Бесплатно | 0,25x | Неявно на 2.5+, TTL около 3–5 мин |
| Grok | Бесплатно | 0,25x | Автоматически |
| Moonshot | Бесплатно | 0,25x | Автоматически |
| Groq | Бесплатно | 0,5x | Только модели Kimi K2 |
| DeepSeek | 1,0x | 0,1x | Запись тарифицируется как обычный input |
| Alibaba Qwen | 1,25x | 0,1x | Нужен явный cache_control |
| Z.AI | Бесплатно | ~0,2x | Кэшированное хранение заявлено как временно бесплатное |
В примере OpenRouter рассматриваются 10 000 повторяющихся токенов на шести ходах: без кэша это 6,0x стоимости одного хода, с 5-минутным кэшем Anthropic и sticky routing — 1,75x, а у провайдера с бесплатной записью и чтением по 0,25x — 2,25x. В расчёте не учитываются растущие сообщения и output-токены.
Дорогая запись у Anthropic выигрывает уже на шести ходах: начиная со второго запроса доминирует чтение по 0,1x, а с ростом числа ходов разрыв увеличивается. Картина меняется, только если между запросами истекает 5-минутный TTL. Тогда за каждый запрос снова платится запись 1,25x — за шесть ходов получится 7,5x, что хуже отсутствия кэша. Провайдер с бесплатной записью и input по 1,0x в такой ситуации хотя бы остаётся на уровне 6,0x без кэширования.
Сначала измерьте: три поля, которые подтверждают попадание
Вердикт по каждому запросу уже есть в объекте usage: это cached_tokens, cache_write_tokens и cache_discount; значения полей описаны в гайде OpenRouter по кэшированию. Прежде чем что-то менять, посмотрите на эти три числа: так вы отличите настоящий cache miss от неожиданностей в тарификации. Если cached_tokens больше нуля, запрос попал в прогретый кэш. Если там ноль — не попал, независимо от того, что показывает Activity.
"usage": {
"prompt_tokens": 10339,
"prompt_tokens_details": {
"cached_tokens": 10318,
"cache_write_tokens": 0
}
}
Здесь hit rate составляет 99,8%: из 10 339 токенов промпта 10 318 взяты из кэша. cache_write_tokens появляется на первом запросе, который создаёт кэш. Поле cache_discount показывает сэкономленную сумму и при записи у Anthropic может быть отрицательным: наценка 1,25x реальна, а последующие чтения её компенсируют. Те же значения доступны в деталях generation в Activity — где именно, показано в нашем гайде по Activity dashboard — и через /api/v1/generation.
Истина — в сырых метаданных, а не в интерфейсе. Один пользователь SillyTavern долго искал несуществующую проблему с кэшем, пока не проверил логи напрямую:
"В сырых метаданных OpenRouter прямо указано
native_tokens_cached: 0[и]usage_cache: null." — u/HauntingWeakness
Если эти три показателя день за днём остаются нулевыми, кэш съедает одна из четырёх проблем ниже.
Четыре причины, по которым прогретый кэш остывает
Документация OpenRouter и опыт сообщества сходятся в четырёх наиболее частых причинах обвала hit rate: промпт меньше минимального порога, TTL истёк между ходами, изменился префикс либо роутер переключил провайдера. У каждой проблемы свой след в логах и свой способ исправления.
1. Промпт не достигает минимального порога
Провайдеры с поддержкой prompt caching устанавливают для моделей свой минимум токенов. Системный промпт на 900 токенов не закэшируется ни на одной модели Claude. Набивать его бессмысленным текстом тоже не стоит: в туториале OpenRouter это прямо не рекомендуют — «не дополняйте запрос пустым текстом только ради принудительного кэширования». Минимумы по каталогу различаются в четыре раза:
Согласно заметкам 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-примеры, в единый префикс либо перейти на модель с меньшим порогом.
2. Кэш истёк между запросами
Стандартный кэш Anthropic живёт 5 минут, а TTL на 1 час требует записи по 2x. Неявный кэш Gemini сохраняется примерно 3–5 минут, и, что принципиально, чтения не продлевают таймер — об этом говорится в туториале OpenRouter. Sticky-сессия, удерживающая запросы у одного провайдера, завершается после 10 минут неактивности. Агентные циклы, которые думают по 5–6 минут между вызовами, выходят за все эти окна:
"OpenRouter отлично подходит для тестирования моделей. Но для production-агентов он незаметно оказывается ужасен. Грязный секрет? В реальных нагрузках кэширование фактически равно нулю." — @ran_cohenn, о 5–6-минутных интервалах агента, после которых истекает sticky affinity и приходят полные cache miss вместе с дорогими записями кэша
TTL Anthropic на 1 час с записью по 2x выгоднее, чем заново платить 1,25x каждые пять минут, если сессия продолжается в пределах часа. Но при двадцатиминутных паузах пользователя ни один доступный TTL не выживает, и кэш помогает лишь внутри плотной серии ходов.
3. У вас изменился префикс
Стандартный conversation key OpenRouter строится как хэш первого системного сообщения и первого несистемного сообщения. Любое изменение начала промпта инвалидирует кэш с этой точки. Самые частые виновники: RAG-контекст, вставленный перед системным промптом, timestamps или request ID в первом сообщении, пересобираемые при каждом вызове определения инструментов и фронтенд-чаты, добавляющие сообщения в середину истории.
"Частота cache miss будет расти, если что-то в начале промпта постоянно меняется." — u/Exact_Law_6489
Иногда изменения вносит инструмент, который вы не писали. «Я заметил, что Claude Code создаёт проблемы с cache hit; думаю, дело в том, как он внедряет инструменты», — сообщает u/askchris. У Gemini есть ещё две ловушки: OpenRouter использует только последний переданный breakpoint cache_control, а системная инструкция рассматривается как неизменяемый кэшируемый контент. Динамические данные нужно переносить в более позднее пользовательское сообщение, а не добавлять после system prompt. Во всех случаях работает одна дисциплина: сначала статичный системный промпт, схемы инструментов и документы; изменяемые от запроса к запросу данные — в конце.
4. Запрос ушёл к непрогретому провайдеру
OpenRouter маршрутизирует запросы между 70+ провайдерами (по данным его туториала), а кэш промпта локален для endpoint, на котором он был создан. Sticky routing возвращает последующие запросы к прогретому провайдеру, но лишь когда чтение кэша у него дешевле обычного input. Ручной provider.order полностью переопределяет эту привязку. Ошибка провайдера также снимает pin.
Данные сообщества по этой проблеме показательны:
- @bruceforai проверил одну и ту же модель у разных провайдеров и получил hit rate от 95,3% до 0%; у части сторонних провайдеров цена кэша оказалась в 10 раз выше официальной.
- @Bryan_1269 получил очень низкий hit rate для GLM 5.2 через OpenRouter и 85%+ на идентичном промпте при прямом обращении через Fireworks.
- @miolini о роутинге через OpenRouter: "cache hit rate действительно плохой, меньше 1%."
Официальная позиция OpenRouter: pinning работает — «когда модель или провайдер вас закэшировал, вы закрепляетесь за ним до истечения кэша» (@OpenRouter). Это соответствует документации: управлять нужно вариативностью провайдеров, а не пытаться исправить сам механизм закрепления.
Куда ставить cache_control и что может его удалить
На моделях Anthropic в OpenRouter доступны два режима кэширования. Первый — единый объект cache_control верхнего уровня, который автоматически продвигается по мере роста диалога; OpenRouter рекомендует его для многоходовых чатов. Второй — явные breakpoints на отдельных content-блоках, максимум четыре. Они подходят для большого неизменного контента: схем инструментов, RAG-документов, CSV-выгрузок или character cards. Вариант верхнего уровня работает с Anthropic native, Vertex, Azure и Bedrock: OpenRouter преобразует его в завершающий breakpoint, поскольку API Bedrock не принимает это поле на верхнем уровне. Явный TTL можно задать через Chat Completions или Anthropic Messages API, но не через Responses.
{
"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 становится breakpoint OpenAI, breakpoint 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_
Проверьте, что маркер доходит до сервиса: посмотрите сырые метаданные запроса в деталях generation в Activity или отправьте один тестовый запрос через curl, исключив промежуточные звенья. Инструмент, который сворачивает сообщения в единый текстовый блок, уничтожит breakpoints даже при идеальном размещении. В репозитории примеров OpenRouter есть готовые образцы для TypeScript, Vercel AI SDK и Effect, сохраняющие маркеры без потерь.
Как закрепить провайдера: session_id и provider order
Самый сильный рычаг для маршрутизации — стабильный идентификатор сессии. session_id закрепляет последующие запросы за провайдером, который обработал первый успешный запрос, ещё до первого обнаруженного попадания в кэш. Без него sticky-привязка начинается лишь после первого cache hit. Идентификатор по умолчанию — хэш первого системного и первого несистемного сообщения — незаметно меняется при любой мутации префикса из третьей проблемы, согласно документации OpenRouter по роутингу.
{
"model": "anthropic/claude-sonnet-4.6",
"session_id": "user-8801-thread-3",
"messages": [ ... ]
}
Несколько деталей: session_id передаётся в теле запроса либо в заголовке x-session-id. Если указаны оба, приоритет у тела. Максимальная длина — 256 символов. Если нет ни одного из них, OpenRouter использует OpenAI-совместимый prompt_cache_key.
В документации есть две оговорки: ошибка провайдера снимает закрепление, а строки Batch API выполняются параллельно и не по порядку. Поэтому запись кэша из одной строки не будет видна следующей. Используйте общий префикс "ttl": "1h" для batch-запросов либо сначала прогрейте кэш одним синхронным вызовом. (О том, как Auto Router пытается переиспользовать разрешённую модель, читайте в гайде по Auto Router.)
Если одного pinning недостаточно, ограничьте список провайдеров напрямую:
"Решение, которое я нашёл: задать предпочтительный список провайдеров в порядке приоритета." — u/nabil9506
Список provider.order из двух-трёх провайдеров с недорогим чтением кэша жертвует широтой failover ради локальности кэша. Для агентных нагрузок это разумный компромисс. u/welcome_to_milliways называет необходимость ручной настройки «довольно фундаментальным недостатком OR». Согласны вы с этим или нет, сейчас контракт работает именно так.
Когда кэширование через роутер не окупается
Кэширование промптов через OpenRouter перестаёт быть выгодным в трёх узнаваемых случаях: промпт не достигает токенного порога модели, паузы в сессии длиннее любого доступного TTL или разовый запрос не успевает окупить повышенную стоимость записи скидкой на чтение. Есть и четвёртый случай — если вы не можете изменить инструмент, который удаляет cache_control до того, как он попадёт в роутер. @grapeot хорошо формулирует масштаб проблемы: когда кэш ломается на уровне gateway, разница в цене достигает порядка величины и перекрывает комиссию за роутинг.
Для нагрузок, где кэш критичен и ни одно из исправлений не применимо, фиксированный upstream лучше роутера: поведение кэша детерминировано, а pinning не нужно администрировать. Прямой Claude API endpoint с нативным кэшированием Anthropic — очевидный запасной вариант, когда дрейф провайдеров невозможно устранить.
Zero Data Retention на уровне аккаунта полностью отключает кэширование ответов. Для prompt caching при ZDR ориентируйтесь на разбор OpenRouter о том, считается ли implicit caching хранением данных.
В каком порядке исправлять проблему
Чтобы вернуть большую часть экономии с минимальным числом изменений, отлаживайте по порядку измерений: сначала подтверждение, затем путь от промпта к маршрутизации и TTL.
| # | Действие | Что выясняет |
|---|---|---|
| 1 | Посмотреть cached_tokens и cache_discount в нескольких реальных запросах | Проблема в hit rate или в ожиданиях от цены |
| 2 | Сопоставить размер промпта с токенным порогом модели | Исключает класс «никогда не кэшируется» до любых других действий |
| 3 | Зафиксировать префикс: сначала статичный system prompt, схемы и документы; timestamps и RAG — в конце | Убирает тихую инвалидацию кэша |
| 4 | Передавать session_id в каждом запросе одного диалога | Закрепляет провайдера с первого хода, а не после первого попадания |
| 5 | Задать в provider.order двух-трёх провайдеров с дешёвым чтением кэша | Убирает дрейф между провайдерами |
| 6 | Добавить "ttl": "1h" для Anthropic или перейти на провайдера с бесплатной записью для длинных сессий | Решает проблему истечения кэша между ходами |
Шаги 1–3 закрывают классы проблем, которые вы контролируете в коде. Шаги 4–6 объясняют, почему наряду с заявленными 82,8% встречаются отчёты о менее чем 1%. По теме также пригодятся гайд по ценам OpenRouter — о том, как кэшированные токены попадают в счёт, гайд по Auto Router — о закреплении моделей, и гайд по Activity dashboard — для мониторинга hit rate со временем.