AI EngineeringZero to ProductionHome·About·Contact
Anthropic API in Practice · Part 8

Request parameters, in full

A precise reference to every parameter of client.messages.create: what each does, its sane default, when you'd actually change it, and the gotchas — don't set both temperature and top_p, max_tokens must fit the model, forcing a tool changes stop_reason, extended thinking runs at temperature=1. Defaults are good; change them deliberately.

⏱️ ~1.5 hours🧪 5 recipes🎯 Beginner→Tech-lead

Learning objectives

  • Name every parameter of client.messages.create and say what each one does.
  • Recite the three required parameters and give sane defaults for the rest.
  • Shape messages and system correctly — roles, content blocks, cache_control.
  • Tune sampling deliberately: temperature vs top_p (never both), top_k, stop_sequences.
  • Force, allow, or forbid tools with tool_choice, and control parallel calls.
  • Turn on extended thinking correctly and know the gotchas (thinking needs temperature=1).
  • Recognise the operational knobs — stream, metadata, service_tier, betas — and when to touch them.

1 · One endpoint, many knobs essential

Almost everything you do with Claude goes through a single call: client.messages.create(...). Tools, vision, streaming, thinking, structured output — none of them are separate APIs. They are all parameters on this one call. Learn the parameters once and the whole surface opens up.

This lesson is a reference. Earlier lessons showed you the parameters you needed in the moment (a model here, a max_tokens there); this one lays every parameter out in a single place, with its default, what it actually changes, and when — if ever — you should touch it. Read it once end to end, then come back to the big table whenever you're unsure.

There are only three required parameters — model, max_tokens, and messages. Everything else has a sensible default, and the single most common mistake is over-configuring: setting knobs you don't understand and making the model worse. The mental model to carry through this lesson: the defaults are good — change them deliberately, one at a time, with a reason.

Defaults are good — change deliberatelyIf you set a parameter, you should be able to say why in one sentence. Every knob you turn is a chance to make the model slower, costlier, or dumber. When in doubt, leave it at the default and let the model do its job.

2 · The parameter groups essential

The parameters fall into five natural groups. Hold this shape in your head and every specific parameter has a home:

Required model, max_tokens, messages Prompt system, content blocks Sampling temperature, top_p, stop Tools tools, tool_choice Operational stream, thinking, metadata
🗺️ How to read this diagram

This picture is the whole parameter surface of messages.create on one line. Every specific parameter in the lesson lives in exactly one of these five boxes — so when a parameter confuses you, first ask which box it's in.

  • Requiredmodel, max_tokens, messages. The minimum any call needs; there are no defaults for these three.
  • Promptsystem and the content blocks inside messages. This is where your actual instructions and the conversation live.
  • Samplingtemperature, top_p, top_k, stop_sequences. These tune how the model picks each next token, not what you ask.
  • Toolstools and tool_choice. Present only when you want the model to call functions.
  • Operationalstream, thinking, metadata, service_tier, betas. These govern delivery, reasoning, and billing rather than the words of the answer.

In short: The big table in §3 is organised by these same five groups. Read left to right — most calls set the first two boxes, occasionally reach into sampling or tools, and leave the operational box to a shared house wrapper.

Required is the minimum any call needs. Prompt shaping is where your actual instructions and conversation live. Sampling tunes how the model picks the next token. Tools let the model call functions. Operational knobs govern delivery, reasoning, and billing rather than the content of the answer. The reference table in §3 uses these same groups.

3 · The full reference table essential

Every parameter of messages.create, its default, what it does, and when you'd actually reach for it. Bookmark this section.

