OpenAI Python SDK: таймауты, ретраи, ошибки и сырые ответы
Клиент openai для Python умеет больше, чем create(). Разбираем таймауты, повторы, исключения, сырые ответы и стриминг на примере шлюза.
5 мин · · разработка · ошибки
Большинство примеров с библиотекой openai для Python заканчиваются на client.chat.completions.create(...). В продакшене начинаются вопросы: сколько ждать ответ, что повторять автоматически, как отличить неверный ключ от пустого баланса, где взять заголовки ответа. Ниже настройки клиента openai python sdk, которые редко объясняют, на примере работы через OpenAI-совместимый шлюз.
Описание сверено с README библиотеки openai-python на 18 октября 2026. Версии SDK меняются быстро (в README этой даты, например, уже фигурирует HTTP-клиент httpx2), поэтому версию своей установки проверьте командой python -c "import openai; print(openai.__version__)", а детали сверьте с README и changelog.
Клиент и переменные окружения
Минимальный клиент для шлюза Zukko:
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.zukko.pro/v1",
api_key=os.environ["ZUKKO_API_KEY"],
)Адрес для OpenAI-формата с /v1. Вместо параметров можно задать переменные окружения OPENAI_BASE_URL и OPENAI_API_KEY, и тогда в коде достаточно OpenAI(). Ключ fetch_... создаётся в кабинете. В README библиотеки основным способом генерации текста назван Responses API, а Chat Completions остаётся поддерживаемым. Оба доступны у шлюза: /v1/chat/completions и /v1/responses. Дальше примеры на chat.completions, но настройки клиента одинаковы для обоих.
Таймауты
По умолчанию запрос ждёт до 10 минут. Для интерактивного приложения это много. Таймаут задаётся числом в секундах:
client = OpenAI(
base_url="https://api.zukko.pro/v1",
api_key=os.environ["ZUKKO_API_KEY"],
timeout=30.0,
)
long_client = client.with_options(timeout=180.0) # для одного тяжёлого запросаМетод with_options возвращает копию клиента с изменёнными настройками, исходный клиент остаётся прежним. Вместо числа можно передать объект с раздельными таймаутами на подключение, чтение и запись. Но тип этого объекта зависит от версии SDK: в README на момент проверки это httpx2.Timeout, в более старых версиях httpx.Timeout. Если не хотите зависеть от версии, оставайтесь на числе.
При превышении выбрасывается openai.APITimeoutError. Учтите, что запрос, который превысил таймаут, по умолчанию повторяется ещё дважды. Три таймаута по 30 секунд дадут полторы минуты ожидания, прежде чем вы увидите ошибку.
Для длинных генераций надёжнее включить стриминг: таймаут чтения действует на паузы между кусками, а не на весь ответ. Подробнее: таймауты при длинных генерациях.
Ретраи: что повторяется и чего повторять не стоит
SDK автоматически повторяет запросы по умолчанию 2 раза с экспоненциальной паузой. Повторяются ошибки соединения, ответы 408, 409, 429 и все 5xx, причём только если тело запроса можно безопасно отправить заново. Настраивается через max_retries:
client = OpenAI(
base_url="https://api.zukko.pro/v1",
api_key=os.environ["ZUKKO_API_KEY"],
max_retries=2, # по умолчанию 2, 0 отключает повторы
)
r = client.with_options(max_retries=5).chat.completions.create(
model="gpt-6.1-sol",
messages=[{"role": "user", "content": "Привет"}],
)Для шлюза у этого правила есть нюансы, которые полезно знать:
- 429. Это может быть ограничение у провайдера модели, и тогда повтор через паузу помогает. Но если исчерпан дневной или месячный лимит ключа (
key_daily_limit_reached,key_monthly_limit_reached), повторять бессмысленно до следующего периода или пока вы не поднимете лимит. - 503. Провайдер временно недоступен: повтор уместен. Код
model_disabledозначает, что модель выключена. Тогда нужно переключиться на другую изGET /v1/models, а не повторять. - 401, 402, 403, 404. Не повторяются: неверный ключ, не хватает баланса, ключу запрещена модель, неизвестное имя модели.
Если запрос нужно безопасно повторять после обрыва и не заплатить дважды, посмотрите про заголовок Idempotency-Key в статье Idempotency-Key: как ретраить запросы.
Классы ошибок
SDK превращает ответы в исключения. Все они наследуют openai.APIError. Для кодов статуса есть свои классы: 400 BadRequestError, 401 AuthenticationError, 403 PermissionDeniedError, 404 NotFoundError, 422 UnprocessableEntityError, 429 RateLimitError, 5xx InternalServerError. Если не удалось подключиться, это APIConnectionError, а его подкласс APITimeoutError означает таймаут.
Соответствие кодам шлюза:
| Что случилось | Код | Исключение |
|---|---|---|
| неверный ключ | 401 | AuthenticationError |
| не хватает баланса | 402 | общий APIStatusError, смотрите status_code |
| модель не разрешена ключу | 403 model_not_allowed_for_key | PermissionDeniedError |
| неизвестное имя модели | 404 model_not_found | NotFoundError |
| слишком большой запрос | 413 | общий APIStatusError |
| лимит ключа или провайдера | 429 | RateLimitError |
| провайдер недоступен, модель выключена | 503 model_disabled | InternalServerError |
Порядок except важен: сначала узкие классы, затем APIStatusError.
import openai
try:
r = client.chat.completions.create(
model="gpt-6.1-sol",
messages=[{"role": "user", "content": "Привет"}],
)
except openai.AuthenticationError:
print("401: проверьте ключ fetch_...")
except openai.PermissionDeniedError as e:
print("403: модель не разрешена этому ключу", e.body)
except openai.NotFoundError as e:
print("404: неверное имя модели, сверьтесь с GET /v1/models", e.body)
except openai.RateLimitError as e:В e.body лежит разобранное тело ошибки, а в e.response.headers заголовки. Какое поле (code или type) у шлюза содержит строки вроде model_not_found, надёжнее всего посмотреть на реальном ответе, напечатав e.body, и дальше ветвиться по нему. Общий разбор кодов: 401, 402, model not found.
Сырые ответы и заголовки
Чтобы получить заголовки и статус, добавьте with_raw_response перед методом:
raw = client.chat.completions.with_raw_response.create(
model="deepseek-v4.1-flash",
messages=[{"role": "user", "content": "Скажи «тест»"}],
)
print(raw.status_code)
print(dict(raw.headers))
completion = raw.parse()
print(completion.choices[0].message.content)parse() возвращает обычный объект ответа. Для стриминга есть with_streaming_response как менеджер контекста. Идентификатор запроса попадает в response._request_id, если сервер вернул соответствующий заголовок, а для неудачных запросов он лежит в e.request_id. Какие именно заголовки отдаёт шлюз, лучше посмотреть, напечатав dict(raw.headers): не стройте логику на заголовках, которых вы не видели в живом ответе.
Стриминг и учёт токенов
В потоке OpenAI-формата Zukko всегда присылает последний чанк с usage и пустым choices, поэтому проверяйте if chunk.choices: перед chunk.choices[0]:
stream = client.chat.completions.create(
model="gpt-6.1-sol",
messages=[{"role": "user", "content": "Расскажи про ретраи в двух предложениях"}],
stream=True,
)
usage = None
for chunk in stream:
if chunk.choices:
print(chunk.choices[0].delta.content or "", end="", flush=True)
if chunk.usage:
usage = chunk.usageОшибка в середине потока вылетает как APITimeoutError (таймаут чтения) или APIConnectionError. Повтор для потока автоматически не делается: иначе уже выданный текст мог бы продублироваться. Решайте сами: перезапустить запрос целиком или вернуть пользователю то, что успели получить.
По usage можно сразу считать стоимость. Цены на 18 октября 2026, доллары за 1M токенов: gpt-6.1-sol 0.27 за вход и 1.32 за выход, deepseek-v4.1-flash 0.013 и 0.038.
PRICES = {"gpt-6.1-sol": (0.27, 1.32), "deepseek-v4.1-flash": (0.013, 0.038)}
def cost(model, usage):
p_in, p_out = PRICES[model]
return usage.prompt_tokens / 1e6 * p_in + usage.completion_tokens / 1e6 * p_outПример: 1 200 токенов входа и 300 токенов выхода на gpt-6.1-sol дают 0.0012 × 0.27 + 0.0003 × 1.32 = 0.000324 + 0.000396 = $0.00072. Это верхняя оценка: у GPT часть входа может прийти из кэша (usage.prompt_tokens_details.cached_tokens), и она стоит по цене чтения кэша, 0.013. Сверяйте итог со списаниями в кабинете.
Асинхронный клиент и отладка
AsyncOpenAI принимает те же параметры (base_url, timeout, max_retries), а вызовы пишутся с await. Для массовых запросов ограничивайте параллелизм asyncio.Semaphore, иначе сотни одновременных запросов упрутся в 429 у провайдера. Чтобы увидеть, что реально отправляет SDK, включите логирование: export OPENAI_LOG=info (или debug для подробного вывода).
Доступ из России — через шлюз, оплата СБП (платёжная страница добавляет 8,5% сверху), картой MasterCard или криптой. Ещё материалы: справочник по Python.
Частые вопросы
Как поменять таймаут только для одного запроса?
client.with_options(timeout=120.0).chat.completions.create(...). Исходный клиент не меняется.
Почему запрос с таймаутом 30 секунд висит дольше?
После таймаута SDK повторяет запрос ещё до двух раз. Задайте max_retries=0 или меньший таймаут, если нужен быстрый отказ.
Нужно ли ловить 402 отдельно?
Для него в таблице SDK нет отдельного класса, поэтому ловите APIStatusError и проверяйте status_code == 402.
Как отключить повторы совсем?
OpenAI(..., max_retries=0). Повторы стоит выключать там, где вы сами реализуете логику отказа или защиту от дублей.
zukko
Платите в 25×
меньше
за Claude Code & ChatGPT
Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.
Похожие статьи
- Как обновить Claude Code, откатить версию или полностью удалитьКоманды для обновления, отката и удаления Claude Code по способу установки, и как не потерять настройки подключения к шлюзу при переустановке.
- Ключи OpenAI API в открытом доступе: почему ими нельзя пользоватьсяОткуда берутся «бесплатные» ключи на GitHub, почему пользоваться ими опасно и для вас, и для владельца, и как защитить собственный ключ от такой же утечки.
- Ошибки GLM API: почему не работает запрос и как это исправитьНеверный id модели, переполненный контекст 128K, ключ, баланс, лимиты: типичные причины ошибок GLM API и быстрый способ найти свою.
- This organization has been disabled в Claude API: что это и как продолжитьКлюч Anthropic внезапно отвечает «organization has been disabled». Объясняем, что это за ошибка, что проверить и как перенести SDK и Claude Code на другой провайдер.