
Gpt Image 2
- 16.8k installs
- 4 repo stars
- Updated April 22, 2026
- gargantuax/openskills
GPT Image 2 is a skill providing complete coverage of OpenAI's image generation, editing, and Responses APIs with validation and streaming.
About
Full OpenAI-compatible GPT Image 2 coverage across images/generations, images/edits, and Responses API with image_generation tool. Handles text-to-image generation, mask edits, multi-image batches, streaming outputs, and mixed text+image Responses. Includes strict pre-flight validation of model constraints. Works with OpenAI API or any OpenAI-compatible gateway. Supports both Markdown and SRT output formats for transcript content.
- Three API routes: text-to-image generations, image edits with masks, Responses API with streaming
- Strict pre-flight validation of model size, aspect, and feature constraints
- Multi-image batching, partial image streaming, and mixed text+image Responses flows
Gpt Image 2 by the numbers
- 16,771 all-time installs (skills.sh)
- +33 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #79 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
gpt-image-2 capabilities & compatibility
- Capabilities
- image generation · image editing · streaming · batch processing · api integration
- Works with
- openai
- Use cases
- image generation
What gpt-image-2 says it does
Full OpenAI-compatible GPT Image 2 coverage across images/generations, images/edits, and responses with the image_generation tool.
A single Python entrypoint that covers every GPT Image 2 route, with strict pre-flight validation of the model's size, aspect, and feature constraints.
npx skills add https://github.com/gargantuax/openskills --skill gpt-image-2Add your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 16.8k |
|---|---|
| repo stars | ★ 4 |
| Security audit | 3 / 3 scanners passed |
| Last updated | April 22, 2026 |
| Repository | gargantuax/openskills ↗ |
How do you call GPT Image 2 from an agent?
Generate, edit, and stream images using OpenAI's GPT Image 2 API with advanced features like masking, batching, and streaming.
Who is it for?
Developers building image generation features; teams integrating OpenAI image APIs
Skip if: Projects not using OpenAI or OpenAI-compatible APIs
When should I use this skill?
The user asks to generate, edit, or stream GPT Image 2 images via OpenAI-compatible API inside an agent session.
What you get
Generated images, edited image files, streamed partial previews, and responses API image_generation tool outputs.
- Generated image files
- Edited image outputs
- Streaming image streams
By the numbers
- Covers 3 API routes: generations, edits, responses
- Supports up to 3 partial images in streaming
Files
GPT Image 2
A single Python entrypoint that covers every GPT Image 2 route, with strict pre-flight validation of the model's size, aspect, and feature constraints.
Workflow
1. Open references/config.md to pick environment variables and defaults. 2. Open references/api-surface.md to choose between generations, edits, and responses. 3. Prefer OPENAI_BASE_URL=https://api.openai.com/v1 unless the user asks for a different OpenAI-compatible endpoint. 4. Use gpt-image-2 for generations and edits; use a text-capable Responses model such as gpt-5.4 for responses. 5. Run scripts/gpt_image.py with one of the three subcommands. 6. Add --dry-run first when the payload shape is the main risk. 7. Add --save-response <path> when the raw JSON body or SSE event stream needs to be kept for debugging.
Commands
Text-to-image through the public Images API:
python .\skills\gpt-image-2\scripts\gpt_image.py generations `
--prompt "A bold product hero image for a developer tool homepage" `
--output .\out\hero.png `
--size 1536x1024 `
--quality high `
--format pngMulti-image batch with a filename pattern:
python .\skills\gpt-image-2\scripts\gpt_image.py generations `
--prompt "A cinematic city skyline at night" `
--output .\out\skyline-{index}.webp `
--n 3 `
--format webp `
--compression 90Image edits with two inputs plus a mask:
python .\skills\gpt-image-2\scripts\gpt_image.py edits `
--prompt "Blend the two references into one clean marketing illustration" `
--image .\refs\subject.png `
--image .\refs\background.png `
--mask .\refs\mask.png `
--output .\out\edit-{index}.png `
--image-field-style brackets `
--n 2Responses API with streaming and partial previews:
python .\skills\gpt-image-2\scripts\gpt_image.py responses `
--input-text "Generate a poster for an AI developer summit" `
--model gpt-5.4 `
--output .\out\poster-{index}.png `
--stream `
--partial-images 2 `
--save-response .\out\poster-events.jsonResponses API edit with a local image plus a mask:
python .\skills\gpt-image-2\scripts\gpt_image.py responses `
--input-text "Turn this product shot into a clean studio ad" `
--model gpt-5.4 `
--input-image .\refs\product.png `
--mask .\refs\mask.png `
--output .\out\studio.png `
--action editInspect the built request without sending it:
python .\skills\gpt-image-2\scripts\gpt_image.py generations `
--prompt "A minimal cover image" `
--output .\out\cover.png `
--dry-runRules
- Use
generationsfor public text-to-image calls. - Use
editsfor multipart image edits and mask uploads. - Use
responsesfor advanced flows: streaming, mixed text + image input,previous_response_id,tool_choice,action, and optionaltool_model. - Process environment variables override
.env; CLI flags override both. - Never print secrets.
--outputtakes either a single path or a pattern such asimage-{index}.pngfor multi-image or streaming flows.responsesuses a top-level Responses model separate from the image model; default it togpt-5.4unless you need another text-capable model.qualityon Responses tool flows is passed through, but final behavior still depends on the hosted image tool.- On OpenAI GPT image models, omit
response_format; image data already comes back as base64. - Fail fast on unsupported
gpt-image-2combinations: transparent background, invalid size,partial_imagesoutside0..3, orstream=truewithn>1on public Images routes.
Resources
- Script: scripts/gpt_image.py
- Config reference: references/config.md
- API surface reference: references/api-surface.md
interface:
display_name: "GPT Image 2"
short_description: "Complete GPT Image 2 skill for generations, edits, and responses."
default_prompt: "Use $gpt-image-2 to generate, edit, or stream GPT Image 2 outputs through an OpenAI-compatible API."
gpt-image-2
An agent skill for the full GPT Image 2 surface on any OpenAI-compatible gateway. One Python entrypoint covers images/generations, images/edits, and responses (with the image_generation tool), including streaming and partial-image previews.
Built for Codex, Claude, and other skill-aware agents, but the script also runs fine by hand.
Features
- Three subcommands in one CLI:
generations,edits,responses. - Text-to-image, multi-image batches, mask edits, multi-reference edits, mixed text + image input.
- Streaming (SSE) with optional
partial_imagesprogressive previews. - Strict pre-flight validation of GPT Image 2 constraints (size, aspect, pixel count, transparent background,
output_compression,stream+n, and unsupportedresponse_formatusage on OpenAI GPT image models). - Automatic base64 de-duplication and multi-image filename templating (
out-{index}.png). - Config via CLI flags, process environment, or
.env— with a predictable override order. - Zero third-party dependencies (standard library only).
Requirements
- Python 3.10+
- An OpenAI-compatible endpoint that serves
gpt-image-2(default:https://api.openai.com/v1). - For
responses, a text-capable Responses model such asgpt-5.4when using the hostedimage_generationtool. OPENAI_API_KEY.
Install
After publishing the repository, the recommended install path is through Skills:
pnpm dlx skills add https://github.com/GargantuaX/openskills --skill gpt-image-2Equivalent shorthand:
pnpm dlx skills add GargantuaX/openskills@gpt-image-2If you want the whole collection instead, install:
pnpm dlx skills add GargantuaX/openskillsYou can also drop the folder into your agent's skill directory, or clone/copy it anywhere and invoke the script directly.
Register with Codex by pointing at agents/openai.yaml. Claude-style agents can consume SKILL.md directly.
Setup After skills.sh Install
1. Set credentials with environment variables, or create a .env in the working directory where you will run the script. 2. Start from the repository example `.env.example`, then adjust values for your endpoint and model defaults. 3. Run a --dry-run command first to confirm the final request shape before making live requests.
Minimal .env:
OPENAI_API_KEY=your-openai-api-key
OPENAI_BASE_URL=https://api.openai.com/v1
OPENAI_IMAGE_MODEL=gpt-image-2
OPENAI_RESPONSES_MODEL=gpt-5.4
OPENAI_IMAGE_SIZE=auto
OPENAI_IMAGE_QUALITY=high
OPENAI_IMAGE_FORMAT=webp
OPENAI_IMAGE_TIMEOUT=300Dry-run check:
python .\scripts\gpt_image.py responses `
--input-text "Generate a poster for an AI tool launch" `
--output .\out\poster.webp `
--dry-runQuick start
# 1. Configure credentials (either export or put in .env next to the script)
$env:OPENAI_API_KEY = "sk-..."
$env:OPENAI_BASE_URL = "https://api.openai.com/v1"
# 2. Generate
python .\scripts\gpt_image.py generations `
--prompt "A bold product hero image" `
--output .\out\hero.webpBuilt-in defaults now match the example .env: size=auto, quality=high, format=webp, timeout=300. For responses, the default top-level model is gpt-5.4; OPENAI_IMAGE_MODEL only applies to generations and edits.
See SKILL.md for the full command catalog, including multi-image batches, masked edits, and streaming Responses.
Project layout
gpt-image-2/
├─ SKILL.md # Agent-facing entry point (workflow + examples + rules)
├─ README.md # You are here
├─ agents/
│ └─ openai.yaml # Codex registration metadata
├─ references/
│ ├─ api-surface.md # When to use generations vs edits vs responses
│ └─ config.md # Environment variables and resolution order
└─ scripts/
└─ gpt_image.py # Single entrypoint, stdlib onlyConfiguration
All options can be set via CLI flags, process environment variables, or a .env file. Full table and resolution rules in references/config.md.
Runtime Notes
OPENAI_IMAGE_MODELcontrolsgenerationsandedits.OPENAI_RESPONSES_MODELcontrols the top-level model forresponses.OPENAI_IMAGE_TOOL_MODELoptionally overrides the hosted image model inside theimage_generationtool.- CLI flags override environment variables, and environment variables override
.env. .envis resolved from the current working directory unless you pass--env-file.- For OpenAI-hosted GPT image requests, omit
response_format.
Dry run
Add --dry-run to any command to print the exact request that would be sent (URL, headers, JSON payload, or multipart preview) without calling the API. Handy for agents that want to validate a plan before spending tokens or credits.
Tests
Run the local regression tests with:
python -m unittest discover -s .\skills\gpt-image-2\tests -p "test_*.py"These tests stay offline and focus on argument validation plus --dry-run request shapes.
Troubleshooting
- `Missing OPENAI_API_KEY` — set it in the environment or in
.env. - `gpt-image-2 requires width and height to be multiples of 16` — adjust
--size. - `Public Images routes do not support stream=true with n>1` — use
responsesinstead, or drop--n. - `OpenAI GPT image models return base64 image data by default. Omit response_format.` — remove
--response-formatfor OpenAI-hosted GPT image requests. - `For responses with the image_generation tool, use a text-capable Responses model such as gpt-5.4` — do not reuse
gpt-image-2as the top-levelresponsesmodel. - `output_compression is only valid with output_format jpeg or webp` — set
--format jpegor--format webp. - `partial_images requires stream=true` — add
--stream.
License
MIT. See the repository root LICENSE.
API Surface
Run scripts/gpt_image.py with one of these subcommands. Each one targets a different OpenAI-compatible route and has its own constraint set.
generations
Target route: POST /v1/images/generations
Use for:
- Text-to-image
- Public Images API compatibility checks
- Multi-image batches via
--n - Public-route streaming and
--partial-images
Constraints:
--promptis required.--nmust be in1..10.- On OpenAI GPT image models, omit
--response-formatentirely. background=transparentis rejected.--streamtogether with--n > 1is rejected.
edits
Target route: POST /v1/images/edits
Use for:
- Multipart image edits
- Multiple uploaded source images
- Mask uploads
- Public Images API edit compatibility checks
Constraints:
--promptis required.- At least one
--imagemust be provided. - Repeated image field style can be
simple,brackets, orindexed;bracketsmatches OpenAI's documented multipart shape. background=transparentis rejected.- On OpenAI GPT image models, omit
--response-formatentirely. --streamtogether with--n > 1is rejected.
responses
Target route: POST /v1/responses
Use for:
- Advanced image workflows
- Streaming-first flows
- Mixed text + image input
previous_response_idtool_choiceaction- Image inputs from local files, URLs, data URLs, or file references
Constraints:
- Uses a text-capable Responses model at the top level (default:
gpt-5.4). - Sends an
image_generationtool by default. - Supports
--mask,--mask-url, and--mask-file-id. --tool-modeloptionally overrides the hosted image model for the tool.--tool-choice image_generationis normalized to{"type":"image_generation"}.--input-fidelityis only valid for supported non-gpt-image-2tool models.qualityis passed through to the hosted image tool.- Local image files are converted to data URLs before being sent.
Shared GPT Image 2 validation
The script runs these checks before every request:
- Size must be
autoorWIDTHxHEIGHT. - Width and height must be multiples of
16. - Longest edge must be
<= 3840. - Aspect ratio must be
<= 3:1. - Total pixels must be in
655360..8294400. - Transparent background is not supported.
output_compressionmust stay in0..100and only applies tojpegorwebp.partial_imagesmust be in0..3and requiresstream=true.
Config
Environment variables consumed by scripts/gpt_image.py:
| Variable | Required | Default | Purpose |
|---|---|---|---|
OPENAI_API_KEY | yes | none | API key for the OpenAI-compatible endpoint |
OPENAI_BASE_URL | no | https://api.openai.com/v1 | Base URL, must include /v1 |
OPENAI_IMAGE_MODEL | no | gpt-image-2 | Default model for generations and edits |
OPENAI_RESPONSES_MODEL | no | gpt-5.4 | Default top-level model for responses |
OPENAI_IMAGE_TOOL_MODEL | no | unset | Optional hosted image model override for the responses image tool |
OPENAI_IMAGE_TIMEOUT | no | 300 | HTTP timeout in seconds |
OPENAI_IMAGE_SIZE | no | auto | e.g. 1024x1024, 1536x1024, auto |
OPENAI_IMAGE_QUALITY | no | high | auto, low, medium, high |
OPENAI_IMAGE_BACKGROUND | no | unset | auto or opaque |
OPENAI_IMAGE_FORMAT | no | webp | png, jpeg, webp |
OPENAI_IMAGE_COMPRESSION | no | unset | 0..100, only for jpeg or webp |
OPENAI_IMAGE_MODERATION | no | unset | auto or low |
OPENAI_IMAGE_USER | no | unset | Optional OpenAI-compatible user field |
OPENAI_IMAGE_N | no | 1 | Default image count for generations and edits |
OPENAI_IMAGE_RESPONSE_FORMAT | no | unset | Legacy compatibility field for non-GPT image models; omit it for OpenAI GPT image models |
OPENAI_IMAGE_STREAM | no | false | Default streaming flag |
OPENAI_IMAGE_PARTIAL_IMAGES | no | unset | 0..3, used together with streaming |
OPENAI_IMAGE_INPUT_FIDELITY | no | unset | Optional input_fidelity for supported tool models in responses |
OPENAI_IMAGE_TOOL_CHOICE | no | image_generation | Default tool_choice for responses; the script maps this to {\"type\":\"image_generation\"} |
Resolution order:
1. CLI flags 2. Process environment variables 3. .env file 4. Built-in defaults
.env lookup:
1. --env-file <path> when provided 2. Search from <cwd>/.env upward through parent directories until the first .env is found
Minimal .env example:
OPENAI_API_KEY=sk-example
OPENAI_BASE_URL=https://api.openai.com/v1
OPENAI_IMAGE_MODEL=gpt-image-2
OPENAI_RESPONSES_MODEL=gpt-5.4
OPENAI_IMAGE_SIZE=auto
OPENAI_IMAGE_QUALITY=high
OPENAI_IMAGE_FORMAT=webp
OPENAI_IMAGE_TIMEOUT=300Notes:
OPENAI_BASE_URLmust already include/v1.OPENAI_IMAGE_MODELdoes not control the top-level model forresponses; useOPENAI_RESPONSES_MODELfor that.OPENAI_IMAGE_STREAM=trueis valid for all three subcommands, but public Images routes still rejectstream=truetogether withn>1.- On OpenAI GPT image models, omit
OPENAI_IMAGE_RESPONSE_FORMATentirely. OPENAI_IMAGE_INPUT_FIDELITYis not configurable forgpt-image-2.- Process environment variables override
.env, so local shell overrides stay predictable.
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import base64
import hashlib
import json
import mimetypes
import os
import re
import sys
import uuid
from pathlib import Path
from typing import Any, Dict, Iterable, List, Sequence
from urllib import error, request
DEFAULT_BASE_URL = "https://api.openai.com/v1"
DEFAULT_IMAGES_MODEL = "gpt-image-2"
DEFAULT_RESPONSES_MODEL = "gpt-5.4"
DEFAULT_TIMEOUT = 300
DEFAULT_SIZE = "auto"
DEFAULT_QUALITY = "high"
DEFAULT_OUTPUT_FORMAT = "webp"
SIZE_PATTERN = re.compile(r"^(\d+)x(\d+)$")
def is_gpt_image_model(model: str) -> bool:
return model.startswith("gpt-image-")
def is_gpt_image_2_model(model: str) -> bool:
return model.startswith("gpt-image-2")
def load_dotenv(path: Path) -> Dict[str, str]:
values: Dict[str, str] = {}
if not path.exists():
return values
for line in path.read_text(encoding="utf-8").splitlines():
stripped = line.strip()
if not stripped or stripped.startswith("#") or "=" not in stripped:
continue
key, value = stripped.split("=", 1)
key = key.strip()
value = value.strip()
if not key:
continue
if value and value[0] == value[-1] and value[0] in {"'", '"'}:
value = value[1:-1]
values[key] = value
return values
def resolve_dotenv_path(raw_path: str | None) -> Path | None:
if raw_path:
return Path(raw_path).expanduser().resolve()
current = Path.cwd().resolve()
for directory in (current, *current.parents):
candidate = directory / ".env"
if candidate.exists():
return candidate
return None
def resolve_value(
cli_value: Any,
env_name: str,
dotenv_values: Dict[str, str],
default: Any = None,
) -> Any:
if cli_value is not None:
return cli_value
env_value = os.environ.get(env_name)
if env_value not in (None, ""):
return env_value
dotenv_value = dotenv_values.get(env_name)
if dotenv_value not in (None, ""):
return dotenv_value
return default
def parse_bool(value: Any) -> bool:
if isinstance(value, bool):
return value
text = str(value).strip().lower()
if text in {"1", "true", "yes", "on"}:
return True
if text in {"0", "false", "no", "off"}:
return False
raise SystemExit(f"Invalid boolean value: {value}")
def parse_int(value: Any, field_name: str) -> int:
try:
return int(value)
except (TypeError, ValueError) as exc:
raise SystemExit(f"{field_name} must be an integer.") from exc
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Work with OpenAI-compatible GPT Image 2 APIs across images/generations, images/edits, and responses."
)
subparsers = parser.add_subparsers(dest="command", required=True)
def add_runtime_args(subparser: argparse.ArgumentParser) -> None:
subparser.add_argument("--env-file", help="Optional .env file path. Defaults to ./.env when present.")
subparser.add_argument("--base-url", help="OpenAI-compatible base URL including /v1.")
subparser.add_argument("--api-key", help="API key. Prefer env var or .env instead of CLI.")
subparser.add_argument("--model", help="Request model name. Defaults depend on the subcommand.")
subparser.add_argument("--timeout", type=int, help="HTTP timeout in seconds.")
subparser.add_argument("--output", required=True, help="Output file path or pattern such as out-{index}.png.")
subparser.add_argument("--save-response", help="Write the raw JSON response or streamed events to this path.")
subparser.add_argument("--json", action="store_true", default=None, help="Print the final summary as JSON.")
subparser.add_argument("--dry-run", action="store_true", default=None, help="Print the built request and exit.")
def add_common_image_args(subparser: argparse.ArgumentParser) -> None:
subparser.add_argument("--size", help="Image size, for example 1024x1024, 1536x1024, or auto.")
subparser.add_argument("--quality", help="Image quality: auto, low, medium, or high.")
subparser.add_argument("--background", help="Image background: auto or opaque.")
subparser.add_argument("--format", dest="output_format", help="Output format: png, jpeg, or webp.")
subparser.add_argument("--compression", type=int, help="Output compression 0..100 for jpeg/webp.")
subparser.add_argument("--moderation", help="Moderation mode: auto or low.")
subparser.add_argument("--user", help="Optional OpenAI-compatible user field.")
subparser.add_argument("--stream", action="store_true", default=None, help="Enable streaming.")
subparser.add_argument("--partial-images", type=int, help="Partial image count for streaming image flows.")
generations = subparsers.add_parser("generations", help="Call POST /v1/images/generations.")
add_runtime_args(generations)
generations.add_argument("--prompt", required=True, help="Generation prompt.")
generations.add_argument("--n", type=int, help="Image count 1..10.")
generations.add_argument("--response-format", help="Legacy compatibility field. Omit it for OpenAI GPT image models.")
add_common_image_args(generations)
edits = subparsers.add_parser("edits", help="Call POST /v1/images/edits.")
add_runtime_args(edits)
edits.add_argument("--prompt", required=True, help="Edit prompt.")
edits.add_argument("--image", action="append", default=[], help="Input image path. Repeat to send multiple images.")
edits.add_argument("--mask", help="Optional mask image path.")
edits.add_argument("--image-field-style", choices=["simple", "brackets", "indexed"], default="brackets")
edits.add_argument("--n", type=int, help="Image count 1..10.")
edits.add_argument("--response-format", help="Legacy compatibility field. Omit it for OpenAI GPT image models.")
add_common_image_args(edits)
responses = subparsers.add_parser("responses", help="Call POST /v1/responses with the image_generation tool.")
add_runtime_args(responses)
responses.add_argument("--input-text", action="append", default=[], help="Input text item. Repeat to add multiple items.")
responses.add_argument("--input-image", action="append", default=[], help="Local image path. Converted to a data URL.")
responses.add_argument("--input-image-url", action="append", default=[], help="Remote image URL.")
responses.add_argument("--input-image-data-url", action="append", default=[], help="Prebuilt image data URL.")
responses.add_argument("--input-image-file-id", action="append", default=[], help="Image file reference.")
responses.add_argument("--mask", help="Optional local mask image path. Sent as a tool object.")
responses.add_argument("--mask-url", help="Optional mask image URL.")
responses.add_argument("--mask-file-id", help="Optional mask file reference.")
responses.add_argument("--tool-model", help="Optional image model for the image_generation tool.")
responses.add_argument("--action", choices=["auto", "generate", "edit"], help="Optional image_generation tool action.")
responses.add_argument("--input-fidelity", choices=["high", "low"], help="Optional input_fidelity for supported tool models.")
responses.add_argument(
"--tool-choice",
help="Optional tool_choice. Accepts auto|required|none|image_generation or a raw JSON object.",
)
responses.add_argument("--previous-response-id", help="Optional previous_response_id value.")
responses.add_argument("--metadata-json", help="Optional metadata JSON string.")
add_common_image_args(responses)
return parser.parse_args()
def validate_size(size: str | None, model: str) -> None:
if not size or size == "auto" or not is_gpt_image_2_model(model):
return
match = SIZE_PATTERN.match(size)
if not match:
raise SystemExit("size must be auto or WIDTHxHEIGHT, for example 1024x1024.")
width = int(match.group(1))
height = int(match.group(2))
if width % 16 != 0 or height % 16 != 0:
raise SystemExit("gpt-image-2 requires width and height to be multiples of 16.")
if max(width, height) > 3840:
raise SystemExit("gpt-image-2 longest edge must be <= 3840.")
ratio = max(width / height, height / width)
if ratio > 3:
raise SystemExit("gpt-image-2 aspect ratio must be <= 3:1.")
pixels = width * height
if pixels < 655360 or pixels > 8294400:
raise SystemExit("gpt-image-2 total pixels must be between 655360 and 8294400.")
def validate_common_options(
model: str,
background: str | None,
output_format: str | None,
output_compression: int | None,
partial_images: int | None,
stream: bool,
) -> None:
if background == "transparent" and is_gpt_image_2_model(model):
raise SystemExit("gpt-image-2 does not support transparent background.")
if output_compression is not None and output_format not in {"jpeg", "webp"}:
raise SystemExit("output_compression is only valid with output_format jpeg or webp.")
if output_compression is not None and not 0 <= output_compression <= 100:
raise SystemExit("output_compression must be between 0 and 100.")
if partial_images is not None and not stream:
raise SystemExit("partial_images requires stream=true.")
if partial_images is not None and not 0 <= partial_images <= 3:
raise SystemExit("partial_images must be between 0 and 3.")
def validate_public_images_route(
model: str,
size: str | None,
stream: bool,
n: int,
response_format: str | None,
) -> None:
validate_size(size, model)
if n < 1 or n > 10:
raise SystemExit("n must be between 1 and 10.")
if is_gpt_image_model(model) and response_format not in (None, ""):
raise SystemExit("OpenAI GPT image models return base64 image data by default. Omit response_format.")
if stream and n > 1:
raise SystemExit("Public Images routes do not support stream=true with n>1.")
def guess_mime_type(path: Path) -> str:
mime_type, _ = mimetypes.guess_type(path.name)
return mime_type or "application/octet-stream"
def file_to_data_url(path: Path) -> str:
mime_type = guess_mime_type(path)
encoded = base64.b64encode(path.read_bytes()).decode("ascii")
return f"data:{mime_type};base64,{encoded}"
def make_output_path(template: str, index: int, total: int, variant: str) -> Path:
path = Path(template)
rendered = str(path)
if "{index}" in rendered or "{variant}" in rendered:
rendered = rendered.replace("{index}", str(index))
rendered = rendered.replace("{variant}", variant)
return Path(rendered).expanduser().resolve()
if total == 1 and variant == "final":
return path.expanduser().resolve()
suffix = path.suffix
stem = str(path.with_suffix(""))
marker = str(index) if variant == "final" else f"{index}-{variant}"
return Path(f"{stem}-{marker}{suffix}").expanduser().resolve()
def write_json_file(path: str, data: Any) -> None:
output_path = Path(path).expanduser().resolve()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
def encode_multipart(fields: Sequence[tuple[str, str]], files: Sequence[tuple[str, Path]]) -> tuple[str, bytes]:
boundary = f"----codex-{uuid.uuid4().hex}"
body = bytearray()
for name, value in fields:
body.extend(f"--{boundary}\r\n".encode("utf-8"))
body.extend(f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode("utf-8"))
body.extend(str(value).encode("utf-8"))
body.extend(b"\r\n")
for field_name, file_path in files:
mime_type = guess_mime_type(file_path)
body.extend(f"--{boundary}\r\n".encode("utf-8"))
body.extend(
f'Content-Disposition: form-data; name="{field_name}"; filename="{file_path.name}"\r\n'.encode("utf-8")
)
body.extend(f"Content-Type: {mime_type}\r\n\r\n".encode("utf-8"))
body.extend(file_path.read_bytes())
body.extend(b"\r\n")
body.extend(f"--{boundary}--\r\n".encode("utf-8"))
return f"multipart/form-data; boundary={boundary}", bytes(body)
def send_request(
*,
url: str,
api_key: str,
body: bytes,
content_type: str,
timeout: int,
stream: bool,
) -> Any:
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": content_type,
}
if stream:
headers["Accept"] = "text/event-stream"
req = request.Request(url, data=body, method="POST", headers=headers)
try:
with request.urlopen(req, timeout=timeout) as response:
if stream:
return read_sse_events(response)
return json.loads(response.read().decode("utf-8"))
except error.HTTPError as exc:
error_body = exc.read().decode("utf-8", errors="replace")
raise SystemExit(f"Request failed: HTTP {exc.code}\n{error_body}") from exc
except error.URLError as exc:
raise SystemExit(f"Request failed: {exc.reason}") from exc
def read_sse_events(response: Any) -> List[Dict[str, Any]]:
events: List[Dict[str, Any]] = []
event_name: str | None = None
data_lines: List[str] = []
def flush() -> None:
nonlocal event_name, data_lines
if not event_name and not data_lines:
return
raw_data = "\n".join(data_lines)
parsed: Any = raw_data
if raw_data and raw_data != "[DONE]":
try:
parsed = json.loads(raw_data)
except json.JSONDecodeError:
parsed = raw_data
events.append(
{
"event": event_name or "message",
"data": parsed,
"raw": raw_data,
}
)
event_name = None
data_lines = []
for raw_line in response:
line = raw_line.decode("utf-8", errors="replace").rstrip("\r\n")
if not line:
flush()
continue
if line.startswith(":"):
continue
field, _, value = line.partition(":")
if value.startswith(" "):
value = value[1:]
if field == "event":
event_name = value
elif field == "data":
data_lines.append(value)
flush()
return events
def extract_image_records(node: Any, *, event_name: str | None = None) -> List[Dict[str, Any]]:
records: List[Dict[str, Any]] = []
def walk(value: Any, path: str) -> None:
if isinstance(value, dict):
if isinstance(value.get("b64_json"), str):
records.append(
{
"kind": "partial" if event_name and "partial" in event_name.lower() else "final",
"base64": value["b64_json"],
"revised_prompt": value.get("revised_prompt"),
"source": path or "root",
}
)
if value.get("type") == "image_generation_call" and isinstance(value.get("result"), str):
records.append(
{
"kind": "final",
"base64": value["result"],
"revised_prompt": value.get("revised_prompt"),
"source": path or "root",
}
)
if isinstance(value.get("image_base64"), str):
records.append(
{
"kind": "partial" if event_name and "partial" in event_name.lower() else "final",
"base64": value["image_base64"],
"revised_prompt": value.get("revised_prompt"),
"source": path or "root",
}
)
for partial_key in ("partial_image_b64", "partial_b64_json", "partial_image_base64"):
if isinstance(value.get(partial_key), str):
records.append(
{
"kind": "partial",
"base64": value[partial_key],
"revised_prompt": value.get("revised_prompt"),
"source": path or "root",
}
)
for key, child in value.items():
walk(child, f"{path}.{key}" if path else key)
elif isinstance(value, list):
for index, child in enumerate(value):
walk(child, f"{path}[{index}]")
walk(node, "")
return records
def decode_image_record(record: Dict[str, Any]) -> Dict[str, Any]:
try:
image_bytes = base64.b64decode(record["base64"])
except Exception as exc: # noqa: BLE001
raise SystemExit(f"Failed to decode base64 image from {record['source']}.") from exc
digest = hashlib.sha256(image_bytes).hexdigest()
return {
**record,
"bytes": image_bytes,
"digest": digest,
}
def save_images(records: List[Dict[str, Any]], output_template: str) -> List[Dict[str, Any]]:
saved: List[Dict[str, Any]] = []
seen: set[str] = set()
filtered = [decode_image_record(record) for record in records]
for record in filtered:
if record["digest"] in seen:
continue
seen.add(record["digest"])
index = len(saved) + 1
output_path = make_output_path(output_template, index=index, total=len(filtered), variant=record["kind"])
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_bytes(record["bytes"])
saved.append(
{
"path": str(output_path),
"kind": record["kind"],
"revised_prompt": record.get("revised_prompt"),
"source": record.get("source"),
"sha256": record["digest"],
}
)
if not saved:
raise SystemExit("No image payloads were found in the response.")
return saved
def load_file_paths(paths: Sequence[str]) -> List[Path]:
resolved: List[Path] = []
for raw in paths:
path = Path(raw).expanduser().resolve()
if not path.exists():
raise SystemExit(f"File not found: {path}")
resolved.append(path)
return resolved
def common_settings(args: argparse.Namespace, dotenv_values: Dict[str, str]) -> Dict[str, Any]:
api_key = resolve_value(args.api_key, "OPENAI_API_KEY", dotenv_values)
if not api_key and not args.dry_run:
raise SystemExit("Missing OPENAI_API_KEY. Set it in the environment or a .env file.")
timeout = parse_int(resolve_value(args.timeout, "OPENAI_IMAGE_TIMEOUT", dotenv_values, DEFAULT_TIMEOUT), "timeout")
settings = {
"api_key": str(api_key) if api_key else "",
"base_url": str(resolve_value(args.base_url, "OPENAI_BASE_URL", dotenv_values, DEFAULT_BASE_URL)).rstrip("/"),
"timeout": timeout,
"json_output": bool(args.json),
"dry_run": bool(args.dry_run),
}
return settings
def resolve_images_model(args: argparse.Namespace, dotenv_values: Dict[str, str]) -> str:
return str(resolve_value(args.model, "OPENAI_IMAGE_MODEL", dotenv_values, DEFAULT_IMAGES_MODEL))
def resolve_responses_model(args: argparse.Namespace, dotenv_values: Dict[str, str]) -> str:
return str(resolve_value(args.model, "OPENAI_RESPONSES_MODEL", dotenv_values, DEFAULT_RESPONSES_MODEL))
def resolve_tool_model(args: argparse.Namespace, dotenv_values: Dict[str, str]) -> str | None:
value = resolve_value(getattr(args, "tool_model", None), "OPENAI_IMAGE_TOOL_MODEL", dotenv_values)
if value in (None, ""):
return None
return str(value)
def normalize_tool_choice(raw_value: Any) -> str | Dict[str, Any]:
if raw_value in (None, ""):
return {"type": "image_generation"}
if isinstance(raw_value, dict):
return raw_value
text = str(raw_value).strip()
if text in {"none", "auto", "required"}:
return text
if text == "image_generation":
return {"type": "image_generation"}
try:
parsed = json.loads(text)
except json.JSONDecodeError as exc:
raise SystemExit("tool_choice must be auto, required, none, image_generation, or a JSON object.") from exc
if not isinstance(parsed, dict):
raise SystemExit("tool_choice JSON must decode to an object.")
return parsed
def validate_responses_model(model: str) -> None:
if is_gpt_image_model(model):
raise SystemExit(
"For responses with the image_generation tool, use a text-capable Responses model such as gpt-5.4 "
"instead of a GPT image model."
)
def validate_input_fidelity(tool_model: str | None, input_fidelity: str | None) -> None:
if input_fidelity in (None, ""):
return
if tool_model is None or is_gpt_image_2_model(tool_model):
raise SystemExit("input_fidelity is not configurable for gpt-image-2. Omit --input-fidelity.")
if tool_model.startswith("gpt-image-1-mini"):
raise SystemExit("input_fidelity is not supported for gpt-image-1-mini.")
def resolve_common_image_options(args: argparse.Namespace, dotenv_values: Dict[str, str]) -> Dict[str, Any]:
size = resolve_value(getattr(args, "size", None), "OPENAI_IMAGE_SIZE", dotenv_values, DEFAULT_SIZE)
quality = resolve_value(getattr(args, "quality", None), "OPENAI_IMAGE_QUALITY", dotenv_values, DEFAULT_QUALITY)
background = resolve_value(getattr(args, "background", None), "OPENAI_IMAGE_BACKGROUND", dotenv_values)
output_format = resolve_value(
getattr(args, "output_format", None), "OPENAI_IMAGE_FORMAT", dotenv_values, DEFAULT_OUTPUT_FORMAT
)
output_compression_raw = resolve_value(getattr(args, "compression", None), "OPENAI_IMAGE_COMPRESSION", dotenv_values)
moderation = resolve_value(getattr(args, "moderation", None), "OPENAI_IMAGE_MODERATION", dotenv_values)
user = resolve_value(getattr(args, "user", None), "OPENAI_IMAGE_USER", dotenv_values)
stream = parse_bool(resolve_value(getattr(args, "stream", None), "OPENAI_IMAGE_STREAM", dotenv_values, False))
partial_images_raw = resolve_value(getattr(args, "partial_images", None), "OPENAI_IMAGE_PARTIAL_IMAGES", dotenv_values)
partial_images = None if partial_images_raw in (None, "") else parse_int(partial_images_raw, "partial_images")
output_compression = None if output_compression_raw in (None, "") else parse_int(output_compression_raw, "output_compression")
return {
"size": size,
"quality": quality,
"background": background,
"output_format": output_format,
"output_compression": output_compression,
"moderation": moderation,
"user": user,
"stream": stream,
"partial_images": partial_images,
}
def build_generations_request(args: argparse.Namespace, dotenv_values: Dict[str, str], settings: Dict[str, Any]) -> Dict[str, Any]:
model = resolve_images_model(args, dotenv_values)
image_options = resolve_common_image_options(args, dotenv_values)
n = parse_int(resolve_value(args.n, "OPENAI_IMAGE_N", dotenv_values, 1), "n")
response_format = resolve_value(args.response_format, "OPENAI_IMAGE_RESPONSE_FORMAT", dotenv_values)
validate_common_options(
model,
image_options["background"],
image_options["output_format"],
image_options["output_compression"],
image_options["partial_images"],
image_options["stream"],
)
validate_public_images_route(model, image_options["size"], image_options["stream"], n, response_format)
payload: Dict[str, Any] = {
"model": model,
"prompt": args.prompt,
"n": n,
}
if response_format not in (None, ""):
payload["response_format"] = response_format
for key, value in image_options.items():
if value in (None, ""):
continue
if key == "stream" and not value:
continue
payload[key] = value
return {
"url": f"{settings['base_url']}/images/generations",
"content_type": "application/json",
"body": json.dumps(payload).encode("utf-8"),
"stream": image_options["stream"],
"request_preview": payload,
}
def build_edits_request(args: argparse.Namespace, dotenv_values: Dict[str, str], settings: Dict[str, Any]) -> Dict[str, Any]:
model = resolve_images_model(args, dotenv_values)
image_options = resolve_common_image_options(args, dotenv_values)
n = parse_int(resolve_value(args.n, "OPENAI_IMAGE_N", dotenv_values, 1), "n")
response_format = resolve_value(args.response_format, "OPENAI_IMAGE_RESPONSE_FORMAT", dotenv_values)
image_paths = load_file_paths(args.image)
if not image_paths:
raise SystemExit("edits requires at least one --image.")
validate_common_options(
model,
image_options["background"],
image_options["output_format"],
image_options["output_compression"],
image_options["partial_images"],
image_options["stream"],
)
validate_public_images_route(model, image_options["size"], image_options["stream"], n, response_format)
fields: List[tuple[str, str]] = [
("model", model),
("prompt", args.prompt),
("n", str(n)),
]
if response_format not in (None, ""):
fields.append(("response_format", str(response_format)))
for key, value in image_options.items():
if value in (None, ""):
continue
if key == "stream" and not value:
continue
fields.append((key, str(value).lower() if isinstance(value, bool) else str(value)))
files: List[tuple[str, Path]] = []
for index, path in enumerate(image_paths):
field_name = "image"
if args.image_field_style == "brackets":
field_name = "image[]"
elif args.image_field_style == "indexed":
field_name = f"image[{index}]"
files.append((field_name, path))
if args.mask:
mask_path = Path(args.mask).expanduser().resolve()
if not mask_path.exists():
raise SystemExit(f"File not found: {mask_path}")
files.append(("mask", mask_path))
content_type, body = encode_multipart(fields, files)
request_preview = {
"fields": fields,
"files": [{"field": field, "path": str(path)} for field, path in files],
}
return {
"url": f"{settings['base_url']}/images/edits",
"content_type": content_type,
"body": body,
"stream": image_options["stream"],
"request_preview": request_preview,
}
def build_response_input_items(args: argparse.Namespace) -> List[Dict[str, Any]]:
content: List[Dict[str, Any]] = []
for text in args.input_text:
content.append({"type": "input_text", "text": text})
for path in load_file_paths(args.input_image):
content.append({"type": "input_image", "image_url": file_to_data_url(path)})
for url in args.input_image_url:
content.append({"type": "input_image", "image_url": url})
for data_url in args.input_image_data_url:
content.append({"type": "input_image", "image_url": data_url})
for file_id in args.input_image_file_id:
content.append({"type": "input_image", "file_id": file_id})
return content
def build_mask_object(args: argparse.Namespace) -> Dict[str, Any] | None:
if args.mask:
path = Path(args.mask).expanduser().resolve()
if not path.exists():
raise SystemExit(f"File not found: {path}")
return {"image_url": file_to_data_url(path)}
if args.mask_url:
return {"image_url": args.mask_url}
if args.mask_file_id:
return {"file_id": args.mask_file_id}
return None
def build_responses_request(args: argparse.Namespace, dotenv_values: Dict[str, str], settings: Dict[str, Any]) -> Dict[str, Any]:
responses_model = resolve_responses_model(args, dotenv_values)
validate_responses_model(responses_model)
tool_model = resolve_tool_model(args, dotenv_values)
tool_validation_model = tool_model or DEFAULT_IMAGES_MODEL
image_options = resolve_common_image_options(args, dotenv_values)
validate_size(image_options["size"], tool_validation_model)
validate_common_options(
tool_validation_model,
image_options["background"],
image_options["output_format"],
image_options["output_compression"],
image_options["partial_images"],
image_options["stream"],
)
input_fidelity = resolve_value(args.input_fidelity, "OPENAI_IMAGE_INPUT_FIDELITY", dotenv_values)
validate_input_fidelity(tool_model, input_fidelity)
tool_choice = normalize_tool_choice(resolve_value(args.tool_choice, "OPENAI_IMAGE_TOOL_CHOICE", dotenv_values))
input_items = build_response_input_items(args)
payload: Dict[str, Any] = {
"model": responses_model,
"tools": [{"type": "image_generation"}],
"tool_choice": tool_choice,
"stream": image_options["stream"],
}
if input_items:
payload["input"] = [{"role": "user", "content": input_items}]
if args.previous_response_id:
payload["previous_response_id"] = args.previous_response_id
if args.metadata_json:
payload["metadata"] = json.loads(args.metadata_json)
for key in ("size", "quality", "background", "output_format", "output_compression", "moderation"):
value = image_options.get(key)
if value not in (None, ""):
payload["tools"][0][key] = value
if tool_model is not None:
payload["tools"][0]["model"] = tool_model
if args.action is not None:
payload["tools"][0]["action"] = args.action
if input_fidelity not in (None, ""):
payload["tools"][0]["input_fidelity"] = input_fidelity
mask_object = build_mask_object(args)
if mask_object is not None:
payload["tools"][0]["input_image_mask"] = mask_object
if image_options["partial_images"] is not None:
payload["tools"][0]["partial_images"] = image_options["partial_images"]
if image_options["user"] not in (None, ""):
payload["user"] = image_options["user"]
return {
"url": f"{settings['base_url']}/responses",
"content_type": "application/json",
"body": json.dumps(payload).encode("utf-8"),
"stream": image_options["stream"],
"request_preview": payload,
}
def summarize_stream(events: List[Dict[str, Any]], output_template: str) -> Dict[str, Any]:
image_records: List[Dict[str, Any]] = []
for event in events:
if event["raw"] == "[DONE]":
continue
image_records.extend(extract_image_records(event["data"], event_name=event["event"]))
saved = save_images(image_records, output_template)
return {
"mode": "stream",
"events": len(events),
"images": saved,
}
def summarize_json_response(response: Any, output_template: str) -> Dict[str, Any]:
image_records = extract_image_records(response)
saved = save_images(image_records, output_template)
return {
"mode": "json",
"images": saved,
}
def print_result(result: Dict[str, Any], as_json: bool) -> None:
if as_json:
print(json.dumps(result, ensure_ascii=False, indent=2))
return
print(f"Mode: {result['mode']}")
if "events" in result:
print(f"Events: {result['events']}")
for item in result["images"]:
print(f"Saved: {item['path']} [{item['kind']}]")
if item.get("revised_prompt"):
print(f"Revised prompt: {item['revised_prompt']}")
def main() -> int:
args = parse_args()
dotenv_path = resolve_dotenv_path(args.env_file)
dotenv_values = load_dotenv(dotenv_path) if dotenv_path else {}
settings = common_settings(args, dotenv_values)
if args.command == "generations":
prepared = build_generations_request(args, dotenv_values, settings)
elif args.command == "edits":
prepared = build_edits_request(args, dotenv_values, settings)
else:
prepared = build_responses_request(args, dotenv_values, settings)
if settings["dry_run"]:
print(
json.dumps(
{
"command": args.command,
"url": prepared["url"],
"stream": prepared["stream"],
"request": prepared["request_preview"],
},
ensure_ascii=False,
indent=2,
)
)
return 0
response = send_request(
url=prepared["url"],
api_key=settings["api_key"],
body=prepared["body"],
content_type=prepared["content_type"],
timeout=settings["timeout"],
stream=prepared["stream"],
)
if args.save_response:
write_json_file(args.save_response, response)
if prepared["stream"]:
result = summarize_stream(response, args.output)
else:
result = summarize_json_response(response, args.output)
result["command"] = args.command
result["url"] = prepared["url"]
print_result(result, settings["json_output"])
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except KeyboardInterrupt:
print("Cancelled.", file=sys.stderr)
raise SystemExit(130)
import json
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
SCRIPT_PATH = Path(__file__).resolve().parents[1] / "scripts" / "gpt_image.py"
class GptImageCliTest(unittest.TestCase):
def run_cli(self, *args: str, cwd: str | None = None) -> subprocess.CompletedProcess[str]:
with tempfile.TemporaryDirectory() as tmpdir:
workdir = cwd or tmpdir
return subprocess.run(
[sys.executable, str(SCRIPT_PATH), *args],
cwd=workdir,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
env={},
check=False,
)
def test_responses_dry_run_uses_responses_model_and_object_tool_choice(self) -> None:
result = self.run_cli(
"responses",
"--input-text",
"Generate a poster",
"--output",
"poster.png",
"--dry-run",
)
self.assertEqual(result.returncode, 0, result.stderr)
payload = json.loads(result.stdout)
request = payload["request"]
self.assertEqual(request["model"], "gpt-5.4")
self.assertEqual(request["tool_choice"], {"type": "image_generation"})
self.assertEqual(request["tools"][0]["type"], "image_generation")
self.assertEqual(request["tools"][0]["size"], "auto")
self.assertEqual(request["tools"][0]["quality"], "high")
self.assertEqual(request["tools"][0]["output_format"], "webp")
def test_generations_rejects_response_format_for_gpt_image_models(self) -> None:
result = self.run_cli(
"generations",
"--prompt",
"test",
"--output",
"test.png",
"--response-format",
"b64_json",
"--dry-run",
)
self.assertNotEqual(result.returncode, 0)
self.assertIn("Omit response_format", result.stderr)
def test_responses_rejects_gpt_image_model_as_top_level_model(self) -> None:
result = self.run_cli(
"responses",
"--model",
"gpt-image-2",
"--input-text",
"Generate a poster",
"--output",
"poster.png",
"--dry-run",
)
self.assertNotEqual(result.returncode, 0)
self.assertIn("text-capable Responses model", result.stderr)
def test_responses_rejects_input_fidelity_for_default_gpt_image_2_tool(self) -> None:
result = self.run_cli(
"responses",
"--input-text",
"Generate a poster",
"--input-fidelity",
"high",
"--output",
"poster.png",
"--dry-run",
)
self.assertNotEqual(result.returncode, 0)
self.assertIn("not configurable for gpt-image-2", result.stderr)
def test_responses_accepts_supported_tool_model_with_input_fidelity(self) -> None:
result = self.run_cli(
"responses",
"--input-text",
"Generate a poster",
"--tool-model",
"gpt-image-1.5",
"--input-fidelity",
"high",
"--output",
"poster.png",
"--dry-run",
)
self.assertEqual(result.returncode, 0, result.stderr)
payload = json.loads(result.stdout)
tool = payload["request"]["tools"][0]
self.assertEqual(tool["model"], "gpt-image-1.5")
self.assertEqual(tool["input_fidelity"], "high")
def test_dry_run_finds_dotenv_in_parent_directory(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
nested = root / "a" / "b"
nested.mkdir(parents=True)
(root / ".env").write_text(
"\n".join(
[
"OPENAI_BASE_URL=http://parent-env.example/v1",
"OPENAI_IMAGE_QUALITY=medium",
]
),
encoding="utf-8",
)
result = self.run_cli(
"generations",
"--prompt",
"test",
"--output",
"test.png",
"--dry-run",
cwd=str(nested),
)
self.assertEqual(result.returncode, 0, result.stderr)
payload = json.loads(result.stdout)
request = payload["request"]
self.assertEqual(payload["url"], "http://parent-env.example/v1/images/generations")
self.assertEqual(request["quality"], "medium")
if __name__ == "__main__":
unittest.main()
Related skills
FAQ
What are the three API routes?
generations for text-to-image, edits for multipart image editing with masks, and responses for advanced flows with streaming and mixed text+image input.
Do I need to handle constraints manually?
No - the script includes strict pre-flight validation of model size, aspect ratio, and feature constraints.
Is Gpt Image 2 safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.