异步任务式图片生成:创建任务(请求体透传给上游)→ 后台队列生成 → 轮询任务状态 → 拿结果图。
所有写操作立即返回任务 ID(202),不阻塞等待出图。
每个 /api/v1/* 请求都要带请求头 X-API-Key,它的值就是你的上游服务商 key。
Authorization: Bearer <key>)。key 对不对由上游服务商判定(错了任务会 failed,并把上游返回写进 error)。sha256(key) 分组,你只能看到用同一个 key 创建的任务(别人的返回 404)。明文 key 不会出现在任何响应里。401(除非服务端配置了默认回退 key)。| 项 | 值 |
|---|---|
| Base URL | http://localhost:8000 |
| 鉴权头 | X-API-Key: <你的上游key> |
| 时间格式 | ISO 8601 UTC,如 2026-06-30T01:16:08.087Z |
| 错误响应体 | {"error": {"code": 401, "message": "..."}} |
| 码 | 含义 |
|---|---|
| 202 | 任务已创建并入队 |
| 200 | 查询成功 |
| 400 | 请求体为空 / 非法(generations 非 JSON、edits 非 multipart) |
| 401 | 缺少 API key |
| 404 | 任务不存在或不属于该 key |
| 410 | 结果文件已被清理 |
| 413 | 请求体超过 MAX_BODY_BYTES(默认 50MB) |
| 422 | 路径/查询参数校验失败(非法 UUID、index 非整数等) |
| 503 | 数据库不可用(仅 /healthz) |
pending → running → succeeded / failed
| 状态 | 含义 |
|---|---|
| pending | 已入队,等待 worker 认领 |
| running | worker 正在调上游生成(含失败重试期间) |
| succeeded | 生成成功,result 含结果图 |
| failed | 重试耗尽仍失败,error/error_status 写明原因 |
请求体是 JSON,会被原样透传给上游 /images/generations。字段以你的上游服务商文档为准,常见如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| model | string | 模型名,如 gpt-image-2 |
| prompt | string | 提示词(必填) |
| size | string | 如 1024x1024 / 3840x2160 |
| quality | string | 如 high / auto |
| output_format | string | 如 png / jpeg |
| stream | bool | true 时让上游走 SSE 流式(服务端会据此设 Accept) |
| image_urls | array | 图生图用:元素可为 http(s) URL(直传上游)、data:image/...;base64, 或裸 base64;后两者服务端落盘换成公网 URL 再发上游 |
| n / background / … | — | 其余字段一并透传 |
image_urls / reference_images / images:[{"image_url":...}],元素可为 http(s) URL / data-URI / 裸 b64;multipart 上传文件同样支持。服务端会把 b64/上传图落盘到公开的 /inputs,统一改写成上游要的 images:[{"image_url":"完整公网URL"}],并转发到 /images/edits(无图则走 /images/generations)。需把 PUBLIC_INPUT_BASE_URL 配成公网域名,上游才 fetch 得到。可在任务详情「实际上传参数」里看到最终发给上游的内容。curl -X POST http://localhost:8000/api/v1/tasks/generations \
-H "X-API-Key: <你的上游key>" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-image-2",
"prompt": "a red apple on a wooden table, studio light",
"size": "1024x1024",
"quality": "high",
"output_format": "png"
}'
202{
"id": "c69550f6-f94f-4e3d-89dc-7cbf5ca429fb",
"status": "pending",
"endpoint": "generations",
"created_at": "2026-06-30T01:16:08.087Z"
}
?stream=0请求体是 multipart/form-data,整体原样透传给上游 /images/edits(服务端不解析)。参考图用字段名 image[] 上传(可多张)。
| 字段 | 类型 | 说明 |
|---|---|---|
| image[] | file | 参考图,可重复多张 |
| model / prompt / size / quality / output_format | form | 同 generations,按上游约定 |
| ?stream=1 | query | 可选,让上游走 SSE(默认关) |
curl -X POST http://localhost:8000/api/v1/tasks/edits \
-H "X-API-Key: <你的上游key>" \
-F "model=gpt-image-2" \
-F "prompt=给人物加一顶帽子" \
-F "size=1024x1024" \
-F "image[][email protected];type=image/jpeg"
202{ "id": "…", "status": "pending", "endpoint": "edits", "created_at": "…" }
仅返回当前 key 的任务,按创建时间倒序。
| 查询参数 | 默认 | 说明 |
|---|---|---|
| status | — | 过滤:pending/running/succeeded/failed |
| limit | 20 | 1~100 |
| offset | 0 | 分页偏移 |
200{
"items": [ { /* TaskOut,见 §10 */ } ],
"total": 42,
"limit": 20,
"offset": 0
}
返回单个任务(TaskOut)。不属于当前 key 或不存在 → 404。轮询这个端点直到 status 为终态。
200(succeeded 示例){
"id": "c69550f6-f94f-4e3d-89dc-7cbf5ca429fb",
"endpoint": "generations",
"status": "succeeded",
"retry_count": 0,
"request_summary": { "model": "gpt-image-2", "size": "1024x1024", "prompt_chars": 42 },
"result": [ { "file_url": "https://api.你的域名.com/outputs/generated_c69550f6_1.png" } ],
"error": null,
"error_status": null,
"created_at": "2026-06-30T01:16:08.087Z",
"started_at": "2026-06-30T01:16:08.096Z",
"finished_at": "2026-06-30T01:16:08.112Z",
"updated_at": "2026-06-30T01:16:08.112Z"
}
{
"id": "…", "status": "failed", "retry_count": 3,
"result": null,
"error": "upstream 401: invalid api key",
"error_status": 401,
...
}
?download=0校验任务归属后直出图片字节。index 是 result 数组下标(从 0 起)。?download=1 触发浏览器下载(attachment)。文件已被清理 → 410。
结果图的公开静态地址(即 result[].file_url)。文件名含不可猜的 UUID,作为能力令牌;无需鉴权,可直接 <img src> 加载。
无需鉴权。{"status":"ok","db":"ok","queue_depth":0,"workers":3};DB 不通返回 503。
| 字段 | 类型 | 说明 |
|---|---|---|
| id | uuid | 任务 ID |
| endpoint | string | generations / edits |
| status | string | pending / running / succeeded / failed |
| retry_count | int | 已重试次数 |
| request_summary | object|null | 脱敏摘要(model/size/quality/prompt_chars 等) |
| result | array|null | 结果项数组,见下;失败为 null |
| error | string|null | 失败时的上游错误文本(已截断) |
| error_status | int|null | 失败时的上游 HTTP 状态码(网络错误为 null) |
| created_at / started_at / finished_at / updated_at | string|null | ISO8601 时间戳 |
| 字段 | 类型 | 说明 |
|---|---|---|
| file_url | string | 结果图的完整 URL,如 https://api.域名/outputs/generated_<uuid>_1.png(按你访问 API 的域名自动生成) |
| remote_url | string? | 上游若直接回图床 URL 时附带(可选) |
result 不含服务端绝对路径,任务响应里也不会出现 key / owner 等敏感字段。# 1) 创建任务,拿到 id
ID=$(curl -s -X POST http://localhost:8000/api/v1/tasks/generations \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"model":"gpt-image-2","prompt":"a red apple","size":"1024x1024"}' \
| python -c "import sys,json;print(json.load(sys.stdin)['id'])")
# 2) 轮询直到终态
curl -s -H "X-API-Key: $KEY" http://localhost:8000/api/v1/tasks/$ID
import time, requests
BASE, KEY = "http://localhost:8000", "<你的上游key>"
h = {"X-API-Key": KEY}
r = requests.post(f"{BASE}/api/v1/tasks/generations", headers=h,
json={"model": "gpt-image-2", "prompt": "a red apple", "size": "1024x1024"})
tid = r.json()["id"]
while True:
t = requests.get(f"{BASE}/api/v1/tasks/{tid}", headers=h).json()
if t["status"] in ("succeeded", "failed"):
break
time.sleep(2)
if t["status"] == "succeeded":
url = t["result"][0]["file_url"] # 已是完整 URL
open("out.png", "wb").write(requests.get(url).content)
else:
print("failed:", t["error_status"], t["error"])
import time, requests
BASE, KEY = "http://localhost:8000", "<你的上游key>"
h = {"X-API-Key": KEY}
# 参考图用字段名 image[] 上传(可多张);其余字段放 data
files = [("image[]", ("raw.jpg", open("raw.jpg", "rb"), "image/jpeg"))]
data = {"model": "gpt-image-2", "prompt": "给人物加一顶帽子", "size": "1024x1024"}
r = requests.post(f"{BASE}/api/v1/tasks/edits", headers=h, files=files, data=data)
tid = r.json()["id"]
while True:
t = requests.get(f"{BASE}/api/v1/tasks/{tid}", headers=h).json()
if t["status"] in ("succeeded", "failed"):
break
time.sleep(2)
if t["status"] == "succeeded":
url = t["result"][0]["file_url"] # 已是完整 URL
open("edited.png", "wb").write(requests.get(url).content)
else:
print("failed:", t["error_status"], t["error"])