← All sessions, cheat sheets & labs
Week 3 · Hands-on lab · after Session 6

Lab 3: Add a Budget, Cache & Fallback Ladder

Take any model call and make it production-grade: track its cost and latency against a budget, cache the repeats, survive failure with a fallback ladder, and make side effects safe to retry.

~2–3 hourstypescript2 model sizes

What you'll build

A wrapper that turns a raw call into a reliable one.

One call(request) that knows what it is allowed to spend, answers instantly on a repeat, degrades instead of throwing when the model misbehaves, and never double-charges on a retry.

💡 In plain English

Like a good waiter: remembers your usual so you do not reorder, has a backup if the kitchen is slammed, and never puts the same meal on your bill twice, even if he has to ask the chef again.

The shape

Cache, budget, and a ladder for when it breaks.

flowchart TB
  A["Big model"] -- "timeout / error" --> B["Small model"]
  B -- "still failing" --> C["Cached / retrieved answer"]
  C -- "no cache" --> D["Deterministic default"]
  D -- "nothing works" --> E["Honest try-again + log"]
  classDef ok fill:#D6F5E3,stroke:#1F2937,color:#0E1726;
  classDef warn fill:#FEF3C7,stroke:#1F2937,color:#0E1726;
  class A,B ok; class D,E warn;
    

Starter code

Wrap the call.

import OpenAI from "openai";
const client = new OpenAI();
const ladder = ["gpt-4o-mini", "gpt-4o"];       // ladder: cheap first, then capable

async function call(request, budgetUsd = 0.02, timeoutMs = 4000) {
    const key = semanticKey(request);
    const hit = cache.get(key);              // semantic cache: skip the model entirely
    if (hit) return hit;

    let spent = 0.0;
    for (const model of ladder) {
        try {
            const out = await client.chat.completions.create(
                { model, messages: [{ role: "user", content: request }] },
                { timeout: timeoutMs },
            );
            spent += costOf(out);
            if (isValid(out) && spent <= budgetUsd) {
                cache.set(key, out);
                return { answer: out, spent, path: model };
            }
        } catch (err) {                       // timeout or rate limit: step down a rung
            continue;
        }
    }

    return fallback(request);                 // cached, default, or honest try-again
}

async function doRefund(orderId, idemKey) {   // side effects: safe to retry
    if (ledger.seen(idemKey)) {              // idempotency: run once, ever
        return ledger.result(idemKey);
    }
    const result = await payments.refund(orderId);
    ledger.record(idemKey, result);
    return result;
}

Do this, in order

The steps.

  1. 1
    Track cost and latency per callCount tokens in and out, multiply by price, time the call. Log spent and ms on every request.
  2. 2
    Set a budget and enforce itA cost ceiling and a timeout. If a call would blow the budget, do not make it, degrade.
  3. 3
    Add a semantic cacheKey on a normalized/embedded form of the request with a similarity floor. A hit returns instantly, no model call.
  4. 4
    Build the fallback ladderBig model, then small model, then cached/default, then an honest "try again". Each rung wrapped in a timeout.
  5. 5
    Add retries with backoffOn a transient error, retry with exponential backoff and jitter, capped. Validate the output shape before returning.
  6. 6
    Make a side effect idempotentGive any tool that spends money or sends a message an idempotency key, so a retry cannot fire it twice.

You're done when

Acceptance criteria.

Go further

Stretch goals.

  • Stream the output and measure time-to-first-token separately from total.
  • Add a circuit breaker: after N failures, skip the primary for a cooldown window.
  • Batch low-priority requests and show throughput up, per-request cost down.
  • Emit the cost / latency / cache-hit numbers as three counters ready for the Week 5 dashboards.

Wrapping up

Ship the wrapper + the budget.

Hand in your call() wrapper, your budget numbers, and the proof that a retry does not double-charge. This becomes your Project 2 (Cost & Latency Budget + Runbook).

Deliverable
The wrapper, the p95 + cost split, and the idempotency proof.
feeds Project 2
Stuck?
Start with just the timeout + one-rung fallback. Add caching and idempotency once the happy path is solid.
rewatch S5
For the instructorHow to run this labclick to open ▾

The idempotency step is the one that saves a career. Have every student prove it with a counter: force three retries on the refund path and show the ledger records exactly one. That single demo prevents a whole class of production incidents.

Make them actually kill the primary model (bad key, or a forced exception) and watch the ladder degrade. The lesson lands when a "dead" system still answers, users forgive slightly-less-smart, they do not forgive a spinner. Ask everyone to post their p95 and cache-hit rate.