Повторы и сбои
Сеть и поставщики иногда подводят. Здесь написано, что делать, чтобы не потерять деньги и не заплатить дважды.
Платный запрос мы сами не повторяем: если поставщик не ответил, генерация могла всё же состояться. Поэтому повторять нужно вам, и безопасно это делается с заголовком Idempotency-Key.
Idempotency-Key
Добавьте к платному запросу заголовок Idempotency-Key: любая уникальная строка до 255 знаков, например UUID. Если запрос оборвался и вы отправите его снова с тем же ключом, вторая генерация не запустится и деньги второй раз не спишутся.
curl https://api.6io.io/v1/chat/completions \
-H "Authorization: Bearer sixio_sk_..." \
-H "Idempotency-Key: 6f1d3a52-8b0e-4c1c-9a4e-2f1f0f8b7c11" \
-H "Content-Type: application/json" \
-d '{"model": "op/openai/gpt-4o-mini", "messages": [{"role": "user", "content": "Привет!"}]}'| Что вы отправили | Ответ |
|---|---|
| новый ключ | обычная обработка |
| тот же ключ и тот же запрос | 409 idempotent_request_exists, в заголовке X-Sixio-Request-Id номер первого запроса; новой генерации нет |
| тот же ключ, другой запрос | 422 idempotency_key_reused |
тот же ключ после отказа без списания (например, 503 upstream_busy или 4xx поставщика) |
обычная обработка: ключ освобождается вместе с резервом |
Ключ действует в пределах вашего аккаунта. Один ключ — одна задача, а не одна попытка: при повторе берите тот же.
Готовый ответ повтором не возвращается: мы не храним тексты запросов и ответов. После 409 можно посмотреть состояние первого запроса через GET /v1/requests/{id}, но сам текст ответа получить уже нельзя. Если ответ нужен надёжно, используйте фоновые задания: их результат хранится сутки.
Что делать при ошибке
| Что вы видите | Что случилось | Деньги | Что делать |
|---|---|---|---|
429 и Retry-After |
сработал лимит аккаунта или IP | не списаны | подождать Retry-After секунд и повторить |
503 upstream_busy |
поставщик не принял запрос из-за частоты или до него не достучались | не списаны | повторить после Retry-After |
503 service_unavailable |
сервис временно недоступен | не списаны | повторить через несколько секунд |
402 insufficient_funds |
не хватает доступных средств | не списаны | пополнить баланс |
4xx от модели |
в запросе что-то не так | резерв снят | исправить запрос |
502 или 503 upstream_unavailable |
поставщик не ответил, генерация могла пройти | резерв удержан | не повторять вслепую: посмотреть GET /v1/requests/{id} или повторить с тем же Idempotency-Key |
| соединение оборвалось после отправки | вы не знаете, дошёл ли запрос | возможно, списаны | повторить с тем же Idempotency-Key: ответ 409 покажет номер первого запроса |
Безопасный повтор в коде
import time
import uuid
import requests
API = "https://api.6io.io/v1"
H = {"Authorization": "Bearer sixio_sk_..."}
body = {
"model": "op/openai/gpt-4o-mini",
"messages": [{"role": "user", "content": "Привет!"}],
"max_tokens": 100,
}
key = str(uuid.uuid4()) # один ключ на задачу, а не на попытку
for attempt in range(4):
try:
r = requests.post(f"{API}/chat/completions", headers={**H, "Idempotency-Key": key}, json=body, timeout=120)
except requests.RequestException:
time.sleep(2) # обрыв: исход неизвестен, повтор с тем же ключом безопасен
continue
if r.status_code == 409: # такой запрос уже принят
print("уже принят, номер:", r.headers["X-Sixio-Request-Id"])
break
if r.status_code in (429, 503): # ничего не списано, либо исход проверяется (тогда следующий ответ будет 409)
time.sleep(int(r.headers.get("Retry-After", "5")))
continue
print(r.status_code, r.json())
breakЕсли исход так и остался неизвестным
Запрос с неизвестным исходом не теряется. Мы проверяем у поставщика, сколько он стоил, и закрываем деньги: списываем факт или снимаем резерв. Это видно в billing_status (billing_pending, затем billing_review). Если сумму не удалось подтвердить автоматически за сутки, запрос уходит в ручной разбор. Резерв в это время остаётся, а решение принимает администратор.