Compression and Moderation

Compression, Multiple Outputs and Moderation Strictness

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).

output_compression measured live with toBlob, and what n candidates add to the billHTMLLive
<!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>