Документация API go2llm

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 — единственному эндпоинту, где один вызов создаёт один платный артефакт разом, а не поток токенов, который можно оборвать и не оплатить недополученное.

Связанные ошибки

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. Цены на все модели — на странице тарифов, инструкции по подключению — в документации.