图片生成
生成图片走一套独立的异步任务接口:提交拿任务号,轮询等结果,结果 URL 24 小时内有效。
别名 gpt-image-2-official。它不是对话模型,不能填进 /v1/chat/completions 或 /v1/messages。
为什么是异步的
一张图片要几秒到几十秒,同步接口在这个时间尺度上不可靠:连接断了你就不知道钱扣没扣、图出没出。所以这里把「接单」和「取货」分开:
POST /v1/image-tasks落库接单,返回任务号和预占金额,202。这一步是原子的:任务、冻结的价格、预占的资金要么都在,要么都不在。GET /v1/image-tasks/{task_id}轮询状态和结果,免费,余额为零也能读。GET /v1/image-tasks/by-client/{client_request_id}用你自己的编号找回——接单响应丢了也能拿回任务,而且不会重新生成一张。
提交
curl https://api.apipace.com/v1/image-tasks \
-H "x-api-key: $APIPACE_KEY" \
-H "content-type: application/json" \
-H "Idempotency-Key: docs-img-20260916-a" \
-d '{
"client_request_id": "docs-img-20260916-a",
"submit_not_after": "2026-09-16T04:16:51Z",
"model": "gpt-image-2-official",
"prompt": "一只橘猫坐在窗台上晒太阳,水彩风格",
"size": "1:1",
"resolution": "1k",
"quality": "low",
"n": 1
}'
{
"request_id": "req_01M2M6SF2M9ZNXTK1CR7R1F32R",
"task_id": "img_01M2M6SF2VG5NWRXQW48DPC4A4",
"status": "accepted",
"billing": {"status":"held","reserved_micro_cny":101785,"sales_micro_cny":0,"currency":"CNY"},
"hold_expires_at": "2026-09-16T04:49:53.535047Z",
"retry_after": 5
}
字段
| 字段 | 必填 | 说明 |
|---|---|---|
client_request_id |
是 | 你自己生成的唯一编号,1–128 字符。Idempotency-Key 头必须填成同一个值 |
submit_not_after |
是 | 接单截止时间(RFC 3339),必须在未来 3 分钟以内。超出返回 INVALID_SUBMIT_DEADLINE |
model |
是 | gpt-image-2-official |
prompt |
是 | 1–64000 字符 |
size |
是 | auto 1:1 3:2 2:3 4:3 3:4 5:4 4:5 16:9 9:16 2:1 1:2 21:9 9:21 |
resolution |
是 | 1k 2k 4k。4k 只支持 16:9 9:16 2:1 1:2 21:9 9:21 |
quality |
是 | low medium high |
n |
是 | 恒为 1,一次只出一张 |
image_urls |
否 | 参考图,最多 4 张。可以是 https:// URL,也可以是 data:image/png;base64,…(png / jpeg / webp,单张解码后 ≤ 1 MiB)。整个请求体 ≤ 8 MiB |
submit_not_after 是给你自己上的保险:网络抖动导致请求晚到时,过了这个点就不会再开一张你已经不想要的图。
轮询
curl https://api.apipace.com/v1/image-tasks/img_01M2M6SF2VG5NWRXQW48DPC4A4 \
-H "x-api-key: $APIPACE_KEY"
{
"request_id": "req_01M2M6T40PJWYGQA2827XQBP73",
"task_id": "img_01M2M6SF2VG5NWRXQW48DPC4A4",
"status": "succeeded",
"billing": {"status":"charged","sales_micro_cny":41965,"currency":"CNY"},
"hold_expires_at": "2026-09-16T04:49:53.535047Z",
"images": [{"url":"https://…/image/9998210467891166-ea292e00-…","expires_at":1789618508}],
"retry_after": 5
}
按响应里的 retry_after 秒数轮询就行(上面这张 1K / low 的图,提交后第 12 秒变 succeeded)。
status |
含义 |
|---|---|
accepted |
已接单,还没开始 |
dispatching / running |
正在生成 |
succeeded |
出图了,看 images[] |
failed |
失败,看 error_code;预占已释放,不扣钱 |
unknown |
结果暂时确认不了。不要重新提交一个新编号,继续按 retry_after 轮询原任务 |
unknown 是这套接口最值得理解的一个状态:它表示「这单可能已经在生成了」。此时新开一单等于付两份钱,所以平台宁可让你等,也不会替你重试。超过 hold_expires_at 预占会自动释放,迟到的成功结果不再向你追扣。
用自己的编号找回
接单响应在网络里丢了?拿你提交时那个 client_request_id 查:
curl https://api.apipace.com/v1/image-tasks/by-client/docs-img-20260916-a \
-H "x-api-key: $APIPACE_KEY"
返回的就是原任务,查询不会创建也不会重新生成。
也可以直接重放提交请求:Idempotency-Key 与请求体都完全相同时返回 202 + 原任务(不会再生成一张,也不会再扣一次);编号相同但请求体变了(哪怕只是 submit_not_after 换了个时间),返回 409 IDEMPOTENCY_CONFLICT。所以重放要原样重放。
结果有效期
images[].expires_at 是 Unix 秒。实测一张 04:15 出的图,expires_at 是次日 04:15——大约 24 小时。过期后 URL 就取不到图了。
请及时把图转存到你自己的存储里。 这个 URL 是临时的,不要写进数据库当长期地址,也不要直接发给最终用户。
计费
图片按五个桶分别计价,不是按张收固定价格:
| 桶 | 是什么 |
|---|---|
| 文本输入 | 你的 prompt |
| 缓存文本输入 | 命中缓存的那部分文本输入,单价更低 |
| 图片输入 | 参考图 |
| 缓存图片输入 | 命中缓存的那部分图片输入,单价更低 |
| 图片输出 | 生成的图本身。分辨率、质量、比例都会改变它 |
五个桶的单价在 GET /v1/pricing 的 image_models[].tariff 里,免鉴权:
{"id":"gpt-image-2-official","display_name":"GPT Image 2 Official",
"capabilities":["image_generation"],
"tariff":{"version":"openai-gpt-image-2-standard-2026-09-14",
"text_input_micro_usd_per_m":5000000,
"cached_text_input_micro_usd_per_m":1250000,
"image_input_micro_usd_per_m":8000000,
"cached_image_input_micro_usd_per_m":2000000,
"image_output_micro_usd_per_m":30000000}}
单位是微美元 / 百万 token(5000000 = $5 / M),结算时按平台汇率折成人民币微元。人看的版本在 https://api.apipace.com/pricing。
这是对话接口之外唯一会因为缓存而便宜的地方:对话类别名冷热同价,图片不是。
几条规矩:
- 接单时按规格价 + 有界的输入预算算一个预占上限(上例
reserved_micro_cny101,785 = ¥0.101785),最终按实际用量结算(上例sales_micro_cny41,965 = ¥0.041965)。任务接下之后这个上限就不变了,不会透支。 - 只有成功且没超过冻结期限的图才扣款。失败、释放、用量确认不了,都不扣。
- 用量缺失或无法拆分到五个桶时,平台不猜价:任务判失败、释放预占、也不会给你一张「免费的成功图」。
- 迟到的证据不会事后追加扣款。
想核对某一单,拿接单响应里的 request_id 去 GET /v1/requests/{id};账户级汇总在 GET /v1/user/statement 的 models[] 里,图片单独计在 images 计数上。
错误码
图片任务有自己的一套错误码(比对话接口的 14 个更细),信封形状一样,但 message 就是 code 本身,请直接判 code:
| code | HTTP | 什么情况 |
|---|---|---|
INVALID_IMAGE_REQUEST |
400 | 请求体解析不了或字段缺失 |
INVALID_IMAGE_SPEC |
400 | size / resolution / quality 组合非法(常见:4K 配了不支持的比例) |
INVALID_IDEMPOTENCY_KEY |
400 | Idempotency-Key 缺失或与 client_request_id 不一致 |
INVALID_SUBMIT_DEADLINE |
400 | submit_not_after 不在未来 3 分钟以内 |
INVALID_REFERENCE_IMAGE |
400 | 参考图不是 png / jpeg / webp,或超过 1 MiB,或 URL 非法 |
TOO_MANY_REFERENCE_IMAGES |
400 | 超过 4 张 |
MODEL_NOT_ALLOWED |
403 | 这个别名不在 key 的白名单里 |
IMAGE_TASK_NOT_FOUND |
404 | 本 key 下没有这个任务 |
IDEMPOTENCY_CONFLICT |
409 | 同一个编号,请求体变了 |
SUBMISSION_EXPIRED |
410 | 过了 submit_not_after 才到 |
IMAGE_BODY_TOO_LARGE |
413 | 请求体超过 8 MiB |
IMAGE_CAPACITY_BUSY |
429 | 并发接单槽位满了,稍后重试 |
IMAGE_PRICE_UNAVAILABLE / IMAGE_ROUTE_UNAVAILABLE / IMAGE_PRICE_OR_ROUTE_UNAVAILABLE |
503 | 这个规格当前没有可用价目或通道 |
IMAGE_SERVICE_UNAVAILABLE |
500 | 平台内部错误 |
任务失败时 error_code 里还会出现这几个(都已释放预占、不扣钱):IMAGE_NOT_STARTED、IMAGE_PROVIDER_REJECTED、IMAGE_GENERATION_FAILED、IMAGE_RESULT_INVALID、IMAGE_USAGE_UNCONFIRMED;status 为 unknown 时通常伴随 IMAGE_RESULT_UNCONFIRMED 或 IMAGE_STATUS_UNAVAILABLE。
余额不足按通用口径返回 INSUFFICIENT_BALANCE(402),见限制与错误码。
一段能跑的轮询代码
import time, requests
BASE = "https://api.apipace.com"
H = {"x-api-key": KEY, "content-type": "application/json"}
cid = "job-2026-09-16-0001" # 你自己的唯一编号,重试时必须复用
r = requests.post(f"{BASE}/v1/image-tasks",
headers={**H, "Idempotency-Key": cid},
json={
"client_request_id": cid,
"submit_not_after": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(time.time() + 120)),
"model": "gpt-image-2-official",
"prompt": "一只橘猫坐在窗台上晒太阳,水彩风格",
"size": "1:1", "resolution": "1k", "quality": "low", "n": 1,
})
r.raise_for_status()
task = r.json()
while task["status"] not in ("succeeded", "failed"):
time.sleep(task.get("retry_after", 5))
task = requests.get(f"{BASE}/v1/image-tasks/{task['task_id']}", headers=H).json()
if task["status"] == "failed":
raise RuntimeError(task.get("error_code"))
url = task["images"][0]["url"] # 24 小时内有效,立刻转存
print(url, task["billing"]["sales_micro_cny"] / 1e6, "CNY")