Перейти к основному содержанию

Сначала тарификация: write_response_body_failed 500 не тарифицируется

Когда шлюз возвращает 500 с write_response_body_failed / connection reset by peer, платформа уже автоматически повторила попытку 2-3 раза и показывает ошибку только после того, как все попытки завершились неудачей. За эти запросы не взимается никакая плата.Так что даже если в ваших логах накапливаются такие ошибки, в вашем счете не появится соответствующее списание — вы не платите за сбои. Есть другой случай, который действительно тарифицируется (когда ваш клиент уходит слишком рано); см. раздел «Влияние на тарификацию» ниже.
Краткий ответ: ответы API для image generation обычно имеют размер от десяти до нескольких десятков МБ, и сбой почти всегда происходит во время загрузки ответа (не из-за того, что тело запроса слишком большое — обычный text-to-image тоже дает сбой). Сначала проверьте, к какой категории относится лог консоли, затем выполните приведенные ниже самопроверки для macOS / Linux / Node.js.

Как выглядит ошибка

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

Со стороны шлюза

Со стороны клиента

В Node.js различайте «killed» и «closed»: ECONNRESET означает, что пришел TCP RST — соединение было принудительно разорвано чем-то на пути следования, что указывает на промежуточный сетевой узел. SocketError: other side closed / ERR_STREAM_PREMATURE_CLOSE означает, что удаленная сторона закрыла соединение в штатном режиме (FIN), что указывает на проблему с финализацией ответа на стороне сервера, например на отсутствие завершающего chunked-терминатора. Они указывают в совершенно разных направлениях; не воспринимайте их как один и тот же симптом.Кроме того, UND_ERR_* может исходить только от undici (движка, стоящего за встроенным в Node 18+ fetch), тогда как read ECONNRESET — это формулировка верхнего уровня libuv, которую возвращают axios / node-fetch / модуль http. Если оба вида ошибок появляются одновременно, сначала проверьте, нет ли в вашем приложении двух разных путей HTTP — в таком случае это вовсе не один и тот же инцидент.

Сначала определите направление: кто разорвал соединение

Код write_response_body_failed — ключевая подсказка: он означает шлюз не смог записать тело ответа обратно вызывающей стороне. Это направление downstream, а не ошибка upstream-модели. Результат уже был сгенерирован; соединение оборвалось в момент, когда его передавали вам.
Это не вызвано слишком большим телом запроса. Загрузки при редактировании изображений используют референсные изображения, из-за чего возникает подозрение на размер загрузки — но обычный text-to-image, у которого тело запроса всего несколько сотен байт, ломается так же часто. Сбой происходит при загрузке ответа: payload изображений имеют размер от десяти до нескольких десятков MB и являются самым хрупким звеном всего пути.

Обрыв downstream

write_response_body_failed, connection reset by peer, SSL EOF на стороне клиента. Соединение исчезло, пока шлюз передавал тело. Если платформа возвращает 500 в таком случае, это не тарифицируется — см. раздел о тарификации ниже.

Сбой upstream (со стороны канала)

Таймауты upstream, upstream_error, 5xx с полезной нагрузкой upstream или HTTP 200 с аномальным finishReason. Это настоящие проблемы канала — передайте x-request-id в поддержку.

Самый сильный сигнал: что журнал консоли говорит об этом вызове

Сначала проверьте журнал вызова в консоли, прежде чем делать что-либо еще. Это ничего не стоит и быстрее любого шага на стороне клиента сужает круг поиска:
Это приводит к выводу, который люди часто понимают наоборот: сбои на этапе соединения, такие как UND_ERR_CONNECT_TIMEOUT, не могут привести к списанию, потому что запрос никогда не достигал шлюза. Так что если вы видите «много connect timeouts» и «много списаний», это точно не одни и те же запросы — разбирайте их отдельно, а не заставляйте одну причину объяснять все.

Четыре шага, чтобы снять с себя подозрение

Выполняйте их по порядку; большинство случаев решается уже на первых двух:
1

Исключите неверное чтение на уровне приложения: вы могли получить его и не сохранить

