409 idempotency_conflict: Idempotency-Key переиспользован для другого запроса
Ошибка 409 idempotency_conflict в go2llm означает, что заголовок Idempotency-Key уже использовался для запроса с другим телом. У go2llm это проверка только на POST /v1/images/generations — идемпотентность здесь обязательна, потому что генерация изображения стоит реальных денег и не должна дублироваться при повторной отправке одного и того же запроса. Под тем же статусом 409 есть вторая, отдельная причина — request_in_progress, когда первый запрос с этим ключом ещё не завершился.
Страница составлена 2026-09-22, обновлена 2026-09-22.
Как выглядит ошибка
go2llm отвечает статусом 409 и стандартным конвертом ошибки — error.type и error.code равны idempotency_conflict, error.message объясняет причину словами, error.request_id начинается с gw_. Точный текст message для конфликта живым запросом не зафиксирован — воспроизвести его можно только с действующим ключом go2llm (два запроса с одинаковым Idempotency-Key и разным телом), а бесплатно, без ключа, авторизация проверяется раньше идемпотентности и любой запрос на этот endpoint без ключа получает 401 no-key, до проверки конфликта дело не доходит.
Второй сценарий под тем же статусом — заголовок retry-after присутствует только на нём: если запрос с этим Idempotency-Key ещё выполняется, go2llm отвечает 409 request_in_progress с Retry-After в секундах, а не сразу повторяет генерацию. У idempotency_conflict заголовка Retry-After нет: повторный запрос с тем же ключом и другим телом не станет валидным сам по себе, сколько ни жди.
Причины
1. Один и тот же ключ отправлен с другим телом запроса
Классический случай — код обновил промпт после неудачной или отменённой попытки, но переиспользовал старую переменную с Idempotency-Key. go2llm видит: ключ тот же, тело другое — и отказывает, а не выполняет новый запрос под старым ключом молча.
2. Идентификатор задачи используется напрямую как Idempotency-Key без версии
Если ключ генерируется из ID заказа или ID задачи без указания версии попытки (order-42 вместо order-42-attempt-2), любое изменение параметров генерации для того же заказа — другой размер, другой промпт, другая модель — даст idempotency_conflict, даже если по смыслу это законное новое изображение для того же заказа.
3. Повторная отправка ушла раньше, чем завершился первый запрос
Клиент с коротким таймаутом или агрессивным ретраем может отправить повтор с тем же Idempotency-Key, пока первый запрос ещё обрабатывается upstream-провайдером. Это не idempotency_conflict — это request_in_progress, и он не про ошибку клиента, а про то, что genерация ещё не закончилась.
4. Ключ не соответствует формату — это другая ошибка, не 409
go2llm требует 8–128 печатных ASCII-символов для Idempotency-Key. Ключ короче, длиннее или с непечатными/не-ASCII символами (например, случайно попавшими юникодными кавычками из буфера обмена) даёт не 409, а 400 idempotency_key_required — проверено живым запросом:
{
"error": {
"type": "idempotency_key_required",
"code": "idempotency_key_required",
"message": "Idempotency-Key must be 8-128 printable ASCII characters",
"request_id": "gw_e8287de12a754324b9792feedaf6007c"
}
}
Это отдельная проверка формата, которая срабатывает раньше поиска конфликта: сервер сначала убеждается, что заголовок вообще валиден, и только потом сравнивает тело с уже сохранённым под этим ключом.
Как починить по шагам
Шаг 1. Проверьте формат ключа перед отправкой:
def valid_idempotency_key(key: str) -> bool:
return 8 <= len(key) <= 128 and key.isascii() and key.isprintable()
Шаг 2. Генерируйте ключ из связки «что генерируем + попытка», а не только из ID задачи:
import hashlib
def make_idempotency_key(order_id: str, attempt: int, prompt: str) -> str:
payload_hash = hashlib.sha256(prompt.encode()).hexdigest()[:16]
return f"{order_id}-a{attempt}-{payload_hash}"[:128]
Хэш от содержимого промпта в ключе — не требование go2llm, а практический приём: если тело запроса действительно не изменилось, такой ключ естественным образом останется прежним и повтор с ним будет безопасным ретраем, а не конфликтом. Изменилось содержимое — изменится и ключ, конфликт не возникнет вовсе.
Шаг 3. Различайте два кода 409 в обработчике — реагировать на них нужно по-разному:
import time
import httpx
def generate_image(payload: dict, idempotency_key: str, api_key: str):
with httpx.Client(timeout=60.0) as client:
r = client.post(
"https://go2llm.tech/v1/images/generations",
headers={
"Authorization": f"Bearer {api_key}",
"Idempotency-Key": idempotency_key,
"content-type": "application/json",
},
json=payload,
)
if r.status_code == 409:
err = r.json().get("error", {})
if err.get("code") == "request_in_progress":
delay = float(r.headers.get("retry-after", 2))
time.sleep(delay)
return generate_image(payload, idempotency_key, api_key)
raise ValueError(
f"Idempotency-Key {idempotency_key!r} уже занят другим телом запроса — "
f"смените ключ, не повторяйте с тем же"
)
r.raise_for_status()
return r.json()
Шаг 4. Проверьте живым запросом на минимальном примере:
curl https://go2llm.tech/v1/images/generations \
-H "Authorization: Bearer $GO2LLM_API_KEY" \
-H "Idempotency-Key: test-run-$(date +%s)" \
-H "content-type: application/json" \
-d '{"model":"gpt-image-2","prompt":"Минималистичная схема API-шлюза","n":1,"size":"1024x1024","quality":"standard","response_format":"url"}'
Уникальный ключ на базе $(date +%s) в примере гарантирует, что запрос не столкнётся со старым использованным ключом при повторных проверках.
Зачем вообще нужен Idempotency-Key
Идемпотентность через заголовок с уникальным ключом — общий паттерн для API, где повтор запроса стоит денег: если сеть оборвалась после того, как сервер принял запрос, но до того, как клиент увидел ответ, клиент не может отличить «запрос не дошёл» от «запрос выполнился, а ответ потерялся». Без идемпотентности безопасный на вид ретрай молча создаёт вторую генерацию и второе списание. Idempotency-Key решает это на уровне протокола: сервер помнит, какой ключ с каким телом уже обрабатывался, и отказывает вместо того, чтобы выполнить операцию повторно.
У go2llm это применимо только к POST /v1/images/generations — единственному эндпоинту, где один вызов создаёт один платный артефакт разом, а не поток токенов, который можно оборвать и не оплатить недополученное.
Связанные ошибки
- 413 request_too_large — тоже про
/v1/images/generations, но про размер тела запроса, а не про повтор ключа. - 400 unsupported_content_type — бинарные вложения в текстовых endpoint'ах, другой класс запросов.
- 429 rate_limit_error — лимит частоты запросов, не связан с повтором конкретного ключа.
- Справочник всех кодов и docs/errors.
FAQ
Нужен ли Idempotency-Key для /v1/messages или /v1/chat/completions? Нет, go2llm требует его только для POST /v1/images/generations. Текстовые endpoint'ы идемпотентность на уровне заголовка не проверяют.
Можно ли переиспользовать один и тот же Idempotency-Key для двух реально одинаковых запросов? Да, это штатный сценарий: если тело запроса идентично, повтор с тем же ключом — безопасный ретрай, а не ошибка. 409 idempotency_conflict возникает только при разном теле под одним ключом.
Списываются ли деньги за запрос, который вернул 409? Нет ни в одном из двух случаев. idempotency_conflict останавливает запрос до выполнения генерации, request_in_progress — это ответ по уже отправленному первому запросу, повторное списание по нему не создаётся.
Сколько ждать после request_in_progress? Ровно столько, сколько указано в заголовке Retry-After — go2llm ставит его именно для этого случая. Жёстко закодированную паузу использовать не стоит: реальное время генерации у разных моделей отличается.
Почему на ошибку формата ключа пришёл 400, а не 409? 409 значит «конфликт с уже существующим состоянием» — а если ключ невалиден по формату, сравнивать ещё нечего: сервер отклоняет заголовок на этапе валидации запроса, раньше, чем ищет совпадение по ключу. Отличать эти два кода в обработчике стоит по error.code, а не по HTTP-статусу — idempotency_key_required чинится изменением формата ключа, idempotency_conflict — сменой ключа на новый.
Получить ключ и сделать первый запрос за 3 минуты → go2llm.tech. Цены на все модели — на странице тарифов, инструкции по подключению — в документации.