Study guides / CCDV-F / Domain 2

Model Selection & Optimisation · Lesson 1 of 5

2.1 - Claude Model Family and Trade-offs

Understand what actually differs between Claude's model tiers - price, capability, and behaviour - so a model choice is a deliberate trade-off, not a default.

Claude is not one model — it is a family of four, each occupying a different point on the speed/cost/intelligence spectrum: Claude Haiku 4.5 (fastest, cheapest, built for high-volume simple tasks), Claude Sonnet 5 (the balanced tier that handles most production workloads well), Claude Opus 5 (frontier-grade reasoning for complex agentic and coding work), and Claude Fable 5 (Anthropic's most capable widely released model, for the hardest long-horizon reasoning). The exam framing to internalise: this is a spectrum you choose a point on per task, not a single "best model" that should handle everything.

The current lineup, concretely

Pricing and context window differ meaningfully across the tiers (rates per million tokens):

ModelInput / 1MOutput / 1MContext
Claude Fable 5$10.00$50.001M tokens
Claude Opus 5$5.00$25.001M tokens
Claude Sonnet 5$2.00$10.001M tokens
Claude Haiku 4.5$1.00$5.00200K tokens

Output tokens are billed at roughly five times the input rate on every current tier — a model that writes a long, rambling response costs far more than the same model asked to be concise (Lesson 2.2 covers this). Fable 5 costs twice what Opus 5 costs per token in both directions, and Opus 5 costs 2.5x Sonnet 5 — the price ladder tracks capability closely, so overpaying for a tier a task doesn't need is a real, measurable cost, not a rounding error, especially once volume is factored in.

Capability differences beyond price

The tiers don't just differ in speed and cost — some API behaviour genuinely differs by tier, and the exam expects you to know this rather than assume every model behaves identically:

None of this shows up if you think of "model tier" as a single dial from cheap to expensive. It's a family of models with genuinely different operating characteristics, and the strongest tier is not simply a slower, pricier version of the mid tier — it can have different defaults, different allowed configurations, and different deployment constraints entirely.

Key concept

A mature application often mixes tiers deliberately — a cheap model for routing/classification, a stronger one for the step that actually needs judgment. This is the routing pattern covered fully in Lesson 2.5.

The mistake in both directions

Defaulting to Fable 5 or Opus 5 everywhere wastes money and adds latency on tasks a cheaper tier handles just as well — simple classification, short extraction, formatting, a five-way ticket router. At volume the difference compounds: a step that runs a million times a month at Opus 5's $5/$25 rate versus Haiku 4.5's $1/$5 rate is a five-times-plus cost multiplier for no measurable quality gain on an easy task. Defaulting to the cheapest tier everywhere to save cost produces worse output exactly where it's most visible: complex reasoning, nuanced judgment calls, long autonomous chains where an early mistake compounds across every later step.

Common exam distractor

An answer that frames model selection purely as "newest or most capable model = best choice" is a trap. Fable 5 being Anthropic's most capable widely released model doesn't make it the right default — it's priced and behaviourally suited (always-on thinking, a 30-day retention requirement, refusal handling) for the hardest long-horizon agentic work, not for a five-way ticket classifier that runs a hundred thousand times a day.

Exam traps

Practice question

An application has two Claude-backed steps: (1) classify an incoming support ticket into one of five categories, high volume, and (2) draft a nuanced response to a complex billing dispute, lower volume. What's the most sensible model assignment?

  • A Use the most capable, most expensive tier for both steps, to maximise quality everywhere.

    Simple five-way classification doesn't need top-tier reasoning capability - this overspends on the high-volume, low-difficulty step.

  • B Use the fastest, cheapest tier for both steps, to minimise cost.

    The nuanced billing-dispute draft is exactly where weaker reasoning is most likely to show, and it's low volume - the cost savings there are small relative to the quality risk.

  • C Use a fast, inexpensive tier for the high-volume classification step and a stronger tier for the nuanced drafting step. Correct

    This matches model capability to task difficulty and volume on each step independently, which is the core of deliberate model selection.

  • D Model choice doesn't affect either step meaningfully, so it doesn't matter which tier is used.

    Model tier does affect both cost at volume and reasoning quality on nuanced tasks - it's a real trade-off, not a non-factor.

Build exercise: Run the same tasks across the model family and compare cost, quality, and behaviour

Beginner · 30 minutes

You'll practice:

  1. Send the same simple classification prompt to Claude Haiku 4.5 and Claude Opus 5, and print the response text plus usage.input_tokens / usage.output_tokens for each.

    Seeing near-identical output quality but a large token/cost gap on an easy task is the concrete case for not over-provisioning model tier.

    You should see: Both models classify the ticket correctly, but Opus 5's response and usage numbers cost roughly five times more per call at these rates.

    Hints
    1. What field in the Anthropic SDK response tells you how many tokens were billed for a call?
    2. Loop over a list of model IDs, call client.messages.create with the same prompt for each, and read response.usage.input_tokens and response.usage.output_tokens after each call.
    3. import anthropic
      client = anthropic.Anthropic()
      
      prompt = "Classify this support ticket into one of: billing, technical, account, shipping, other.\n\nTicket: 'My package says delivered but I never received it.'"
      
      for model in ["claude-haiku-4-5", "claude-opus-5"]:
          response = client.messages.create(
              model=model,
              max_tokens=256,
              messages=[{"role": "user", "content": prompt}],
          )
          text = next(b.text for b in response.content if b.type == "text")
          print(model, "->", text)
          print("  in:", response.usage.input_tokens, "out:", response.usage.output_tokens)
  2. Send a genuinely hard multi-constraint reasoning prompt to Claude Sonnet 5 and Claude Opus 5 at output_config.effort = 'high', and compare the depth and correctness of each response.

    This is where the quality gap between tiers should actually show up - validating the trade-off on a hard case, not just the easy classification from step 1.

    You should see: A visible difference in how thoroughly each model reasons through the edge cases in the prompt, not just a difference in response length.

    Hints
    1. Where does the effort setting live in a messages.create call - is it top-level or nested?
    2. effort goes inside output_config, not at the top level of the request. Keep the prompt and effort identical across both models so the comparison isolates model tier.
    3. hard_prompt = "A warehouse has 3 shipping lanes with different SLAs. A customer needs delivery by Friday, ordering Wednesday at 4pm from a lane with a 3-business-day SLA that excludes weekends. Will the order arrive on time? Show your reasoning and account for edge cases."
      
      for model in ["claude-sonnet-5", "claude-opus-5"]:
          response = client.messages.create(
              model=model,
              max_tokens=2000,
              output_config={"effort": "high"},
              messages=[{"role": "user", "content": hard_prompt}],
          )
          text = next(b.text for b in response.content if b.type == "text")
          print(model, "response length:", len(text))
          print(text[:300])
  3. Write a small helper that computes the dollar cost of a call from response.usage and the published per-tier rates, and run it against both calls from step 1.

    Turning token counts into a dollar figure is what actually lets you defend a model-selection decision - 'fewer tokens' alone doesn't tell you the size of the trade-off.

    You should see: A printed dollar amount per call, with the Opus 5 call costing several times more than the Haiku 4.5 call for the same task.

    Hints
    1. You already have the per-tier input/output rates from the lesson table - how would you turn a token count into a dollar amount?
    2. Divide each token count by 1,000,000 and multiply by the matching per-million rate for that model, then sum the input and output portions.
    3. PRICES = {
          "claude-fable-5": (10.00, 50.00),
          "claude-opus-5": (5.00, 25.00),
          "claude-sonnet-5": (2.00, 10.00),
          "claude-haiku-4-5": (1.00, 5.00),
      }
      
      def call_cost(model, usage):
          in_rate, out_rate = PRICES[model]
          return (usage.input_tokens / 1_000_000) * in_rate + (usage.output_tokens / 1_000_000) * out_rate
      
      print(f"{model}: ${call_cost(model, response.usage):.6f}")
  4. Send a request to Claude Opus 5 and check response.stop_reason before reading response.content - handle the case where it equals 'refusal' by reading response.stop_details instead of assuming content always contains text.

    Refusal is a normal 200-status response state on Fable 5 and Opus 5, not an exception - code that unconditionally reads content[0].text will crash or silently mishandle a declined request.

    You should see: A code path that checks stop_reason first and only extracts text when it isn't 'refusal'.

    Hints
    1. What HTTP status code does a refusal come back as - and what does that imply about how you should check for it?
    2. A refusal is a normal 200 response with stop_reason set to 'refusal' and a populated stop_details object - check stop_reason before you index into content.
    3. response = client.messages.create(
          model="claude-opus-5",
          max_tokens=1024,
          messages=[{"role": "user", "content": user_prompt}],
      )
      
      if response.stop_reason == "refusal":
          category = response.stop_details.category if response.stop_details else "unknown"
          print("Declined:", category)
      else:
          text = next(b.text for b in response.content if b.type == "text")
          print(text)
  5. Attempt to send thinking={'type': 'disabled'} to claude-fable-5 and confirm it raises anthropic.BadRequestError, demonstrating that Fable 5's always-on thinking cannot be turned off.

    This makes the tier-specific behaviour difference from the lesson concrete - code written assuming every model accepts the same thinking configuration will fail specifically on this tier.

    You should see: A caught BadRequestError with a 400-style message rather than a successful response.

    Hints
    1. Which exception class does the Anthropic Python SDK raise for a 400-level request error?
    2. Wrap the call in a try/except for anthropic.BadRequestError - Fable 5 rejects an explicit disabled thinking configuration, unlike Opus 5 which allows it below effort high.
    3. import anthropic
      
      try:
          client.messages.create(
              model="claude-fable-5",
              max_tokens=1024,
              thinking={"type": "disabled"},
              messages=[{"role": "user", "content": "Hello"}],
          )
      except anthropic.BadRequestError as e:
          print("Expected 400:", e.message)

Sources