«Ответа не было» обычно оказывается выводом после того, как код выбросил исключение и был пойман как «request failed» — а не доказательством, что байты вообще не пришли. Самый частый случай: gpt-image-2-all по умолчанию возвращает b64_json без префикса data:, поэтому код, который читает data[0].url, получает undefined, выбрасывает downstream, считается неудачным и запускает повторную попытку — за которую снова взимается плата.Симптомы идентичны сетевому сбою, но сетевой сбой здесь не требуется. Выведите одну строку для проверки:
Различия полей и префиксов по сериям приведены в справочнике префикса base64.
2

Проверьте, не сбоят ли все каналы и модели одновременно

Если два разных канала, работающих на двух разных моделях, в один и тот же временной интервал выдают одну и ту же ошибку, причины, специфичные для канала, по сути исключены — upstream не отказывают так синхронно.
3

Проверьте среду выполнения клиента: TLS stack (Python) или таймауты undici (Node.js)

Python: проверьте версию TLS stack. Node.js: проверьте три таймаута undici. Оба случая разобраны в следующих двух разделах. Это самая частая первопричина на практике, она полностью находится на вашей машине, и одна команда это подтверждает.
4

Снизьте concurrency, перейдите на последовательный режим и отключите локальный proxy

Повторно запустите те же запросы при concurrency 1-2 с отключенными VPN или proxy. Если в таком режиме ошибка ни разу не воспроизводится, проблема в обработке соединений клиентом, локальных ресурсах (connection pool, file descriptors, memory) или сетевом пути — не в канале.

Главный подозреваемый: TLS-стек клиента (особенно в macOS)

Python, который поставляется с macOS (/usr/bin/python3), использует LibreSSL 2.8.3, а не OpenSSL. В сочетании с urllib3 v2 такая связка надежно вызывает SSLEOFError при одновременной загрузке больших тел ответов. Клиент самопроизвольно разрывает соединение, а шлюз исправно фиксирует стену из connection reset by peer.

Одна команда для проверки

Если при импорте requests вы видите это предупреждение, это тот же сигнал:

Исправление: переключите интерпретатор

Не понижайте версию urllib3 — просто используйте Python, собранный с полноценным OpenSSL:

Измеренное сравнение (2026-07-29, UTC+8)

Реальные цифры из сравнительного теста на двух каналах серии Nano Banana (gemini-3-pro-image / gemini-3.1-flash-image): Вывод однозначен: разница на порядок после переключения интерпретатора, а тот факт, что ранее оба канала падали одновременно, уже доказывал, что причина не в канале.

Что проверять на Linux-сервере (совершенно другой список)

Проверка TLS-stack выше проходит практически на любой Linux-машине — Python из дистрибутива линкуется с обычным OpenSSL, так что ловушки LibreSSL там нет. Не останавливайтесь на этой проверке. Проблемы на стороне сервера живут в исходящем пути и в ограничениях контейнера, а это уже совсем другой мир по сравнению с локальной разработкой.

1. Таймауты бездействия на облачных NAT-шлюзах и балансировщиках нагрузки (главная причина на стороне сервера)

Это главный источник connection reset by peer в боевой среде. Возьмите AWS NAT Gateway: он использует фиксированный, не настраиваемый таймаут бездействия в 350 секунд, и когда он срабатывает, он отправляет RST, а не FIN — так что клиент видит ровно ECONNRESET. Плохая часть в том, что это каскадируется: как только соединения из пула простаивали дольше 350 секунд, они все мертвы, поэтому ваш запрос получает RST на первом, клиент незаметно повторяет попытку на следующем соединении из пула — которое было столь же простаивающим и тоже получает RST. Типичный признак — «тихо какое-то время, потом несколько вызовов подряд падают, затем все снова нормально».
Это тот же механизм, что и случай «keep-alive повторно использует мертвое соединение» в разделе Node.js — на сервере виновником обычно является NAT-шлюз облачного провайдера, а не локальное прокси-ПО.
Исправления (любое из них; первые два предпочтительнее):
  • Установите TCP keepalive ниже 350 секунд, чтобы трафик шел даже в тихие периоды;
  • Ограничьте, как долго соединения могут простаивать в пуле, чтобы потенциально мертвые отбрасывались (Node: new Agent({ keepAliveTimeout: 60_000 }); Python requests: настройте пул через HTTPAdapter);
  • Обходите NAT-шлюз полностью, например через VPC endpoints.
