Sunburst, Flare and gpt-image-2 86 share one price list per million tokens: text input $5, image input $8 and image output $30 (cached inputs $1.25 and $2 in the Responses API; the Batch API halves all rates). The older gpt-image-1 charges $40 per million output tokens. OpenAI 86 's cost calculator derives output tokens from a patch grid per quality level, and its formula ports to a few lines:
export const LEDGER = process.env.IMAGE_LEDGER ?? 'image-api-ledger.md';
export const CAP = Number(process.env.IMAGE_CAP ?? 5); // US dollars
const RATE = { text: 5, image: 8, out: 30 }; // $ per 1M tokens
const PATCHES = { low: 16, medium: 24, high: 48, xhigh: 64, max: 96 }; // gpt-image-2.5
export function outputTokens(size, quality, partials = 0) { // OpenAI's calculator formula
const [w, h] = size.split('x').map(Number);
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) + 100 * partials;
}
export const dollars = ({ textIn = 0, imageIn = 0, out = 0 }) =>It matched every non-streamed call in this chapter exactly (120, 196, 215 and 229 tokens):
| Size | low | medium | high | max |
|---|---|---|---|---|
| 1024x1024 | 196, $0.006 | 439, $0.013 | 1756, $0.053 | 7024, $0.211 |
| 1536x1024 | 158, $0.005 | 343, $0.010 | 1372, $0.041 | 5488, $0.165 |
| 3840x2160 | 371, $0.011 | 865, $0.026 | 3336, $0.100 | 13342, $0.400 |
Quality multiplies cost 36 times from low to max, while a 4K image at low costs about a cent. Non-square sizes get fewer patches on the short side, so 1536x1024 undercuts the square.
<!doctype html>
<style>
body { margin: 0; padding: 8px; background: #fafaf7; font: 11px 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>
<p>Show <label><input type="radio" name="m" value="usd" checked> dollars</label> <label><input type="radio" name="m" value="tok"> tokens</label>
· output rate <select id="rate"><option value="30">$30/M (gpt-image-2, 2.5)</option><option value="40">$40/M (gpt-image-1)</option><option value="15">$15/M (Batch API)</option></select></p>
<svg viewBox="0 0 600 250" font-size="11"></svg>
<script>
const PATCHES = { low: 16, medium: 24, high: 48, xhigh: 64, max: 96 };
function outputTokens(size, quality) { // OpenAI's calculator formula
const [w, h] = size.split('x').map(Number);
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 sizes = ['512x1536', '1024x1024', '1536x1024', '1024x1536', '2048x2048', '3840x2160'];
const qualities = Object.keys(PATCHES);
const x = d3.scaleBand(qualities, [90, 590]).padding(0.05), y = d3.scaleBand(sizes, [20, 240]).padding(0.05);
const svg = d3.select('svg');
svg.selectAll('text.q').data(qualities).join('text').attr('class', 'q').attr('x', q => x(q) + x.bandwidth() / 2)
.attr('y', 12).attr('text-anchor', 'middle').attr('font-weight', 'bold').text(q => q);
svg.selectAll('text.s').data(sizes).join('text').attr('class', 's').attr('x', 84).attr('text-anchor', 'end')
.attr('y', s => y(s) + y.bandwidth() / 2).attr('dy', '0.35em').text(s => s);
const cells = sizes.flatMap(s => qualities.map(q => ({ s, q, tokens: outputTokens(s, q) })));
const color = d3.scaleSequentialLog([50, 20000], d3.interpolateYlOrRd);
const cell = svg.selectAll('g.c').data(cells).join('g').attr('class', 'c').attr('transform', d => `translate(${x(d.q)},${y(d.s)})`);
cell.append('rect').attr('width', x.bandwidth()).attr('height', y.bandwidth()).attr('fill', d => color(d.tokens));
const label = cell.append('text').attr('x', x.bandwidth() / 2).attr('y', y.bandwidth() / 2).attr('dy', '0.35em')
.attr('text-anchor', 'middle').attr('fill', d => d.tokens > 3000 ? '#fff' : '#263238');
function update() {
const m = document.querySelector('input[name=m]:checked').value, rate = +document.getElementById('rate').value;
label.text(d => m === 'tok' ? d.tokens.toLocaleString() : `$${(d.tokens * rate / 1e6).toFixed(3)}`);
}
document.querySelectorAll('input, select').forEach(e => e.addEventListener('input', update));
update();
</script>