The free models endpoint lists what your key can use. Run npm 2,036 install openai (7.23.0 here, on Node.js 25 2,131 ) and export OPENAI_API_KEY first.
import OpenAI from 'openai';
const openai = new OpenAI(); // reads OPENAI_API_KEY from the environment
const { data } = await openai.models.list(); // free: no tokens are billed
const images = data.filter(m => /image|dall-e/.test(m.id))
.sort((a, b) => b.created - a.created)
.map(m => `${m.id} (${new Date(m.created * 1000).toISOString().slice(0, 10)})`.padEnd(48));
for (let i = 0; i < images.length; i += 2) {
console.log(images.slice(i, i + 2).join('').trimEnd());
}
try { // DALL-E 3 was shut down on 12 May 2026
await openai.images.generate({ model: 'dall-e-3', prompt: 'A bookshop cat' });
} catch (err) { // failed requests are not billed
console.log(`dall-e-3: ${err.message}`);
}Output
gpt-image-2.5-sunburst-2026-09-08 (2026-09-08) gpt-image-2.5-flare-2026-09-08 (2026-09-08) gpt-image-2.5-sunburst (2026-09-04) gpt-image-2.5-flare (2026-09-04) gpt-image-2-2026-04-21 (2026-04-17) gpt-image-2 (2026-04-17) chatgpt-image-latest (2025-12-16) gpt-image-1.5 (2025-11-25) gpt-image-1-mini (2025-09-26) gpt-image-1 (2025-04-24) dall-e-3: 400 The model 'dall-e-3' does not exist.
Each alias points at a dated snapshot; pin the snapshot in production. OpenAI 86 recommends Sunburst "for workflows where editing precision matters most" and Flare "for fast, high-quality everyday image generation." Both add xhigh and max to the low, medium and high quality levels and share gpt-image-2 86 's prices; gpt-image-2 (April 2026) introduced free-form sizes. The older models still work but shut down on 23 October 2026 (gpt-image-1) and 1 December 2026 (gpt-image-1-mini, gpt-image-1.5 and chatgpt-image-latest).
<!doctype html>
<style>
body { margin: 0; padding: 8px; background: #fafaf7; font: 12px system-ui, sans-serif; color: #263238; }
svg { width: 100%; max-width: 600px; display: block; }
</style>
<script src="https://cdn.jsdelivr.net/npm/d3@7.9.0/dist/d3.min.js"></script>
<svg viewBox="0 0 600 300" font-size="11"></svg>
<p id="out">Hover a model. Pin a dated snapshot in production; aliases move.</p>
<script>
// start, end (shutdown) or null if current; note shown on hover
const MODELS = [
['dall-e-2', '2022-04-06', '2026-05-12', 'shut down 12 May 2026'],
['dall-e-3', '2023-10-03', '2026-05-12', 'shut down 12 May 2026'],
['gpt-image-1', '2025-04-23', '2026-10-23', 'shuts down 23 October 2026'],
['gpt-image-1-mini', '2025-10-06', '2026-12-01', 'shuts down 1 December 2026'],
['gpt-image-1.5', '2025-12-16', '2026-12-01', 'shuts down 1 December 2026'],
['gpt-image-2', '2026-04-15', null, 'free-form sizes; same prices as 2.5'],
['gpt-image-2.5-sunburst', '2026-09-08', null, '"where editing precision matters most"'],
['gpt-image-2.5-flare', '2026-09-08', null, '"fast, high-quality everyday image generation"'],
].map(([id, start, end, note]) => ({ id, start: new Date(start), end: end && new Date(end), note }));
const today = new Date('2026-09-29');
const x = d3.scaleUtc([new Date('2022-01-01'), new Date('2027-03-01')], [150, 590]);
const y = d3.scaleBand(MODELS.map(m => m.id), [10, 260]).padding(0.3);
const svg = d3.select('svg');
svg.append('g').attr('transform', 'translate(0,262)').call(d3.axisBottom(x).ticks(d3.utcYear.every(1)));
svg.append('line').attr('x1', x(today)).attr('x2', x(today)).attr('y1', 5).attr('y2', 262)
.attr('stroke', '#263238').attr('stroke-dasharray', '3 3');
svg.append('text').attr('x', x(today) + 3).attr('y', 290).text('today');
const row = svg.selectAll('g.m').data(MODELS).join('g').attr('class', 'm')
.attr('transform', m => `translate(0,${y(m.id)})`)
.on('pointerenter', (e, m) => d3.select('#out').text(`${m.id}: released ${d3.utcFormat('%d %b %Y')(m.start)}; ${m.note}`));
row.append('text').attr('x', 144).attr('y', y.bandwidth() / 2).attr('dy', '0.35em').attr('text-anchor', 'end').text(m => m.id);
// Solid: available; hatched: still callable but scheduled for shutdown; gray: gone
row.append('rect').attr('x', m => x(m.start)).attr('height', y.bandwidth()).attr('rx', 3)
.attr('width', m => x(m.end && m.end < today ? m.end : today) - x(m.start))
.attr('fill', m => m.end && m.end < today ? '#b0bec5' : m.end ? '#e09a10' : '#3f7d3a');
row.filter(m => m.end && m.end > today).append('rect').attr('x', x(today)).attr('height', y.bandwidth())
.attr('width', m => x(m.end) - x(today)).attr('fill', 'none').attr('stroke', '#e09a10').attr('stroke-dasharray', '3 2');
row.filter(m => !m.end).append('path').attr('d', `M${x(today)},0l12,${y.bandwidth() / 2}l-12,${y.bandwidth() / 2}z`).attr('fill', '#3f7d3a');
row.filter(m => m.end).append('text').attr('x', m => x(m.end) + 4).attr('y', y.bandwidth() / 2).attr('dy', '0.35em')
.attr('fill', '#78909c').text(m => d3.utcFormat('%b %Y')(m.end));
</script>