AIREITER

Кэширование промптов в OpenRouter: почему кэш не срабатывает

Последнее обновление: 2026-08-22 01:33:27

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:

Страница документации OpenRouter по кэшированию промптов

Во сколько обходится prompt caching у разных провайдеров

Во всех случаях чтение из кэша стоит лишь часть обычной цены input. Но создание кэша иногда дороже стандартного ввода: у Anthropic это 1,25x при TTL 5 минут и 2x для варианта на 1 час. Экономия появляется, когда один префикс достаточно часто читается и окупает первоначальную запись. Для разового запроса кэширование может обойтись дороже, чем работа без него. По расчётам самого OpenRouter, у Claude Sonnet 4.6 кэшированный input стоит $0.30/M вместо $3.00/M для новых токенов.

Множители для записи и чтения кэша у провайдеров из того же источника:

ПровайдерЗапись в кэшЧтение из кэшаПримечания
Anthropic1,25x (5 мин) / 2x (1 ч)0,1xTTL можно выбрать для каждой точки останова
OpenAI, до GPT-5.6Бесплатно0,25–0,5xАвтоматически от 1 024 токенов
OpenAI GPT-5.6+1,25x0,25–0,5xТеперь поддерживаются явные точки останова
Google GeminiБесплатно0,25xНеявно на 2.5+, TTL около 3–5 мин
GrokБесплатно0,25xАвтоматически
MoonshotБесплатно0,25xАвтоматически
GroqБесплатно0,5xТолько модели Kimi K2
DeepSeek1,0x0,1xЗапись тарифицируется как обычный input
Alibaba Qwen1,25x0,1xНужен явный cache_control
Z.AIБесплатно~0,2xКэшированное хранение заявлено как временно бесплатное

В примере OpenRouter рассматриваются 10 000 повторяющихся токенов на шести ходах: без кэша это 6,0x стоимости одного хода, с 5-минутным кэшем Anthropic и sticky routing — 1,75x, а у провайдера с бесплатной записью и чтением по 0,25x — 2,25x. В расчёте не учитываются растущие сообщения и output-токены.

Сравнение относительной стоимости input для 10 000 токенов за шесть ходов при четырёх вариантах кэширования

Дорогая запись у 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 со временем.