output_compression (0-100, default 100) applies to JPEG and WebP. At 60, each JPEG icon was 75 KB, against 351 KB for the WebP and 1.4-1.5 MB for the PNG scenes, and OpenAI 86 notes that JPEG is also faster to produce. n (1-10) returns several candidates, each billed as an image: the n: 2 run paid 458 output tokens and its prompt twice.
moderation is auto (standard filtering) or low (less restrictive). A blocked request fails with error.code 'moderation_blocked' and a moderation_details.moderation_stage of input or output; OpenAI's guide says to change the prompt rather than retry automatically (Content Policy).
<!doctype html>
<style>
body { margin: 0; padding: 8px; background: #fafaf7; font: 12px system-ui, sans-serif; color: #263238; max-width: 600px; }
.row { display: flex; gap: 10px; align-items: flex-start; }
canvas { display: block; }
</style>
<p>output_compression <input type="range" id="q" min="0" max="100" value="60"> <b id="qv"></b> · n <input type="range" id="n" min="1" max="10" value="2"> <b id="nv"></b></p>
<div class="row"><canvas id="img" width="200" height="200"></canvas><canvas id="chart" width="380" height="200"></canvas></div>
<p id="note"></p>
<script>
// An illustration to encode: gradients and detail give the encoders real work
const img = document.getElementById('img'), g = img.getContext('2d');
const sky = g.createLinearGradient(0, 0, 0, 200);
sky.addColorStop(0, '#1f5f8b'); sky.addColorStop(0.7, '#e09a10'); sky.addColorStop(1, '#16435f');
g.fillStyle = sky; g.fillRect(0, 0, 200, 200);
for (let i = 0; i < 60; i++) {
g.fillStyle = `hsl(${200 + i * 3} 60% ${30 + (i % 7) * 6}%)`;
g.fillRect(i * 3.4, 150 + Math.sin(i) * 6, 3, 50);
}
g.fillStyle = '#fff3e0'; g.fillRect(140, 60, 16, 90); g.fillStyle = '#b5452f'; g.fillRect(140, 80, 16, 10);
const chart = document.getElementById('chart').getContext('2d');
const encode = (type, q) => new Promise(done => img.toBlob(done, type, q));
async function update() {
const q = +document.getElementById('q').value, n = +document.getElementById('n').value;
document.getElementById('qv').textContent = q; document.getElementById('nv').textContent = n;
// output_compression applies to JPEG and WebP only; PNG is lossless
const sizes = [['png', await encode('image/png')], ['webp', await encode('image/webp', q / 100)], ['jpeg', await encode('image/jpeg', q / 100)]];
const max = Math.max(...sizes.map(([, b]) => b.size));
chart.clearRect(0, 0, 380, 200);
chart.font = '12px system-ui'; chart.fillStyle = '#263238';
chart.fillText('this 200×200 image, encoded in the browser', 0, 14);
sizes.forEach(([name, blob], i) => {
const w = blob.size / max * 230;
chart.fillStyle = ['#b5452f', '#5b3f99', '#3f7d3a'][i]; chart.fillRect(60, 28 + i * 34, w, 24);
chart.fillStyle = '#263238'; chart.fillText(name, 0, 44 + i * 34);
chart.fillText(`${(blob.size / 1024).toFixed(1)} KB`, 66 + w, 44 + i * 34);
});
// n candidates are each billed as an image, and the prompt is paid n times
const out = 229 * n, cost = (out * 30 + 33 * n * 5) / 1e6;
chart.fillText(`n = ${n}: ${out} output tokens (229 each), about $${cost.toFixed(4)}`, 0, 150);
for (let i = 0; i < n; i++) { chart.fillStyle = '#e09a10'; chart.fillRect(i * 36, 162, 30, 30); }
document.getElementById('note').textContent = "moderation: 'auto' (standard) or 'low' (less restrictive); a blocked request " +
"fails with code 'moderation_blocked': change the prompt instead of retrying.";
}
document.querySelectorAll('input').forEach(i => i.addEventListener('input', update));
update();
</script>