size takes 1024x1024, 1536x1024, 1024x1536, auto, or any WIDTHxHEIGHT with sides that are multiples of 16, an aspect ratio from 1:3 to 3:1, no side over 3,840 pixels and 655,360 to 8,294,400 pixels in all. quality runs low, medium, high, xhigh and max, and defaults to auto.
// params.mjs: four parameter sets through the Images API (Section 8.3)
import fs from 'node:fs';
import OpenAI from 'openai';
const openai = new OpenAI();
const scene = 'A reading nook in a small bookshop: armchair, lamp, shelves, '
+ 'flat vector illustration in deep blue and saffron';
const icon = 'A flat icon of an open book with a small bird nesting on it, deep blue';
const runs = {
auto: { prompt: scene, size: 'auto', quality: 'auto' },
wide: { prompt: scene, size: '1536x864', quality: 'low' },
clear: { prompt: icon, quality: 'low', background: 'transparent', output_format: 'webp' },
jpeg: { prompt: icon, quality: 'low', n: 2, output_format: 'jpeg', output_compression: 60 },
};
for (const [name, params] of Object.entries(runs)) {
const t0 = performance.now();
const res = await openai.images.generate({ model: 'gpt-image-2.5-flare', ...params });
const secs = ((performance.now() - t0) / 1000).toFixed(1);
const kb = res.data.map((d, i) => {
const bytes = Buffer.from(d.b64_json, 'base64');
fs.writeFileSync(`${name}-${i}.${res.output_format}`, bytes);
return `${Math.round(bytes.length / 1024)} KB`;
});
console.log(name.padEnd(5), res.size, res.quality, res.background, res.output_format,
kb.join(' + '), `${res.usage.output_tokens} tokens ${secs} s`);
}Output
auto 1214x1295 low opaque png 1519 KB 215 tokens 16.3 s wide 1536x864 low opaque png 1375 KB 120 tokens 12.3 s clear 1254x1254 low transparent webp 351 KB 229 tokens 10.6 s jpeg 1254x1254 low opaque jpeg 76 KB + 73 KB 458 tokens 9.5 s

Left to auto, the model picked low quality and a 1214x1295 portrait, a size you could not request yourself; omitting size gave the icons 1254x1254. Name both values in production so cost and layout stay predictable.
<!doctype html>
<style>
body { margin: 0; padding: 8px; background: #fafaf7; font: 12px system-ui, sans-serif; color: #263238; max-width: 600px; }
input[type=number] { width: 70px; }
.row { display: flex; gap: 12px; align-items: flex-start; }
ul { list-style: none; padding: 0; margin: 4px 0; }
li.ok::before { content: '✔ '; color: #3f7d3a; } li.bad::before { content: '✘ '; color: #b5452f; }
table { border-collapse: collapse; background: #fff; margin-top: 6px; }
td, th { border: 1px solid #ddd; padding: 2px 8px; text-align: right; }
</style>
<p>size: <input type="number" id="w" value="1536" step="16"> × <input type="number" id="h" value="864" step="16">
presets: <button data-s="1024x1024">1024²</button> <button data-s="1536x1024">1536×1024</button>
<button data-s="512x1536">512×1536</button> <button data-s="500x1500">500×1500</button> <button data-s="3840x2160">4K</button></p>
<div class="row"><canvas id="shape" width="200" height="200"></canvas><div><ul id="rules"></ul><div id="cost"></div></div></div>
<script>
// OpenAI's calculator formula for gpt-image-2.5 output tokens
const PATCHES = { low: 16, medium: 24, high: 48, xhigh: 64, max: 96 };
function outputTokens(w, h, quality) {
const p = PATCHES[quality], s = p / (Math.max(w, h) / Math.min(w, h));
const f = Math.floor(s), q = s - f === 0.5 ? f + (f % 2) : Math.round(s);
return Math.ceil(p * q * (2e6 + w * h) / 4e6);
}
const $ = id => document.getElementById(id);
function update() {
const w = +$('w').value, h = +$('h').value, ratio = Math.max(w, h) / Math.min(w, h);
const rules = [
[w % 16 === 0 && h % 16 === 0, 'both sides are multiples of 16'],
[ratio <= 3, `aspect ratio ${ratio.toFixed(2)}:1 is within 1:3 to 3:1`],
[Math.max(w, h) <= 3840, 'no side over 3,840 pixels'],
[w * h >= 655360 && w * h <= 8294400, `${(w * h).toLocaleString()} pixels, within 655,360 to 8,294,400`],
];
$('rules').innerHTML = rules.map(([ok, text]) => `<li class="${ok ? 'ok' : 'bad'}">${text}</li>`).join('');
const valid = rules.every(r => r[0]);
// The shape to scale inside a 3840-pixel frame
const g = $('shape').getContext('2d'), k = 190 / 3840;
g.clearRect(0, 0, 200, 200);
g.strokeStyle = '#ccc'; g.strokeRect(5, 5, 190, 190);
g.fillStyle = valid ? '#1f5f8b' : '#b5452f'; g.globalAlpha = 0.8;
g.fillRect(5, 5, Math.min(190, w * k), Math.min(190, h * k)); g.globalAlpha = 1;
g.fillStyle = '#263238'; g.fillText(`${w} × ${h}`, 10, 190);
$('cost').innerHTML = valid
? '<table><tr><th>quality</th><th>tokens</th><th>$ at $30/M</th></tr>' + Object.keys(PATCHES).map(q => {
const t = outputTokens(w, h, q);
return `<tr><td>${q}</td><td>${t.toLocaleString()}</td><td>$${(t * 30 / 1e6).toFixed(4)}</td></tr>`;
}).join('') + '</table><p>auto may pick any size and quality: name both to keep cost predictable.</p>'
: `<p style="color:#b5452f">400 invalid_value: Invalid size '${w}x${h}'. Not billed.</p>`;
}
document.querySelectorAll('input').forEach(i => i.addEventListener('input', update));
document.querySelectorAll('[data-s]').forEach(b => b.onclick = () => {
[$('w').value, $('h').value] = b.dataset.s.split('x'); update();
});
update();
</script>