Другие облака и self-hosted балансировщики нагрузки используют другие значения бездействия, но подход одинаков: найдите самый короткий таймаут бездействия на пути и установите keepalive ниже него.

2. Значение TCP keepalive по умолчанию фактически «выключено»

В Linux значение по умолчанию tcp_keepalive_time 7200 секунд (2 часа), что намного дольше любых таймаутов бездействия выше, поэтому на практике это бесполезно:
Включать keepalive непосредственно в HTTP client — более надежный путь, поскольку контейнеры часто не могут менять параметры ядра.

3. MTU контейнерной сети

Overlay-сети Docker / Kubernetes (flannel VXLAN и им подобные) обычно работают с MTU 1450, а не 1500. Добавьте к этому PMTUD black hole на пути — и получите классический случай «маленькие запросы всегда проходят, большие ответы всегда зависают»:

4. Ограничения памяти контейнера → процесс получает OOMKilled

Один 4K base64 payload может достигать 20-30 MB; если загружать его целиком с resp.json() при параллельных запросах, легко превысить лимит памяти контейнера, и ядро убьет процесс — что снова выглядит как «соединение просто оборвалось»:
См. ниже «Потоково передавайте ответ вместо загрузки целиком» для решения.

5. Переменные среды proxy (самая коварная на серверах)

На серверах часто глобально заданы значения HTTP_PROXY / HTTPS_PROXY / NO_PROXY в /etc/environment, systemd unit или Dockerfile — и тот, кто их задавал, давно о них забыл. Хуже того, языки по-разному относятся к их учету: Это несоответствие дает по-настоящему запутанные результаты: на одной машине curl и Python ходят через прокси, а Node подключается напрямую (или наоборот), поэтому они расходятся, и диагностика приводит к противоречивым выводам. Сначала проверьте:
APIYI доступен напрямую, так что на сервере обычно вам нужен api.apiyi.com в NO_PROXY — или просто никаких переменных proxy вообще.

Разовая самопроверка на стороне сервера

Потеря пакетов на линии: заголовки приходят, тело ответа обрывается на полпути

Когда ваш сервер обращается к APIYI напрямую, и у этой машины (или её дата-центра) исходящая линия связи низкого качества, возникает очень узнаваемый сбой: заголовки HTTP 200 приходят, небольшая часть тела ответа скачивается, а затем соединение закрывается. Изображение было успешно сгенерировано. Обрыв происходит на публичном сетевом маршруте, по которому результат передаётся обратно на ваш сервер.

Типичные ошибки

Как это распознать: три признака

  1. Статус 200, а в ошибке указано, сколько байт не хватило. Если клиент может сообщить количество недостающих байт, ответ содержал Content-Length. Это означает, что шлюз получил результат целиком и знал его общий размер еще до начала отправки. Генерация прошла успешно; сбой мог произойти только при скачивании.
  2. Общее время значительно превышает время генерации. time_starttransfer в curl (время до первого байта) примерно равно времени генерации. Если time_total на несколько минут дольше, а size_download составляет всего от десятков до сотен КБ, данные не доходят.
  3. Сбои возникают в определенные интервалы времени, а на другой машине все работает нормально. Один и тот же сервер часто выдает ошибки в определенные часы дня и работает без сбоев в остальное время. В тот же самый момент на другой машине (или на вашем ноутбуке) точно такой же запрос выполняется идеально.
Почему все завершается ошибкой «connection closed», а не зависает навсегда: на линии с высокой потерей пакетов теряются как пакеты данных, так и подтверждения. Когда сервер долгое время не получает подтверждения от клиента, он закрывает соединение. Поэтому ситуация выглядит как тайм-аут, но первопричина — потеря пакетов. Увеличение тайм-аута с вашей стороны не решит проблему.

Шаги для самостоятельной проверки

