Idempotency-Key で安全にリトライする

· Hyperouter · 読了目安 6 分

料金がかかるリクエストを送信した直後に、接続が切れたとします。Hyperouter はそのリクエストを受信したでしょうか。確かめる方法はありません。受信されていたのに再送すると、2 つ目のジョブが開始され、二重に支払うことになります。

Idempotency-Key ヘッダーを付ければ、この迷いはなくなります。Hyperouter は再送を識別し、2 つ目のジョブを開始せず、2 回目のクレジット確保も行わずに、最初のリクエストの結果で応答します。

料金がかかるすべてのリクエストに付ける

新しいリクエストごとに、UUID v4 などの新しいランダムな値を生成します。リクエストと一緒に保存しておき、そのリクエストを再送するたびに同じ値を送信してください。

curl https://api.hyperouter.app/v1/images/generations \
  -H "Authorization: Bearer $HYPEROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7" \
  -d '{"model": "x-ai/grok-imagine-image", "prompt": "A lighthouse at dusk", "n": 1}'

OpenAI SDK は、このヘッダーを自動では送信しません。追加のヘッダーとして渡してください。SDK は、自動リトライでもそれを変更せずに再送します。

import uuid

key = str(uuid.uuid4())  # one per request; reuse it when you retry this request
img = client.images.generate(
    model="x-ai/grok-imagine-image",
    prompt="A lighthouse at dusk",
    extra_headers={"Idempotency-Key": key},
)

エンドポイントごとの再送で返されるもの

Endpoint再送で返されるもの
POST /v1/images/generations、POST /v1/images/edits最初のリクエストの画像。Prefer: respond-async の場合は、同じ id を持つ 202。
POST /v1/videos、/v1/videos/edits、/v1/videos/extensions、/v1/videos/{id}/remix最初のリクエストの Video オブジェクト(現在の状態)。
POST /v1/chat/completions最初のリクエストの ID を伴う 409 idempotency_key_replayed。

チャットは異なります

チャットの応答は再送されません。同じキーで再送すると、コード idempotency_key_replayed の 409 が返り、error.generation_id が最初のリクエストの ID になります。リクエストは再実行も再課金もされません。

{
  "error": {
    "message": "A request with this Idempotency-Key already ran as generation 019a1f3c-5b7e-7d21-9c4a-2e8f6b0d3a17. ...",
    "type": "conflict",
    "code": "idempotency_key_replayed",
    "param": null,
    "generation_id": "019a1f3c-5b7e-7d21-9c4a-2e8f6b0d3a17"
  }
}

最初のリクエストは GET /v1/generations/{id} で調べると、ステータス、利用量、課金額がわかります。最初のチャット補完が失敗して課金されなかった場合は、そのキーは解放され、再送は新しいリクエストとして実行されます。

つまりチャットでは、キーは二重課金を防ぐためのもので、同じ回答をもう一度取得する手段ではありません。409 は「すでに実行された」という意味と受け取り、記録を読み取ってください。

import os

import httpx
import openai

try:
    res = client.chat.completions.create(
        model="x-ai/grok-4.7",
        messages=[{"role": "user", "content": "Hello"}],
        extra_headers={"Idempotency-Key": key},
    )
except openai.ConflictError as e:
    body = e.body or {}  # the error object
    if body.get("code") != "idempotency_key_replayed":
        raise
    record = httpx.get(
        f"https://api.hyperouter.app/v1/generations/{body['generation_id']}",
        headers={"Authorization": f"Bearer {os.environ['HYPEROUTER_API_KEY']}"},
    ).json()["data"]

判定には message ではなく、常に code を使ってください。

再送で返されるもの

送信するものHyperouter の応答
新しいキー通常どおりリクエストを実行します。
同じキーと同じリクエスト(最初のリクエストが受理された後)上の表のとおり、最初のリクエストの結果を返します。2 つ目のジョブも、2 回目のクレジット確保も、2 回目の課金もありません。
同じキーと同じリクエスト(最初のリクエストがまだ受理中)最初のリクエストを待ってから、上記のとおり応答します。最初のリクエストが 60 秒以内にジョブを開始しなかった場合は、409 idempotency_key_in_use が返ります。同じキーで再試行してください。
同じキーと異なるリクエスト409 idempotency_key_reused。何も実行されません。新しいキーを使ってください。
同じキー(最初のリクエストがジョブの開始前に拒否された後。400、402、または x-should-retry: false なしで送信された 5xx)リクエストを新規として実行するため、修正して同じキーで再送できます。
同じキー(最初のリクエストから 30 分以上経過した後)リクエストを新規として実行します。
255 文字を超える、または印字可能な ASCII ではないキー400 invalid_idempotency_key。何も実行されません。

ルール

SDK の自動リトライ

OpenAI SDK は、408、409、429、5xx のレスポンスを、既定で 2 回再試行します。ただし、レスポンスにヘッダー x-should-retry: false が付いている場合は再試行しません。Hyperouter は、再試行すると 2 つ目のジョブや 2 回目の課金が発生するすべてのレスポンスに、このヘッダーを付けて送信します。対象は、同期画像の 503 timeout、ジョブの作成後の 500、409 idempotency_key_reused と idempotency_key_replayed です。

429、409 idempotency_key_in_use、ジョブが存在する前の 5xx には、このヘッダーを付けません。これらは安全に再試行できます。Idempotency-Key を付けていれば、すべての再試行が安全です。

詳しいルールは、リトライと冪等性に掲載しています。