Кеширование промптов
Снижайте стоимость входных токенов сверх половины цены: используйте кеширование промптов каждого вендора, а Half Off Dev будет возвращать каждый разговор на канал, где лежит его кеш.
Половина цены — лишь отправная точка. Кеширование промптов снижает стоимость входных токенов гораздо сильнее, если вы повторно отправляете один и тот же префикс: длинные чаты, ассистенты для программирования, поиск по фиксированному набору документов. Модель, недавно обработавшая префикс, может переиспользовать эту работу, а переиспользованные токены тарифицируются по цене чтения кеша. Для claude-sonnet-5.5 это $0.05 за миллион токенов против $1.00 за входные токены без кеша.
Кеширование через Half Off Dev держится на двух составляющих:
- Собственное кеширование вендора, которое используется ровно так, как описано в документации вендора. Поля вроде
cache_controlиprompt_cache_keyпересылаются без изменений. - Привязка разговора к каналу. Кеш существует только на канале, который его записал. Шлюз узнаёт последовательные реплики одного разговора и отправляет их на тот же канал, поэтому к следующей реплике кеш всё ещё на месте.
Claude: отметьте, что кешировать#
Claude кеширует только те части, которые вы отметили. Добавьте cache_control к последнему блоку стабильного префикса (определения инструментов, системный промпт, предыдущие реплики). Допускается до четырёх точек разрыва, либо один cache_control верхнего уровня кеширует всё до последнего блока.
response = client.messages.create(
model="claude-sonnet-5.5",
max_tokens=1024,
system=[{
"type": "text",
"text": PRODUCT_MANUAL,
"cache_control": {"type": "ephemeral"},
}],
messages=chat_history,
)
print(response.usage)В usage приходят cache_creation_input_tokens (записано, тарифицируется по цене записи кеша) и cache_read_input_tokens (прочитано, тарифицируется по цене чтения кеша). Префиксы короче минимума модели не кешируются. О том, где ставить точки разрыва и что сбрасывает кеш, рассказано в руководстве Anthropic по кешированию.
GPT: автоматически, с необязательным ключом#
Модели GPT кешируют длинные префиксы сами, так что отмечать ничего не нужно. Ставьте стабильное содержимое в начало, а изменяющееся в конец. prompt_cache_key, который остаётся одним и тем же для разговора или пользователя, повышает долю попаданий, и шлюз использует то же значение, чтобы держать разговор на одном канале.
response = client.chat.completions.create(
model="gpt-6.1-sol",
messages=chat_history,
prompt_cache_key="thread_51c7d0",
)
print(response.usage.prompt_tokens_details.cached_tokens)usage.prompt_tokens_details.cached_tokens тарифицируется по цене чтения кеша, остальная часть prompt_tokens — по цене входных токенов. Отдельной платы за запись кеша для моделей GPT нет. Подробности в руководстве OpenAI по кешированию.
Gemini: неявное кеширование#
Модели Gemini кешируют повторяющиеся префиксы автоматически. Кешированные токены отражаются в usageMetadata.cachedContentTokenCount и тарифицируются по цене чтения кеша, без платы за запись. Явное кеширование контекста через API cachedContents не предоставляется.
Как реплики сопоставляются с разговором#
У каждого разговора есть отпечаток, который берётся из первого присутствующего источника из списка:
| Источник | Формат API |
|---|---|
заголовок x-session-id | Все |
заголовок x-halfoff-session | Все |
заголовок x-claude-code-session-id (его отправляет Claude Code) | Все |
metadata.user_id в теле | Anthropic |
prompt_cache_key в теле | OpenAI |
user в теле | OpenAI |
| Системный промпт плюс первое сообщение пользователя | Все |
Последняя строка работает без каких-либо усилий с вашей стороны. Разговор растёт за счёт добавления сообщений, поэтому его системный промпт и первое сообщение пользователя остаются на месте, и каждая реплика даёт один и тот же отпечаток. Сравниваются только первые 4000 символов каждого из них.
Отпечаток принадлежит одному API-ключу, одной модели и одной группе каналов. Если отправить тот же разговор с другим ключом или к другой модели, закрепление начнётся заново.
Как долго держится закрепление#
Первый успешный вызов в разговоре закрепляет его за обслужившим каналом, а последующие реплики сначала пробуют этот канал.
- Срок жизни. Закрепление живёт до часа и продлевается, пока разговор активен. После 30–60 минут тишины оно истекает, и следующая реплика размещается как в новом разговоре.
- Кеша уже нет. В формате Anthropic Messages закрепление также прекращается, когда разговор простаивает дольше, чем живёт его кеш: 5 минут, либо 1 час, если ваши точки разрыва
cache_controlиспользуютttl: "1h". Защищать уже нечего, поэтому следующая реплика размещается заново, а не ждёт свой прежний канал. - Канал занят. Когда закреплённый канал загружен полностью, реплика некоторое время ждёт его (около секунды для коротких промптов, до десяти секунд для длинных), вместо того чтобы перейти на другой и заплатить за новую запись кеша. Если он всё ещё занят, реплика переходит на другой канал.
- Канал неисправен. Когда закреплённый канал нездоров, реплика переходит на здоровый, и закрепление переезжает вместе с ней. Эта реплика записывает новый кеш, а следующие читают его.
Задайте разговорам явный id#
Автоматический отпечаток ломается, если первое сообщение пользователя меняется от реплики к реплике, например когда ваше приложение сжимает или переписывает старую историю. Тогда отправляйте один стабильный id на разговор. Заголовок работает во всех форматах:
curl https://halfoff.dev/v1/messages \
-H "x-api-key: $HALFOFF_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "x-session-id: thread_51c7d0" \
-H "content-type: application/json" \
-d '{"model": "claude-sonnet-5.5", "max_tokens": 1024,
"messages": [{"role": "user", "content": "Pick up where we left off."}]}'Поля тела делают то же самое: metadata.user_id в формате Anthropic, prompt_cache_key в формате OpenAI. Используйте один id на разговор; если один id разделить между многими несвязанными разговорами, все они сгрудятся на одном канале.
Кеш в логах и биллинге#
Для каждого вызова Логи показывают:
| Поле | Значение |
|---|---|
| In | Входные токены без кеша плюс записи в кеш. |
| Cached | Входные токены, прочитанные из кеша. |
| Cache write (подробности) | Входные токены, записанные в кеш. |
| Cache hit (подробности) | Доля кешированных токенов во всём входе. |
| Affinity (подробности) | explicit: вы передали id сессии. sticky: разговор был закреплён и вернулся на свой канал. new: первый вызов, либо закрепление истекло или его канал был недоступен. none: не удалось построить отпечаток. |
Обзор на панели показывает долю попаданий в кеш по всем вызовам. У каждого вида токенов своя цена в половину тарифа; см. формулу.
Низкая доля попаданий?
Если каждая реплика разговора показывает new, значит, что-то в системном промпте или первом сообщении пользователя меняется от реплики к реплике. Обычный виновник — временная метка в системном промпте, которая заодно сводит на нет и кеш вендора: перенесите её в последнее сообщение пользователя или отправляйте явный id сессии. Если реплики показывают sticky, но Cached остаётся пустым, проверьте, что префикс достигает минимальной длины модели и, для Claude, что у него есть cache_control.