Повтор без двойного списания: 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.