用 Idempotency-Key 安全重试
你发送了一个会产生费用的请求,连接断了。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}, )
import { randomUUID } from "node:crypto"; const key = randomUUID(); // one per request; reuse it when you retry this request const img = await client.images.generate( { model: "x-ai/grok-imagine-image", prompt: "A lighthouse at dusk" }, { 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。不运行任何内容。 |
规则
- 作用域。 键归属于发送它的 API 密钥。两个 API 密钥可以使用相同的值,互不影响。
- 有效期。 键在首次请求后 30 分钟内有效。请在此时间内重试。
- 相同请求。 方法、路径和请求体都一致时,两个请求视为相同。JSON 会在解析后比较,因此字段顺序和空白不影响判断。对于 multipart 上传,文件字节也会参与比较。
- 计费。 使用同一个键发出的请求,无论重复多少次,最多只计费一次。由首次请求的结果直接应答的重复请求不产生费用。
- 保证。 同一个键绝不会启动多于一个任务,包括两个重复请求同时到达的情况。
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 时,所有重试都是安全的。
完整规则见重试与幂等。