ParameterDefaultWhat it doesWhen to touch it
model (required)Which Claude answers (e.g. claude-opus-4-8).Always. Pick by task: Opus for hard work, Haiku for cheap/fast.
max_tokens (required)Hard cap on output tokens. The model stops here even mid-sentence.Always set it. Big enough to finish; must fit the model's output limit.
messages (required)The conversation: a list of {role, content}. Roles alternate user/assistant.Always. This is the prompt (plus any history you carry).
systemNoneTop-level instructions / persona. A string, or a list of text blocks (for cache_control).Almost always — it's where role, rules, and tools context go.
temperature1.0Randomness of token choice. 0 ≈ deterministic-ish, 1 = full range.Lower (0–0.3) for extraction/classification; leave at 1 for creative work.
top_p1.0Nucleus sampling: consider only the top tokens summing to p probability.Rarely. An alternative to temperaturenever set both.
top_kunsetOnly sample from the k most likely tokens.Rarely — an advanced sampling lever; most workloads never need it.
stop_sequences[]Strings that force the model to stop when generated (the string is not included).When you need a hard delimiter — end of a section, a custom terminator.
streamFalseReturn tokens incrementally over SSE instead of one final message.Interactive UIs, and any large-max_tokens call (avoids timeouts).
toolsNoneList of tool definitions (name, description, JSON-schema input) the model may call.When the model needs to act — call functions, search, run code.
tool_choice{"type":"auto"}Whether the model may / must / must-not use a tool.To force a specific tool, force some tool, or forbid tools for one turn.
thinkingoffExtended (adaptive) reasoning before the answer: {"type":"adaptive"}.Hard multi-step problems. Pair with output_config.effort.
metadataNoneOpaque request metadata, notably {"user_id": ...} for abuse tracking.In production, to attach a stable per-end-user id (hashed, not PII).
service_tierprovider defaultRequests a priority/standard/batch service level (verify current values in docs).Rarely — when you have a priority-tier arrangement and want to steer to it.
betas / extra_headersnoneOpt into beta features via client.beta.messages + a beta flag string.Only when a specific beta feature needs it (files, caching betas, etc.).
output_configNoneStructured-output format and the effort dial for thinking depth.For guaranteed JSON (format) or to tune reasoning cost (effort).
"Sane default" means: safe to omitEvery non-required row above is safe to leave out entirely. The values shown are what the API assumes when you don't pass the parameter. You never have to set them — the table's last column is your prompt for when it's worth the deviation.

4 · Required + prompt shaping — model, max_tokens, messages, system essential

These are the parameters you set on essentially every call. model and max_tokens are scalars; the interesting shape is in messages and system.

messages is a list of turns. Each turn has a role ("user" or "assistant") and content. Content can be a plain string, or — when you need images, documents, or tool results — a list of content blocks ({"type": "text", ...}, {"type": "image", ...}, and so on). system is separate from messages: it's top-level instructions, given as a string, or as a list of text blocks when you want to attach cache_control to cache a large stable prompt.

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 · anthropic SDK — the three required params + system ▶ needs API key + network
required.py# needs: pip install anthropic + ANTHROPIC_API_KEY; makes a real API call
import anthropic

client = anthropic.Anthropic()          # reads ANTHROPIC_API_KEY from the environment

resp = client.messages.create(
    model="claude-opus-4-8",            # REQUIRED: which model answers
    max_tokens=1024,                    # REQUIRED: hard cap on output tokens
    system=[                            # top-level instructions (list form → cacheable)
        {
            "type": "text",
            "text": "You are a terse DevOps assistant. Answer in one sentence.",
            "cache_control": {"type": "ephemeral"},   # cache this stable prefix
        }
    ],
    messages=[                          # REQUIRED: the conversation
        {"role": "user", "content": "What does 'kubectl get pods' show?"},
        # content can also be a LIST of blocks, e.g. text + an image:
        # {"role": "user", "content": [
        #     {"type": "text", "text": "What's in this screenshot?"},
        #     {"type": "image", "source": {"type": "url", "url": "https://.../shot.png"}},
        # ]},
    ],
)

# content is a LIST of blocks — read only the text ones
print(next((b.text for b in resp.content if b.type == "text"), ""))
▶ How this works

This is the smallest useful call — the three required parameters plus a system prompt. It needs pip install anthropic and an ANTHROPIC_API_KEY, and it makes a live request. Everything else in the lesson is an addition to this shape.

  1. client = anthropic.Anthropic() builds the client and reads your key from the environment, so the key never appears in the code.
  2. model and max_tokens are the two required scalars: which Claude answers, and the hard ceiling on how many tokens it may produce.
  3. system is given here as a list of text blocks (not a plain string) so a cache_control marker can be attached — that caches the stable prompt prefix. A plain string works too when you don't need caching.
  4. messages is the conversation. Each turn is {role, content}; content is a string, or (see the commented lines) a list of blocks when you add an image or document.

What the output means: The one-sentence answer prints. Because resp.content is a list of blocks, we pull the first text block rather than assuming a string.

Try this: Swap the system list for a plain string (system="...") and re-run — it still works; you just lose the ability to cache that prefix.

max_tokens must fit the modelmax_tokens is the output ceiling, and it can't exceed the model's max output (e.g. 128K on Opus 4.8, 64K on Sonnet/Haiku). Set it too low and the reply is truncated mid-thought with stop_reason == "max_tokens"; set it very high on a non-streaming call and the SDK may refuse it to avoid an HTTP timeout — stream instead.