1

Воспроизведите запрос с помощью curl и запишите профиль времени

Как читать результаты: http=200, size заметно меньше, чем Content-Length, а total значительно превышает ttfb — это означает, что проблема в загрузке. Переходите к следующему шагу.
2

Измерьте потерю пакетов с помощью ping и mtr

Исправная линия показывает потерю пакетов, близкую к 0%. Учитывайте только те потери, которые сохраняются до последнего узла. Если отдельный промежуточный узел показывает потери, а последующие узлы чистые, это обычно означает лишь то, что данный маршрутизатор снижает приоритет ответов на ping. Запускайте проверку в те часы, когда происходят сбои; в обычное время исправная линия ничего не покажет.
3

Сравните с другой машиной

Одновременно выполните команду из шага 1 на другом сервере (в идеале в другом дата-центре или у другого облачного провайдера) либо на своем ноутбуке. Если на контрольной машине все работает, а сбои происходят только на этой, проблема заключается в исходящем сетевом канале этой машины, а не в API.
4

Проверьте зависимость от времени суток

Фиксируйте время, http_code, size и total каждого вызова и проверьте, группируются ли сбои в определенные часы. Сбои, возникающие в конкретные временные интервалы, — классический признак перегрузки линии.
Реальный случай (2026-09-27): изображение было сгенерировано за 48 секунд, и полный ответ уже поступил на исходящий узел. Однако на линии в дата-центре клиента в этот момент терялось 17%–37% пакетов (в то же самое время с этих же узлов наблюдались 0% потерь до других серверов в Китае), и клиент получил лишь около 4% данных. Все остальные клиенты на тех же узлах в этот временной интервал получили свои ответы полностью.

Способы решения

  1. Уменьшите размер ответа (сработает сразу): для серии gpt-image добавьте output_format=jpeg и output_compression=85. Замеры от 27.09.2026: размер ответа при n=1 снизился примерно с 2,4 МБ до 0,5 МБ, а при n=5 — примерно с 12 МБ до 2,6 МБ, то есть стал примерно в 4,7 раза меньше. Чем меньше данных передаётся, тем выше вероятность успешной доставки по каналу с потерями пакетов. Модели с поддержкой response_format: "url" (например, gpt-image-2-vip и gpt-image-2-all) могут возвращать ссылку вместо самих данных, что уменьшает размер ответа до нескольких сотен байт.
  2. Разделите n>1 на несколько параллельных запросов с n=1: ответ при n=5 в пять раз больше ответа при n=1, поэтому соединение прерывается легче, а при сбое теряется весь пакет целиком. При разделении каждый ответ будет небольшим, а в случае сбоя будет потеряно только одно изображение.
  3. Смените исходящий узел (настоящее решение): запускайте сервис, вызывающий APIYI, на машине с качественным каналом связи (например, на инстансе крупного облачного провайдера с BGP или премиум-линией) либо настройте релей через такую машину. Вы также можете передать результаты mtr вашему дата-центру или интернет-провайдеру.
  4. Настройка клиента: это не устранит потерю пакетов, но предотвратит другие проблемы после восстановления связи. В Java WebClient значение maxInMemorySize по умолчанию составляет всего 256 КБ, тогда как ответы при n>1 часто превышают 10 МБ, поэтому увеличьте его как минимум до 32 МБ, иначе вы получите DataBufferLimitException. Установите responseTimeout не менее чем на 300 секунд:
Подумайте перед повторной попыткой: такие сбои тарифицируются как обычно. В момент обрыва соединения изображение уже сгенерировано, а платформа завершила отправку, поэтому в логах консоли отображается обычная запись о тарификации. Каждая повторная попытка означает повторную генерацию и повторную оплату. Пока качество канала не улучшится, слепые повторные попытки лишь умножат расходы. Сначала выполните пункты 1–3 выше.
Если вам требуется помощь службы поддержки для расследования проблемы, пожалуйста, предоставьте:
  • Время сбоя с точностью до минуты и с указанием часового пояса, например 2026-09-27 13:32 (UTC+8)
  • Исходящий IP-адрес вашего сервера (можно узнать с помощью curl -s ipinfo.io/ip)
  • Заголовок ответа X-Shellapi-Request-Id (команда из шага 1 выводит его)
  • Строку, выведенную командой curl -w на шаге 1
  • Вывод mtr
