-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat(openai-image-gen): add Atlas Cloud as an optional image provider #296
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |
| import random | ||
| import re | ||
| import sys | ||
| import time | ||
| import urllib.error | ||
| import urllib.request | ||
| from collections.abc import Callable | ||
|
|
@@ -206,6 +207,83 @@ def request_images( | |
| raise RuntimeError(f"OpenAI Images API failed ({e.code}): {payload}") from e | ||
|
|
||
|
|
||
| ATLAS_BASE_URL = "https://api.atlascloud.ai/api/v1/model" | ||
| ATLAS_DEFAULT_MODEL = "alibaba/wan-2.7/text-to-image" | ||
| ATLAS_POLL_TIMEOUT_S = 300 | ||
| ATLAS_POLL_INTERVAL_S = 3 | ||
| # Atlas sits behind an edge that rejects the stock urllib User-Agent with a | ||
| # 403 (error code 1010), so send an explicit one on every request. | ||
| ATLAS_USER_AGENT = "openclaw-openai-image-gen/1.0" | ||
|
|
||
|
|
||
| def atlas_size(size: str) -> str: | ||
| """Atlas expects width*height, not the OpenAI width x height form.""" | ||
| return size.replace("x", "*") | ||
|
|
||
|
|
||
| def request_images_atlas(api_key: str, prompt: str, model: str, size: str) -> dict: | ||
| """Generate one image through Atlas Cloud's async API. | ||
|
|
||
| Atlas is not OpenAI Images API compatible: a POST queues a prediction and | ||
| the result is fetched by polling. The return value is shaped like an | ||
| OpenAI Images response so the caller stays provider-agnostic. | ||
| """ | ||
| body = json.dumps( | ||
| {"model": model, "prompt": prompt, "size": atlas_size(size)} | ||
| ).encode("utf-8") | ||
| headers = { | ||
| "Authorization": f"Bearer {api_key}", | ||
| "Content-Type": "application/json", | ||
| "User-Agent": ATLAS_USER_AGENT, | ||
| } | ||
|
Comment on lines
+234
to
+238
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- repository conventions and relevant source ---'
sed -n '1,90p' skills/openai-image-gen/scripts/gen.py
sed -n '210,285p' skills/openai-image-gen/scripts/gen.py
printf '%s\n' '--- local urllib redirect implementation ---'
python3 - <<'PY'
import inspect
import urllib.request
print(inspect.getsource(urllib.request.HTTPRedirectHandler.redirect_request))
print(inspect.getsource(urllib.request.HTTPRedirectHandler.http_error_302))
PYRepository: linuxhsj/openclaw-zero-token Length of output: 9792 🤖 get_repo_knowledge executed:
Length of output: 8889 🌐 Web query:
💡 Result: In CPython's urllib.request module, the HTTPRedirectHandler does not automatically strip the Authorization header when following an HTTP redirect to a different host or origin [1][2]. Consequently, if a request is redirected to a new host, any Authorization header included in the original request is forwarded to that new destination [1][2]. This behavior is a known security concern [3][2]. Because headers added via Request.add_header are explicitly documented to be included in redirected requests, the handler preserves them without checking if the target of the redirect shares the same origin [1][4][5]. This can lead to the unintended disclosure of sensitive credentials (such as Bearer tokens or Basic auth headers) to third-party servers if an initial request is redirected to an untrusted domain [1][2]. To avoid this vulnerability, it is recommended to use an alternative library like requests, which automatically drops Authorization headers during cross-domain redirects [1], or to implement custom logic to manage redirects and headers securely [2]. Note that while specific authentication handlers like HTTPBasicAuthHandler can sometimes manage credential state, the default HTTPRedirectHandler mechanism itself does not provide built-in protection against this credential leakage [2]. Citations:
Sensitive Data Exposure (CWE-522): Insufficiently Protected Credentials Exploitability: Difficult 禁止重定向转发 Atlas API key。
📍 Affects 1 file
🤖 Prompt for AI Agents |
||
| req = urllib.request.Request( | ||
| f"{ATLAS_BASE_URL}/generateImage", method="POST", headers=headers, data=body | ||
| ) | ||
| try: | ||
| with urllib.request.urlopen(req, timeout=60) as resp: | ||
| submitted = json.loads(resp.read().decode("utf-8")) | ||
| except urllib.error.HTTPError as e: | ||
| payload = e.read().decode("utf-8", errors="replace") | ||
| raise RuntimeError(f"Atlas Cloud submit failed ({e.code}): {payload}") from e | ||
|
|
||
| prediction_id = (submitted.get("data") or {}).get("id") | ||
| if not prediction_id: | ||
| raise RuntimeError(f"Unexpected Atlas submit response: {json.dumps(submitted)[:400]}") | ||
|
|
||
| deadline = time.monotonic() + ATLAS_POLL_TIMEOUT_S | ||
| while True: | ||
| time.sleep(ATLAS_POLL_INTERVAL_S) | ||
| poll = urllib.request.Request( | ||
| f"{ATLAS_BASE_URL}/prediction/{prediction_id}", | ||
| method="GET", | ||
| headers={ | ||
| "Authorization": f"Bearer {api_key}", | ||
| "User-Agent": ATLAS_USER_AGENT, | ||
| }, | ||
| ) | ||
| try: | ||
| with urllib.request.urlopen(poll, timeout=60) as resp: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 在截止时间前停止新的轮询请求。 代码先休眠并执行 🤖 Prompt for AI Agents |
||
| result = json.loads(resp.read().decode("utf-8")) | ||
| except urllib.error.HTTPError as e: | ||
| payload = e.read().decode("utf-8", errors="replace") | ||
| raise RuntimeError(f"Atlas Cloud poll failed ({e.code}): {payload}") from e | ||
|
|
||
| data = result.get("data") or {} | ||
| status = data.get("status") | ||
| if status == "completed": | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- target context ---'
sed -n '220,310p' skills/openai-image-gen/scripts/gen.py
printf '%s\n' '--- status references ---'
rg -n -C 3 'completed|succeeded|request_images_atlas|Atlas' skills/openai-image-gen/scripts/gen.pyRepository: linuxhsj/openclaw-zero-token Length of output: 7399 🤖 get_repo_knowledge executed:
Length of output: 7907 🌐 Web query:
💡 Result: Atlas Cloud image generation is an asynchronous, two-step REST API workflow consisting of task submission and status polling [1][2]. Submission To generate an image, submit a POST request to the Citations:
🌐 Web query:
💡 Result: In technical and software contexts, the terms succeeded and completed are often used interchangeably to describe a process, job, or task that has finished its execution [1]. While they are frequently used synonymously, there is a subtle distinction often applied in system design: Completed generally denotes that a process has reached the end of its lifecycle, regardless of the outcome. A task can be considered completed if it has finished running, even if it resulted in an error or failed to achieve its primary objective. Succeeded specifically denotes that a process has finished its execution and achieved the intended goal or result without errors. In many APIs and job scheduling systems, these states are treated as final status indicators [1]. Developers often implement logic that monitors these statuses to determine when to proceed with subsequent actions, such as retrieving output data [1]. Because system implementations vary, it is common to see documentation treat them as equivalent final states, as evidenced by polling mechanisms that accept either status as a signal that the process is no longer active and results are available [1]. Citations: 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
from urllib.request import urlopen
url = "https://www.atlascloud.ai/id/models/alibaba/wan-2.7/text-to-image"
try:
text = urlopen(url, timeout=15).read().decode("utf-8", errors="replace")
except Exception as exc:
print(type(exc).__name__ + ": " + str(exc))
else:
for term in ("succeeded", "completed", "status", "outputs"):
print(f"{term}: {term.lower() in text.lower()}")
lower = text.lower()
for term in ("succeeded", "completed"):
pos = lower.find(term)
if pos >= 0:
print(f"--- {term} context ---")
print(text[max(0, pos-300):pos+500])
PYRepository: linuxhsj/openclaw-zero-token Length of output: 311 🌐 Web query:
💡 Result: The Atlas Cloud API uses an asynchronous flow for generation tasks (such as image, video, or audio generation) [1][2]. When you submit a request, the API returns a prediction ID, which you must then use to poll the status of the task until it reaches a terminal state [1][3][2]. Prediction Status and Model Response: - Submission: You POST a request to an endpoint (e.g., /api/v1/model/generateImage or /api/v1/model/generateVideo), which returns a unique prediction ID [3][2]. - Polling: You perform a GET request to /api/v1/model/prediction/{prediction_id} to check the status of your task [1][3][4]. - Status Values: The status field in the JSON response indicates the task's progress [1][5]. Common values include: - processing: The model is actively generating the content [1][3]. - completed / succeeded: The task finished successfully [1][6][7]. - failed: The task failed; error details are provided in the response [1][3][7]. When the status is completed or succeeded, the API response object contains an outputs field, which is an array of strings representing URLs to the generated content [1][3][4][8]. Example JSON response for a completed task: { "data": { "id": "prediction_id", "status": "completed", "outputs": ["https://storage.atlascloud.ai/outputs/result.png"], "created_at": "2026-09-04T00:00:00Z", "completed_at": "2026-09-04T00:00:10Z" } } Note: The Atlas Cloud AI API is distinct from the MongoDB Atlas Administration API; ensure you are using the correct base URL (https://api.atlascloud.ai/) and authentication method [9]. Citations:
🌐 Web query:
💡 Result: For long-running tasks like machine learning predictions, the standard architectural pattern is the asynchronous request-polling model [1]. This approach separates the submission of a task from the retrieval of its result, preventing timeout issues and allowing for better resource management [1]. Design Pattern: 1. Submission: The client sends a POST request to an endpoint (e.g., /generateImage) to initiate the task [1]. The API returns a unique prediction identifier (prediction_id) and an initial status (e.g., processing) [1]. Clients may also be offered an optional synchronous mode to wait for completion within the initial request [1]. 2. Polling: The client periodically sends a GET request to a dedicated status endpoint (e.g., /prediction/{prediction_id}) using the provided identifier [1]. 3. Status Management: The status endpoint should return the current state of the task, typically using values such as processing, completed, succeeded, or failed [1]. The client continues polling until the task reaches a terminal state [1]. Example Implementation: Step 1: Start the task (POST) curl -X POST "https://api.atlascloud.ai/api/v1/model/generateImage" \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "example-model", "prompt": "..."}' Response: {"code": 200, "data": {"id": "prediction_123", "status": "processing"}} Step 2: Poll for results (GET) curl -X GET "https://api.atlascloud.ai/api/v1/model/prediction/prediction_123" \ -H "Authorization: Bearer $API_KEY" When the status becomes completed or succeeded, the response data will contain the final output or URL [1]. Citations: 将 当前轮询循环只处理 🤖 Prompt for AI Agents |
||
| outputs = data.get("outputs") or [] | ||
| if not outputs: | ||
| raise RuntimeError(f"Atlas Cloud returned no output: {json.dumps(result)[:400]}") | ||
| return {"data": [{"url": outputs[0]}]} | ||
| if status == "failed": | ||
| raise RuntimeError(f"Atlas Cloud generation failed: {data.get('error') or result}") | ||
| if time.monotonic() > deadline: | ||
| raise RuntimeError( | ||
| f"Atlas Cloud prediction {prediction_id} still {status} after " | ||
| f"{ATLAS_POLL_TIMEOUT_S}s" | ||
| ) | ||
|
|
||
|
|
||
| def write_gallery(out_dir: Path, items: list[dict]) -> None: | ||
| thumbs = "\n".join( | ||
| [ | ||
|
|
@@ -241,10 +319,18 @@ def write_gallery(out_dir: Path, items: list[dict]) -> None: | |
|
|
||
|
|
||
| def main() -> int: | ||
| ap = argparse.ArgumentParser(description="Generate images via OpenAI Images API.") | ||
| ap = argparse.ArgumentParser( | ||
| description="Generate images via the OpenAI Images API (or Atlas Cloud with --provider atlas)." | ||
| ) | ||
| ap.add_argument( | ||
| "--provider", | ||
| default="openai", | ||
| choices=["openai", "atlas"], | ||
| help="Image backend. 'atlas' uses Atlas Cloud's async API and ATLASCLOUD_API_KEY.", | ||
| ) | ||
| ap.add_argument("--prompt", help="Single prompt. If omitted, random prompts are generated.") | ||
| ap.add_argument("--count", type=int, default=8, help="How many images to generate.") | ||
| ap.add_argument("--model", default="gpt-image-1", help="Image model id.") | ||
| ap.add_argument("--model", default="", help="Image model id. Defaults per provider.") | ||
| ap.add_argument("--size", default="", help="Image size (e.g. 1024x1024, 1536x1024). Defaults based on model if not specified.") | ||
| ap.add_argument("--quality", default="", help="Image quality (e.g. high, standard). Defaults based on model if not specified.") | ||
| ap.add_argument("--background", default="", help="Background transparency (GPT models only): transparent, opaque, or auto.") | ||
|
|
@@ -253,11 +339,15 @@ def main() -> int: | |
| ap.add_argument("--out-dir", default="", help="Output directory (default: ./tmp/openai-image-gen-<ts>).") | ||
| args = ap.parse_args() | ||
|
|
||
| api_key = (os.environ.get("OPENAI_API_KEY") or "").strip() | ||
| key_env = "ATLASCLOUD_API_KEY" if args.provider == "atlas" else "OPENAI_API_KEY" | ||
| api_key = (os.environ.get(key_env) or "").strip() | ||
| if not api_key: | ||
| print("Missing OPENAI_API_KEY", file=sys.stderr) | ||
| print(f"Missing {key_env}", file=sys.stderr) | ||
| return 2 | ||
|
|
||
| if not args.model: | ||
| args.model = ATLAS_DEFAULT_MODEL if args.provider == "atlas" else "gpt-image-1" | ||
|
|
||
| # Apply model-specific defaults if not specified | ||
| default_size, default_quality = get_model_defaults(args.model) | ||
| size = args.size or default_size | ||
|
|
@@ -273,13 +363,27 @@ def main() -> int: | |
|
|
||
| prompts = [args.prompt] * count if args.prompt else pick_prompts(count) | ||
|
|
||
| try: | ||
| normalized_background = normalize_background(args.model, args.background) | ||
| normalized_style = normalize_style(args.model, args.style) | ||
| normalized_output_format = normalize_output_format(args.model, args.output_format) | ||
| except ValueError as e: | ||
| print(str(e), file=sys.stderr) | ||
| return 2 | ||
| if args.provider == "atlas": | ||
| for flag, value in ( | ||
| ("--quality", args.quality), | ||
| ("--background", args.background), | ||
| ("--output-format", args.output_format), | ||
| ("--style", args.style), | ||
| ): | ||
| if value: | ||
| print( | ||
| f"Warning: {flag} is an OpenAI-only option; ignoring for --provider atlas.", | ||
| file=sys.stderr, | ||
| ) | ||
| normalized_background = normalized_style = normalized_output_format = "" | ||
| else: | ||
| try: | ||
| normalized_background = normalize_background(args.model, args.background) | ||
| normalized_style = normalize_style(args.model, args.style) | ||
| normalized_output_format = normalize_output_format(args.model, args.output_format) | ||
| except ValueError as e: | ||
| print(str(e), file=sys.stderr) | ||
| return 2 | ||
|
|
||
| # Determine file extension based on output format | ||
| if args.model.startswith("gpt-image") and normalized_output_format: | ||
|
|
@@ -290,16 +394,19 @@ def main() -> int: | |
| items: list[dict] = [] | ||
| for idx, prompt in enumerate(prompts, start=1): | ||
| print(f"[{idx}/{len(prompts)}] {prompt}") | ||
| res = request_images( | ||
| api_key, | ||
| prompt, | ||
| args.model, | ||
| size, | ||
| quality, | ||
| normalized_background, | ||
| normalized_output_format, | ||
| normalized_style, | ||
| ) | ||
| if args.provider == "atlas": | ||
| res = request_images_atlas(api_key, prompt, args.model, size) | ||
| else: | ||
| res = request_images( | ||
| api_key, | ||
| prompt, | ||
| args.model, | ||
| size, | ||
| quality, | ||
| normalized_background, | ||
| normalized_output_format, | ||
| normalized_style, | ||
| ) | ||
| data = res.get("data", [{}])[0] | ||
| image_b64 = data.get("b64_json") | ||
| image_url = data.get("url") | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🔎 Supported by static analysis
🏁 Script executed:
Repository: linuxhsj/openclaw-zero-token
Length of output: 13533
🌐 Web query:
Atlas Cloud image generation API alibaba/wan-2.7/text-to-image size width*height google/nano-banana-pro/text-to-image aspect_ratio resolution💡 Result:
Atlas Cloud is an AI API aggregation platform that provides unified access to various AI models [1][2]. Image generation parameters for the requested models are detailed below. Alibaba Wan 2.7 (Text-to-Image) The Alibaba Wan 2.7 model allows for custom image generation via size specifications [3][4]. * Size/Resolution: You can specify resolution using preset values like "1K" or "2K", or by providing explicit pixel dimensions in the format "width*height" [5][3]. * Constraints: Total pixels must be between 768×768 and 2048×2048 [3][4]. Individual dimensions must be within the 512–4096 range [4]. The aspect ratio must be between 1:8 and 8:1 [3][4]. Google Nano Banana Pro (Text-to-Image) The Google Nano Banana Pro model uses distinct parameters for aspect ratio and resolution, rather than a raw "size" field [6][7]. * Aspect Ratio: Specified via the
aspect_ratioparameter (e.g., "1:1", "16:9", "9:16", "21:9", "3:2", "2:3", "3:4", "4:3", "4:5", "5:4") [6][8][9]. * Resolution: Specified via theresolutionparameter (options: "1K", "2K", "4K") [6][8][9]. * Implementation Note: Developers are advised not to use an OpenAI-style "size" field (e.g., "1024x1024") when calling the native Google model interfaces, as it may cause errors; instead, defineaspect_ratioandresolutionseparately within the configuration [7]. Top Results: [1], [3], [4], [6], [7]Citations:
🤖 get_repo_knowledge executed:
get_repo_knowledge linuxhsj/openclaw-zero-token /tmp/coderabbit-repo-knowledge/linuxhsj-openclaw-zero-token-85b0394c/conventionsLength of output: 6632
🌐 Web query:
site:atlascloud.ai/models/google/nano-banana-pro/text-to-image "aspect_ratio" "resolution" "size"💡 Result:
Aspect ratio, resolution, and screen size are distinct technical specifications used to describe digital displays and media: Aspect Ratio Aspect ratio defines the proportional relationship between the width and the height of an image or screen. It is expressed as two numbers separated by a colon, such as 16:9 or 4:3. This ratio describes the shape of the display area regardless of its physical size or total pixel count. Resolution Resolution refers to the total number of distinct pixels that can be displayed on a screen or in an image, typically expressed as horizontal pixels by vertical pixels (e.g., 1920x1080). In some digital contexts, such as content generation APIs, resolution may be represented by categorical labels like 1k, 2k, 4k, or 8k, which denote specific pixel density standards. Screen Size Screen size describes the physical dimensions of a display, usually measured as the length of the diagonal line spanning from one corner of the screen to the opposite corner, typically in inches. Relationship These three concepts are independent but interrelated: 1. A single aspect ratio can support many different resolutions. 2. A specific resolution can be rendered on screens of various physical sizes. 3. The combination of resolution and physical screen size determines the pixel density (often measured in pixels per inch, or PPI), which affects the sharpness of the image.
Citations:
🌐 Web query:
site:atlascloud.ai/docs/en/models/image Atlas generateImage model size aspect_ratio resolution💡 Result:
Atlas Cloud provides a unified API for image generation, accessed via the generateImage endpoint at https://api.atlascloud.ai/api/v1/model/generateImage [1]. While the service supports various models such as Seedream, FLUX, Qwen-Image, Ideogram, HiDream, and Nano Banana, specific configuration parameters like aspect ratio and resolution are not detailed in the available documentation [1]. Users are directed to consult the Model Library and the API Reference on the official Atlas Cloud documentation site for comprehensive specifications and model-specific parameters [1]. Example API usage: requests.post( "https://api.atlascloud.ai/api/v1/model/generateImage", headers={ "Authorization": "Bearer your-api-key", "Content-Type": "application/json" }, json={ "model": "seedream-3.0", "prompt": "A serene Japanese garden with cherry blossoms, watercolor style" }) [1]
Citations:
🏁 Script executed:
Repository: linuxhsj/openclaw-zero-token
Length of output: 32082
按模型映射 Atlas 的尺寸参数。
request_images_atlas()始终发送size。该参数对默认的alibaba/wan-2.7/text-to-image支持width*height,但google/nano-banana-pro/text-to-image使用aspect_ratio和resolution。因此,Nano Banana Pro 示例可能忽略尺寸或因参数无效而失败。请按模型构建请求参数,并同步更新示例、说明和测试。🧰 Tools
🪛 ast-grep (0.45.2)
[info] 230-232: use jsonify instead of json.dumps for JSON output
Context: json.dumps(
{"model": model, "prompt": prompt, "size": atlas_size(size)}
)
Note: [CWE-116] Improper Encoding or Escaping of Output.
(use-jsonify)
📍 Affects 3 files
skills/openai-image-gen/scripts/gen.py#L231-L233(this comment)skills/openai-image-gen/SKILL.md#L64-L64skills/openai-image-gen/SKILL.md#L69-L71skills/openai-image-gen/scripts/test_gen.py#L147-L153🤖 Prompt for AI Agents