Таймауты при длинных генерациях: как их не ловить
Оборванный на середине ответ почти всегда значит не лимит по токенам, а таймаут вашего же клиента. Разбираем, где он берётся и как повторять запрос без двойного списания.
4 мин · · Обновлено · разработка
Длинный ответ, который обрывается на середине предложения или посреди JSON, выглядит как ошибка сервера. Почти всегда это не так: причина — таймаут клиента или сети, а не лимит по токенам. Модель продолжала бы генерировать дальше, но соединение уже закрылось с вашей стороны или где-то на промежуточном узле сети.
Почему это не лимит токенов
Лимит по токенам задаётся явно через max_tokens в запросе, и его достижение не выглядит как обрыв — модель просто останавливается на границе, а ответ приходит целиком, синтаксически завершённым в рамках того, что успело сгенерироваться. Обрыв посередине структуры, наоборот, почти всегда означает, что соединение прервалось до того, как модель закончила: либо клиентская библиотека закрыла его по своему таймауту, либо прокси или балансировщик где-то на пути оборвал долго висящее соединение.
Первое, что стоит проверить в такой ситуации — оба параметра сразу: max_tokens в теле запроса (не занижен ли он специально или по умолчанию библиотекой) и таймаут HTTP-клиента (не истекает ли он раньше, чем модель успевает закончить генерацию).
Таймауты по умолчанию редко рассчитаны на LLM
Значения таймаута, которые ставят по умолчанию HTTP-библиотеки и SDK, изначально рассчитаны на обычные веб-запросы — доли секунды, максимум несколько секунд. Генерация текста с рассуждениями (extended thinking) или с большим объёмом контекста — это принципиально другая по длительности операция, и дефолтный таймаут просто не рассчитан на такое ожидание. Разумный подход — не полагаться на значение по умолчанию, а выставлять таймаут клиента явно, с запасом относительно ожидаемой длительности конкретного запроса:
import httpx
client = httpx.Client(timeout=httpx.Timeout(300.0, connect=10.0))
response = client.post(
"https://api.zukko.pro/v1/messages",
headers={
"Authorization": "Bearer fetch_ваш_ключ",
"anthropic-version": "2023-06-01",
},
json={
"model": "claude-opus-5-5",
"max_tokens": 8000,
"messages": [{"role": "user", "content": "Длинная задача с рассуждением"}],
},
)Явный таймаут в 300 секунд для тяжёлого запроса с extended thinking — не гарантия, что его хватит на любую задачу, но это лучше, чем незаметный дефолт в 10–30 секунд, который оборвёт соединение почти наверняка.
Отличайте таймаут от 503
Если модель недоступна у провайдера (provider_unavailable) или выключена в Zukko (model_disabled), сервер отвечает быстро — приходит настоящий 503 с телом ответа, и это происходит почти мгновенно, а не через несколько минут ожидания. Настоящий таймаут выглядит иначе: клиент просто не получает вообще никакого ответа в течение всего заданного окна ожидания, соединение зависает, а не закрывается сразу с кодом ошибки. Если ошибка приходит быстро — это не таймаут, дело в доступности конкретной модели у провайдера. Если запрос висит и обрывается только по истечении вашего собственного таймаута — вопрос в клиентских настройках, а не в шлюзе.
Безопасный повтор без двойного списания
Главная опасность после таймаута — не понять, выполнился ли запрос на сервере, и попытаться повторить его вслепую, рискуя оплатить одну и ту же генерацию дважды. Риск реальный: обрыв соединения на стороне клиента списание не останавливает — шлюз дочитывает ответ провайдера и списывает всю генерацию, даже если вы её так и не получили. Для этого есть заголовок Idempotency-Key: повтор запроса с тем же значением ключа в течение 24 часов возвращает тот же результат, что и первый успешный вызов, не списывая средства повторно.
curl https://api.zukko.pro/v1/messages \
-H "Authorization: Bearer fetch_ваш_ключ" \
-H "Content-Type: application/json" \
-H "anthropic-version: 2023-06-01" \
-H "Idempotency-Key: task-42" \
-d '{"model":"claude-opus-5-5","max_tokens":8000,"messages":[{"role":"user","content":"Длинная задача"}]}'Если клиент оборвал соединение по таймауту, но запрос на сервере успел выполниться, повтор с тем же Idempotency-Key вернёт готовый результат мгновенно вместо повторной полной генерации — и не спишет деньги во второй раз. Стоит только помнить: если в повторном запросе изменить тело при том же ключе, сервер ответит 422 с кодом idempotency_conflict, так что ключ должен однозначно соответствовать одному конкретному телу запроса. Ключ задаётся один раз на логический запрос и переиспользуется при всех его повторах — генерировать новый на каждую попытку нельзя, иначе каждая попытка будет оплачена отдельно.
Что делать с действительно долгими задачами
Для задач, которые заведомо займут много времени — большой контекст, extended thinking, объёмная генерация с высоким max_tokens — увеличивать таймаут до бесконечности не всегда практично. Разумнее заранее оценить порядок длительности на тестовых запросах меньшего объёма, заложить таймаут с запасом в несколько раз, и обязательно снабжать такие запросы Idempotency-Key, чтобы обрыв соединения не превращался в риск двойной оплаты при повторе.
Таймауты на стороне промежуточных узлов
Собственный таймаут HTTP-клиента — не единственное узкое место. Если запрос идёт через корпоративный прокси, балансировщик нагрузки или CDN перед тем, как достичь шлюза, у каждого из этих узлов может быть собственный лимит на время удержания соединения, независимый от настроек клиентской библиотеки. Симптом тот же самый — обрыв на середине ответа, — но исправить это увеличением таймаута в коде не получится, если узкое место находится не в вашем коде, а в инфраструктуре между клиентом и шлюзом. Если увеличение клиентского таймаута не помогает, стоит проверить, нет ли на пути запроса прокси или VPN с собственными ограничениями на длительность соединения.
Инструменты вроде Claude Code и Cursor
У готовых инструментов, которые ходят в шлюз под капотом — Claude Code, Cursor, Cline, Roo Code — таймаут запроса обычно не настраивается напрямую через переменные окружения проекта, а зашит во внутреннюю логику клиента. Если такой инструмент регулярно обрывает длинные ответы от тяжёлых моделей вроде claude-opus-5-5 с включённым extended thinking, часто помогает не гнаться за максимальным качеством в каждом запросе, а сознательно ограничивать max_tokens под конкретную задачу — короткий точный ответ надёжнее длинного, который может не успеть дойти целиком.
zukko
Платите в 25×
меньше
за Claude Code & ChatGPT
Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.
Похожие статьи
- Промпт-инъекция: что это и как защитить бота и агентаОбъясняем, как работает промпт-инъекция через пользовательский ввод и чужие документы, и какие меры снижают ущерб: изоляция данных, права инструментов, лимиты ключа.
- Свой прокси к нейросети на Node.js: не показывать API-ключ во фронтендеНебольшой сервер на Express между вашим фронтендом и API нейросети: ключ остаётся на сервере, а модели, длина запроса и частота ограничены.
- DeepSeek API на Python: запросы, стриминг, JSON и обработка ошибокГотовый код для DeepSeek V4.1 Flash и V4 Pro на OpenAI SDK: запрос, стриминг, разбор JSON и аккуратная обработка ошибок.
- Промпт-инжиниринг: примеры и 12 готовых шаблонов для работыДвенадцать шаблонов промптов для рабочих задач: что в них написано, почему именно так и сколько стоит один вызов по API.