
Gpt Image 2 Api
- 2 installs
- 14 repo stars
- Updated August 1, 2026
- benedictking/benedictking-skills
gpt-image-2-api is a Claude Code skill that generates and edits images with gpt-image-2 through a third-party OpenAI-compatible API using env-configured keys.
About
This skill generates and edits images with gpt-image-2 through a third-party OpenAI-compatible API endpoint. A developer uses it to create illustrations, visual assets, or image variations, and to edit reference images with optional masks and transparent backgrounds. It is configured via OPENAI_API_KEY and OPENAI_BASE_URL and driven by a Node.js script that calls the images generation and edit endpoints.
- Generates and edits images with gpt-image-2 via an OpenAI-compatible API
- Supports edits, masks, transparent backgrounds, and multiple output formats
- Configured through OPENAI_API_KEY and OPENAI_BASE_URL in env or .env
Gpt Image 2 Api by the numbers
- 2 all-time installs (skills.sh)
- Ranked #1,166 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
gpt-image-2-api capabilities & compatibility
Requires OPENAI_API_KEY and OPENAI_BASE_URL for a third-party OpenAI-compatible endpoint; cost depends on that endpoint.
- Capabilities
- image generation
- Works with
- openai
- Use cases
- image generation
- Pricing
- Bring your own API key
What gpt-image-2-api says it does
Generate and edit images with gpt-image-2 through a third-party OpenAI-compatible API using .env-configured OPENAI_API_KEY and OPENAI_BASE_URL values.
npx skills add https://github.com/benedictking/benedictking-skills --skill gpt-image-2-apiAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 14 |
| Last updated | August 1, 2026 |
| Repository | benedictking/benedictking-skills ↗ |
What it does
Generate or edit images with gpt-image-2 through an OpenAI-compatible API endpoint configured via env or .env.
Who is it for?
Creating illustrations and visual assets or editing reference images via a configurable API endpoint
Skip if: Users without an OpenAI-compatible image endpoint or the required env keys
When should I use this skill?
The user asks to create images, edit reference images, or produce illustrations and variations
What you get
Generated or edited image files saved locally with reported paths and API errors
- Generated image files
- Edited image files
By the numbers
- default size 1024x1024
- n range 1-10 images
- default model gpt-image-2
Files
gpt-image-2-api
Purpose
Generate and edit images with gpt-image-2 through a third-party OpenAI-compatible API using .env-configured OPENAI_API_KEY and OPENAI_BASE_URL values. Use when the user asks to create images, edit reference images, create visual assets, illustrations, or image variations with a configurable API endpoint.
Environment Variables & API Key
Two ways to configure API settings (priority: environment variable > .env):
1. Environment variables: OPENAI_API_KEY, OPENAI_BASE_URL, and optional OPENAI_IMAGE_* overrides 2. .env file: Place in .env, can copy from .env.example
You can manage the .env file manually or through skill-master env set.
Configuration
OPENAI_API_KEY: API key for the third-party endpointOPENAI_BASE_URL: OpenAI-compatible base URL, for examplehttp://127.0.0.1:3688/v1OPENAI_IMAGE_MODEL: image model name, defaults togpt-image-2OPENAI_IMAGE_PROTOCOL:openai_imagesoropenai_chat, defaults toopenai_imagesOPENAI_IMAGE_SIZE: image size, defaults to1024x1024OPENAI_IMAGE_QUALITY: optional image qualityOPENAI_IMAGE_N: number of images (1-10), defaults to1OPENAI_IMAGE_OUTPUT_FORMAT: optional output format (png,jpeg,webp)OPENAI_IMAGE_FORMAT: legacy alias forOPENAI_IMAGE_OUTPUT_FORMATOPENAI_IMAGE_RESPONSE_FORMAT: optional response format (url,b64_json)OPENAI_IMAGE_BACKGROUND: optional background (transparent,opaque,auto)OPENAI_IMAGE_MODERATION: optional moderation level (low,auto)OPENAI_IMAGE_OUTPUT_COMPRESSION: optional compression level0–100forjpeg/webpOPENAI_IMAGE_PARTIAL_IMAGES: optional streaming partial image count0–3OPENAI_IMAGE_INPUT_FIDELITY: optional edit fidelity (high,low)OPENAI_IMAGE_STYLE: optional generation style fordall-e-3(vivid,natural)OPENAI_IMAGE_STREAM: optionalopenai_imagesSSE toggle (true,false)OPENAI_IMAGE_USER: optional end-user identifierOPENAI_IMAGE_EXTRA_JSON: optional JSON object merged into the request body
Workflow
1. Use scripts/gpt-image-2-api.mjs for text-to-image and image-edit requests. 2. If required OPENAI_* settings are not available from the current environment or this skill's .env, stop and tell the user which variables need to be configured. 3. openai_images generation uses /images/generations with JSON; openai_images editing uses /images/edits with multipart/form-data. 4. Use OPENAI_IMAGE_PROTOCOL=openai_chat only when the endpoint lacks /images/* support; it does not support --mask, --background, --output-format, or streaming image events. 5. Add one or more --image <path> arguments to edit/reference an input image. 6. Use --mask <path> or --mask auto only with openai_images edits. 7. Save generated files under the user requested output directory, or the current working directory when unspecified. 8. Report saved file paths and any API errors exactly.
Available Scripts
scripts/gpt-image-2-api.mjs— Sends a text prompt, optionally with input images, to the configured OpenAI-compatible endpoint and saves returned images.
Generate Image
node scripts/gpt-image-2-api.mjs --prompt "a concise image prompt" --output ./generated-images --size 1024x1024 --n 1 --output-format pngUse --base-url, --model, --protocol, --size, --quality, --n, --output-format, --background, --moderation, --response-format, and --extra-json only when the user or endpoint requires overrides.
Edit Image
node scripts/gpt-image-2-api.mjs --prompt "make the dog wear a red scarf" --image ./input.png --output ./edited-imagesMultiple input images are supported by repeating --image. --input is accepted as an alias for --image.
Mask example for openai_images:
node scripts/gpt-image-2-api.mjs --protocol openai_images --prompt "replace the background" --image ./input.png --mask ./mask.png --output ./edited-imagesAuto-mask example for openai_images:
node scripts/gpt-image-2-api.mjs --protocol openai_images --prompt "remove the background" --image ./input.png --mask auto --background transparent --output-format png --output ./edited-imagesFallback example when the endpoint only supports chat completions:
OPENAI_IMAGE_PROTOCOL=openai_chat node scripts/gpt-image-2-api.mjs --prompt "a concise image prompt" --output ./generated-imagesParameters
| Parameter | Required | Type | Default | Description |
|---|---|---|---|---|
prompt | yes | string | — | Text description for generation or edit |
model | no | string | gpt-image-2 | Image model name |
n | no | integer | 1 | Number of images, range 1–10 |
size | no | string | 1024x1024 | Output size passed through to the upstream endpoint |
quality | no | string | endpoint default | low, medium, high, auto |
output-format | no | string | endpoint default | png, jpeg, webp |
response-format | no | string | endpoint default | url, b64_json; mainly for DALL·E-compatible responses |
background | no | string | endpoint default | transparent, opaque, auto |
moderation | no | string | endpoint default | low, auto |
output-compression | no | integer | endpoint default | Compression 0–100 for jpeg/webp |
partial-images | no | integer | endpoint default | Streaming partial image count 0–3 |
input-fidelity | no | string | endpoint default | Edit fidelity: high, low |
style | no | string | endpoint default | Generation style for dall-e-3: vivid, natural |
user | no | string | — | End-user identifier |
format | no | string | — | Legacy alias for output-format |
Size Behavior
The script does not hard-validate size; values are passed through so OpenAI-compatible upstream endpoints can decide what they support.
For the Yunwu-compatible docs currently checked in this project:
/images/generationsdocumentsauto,1024x1024,1536x1024,1024x1536,2048x2048,2048x1152,3840x2160, and2160x3840/images/editscurrently documents the classic GPT image set:auto,1024x1024,1536x1024,1024x1536dall-e-2documents256x256,512x512,1024x1024dall-e-3documents1024x1024,1792x1024,1024x1792- Chat-compatible fallbacks remain endpoint-specific
Because provider behavior may differ by endpoint and documentation may lag behind the implementation, this skill forwards size unchanged and lets the upstream API accept or reject it.
Size Constraints
1. Max side length ≤ 3840px 2. Both sides must be multiples of 16px 3. Aspect ratio ≤ 3:1 (long/short) 4. Total pixels: min 655360, max 8294400
# gpt-image-2 OpenAI-compatible API configuration.
# OPENAI_IMAGE_PROTOCOL=openai_chat is the verified local proxy path; openai_images calls /images/* endpoints.
OPENAI_API_KEY=
OPENAI_BASE_URL=http://127.0.0.1:3688/v1
OPENAI_IMAGE_MODEL=gpt-image-2
OPENAI_IMAGE_PROTOCOL=openai_images
OPENAI_IMAGE_SIZE=1024x1024
OPENAI_IMAGE_QUALITY=
OPENAI_IMAGE_N=1
OPENAI_IMAGE_OUTPUT_FORMAT=
OPENAI_IMAGE_FORMAT=
OPENAI_IMAGE_RESPONSE_FORMAT=
OPENAI_IMAGE_BACKGROUND=
OPENAI_IMAGE_MODERATION=
OPENAI_IMAGE_OUTPUT_COMPRESSION=
OPENAI_IMAGE_PARTIAL_IMAGES=
OPENAI_IMAGE_INPUT_FIDELITY=
OPENAI_IMAGE_STYLE=
OPENAI_IMAGE_STREAM=
OPENAI_IMAGE_USER=
OPENAI_IMAGE_EXTRA_JSON=
#!/usr/bin/env node
import { existsSync } from 'node:fs';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
function parseEnv(content) {
const data = {};
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eq = trimmed.indexOf('=');
if (eq === -1) continue;
const key = trimmed.slice(0, eq).trim();
let value = trimmed.slice(eq + 1).trim();
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
data[key] = value;
}
return data;
}
let cachedSkillEnv;
async function readSkillEnv() {
if (cachedSkillEnv) return cachedSkillEnv;
const envPath = path.join(__dirname, '..', '.env');
if (!existsSync(envPath)) {
cachedSkillEnv = {};
return cachedSkillEnv;
}
cachedSkillEnv = parseEnv(await readFile(envPath, 'utf-8'));
return cachedSkillEnv;
}
function readEnvValue(skillEnv, key) {
return process.env[key] || skillEnv[key];
}
function readArg(name, fallback) {
const index = process.argv.indexOf(name);
if (index !== -1) {
const value = process.argv[index + 1];
if (!value || value.startsWith('--')) throw new Error(`${name} requires a value`);
return value;
}
return fallback;
}
function hasFlag(name) {
return process.argv.includes(name);
}
const VALUE_ARGS = new Set([
'--background',
'--base-url',
'--extra-json',
'--format',
'--image',
'--input',
'--input-fidelity',
'--mask',
'--model',
'--moderation',
'--n',
'--output',
'--output-compression',
'--output-format',
'--partial-images',
'--prompt',
'--protocol',
'--quality',
'--response-format',
'--size',
'--stream',
'--style',
'--user',
]);
function collectPositionalPrompt() {
const values = [];
for (let index = 2; index < process.argv.length; index += 1) {
const arg = process.argv[index];
if (VALUE_ARGS.has(arg)) {
index += 1;
continue;
}
if (arg.startsWith('--')) continue;
values.push(arg);
}
return values.join(' ');
}
function readArgs(name) {
const values = [];
for (let index = 2; index < process.argv.length; index += 1) {
if (process.argv[index] !== name) continue;
const value = process.argv[index + 1];
if (!value || value.startsWith('--')) throw new Error(`${name} requires a value`);
values.push(value);
index += 1;
}
return values;
}
function normalizeProtocol(value) {
const normalized = String(value || 'openai_images').trim().toLowerCase().replace(/-/g, '_');
if (['image', 'images', 'openai_images'].includes(normalized)) return 'openai_images';
if (['chat', 'chat_completions', 'openai_chat'].includes(normalized)) return 'openai_chat';
throw new Error(`Unsupported OPENAI_IMAGE_PROTOCOL: ${value}. Use openai_images or openai_chat.`);
}
function parseJsonObject(value, source) {
if (!value) return {};
const parsed = JSON.parse(value);
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error(`${source} must be a JSON object`);
}
return parsed;
}
function endpointPath(protocol, operation) {
if (protocol === 'openai_chat') return '/chat/completions';
return operation === 'edit' ? '/images/edits' : '/images/generations';
}
function buildRequestBody({ protocol, model, prompt, size, quality, n, responseFormat, outputFormat, inputImages, extraParams }) {
if (protocol === 'openai_chat') {
const content = [{ type: 'text', text: prompt }];
for (const image of inputImages) {
content.push({
type: 'image_url',
image_url: { url: image.dataUrl },
});
}
const chatBody = {
model,
messages: [
{
role: 'user',
content,
},
],
...extraParams,
};
if (n && n !== 1) chatBody.n = n;
return chatBody;
}
const body = { model, prompt, n };
if (size) body.size = size;
if (quality) body.quality = quality;
if (responseFormat) body.response_format = responseFormat;
if (outputFormat) body.output_format = outputFormat;
return { ...body, ...extraParams };
}
function buildMultipartBody({ model, prompt, size, quality, n, responseFormat, outputFormat, inputImages, mask, extraParams }) {
const form = new FormData();
form.set('model', model);
form.set('prompt', prompt);
if (size) form.set('size', size);
if (quality) form.set('quality', quality);
if (n) form.set('n', String(n));
if (responseFormat) form.set('response_format', responseFormat);
if (outputFormat) form.set('output_format', outputFormat);
for (const image of inputImages) {
form.append('image', new Blob([image.bytes], { type: image.mimeType }), image.name);
}
if (mask) {
if (mask === 'auto') {
form.set('mask', 'auto');
} else {
form.set('mask', new Blob([mask.bytes], { type: mask.mimeType }), mask.name);
}
}
for (const [key, value] of Object.entries(extraParams)) {
appendFormValue(form, key, value);
}
return form;
}
function appendFormValue(form, key, value) {
if (value === undefined || value === null) return;
if (Array.isArray(value)) {
for (const item of value) appendFormValue(form, `${key}[]`, item);
return;
}
if (typeof value === 'object') {
form.set(key, JSON.stringify(value));
return;
}
form.set(key, String(value));
}
function decodeDataUrl(value) {
if (!value.startsWith('data:') || !value.includes(';base64,')) return undefined;
const [header, data] = value.split(',', 2);
const mimeType = header.replace(/^data:/, '').split(';', 1)[0] || 'image/png';
return decodeBase64Image(data, mimeType);
}
function decodeBase64Image(base64, mimeType = 'image/png') {
return {
bytes: Buffer.from(base64, 'base64'),
mimeType,
};
}
function mimeTypeForPath(filePath) {
const ext = path.extname(filePath).toLowerCase();
if (ext === '.jpg' || ext === '.jpeg') return 'image/jpeg';
if (ext === '.png') return 'image/png';
if (ext === '.webp') return 'image/webp';
if (ext === '.gif') return 'image/gif';
return 'application/octet-stream';
}
async function readImageInput(filePath) {
const resolvedPath = path.resolve(filePath);
const bytes = await readFile(resolvedPath);
const mimeType = mimeTypeForPath(resolvedPath);
const base64 = bytes.toString('base64');
return {
name: path.basename(resolvedPath),
mimeType,
base64,
dataUrl: `data:${mimeType};base64,${base64}`,
};
}
function extractMarkdownImageUrl(text) {
const match = text.match(/!\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/);
return match?.[1];
}
function addImageUrl(value, images) {
if (typeof value !== 'string' || !value) return;
const decoded = decodeDataUrl(value);
images.push(decoded || { url: value });
}
function addImageObject(value, images) {
if (!value || typeof value !== 'object') return;
const imageBase64 = value.b64_json || value.image_base64;
if (typeof imageBase64 === 'string' && imageBase64) {
images.push(decodeBase64Image(imageBase64, value.mime_type || value.mimeType || 'image/png'));
}
if (typeof value.url === 'string') addImageUrl(value.url, images);
if (value.image_url && typeof value.image_url.url === 'string') addImageUrl(value.image_url.url, images);
if (typeof value.image_url === 'string') addImageUrl(value.image_url, images);
if (value.image && typeof value.image === 'object') addImageObject(value.image, images);
}
function collectImages(protocol, payload) {
if (protocol === 'openai_images') {
return Array.isArray(payload.data)
? payload.data.flatMap((item) => {
const images = [];
addImageObject(item, images);
return images;
})
: [];
}
const message = Array.isArray(payload.choices) ? payload.choices[0]?.message : undefined;
const images = [];
if (!message || typeof message !== 'object') return images;
if (typeof message.content === 'string') addImageUrl(extractMarkdownImageUrl(message.content), images);
if (Array.isArray(message.content)) {
for (const part of message.content) {
addImageObject(part, images);
if (part && typeof part.text === 'string') addImageUrl(extractMarkdownImageUrl(part.text), images);
}
}
if (Array.isArray(message.images)) {
for (const image of message.images) addImageObject(image, images);
}
return images;
}
function extractChatText(payload) {
const message = Array.isArray(payload.choices) ? payload.choices[0]?.message : undefined;
if (!message || typeof message !== 'object') return '';
if (typeof message.content === 'string') return message.content.trim();
if (!Array.isArray(message.content)) return '';
const texts = [];
for (const part of message.content) {
if (!part || typeof part !== 'object') continue;
if (!['text', 'output_text'].includes(part.type)) continue;
if (typeof part.text === 'string' && part.text.trim()) texts.push(part.text.trim());
}
return texts.join('\n');
}
function summarizeNoImagePayload(protocol, payload) {
const chatText = protocol === 'openai_chat' ? extractChatText(payload) : '';
const summary = chatText || JSON.stringify(payload);
return summary.length > 1000 ? `${summary.slice(0, 1000)}...` : summary;
}
async function resolveImageBytes(image) {
if (image.bytes) return image;
if (!image.url) throw new Error('Image response item has neither bytes nor url');
const response = await fetch(image.url);
if (!response.ok) throw new Error(`Failed to download image URL: HTTP ${response.status}`);
const contentType = response.headers?.get?.('content-type') || 'image/png';
return {
bytes: Buffer.from(await response.arrayBuffer()),
mimeType: contentType.split(';', 1)[0].trim() || 'image/png',
};
}
function extensionForMimeType(mimeType) {
const normalized = String(mimeType || '').toLowerCase();
if (normalized.includes('jpeg')) return '.jpg';
if (normalized.includes('png')) return '.png';
if (normalized.includes('webp')) return '.webp';
if (normalized.includes('gif')) return '.gif';
return '.bin';
}
function parseOptionalInt(value, name) {
if (value === undefined || value === null || value === '') return undefined;
const parsed = Number.parseInt(String(value), 10);
if (!Number.isInteger(parsed)) throw new Error(`${name} must be an integer`);
return parsed;
}
function buildExtraParams({ operation, background, moderation, outputCompression, outputFormat, partialImages, inputFidelity, style, user }) {
const extra = {};
if (background) extra.background = background;
if (moderation) extra.moderation = moderation;
if (outputCompression !== undefined) extra.output_compression = outputCompression;
if (outputFormat) extra.output_format = outputFormat;
if (partialImages !== undefined) extra.partial_images = partialImages;
if (operation === 'edit' && inputFidelity) extra.input_fidelity = inputFidelity;
if (operation === 'generate' && style) extra.style = style;
if (user) extra.user = user;
return extra;
}
function validateArgs({ protocol, operation, n, responseFormat, outputFormat, background, partialImages, size, inputFidelity, style, stream, hasMask }) {
if (!Number.isInteger(n) || n < 1 || n > 10) throw new Error('--n must be an integer between 1 and 10');
if (background && !['transparent', 'opaque', 'auto'].includes(background)) {
throw new Error('--background must be one of: transparent, opaque, auto');
}
if (responseFormat && !['url', 'b64_json'].includes(responseFormat)) {
throw new Error('--response-format must be url or b64_json');
}
if (outputFormat && !['png', 'jpeg', 'webp'].includes(outputFormat)) {
throw new Error('--output-format must be one of: png, jpeg, webp');
}
if (partialImages !== undefined && (partialImages < 0 || partialImages > 3)) {
throw new Error('--partial-images must be an integer between 0 and 3');
}
if (inputFidelity && !['high', 'low'].includes(inputFidelity)) {
throw new Error('--input-fidelity must be high or low');
}
if (style && !['vivid', 'natural'].includes(style)) {
throw new Error('--style must be vivid or natural');
}
if (background === 'transparent' && outputFormat && !['png', 'webp'].includes(outputFormat)) {
throw new Error('transparent background requires --output-format to be png or webp');
}
if (protocol === 'openai_chat') {
if (responseFormat) throw new Error('--response-format is not supported with openai_chat');
if (outputFormat) throw new Error('--output-format is not supported with openai_chat');
if (background) throw new Error('--background is not supported with openai_chat');
if (partialImages !== undefined) throw new Error('--partial-images is not supported with openai_chat');
if (inputFidelity) throw new Error('--input-fidelity is not supported with openai_chat');
if (style) throw new Error('--style is not supported with openai_chat');
if (stream !== undefined) throw new Error('--stream is not supported with openai_chat');
if (hasMask) throw new Error('--mask is not supported with openai_chat');
}
}
function mergeExtraParams(baseParams, cliExtraParams) {
return { ...baseParams, ...cliExtraParams };
}
function buildRequestOptions({ protocol, operation, apiKey, baseUrl, model, prompt, size, quality, n, responseFormat, outputFormat, inputImages, mask, extraParams }) {
const endpoint = `${baseUrl}${endpointPath(protocol, operation)}`;
const headers = { Authorization: `Bearer ${apiKey}` };
if (protocol === 'openai_images' && operation === 'edit') {
return {
url: endpoint,
options: {
method: 'POST',
headers,
body: buildMultipartBody({ model, prompt, size, quality, n, responseFormat, outputFormat, inputImages, mask, extraParams }),
},
};
}
headers['Content-Type'] = 'application/json';
return {
url: endpoint,
options: {
method: 'POST',
headers,
body: JSON.stringify(buildRequestBody({ protocol, model, prompt, size, quality, n, responseFormat, outputFormat, inputImages, extraParams })),
},
};
}
function parseSseEvents(text) {
const events = [];
for (const chunk of text.split(/\n\n+/)) {
const lines = chunk.split('\n');
const dataLines = lines.filter((line) => line.startsWith('data:')).map((line) => line.slice(5).trim());
if (dataLines.length === 0) continue;
const dataText = dataLines.join('\n');
if (!dataText || dataText === '[DONE]') continue;
try {
events.push(JSON.parse(dataText));
} catch {
continue;
}
}
return events;
}
function normalizePayload(protocol, operation, text) {
try {
return JSON.parse(text);
} catch {
if (protocol !== 'openai_images') {
throw new Error(`Image API returned non-JSON response: ${text.slice(0, 1000)}`);
}
const events = parseSseEvents(text);
const finalEvent = [...events].reverse().find((event) => typeof event?.b64_json === 'string');
if (!finalEvent) throw new Error(`Image API returned non-JSON response: ${text.slice(0, 1000)}`);
const kind = operation === 'edit' ? 'image_edit' : 'image_generation';
return {
data: [{ b64_json: finalEvent.b64_json, mime_type: finalEvent.output_format ? `image/${finalEvent.output_format}` : 'image/png' }],
usage: finalEvent.usage,
output_format: finalEvent.output_format,
background: finalEvent.background,
quality: finalEvent.quality,
size: finalEvent.size,
stream_event_type: `${kind}.completed`,
};
}
}
function printHelp() {
console.log(`Usage:
node scripts/gpt-image-2-api.mjs --prompt "image prompt" [--output ./out] [--size 1024x1024] [--n 1] [--output-format png|jpeg|webp] [--quality low|medium|high|auto] [--protocol openai_images|openai_chat]
node scripts/gpt-image-2-api.mjs --prompt "edit prompt" --image ./input.png [--image ./reference.png] [--mask ./mask.png|auto] [--output ./out]
Options:
--n <int> Number of images (1-10), defaults to 1
--size <str> Image size passed through to the upstream endpoint
--quality <str> Image quality: low, medium, high, auto
--response-format <str> Response format: url, b64_json
--output-format <str> Output format: png, jpeg, webp
--output-compression <int> Compression level 0-100 for jpeg/webp
--background <str> Background: transparent, opaque, auto
--moderation <str> Moderation: low, auto
--partial-images <int> Streaming partial images count: 0-3
--input-fidelity <str> Edit fidelity: high, low
--style <str> Generation style for dall-e-3: vivid, natural
--user <str> End-user identifier for abuse monitoring
--format <str> Legacy alias kept for compatibility
Environment:
OPENAI_API_KEY API key for the OpenAI-compatible endpoint
OPENAI_BASE_URL Base URL, for example http://127.0.0.1:3688/v1
OPENAI_IMAGE_MODEL Image model, defaults to gpt-image-2
OPENAI_IMAGE_PROTOCOL openai_images or openai_chat, defaults to openai_images
OPENAI_IMAGE_SIZE Image size, defaults to 1024x1024
OPENAI_IMAGE_QUALITY Optional image quality
OPENAI_IMAGE_N Number of images (1-10), defaults to 1
OPENAI_IMAGE_OUTPUT_FORMAT Optional output format: png, jpeg, webp
OPENAI_IMAGE_FORMAT Legacy output format alias: png, jpeg, webp
OPENAI_IMAGE_RESPONSE_FORMAT Optional response format: url, b64_json
OPENAI_IMAGE_BACKGROUND Optional background: transparent, opaque, auto
OPENAI_IMAGE_MODERATION Optional moderation: low, auto
OPENAI_IMAGE_OUTPUT_COMPRESSION Optional compression 0-100 for jpeg/webp
OPENAI_IMAGE_PARTIAL_IMAGES Optional streaming partial image count: 0-3
OPENAI_IMAGE_INPUT_FIDELITY Optional edit fidelity: high, low
OPENAI_IMAGE_STYLE Optional generation style for dall-e-3: vivid, natural
OPENAI_IMAGE_STREAM Optional openai_images SSE toggle: true, false
OPENAI_IMAGE_USER Optional end-user identifier
OPENAI_IMAGE_EXTRA_JSON Optional JSON object merged into the request body
`);
}
async function main() {
if (hasFlag('--help') || hasFlag('-h')) {
printHelp();
return;
}
const skillEnv = await readSkillEnv();
const apiKey = readEnvValue(skillEnv, 'OPENAI_API_KEY');
const baseUrl = readArg('--base-url', readEnvValue(skillEnv, 'OPENAI_BASE_URL') || 'http://127.0.0.1:3688/v1').replace(/\/+$/, '');
const model = readArg('--model', readEnvValue(skillEnv, 'OPENAI_IMAGE_MODEL') || 'gpt-image-2');
const protocol = normalizeProtocol(readArg('--protocol', readEnvValue(skillEnv, 'OPENAI_IMAGE_PROTOCOL') || 'openai_images'));
const prompt = readArg('--prompt', collectPositionalPrompt()).trim();
const size = readArg('--size', readEnvValue(skillEnv, 'OPENAI_IMAGE_SIZE') || '1024x1024');
const quality = readArg('--quality', readEnvValue(skillEnv, 'OPENAI_IMAGE_QUALITY'));
const n = parseInt(readArg('--n', readEnvValue(skillEnv, 'OPENAI_IMAGE_N') || '1'), 10);
const legacyFormat = readArg('--format', readEnvValue(skillEnv, 'OPENAI_IMAGE_FORMAT'));
const responseFormat = readArg('--response-format', readEnvValue(skillEnv, 'OPENAI_IMAGE_RESPONSE_FORMAT'));
const outputFormat = readArg('--output-format', readEnvValue(skillEnv, 'OPENAI_IMAGE_OUTPUT_FORMAT') || legacyFormat);
const background = readArg('--background', readEnvValue(skillEnv, 'OPENAI_IMAGE_BACKGROUND'));
const moderation = readArg('--moderation', readEnvValue(skillEnv, 'OPENAI_IMAGE_MODERATION'));
const outputCompression = parseOptionalInt(readArg('--output-compression', readEnvValue(skillEnv, 'OPENAI_IMAGE_OUTPUT_COMPRESSION')), '--output-compression');
const partialImages = parseOptionalInt(readArg('--partial-images', readEnvValue(skillEnv, 'OPENAI_IMAGE_PARTIAL_IMAGES')), '--partial-images');
const inputFidelity = readArg('--input-fidelity', readEnvValue(skillEnv, 'OPENAI_IMAGE_INPUT_FIDELITY'));
const style = readArg('--style', readEnvValue(skillEnv, 'OPENAI_IMAGE_STYLE'));
const streamValue = readArg('--stream', readEnvValue(skillEnv, 'OPENAI_IMAGE_STREAM'));
const stream = streamValue === undefined ? undefined : streamValue === 'true';
const user = readArg('--user', readEnvValue(skillEnv, 'OPENAI_IMAGE_USER'));
const cliExtraParams = parseJsonObject(readArg('--extra-json', readEnvValue(skillEnv, 'OPENAI_IMAGE_EXTRA_JSON')), 'OPENAI_IMAGE_EXTRA_JSON');
const outputDir = path.resolve(readArg('--output', process.cwd()));
const imagePaths = [...readArgs('--image'), ...readArgs('--input')];
const inputImages = await Promise.all(imagePaths.map(readImageInput));
const maskValue = readArg('--mask');
const mask = maskValue && maskValue !== 'auto' ? await readImageInput(maskValue) : maskValue;
const operation = inputImages.length > 0 ? 'edit' : 'generate';
if (!apiKey) throw new Error('OPENAI_API_KEY is required');
if (!prompt) throw new Error('Prompt is required. Use --prompt "..."');
if (mask && operation !== 'edit') throw new Error('--mask requires at least one --image');
validateArgs({ protocol, operation, n, responseFormat, outputFormat, background, partialImages, size, inputFidelity, style, stream, hasMask: Boolean(maskValue) });
const baseExtraParams = buildExtraParams({
operation,
background,
moderation,
outputCompression,
outputFormat,
partialImages,
inputFidelity,
style,
user,
});
if (stream !== undefined) baseExtraParams.stream = stream;
const extraParams = mergeExtraParams(baseExtraParams, cliExtraParams);
const { url, options } = buildRequestOptions({
protocol,
operation,
apiKey,
baseUrl,
model,
prompt,
size,
quality,
n,
responseFormat,
outputFormat,
inputImages,
mask,
extraParams,
});
const response = await fetch(url, options);
const text = await response.text();
if (!response.ok) {
throw new Error(`Image API failed with HTTP ${response.status}: ${text}`);
}
const payload = normalizePayload(protocol, operation, text);
await mkdir(outputDir, { recursive: true });
const saved = [];
const images = collectImages(protocol, payload);
for (const [index, image] of images.entries()) {
const resolved = await resolveImageBytes(image);
const filePath = path.join(outputDir, `gpt-image-2-${Date.now()}-${index + 1}${extensionForMimeType(resolved.mimeType)}`);
await writeFile(filePath, resolved.bytes);
saved.push(filePath);
}
if (saved.length === 0) {
throw new Error(`Image API response returned no image for ${protocol}. Upstream response: ${summarizeNoImagePayload(protocol, payload)}`);
}
console.log(JSON.stringify({ files: saved }, null, 2));
}
main().catch((error) => {
console.error(error.message);
process.exit(1);
});
Related skills
FAQ
How is the endpoint configured?
Via OPENAI_API_KEY and OPENAI_BASE_URL as env vars or in a .env file, with env vars taking priority.
What if the endpoint lacks /images support?
Set OPENAI_IMAGE_PROTOCOL=openai_chat, though it does not support masks, backgrounds, output-format, or streaming.