Чтобы измерить реальную пропускную способность между вашим сервером и нашим API, см. В логах указано, что запрос завершён, но клиент ничего не получил.

Node.js: три независимых тайм-аута, до которых SDK timeout не может дотянуться

Встроенный fetch в Node 18+ работает на undici, который имеет три независимых тайм-аута, покрывающих три этапа запроса. «Но я выставил тайм-аут на 5 минут» обычно означает, что вы изменили четвёртое значение, которое не относится ни к одному из них:
Параметр timeout в openai-node — это тайм-аут всего запроса на основе AbortController, и он не распространяется ни на один из трёх тайм-аутов выше. Увеличение timeout с 60 до 300 секунд оставляет connectTimeout на уровне 10 секунд. То же самое верно и для AbortSignal.timeout() в чистом fetch().Это самая частая причина «мой тайм-аут огромный, но он все равно срабатывает» — был изменён не тот уровень.

Правильная настройка

Для увеличения трёх тайм-аутов undici требуется Agent, либо глобально, либо для каждого запроса:

maxRetries по умолчанию равен 2 и автоматически повторяет ошибки подключения

openai-node по умолчанию использует maxRetries: 2, и как ошибки подключения, так и тайм-ауты входят в область действия этих автоматических повторов. Один логический вызов поэтому может породить три фактических запроса даже тогда, когда в вашем коде вообще нет логики повторов (будет ли каждый из них тарифицироваться, зависит от того, в какую категорию «Влияние на тарификацию» он попадет). Эндпоинты для изображения — это дорогие синхронные долгие запросы, поэтому всегда задавайте maxRetries: 0 явно и берите логику повторов на себя, со своим backoff и своим пределом попыток. Правила тарификации описаны в Стратегия повторов.
Сначала убедитесь, какой стек у вас на самом деле: node -v, npm ls openai undici axios node-fetch. Код UND_ERR_* лишь доказывает, что под капотом используется undici — это не доказывает, что вы используете OpenAI SDK. Обычный fetch() выдаёт те же коды, а обычный fetch() вообще не имеет maxRetries.

повторное использование keep-alive уже мертвого соединения

undici по умолчанию включает пул соединений с keep-alive. Когда VPN, NAT или proxy незаметно забирает неактивное соединение, клиент этого не узнает и все равно берет это соединение из пула для следующего запроса — запись немедленно получает RST, что проявляется как read ECONNRESET. Это самый частый источник ECONNRESET, когда вызовы идут с паузами, и это объясняет и «ошибки группируются в одном временном окне», и «даже первый повторный запрос завершается неудачей». Проверьте это, отключив повторное использование:

Локальный proxy / VPN: первый hop, который выявляют image endpoints

APIYI напрямую доступен внутри материкового Китая и не требует proxy или VPN (см. Нужен ли мне proxy для использования API?). Поэтому выключить proxy и повторно проверить — это самый дешевый и наиболее информативный одиночный шаг, который вы можете сделать.Но чтобы было ясно: proxy — лишь крупнейший подозреваемый фактор, а не установленная первопричина. Именно матрица ниже реально локализует неисправность.
Два свойства делают image endpoints гораздо более чувствительными, чем текстовые: 30-60 секунд, когда во время генерации не проходит ни одного байта, и тело размером в мегабайты, доставляемое одним всплеском. Когда chat-эндпоинты работают нормально, а image-эндпоинты падают, обычно это один из этих двух случаев.

fake-ip / промах правила маршрутизации

В режиме fake-ip у proxy промах по правилу отправляет вас на недостижимый адрес вроде 198.18.x.x, что приводит к точно 10-секундному тайм-ауту соединения. Учтите: это не «медленное подключение» — маршрута вообще нет, так что повышение connect.timeout не поможет. Всегда фиксируйте remote_ip, который был реально достигнут.

Возвращено как idle connection во время генерации

