The Messages API
Stateless request in, one message out.
Using a language model means sending it a request over the internet and getting a reply back. The request is a list of messages, each labelled with who said it — standing instructions, the person, or the model's own earlier replies. The response is one more message. That exchange is the entire interface, and the model keeps nothing afterwards: it has no memory of the request you sent a second ago.
Because nothing is remembered, a conversation only exists if you rebuild it yourself, resending the whole history on every turn. Miss that and your chatbot forgets everything after one reply. Understand it and you can see why a long conversation gets steadily slower and more expensive: you are paying to send the same earlier turns again and again.
It sits on ordinary HTTP APIs and on tokenization, which is the unit the request is measured and charged in. It is also the doorway to everything above it: prompting, structured output, tool calling, agents and RAG are all answers to one question, which is what goes into this request this time.
The model is a stateless function — the request is everything it knows, and it forgets all of it the moment it replies.
In more detailAn LLM call is a stateless HTTP POST: you send role-tagged messages, you get one message back, and the model retains nothing between calls. Conversation is an illusion the caller constructs by resending the whole history each turn, which is why cost grows with the square of conversation length rather than linearly.
Learn first
Making a call
By the end: You can call a model, read the reply, and know what every part of the response means.
- The shape of a request
- Roles, and what each is for
- Reading the response properly
- Checking why generation stopped
Talking to a model means sending some text over the internet and reading what comes back, and this is where that exchange gets pinned down: how the thing you send is put together, why each piece of it is labelled with a speaker, and how to read the reply properly rather than just printing it. What comes back carries more than the answer, including a field saying whether the model finished or simply ran out of room, and ignoring that field is how truncated text reaches users. Everything else you build sits on top of this one exchange.
A request is a list of messages#
You send a list. Each item has a role saying who is speaking, and some content. The model replies with one message.
# pip install anthropic
import os
from anthropic import Anthropic
client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
messages=[{"role": "user", "content": "What is a stateless API?"}],
)
print(response.content[0].text)Later examples shorten this
Once past this section, examples write client.chat(messages) or client.generate(prompt) as shorthand for the call above, so that the code stays about the thing being taught rather than repeating six lines of setup. Those are not real methods on any SDK. Wherever you see one, the real call is client.messages.create as written here.
The three roles#
- system: your instructions. Who the model is, what it should do, what format to use. Not shown to the user.
- user: what the person said.
- assistant: what the model said previously. Providers differ here; some call this role model rather than assistant.
messages = [
{"role": "system", "content": "You answer in one sentence."},
{"role": "user", "content": "What is Python?"},
{"role": "assistant", "content": "A high-level programming language."},
{"role": "user", "content": "Who created it?"},
]Role names differ between providers
Anthropic and OpenAI use assistant. Google uses model. The concept is identical, only the spelling changes. This is the kind of detail to check in the documentation rather than remember.
Reading the response#
Beyond the text, two fields matter on every response and most people ignore both.
response.text # the answer
response.usage.input_tokens # what you were charged for sending
response.usage.output_tokens # what you were charged for receiving
response.stop_reason # why it stoppedstop_reason tells you whether you got the whole answer. The common values are a natural finish, hitting the maximum token limit, or the model asking to call a tool.
if response.stop_reason == "max_tokens":
# The answer is cut off mid-sentence. No exception was raised.
raise ValueError("truncated: raise max_tokens or shorten the task")Truncation is silent
When output hits the limit, you get a partial string back and nothing raises. Your JSON parse then fails somewhere unrelated, or a summary is quietly half written. Checking stop_reason is a one-line habit that saves hours.
Where people go wrong
Reading response.text without checking stop_reason. A truncated answer looks like a complete one until something downstream fails on it.
Build this: Call a model with max_tokens set to 20 and ask for a long story. Print the text and the stop reason, and confirm it stops mid-sentence with no error raised.
Open this section on its own pageThe model has no memory
By the end: You can explain why conversations work the way they do, and build one correctly.
- What stateless means here
- Proving it in two calls
- Building a conversation by hand
- Why cost grows faster than the conversation does
Chat products feel as though they remember you. The thing underneath remembers nothing at all, and this section is about what follows from that. You will prove it to yourself, then build a conversation the way every chat product does, by resending the entire history on every turn. The consequence that catches people out lands on the bill, because the twentieth turn is paying for the first nineteen all over again.
Every call is the first call#
The model remembers nothing between requests. Each call is a separate HTTP request that carries everything the model will know. Nothing is stored on the other end.
This is easy to accept and hard to believe, because chat products feel like they remember. Prove it to yourself in two calls.
ask("My name is Jack.")
# "Nice to meet you, Jack!"
ask("What is my name?")
# "I don't have access to your name."So what is a conversation?#
Something you build. To make turn five work, you resend turns one to four along with the new question. The illusion of memory is entirely on your side of the wire.
history = []
def ask(text):
history.append({"role": "user", "content": text})
reply = client.chat(history) # send ALL of it
history.append({"role": "assistant", "content": reply.text})
return reply.textAppend the model's replies too
Forgetting the second append is the most common mistake here. The model then sees a run of user messages with no answers in between, and gets confused about what it already said. The symptom is oddly incoherent later turns.
Why this gets expensive#
Each turn resends everything before it. Turn one sends one message, turn ten sends nineteen. Total cost across a conversation grows with the square of its length, not in proportion to it.
Turn 1: 7 input tokens
Turn 2: 33 input tokens
Turn 3: 48 input tokensThere is a second surprise in those numbers. Model replies are usually longer than user questions, so most of what you are paying to resend is the model's own earlier output.
Every technique you will meet later, prompt caching, summarising old turns, retrieval instead of history, is an answer to this one problem.
Where people go wrong
Not appending the assistant's replies to the history. The model receives several user turns in a row with no responses between them, and later answers become subtly wrong about what was already said.
Build this: Build a conversation loop that prints the input token count each turn. Run it for five turns with short questions and watch the count climb. Then remove the assistant appends and see how the answers degrade.
Open this section on its own pageParameters that change the output
By the end: You can choose settings deliberately instead of copying them from an example.
- Temperature, and what it can and cannot do
- max_tokens as a ceiling, not an instruction
- Why temperature zero is not reproducible
- System prompts
Alongside your messages you can send a few settings that change how the model writes: how adventurous it is allowed to be, how long the reply may run, and what standing instructions it should follow throughout. This section is about what each one really does, so that you set them deliberately rather than copying them out of an example. The two most often misused are temperature, which does not make a model more accurate, and the length ceiling, which cuts a reply off rather than asking for a shorter one.
Temperature#
At each step the model has a probability for every possible next token. Temperature changes how sharply it favours the likely ones. Low temperature makes it pick the most probable token almost every time. High temperature flattens the distribution so less likely tokens get a chance.
- 0 to 0.3: extraction, classification, anything you will parse, anything you will evaluate.
- 0.7 to 1.0: writing, brainstorming, anywhere variety is the point.
- Above about 1.5: usually degrades into nonsense.
Temperature cannot create variety that is not there
Ask a model to name a colour and it will say blue at temperature 0 and still say blue at temperature 2, because the distribution is overwhelmingly peaked on one answer. High temperature only produces variety where the model was genuinely uncertain. This surprises people who expect the setting to force creativity.
"Name one colour." t=0.0 -> Blue, Blue, Blue
"Name one colour." t=2.0 -> Blue, Blue, Blue
"Name any animal." t=0.0 -> Elephant, Lion, Lion, ElephantTemperature 0 is not deterministic#
People assume temperature 0 means the same input always gives the same output. On a hosted API it does not. Requests are batched together on GPUs, floating point addition is not perfectly associative, and which other requests share your batch changes the result slightly. Different hardware and expert routing add more variation.
This matters most when you write evals. An exact-match assertion on generated text will flake, get marked as unreliable, and eventually be deleted. Assert on structure and properties instead, or run repeatedly and check a pass rate.
max_tokens#
This caps the length of the reply. It does not tell the model to be brief. The model writes as though it had unlimited room and generation is cut off when the cap is reached, usually mid-sentence.
# Wrong way to get a short answer
client.chat(messages, max_tokens=50)
# Right way
messages = [
{"role": "system", "content": "Answer in at most two sentences."},
...
]Still set max_tokens, as a safety net against a runaway response. Set it comfortably above what you expect, and check stop_reason to find out whether you hit it.
System prompts#
The system message sets standing instructions for the whole conversation: the role, the format, the tone, what to do when unsure. Keep it stable, because a stable prefix is what makes prompt caching work later.
SYSTEM = """You extract invoice data.
Return only JSON matching the schema you are given.
If a field is not present in the text, use null rather than guessing.
Never invent a number."""Where people go wrong
Copying temperature settings from a tutorial. A temperature of 0.7 on an extraction task is why your JSON parses 95 percent of the time instead of always.
Build this: Run the same prompt ten times at temperature 0 and ten times at 1.5, and compare the outputs. Then try a prompt with one obvious answer and confirm that temperature barely changes it.
Open this section on its own pageWhere you use this
A topic is easier to care about when you can see what it is for. These projects lean on it at a named stage.
- Stage 4, Answer, with citations that resolve: Where the retrieved text goes in the request, and what the system prompt is for.
- Stage 2, The loop, written by hand: Why the whole transcript goes back every turn, and why that makes the list your problem.
Now actually do it
Reading a topic is not learning it. These are the hours that count, and each one tells you when you are finished.
1Prove statelessness, then fix it
2hTwo calls: tell it your name, then ask for it. Watch it fail. Then build the history loop correctly, and deliberately omit the assistant appends to see how the answers degrade.
Done when: You have seen both failures and can explain why the second one is subtle.
2Catch a silent truncation
2hSet max_tokens low and ask for something long. Confirm no exception is raised. Then add a stop-reason check and make it fail loudly.
Done when: Your client refuses to return a truncated response as if it were complete.
3Measure temperature honestly
2hRun one prompt ten times at temperature 0 and ten at 1.5. Then run a prompt with one obvious answer at both. Count distinct outputs in all four cases.
Done when: You have found a case where temperature 0 was not reproducible, and a case where temperature 2 changed nothing.
6 hours of practice
Interview questions
10 questions, grouped by the section that teaches the answer. An answer you have read is not an answer you can give, so use Test yourself and say it out loud first.
Making a call
What are the roles in a chat request and what is each for?
system carries your instructions and is not shown to the user. user is what the person said. assistant, called model by some providers, is what the model said on previous turns. The list is the whole conversation.
What is stop_reason and why check it?
It says why generation ended: finished naturally, hit the token limit, or requested a tool. Hitting the limit truncates the output without raising an exception, so without checking it you get silently incomplete data.
Does max_tokens make the model give shorter answers?
No. It is a hard cut-off, not an instruction. The model writes as though unconstrained and gets chopped mid-word. To get a shorter answer, ask for one in the prompt.
The model has no memory
Are LLM APIs stateful or stateless?
Stateless. The model retains nothing between calls. Conversation is created by the caller resending the full history each turn. Some products offer a session id, but underneath they are storing and resending the history for you.
Why does a long conversation get expensive faster than you would expect?
Every turn resends all previous turns, so total tokens grow with the square of the number of turns rather than linearly. Most of that resent text is the model's own previous output, which is typically longer than the user's messages.
Then they push further
Roughly how much faster?
If each turn adds a similar amount, total input over n turns grows with the square of n, because every turn resends everything before it. Twice as many turns costs roughly four times as much input.
What would you do about it?
Cache the stable prefix, trim or summarise older turns, and drop bulky tool results once they are no longer needed.
What gets marked up. Strong answers explain that the API is stateless so the whole history is sent every turn, and can reason about the growth.
How would you keep a long-running chat affordable?
Prompt caching on the stable prefix first, since it is free quality-wise. Then summarise or drop older turns, keeping a short running summary. Then retrieve only relevant history instead of carrying all of it. Increasing the context window is the last resort, not the first.
Parameters that change the output
What does temperature do?
It scales the probability distribution before a token is sampled. Low values concentrate probability on the most likely token, high values spread it out. It can only produce variety where the model was already uncertain.
Is temperature 0 deterministic?
Not on a hosted API. Batched GPU execution, non-associative floating point addition, expert routing and varying hardware all introduce small differences. This is why exact-match eval assertions on generated text flake.
Then they push further
If you need the same output for the same input, what do you do?
Cache results keyed by the exact input and settings, and design so small variations do not matter, for example by validating the output rather than comparing it to a stored string.
What gets marked up. Marked up for knowing hosted inference varies even at temperature 0, because of batching and floating point effects, and for designing around that rather than relying on it.
How do you make a model give shorter answers?
Ask for it in the prompt. max_tokens is a hard cut-off that truncates mid-sentence rather than an instruction to be concise. Keep max_tokens as a safety limit and check stop_reason to detect when it fires.
What belongs in the system prompt rather than the user message?
Anything constant across the conversation: role, output format, tone, and the rule for what to do when information is missing. Keeping it stable also lets prompt caching apply to it.
Resources
Free to watch and read, picked for this topic.
For making a call
For the model has no memory
For parameters that change the output
Full courses: the whole topic, start to finish
Read and explore
Stop 4 of 50 on the AI Engineer route
0 of 50 topics done · 0h behind you, 603h to go
What you have marked done is stored in this browser only. It does not reach a server, and clearing site data will clear it.