5 · Recipe — temperature vs top_p (and stop_sequences) intermediate

Sampling parameters change how the model chooses each next token. The two you'll meet most are temperature and top_p, and the golden rule is: set at most one of them. They're two different ways to control the same thing (randomness), and steering with both at once is confusing and unnecessary — Anthropic recommends adjusting temperature and leaving top_p alone.

temperature ranges 0 to 1. Near 0 the model almost always picks the most likely token — good for extraction, classification, and anything you want reproducible-ish. At 1 (the default) it samples across the full distribution — good for brainstorming and prose. stop_sequences is unrelated to randomness: it's a list of strings that, when the model generates one, cut the response off immediately (the stop string itself is not returned). When that happens, stop_reason is "stop_sequence" and stop_sequence tells you which one fired.

Python · anthropic SDK — low temperature + a stop sequence ▶ needs API key + network
sampling.py# needs: pip install anthropic + ANTHROPIC_API_KEY; makes a real API call
import anthropic

client = anthropic.Anthropic()

resp = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=256,
    temperature=0,                      # near-deterministic: extraction / classification
    # top_p=0.9,                        # DON'T also set this — pick ONE knob, not both
    stop_sequences=["\n\nEND"],       # stop the moment the model writes this marker
    system="Extract the error code. Output only the code, then write \n\nEND.",
    messages=[{"role": "user",
               "content": "Log: FATAL 2026-09-06 db timeout code=E5521 retry=3"}],
)

text = next((b.text for b in resp.content if b.type == "text"), "")
print("text:", repr(text))
print("stop_reason:", resp.stop_reason)      # "stop_sequence" if the marker fired
print("stop_sequence:", resp.stop_sequence)  # which sequence caused the stop
▶ How this works

This call is tuned for a deterministic extraction task — pull one error code out of a log line. It shows the two sampling ideas you'll use most: a low temperature, and a stop_sequence that ends the reply at a marker.

  1. temperature=0 makes the model almost always pick the single most likely token — exactly what you want for extraction, where creativity is a bug, not a feature.
  2. The commented top_p=0.9 line is a trap on purpose: temperature and top_p are two controls for the same thing, so you set one, never both.
  3. stop_sequences=["\n\nEND"] tells the model to stop the instant it writes that marker. The marker itself is not included in the returned text.
  4. After the call, resp.stop_reason reports why it stopped and resp.stop_sequence says which marker fired — that's how you confirm the stop sequence did the work rather than a natural end or the token cap.

What the output means: The extracted code prints, then stop_reason ("stop_sequence" if the marker fired) and the specific stop_sequence that caused it.

Try this: Delete the stop_sequences argument and re-run — the model finishes on its own and stop_reason becomes "end_turn" instead.

Never set both temperature and top_pThey are alternative controls for the same knob. Setting both muddies the sampling distribution and gives you no clearer control than one alone. Adjust temperature for almost everything; reach for top_p only if you have a specific reason, and then drop temperature.
stop_sequences vs stop_reasonstop_sequences is an input (strings you supply that end generation). stop_reason is an output field telling you why the model stopped: "end_turn" (finished naturally), "max_tokens" (hit your cap), "stop_sequence" (hit one of yours), "tool_use", or "refusal". Always read stop_reason before trusting the content.

6 · Recipe — tools & tool_choice (force / allow / forbid) intermediate

tools is a list of tool definitions (each a name, a description, and a JSON-schema for its input). tool_choice controls whether and which tool the model uses. It has four modes:

tool_choiceMeaning
{"type": "auto"}Default. Model decides whether to use a tool at all.
{"type": "any"}Model must use one of the tools (its choice which).
{"type": "tool", "name": "X"}Model must use tool X — forced.
{"type": "none"}Model may not use any tool this turn.

Any of these can also carry "disable_parallel_tool_use": true to force at most one tool call per turn (by default the model may request several at once). Here is a forced tool call — useful when you want structured extraction and you know the model should always fill in this one tool:

Python · anthropic SDK — force a specific tool ▶ needs API key + network
tool_choice.py# needs: pip install anthropic + ANTHROPIC_API_KEY; makes a real API call
import anthropic

client = anthropic.Anthropic()

record_incident = {
    "name": "record_incident",
    "description": "Record a parsed incident from an alert line.",
    "input_schema": {
        "type": "object",
        "properties": {
            "service": {"type": "string"},
            "severity": {"type": "string", "enum": ["low", "high", "critical"]},
        },
        "required": ["service", "severity"],
    },
}

