Quality vs Cost

Trade-offs Between Quality, Size and Cost

The chapter's real calls (Budget Logging's ledger) show where money and seconds go. Price tracks tokens, not pixels: the 1536x864 banner cost $0.0038 and the 1024x1024 square $0.0060. Latency is nearly flat, 9.5 to 18 seconds for almost every call at low or medium quality, so stream or queue requests instead of blocking a page. Input images are not free: the mask edit paid 1,024 image tokens ($0.0082) before drawing anything, more than its 196 output tokens cost. Medium quality with two previews came to $0.0179. So draft at low, pick a winner, and pay for quality only where the result will be seen large.

What the chapter's real calls cost, split into input and output tokens, with latency nearly flatHTMLLive
<!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>
<svg viewBox="0 0 600 300" font-size="11"></svg>
<p>Price tracks tokens, not pixels; every call took about 9.5 to 18 seconds. Draft at low, pay for quality only where it shows.</p>
<script>
  // Calls from the chapter's ledger: input cost (text + image in) and output cost, US$
  const calls = [
    { what: 'bookmark 512×1536, low', inp: 0.0002, out: 0.0017 },
    { what: 'banner 1536×864, low', inp: 0.0002, out: 0.0036 },
    { what: 'gift card 1536×1024, low', inp: 0.0003, out: 0.0047 },
    { what: 'window 1024², low', inp: 0.0002, out: 0.0059 },
    { what: 'mask edit, low', inp: 0.0083, out: 0.0059 },
    { what: 'winter edit, low', inp: 0.0125, out: 0.0047 },
    { what: 'medium + 2 partials', inp: 0.0002, out: 0.0177 },
    { what: 'compose 2 images, low', inp: 0.0167, out: 0.0059 },
  ];
  const svg = d3.select('svg');
  const y = d3.scaleBand(calls.map(c => c.what), [10, 270]).padding(0.25);
  const x = d3.scaleLinear([0, 0.025], [150, 560]);
  svg.append('g').attr('transform', 'translate(0,270)').call(d3.axisBottom(x).ticks(5, '$.3f'));
  const row = svg.selectAll('g.c').data(calls).join('g').attr('class', 'c').attr('transform', c => `translate(0,${y(c.what)})`);
  row.append('text').attr('x', 144).attr('y', y.bandwidth() / 2).attr('dy', '0.35em').attr('text-anchor', 'end').text(c => c.what);
  row.append('rect').attr('x', x(0)).attr('height', y.bandwidth()).attr('fill', '#90a4ae').attr('width', c => x(c.inp) - x(0));
  row.append('rect').attr('x', c => x(c.inp)).attr('height', y.bandwidth()).attr('fill', '#1f5f8b').attr('width', c => x(c.out) - x(0));
  row.append('text').attr('x', c => x(c.inp + c.out) + 4).attr('y', y.bandwidth() / 2).attr('dy', '0.35em')
    .text(c => `$${(c.inp + c.out).toFixed(4)}`);
  const legend = svg.append('g').attr('transform', 'translate(380,20)');
  [['input tokens (text + images)', '#90a4ae'], ['output image tokens', '#1f5f8b']].forEach(([t, c], i) => {
    legend.append('rect').attr('y', i * 18).attr('width', 12).attr('height', 12).attr('fill', c);
    legend.append('text').attr('x', 18).attr('y', i * 18 + 10).text(t);
  });
  svg.append('text').attr('x', 380).attr('y', 68).attr('fill', '#b5452f').text('input images cost before anything is drawn');
</script>