用 Idempotency-Key 安全重试

· Hyperouter · 阅读约 4 分钟

你发送了一个会产生费用的请求,连接断了。Hyperouter 收到了吗?你无从得知。如果再发一次,而它其实已经送达,就会启动第二个任务,你要付两次钱。

Idempotency-Key 请求头可以消除这种疑虑。Hyperouter 会识别出重复请求,不启动第二个任务,不再次预扣,并直接返回首次请求的结果。

对每一个会产生费用的请求都发送一个

为每个新请求生成一个新的随机值,例如 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},
)

各接口如何重放

接口重复请求返回
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。

聊天补全不同

聊天回复不会被重放。使用相同键的重复请求会返回 409,code 为 idempotency_key_replayed,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"]

请以 code 匹配,绝不要以 message 匹配。

重复请求的返回结果

你发送的内容Hyperouter 的响应
新的键照常运行请求。
相同的键和相同的请求,且首次请求已被接受如上表所示,返回首次请求的结果。不启动第二个任务,不再次预扣,不再次扣费。
相同的键和相同的请求,但首次请求仍在受理中等待首次请求,然后按上述方式响应。如果首次请求在 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 响应重试两次,除非响应带有响应头 x-should-retry: false。凡是重试会启动第二个任务或产生第二次扣费的响应,Hyperouter 都会附带该响应头:同步图像请求的 503 timeout、任务创建之后出现的 500,以及 409 idempotency_key_reused 和 idempotency_key_replayed。

对于 429、409 idempotency_key_in_use,以及任务创建之前的 5xx,Hyperouter 从不附带该响应头,这些情况可以放心重试。使用 Idempotency-Key 时,所有重试都是安全的。

完整规则见重试与幂等。