В течение 30-60 секунд после отправки запроса не идет ни одного байта, и proxy освобождает соединение по своей политике простоя. Признак — время сбоя, приходящееся на круглое число — 30 / 60 / 120 секунд — независимо от размера image.

Черная дыра MTU / PMTUD

MTU туннеля ниже path MTU, при этом ICMP “fragmentation needed” отбрасывается, из-за чего ломается PMTUD. Классический признак — маленькие запросы всегда проходят, большие ответы всегда зависают, а полученные байты замирают на уровне нескольких KB до нескольких десятков KB. Обычно помогает уменьшение MTU туннеля примерно до 1400.

Расшифровка MITM и полная буферизация

Proxy с включенной HTTPS-расшифровкой часто полностью буферизуют большие тела и могут упереться в предел размера, либо переписывают chunked в Content-Length и неверно вычисляют длину, из-за чего возникает RST. И снова это затрагивает только image responses размером в мегабайты, но никогда не затрагивает текстовые вызовы.

Диагностическая матрица

Это основа раздела. Здесь только две оси: сбой произошел до или после первого байта, и сколько байт пришло. Последняя строка — самый ценный одиночный тест: он уменьшает тело с нескольких MB примерно до 1KB. Если режим URL стабильно проходит, а режим base64 стабильно падает, проблема масштабируется вместе с объемом передачи, что сразу исключает первые два столбца.
Правый столбец (отсутствует chunked-терминатор) недавно принял новую форму: нет терминатора, нет закрытия, зависает бесконечно — вместо старого «корректно закрыто через +300 секунд». Общее у обеих форм то, что данные уже полностью получены и изображение можно использовать напрямую, а обработка одна и та же: завершите ответ на стороне клиента. См. Requests Hanging at the End.

Одна команда для полного профиля таймингов

Сопоставьте это с матрицей: нет значения connect -> стадия соединения; нет ttfb и круглое total -> возврат по простоям; полный bytes, но total ≈ ttfb + 300 плюс curl: (18) -> отсутствует terminator; bytes замирает на десятках KB -> MTU.
При A/B-тестировании proxy-on и proxy-off чередуйте прогоны — никогда не запускайте их пачками. Пять прогонов через proxy, а затем пять прямых прогонов позволяют сбою, зависящему от временного окна, исказить результат до полностью неверного вывода; мы измеряли интервалы, когда все падало, затем через несколько минут все работало, а потом снова падало. Вместо этого запускайте proxy → direct → proxy → direct, каждый раз записывая remote_ip.

Other common triggers

Ручное прерывание во время выполнения

Ctrl+C во время отладки, перезапуск процесса, hot reload, завершение работающего скрипта — каждый крупный ответ, который еще находится в пути, оставляет на write_response_body_failed след на шлюзе. Это ложная тревога, которую чаще всего принимают за «канал нестабилен».

Срабатывает внешний таймаут первым

Таймауты worker в очереди задач, лимиты выполнения Serverless, таймауты origin на шлюзе/CDN (обычно по умолчанию 60 секунд). Любой уровень, у которого таймаут меньше времени генерации, первым разрывает соединение — см. Обязательно к прочтению и лучшие практики.

Пул соединений и чрезмерные параллельные запросы

Пределы пула соединений, локальные лимиты file descriptor, NAT/межсетевой экран, незаметно освобождающие долгоживущие соединения. Большие ответы остаются открытыми гораздо дольше, поэтому они намного чаще упираются в эти лимиты, чем текстовые эндпоинты.

Тело ответа исчерпывает память

Один base64-блок размером 4K может достигать 20-30MB. Если загружать его целиком с resp.json() при параллельных запросах, это может исчерпать память контейнера и привести к завершению процесса из-за OOM — что снова выглядит как «соединение оборвалось без причины».

Влияние на тарификацию: какие сбои стоят денег, а какие нет

Эти два сценария постоянно смешивают, хотя тарифицируются они противоположным образом:

Шлюз возвращает 500 write_response_body_failed — не тарифицируется

