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.
client.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
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.
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.
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.
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