resp = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=512,
    tools=[record_incident],
    # FORCE this exact tool + at most one call this turn:
    tool_choice={"type": "tool", "name": "record_incident",
                 "disable_parallel_tool_use": True},
    messages=[{"role": "user",
               "content": "CRITICAL checkout-api is down, 500s everywhere"}],
)

# a forced tool means stop_reason == "tool_use" and a tool_use block with the args
tool_use = next((b for b in resp.content if b.type == "tool_use"), None)
print(resp.stop_reason)          # "tool_use"
print(tool_use.name, tool_use.input)   # record_incident {'service': 'checkout-api', ...}
▶ How this works

This forces the model to call one specific tool — a clean way to get guaranteed structured data out of free text. The magic is entirely in tool_choice; the tool definition itself is an ordinary name + description + JSON schema.

  1. tools=[record_incident] makes one tool available. Its input_schema declares the fields (service, severity with an enum) the model must fill in.
  2. tool_choice={"type": "tool", "name": "record_incident"} forces that exact tool — the model won't answer in prose, it will emit a tool_use block with the parsed arguments.
  3. "disable_parallel_tool_use": True caps it at a single tool call this turn (without it, the model may request several at once).
  4. Because a tool was forced, resp.stop_reason is "tool_use" and you read the answer from the tool_use block's .input — not from a text block.

What the output means: stop_reason is "tool_use", and the tool's .input is a dict like {'service': 'checkout-api', 'severity': 'critical'} — structured, not prose.

Try this: Change tool_choice to {"type": "auto"} and re-run. Now the model decides whether to call the tool — on a clear incident it usually still does, but it may answer in text instead.

Forcing a tool changes stop_reasonWith {"type":"tool"} or {"type":"any"} the model will emit a tool_use block and stop_reason becomes "tool_use" — it won't answer in prose. Don't force a tool and then look for a text answer; read the tool_use block instead. Forcing is great for guaranteed structured extraction, wrong for open-ended chat.

7 · Recipe — extended thinking advanced

Extended thinking lets the model reason privately before it answers — a real lever on hard, multi-step problems (planning, tricky debugging, math). On current models you turn it on with adaptive thinking, where the model itself decides how much to think, and you tune the overall depth with output_config.effort.

Two accuracy points that trip people up. First, on Opus 4.8 / 4.7 the old {"type": "enabled", "budget_tokens": N} form is gone — it returns a 400. Use {"type": "adaptive"}. Second, thinking runs at temperature=1 — you cannot combine extended thinking with a lowered temperature (or with top_p/top_k); those sampling params are incompatible with thinking. So the recipe is: turn thinking on, leave sampling alone.

Python · anthropic SDK — adaptive thinking + effort ▶ needs API key + network
thinking.py# needs: pip install anthropic + ANTHROPIC_API_KEY; makes a real API call
import anthropic

client = anthropic.Anthropic()

resp = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=8000,
    thinking={"type": "adaptive", "display": "summarized"},  # model decides how much
    output_config={"effort": "high"},   # low | medium | high | max  — the depth dial
    # NOTE: do NOT also set temperature / top_p / top_k here — thinking runs at
    # temperature=1 and rejects those sampling params.
    messages=[{"role": "user",
               "content": "Plan a zero-downtime Postgres major-version upgrade."}],
)

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

This turns on extended thinking — private reasoning before the answer — for a genuinely hard planning task. Two things are load-bearing here: the adaptive type, and the absence of any sampling parameter.

  1. thinking={"type": "adaptive"} is the current way to enable thinking on Opus 4.8 / 4.7 — the model decides how much to think. The old {"type": "enabled", "budget_tokens": N} form is gone and returns a 400.
  2. output_config={"effort": "high"} is the depth dial — low through max. This is how you control reasoning cost, not temperature.
  3. There is deliberately no temperature / top_p / top_k here: extended thinking always runs at temperature=1, and combining it with a custom sampling value is a 400.
  4. The reply comes back as blocks — a thinking block (the summarised reasoning) followed by the text answer — so the loop checks block.type for each.

What the output means: A truncated summary of the model's reasoning prints first (the thinking block), then the actual upgrade plan (the text block).

Try this: Lower effort to "low" and compare the depth of the plan against the token count in resp.usage — that's the quality-vs-cost trade thinking gives you.

thinking + effort, not thinking + temperatureControl reasoning depth with output_config.effort, not with temperature. Extended thinking always runs at temperature=1; passing a custom temperature (or top_p/top_k) alongside it is a 400. Reach for thinking on genuinely hard problems — it costs more tokens and latency, so it's overkill for a lookup or a classification.

