09API 基础
重试不重复扣费
如果不确定请求是否送达,可以带着同一个幂等键再次发送:图片和扣费都只会有一次。
这个模型并不支持所有分辨率和宽高比:示例中使用的是它支持的那些。
为什么需要幂等键
连接可能在请求已到达我们、但你尚未收到响应时中断。这时你不知道请求是否已被接收。没有幂等键就再发一次,等于多买了一张图片。
没有幂等键
- 发送了请求——连接中断,没有收到响应。
- 又发送了一次。
- 得到两张图片,被扣费两次。
使用幂等键
- 带着幂等键发送了请求——连接中断。
- 带着同一个幂等键发送了同一个请求。
- 得到相同的
id,只扣费一次。
如何使用
- 在生成请求中传入请求头
X-Idempotency-Key。可以使用 UUID 或你系统中的操作编号:非空字符串,长度不超过 128 个字符,否则返回400 ERR_INVALID_REQUEST。 - 新图片使用新的幂等键。重试同一个请求时使用原来的幂等键。
- 发送之前请先保存幂等键和原样的请求体:重试时需要它们。
- 幂等键只在同一个 API 密钥范围内有效。换一个 API 密钥,即使属于同一账号,也会把该操作视为新的操作。
- 幂等键不会比对请求体。如果用原来的幂等键发送不同的提示词,返回的仍是原来的响应,新的提示词不会产生任何影响。新的操作请使用新的幂等键。
没有这个请求头,就没有防止重复执行的保护。
重试会返回什么
如果第一个请求已被接收,使用同一个幂等键重试会返回相同的 202 响应:id、status、task_id 和 cost 都相同。不会有新的图片,也不会有新的扣费。
{
"id": "5b0214ae-9e39-460a-820a-f6ae3e24400b",
"status": "queued",
"task_id": "5b0214ae-9e39-460a-820a-f6ae3e24400b",
"cost": 0.04
}这是对该次提交保存下来的响应,而不是当前状态:即使图片已经就绪,其中仍可能是 queued。最新状态请始终从 GET /creations/。
带重试的示例
下面的代码最多尝试三次,设置了超时,等待时间逐次变长,并遵循 Retry-After。幂等键每个操作只创建一次,各次尝试之间保持不变。
# 需要 bash、curl 和 uuidgen。每个操作只创建一次幂等键。 IDEMPOTENCY_KEY="$(uuidgen)" || exit 1 curl --fail-with-body --max-time 30 \ -X POST https://api.rendergrid.io/ api/ public/ v1/ images/ generate \ -H "Authorization: Bearer $RENDERGRID_API_KEY" \ -H "Content-Type: application/json" \ -H "X-Idempotency-Key: $IDEMPOTENCY_KEY" \ -d '{ "model": "nano-banana-pro", "prompt": "大理石上的陶瓷杯,柔和的窗光" }'
import os import random import time import requests API = "https://api.rendergrid.io/ api/ public/ v1" KEY = os.environ["RENDERGRID_API_KEY"] def submit( payload: dict, operation_key: str, max_attempts: int = 3 ) -> dict: headers = { "Authorization": f"Bearer {KEY}", "X-Idempotency-Key": operation_key, } for attempt in range(max_attempts): last = attempt + 1 == max_attempts delay = 2 ** (attempt + 1) + random.random() try: resp = requests.post( f"{API}/images/generate", headers=headers, json=payload, timeout=(5, 30), ) except (requests.Timeout, requests.ConnectionError): if last: raise else: if resp.status_code == 202: return resp.json() try: details = resp.json()["error"].get("details") or {} except (ValueError, KeyError, AttributeError): details = {} retryable = ( resp.status_code == 429 or resp.status_code >= 500 or details.get("retryable") is True ) blocked = details.get("reason") == "burst_block" if blocked or not retryable or last: raise RuntimeError(resp.status_code, resp.text) wait = resp.headers.get("Retry-After", "") if wait.isascii() and wait.isdigit(): delay = max(delay, int(wait)) time.sleep(delay) payload = { "model": "nano-banana-pro", "prompt": "大理石上的陶瓷杯,柔和的窗光", } creation = submit(payload, operation_key="order-8412-cover-v1") print(creation["id"])
// Node.js 22+,文件扩展名为 .mjs const API = "https://api.rendergrid.io/ api/ public/ v1"; const KEY = process.env.RENDERGRID_API_KEY; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function submit(payload, operationKey, maxAttempts = 3) { const body = JSON.stringify(payload); for (let attempt = 0; attempt < maxAttempts; attempt++) { const last = attempt + 1 === maxAttempts; let delay = (2 ** (attempt + 1) + Math.random()) * 1_000; let resp; try { resp = await fetch(`${API}/images/generate`, { method: "POST", headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json", "X-Idempotency-Key": operationKey, }, body, signal: AbortSignal.timeout(30_000), }); } catch (error) { const network = error instanceof TypeError; if (!network && error.name !== "TimeoutError") throw error; if (last) throw error; } if (resp) { if (resp.status === 202) return resp.json(); const text = await resp.text(); let details; try { details = JSON.parse(text)?.error?.details; } catch { details = undefined; } const retryable = resp.status === 429 || resp.status >= 500 || details?.retryable === true; const blocked = details?.reason === "burst_block"; if (blocked || !retryable || last) { throw new Error(`HTTP ${resp.status}: ${text}`); } const wait = resp.headers.get("Retry-After"); if (wait && /^[0-9]+$/.test(wait)) { delay = Math.max(delay, Number(wait) * 1_000); } } await sleep(delay); } } const payload = { model: "nano-banana-pro", prompt: "大理石上的陶瓷杯,柔和的窗光", }; const creation = await submit(payload, "order-8412-cover-v1"); console.log(creation.id);
curl 示例只发送一次请求。如果结果未知,请手动用相同的幂等键和请求体重试。
遇到这些情况怎么办
| 情况 | 怎么办 |
|---|---|
| 超时或连接中断,没有响应 | 用相同的幂等键和相同的请求体重试 |
响应为 429:第一个请求仍在接收中 | 响应中有 details.reason: idempotency_in_flight。等待 Retry-After 后,用同一个幂等键重试 |
响应为 400、402 或 404 | 请求未被接收。消除原因后,用同一个幂等键重试 |
响应为 5xx | 用同一个幂等键重试,并限制尝试次数。500 并不说明请求是否已被接收 |
收到 202,之后任务变为 failed | 原来的幂等键不会重新启动任务。再试一次需要新的幂等键:这是一次新的付费操作,而之前那笔费用会自动退回 |
| 需要第二张图片 | 使用新的幂等键发送新的请求 |
请求失败不收费:金额会立即退回余额。
幂等键保存多久
保存的响应有效期为 30 天。之后记录可能被删除,同一个幂等键会启动一次新的付费生成。请自行保存“你的操作 → 任务 id”的对应关系,不要把旧的幂等键当作查找手段。
幂等键并不能在服务故障时提供无条件的保证。同一个操作的请求请依次发送,限制尝试次数,并自行记录。如果重试之后仍无法确定请求是否已被接收,请在用新的幂等键发送之前,先查看请求记录,或联系技术支持。
常见问题
每个请求都需要幂等键吗?
是的,每个生成请求都需要:没有它,连接中断后的重试可能会产生第二张图片和第二次扣费。
可以用同一个幂等键生成不同的图片吗?
不可以。一个幂等键对应一个操作。如果用原来的幂等键发送新的请求,返回的是第一个请求的响应,不会生成新的图片。
幂等键超过 128 个字符会怎样?
API 会返回
400 ERR_INVALID_REQUEST。请改用 UUID 或较短的操作编号。