AI EngineeringZero to ProductionHome·About·Contact
Frontier Agent Capabilities · Part 3

Reasoning models & test-time compute

Reasoning models spend extra compute thinking before they answer — test-time compute that lifts accuracy on hard tasks. Learn the effort dial (low→max), when to reason vs go fast, and how to route and budget it in production.

⏱️ ~1.5 hours🧪 4 labs🎯 Beginner→Tech-lead

Learning objectives

  • Explain what a reasoning model is and how it differs from a fast direct model.
  • Describe extended / adaptive thinking: spending tokens to think before answering.
  • Explain test-time compute — more compute at inference buys accuracy on hard tasks.
  • Weigh the effort dial (low|medium|high|max) as an accuracy vs cost vs latency tradeoff.
  • Route requests between a fast model and a reasoning model by task difficulty.
  • Control reasoning cost in production with budgets and adaptive effort.

1 · What a reasoning model is essential

A normal model reads your prompt and starts writing the answer immediately — one fast pass. A reasoning model first produces a private chain of thinking tokens — it works the problem out step by step — and only then writes the visible answer. Those thinking tokens are extra compute spent before the answer appears. You usually don't see them, but you pay for them, and they wait for them: more thinking means a slower, pricier, but often more correct reply on hard problems.

The setting that turns this on and up is the thinking / effort control introduced in Ch 02 · Prompting. This lesson is about when to reach for it and how much to spend.

Hard question prompt Model thinks step by step Hidden reasoning tokens you pay for these Answer more accurate
🗺️ How to read this diagram

This diagram shows the reasoning path: what happens inside a reasoning model between your question and its answer. Read it left to right.

  • Hard question — your prompt arrives. Nothing special yet.
  • Model thinks — instead of answering straight away, the model works the problem out step by step. This stage costs time.
  • Hidden reasoning tokens — that thinking is made of tokens you usually don't see, but you still pay for them. This is where the extra cost comes from.
  • Answer — only now does the visible reply appear, and on a hard task it's more accurate for having thought first.

In short: A fast model skips the two middle boxes entirely — question → answer. For an easy question that's the right choice; the thinking boxes would just cost money for no gain.

Compare that to the fast path: question → answer, no thinking budget, cheap and low-latency. For an easy question the fast path is the right tool; spending thinking tokens on "what's 2+2" just burns money and time for no gain.

2 · Extended & adaptive thinking essential

Extended thinking means you allow the model a larger budget of thinking tokens so it can reason further before answering. Adaptive thinking means the model spends as much as the problem needs up to that budget — an easy question finishes quickly; a hard one uses more of the allowance. You set the ceiling; the model decides how much of it to use.

The key mental model: thinking tokens are spent before the answer, and they are billed like output tokens. A 2,000-token answer with a 6,000-token thinking budget can cost like an 8,000-token response and take proportionally longer to arrive.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Python · thinking spends tokens before the answer (runs)
thinking_cost.pydef response_cost(thinking_tokens, answer_tokens, price_per_1k=0.015):
    """Thinking tokens are billed before the answer even starts."""
    total = thinking_tokens + answer_tokens
    return {
        "thinking_tokens": thinking_tokens,
        "answer_tokens": answer_tokens,
        "billed_tokens": total,
        "cost_usd": round(total / 1000 * price_per_1k, 5),
    }

fast = response_cost(thinking_tokens=0,    answer_tokens=2000)
deep = response_cost(thinking_tokens=6000, answer_tokens=2000)
print("fast:", fast)
print("deep:", deep)
print("deep costs", round(deep["cost_usd"] / fast["cost_usd"], 1), "x the fast reply")
fast: {'thinking_tokens': 0, 'answer_tokens': 2000, 'billed_tokens': 2000, 'cost_usd': 0.03}
deep: {'thinking_tokens': 6000, 'answer_tokens': 2000, 'billed_tokens': 8000, 'cost_usd': 0.12}
deep costs 4.0 x the fast reply
▶ How this works

This little function makes the core cost idea concrete: thinking tokens are spent before the answer and are billed just like the answer's tokens.

  1. response_cost(...) adds thinking_tokens and answer_tokens into one billed_tokens total, then prices it (here $0.015 per 1,000 tokens).
  2. fast uses 0 thinking tokens — a direct answer. deep uses 6,000 thinking tokens on top of the same 2,000-token answer.
  3. The last line divides the two costs to show the multiplier.

