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

5.4 KiB

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:

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):

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

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:

{
  "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:

curl https://api.example.com/v1/images/tasks/imgtask_0123456789abcdef \
  -H 'Authorization: Bearer sk-...'

While work is in progress:

{
  "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):

{
  "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:

{
  "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.