Таймауты при длинных генерациях: как их не ловить

Оборванный на середине ответ почти всегда значит не лимит по токенам, а таймаут вашего же клиента. Разбираем, где он берётся и как повторять запрос без двойного списания.

4 мин · · Обновлено · разработка

Длинный ответ, который обрывается на середине предложения или посреди JSON, выглядит как ошибка сервера. Почти всегда это не так: причина — таймаут клиента или сети, а не лимит по токенам. Модель продолжала бы генерировать дальше, но соединение уже закрылось с вашей стороны или где-то на промежуточном узле сети.

Почему это не лимит токенов

Лимит по токенам задаётся явно через max_tokens в запросе, и его достижение не выглядит как обрыв — модель просто останавливается на границе, а ответ приходит целиком, синтаксически завершённым в рамках того, что успело сгенерироваться. Обрыв посередине структуры, наоборот, почти всегда означает, что соединение прервалось до того, как модель закончила: либо клиентская библиотека закрыла его по своему таймауту, либо прокси или балансировщик где-то на пути оборвал долго висящее соединение.

Первое, что стоит проверить в такой ситуации — оба параметра сразу: max_tokens в теле запроса (не занижен ли он специально или по умолчанию библиотекой) и таймаут HTTP-клиента (не истекает ли он раньше, чем модель успевает закончить генерацию).

Таймауты по умолчанию редко рассчитаны на LLM

Значения таймаута, которые ставят по умолчанию HTTP-библиотеки и SDK, изначально рассчитаны на обычные веб-запросы — доли секунды, максимум несколько секунд. Генерация текста с рассуждениями (extended thinking) или с большим объёмом контекста — это принципиально другая по длительности операция, и дефолтный таймаут просто не рассчитан на такое ожидание. Разумный подход — не полагаться на значение по умолчанию, а выставлять таймаут клиента явно, с запасом относительно ожидаемой длительности конкретного запроса:

bash
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 часов возвращает тот же результат, что и первый успешный вызов, не списывая средства повторно.

bash
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.