Files
sub2api/docs/ASYNC_IMAGE_TASKS.md
T
harukaandClaude Opus 4.8 0eb6e21aaa feat: 异步图片任务结果落对象存储
为异步生图任务增加 S3 兼容对象存储支持,任务结果不再把大图内联存进 Redis:

- 新增可插拔接口 service.ImageStorage(Save -> url),适配别的厂商只需实现它
- S3 实现 S3ImageStorage(AWS S3 / R2 / 阿里云 OSS / MinIO),与备份共用 S3 客户端构造
- 新增 image_storage 配置(config.yaml + IMAGE_STORAGE_* 环境变量),默认关闭
- enabled 同时作为总开关:关闭或未配置对象存储时,异步生图接口返回 404 且不写
  Redis,从根上避免几 MB 的 b64_json 结果撑爆 Redis
- 完成时把图片上传对象存储并把结果改写为短链接(公开直链或 presigned),
  上传失败则任务标记为失败

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SM1tf3CFVRzC7guuhBXvMd
2026-07-15 19:57:37 -07:00

137 lines
5.4 KiB
Markdown

# Asynchronous Image Tasks
Asynchronous image tasks let clients submit long-running OpenAI-compatible image requests without keeping one HTTP connection open. This avoids proxy/CDN response timeouts such as Cloudflare 524 while preserving the existing image routing, billing, moderation, concurrency, and failover behavior.
## Endpoints
The authenticated gateway exposes both `/v1` paths and their existing no-prefix aliases:
```text
POST /v1/images/generations/async
POST /v1/images/edits/async
GET /v1/images/tasks/{task_id}
```
The aliases are `/images/generations/async`, `/images/edits/async`, and `/images/tasks/{task_id}`.
Only OpenAI and Grok groups are supported. Requests use the same JSON or multipart payload as the corresponding synchronous endpoint. Streaming image requests are rejected because a polled task returns one final JSON result.
## Enabling the feature (object storage)
Asynchronous image tasks are **disabled by default** and gated on object storage. When the switch is off — or the S3 credentials are incomplete — the async endpoints return `404` and never create a task or write to Redis. This is deliberate: without offloading, large `b64_json` results (several MB each, e.g. `gpt-image-1`) would accumulate in Redis and exhaust its memory.
Configure an S3-compatible object store (AWS S3, Cloudflare R2, Aliyun OSS, MinIO, …) in `config.yaml` (all keys also accept the `IMAGE_STORAGE_*` environment overrides):
```yaml
image_storage:
enabled: true
endpoint: "https://<account_id>.r2.cloudflarestorage.com" # AWS 官方可留空
region: "auto"
bucket: "my-images"
access_key_id: "..."
secret_access_key: "..."
prefix: "images/"
force_path_style: false # MinIO/path-style buckets set true
public_base_url: "" # set to return public_base_url/key直链; empty → presigned URL
presign_expiry_hours: 24 # presigned link TTL when public_base_url is empty
max_download_bytes: 33554432 # cap when re-hosting an upstream image URL (32MB)
```
When a task completes, each generated image is uploaded to the bucket and the result is rewritten to a compact form: `data[].url` points at the stored object (a permanent `public_base_url/key` link, or a time-limited presigned URL) and `b64_json` is removed. Only this small JSON is stored in Redis. If an upload fails, the task is marked `failed` rather than persisting the raw base64.
To support a different vendor beyond the S3-compatible client, implement the `service.ImageStorage` interface (`Save(ctx, key, contentType, data) (url, error)`) and provide it in place of the S3 implementation.
## Submit a task
```bash
curl -i https://api.example.com/v1/images/generations/async \
-H 'Authorization: Bearer sk-...' \
-H 'Content-Type: application/json' \
-d '{
"model": "gpt-image-1",
"prompt": "A lighthouse during a winter storm",
"size": "1536x1024"
}'
```
The server stores the initial task in Redis and responds with `202 Accepted`:
```json
{
"id": "imgtask_0123456789abcdef",
"task_id": "imgtask_0123456789abcdef",
"object": "image.generation.task",
"status": "processing",
"created_at": 1784092800,
"expires_at": 1784179200,
"poll_url": "/v1/images/tasks/imgtask_0123456789abcdef"
}
```
`Location` contains the polling path and `Retry-After: 3` provides the recommended polling interval.
## Poll a task
Use the same API key that submitted the task:
```bash
curl https://api.example.com/v1/images/tasks/imgtask_0123456789abcdef \
-H 'Authorization: Bearer sk-...'
```
While work is in progress:
```json
{
"id": "imgtask_0123456789abcdef",
"task_id": "imgtask_0123456789abcdef",
"object": "image.generation.task",
"status": "processing",
"created_at": 1784092800,
"expires_at": 1784179200
}
```
On success, `result` mirrors the synchronous image API body, except each image has been offloaded to object storage: `data[].url` points at the stored object and `b64_json` is stripped (so both URL and base64 upstream formats end up as compact stored links):
```json
{
"id": "imgtask_0123456789abcdef",
"task_id": "imgtask_0123456789abcdef",
"object": "image.generation.task",
"status": "completed",
"http_status": 200,
"image_url": "https://...",
"result": {
"created": 1784092923,
"data": [{"url": "https://..."}]
},
"created_at": 1784092800,
"completed_at": 1784092923,
"expires_at": 1784179323
}
```
For URL responses, `image_url` mirrors the first `data[].url` for simple clients. On failure, the task reaches `failed` and exposes the original OpenAI-compatible error object where available:
```json
{
"id": "imgtask_0123456789abcdef",
"task_id": "imgtask_0123456789abcdef",
"object": "image.generation.task",
"status": "failed",
"http_status": 502,
"error": {
"type": "api_error",
"message": "Upstream request failed"
},
"created_at": 1784092800,
"completed_at": 1784092923,
"expires_at": 1784179323
}
```
All submit and poll responses include `Cache-Control: no-store`, preventing a CDN from caching the `processing` state. Tasks and results expire 24 hours after their latest state update. A task executes for at most 30 minutes.
Task ownership is scoped to both user and API key. Unknown task IDs and IDs owned by another key both return `404`, avoiding task-existence disclosure. Polling remains available when the completed generation used the key's remaining balance; normal authentication, disabled-key, user, IP, and group checks still apply.