Budget Logging

Logging Every Call Against a Five-Dollar Cap

The ledger is a Markdown table; spent() reads the running total from its last cell. guard() refuses a call that would pass the cap, and log() appends a row from the response's usage:

budget.mjs, part 2: the ledger, the guard and the loggerJavaScript
export function guard(cost) {                      // call before every request
  const total = spent();
  if (total + cost > CAP) {
    throw new Error(`Refused: $${total.toFixed(4)} spent + $${cost.toFixed(4)} > $${CAP}`);
  }
  return total;
}
export function log({ model, size, quality, n = 1, what }, usage, extra = 0) {
  const d = usage.input_tokens_details ?? {};
  const t = { textIn: d.text_tokens ?? 0, imageIn: d.image_tokens ?? 0,
    out: usage.output_tokens };
  const cost = dollars(t) + extra, total = spent() + cost, time = new Date().toISOString();
  const cells = [rows().length + 1, time.slice(0, 16), model, size, quality, n, what,
    t.textIn, t.imageIn, t.out, `$${cost.toFixed(4)}`, `$${total.toFixed(4)}`];
  fs.appendFileSync(LEDGER, `| ${cells.join(' | ')} |\n`);
  return { cost, total };
}

Every call in this chapter ran through these functions, with IMAGE_CAP=2 as a stricter stop than the author's $5; failed requests are not billed and not logged. After Masks and References the ledger held 16 rows and $0.1622, and the guard refused ten 4K images at max: Refused: $0.1622 spent + $4.0026 > $2.

The image ledger as a running total against the cap, with guard() refusing a call that would pass itHTMLLive
<!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; }
  #msg { font-weight: bold; min-height: 1.4em; }
</style>
<script src="https://cdn.jsdelivr.net/npm/d3@7.9.0/dist/d3.min.js"></script>
<p>cap <select id="cap"><option value="2">$2 (IMAGE_CAP=2)</option><option value="5">$5</option></select>
  <button id="small">Log a low-quality call ($0.006)</button> <button id="big">Try ten 4K images at max ($4.0026)</button></p>
<p id="msg"></p>
<svg viewBox="0 0 600 230" font-size="11"></svg>
<script>
  // The chapter's 16 ledger rows (cost per call); the total reaches $0.1622
  const ledger = [0.0060, 0.0070, 0.0076, 0.0142, 0.0143, 0.0225, 0.0078, 0.0179, 0.0019, 0.0050, 0.0172, 0.0064, 0.0142, 0.0143, 0.0038, 0.0021]
    .map((cost, i) => ({ row: i + 1, cost }));
  const spent = () => d3.sum(ledger, r => r.cost);
  const svg = d3.select('svg');
  const gx = svg.append('g').attr('transform', 'translate(0,200)'), gy = svg.append('g').attr('transform', 'translate(40,0)');
  const path = svg.append('path').attr('fill', 'none').attr('stroke', '#1f5f8b').attr('stroke-width', 2);
  const capLine = svg.append('line').attr('stroke', '#b5452f').attr('stroke-dasharray', '5 3');
  const capText = svg.append('text').attr('fill', '#b5452f');

  // guard(): refuse a call that would take the running total past the cap
  function guard(cost) {
    const total = spent(), cap = +document.getElementById('cap').value;
    if (total + cost > cap) throw new Error(`Refused: $${total.toFixed(4)} spent + $${cost.toFixed(4)} > $${cap}`);
    return total;
  }
  function draw() {
    const cap = +document.getElementById('cap').value;
    let run = 0;
    const points = [{ row: 0, total: 0 }, ...ledger.map(r => ({ row: r.row, total: run += r.cost }))];
    const x = d3.scaleLinear([0, Math.max(20, ledger.length)], [40, 590]);
    const y = d3.scaleLinear([0, Math.max(cap * 1.05, run)], [200, 10]);
    gx.call(d3.axisBottom(x).ticks(10)).append('text');
    gy.call(d3.axisLeft(y).ticks(5, '$.2f'));
    path.datum(points).attr('d', d3.line(d => x(d.row), d => y(d.total)).curve(d3.curveStepAfter));
    svg.selectAll('circle').data(points.slice(1)).join('circle').attr('r', 3).attr('fill', '#1f5f8b')
      .attr('cx', d => x(d.row)).attr('cy', d => y(d.total));
    capLine.attr('x1', 40).attr('x2', 590).attr('y1', y(cap)).attr('y2', y(cap));
    capText.attr('x', 44).attr('y', y(cap) - 5).text(`cap $${cap}`);
    svg.selectAll('text.tot').data([run]).join('text').attr('class', 'tot').attr('x', x(ledger.length) + 6).attr('y', y(run) - 6)
      .text(t => `$${t.toFixed(4)} after ${ledger.length} rows`);
  }
  function attempt(cost, what) {
    try {
      guard(cost);
      ledger.push({ row: ledger.length + 1, cost });
      document.getElementById('msg').style.color = '#3f7d3a';
      document.getElementById('msg').textContent = `logged row ${ledger.length}: ${what}, $${cost.toFixed(4)}`;
    } catch (err) {
      document.getElementById('msg').style.color = '#b5452f';
      document.getElementById('msg').textContent = err.message;
    }
    draw();
  }
  document.getElementById('small').onclick = () => attempt(0.006, '1024x1024 low');
  document.getElementById('big').onclick = () => attempt(4.0026, 'ten 3840x2160 at max');
  document.getElementById('cap').onchange = draw;
  draw();
  attempt(4.0026, 'ten 3840x2160 at max');             // the refusal from the chapter
</script>