openai-image-api
v3.1.0
Published
Generate and edit images through the OpenAI Image API — GPT Image 2.5 (Sunburst, Flare), GPT Image 2 and 1.x — with streaming partial images, an organization image-cost assessment, a CLI, and TypeScript types
Maintainers
Readme
OpenAI Image Generation, Editing & Cost Assessment
A Node.js wrapper for the OpenAI Image API — /v1/images/generations and /v1/images/edits — for the GPT Image model family: GPT Image 2.5 (Sunburst, Flare), GPT Image 2, and the deprecated GPT Image 1.x models. Generate and edit images, with streaming partial-image delivery, via CLI or programmatic API — and reconcile what those images cost with openai-img cost, which joins the organization usage and costs endpoints per day, scope and model.
This service follows the data-collection architecture pattern with organized data storage, logging, parameter validation, and CLI orchestration. Written in TypeScript with full type definitions included. Requires Node.js 18 or newer.
Upgrading from 2.x? DALL-E 2/3, image variations, and Sora video generation were removed in 3.0.0 because OpenAI has shut down (or is about to shut down) those APIs. See Migrating from 2.x.
Quick Start
CLI Usage
# Install globally
npm install -g openai-image-api
export OPENAI_API_KEY="your-api-key-here"
# Generate an image (default model: gpt-image-2.5-flare)
openai-img --prompt "a serene mountain landscape"
# Stream partial frames while it renders
openai-img --stream --partial-images 2 --prompt "a river made of owl feathers"
# Edit an image
openai-img --sunburst --edit --image photo.png --prompt "make the sky stormy"Programmatic Usage
import { OpenAIImageAPI } from 'openai-image-api';
const api = new OpenAIImageAPI();
// Buffered generation
const result = await api.generateImage({
prompt: 'a serene mountain landscape',
model: 'gpt-image-2.5-flare',
quality: 'high',
size: '1536x1024',
});
await api.saveImages(result, './out', 'mountains'); // → ./out/mountains.png
// Streaming generation with partial frames
const streamed = await api.generateImageStream(
{ prompt: 'a river made of owl feathers', partial_images: 2 },
{ onPartialImage: (e) => console.log(`partial ${e.partial_image_index} arrived`) }
);
await api.saveImages(streamed, './out', 'river');Table of Contents
- Overview
- Models
- Authentication Setup
- Installation
- TypeScript Support
- CLI Usage
- API Methods
- Streaming
- Examples
- Cost Assessment
- Data Organization
- Testing
- Error Handling
- Troubleshooting
- Migrating from 2.x
Overview
The package wraps the two Image API endpoints, and — with an admin key — the three organization endpoints that say what the images cost:
| Endpoint | Class / method | Streaming variant |
| ---------------------------------------- | ------------------------------------------------------- | ------------------------------------------------- |
| POST /v1/images/generations | OpenAIImageAPI.generateImage() | streamImage() / generateImageStream() |
| POST /v1/images/edits | OpenAIImageAPI.generateImageEdit() | streamImageEdit() / generateImageEditStream() |
| GET /v1/organization/usage/completions | OpenAIAdminAPI.listCompletionsUsage() | — |
| GET /v1/organization/usage/images | OpenAIAdminAPI.listImagesUsage() | — |
| GET /v1/organization/costs | OpenAIAdminAPI.listCosts() | — |
| all three, joined | OpenAIAdminAPI.assessImageCosts() / openai-img cost | — |
Every request is validated client-side against the model's published constraints (sizes, quality tiers, formats, n, partial_images, input_fidelity) before any network call, so a bad parameter fails fast with a specific message rather than a generic 400.
The constraint tables are a transcription of OpenAI's reference as of 2026-09-20. That cuts both ways: when OpenAI tightens a limit the request goes out and the API's own 400 comes back; when OpenAI loosens one, or ships a model this release does not know, the validator says no before the network. For that case pass skipValidation: true (library) or --no-validate (CLI): the request is sent as-is and the API is the judge. --dry-run reports what this package would reject, not what the API would.
The Responses API image_generation tool (multi-turn conversational editing) is a different surface and is not wrapped here.
Models
| Model | Sizes | Quality | Notes |
| ----------------------------------- | ------------------- | ------------------------------------------ | -------------------------------------- |
| gpt-image-2.5-sunburst | standard + flexible | auto low medium high xhigh max | Editing precision |
| gpt-image-2.5-flare (default) | standard + flexible | auto low medium high xhigh max | Fast, high-quality everyday generation |
| gpt-image-2 | standard + flexible | auto low medium high | Up to 4K |
| gpt-image-1.5 | standard | auto low medium high | Shutdown 2026-12-01 |
| gpt-image-1 | standard | auto low medium high | Shutdown 2026-10-23 |
| gpt-image-1-mini | standard | auto low medium high | Shutdown 2026-12-01 |
Standard sizes: 1024x1024, 1536x1024, 1024x1536, auto.
Flexible sizes (gpt-image-2 and 2.5): any WIDTHxHEIGHT where both edges are multiples of 16, the aspect ratio is between 1:3 and 3:1, neither edge exceeds 3840 px, and total pixels fall between 655,360 and 8,294,400. Resolutions above 2560x1440 are documented as experimental. Examples: 1536x864, 2048x1152, 3840x2160.
Common parameters (all models): n 1–10 · background auto|transparent|opaque · output_format png|jpeg|webp · output_compression 0–100 (jpeg/webp only) · moderation auto|low · prompt up to 32,000 characters · up to 16 input images per edit.
input_fidelity (high|low) is accepted on edits by the gpt-image-1.x models only. gpt-image-2 and both 2.5 models reject it and always process inputs at high fidelity — the API reference documents this for gpt-image-2; the 2.5 behaviour was confirmed against the live API on 2026-09-20.
Dated snapshots — gpt-image-2.5-sunburst-2026-09-08, gpt-image-2.5-flare-2026-09-08, gpt-image-2-2026-04-21 — are accepted anywhere a model is, via --model <id> on the CLI or model: in code, and validate with their family's constraints.
Deprecated models still work until their shutdown dates. The API class logs a warning (once per model per instance) when one is used; the CLI prints the same notice. The dates are relayed, not enforced — after the date the notice changes tense and the request still goes out, coming back as the API's 404. Source: OpenAI deprecations.
Authentication Setup
1. Get Your API Key
- Visit https://platform.openai.com/
- Create an account or sign in
- Navigate to API keys section
- Generate your API key
- Copy your API key
2. Configure Your API Key
You can provide your API key in multiple ways (listed in priority order):
Option A: CLI Flag (Highest Priority)
openai-img --api-key YOUR_API_KEY --prompt "a cat"On a shared host the flag is visible to other users through the process list for the life of the run; the CLI prints a warning. Prefer Option B or D there.
Option B: Environment Variable
# Add to your ~/.bashrc, ~/.zshrc, or equivalent
export OPENAI_API_KEY=your_actual_api_key_here
# Or use it for a single command
OPENAI_API_KEY=your_key openai-img --prompt "a cat"Option C: Local .env File
# In your project directory
echo "OPENAI_API_KEY=your_actual_api_key_here" > .envOption D: Global Config
# Create config directory
mkdir -p ~/.openai
# Add your API key
echo "OPENAI_API_KEY=your_actual_api_key_here" > ~/.openai/.envSecurity Note: Never commit .env files or expose your API key publicly.
Library consumers: .env files are read only when a key is actually being looked up (no apiKey passed and OPENAI_API_KEY unset) — not at import time as in 2.x — and never when OPENAI_IMAGE_API_NO_DOTENV is set. A server that manages its own configuration can set that variable and the SDK will not touch the filesystem for credentials.
3. Organization Verification
OpenAI may require API Organization Verification before GPT Image models can be used. Without it you'll receive a 400-level error.
- Go to your OpenAI Organization Settings
- Complete the verification process (government ID + selfie)
- Wait for approval (typically a few business days)
Installation
Option 1: Install from npm (Recommended)
# Install globally for CLI usage
npm install -g openai-image-api
# Or install locally in your project
npm install openai-image-apiOption 2: Install from source
git clone https://github.com/aself101/openai-image-api.git
cd openai-image-api
npm install
npm run buildTypeScript Support
This package is written in TypeScript and includes full type definitions. All types are exported.
Importing Types
Every type is re-exported from the main entry; openai-image-api/types also works.
import {
OpenAIImageAPI,
type GenerateImageParams,
type EditImageParams,
type StreamImageParams,
type StreamEditImageParams,
type ImageResponse,
type ImageModel,
type ImageQuality,
type ImageGenerationStreamEvent,
type ImageEditStreamEvent,
type APIOptions,
} from 'openai-image-api';
// Constraints and helpers
import {
MODEL_CONSTRAINTS, // per-family sizes/quality/limits
MODEL_DEPRECATIONS, // shutdown dates for the 1.x models
MODEL_ALIASES, // dated snapshot → family
MODELS, // CLI short names → model ids
DEFAULT_MODEL, // 'gpt-image-2.5-flare'
validateModelParams, // the pre-flight validator the API class runs
validateFlexibleSize, // WIDTHxHEIGHT rules for gpt-image-2 / 2.5
getModelConstraints,
getModelDeprecation,
resolveModelFamily,
isSupportedModel,
getOpenAIApiKey, // CLI flag → env resolution used by the constructor
validateApiKeyFormat, // shape check only; does not call the API
getOutputDir, // OPENAI_OUTPUT_DIR or 'datasets/openai'
} from 'openai-image-api/config';
// File and stream helpers used by the CLI, exported for reuse
import {
decodeBase64Image, // write a b64 payload to disk (creates directories)
validateImagePath, // magic-byte check: PNG/JPEG/WebP/GIF
validateOutputPath, // reject '..' traversal, optionally pin to a base dir
generateTimestampedFilename,
sanitizeForFilename,
parseSSEStream, // raw SSE → { event, data } async generator
getErrorMessage, // message from an `unknown` catch value
} from 'openai-image-api/utils';ensureDirectory, writeToFile, readStreamToString, promptToFilename, createSpinner, setLogLevel, logger, and getErrorCode are also exported from ./utils — small internals the CLI uses; they carry JSDoc but no compatibility promise beyond the current major.
Project Structure
openai-image-api/
├── src/ # Published alongside dist/ so source maps resolve
│ ├── api.ts # OpenAIImageAPI class (buffered + streaming)
│ ├── config.ts # Model constraints, deprecations, validation
│ ├── utils.ts # File I/O, image header checks, SSE parser
│ ├── cli-core.ts # CLI logic (testable in-process)
│ ├── cli-options.ts # Option coercion helpers shared by both CLIs
│ ├── cost.ts # Cost assessment algorithm (pure)
│ ├── admin-api.ts # OpenAIAdminAPI: paginated usage / costs client
│ ├── cost-cli.ts # `openai-img cost` subcommand
│ ├── errors.ts # OpenAIImageAPIError
│ ├── cli.ts # CLI bin entry (argv / exit wiring)
│ └── types.ts # Type definitions
├── scripts/check-reference.mjs # Diff config against OpenAI's published reference
├── dist/ # Compiled JavaScript (committed for npm)
├── test/ # Vitest suites
└── tsconfig.jsonBuilding from Source
npm install
npm run build # tsc → dist/
npm run build:watchCLI Usage
Basic Command Structure
openai-img [model] [operation] --prompt "..." [options]Model Selection
The default is gpt-image-2.5-flare. Override with a shortcut flag or --model:
--flare # gpt-image-2.5-flare (default)
--sunburst # gpt-image-2.5-sunburst
--gpt-image-2 # gpt-image-2
--gpt-image-15 # gpt-image-1.5 (deprecated, shutdown 2026-12-01)
--gpt-image-1 # gpt-image-1 (deprecated, shutdown 2026-10-23)
--gpt-image-1-mini # gpt-image-1-mini (deprecated, shutdown 2026-12-01)
--model <id> # any supported id, including dated snapshots; wins over shortcutsOperation Mode
# Default: generate
openai-img --prompt "a cat"
# Edit one or more images (repeat --image up to 16 times)
openai-img --edit --image a.png --image b.png --prompt "combine into a collage"
# Stream, saving partial frames beside the final image
openai-img --stream --partial-images 2 --prompt "a cat"Options
| Option | Description |
| ---------------------------- | --------------------------------------------------------------- |
| --prompt <text> | Text prompt (repeat for batch generation) |
| --image <path> | Input image for --edit (repeatable) |
| --mask <path> | Mask image for --edit |
| --size <size> | WIDTHxHEIGHT or auto |
| --quality <q> | auto, low, medium, high; xhigh, max on 2.5 |
| --n <number> | Images per request, 1–10 |
| --background <bg> | auto, transparent, opaque |
| --output-format <f> | png, jpeg, webp |
| --output-compression <pct> | 0–100, jpeg/webp only |
| --moderation <level> | auto, low |
| --input-fidelity <level> | high, low — gpt-image-1.x edits only |
| --stream | Stream the response |
| --partial-images <n> | 0–3 partial frames (requires --stream) |
| --no-validate | Skip the client-side constraint check; accepts any --model id |
| --user <id> | End-user identifier |
| --api-key <key> | Override environment key |
| --output-dir <path> | Output directory (default datasets/openai/<model>) |
| --log-level <level> | DEBUG, INFO, WARNING, ERROR |
| --dry-run | Validate parameters without calling the API |
| --examples | Show usage examples |
--dry-run runs the full pre-flight a real request runs — API key present, prompt present, constraint table, and for edits every input file opened and header-checked — without sending anything. It reports the rejection this package would issue, not what the API would say. With --no-validate the constraint table is skipped but files are still verified. The same check is available in code as api.validateRequest(params).
API Methods
All methods live on OpenAIImageAPI.
new OpenAIImageAPI(options?)
const api = new OpenAIImageAPI({
apiKey: 'sk-...', // default: OPENAI_API_KEY
baseUrl: 'https://...', // default: https://api.openai.com (HTTPS enforced)
logLevel: 'WARNING', // DEBUG | INFO | WARNING | ERROR — default WARNING; 2.x defaulted to INFO
rateLimitDelay: 1000, // ms between requests (serialized across concurrent calls)
requestTimeout: 180000, // ms; image generation can take minutes at high quality
skipValidation: false, // true: send unknown models / out-of-table params, let the API judge
});
logLevelin 3.0.0 and earlier:'WARNING'— the documented value and, from 3.0.0, the default — silently muted every log line, errors included (winston has nowarninglevel). Fixed in 3.1.0: levels are mapped correctly, and an unrecognised level name now throws instead of muting. If you relied onWARNINGmeaning "quiet", it now means "warnings and errors", which is what it always said.OpenAIAdminAPItakes the same option.
validateRequest(params, { streaming? }): Promise<ImageModel>
Runs every check a request would run — key, prompt, constraint table (unless skipValidation), and for edits every input file opened, size- and header-checked — and rejects with the same OpenAIImageAPIError the request would, without sending anything. This is what --dry-run calls.
generateImage(params): Promise<ImageResponse>
const result = await api.generateImage({
prompt: 'a cat', // required, ≤ 32,000 chars
model: 'gpt-image-2.5-flare', // default
size: '1536x1024',
quality: 'high',
n: 1,
background: 'auto',
output_format: 'png',
output_compression: 80, // jpeg/webp only
moderation: 'auto',
user: 'user-123',
});
// result.data[i].b64_json, result.usage, result.output_format, result.quality, result.size, result.backgroundgenerateImageEdit(params): Promise<ImageResponse>
const result = await api.generateImageEdit({
image: ['a.png', 'b.png'], // string | string[], up to 16
prompt: 'combine these',
model: 'gpt-image-2.5-sunburst',
mask: 'mask.png', // optional
input_fidelity: 'high', // gpt-image-1.x only
// ...plus every generateImage option except prompt handling
});Images are sent as multipart image[] parts; the mask as mask. Every input file is opened once, checked for size (≤ 50 MB) and image magic bytes (PNG/JPEG/WebP), and uploaded from that same open handle — the bytes checked are the bytes sent, so a path swapped between check and upload is not picked up. Files are opened after the rate-limit wait, not held across it.
streamImage(params): AsyncGenerator<ImageGenerationStreamEvent>
Yields image_generation.partial_image events (0–partial_images of them) followed by one image_generation.completed event. See Streaming.
generateImageStream(params, handlers?): Promise<ImageResponse>
Wraps streamImage: calls handlers.onPartialImage(event) for each partial frame and resolves to the completed image in the same shape generateImage returns, so saveImages works on either.
streamImageEdit(params) / generateImageEditStream(params, handlers?)
Edit counterparts; events are image_edit.partial_image / image_edit.completed.
saveImages(response, outputDir, baseFilename, format?): Promise<string[]>
Decodes each b64_json entry to <outputDir>/<baseFilename>.<format> (numbered _1, _2… when n > 1). format defaults to response.output_format, then png.
Path inputs are checked before writing: outputDir may not contain a .. segment and baseFilename must be a single path component (no /, \, ..). A server forwarding end-user input here cannot be steered outside outputDir; to write elsewhere, validate your own path and call decodeBase64Image directly.
Streaming
Streaming uses stream: true on the same endpoints; the API answers with Server-Sent Events. This package parses them and exposes both an async generator and a callback wrapper.
import fs from 'fs';
import { OpenAIImageAPI } from 'openai-image-api';
const api = new OpenAIImageAPI();
for await (const event of api.streamImage({ prompt: 'a storm', partial_images: 3 })) {
if (event.type === 'image_generation.partial_image') {
fs.writeFileSync(`partial-${event.partial_image_index}.png`, Buffer.from(event.b64_json, 'base64'));
} else {
fs.writeFileSync('final.png', Buffer.from(event.b64_json, 'base64'));
console.log(event.usage);
}
}Things the API documents that are worth knowing before relying on partials:
- Each partial frame costs an additional 100 image output tokens.
- You may receive fewer partials than requested — "the final image may be sent before the full number of partial images are generated if the full image is generated more quickly." In testing,
lowquality at 1024x1024 frequently delivered zero partials;mediumdelivered one of two requested. Write consumers that treat partials as optional. - Partial and final payloads are large (a
medium1024x1024 PNG is ~3 MB of base64 per event); the parser buffers per event, not per line.
Examples
Example 1: Highest quality Sunburst render
openai-img --sunburst --prompt "photorealistic portrait of an astronaut" --size 1024x1536 --quality maxExample 2: 4K landscape with gpt-image-2
openai-img --gpt-image-2 --prompt "wide cinematic desert vista" --size 3840x2160 --quality highExample 3: Transparent background as WebP
openai-img --prompt "a cute robot character, sticker style" --background transparent --output-format webpExample 4: Streaming with partial frames
openai-img --stream --partial-images 2 --quality medium --prompt "a lighthouse in a storm"
# → ..._partial_0.png (if delivered), ..._lighthouse.png, ..._metadata.jsonExample 5: Multi-image edit
openai-img --sunburst --edit --image lotion.png --image soap.png --image candle.png \
--prompt "arrange these in a gift basket"Example 6: Batch generation
openai-img --prompt "a red apple" --prompt "a green pear" --prompt "a yellow banana"Example 7: Programmatic edit with streaming
import fs from 'fs';
import { OpenAIImageAPI } from 'openai-image-api';
const api = new OpenAIImageAPI({ logLevel: 'WARNING' });
const edited = await api.generateImageEditStream(
{
image: 'photo.png',
prompt: 'make it look like autumn',
model: 'gpt-image-2.5-sunburst',
partial_images: 1,
},
{
onPartialImage: async (e) => {
await fs.promises.writeFile(`preview-${e.partial_image_index}.png`, Buffer.from(e.b64_json, 'base64'));
},
}
);
const [path] = await api.saveImages(edited, './out', 'autumn');
console.log(path, edited.usage);Cost Assessment
openai-img cost reconciles your organization's image-model usage with its costs, per UTC day, per scope (project × API key) and per model. It needs an admin key (sk-admin-…, Organization → Admin keys) in OPENAI_ADMIN_KEY — a different credential from the project key the image commands use.
export OPENAI_ADMIN_KEY="sk-admin-..."
openai-img cost --start 7d # last seven UTC days through today
openai-img cost --start 2026-09-01 --end 2026-09-08 # end is exclusive; both snap to UTC midnight
openai-img cost --start 30d --project-id proj_abc --api-key-id key_123 # scope filters, repeatable
openai-img cost --start 7d --json --output costs.json # full report as JSON, also written to a file
openai-img cost --start 7d --admin-key sk-admin-... --log-level DEBUG # key on the command line (visible in ps), verboseOptions: --start <time> (required), --end <time> (default: end of the current UTC day), --project-id <id>, --api-key-id <id>, --json, --output <path>, --admin-key <key>, --log-level DEBUG|INFO|WARNING|ERROR (default INFO). Times are Unix seconds, ISO-8601, today, yesterday, or <N>d.
day scope / model requests out img tk image cost unclassified all-API avg/req attribution / components
2026-09-20 proj=proj_… key=key_… 11 26392 0.897799 usd - 0.897799 usd 0.081618 exact_scope_reconciliation
gpt-image-1 [family] 1 4160 0.166505 usd 0.166505 img-out 0.1664 txt-in 0.000105
gpt-image-2.5-flare 5 2183 0.06587 usd 0.013174 img-out 0.06549 txt-in 0.00038import { OpenAIAdminAPI } from 'openai-image-api';
const end_time = Math.ceil(Date.now() / 1000 / 86400) * 86400; // end of today, UTC
const start_time = end_time - 7 * 86400;
const admin = new OpenAIAdminAPI(); // reads OPENAI_ADMIN_KEY
const report = await admin.assessImageCosts({ start_time, end_time }, { project_ids: ['proj_abc'] });
for (const row of report.rows) {
for (const m of row.models) {
console.log(
row.period_start_iso.slice(0, 10),
row.project_id,
m.model,
m.requests,
m.cost.total,
m.tokens_reconcile
);
}
}
console.log(report.totals.by_model); // [{ family, currency, requests, output_image_tokens, image_cost }]Where the numbers come from. Three organization endpoints, one query each, every page fetched before anything is computed:
GET /v1/organization/usage/completionsgrouped by project, API key and model — GPT Image activity lives here (num_model_requests,input_image_tokens,output_image_tokens, …). Non-image models are dropped and counted.GET /v1/organization/usage/images— DALL-E-eraimage.generation/edit/variationactivity. Empty for an organization that only uses GPT Image; still read.GET /v1/organization/costsgrouped by project, API key and line item, with no line-item filter so unmatched spend stays visible.
How rows are classified and joined. A cost row is image spend when its quantity_unit is images, or its line_item parses as <model> <image|text>, <input|cached input|output> and the model is an image model (gpt-image-*, dall-e-*). That structured line item — gpt-image-2.5-flare image, output, quantity in tokens — is what OpenAI emits today (observed 2026-09-21), and it names the model, so model-level cost is reported: within a day and scope, cost rows join usage rows by model id (exactly, else by family across a -YYYY-MM-DD snapshot suffix, labelled [family]). Usage token counts are compared with the cost-side quantity component by component (tokens_reconcile). Everything the parser does not recognise is unclassified and stays visible; cost that merely lands on the same day as activity is never attributed to it.
What a row carries. attribution_level (exact_scope_reconciliation → image_line_item_reconciliation → shared_scope_estimate → unattributed), partial (the day runs past the requested end), classified_image_cost / unclassified_cost / total_cost (all API products at that scope), average_cost_per_request and average_cost_per_image (exact scope only), excluded_foreign_currency (rows in another currency, excluded atomically from every total), models[] with per-component cost and token counts, the raw line_items, an image_breakdown, warnings, and provenance back to the source pages. Amounts are parsed from their decimal text (the API sends 34 significant digits) into integers at 12 decimal places; currencies are never mixed.
Costs lag usage. The current UTC day shows activity within minutes and cost hours later; a model with usage and no cost is activity_only with a re-run notice, never $0. Both endpoints bucket at UTC midnight, so the CLI snaps --start down and --end up and prints the effective range.
Building blocks. From openai-image-api/cost: assessImageCosts(imagePages, completionPages, costPages, range) — the pure algorithm (this one, plus OpenAIAdminAPI and every cost type, is also on the main entry); normalizeCompletions / normalizeImages / normalizeCosts — page sets to typed rows (they refuse a page set with a gap); parseLineItem, classifyCostRow, modelFamily, isImageModel; toScaled / formatAmount / AMOUNT_SCALE — exact money. From openai-image-api/admin: OpenAIAdminAPI with listCompletionsUsage, listImagesUsage, listCosts (all pages, retried on 429/5xx with backoff, amounts kept as text), assessImageCosts, and the USAGE_LIMITS / COSTS_LIMITS bounds. Spec and the dated revision that records the live findings: docs/openai-image-cost-assessment-spec.md; endpoint references in docs/reference/.
Data Organization
Generated images and metadata are organized by model:
datasets/
└── openai/
├── gpt-image-2.5-flare/
│ ├── 2026-09-20_22-37-00_gpt-image-2.5-flare_a_lighthouse_partial_0.png
│ ├── 2026-09-20_22-37-00_gpt-image-2.5-flare_a_lighthouse.png
│ ├── 2026-09-20_22-37-00_gpt-image-2.5-flare_a_lighthouse_metadata.json
│ └── ...
├── gpt-image-2.5-sunburst/
│ └── 2026-09-20_22-38-19_gpt-image-2.5-sunburst-edit_make_the_fox_blue.png
└── gpt-image-2/
└── ...Partial frames, the final image, and the metadata sidecar for one request share a stem — YYYY-MM-DD_HH-MM-SS-mmm_<4 hex>_<model>_<prompt> — so they sort together and two processes rendering the same prompt in the same millisecond do not overwrite each other. Prompt text keeps letters and digits in any script (a Japanese prompt keeps its characters); everything else becomes _; Windows-reserved stems (con, nul, com1…) get a trailing _. saveImages() does not sanitize the baseFilename you pass beyond requiring it to be a single path component.
The metadata sidecar stores the prompt and any --user id in plaintext. Sharing or backing up an output directory shares the prompts.
Metadata Format:
{
"model": "gpt-image-2.5-flare",
"operation": "generate",
"timestamp": "2026-09-20T22:37:11.223Z",
"parameters": {
"model": "gpt-image-2.5-flare",
"size": "1024x1024",
"quality": "medium",
"partial_images": 2,
"prompt": "a detailed watercolor of a lighthouse in a storm"
},
"response": {
"created": 1789943831,
"images": ["datasets/openai/gpt-image-2.5-flare/..._a_lighthouse.png"],
"partial_images": ["datasets/openai/gpt-image-2.5-flare/..._a_lighthouse_partial_0.png"],
"usage": {
"input_tokens": 15,
"input_tokens_details": { "image_tokens": 0, "text_tokens": 15 },
"output_tokens": 516,
"output_tokens_details": { "image_tokens": 516, "text_tokens": 0 },
"total_tokens": 531
},
"output_format": "png",
"quality": "medium",
"size": "1024x1024",
"background": "opaque"
}
}Testing
npm test # run all tests
npm run test:watch
npm run test:ui
npm run test:coverage
npm run lint # eslint (type-aware) — also run in CI
npm run format:check # prettier
npm run verify # lint + format + type-check (src and tests) + build + test, what CI runsThe suite has 295 tests across seven files:
- config — model catalogue and deprecation table, flexible-size rules (multiples of 16, aspect ratio, pixel bounds), per-model quality gating,
input_fidelityrejection, cross-field rules (transparent+jpeg, compression without jpeg/webp), snapshot resolution. - api — request payloads per model family, default model, deprecation warning once per model, streaming (SSE reassembly across chunk boundaries, event ordering, callback wrapper, error-body recovery from a failed stream, terminal error events), edit pre-flight,
saveImages, security (HTTPS enforcement, key redaction, production error sanitisation, rate limiting). - utils — file I/O, filename generation, image magic-byte validation, path traversal, error-message extraction, SSE parser edge cases (CRLF, multi-line data, comments, trailing event, 200 kB payloads).
- cli-core — in-process tests of the CLI logic (
src/cli-core.ts): option parsing and enum checks, model resolution, cross-flag validation, job construction,runCliexit codes for dry runs and batch failures. - cost — exact decimal parsing (34-digit wire amounts,
0E-6176, half-up at the 13th place), line-item parsing and classification, completions/images/costs normalization, page-gap detection, the live 2026-09-20 fixture (5 requests / 2183 tokens ↔ $0.06549), token mismatch, family match across a snapshot suffix, one-sided models, component sums, unclassified spend, atomic foreign-currency exclusion, scope isolation, DALL-E-era images path, partial days, ordering, provenance; admin client pagination, shape check, retry/backoff (429/5xx retried, 400 not), auth errors, amount-text preservation. - cost-cli — time parsing, UTC-day range snapping, text rendering (model lines, family totals, partial marker, lag footer), routed failure paths,
--json/--output. - cli — subprocess smoke tests against the built
dist/cli.js:--dry-runvalidation failures exit non-zero with the validator's message,--modelrejects removed ids, invalid enum flags are refused before any request.
Network calls are mocked. Live verification of streaming, editing, and the input_fidelity behaviour was performed against the real API on 2026-09-20 and again from the published 3.0.0 tarball on 2026-09-21; openai-img cost was run live on 2026-09-21 against an organization with GPT Image spend (7 model-rows across four families, every one reconciling usage tokens to cost quantity). None of that is part of npm test.
Error Handling
Every failure thrown by OpenAIImageAPI methods is an OpenAIImageAPIError (exported from the main entry) — API responses, client-side rejections, input-file checks, and stream failures alike. The message is the stable human-readable vocabulary below; the fields let you branch without parsing it:
import { OpenAIImageAPI, OpenAIImageAPIError } from 'openai-image-api'; // also: openai-image-api/errors
// openai-image-api/errors also exports apiErrorBody(err): the parsed { message, code, type } of an OpenAI
// error response, or undefined — the helper the package uses to fill code/type/apiMessage below.
try {
await api.generateImage({ prompt });
} catch (err) {
if (err instanceof OpenAIImageAPIError) {
err.status; // HTTP status when the API answered; undefined for client-side rejections
err.code; // API error.code — the stable discriminator
err.type; // API error.type (e.g. 'image_generation_user_error': fix the input, do not retry unchanged)
// or the package's own: 'validation_error' | 'input_error' | 'configuration_error' | 'stream_error'
err.apiMessage; // the API's own message — deliberately exempt from NODE_ENV=production sanitization
// (it is addressed to the key holder and names the rejected parameter or policy, not
// internals); do not forward it to end users unreviewed
err.cause; // the original axios error (or the underlying fs error for input_error)
}
}The constructor throws an OpenAIImageAPIError with type: 'configuration_error' for a non-HTTPS baseUrl, so one instanceof covers construction too.
| Error | Meaning |
| --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| Authentication failed. Please check your API key. | 401 |
| Bad request: <API message> | 400 — the API's own message is passed through (sanitised to a generic string when NODE_ENV=production) |
| Rate limit exceeded. Please try again later. | 429 |
| OpenAI service error. Please try again later. | 500 / 502 / 503 |
| Parameter validation failed:\n - ... | Rejected client-side before any request; lists every failing rule (type: 'validation_error') |
| Streaming requests generate a single image; omit n or set it to 1 | This package's streaming wrappers return one image; n > 1 on a stream is refused |
| Image file not found: <path> / File does not appear to be a valid image | Edit input failed the pre-upload check |
| Unknown model "<id>". Supported: ... | Model id not in the catalogue (DALL-E ids land here) |
| Stream error: <message> | The API sent a terminal error event mid-stream (type: 'stream_error') |
| Stream ended without an image_generation.completed event | Connection closed early |
| Unexpected response shape from <endpoint> | 200 without a data[] array |
Example validation failure:
Error: Parameter validation failed:
- Size "1000x1000": width and height must both be multiples of 16
- Invalid quality "max" for gpt-image-2. Valid options: auto, low, medium, highTroubleshooting
API Key Not Found
Set OPENAI_API_KEY via one of the four methods in Authentication Setup.
does not support the 'input_fidelity' parameter
You're editing with gpt-image-2 or a 2.5 model. Drop --input-fidelity; these models always use high fidelity. The client-side validator catches this before the request when the model is known.
CI runner shows Bad request: Invalid request parameters
NODE_ENV=production sanitizes message. The CLI appends the API's own reason in parentheses (API: ...; code: ...) and the library keeps it on err.apiMessage.
The API accepts something this package rejects
The constraint tables date from 2026-09-20. Pass --no-validate / skipValidation: true and file an issue with the API's response so the table can be updated.
openai-img cost says Authentication failed
The usage endpoints take an admin key (sk-admin-…), not a project key. Create one under Organization → Admin keys and set OPENAI_ADMIN_KEY.
Requests time out
Default timeout is 180 s. max quality at large sizes can exceed that; raise requestTimeout in APIOptions.
Organization Not Verified
A 400 mentioning verification means your org must complete API Organization Verification.
Model returns 404
dall-e-2, dall-e-3 (since 2026-05-12) and, after their dates, gpt-image-1 (2026-10-23), gpt-image-1.5 / gpt-image-1-mini (2026-12-01) are removed from the API. Migrate to gpt-image-2 or a 2.5 model.
Migrating from 2.x
3.0.0 is a breaking release. Everything removed was removed because OpenAI shut down or scheduled shutdown of the underlying API:
| Removed | Why | Replacement |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------- | ---------------------------------------------------------- |
| dall-e-2, dall-e-3 models | Shut down 2026-05-12 | gpt-image-2.5-flare (new default) or any GPT Image model |
| generateImageVariation(), --variation | /v1/images/variations was DALL-E-2-only | Use an edit with a descriptive prompt |
| response_format, style, url in responses | DALL-E-only fields; GPT Image always returns base64 | data[i].b64_json |
| OpenAIVideoAPI, openai-image-api/video-api, --video, --sora-2, --sora-2-pro, --remix-video, --list-videos, --delete-video, --seconds, --input-image, --variant | Videos API and Sora 2 shut down 2026-09-24 | — |
| --dalle-2, --dalle-3 flags | as above | --flare, --sunburst, --gpt-image-2, --model <id> |
| 30 s request timeout | Too short for image generation | 180 s default, requestTimeout option |
Other behaviour changes:
- Default model is
gpt-image-2.5-flare, notdall-e-2. Cost note:dall-e-2had been returning 404 since 2026-05-12, so no working 2.x default is being upgraded — but a pipeline that pinnedopenai-image-api@2and switches to 3 with no--qualityflag now pays GPT Image 2.5auto-quality token rates (see pricing). Set--quality lowfor drafts. gpt-image-1.5now actually works. In 2.1.x its options were gated onmodel === 'gpt-image-1', so 1.5 requests carriedresponse_format(a 400) and silently droppedbackground/output_format/moderation/input_fidelity. All GPT Image models now share one code path.- Streaming exists. The 2.1.0 changelog announced partial-image streaming; only a constraints entry shipped.
streamImage,generateImageStream,streamImageEdit,generateImageEditStream, and--streamare new in 3.0.0. --dry-runvalidates. Previously it printed parameters and declared them valid without checking.saveImagesformatis optional and defaults to the response'soutput_format.- Output directories are named by the full model id (
datasets/openai/gpt-image-2.5-flare/), not a shortened alias. - New client-side rules:
background: transparentwithoutput_format: jpegandoutput_compressionwithout jpeg/webp are rejected before the request; edit inputs are magic-byte checked before upload. - Batch exit code. A
--promptbatch with any failed prompt now exits 1 and lists the failures; 2.x printed the success banner and exited 0 even when every prompt failed. - Rate limiting is serialized across concurrent calls on one instance; 2.x spaced only sequential callers.
- Errors are
OpenAIImageAPIErrorinstances withstatus/code/type/apiMessage/cause; messages are unchanged. - Library log level defaults to
WARNING(wasINFO):new OpenAIImageAPI()no longer writes a progress line to stdout on every request. PasslogLevel: 'INFO'to restore. The CLIs are unchanged (--log-level, default INFO). Note that in 3.0.0 itselfWARNINGmuted everything — see thelogLevelnote under API Methods. - Bounded buffers. Buffered responses are capped at 256 MiB (
maxContentLength/maxBodyLength) and a single SSE event at 128 MiB; both are far above any real image and exist so a hostile or broken upstream cannot exhaust memory. - Removed utilities:
validateImageUrl,downloadImage,imageToBase64,validateImageFile,pause(openai-image-api/utils). The first three served DALL-E URL responses; the package no longer fetches anything but the API itself.RequestOptionsandImageFileConstraintstypes are gone with them.
Releasing
Releases are published by hand; CI (.github/workflows/ci.yml) gates every push and pull request but never publishes.
npm run verify # lint, format, type-check src + tests, build, test
npm run check:reference # config still matches OpenAI's published reference
# edit CHANGELOG.md: move [Unreleased] under a new version heading with today's date
npm version <major|minor|patch> -m "chore(release): %s" # bumps package.json, commits, tags v<version>
git push origin main --follow-tags # wait for CI to go green on the release commit
npm publish # prompts for the npm one-time password
gh release create v<version> --notes-from-tag # optionalprepublishOnly runs npm run check:release and then rebuilds dist/. The check refuses to publish when the CHANGELOG has no heading for package.json's version, when [Unreleased] still holds entries, when the committed dist/ differs from a fresh build, or when npm pack would ship anything besides dist/, src/, README.md, LICENSE and package.json. npm run check:release -- --control proves it can fail. Publish after CI is green on the pushed release commit, not before: a tag that fails CI after the tarball is on npm cannot be un-published cleanly.
Additional Resources
Related Packages
This package is part of the img-gen ecosystem. Check out these other AI generation services:
ideogram-api- Ideogram API wrapper for image generation, editing, remixing, and manipulationbfl-api- Black Forest Labs API wrapper for FLUX and Kontext modelsstability-ai-api- Stability AI API wrapper for Stable Diffusion 3.5 and image upscalinggoogle-genai-api- Google Generative AI (Imagen) wrapper
Disclaimer: This project is an independent community wrapper and is not affiliated with OpenAI.
License
This project is licensed under the MIT License - see the LICENSE file for details.
