# 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://.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.