Requests and messages
Send a real request to a model from the provider of your choice, then make the surrounding program trustworthy: constrain the output, validate it, handle streaming and failures, and keep cost bounded.
THE IDEA
A request is just a small structured message
Calling a model is an ordinary web request. You send a block of JSON to a URL with your key in a header, and you get a block of JSON back. Nothing else is hidden behind it. Four things are always in that JSON: which model, the messages (the conversation so far), optional system instructions, and an output limit (the most tokens the reply may contain).
Providers agree on the ideas and disagree on the spelling. Change the provider family below and watch the same four ingredients move to different places: Anthropic wants max_tokens and a top-level system; OpenAI-style servers put the system text in the message list; Gemini wraps everything in parts and calls the assistant model.
Edit the request
What goes over the wire
POST https://api.anthropic.com/v1/messages
x-api-key: $ANTHROPIC_API_KEY
anthropic-version: 2023-06-01
content-type: application/json
{
"model": "claude-haiku-4-5",
"max_tokens": 120,
"system": "You are a concise tutor.",
"messages": [
{
"role": "user",
"content": "What is an API key?"
}
]
}| Idea | In this family |
|---|---|
| Output limit | max_tokens (required) |
| System instruction | top-level system |
| Model's own turns are called | assistant |
| Stop reason: finished / hit limit | end_turn / max_tokens |
The same request as SDK or curl code (read only)
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY
message = client.messages.create(
model="claude-haiku-4-5", # Model
max_tokens=120, # max output tokens
system="You are a concise tutor.", # System prompt
messages=[{"role": "user", "content": "What is an API key?"}], # User message
)
print(message.content[0].text)
print(message.stop_reason) # end_turn or max_tokens
print(message.usage.input_tokens, message.usage.output_tokens)Update the controls above and this code changes with them. Run it yourself with the key in an environment variable.The model remembers nothing between calls
A chat app feels like a conversation with someone who remembers. The API does not. Each request stands alone, so the app must resend the whole conversation every turn, and the input you pay for grows with it. Roles label who said what: user for you, assistant (or model on Gemini) for earlier replies, and system instructions for standing rules.
Your chat window
Nothing yet. Send a message.
The replies here are canned, only to show the structure. No model is called.
What the next request must carry
[
{
"role": "user",
"content": "My name is Ada."
}
]Turns the model can see: 1 · estimated input tokens (system plus messages, about 4 characters each): 10.
The reply: text, a stop reason and usage
The response carries the generated text, a stop reason and usage counts for input and output tokens. The output limit is a ceiling, not a target. If the model finishes before it, you get a natural stop. If it reaches the ceiling, it is cut off at that token, even in the middle of a word.
stop reason: max_tokens The reply was cut off at your limit and may end mid-sentence.
| Provider family | finished | hit the limit |
|---|---|---|
| Anthropic Messages | end_turn | max_tokens |
| OpenAI chat completions | stop | length |
| Google Gemini generateContent | STOP | MAX_TOKENS |
| Groq / Mistral / local (OpenAI-compatible) | stop | length |
You pay for input and for the output actually written (here 120), not for the limit. Some models also spend part of the limit on hidden reasoning, so a small limit can leave little or no visible text.
Predict first. You set the limit to 50 tokens and the model wanted to write 120. The reply text is a clean, grammatical paragraph. Is it a complete answer?
Chapter context
A model API turns input messages into generated output, but the program around it stays responsible for validation, retries and side effects. Every major provider (Anthropic, OpenAI, Google, Mistral, Groq, OpenRouter and local OpenAI-compatible servers) exposes the same few ideas under different field names: a model id, a list of messages with optional system instructions, an output-token limit, and a response carrying the text, a reason it stopped, and token usage. The labs in this chapter run against whichever provider you choose with your own key, so you see real behavior rather than a mock.
TRY IT YOURSELF
Part 1. Send a real request
Bring your own key from Anthropic, OpenAI, Google, Mistral, Groq, OpenRouter, xAI, DeepSeek, or point the Custom option at a local OpenAI-compatible server. The key stays in your browser and goes straight to the provider. With no key, the lab shows a clearly labelled sample response so you can still follow along.
After you get a complete reply, lower the limit to 8 and send again. Notice the stop reason, then raise it back before pressing Check.
TRY IT YOURSELF
Send your first request
Choose any provider, send one real request, then read the three things that matter in the response: the text, the stop reason and the token usage.
Get a complete reply (stop reason end_turn) of 24 or fewer output tokens. Then lower the limit until the reply is cut off, and watch the stop reason change.
This request as code
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY
message = client.messages.create(
model="claude-haiku-4-5", # Model
max_tokens=64, # max output tokens
messages=[{"role": "user", "content": "Reply with a greeting for a new learner in at most six words."}], # User message
)
print(message.content[0].text)
print(message.stop_reason) # end_turn or max_tokens
print(message.usage.input_tokens, message.usage.output_tokens)Update the controls above and this code changes with them. Run it yourself with the key in an environment variable.No key yet, so this shows one sample response for the starting prompt. Add a key above to experiment for real.
Part 2. Code: read three providers' replies the same way
- Three providers return the same ideas under different names. Write
parse_reply(provider, resp)that returns a dict withtext,complete,input_tokensandoutput_tokens. providerisanthropic,openaiorgemini. Anthropic:contentblocks,stop_reason,usage.input_tokens/output_tokens. OpenAI-style:choices[0].message.content,finish_reason,usage.prompt_tokens/completion_tokens. Gemini:candidates[0].content.parts,finishReason,usageMetadata.completeis True only when the model stopped on its own (end_turn/stop_sequence,stop,STOP). A limit, a tool call or a filter is not complete. An unknown provider raisesValueError.- Gemini thinking models report reasoning in
thoughtsTokenCount; it counts against the output limit, so add it to the output tokens.
Both parts must be solved to finish this lesson.
Stuck, or something not working? You can come back to it any time.
CHECK YOUR UNDERSTANDING
A reply ends with a stop reason meaning "hit the output limit" (`max_tokens`, `length` or `MAX_TOKENS`). Is the text a complete answer?
Chapter reference: math, worked example & code
This section belongs to the whole chapter, so it is the same on every lesson in it and may cover a different lesson's topic. The code for the experiment you just ran is inside the experiment itself.
Math and notation
Four small rules recur through the chapter.
- Complete or cut off (F8.1): treat the reply as an answer only when the provider's stop reason means a natural stop (
end_turn,stop,STOP), not a limit (max_tokens,length,MAX_TOKENS). - Validation order (F8.2-F8.4): parse, then check fields, then check domain rules. Passing one stage says nothing about the next.
- Backoff (F8.5): wait before retry , where is the base delay and the jitter factor is a random number in (or without jitter). Retry only temporary errors, and stop after a fixed number of retries.
- Cost (F8.6): for prices and per million input and output tokens, and token counts and :
With an output cap , the worst-case output side for calls is . The prices below are invented arithmetic inputs, not a product recommendation; use the provider's current price list for real budgeting.
Worked example
One small helper per idea, all checked offline. A reply stopped with end_turn is usable but one stopped with max_tokens is not. The text {"sentiment": "neutral", "confidence": 0.7} passes all three validation stages, while a confidence of 7 or a reply with chatty text around the JSON is rejected. A 429 is retried but a 401 is not, and the delays for retries 0, 1 and 2 at a 0.5 s base are 0.5, 1.0 and 2.0 seconds. At illustrative prices of 6 per million tokens, a call with 1,000 input and 500 output tokens costs (1000×2 + 500×6)/1,000,000 = $0.005.
Reference code
The following example uses the Python standard library and does not call a remote service. It is the same logic the labs let you try against a real model.
import json
from math import isclose
def usable(stop_reason): # F8.1: a limit means the text may be cut off
return stop_reason in {"end_turn", "stop", "STOP"}
def validate(text): # F8.3: syntax, then fields, then rules
data = json.loads(text) # raises ValueError on bad syntax
if data.get("sentiment") not in {"positive", "negative", "neutral"}:
raise ValueError("bad sentiment")
if not 0 <= data.get("confidence", -1) <= 1:
raise ValueError("confidence out of range")
return data
def should_retry(status, attempt, max_retries=3): # F8.5: temporary errors only, bounded
return status in {429, 529} and attempt < max_retries
def delay(attempt, base=0.5, jitter=1.0): # F8.5: exponential backoff
return base * 2 ** attempt * jitter
def cost(n_in, n_out, p_in=2, p_out=6): # F8.6: illustrative prices
return (n_in * p_in + n_out * p_out) / 1_000_000
assert usable("end_turn") and not usable("max_tokens") and not usable("length")
assert validate('{"sentiment": "neutral", "confidence": 0.7}')["confidence"] == 0.7
for bad in ['{"sentiment": "neutral", "confidence": 7}', 'Sure! {"sentiment": "neutral"}']:
try:
validate(bad)
except ValueError:
pass
else:
raise AssertionError("accepted bad output")
assert should_retry(429, 0) and not should_retry(401, 0) and not should_retry(429, 3)
assert [delay(a) for a in range(3)] == [0.5, 1.0, 2.0]
assert isclose(cost(1000, 500), 0.005)
assert isclose(cost(1000, 500) * 10_000, 50)
Python examples are provided for study and local execution; this page does not execute Python.
Common mistakes
JSON output can still be false or out of range. A model call is not required for every rule-based task. Never place a reusable private API credential in a public browser bundle: the labs here use a key you paste yourself, kept in your browser and sent only to the provider, and a shipped product would call the provider from a server instead. Reasoning models can spend part of the output limit on hidden reasoning, so a tiny limit may return empty text.