Эта ошибка означает, что соединение оборвалось в тот момент, когда шлюз записывал обратно вам данные изображения. Платформа автоматически повторяет попытку внутри системы 2-3 раза, и только после неудачи всех попыток возвращает 500.В этом случае списание не происходит. Даже если такие ошибки идут подряд в одном и том же запросе, в вашем счете не появляется соответствующая строка — вы никогда не платите за эти сбои.

Ваш клиент отключился слишком рано — тарифицируется как обычно

Другой случай — когда шлюз завершает доставку, а отключается первым ваша сторона: срабатывает таймаут клиента, во время отладки нажимается Ctrl+C, происходит перезапуск процесса или его принудительно завершает OOM.Генерация на сервере и в upstream уже завершилась, поэтому такие запросы тарифицируются как обычно — «я не получил изображение» не означает «с меня не списали». Многократная отправка крупных запросов на генерацию изображений во время отладки вполне может привести к вполне реальному счету.
Именно так их и различают в таблице выше: проверьте, что в консоли был зарегистрирован обычный вызов или 500 write_response_body_failed. Политику повторных попыток следует выдерживать соответствующе: сбои на уровне транспорта имеет смысл повторять, но каждая повторная попытка может быть отдельно тарифицируемым вызовом (в зависимости от того, к какой из двух категорий она попадет). Никогда не пишите бесконечный цикл повторных попыток.

Как правильно повторять попытку

Основное правило: повторяйте только исключения на уровне транспорта, никогда — ошибки на уровне HTTP. Повторно отправленный 4xx десять тысяч раз все равно остается 4xx, и это лишь тратит время.
Записывайте каждую попытку отдельно (список attempts выше). Иначе при успешной повторной попытке клиента в журналах останется только чистый 200, и вы никогда не увидите, сколько раз фактически ломался транспорт. Эти данные критически важны при оценке качества канала, и они не дают вам ошибочно принять собственные повторные попытки за поведение канала.

Передавайте ответ потоково вместо загрузки целиком

Для больших тел читайте их по частям с stream=True. Это снижает пиковое потребление памяти и показывает вам точно, на каком этапе передачи произошел сбой:
Не повторяйте этот второй случай: данные уже полностью получены, и изображение можно использовать как есть. Полный код завершения на стороне клиента см. в разделе Зависание Requests в конце.

Когда действительно стоит обращаться в поддержку

После того как вы исключили собственную сторону, эскалируйте, если выполняется любое из следующих условий:
  • Проблема по-прежнему стабильно воспроизводится после перехода на корректный интерпретатор OpenSSL и снижения до последовательного выполнения;
  • Сбой наблюдается только у одного конкретного канала или модели, тогда как остальные в том же окне работают нормально;
  • Тело ответа пришло полностью (количество байт совпадает с Content-Length), но соединение не закрывается до тайм-аута — это отсутствие завершающего chunked-терминатора upstream, проблема на стороне канала. Сначала добавьте клиентское завершение, описанное в Requests Hanging at the End, чтобы восстановить изображение, затем создайте тикет, если проблемы останутся;
  • Ошибка однозначно связана с upstream (upstream_error, необработанный upstream 5xx).
В тикет включите: x-request-id, время вызова (с часовым поясом, например 2026-07-29 14:32 (UTC+8)), название модели, ключевые параметры, такие как imageSize, необработанное исключение клиента и шаги самопроверки, которые вы уже выполнили.

Связанная документация

Зависание запросов в конце

Завершение на стороне клиента, когда изображение уже получено, но соединение так и не завершается

Обязательно к прочтению и лучшие практики

Синхронные вызовы, тайм-ауты для каждой модели, обработка base64, тарификация при разрыве соединения

Создайте собственную асинхронную очередь

Оборачивайте синхронные вызовы в очередь задач и компенсируйте редкие обрывы повторными попытками

Нужен ли мне прокси?

APIYI подключается напрямую без прокси; самопроверка сертификата и проблем с DNS

Обработка ошибок генерации изображений Gemini

Коды ошибок и обработка finishReason для генерации изображений Gemini

Сбор ошибок

Как вывести эти ошибки полностью и какие поля включить в обращение в поддержку