8 · Validate before you send (this one runs offline) professional

Most parameter mistakes are catchable before you spend a request: both temperature and top_p set, a max_tokens that exceeds the model's output cap, or extended thinking combined with a non-default temperature. The helper below is pure stdlib — no API key, no network — and runs with a plain python file.py. It inspects a params dict and returns a list of warnings, so you can lint your call sites in a test.

Python · pre-flight parameter linter (runs offline)
validate_params.py# Catch the common messages.create mistakes locally, before spending a request.
# Model output caps as of this writing (verify current values in the docs):
MODEL_MAX_OUTPUT = {
    "claude-opus-4-8": 128_000,
    "claude-sonnet-4-6": 64_000,
    "claude-haiku-4-5": 64_000,
}


def validate_params(params):
    """Return a list of human-readable warnings for a messages.create params dict.
    Pure stdlib; does not call the API."""
    warnings = []
    model = params.get("model")
    if not model:
        warnings.append("model is REQUIRED and missing")
    if "max_tokens" not in params:
        warnings.append("max_tokens is REQUIRED and missing")
    if not params.get("messages"):
        warnings.append("messages is REQUIRED and empty/missing")

    # don't set both temperature and top_p
    if "temperature" in params and "top_p" in params:
        warnings.append("both temperature and top_p set — pick ONE, not both")

    # max_tokens must fit the model's output cap
    cap = MODEL_MAX_OUTPUT.get(model)
    mt = params.get("max_tokens")
    if cap is not None and isinstance(mt, int) and mt > cap:
        warnings.append(f"max_tokens={mt} exceeds {model} output cap of {cap}")

    # extended thinking requires temperature == 1 (i.e. leave it unset)
    thinking = params.get("thinking")
    thinking_on = isinstance(thinking, dict) and thinking.get("type") in ("adaptive", "enabled")
    if thinking_on and params.get("temperature", 1) != 1:
        warnings.append("thinking is on but temperature != 1 — thinking runs at temperature=1")
    if thinking_on and ("top_p" in params or "top_k" in params):
        warnings.append("thinking is on but top_p/top_k set — incompatible with thinking")

    return warnings


bad = {
    "model": "claude-opus-4-8",
    "max_tokens": 200_000,          # over the 128K cap
    "messages": [{"role": "user", "content": "hi"}],
    "temperature": 0.2,             # both temperature ...
    "top_p": 0.9,                   # ... and top_p
    "thinking": {"type": "adaptive"},  # thinking on, but temperature != 1
}
for w in validate_params(bad):
    print("WARN:", w)
print("ok:", validate_params({
    "model": "claude-opus-4-8", "max_tokens": 1024,
    "messages": [{"role": "user", "content": "hi"}],
}))
WARN: both temperature and top_p set — pick ONE, not both
WARN: max_tokens=200000 exceeds claude-opus-4-8 output cap of 128000
WARN: thinking is on but temperature != 1 — thinking runs at temperature=1
WARN: thinking is on but top_p/top_k set — incompatible with thinking
ok: []
▶ How this works

Unlike the four blocks above, this one is pure Python — no key, no network — so it runs with a plain python validate_params.py. It's a pre-flight linter: hand it a params dict and it lists the mistakes that would otherwise cost you a wasted (or 400'd) request.

  1. It first checks the three required parameters are present — model, max_tokens, messages.
  2. Then the classic mutual-exclusion bug: both temperature and top_p set. Pick one.
  3. MODEL_MAX_OUTPUT holds each model's output cap, so it can flag a max_tokens that exceeds what the model can actually produce.
  4. Finally the thinking rules: if thinking is on, temperature must be 1 and top_p/top_k must be unset — otherwise the real API 400s. The deliberately-broken bad dict trips all of these at once.

What the output means: Four WARN: lines for the broken dict (both-sampling, over-cap, thinking+temp, thinking+top_p), then ok: [] for the clean one — an empty warning list means the params are safe to send.

Try this: Drop this into a unit test over your real call sites. Catching a bad params dict offline costs nothing; catching it as a 400 in production costs a request and a page.

Lint your call sitesDropping validate_params into a unit test catches the whole class of "why did my request 400?" bugs without ever hitting the network. It's the cheapest possible check — no key, no tokens, no latency.

9 · Tech-lead — operational knobs & a house policy tech-lead

The last group of parameters doesn't change what the model says — it changes how the request is delivered, tracked, and billed. A lead sets a house policy for these so every call site is consistent:

Operational parameter policy

  1. stream for anything interactive or large. Streaming isn't just UX — it's how you avoid HTTP timeouts on big max_tokens calls. Policy: stream any user-facing response and any call with max_tokens above ~16K; use .get_final_message() to still get the assembled reply and usage.
  2. metadata.user_id on every production call. Attach a stable, hashed per-end-user identifier (never raw PII) so abuse and rate-limit issues can be traced to a tenant. It costs nothing and it's the field support will ask you for.
  3. thinking + effort by route, not globally. Turn thinking on for the hard routes (planning, debugging) and leave it off for lookups and classification. Standardise the effort level per route so cost is predictable.
  4. service_tier only if you have an arrangement. It requests a priority / standard / batch service level; the exact accepted values and behaviour evolve, so treat it as opt-in and verify the current options in the docs before wiring it in.
  5. betas deliberately, never by default. Beta features live on client.beta.messages with an explicit flag string. Pin the flag, document why you need it, and plan to remove it when the feature goes GA — don't leave stale beta headers scattered across the codebase.
  6. Centralise the defaults. Wrap messages.create in one thin house function that fills model, sane max_tokens, metadata, and the streaming decision. Then validate_params from §8 runs in that one place.

The through-line: the content parameters (messages, system, tools) are per-request and belong to the feature; the operational parameters (stream, metadata, service_tier, betas) are cross-cutting and belong to the platform. Put the second set behind one wrapper and every engineer inherits the house policy for free.

service_tier and betas move — verify in docsservice_tier's accepted values and betas' flag strings change as features ship and graduate. This lesson deliberately doesn't hard-code them: when you reach for either, check the current Anthropic docs for the exact string rather than copying one that may have rotated.

Exercise AP8.1 — Read the table, build the call

Context: Choosing parameters is a design decision, not a default-copy. A classification task wants a cheap model, a tiny output, and near-zero randomness — and every extra knob you add should earn its place.

Your task: Without looking at the recipes, write a messages.create call for a classification task (label a support ticket): pick the right model, a small max_tokens, the right temperature, and decide whether you need thinking. Justify each non-required parameter in one sentence and delete any you can't justify.

Requirements:

  • Pick a fast, cheap model appropriate to a simple labeling task
  • Set a small max_tokens that fits just the label
  • Choose a low/zero temperature for stable, repeatable labels
  • Decide (and justify) that thinking is unnecessary for classification
  • Write a one-sentence justification per non-required parameter — and drop any that has none

💡 Hint: Start from the three required params and add a knob only when you can name the behavior it buys; classification rarely needs more than a low temperature.

Exercise AP8.2 — Extend the linter

Context: A linter is only as good as its coverage. Two more checks catch config mistakes that silently waste requests: a forced tool with no tools defined, and a huge output with streaming off.

Your task: Extend validate_params with two checks: (1) warn if tool_choice is {"type":"tool"} but tools is empty or missing, and (2) warn if max_tokens is very large (say > 16000) but stream is not True. Run it on a params dict that trips both.

Requirements:

  • Add a check for a forced tool_choice with no usable tools list
  • Add a check for a large max_tokens (> 16000) without stream=True
  • Preserve the linter's existing warnings — extend, don't replace
  • Both new warnings are appended as clear strings to the returned list
  • Demonstrate on a dict that trips both and confirm both warnings print

💡 Hint: Read tool_choice, tools, and stream defensively so a missing key is treated as absent, not an exception.

🪜 Practice ladder beginner → industry

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

Exercise 1 · The three required parametersBeginner

Context: Every Claude call rests on three required fields; everything else is a knob you opt into. Knowing the minimum keeps you from cargo-culting parameters you don't understand.

Your task: Write a messages.create() call using only the three required parameters — model, max_tokens, and messages — and explain in a comment why system is optional.

Requirements:

  • Pass exactly model, max_tokens, and messages — nothing else
  • max_tokens is understood as a hard cap on the output length
  • messages is a list of role/content dicts with a single user turn
  • A comment states that without system the model falls back to its defaults
  • Print the reply's text from the first content block

💡 Hint: Reach for a current model id such as claude-sonnet-4-6; resist adding temperature or anything else until a task demands it.

Show solution

system is optional; the three required fields are the minimum to get a reply.

import anthropic
client = anthropic.Anthropic()