What the output means: The deep reply is billed for 8,000 tokens vs 2,000, so it costs 4.0x the fast reply — for the same-length answer. That extra 4x is pure thinking.

Try this: Lower thinking_tokens to 2000 and re-run — the multiplier drops. This is the dial you turn in section 4.

You pay for thinkingThinking tokens are real tokens: metered and billed like output. "Turn thinking up" is never free — it trades money and latency for accuracy. That trade is only worth it when the task is actually hard.

3 · Test-time compute — buying accuracy at inference intermediate

Test-time compute is the idea behind all of this: instead of only making the model bigger at training time, you spend more compute at inference (test) time — more thinking tokens, or several attempts you vote over. On hard reasoning tasks this reliably lifts accuracy: the model that "thinks longer" gets more of them right. On easy tasks it does almost nothing, because the fast answer was already correct.

The classic curve: accuracy rises with compute but with diminishing returns — each extra unit of thinking helps less than the last. So the goal is never "max everything"; it's "enough compute for this task's difficulty."

Python · diminishing returns of test-time compute (runs)
test_time_compute.pydef accuracy(base_acc, compute_units):
    """Each extra unit of compute closes half the remaining gap to 1.0.
    Deterministic: no randomness, so the curve is reproducible."""
    acc = base_acc
    for _ in range(compute_units):
        acc = acc + (1.0 - acc) * 0.5
    return round(acc, 4)

print("difficulty: HARD (base 0.40)")
for units in range(0, 6):
    print(f"  compute={units}: accuracy={accuracy(0.40, units)}")
print("gain 0->1 unit:", round(accuracy(0.40, 1) - accuracy(0.40, 0), 3))
print("gain 4->5 unit:", round(accuracy(0.40, 5) - accuracy(0.40, 4), 3))
difficulty: HARD (base 0.40)
  compute=0: accuracy=0.4
  compute=1: accuracy=0.7
  compute=2: accuracy=0.85
  compute=3: accuracy=0.925
  compute=4: accuracy=0.9625
  compute=5: accuracy=0.9812
gain 0->1 unit: 0.3
gain 4->5 unit: 0.019
▶ How this works

This models test-time compute: spending more compute at inference to buy accuracy on a hard task — and shows why the gains shrink.

  1. accuracy(base_acc, compute_units) starts at the base accuracy and, for each unit of compute, closes half the remaining gap to a perfect 1.0. It's deterministic — no random numbers — so the curve is the same every run.
  2. The loop prints accuracy for 0 through 5 units of compute on a HARD task (base 0.40).
  3. The last two lines measure the gain from the first extra unit vs the fifth.

What the output means: The first unit adds +0.30 accuracy; the fifth adds only +0.019. Same amount of compute, far less payoff — that's diminishing returns.

Try this: Change the base to 0.90 (an easy task). Notice the gains are tiny from the start — test-time compute barely helps when the fast answer was already right.

The first unit of thinking bought +0.30 accuracy; the fifth bought +0.019. That shape is why a sensible policy caps effort rather than always maxing it.

4 · The effort dial: low → medium → high → max advanced

In practice you don't hand-set token counts — you turn an effort dial with a few named settings. Higher effort = a bigger thinking budget = more accuracy on hard tasks, but more cost and more latency. Think of it as one knob trading three things at once.

EffortThinking budgetRel. costRel. latencyReach for it when…
lowsmall / none1xfasteasy, high-volume, latency-critical
mediummoderate~3xnoticeableeveryday tasks with some reasoning
highlarge~6xslowgenuinely hard analysis / multi-step math
maxvery large~10xslowestthe hardest problems where accuracy is worth any cost
Python · effort -> cost / latency / accuracy table (runs)
effort_dial.pyEFFORT = {
    # effort: (thinking_tokens, base cost multiplier, latency seconds)
    "low":    (0,     1.0, 0.8),
    "medium": (2000,  3.0, 2.5),
    "high":   (6000,  6.0, 6.0),
    "max":    (12000, 10.0, 12.0),
}

