Код 499: клиент оборвал соединение раньше ответа
Ответ пришёл наполовину и пропал — знакомая картина при долгой генерации. Разбираем, почему это почти никогда не лимит токенов, а таймаут на стороне клиента.
4 мин · · Обновлено · ошибки
Код 499 — статус, которым nginx и большинство прокси перед API помечают ситуацию, когда клиент сам закрыл соединение раньше, чем сервер успел дописать ответ. С точки зрения API это не ошибка в привычном смысле: сервер ничего не возвращал специально, разговор оборвался с другой стороны провода.
Почему это почти всегда таймаут, а не лимит токенов
Самая частая причина обрыва — таймаут HTTP-клиента, выставленный короче, чем реально нужно модели на генерацию. Это особенно заметно на длинных ответах: большой max_tokens или включённый extended thinking — оба случая требуют заметно больше времени, чем короткий чат-ответ на пару предложений.
Разница с ограничением по токенам принципиальная. Если бы модель упёрлась в лимит контекста или max_tokens, ответ пришёл бы полностью, просто обрезанным по finish_reason. При 499 ответа не приходит вообще — соединение разрывается раньше, чем данные успевают дойти до клиента, независимо от того, сколько токенов модель успела сгенерировать.
Где чаще всего стоит таймаут по умолчанию
curl без явного --max-time не оборвёт соединение сам, но библиотеки внутри инструментов почти всегда ставят свой таймаут. У requests в Python его нет по умолчанию тоже, а вот у многих HTTP-клиентов в IDE-плагинах и прокси-обёртках — есть, и часто он в районе 30-60 секунд, чего extended thinking на сложном запросе может не уложиться.
Проверить это можно, увеличив таймаут явно:
curl https://api.zukko.pro/v1/messages \
--max-time 180 \
-H "Authorization: Bearer fetch_ваш_ключ" \
-H "Content-Type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d '{"model":"claude-opus-5-5","max_tokens":4096,"thinking":{"type":"enabled","budget_tokens":8000},"messages":[{"role":"user","content":"Распиши план миграции сервиса на новую очередь сообщений"}]}'import requests
resp = requests.post(
"https://api.zukko.pro/v1/messages",
timeout=180, # секунд, а не значение по умолчанию библиотеки
headers={
"Authorization": "Bearer fetch_ваш_ключ",
"anthropic-version": "2023-06-01",
},
json={
"model": "claude-opus-5-5",
"max_tokens": 4096,
"thinking": {"type": "enabled", "budget_tokens": 8000},
"messages": [{"role": "user", "content": "Распиши план миграции"}],
},
)const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 180_000);
const resp = await fetch("https://api.zukko.pro/v1/chat/completions", {
method: "POST",
signal: controller.signal,
headers: {
Authorization: "Bearer fetch_ваш_ключ",
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "gpt-5.6-sol",
max_tokens: 4096,
messages: [{ role: "user", content: "Распиши план миграции" }],
}),
});
clearTimeout(timeout);Стриминг снижает шанс обрыва
Если ответ ждётся целиком одним куском, любой промежуточный прокси или балансировщик может решить, что соединение зависло, и закрыть его сам — даже если ваш собственный таймаут выставлен щедро. У стриминга ("stream": true в /v1/chat/completions или /v1/responses, аналогично у /v1/messages) от сервера постоянно идут байты, и большинству промежуточных узлов этого достаточно, чтобы не считать соединение мёртвым.
Генерация изображений и более долгие таймауты
Генерация изображений через Zukko временно недоступна — раздел оставлен для справки. Актуальный список моделей —
GET https://api.zukko.pro/v1/models.
У моделей с kind=image — nano-banana-2-text-to-image, openai-gpt-image-2-edit и остальных из каталога — обработка одного запроса обычно занимает больше времени, чем текстовый ответ на пару абзацев, а тарификация у них отдельная: за штуку, а не за токены. Клиентский таймаут, рассчитанный на чат, для /v1/images/generations часто оказывается слишком коротким. Если 499 стабильно случается именно на генерации изображений, а не на тексте — это первое, что стоит поднять отдельно от таймаута, выставленного для /v1/messages и /v1/chat/completions.
Idempotency-Key: как повторить запрос, не заплатив дважды
Если запрос оборвался, самый простой способ — повторить его. Проблема в том, что обрыв соединения клиентом не останавливает списание: шлюз дочитывает ответ провайдера до конца и списывает всю генерацию, так что простой повтор без защиты означает оплату дважды. Чтобы этого не было, добавляйте заголовок Idempotency-Key со случайным значением при первой попытке — уникальным для этого запроса и тем же самым на всех его повторах (не генерируйте его заново при ретрае):
curl https://api.zukko.pro/v1/messages \
-H "Authorization: Bearer fetch_ваш_ключ" \
-H "Idempotency-Key: retry-20260919-01" \
-H "anthropic-version: 2023-06-01" \
-d '{"model":"claude-opus-5-5","max_tokens":4096,"messages":[...]}'Повтор с тем же ключом и тем же телом запроса в течение 24 часов вернёт тот же результат без повторного списания. Если тело запроса изменится, а ключ останется тем же — получите 422 idempotency_conflict, и это тоже полезный сигнал: значит, вы действительно повторяете, а не отправляете новый запрос под старым именем.
Как проверить, что произошло на самом деле
Раз ответ на клиента не пришёл, узнать, добралась ли генерация до конца, можно по истории. GET /v1/usage отдаёт список списаний с курсорной пагинацией — если оборвавшийся запрос там есть, значит модель успела отработать и результат существует на стороне провайдера, просто не долетел до вас. Учтите, что обрыв не отменяет генерацию: шлюз дочитывает ответ провайдера и списывает её целиком, поэтому запись может появиться не сразу, а когда генерация завершится. Если записи нет и через несколько минут — запрос действительно не состоялся; для надёжности повторяйте его с тем же Idempotency-Key, тогда повторный вызов ничего не задвоит.
Заголовки X-Zukko-Cost-Micros и X-Fetch-Balance-Cents приходят только вместе с успешным обычным (не потоковым) ответом, поэтому при 499 вы их не увидите в принципе — это ещё один способ отличить "ответ был, но не долетел" от "запроса не случилось": сверяйтесь с балансом через GET /v1/balance до и после (с поправкой на то, что списание за оборванную генерацию всё равно произойдёт).
Что делать на практике
Если 499 повторяется регулярно на одних и тех же запросах — это почти всегда сочетание большого max_tokens или включённого extended thinking с таймаутом клиента, унаследованным по умолчанию от библиотеки или прокси. Поднимите таймаут явно, переключитесь на стриминг там, где инструмент это поддерживает, и используйте Idempotency-Key на любые запросы, где повтор возможен — тогда обрыв соединения превращается из риска двойного списания в обычную сетевую случайность, которую можно просто повторить.
zukko
Платите в 25×
меньше
за Claude Code & ChatGPT
Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.
Похожие статьи
- Ключи OpenAI API в открытом доступе: почему ими нельзя пользоватьсяОткуда берутся «бесплатные» ключи на GitHub, почему пользоваться ими опасно и для вас, и для владельца, и как защитить собственный ключ от такой же утечки.
- Ошибки GLM API: почему не работает запрос и как это исправитьНеверный id модели, переполненный контекст 128K, ключ, баланс, лимиты: типичные причины ошибок GLM API и быстрый способ найти свою.
- This organization has been disabled в Claude API: что это и как продолжитьКлюч Anthropic внезапно отвечает «organization has been disabled». Объясняем, что это за ошибка, что проверить и как перенести SDK и Claude Code на другой провайдер.
- JetBrains AI Assistant не работает: причины и обходные путиAI Assistant не активируется, молчит или недоступен в отдельных файлах. Разбираем причины по документации JetBrains и подключаем свои модели через шлюз.