resp = client.messages.create(
    model="claude-sonnet-4-6",   # required
    max_tokens=256,               # required: hard cap on output
    messages=[{"role": "user", "content": "Define idempotency."}],  # required
)
# system is optional: with no role/rules given, the model uses its defaults.
print(resp.content[0].text)
Exercise 2 · Temperature vs top_p (never both)Intermediate

Context: temperature and top_p are two different ways to shape randomness, and Anthropic's guidance is to tune only one. For deterministic extraction you want the temperature floor.

Your task: Show one call that lowers temperature for a deterministic extraction, plus a comment stating the rule about setting temperature and top_p together.

Requirements:

  • Set temperature=0 for focused, repeatable output
  • A comment states you should not also set top_p — pick one knob
  • Use stop_sequences to cut the output at a natural boundary
  • The task is a small, single extraction (one fact out of one sentence)
  • Print both the text and the response's stop_reason

💡 Hint: Tuning both sampling knobs at once fights itself — lower temperature alone, and let a stop_sequences entry end the reply cleanly.

Show solution

Tune one sampling knob at a time. Setting both temperature and top_p is discouraged; pick one.

import anthropic
client = anthropic.Anthropic()

resp = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=64,
    temperature=0,           # focused, repeatable; do NOT also set top_p
    stop_sequences=["\n\n"],  # stop at first blank line
    messages=[{"role": "user", "content": "Extract the year: Founded in 1994."}],
)
print(resp.content[0].text, resp.stop_reason)
Exercise 3 · Force a tool and read the tool_use blockAdvanced

Context: When you force a tool, Claude's answer is no longer prose — the structured arguments arrive in a tool_use block and stop_reason flips to tool_use. Reading content[0].text then gets you nothing.

Your task: Define one tool and force it with tool_choice, then explain in a comment why you must read the tool_use block rather than content[0].text.

Requirements:

  • Declare a tool with a name, description, and an input_schema
  • Force it via tool_choice={"type": "tool", "name": ...}
  • Recognize that forcing a tool drives stop_reason to tool_use
  • Scan resp.content for the block whose type is tool_use and read its input
  • A comment explains the arguments live in that block, not in a text block

💡 Hint: Never index content[0] blindly — iterate the blocks and branch on block.type so you pick up the arguments wherever they land.

Show solution

Forcing a tool sets stop_reason to "tool_use"; the arguments live in a tool_use block, not in text.

import anthropic
client = anthropic.Anthropic()

tools = [{
    "name": "record_temp",
    "description": "Record a temperature reading.",
    "input_schema": {"type": "object",
        "properties": {"celsius": {"type": "number"}},
        "required": ["celsius"]},
}]
resp = client.messages.create(
    model="claude-sonnet-4-6", max_tokens=256, tools=tools,
    tool_choice={"type": "tool", "name": "record_temp"},
    messages=[{"role": "user", "content": "It is 21 degrees C."}],
)
# stop_reason == 'tool_use' -> the args are in a tool_use block:
for block in resp.content:
    if block.type == "tool_use":
        print(block.input)   # {'celsius': 21}
Exercise 4 · Extended thinking with effortExpert

Context: Extended thinking lets Claude reason before answering, but it changes the rules: thinking runs at temperature 1 and rejects the sampling knobs. Depth is dialed with output_config.effort, not temperature.

Your task: Enable extended thinking and set the reasoning depth via output_config, and note the two constraints thinking imposes on sampling parameters.

Requirements:

  • Turn thinking on with an adaptive thinking setting ({"type": "adaptive"})
  • Set reasoning depth through output_config with an effort of low|medium|high|max
  • Do not pass temperature, top_p, or top_k — a comment notes thinking runs at temperature 1 and rejects them (a 400)
  • Give max_tokens real headroom for a reasoning task
  • Print only the final text blocks from the response

💡 Hint: Treat output_config.effort as the single dial for how hard the model thinks, and leave every sampling parameter unset while thinking is on.

Show solution

Thinking uses {"type": "adaptive"}; it runs at temperature 1 and rejects temperature/top_p/top_k. Depth is tuned with output_config.effort.

import anthropic
client = anthropic.Anthropic()

resp = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=2048,
    thinking={"type": "adaptive", "display": "summarized"},
    output_config={"effort": "high"},   # low | medium | high | max
    # NOTE: with thinking on, do NOT pass temperature/top_p/top_k -> 400.
    messages=[{"role": "user", "content": "Prove sqrt(2) is irrational."}],
)
for b in resp.content:
    if b.type == "text":
        print(b.text)
Exercise 5 · An offline parameter validatorProfessional

Context: A bad parameter combination burns a request (and money) before you learn it was invalid. A small offline linter catches the classic mistakes at your boundary instead of at the API.

