Node.js Generation

Generating an Image from Node.js

Keep the key in a .env file that Git 1,932 ignores and load it with Node.js 2,131 's --env-file. .withResponse() also returns the request ID that OpenAI 86 's support asks for.

bookmark.mjs: a pinned snapshot, a custom size and error handlingJavaScript
// bookmark.mjs: run with  node --env-file=.env bookmark.mjs
import fs from 'node:fs';
import OpenAI from 'openai';
const openai = new OpenAI({ timeout: 120_000, maxRetries: 2 });   // 2 retries is the default
try {
  const { data: res, request_id } = await openai.images.generate({
    model: 'gpt-image-2.5-flare-2026-09-08',          // a pinned snapshot
    prompt: 'A tall bookmark: a small bird on a stack of books under a crescent moon, '
      + 'flat vector illustration, deep purple and saffron, no text',
    size: '512x1536',                                  // 1:3, the tallest shape allowed
    quality: 'low',
    output_format: 'webp',
  }).withResponse();
  fs.writeFileSync('bookmark.webp', Buffer.from(res.data[0].b64_json, 'base64'));
  console.log(res.size, res.output_format, `${res.usage.output_tokens} tokens`, request_id);
} catch (err) {
  if (!(err instanceof OpenAI.APIError)) throw err;
  console.error(`${err.status} ${err.code}: ${err.message} (request ${err.requestID})`);
  process.exitCode = 1;
}
Output
512x1536 webp 56 tokens req_ad27ac7206c643fd80e6cbf67fff0835

The 512x1536 bookmark cost 56 output tokens, $0.0019. With size: '500x1500' the same script printed 400 invalid_value: 400 Invalid size '500x1500'. Width and height must both be divisible by 16. and was not billed. Subclasses such as RateLimitError (429) let you retry only what is worth retrying.

The bookmark call from Node.js, simulated: a 512×1536 result, a rejected 500×1500 size and the retry policyHTMLLive
<!doctype html>
<style>
  body { margin: 0; padding: 8px; background: #fafaf7; font: 12px system-ui, sans-serif; color: #263238; }
  .row { display: flex; gap: 12px; align-items: flex-start; }
  pre { margin: 0; font-size: 11px; background: #1d2327; color: #d7f0dd; padding: 8px; width: 390px; height: 250px; overflow: auto; white-space: pre-wrap; }
  canvas { display: block; }
</style>
<p>size <select id="size"><option>512x1536</option><option>500x1500</option><option>1024x1024</option></select>
  simulate <select id="fault"><option value="">no fault</option><option value="429">429 rate limit</option></select> <button id="run">node --env-file=.env bookmark.mjs</button></p>
<div class="row"><canvas id="c" width="100" height="300"></canvas><pre id="term"></pre></div>
<script>
  const term = document.getElementById('term'), ctx = document.getElementById('c').getContext('2d');
  const say = line => { term.textContent += line + '\n'; };
  // The bookmark the call returns: a small bird on a stack of books under a crescent moon
  function bookmark(w, h) {
    const k = 100 / w;  ctx.clearRect(0, 0, 100, 300);
    const s = ctx.createLinearGradient(0, 0, 0, h * k);
    s.addColorStop(0, '#2d1f4f');  s.addColorStop(1, '#5b3f99');
    ctx.fillStyle = s;  ctx.fillRect(0, 0, w * k, h * k);
    ctx.fillStyle = '#e09a10';  ctx.beginPath();  ctx.arc(62, 50, 16, 0, 7);  ctx.fill();
    ctx.fillStyle = '#2d1f4f';  ctx.beginPath();  ctx.arc(70, 44, 15, 0, 7);  ctx.fill();         // crescent
    ['#e09a10', '#fff3e0', '#b5452f', '#e09a10'].forEach((c, i) => { ctx.fillStyle = c;  ctx.fillRect(22 + (i % 2) * 6, 220 - i * 16, 56, 14); });
    ctx.fillStyle = '#e09a10';  ctx.beginPath();  ctx.ellipse(52, 146, 14, 10, 0, 0, 7);  ctx.fill();   // bird
    ctx.beginPath();  ctx.arc(62, 136, 7, 0, 7);  ctx.fill();
  }
  const sleep = ms => new Promise(r => setTimeout(r, ms));
  async function run() {
    term.textContent = '';
    const size = document.getElementById('size').value, fault = document.getElementById('fault').value;
    const [w, h] = size.split('x').map(Number);
    say(`$ node --env-file=.env bookmark.mjs   (size: '${size}')`);
    if (w % 16 || h % 16) {                                  // validated by the API before any work
      await sleep(300);
      say(`400 invalid_value: 400 Invalid size '${size}'. Width and height must both be divisible by 16.`);
      say('(request req_…; not billed)');  ctx.clearRect(0, 0, 100, 300);  return;
    }
    // maxRetries: 2 → up to three attempts, with backoff, only for retryable errors
    for (let attempt = 1; attempt <= 3; attempt++) {
      say(`attempt ${attempt}: POST /v1/images/generations`);
      await sleep(400);
      if (fault && attempt < 3) { say(`  429 RateLimitError, retrying in ${0.5 * 2 ** (attempt - 1)} s`);  await sleep(300);  continue; }
      break;
    }
    await sleep(500);
    bookmark(w, h);
    const tokens = size === '512x1536' ? 56 : 196;
    say(`${size} webp ${tokens} tokens req_7f3c…  ($${(tokens * 30 / 1e6 + 0.0002).toFixed(4)})`);
    say('wrote bookmark.webp');
  }
  document.getElementById('run').onclick = run;
  run();
</script>