Повтор без двойного списания: Idempotency-Key

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

Как это работает

Придумайте для запроса уникальное значение, например UUID, и передайте его в заголовке. Первый запрос выполняется как обычно, а его ответ сохраняется на 24 часа. Повтор с тем же значением и тем же телом получает сохранённый ответ: модель не вызывается, баланс не списывается.

curl https://infergate.ru/api/v1/chat/completions \
  -H "Authorization: Bearer $INFERGATE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9f8b7c6d-5e4a-4b3c-8d1e-0f9e8d7c6b5a" \
  -d '{
    "model": "openai/gpt-4o-mini",
    "messages": [{"role": "user", "content": "Привет!"}]
  }'

Значение действует в пределах одного API-ключа: тот же заголовок с другим ключом — это уже другой, независимый запрос. Для каждого нового запроса берите новое значение, а для повторов — прежнее.

Что вернёт повтор

  • Тот же ключ и то же тело — сохранённый ответ с тем же статусом. Для видео это та же задача и 202.
  • Тот же ключ, но другое тело или другой эндпоинт — 422 с кодом idempotency_key_reuse. Тела сравниваются побайтно, поэтому при повторе отправляйте ровно то же, что в первый раз.
  • Первый запрос ещё выполняется — 409. Подождите и повторите с тем же ключом.
  • Первый запрос закончился ошибкой, например 402 или 502, — ошибка не сохраняется, и повтор с тем же ключом выполнится заново.
  • Прошло больше 24 часов — сохранённый ответ удаляется, обычно в течение часа, и после этого запрос выполнится как новый и будет оплачен.
{
  "error": {
    "message": "This Idempotency-Key was used with a different request body.",
    "type": "invalid_request_error",
    "code": "idempotency_key_reuse"
  }
}

Повтор проходит обычные проверки: например, если превышен лимит параллельных запросов, он получит 429, а не сохранённый ответ.

Потоковые запросы

Повтор потокового запроса приходит не потоком, а одним JSON, и его содержимое зависит от API:

  • Anthropic Messages — сообщение целиком, как в ответе без стриминга;
  • Responses API — событие response.completed со всем ответом;
  • Chat Completions — только последний фрагмент потока, с usage. Текст ответа в нём не повторяется.

Если при повторе в Chat Completions вам нужен сам текст, отправляйте такие запросы без стриминга.

Где работает

Заголовок принимают все POST-эндпоинты, кроме синтеза речи (POST/api/v1/audio/speech): Chat Completions, в том числе адрес для Cursor, Responses API, Anthropic Messages, эмбеддинги, реранкинг, решения, изображения, создание видео и распознавание речи. GET-запросы ничего не списывают, и заголовок им не нужен.

Пример с повторами

Ключ создаётся один раз до первой попытки и передаётся во всех повторах:

import os, time, uuid
import openai

client = openai.OpenAI(
    base_url="https://infergate.ru/api/v1",
    api_key=os.environ["INFERGATE_API_KEY"],
    max_retries=0,  # повторяем сами, с одним и тем же ключом
)
key = str(uuid.uuid4())

for attempt in range(4):
    try:
        resp = client.chat.completions.create(
            model="openai/gpt-4o-mini",
            messages=[{"role": "user", "content": "Привет!"}],
            extra_headers={"Idempotency-Key": key},
        )
        break
    except (openai.APIConnectionError, openai.ConflictError, openai.InternalServerError):
        time.sleep(2 ** attempt)

APIConnectionError — обрыв или таймаут, ConflictError — 409, первый запрос ещё идёт, InternalServerError — ответы 5xx.