Your task: Implement validate_params(p, model_cap) that returns warnings for setting both temperature and top_p, for max_tokens above the model cap, and for thinking enabled together with a non-default temperature.

Requirements:

  • Warn when both temperature and top_p are present
  • Warn when max_tokens exceeds the supplied model_cap
  • Warn when thinking is on but temperature is not the required default of 1
  • Also flag any missing required param (model, max_tokens, messages)
  • Return a list of warning strings; run fully offline against a sample dict

💡 Hint: Read the params dict defensively with .get() so a missing key is just another warning rather than a KeyError.

Show solution

This is the lesson's linter: catch config mistakes before spending a request.

def validate_params(p, model_cap):
    warns = []
    if "temperature" in p and "top_p" in p:
        warns.append("set temperature OR top_p, not both")
    if p.get("max_tokens", 0) > model_cap:
        warns.append(f"max_tokens {p['max_tokens']} > cap {model_cap}")
    if p.get("thinking") and p.get("temperature", 1) != 1:
        warns.append("thinking requires temperature=1 (drop it)")
    for k in ("model", "max_tokens", "messages"):
        if k not in p:
            warns.append(f"missing required param: {k}")
    return warns

bad = {"model": "claude-opus-4-8", "max_tokens": 200000,
       "messages": [], "temperature": 0.3, "top_p": 0.9}
print(validate_params(bad, model_cap=128000))
Exercise 6 · A house-policy parameter guard for shared infraIndustry scenario

Context: When a platform fronts every Claude call, per-team discipline doesn't scale — policy has to be enforced in one sanitizer that rewrites requests before they leave the building.

Your task: Enforce a house policy on incoming params: default max_tokens if unset, cap it at the model's limit, strip top_p when temperature is present, and require a metadata.user_id for abuse tracking, returning the sanitized params.

Requirements:

  • Look the cap up per-model and clamp max_tokens to it (defaulting it first when absent)
  • Drop top_p whenever temperature is set, so only one sampling knob survives
  • Reject the request (raise) if metadata.user_id is missing
  • Return a copy — do not mutate the caller's dict
  • Runs offline; demonstrate it on a params dict that trips every rule

💡 Hint: Copy the dict up front, then apply each rule as an independent transform so adding the next policy later is just one more step.

Show solution

A single sanitizer keeps every team inside the guardrails without hand-review.

CAPS = {"claude-opus-4-8": 128000, "claude-sonnet-4-6": 64000,
        "claude-haiku-4-5": 64000}

def house_policy(p):
    p = dict(p)
    cap = CAPS.get(p["model"], 4096)
    p.setdefault("max_tokens", 1024)
    p["max_tokens"] = min(p["max_tokens"], cap)      # never exceed the cap
    if "temperature" in p:
        p.pop("top_p", None)                          # one sampling knob
    md = p.setdefault("metadata", {})
    if "user_id" not in md:
        raise ValueError("metadata.user_id required for abuse tracking")
    return p

out = house_policy({"model": "claude-sonnet-4-6", "max_tokens": 999999,
                    "temperature": 0.2, "top_p": 0.8,
                    "metadata": {"user_id": "u-42"}})
print(out["max_tokens"], "top_p" in out)   # 64000 False

✓ Checkpoint — you can move on when you can…

  • Name the three required parameters and give a sane default for every other one.
  • Shape messages (roles + content blocks) and system (string or cacheable list) correctly.
  • Explain why you never set both temperature and top_p.
  • Distinguish stop_sequences (input) from stop_reason (output).
  • Force, allow, and forbid tools with tool_choice, and disable parallel calls.
  • Turn on adaptive thinking correctly and state why you can't also lower the temperature.
  • Name the operational knobs (stream, metadata, service_tier, betas) and a house policy for each.

Knowledge check check yourself

✓ Knowledge check

Why should you never set both temperature and top_p, and what do they each control?

Show answer
They are two different controls for the same randomness knob (temperature scales the whole distribution; top_p does nucleus sampling over the top tokens summing to probability p), so setting both is contradictory and is a common source of 400 errors. Pick one sampling lever.
✓ Knowledge check

What happens if max_tokens is set too low, and why can setting it very high on a non-streaming call also fail?

Show answer
Too low and the reply truncates mid-output with stop_reason == "max_tokens". Set very high on a non-streaming call and the SDK may refuse to avoid an HTTP timeout on a long generation; the fix is to stream (stream=True) so tokens arrive incrementally.
© 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