halfoff.devДокументация
Получить ключ
Models API

Кеширование промптов

Снижайте стоимость входных токенов сверх половины цены: используйте кеширование промптов каждого вендора, а Half Off Dev будет возвращать каждый разговор на канал, где лежит его кеш.

Половина цены — лишь отправная точка. Кеширование промптов снижает стоимость входных токенов гораздо сильнее, если вы повторно отправляете один и тот же префикс: длинные чаты, ассистенты для программирования, поиск по фиксированному набору документов. Модель, недавно обработавшая префикс, может переиспользовать эту работу, а переиспользованные токены тарифицируются по цене чтения кеша. Для claude-sonnet-5.5 это $0.05 за миллион токенов против $1.00 за входные токены без кеша.

Кеширование через Half Off Dev держится на двух составляющих:

  1. Собственное кеширование вендора, которое используется ровно так, как описано в документации вендора. Поля вроде cache_control и prompt_cache_key пересылаются без изменений.
  2. Привязка разговора к каналу. Кеш существует только на канале, который его записал. Шлюз узнаёт последовательные реплики одного разговора и отправляет их на тот же канал, поэтому к следующей реплике кеш всё ещё на месте.

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.