Calling a Model API

The request shape every provider shares: roles, a system prompt, a token cap, and how to find the text in the reply.

Syntaxclient.messages.create(model="claude-opus-5-5", max_tokens=1024, system="...", messages=[{"role": "user", "content": "..."}])

Every provider exposes the same four ideas: a model name, a list of messages with roles, a system instruction, and a ceiling on how much the model may generate. The API is stateless, so a conversation is you resending the whole history on every call.

The request

Messages alternate user and assistant, and the first one must be user. The standing system prompt is a separate top-level argument rather than the first message, because it carries operator authority rather than conversation content. Current models such as claude-opus-5-5 do also accept a role: "system" entry later in messages, as an operator instruction added mid-conversation without disturbing the cached prefix; that is a separate feature, not a replacement for the top-level prompt. max_tokens bounds the reply only; it does not limit what you send.

Shorthand and longhand

A message's content takes either a plain string or a list of typed blocks. {"role": "user", "content": "Hello"} and {"role": "user", "content": [{"type": "text", "text": "Hello"}]} are the same request; the string is shorthand the SDK expands for you. You need the long form as soon as one turn carries an image, a document or a tool result alongside its text.

The response

response.content is a list of blocks, not a string. Filter on block.type == "text" and join, because a reply can also contain thinking or tool_use blocks. Check response.stop_reason before you trust the text: end_turn is a finished answer, max_tokens is a truncated one, tool_use means the model is waiting for you.

What trips people up

Sampling controls are not portable. On current models such as claude-opus-5-5, temperature is rejected with HTTP 400 and depth is set with output_config={"effort": "low"} instead; older models accept temperature. Read the model's own reference before copying a parameter from a tutorial.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Adding a summarise button to an existing web handler
  • Porting a working curl request into application code
  • Debugging a reply that arrives empty because the code indexed content[0] blindly
  • Attaching a screenshot or PDF to a question about it

More examples

A multi-turn conversation

Memory is a list you own. Appending the assistant turn is the step people forget, and the symptom is a model that answers every follow-up as if it were the first message. Note that history grows every turn, which is where the context and cost lesson starts to matter.

Example · python
import anthropic

client = anthropic.Anthropic()
SYSTEM = "You are a terse release-notes editor."
history = []

def ask(question: str) -> str:
    history.append({"role": "user", "content": question})
    reply = client.messages.create(
        model="claude-opus-5-5", max_tokens=1024,
        system=SYSTEM, messages=history,
    )
    text = "".join(b.text for b in reply.content if b.type == "text")
    # The API is stateless: the assistant turn must go back into history
    # or the next call has no idea what was just said.
    history.append({"role": "assistant", "content": text})
    return text

print(ask("Summarise: fixed a race in the uploader."))
print(ask("Now make it one line."))

Mixed content in one turn

The long form of content, carrying two blocks in a single user turn. base64.standard_b64encode produces no newlines, which the API requires. Use this shape whenever a turn is more than plain text: images, PDFs as document blocks, or tool results.

Example · python
import base64

with open("invoice.png", "rb") as handle:
    encoded = base64.standard_b64encode(handle.read()).decode()

message = {
    "role": "user",
    "content": [
        {"type": "image", "source": {
            "type": "base64", "media_type": "image/png", "data": encoded}},
        {"type": "text", "text": "What is the total on this invoice?"},
    ],
}
# The image block goes before the text block; the string shorthand cannot
# express this at all, which is why the long form exists.

Checking stop_reason properly

A match statement (Python 3.10+) over stop_reason, with an explicit arm for every case the API documents and a catch-all that fails loudly. Writing this once is cheaper than debugging a user report about an answer that ends mid-word.

Example · python
reply = client.messages.create(
    model="claude-opus-5-5", max_tokens=256,
    messages=[{"role": "user", "content": prompt}],
)

match reply.stop_reason:
    case "end_turn":
        text = "".join(b.text for b in reply.content if b.type == "text")
    case "max_tokens":
        raise RuntimeError("truncated at 256 tokens")
    case "tool_use":
        pass                      # see the tool calling lesson
    case "refusal":
        # stop_details is populated only for a refusal; it is None otherwise.
        raise RuntimeError(reply.stop_details.category)
    case other:
        raise RuntimeError(f"unhandled stop_reason {other!r}")

Discussion

  • Be the first to comment on this lesson.