def tradeoff(hard_task_base_acc=0.40):
    rows = []
    for name, (think, cost_mult, latency) in EFFORT.items():
        # more thinking closes the accuracy gap, with diminishing returns
        acc = hard_task_base_acc
        for _ in range(think // 2000):
            acc = acc + (1.0 - acc) * 0.5
        rows.append((name, think, cost_mult, latency, round(acc, 3)))
    return rows

print(f"{'effort':7} {'think':>6} {'cost':>5} {'lat_s':>6} {'acc':>6}")
for name, think, cost, lat, acc in tradeoff():
    print(f"{name:7} {think:6d} {cost:5.1f} {lat:6.1f} {acc:6.3f}")
effort   think  cost  lat_s    acc
low          0   1.0    0.8  0.400
medium    2000   3.0    2.5  0.700
high      6000   6.0    6.0  0.925
max      12000  10.0   12.0  0.991
▶ How this works

This turns the abstract curve into the effort dial you actually use: four named settings, each with a thinking budget, a cost multiplier, and a latency.

  1. EFFORT maps each setting (low/medium/high/max) to (thinking_tokens, cost_multiplier, latency_seconds).
  2. tradeoff() computes the resulting hard-task accuracy by reusing the same "close half the gap" rule — one gap-closing step per 2,000 thinking tokens.
  3. It prints a lined-up table so you can read all three tradeoffs at once.

What the output means: Going low → max takes accuracy 0.40 → 0.99, but cost goes 1x → 10x and latency 0.8s → 12s. Crucially, most of the accuracy is already there by high.

Try this: Add a "xmax" row with 24,000 thinking tokens and see how little accuracy it adds over max — proof that piling on compute stops paying off.

Max is 10x the cost for the last few pointsGoing low→max here roughly 10x's cost and 15x's latency to move a hard-task accuracy from 0.40 to 0.98 — but most of that gain arrives by "high". Reserve "max" for tasks where being right is genuinely worth 10x. Defaulting everything to max is the most common way reasoning bills explode.

5 · Routing: fast model vs reasoning by difficulty advanced

The single biggest lever is routing: send easy work to the fast, cheap path and only the hard work to the reasoning path. Most real traffic is easy; if you route it all through a reasoning model you pay 5–10x for accuracy you didn't need. A cheap difficulty classifier in front of the models pays for itself immediately.

Python · route fast vs reasoning by difficulty (runs)
router.pydef estimate_difficulty(task):
    """Cheap heuristic difficulty score in [0,1] — deterministic, no model call."""
    hard_signals = ("prove", "derive", "multi-step", "optimize", "why", "plan", "debug")
    score = 0.1
    if task.get("needs_math"):        score += 0.4
    if task.get("multi_step"):        score += 0.3
    text = task.get("text", "").lower()
    score += 0.1 * sum(sig in text for sig in hard_signals)
    return min(score, 1.0)

def route(task, threshold=0.5):
    d = estimate_difficulty(task)
    path = "reasoning (high effort)" if d >= threshold else "fast (low effort)"
    return round(d, 2), path

tasks = [
    {"text": "What is the capital of France?"},
    {"text": "Summarize this email in one line."},
    {"text": "Derive the optimal batch size, show the multi-step math.", "needs_math": True, "multi_step": True},
    {"text": "Why does the checkout service crash under load? Debug it.", "multi_step": True},
]
for t in tasks:
    d, path = route(t)
    print(f"diff={d:<4} -> {path:24} | {t['text'][:38]}")
diff=0.1  -> fast (low effort)        | What is the capital of France?
diff=0.1  -> fast (low effort)        | Summarize this email in one line.
diff=1.0  -> reasoning (high effort)  | Derive the optimal batch size, show th
diff=0.6  -> reasoning (high effort)  | Why does the checkout service crash un
▶ How this works

This is the single biggest cost lever: a cheap router that sends easy work to the fast path and only hard work to the expensive reasoning path.

  1. estimate_difficulty(task) scores a task from 0 to 1 using cheap signals — does it need math, is it multi-step, does its text contain hard words like "prove" or "debug". No model call, so it's nearly free.
  2. route(task, threshold=0.5) compares that score to a threshold and picks fast (low effort) below it, reasoning (high effort) at or above it.
  3. The loop routes four sample tasks and prints the decision for each.

What the output means: The two trivial questions score 0.1 and go fast; the math derivation scores 1.0 and the debugging task 0.6, so both go to reasoning. Cheap questions never pay the reasoning price.

Try this: Lower threshold to 0.3 and watch more tasks route to reasoning — that is the accuracy-vs-cost knob at the routing level.

Route first, then pick effortA tiny, cheap classifier (even a heuristic like this, or a small fast model) deciding fast-vs-reasoning saves more money than any amount of effort tuning. Route the 90% easy traffic to the cheap path and reserve the expensive reasoning budget for the 10% that needs it.

6 · Production cost control: budgets & self-consistency professional

At scale you can't let effort float freely — you give it a budget. A budget-aware selector picks the highest effort you can still afford for the remaining requests, degrading gracefully instead of blowing the daily spend by lunchtime.

Python · budget-aware effort selector (runs)
budget_selector.pyCOST = {"low": 0.01, "medium": 0.03, "high": 0.06, "max": 0.10}  # $ / request

def pick_effort(remaining_budget, remaining_requests, desired):
    """Pick the desired effort if the budget can sustain it for all remaining
    requests, else step down until it fits. Deterministic."""
    order = ["max", "high", "medium", "low"]
    start = order.index(desired)
    for effort in order[start:]:
        if COST[effort] * remaining_requests <= remaining_budget:
            return effort
    return "low"  # cheapest fallback

print(pick_effort(remaining_budget=100.0, remaining_requests=500,  desired="high"))
print(pick_effort(remaining_budget=20.0,  remaining_requests=500,  desired="high"))
print(pick_effort(remaining_budget=3.0,   remaining_requests=500,  desired="high"))
high
medium
low
▶ How this works

This keeps production spend under control: given a remaining budget and request count, it picks the highest effort you can afford for all of them, stepping down gracefully instead of overspending.

  1. COST is the per-request price of each effort setting.
  2. pick_effort(...) starts at your desired effort and walks down the list (max→high→medium→low) until one fits: its cost times the remaining requests must stay within the remaining budget.
  3. If nothing fits, it falls back to the cheapest setting, low.

What the output means: With a fat budget it keeps high; with a tight one it steps down to medium, then to low — the same code degrading gracefully as money runs low.

Try this: Set remaining_requests to 50 with the $20 budget — now high fits again, because there are far fewer requests to pay for.

Another test-time-compute technique is self-consistency: instead of one long think, sample several independent answers at moderate effort and take a majority vote. On problems with a single right answer, voting over a handful of samples often beats one expensive attempt — trading parallel calls for a bigger single thinking budget.

Python · self-consistency majority vote (deterministic samples) (runs)
self_consistency.pydef fake_sample(task_seed, i):
    """A DETERMINISTIC stand-in for an independent model sample.
    Varies by index (no random): most samples land on the correct answer,
    a minority drift to wrong ones — like real reasoning noise."""
    correct = "42"
    distractors = ["41", "43", "40"]
    # every 4th sample goes astray, cycling through distractors
    if i % 4 == 3:
        return distractors[(i // 4) % len(distractors)]
    return correct

def self_consistency(task_seed, n_samples=8):
    votes = {}
    for i in range(n_samples):
        ans = fake_sample(task_seed, i)
        votes[ans] = votes.get(ans, 0) + 1
    winner = max(votes, key=lambda k: votes[k])
    return winner, votes

winner, votes = self_consistency("hard-math-q", n_samples=8)
print("votes:", votes)
print("majority answer:", winner)
votes: {'42': 6, '41': 1, '43': 1}
majority answer: 42
▶ How this works

This shows the other way to spend test-time compute: take several answers and let them vote, instead of one long think. It's the self-consistency technique.

  1. fake_sample(task_seed, i) is a deterministic stand-in for one independent model answer — it varies by index i (no random!), returning the correct answer most of the time and a wrong "distractor" every 4th sample, mimicking real reasoning noise.
  2. self_consistency(...) collects 8 samples, tallies the votes in a dictionary, and returns the answer with the most votes.
  3. max(votes, key=...) picks the winning answer.

What the output means: Six samples said 42 and two drifted to wrong answers, so the majority vote returns 42 — voting recovered the right answer even though some samples were wrong.

Try this: Raise n_samples to 12 and re-read the tally. More votes make the majority more robust — but each sample is another paid call, which is the cost side of this technique.

Two ways to spend test-time computeYou can spend it serially (one longer think = higher effort) or in parallel (several samples + a vote = self-consistency). Both are test-time compute; which is cheaper depends on your latency budget and whether the task has a single checkable answer.

7 · Turning it on in code professional

In real code you enable extended thinking on the request. This snippet is illustrative — it needs the SDK and a network call, so it does not run offline like the labs above.

Python · enable extended thinking (needs the SDK — does not run offline)
thinking_sdk.py# needs the SDK: pip install anthropic; requires network + credentials.
# Illustrative only — the offline labs above model the same tradeoffs.
from anthropic import Anthropic

client = Anthropic()

resp = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=4000,
    thinking={"type": "enabled", "budget_tokens": 6000},  # the effort dial
    messages=[{"role": "user",
               "content": "Prove that the sum of the first n odd numbers is n^2."}],
)

# The response carries hidden `thinking` blocks (billed) plus the final answer.
for block in resp.content:
    print(block.type)
budget_tokens IS the effort dialRaising budget_tokens is exactly the low→max dial modeled in effort_dial.py. Same setting introduced in Ch 02; here you're deciding how high to turn it per request.

8 · A reasoning strategy for the system tech-lead

A lead doesn't set effort per prompt by feel — they design the whole reasoning tier: a difficulty router in front (fast path for the easy majority), default effort tuned per route, a spend budget with graceful degradation, and evals proving the extra compute actually buys accuracy on your tasks (not just benchmarks). Reasoning is a cost center you manage, not a switch you leave on.

The reasoning-tier playbook

  1. Route by difficulty first — most traffic never needs the reasoning path (router.py).
  2. Set a default effort per route, not a global one — easy routes stay low, hard routes go high.
  3. Cap spend with a budget that degrades effort gracefully under pressure (budget_selector.py).
  4. Prove the win on evals — measure accuracy vs cost at each effort on your own tasks; stop where returns flatten.
  5. Consider self-consistency for single-answer tasks where parallel votes beat one long think.

This connects to two neighbours. When even high effort on a base model can't reliably hit a narrow behavior, that's the signal to consider the fine-tuning decision (XT4) — reasoning tunes compute at inference, fine-tuning tunes the model itself. And the effort/thinking setting you're dialing here is the one introduced in Ch 02 · Prompting.

Reasoning is a managed cost, not a defaultThe tech-lead move is to treat test-time compute as an explicit, budgeted, eval-gated tier — route to it, cap it, and prove it earns its cost — rather than turning thinking to max everywhere and discovering the bill later.

Exercise FA3.1 — Route and price a mixed workload

Context: Routing only earns its keep if you can quantify the savings. A realistic mixed workload lets you price routed traffic against sending everything at high effort.

Your task: Route a day of mixed traffic (say 900 easy lookups + 100 hard analyses) with router.py, price the fast vs reasoning paths with effort_dial.py, and compare the total against sending everything at high effort.

Requirements:

  • Route every request in the mixed workload
  • Price the fast and reasoning paths separately
  • Compute the total routed cost
  • Compare against an all-high-effort baseline and report the % saved

💡 Hint: Most of the volume is the cheap path, so the savings come from not paying reasoning prices on the 900 easy lookups.

Exercise FA3.2 — Pick effort under a budget

Context: Sometimes the budget, not the workload, sets the ceiling. Given a fixed daily budget you must find the highest effort you can actually sustain across expected traffic.

Your task: With a $30 daily budget and ~800 expected requests, use budget_selector.py to find the effort you can sustain, then decide which routes should still get high effort within that cap.

Requirements:

  • Divide the budget across expected requests to find affordable effort
  • Pick the highest effort level that fits the per-request budget
  • Decide which routes still justify high effort within the cap
  • State the tradeoff you're accepting

💡 Hint: Per-request budget is total budget ÷ expected requests; the selector finds the richest effort tier that stays under it.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Route by difficultyBeginner

Context: Reasoning models are expensive, and most traffic doesn't need them. A router that sends easy prompts to a fast model and hard ones to a reasoning model is the first cost lever.

Your task: Write a router that sends easy prompts to a fast model and hard ones to a reasoning model, using simple difficulty signals.

Requirements:

  • Score difficulty from cheap signals (keywords, length, structure)
  • Route below a threshold to the fast model, above it to the reasoning model
  • Return the chosen model per prompt
  • Signals are easy to extend

💡 Hint: Words like "prove", "derive", or "step by step" are strong hard-prompt signals; a simple score over them is enough to route.

Show solution
def difficulty(prompt):
    hard_signals = ["prove", "step by step", "derive", "plan", "why"]
    score = sum(sig in prompt.lower() for sig in hard_signals)
    return "hard" if score or len(prompt) > 200 else "easy"

def route(prompt):
    return "reasoning-model" if difficulty(prompt) == "hard" else "fast-model"

print(route("What's the capital of France?"))          # fast-model
print(route("Derive the shortest path step by step"))  # reasoning-model

Routing keeps cost and latency low: pay for extended thinking only on the requests that actually benefit from it.

Exercise 2 · The effort dial → costIntermediate

Context: The reasoning effort dial (low/medium/high/max) buys accuracy by spending more thinking tokens. Turning that dial into a token budget lets you estimate cost before you spend it.

Your task: Model the effort dial as a thinking-token budget per level and estimate the cost per request at each setting.

Requirements:

  • Map each effort level to a thinking-token budget
  • Combine thinking + output tokens with a per-token price to get cost
  • Show cost rising with effort
  • Make the price and budgets easy to adjust

💡 Hint: Higher effort mostly means more thinking tokens; the cost model is thinking + output tokens times their prices.

Show solution
THINKING_BUDGET = {"low": 1000, "medium": 4000, "high": 16000, "max": 32000}
PRICE_PER_1K = 0.015   # illustrative output-token price in dollars

def est_cost(effort, answer_tokens=500):
    tokens = THINKING_BUDGET[effort] + answer_tokens
    return round(tokens / 1000 * PRICE_PER_1K, 4)

for e in THINKING_BUDGET:
    print(f"{e:6} -> ~{THINKING_BUDGET[e]} think tokens, ${est_cost(e)}")
# max costs ~20x low; use the lowest effort that clears your accuracy bar

Test-time compute is a dial, not a switch: more thinking tokens raise accuracy on hard problems but cost more and add latency, so tune effort to the task.

Exercise 3 · Self-consistency votingAdvanced

Context: Self-consistency — sampling several answers and taking the majority — is a cheap, reliable accuracy boost on hard reasoning problems.

Your task: Implement self-consistency: given N candidate answers, return the most common one and a confidence score.

Requirements:

  • Tally the N candidate answers
  • Return the majority answer
  • Report confidence as the winner's share of the votes
  • Handle ties in a defined way

💡 Hint: A Counter over the candidates gives both the modal answer and the vote fraction to use as confidence.

Show solution
from collections import Counter

def self_consistency(answers):
    counts = Counter(answers)
    best, n = counts.most_common(1)[0]
    return {"answer": best, "votes": n, "confidence": round(n / len(answers), 2)}

# imagine 5 independent reasoning samples for one arithmetic problem
samples = ["42", "42", "42", "41", "42"]
print(self_consistency(samples))
# {'answer': '42', 'votes': 4, 'confidence': 0.8}

Independent reasoning paths make different mistakes but tend to agree on the right answer, so majority voting raises accuracy — at N× the cost, another form of test-time compute.

Exercise 4 · A thinking-token budget guardExpert

Context: A runaway chain-of-thought can quietly burn a budget. Production reasoning needs a hard ceiling that downgrades effort once cumulative thinking tokens cross a line.

Your task: Track cumulative thinking tokens across a session and downgrade the effort level when a ceiling is hit.

Requirements:

  • Accumulate thinking tokens across requests in a session
  • Compare the running total against a ceiling
  • Downgrade the effort level once the ceiling is crossed
  • Keep serving requests at the reduced effort rather than failing

💡 Hint: The guard is stateful across the session; crossing the ceiling should step the dial down (e.g. high → medium), not hard-stop.

Show solution
class ReasoningBudget:
    def __init__(self, ceiling=50000):
        self.ceiling = ceiling; self.used = 0
    def effort_for(self, requested):
        order = ["low", "medium", "high", "max"]
        budget = {"low": 1000, "medium": 4000, "high": 16000, "max": 32000}
        # downgrade until it fits the remaining budget
        i = order.index(requested)
        while i > 0 and self.used + budget[order[i]] > self.ceiling:
            i -= 1
        chosen = order[i]; self.used += budget[chosen]
        return chosen

b = ReasoningBudget(ceiling=20000)
print(b.effort_for("max"))    # high  (32k would overflow -> downgraded)
print(b.effort_for("max"))    # low   (little budget left)
print("used:", b.used)

Capping and gracefully downgrading effort protects the bill and latency SLOs when many hard requests arrive at once.

Exercise 5 · Turn on extended thinking (SDK)Professional

Context: Extended thinking is a real API feature: the model returns its reasoning separately from its final answer. Knowing when to surface that thinking versus hide it is a product decision. This uses the Anthropic SDK.

Your task: Enable extended thinking in a real request, read back both the thinking and the final answer, and explain when to expose the thinking versus hide it.

Requirements:

  • Turn on extended thinking in the request
  • Read back the thinking content and the final answer separately
  • Explain a case for exposing thinking (transparency/debugging)
  • Explain a case for hiding it (UX, leaking chain-of-thought)

💡 Hint: Thinking is useful for debugging and trust but is usually hidden from end users; the final answer is what ships.

Show solution

Needs the SDK + network. Uses the documented thinking parameter (no invented fields):

from anthropic import Anthropic

client = Anthropic()

resp = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=4000,
    thinking={"type": "enabled", "budget_tokens": 8000},
    messages=[{"role": "user",
               "content": "A train leaves at 3pm going 60mph... solve step by step."}],
)

for block in resp.content:
    if block.type == "thinking":
        print("[reasoning]", block.thinking[:80], "...")
    elif block.type == "text":
        print("[answer]", block.text)

The budget_tokens is the effort dial. Log the thinking for debugging/evals, but usually show users only the final text block — the reasoning is a means, not the product.

Exercise 6 · A reasoning strategy for the systemIndustry scenario

Context: As tech lead you must decide, system-wide, when reasoning is worth paying for. That means folding routing, an effort policy, and a fallback into one coherent decision function.

Your task: Combine routing, an effort policy, and a fallback into a single decision function that defines when the system pays for reasoning, and justify each branch.

Requirements:

  • Route easy vs hard traffic first
  • Apply an effort policy (possibly budget-aware) to the hard path
  • Include a fallback for failures or exhausted budget
  • Justify each branch of the decision
  • Produce one clear decision per request

💡 Hint: This rung composes the earlier rungs: routing decides whether to reason, the effort policy decides how hard, and the fallback covers the edges.

Show solution
def reasoning_policy(request):
    tier = request["tier"]           # 'free' | 'pro'
    kind = request["kind"]           # 'chat' | 'math' | 'code' | 'plan'
    latency_sla_ms = request["sla_ms"]

    if kind == "chat" or latency_sla_ms < 500:
        return {"model": "fast", "thinking": None}      # cheap, snappy
    if kind in {"math", "plan"}:
        effort = "high" if tier == "pro" else "medium"  # gate cost by tier
        return {"model": "reasoning", "thinking": effort}
    if kind == "code":
        return {"model": "reasoning", "thinking": "medium"}
    return {"model": "fast", "thinking": None}

print(reasoning_policy({"tier": "pro", "kind": "plan", "sla_ms": 5000}))
# {'model': 'reasoning', 'thinking': 'high'}
print(reasoning_policy({"tier": "free", "kind": "chat", "sla_ms": 300}))
# {'model': 'fast', 'thinking': None}

The policy makes the cost/accuracy/latency tradeoff explicit and auditable: reasoning is reserved for task types that need it, effort scales with the customer tier, and tight-SLA paths always take the fast model.

✓ Checkpoint — you can move on when you can…

  • Explain a reasoning model vs a fast model, and that thinking tokens are spent (and billed) before the answer.
  • Describe extended vs adaptive thinking and what test-time compute buys.
  • Read the effort dial as an accuracy vs cost vs latency tradeoff with diminishing returns.
  • Route fast vs reasoning by difficulty and pick effort under a budget.
  • Design a routed, budgeted, eval-gated reasoning tier for production.

Knowledge check check yourself

✓ Knowledge check

What is “test-time compute,” and why does maxing the effort dial usually not pay off?

Show answer
Test-time compute spends more at inference (more thinking tokens or several voted attempts) to raise accuracy on hard tasks. Returns diminish — each extra unit closes less of the remaining gap — so most of the accuracy is already there by “high,” and “max” adds little for a lot more cost and latency.
✓ Knowledge check

Why is routing by difficulty the single biggest lever for controlling reasoning-model cost?

Show answer
Most real traffic is easy, and thinking tokens are billed like output tokens. Sending everything through the reasoning path pays 5–10x for accuracy easy questions didn't need, so routing easy work to the fast/cheap model and only hard work to the reasoning model captures the accuracy where it matters without the blanket cost.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in