Calling a model
Lesson 1 of 10
1Calling a model
The big pictureCalling a language model looks like one line, but behind it are four decisions that set your system's quality and cost: how you arrange the messages, which temperature you pick, how many tokens you send per request, and whether you ask for free text or structured data your code can check.
What the picture shows: The scores did not change, and temperature alone raised the chance of a wrong word from zero to 11%. In a system answering about a request's status, that is roughly one reply in nine saying "rejected" for an approved request, which is why systems answering from documents start with a low temperature.
Map of this part
The concepts in order, click any one to jump to it:
Two ways into the same idea
Calling a model from your program is like sending a message to an adviser and waiting for the reply. You set their role, write your question, and choose whether you want precise or creative. Every word costs money, and asking for the reply in a set form (like a form to fill) makes it easy for your program to read.
A request is a list of system, user and assistant messages; temperature controls randomness, max_tokens caps length, and cost is counted in input and output tokens. structured output binds the model to a JSON Schema you validate with a library like pydantic.
Before You Start
✂️Tokenisation
You know price and context limits are in tokens, not words, and that Arabic takes more tokens for the same meaning.
🔨Build a small transformer
You saw temperature divide the scores before softmax in generate, the same temperature you send here.
The Core Idea
Calling a model is one line, but four decisions sit behind it. In this lesson you build the messages and price them, send a real request, set the temperature, then ask for data your code can check.
1Messages and cost
A conversation is a list of messages, each with a role: system, then user and assistant in turns. The model has no memory, so every request resends the whole list. Look at the picture: the new question travels with everything before it.
Cost is counted in tokens: input tokens at their price, output tokens at theirs, and output is usually three to five times dearer.
messages = [
{"role": "system", "content": "You answer questions about the leave policy. Be brief."},
{"role": "user", "content": "How many days of annual leave do I get?"},
{"role": "assistant", "content": "21 working days per year."},
{"role": "user", "content": "And after five years?"},
]
def rough_tokens(text):
return max(1, round(len(text) / 4)) # about 4 characters per English token
prompt_tokens = sum(rough_tokens(m["content"]) + 4 for m in messages)
answer_tokens = 60
price_in, price_out = 0.50, 1.50 # dollars per million tokens, an example price
cost = prompt_tokens * price_in / 1e6 + answer_tokens * price_out / 1e6
print("turns sent:", len(messages))
print("prompt tokens ~", prompt_tokens)
print(f"cost of this call: ${cost:.6f}")
print(f"cost of 100,000 such calls: ${cost * 100_000:.2f}")
The fourth request sends all four messages. The estimate here is rough (four characters per English token, far fewer in Arabic as you saw in the tokenisation lesson), but multiplying by the number of requests is what matters: fractions of a cent become hundreds of dollars. Work this out before you choose a model, not after the bill arrives.
2A real call
Now you actually send the messages. Three things matter in every request: where the key comes from, the ceiling on the reply, and how many tokens you were billed for.
Most providers accept the same OpenAI-compatible shape. This needs an API key, so it is here for reading.
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.deepseek.com", api_key=os.environ["DEEPSEEK_API_KEY"])
reply = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "system", "content": "أجب عن أسئلة سياسة الإجازات باختصار."},
{"role": "user", "content": "كم يوم إجازة سنوية لي؟"}],
temperature=0.2,
max_tokens=200,
)
print(reply.choices[0].message.content)
print(reply.usage) # prompt_tokens, completion_tokens: what you are billed for
The key comes from an environment variable, not the code; max_tokens is a ceiling that protects you from a needlessly long reply; and usage is the real number you are billed for. Log it with every request from day one.
3Temperature
The model gives every possible next word a score, then turns the scores into probabilities with softmax. Temperature divides the scores first: near zero the strongest takes everything, and above 1 the probabilities flatten.
Look at the picture: as the temperature rises, approved falls and rejected climbs, and the two lines draw closer.
The same scores for five possible next words, at four temperatures.
import numpy as np
words = ["approved", "accepted", "pending", "rejected", "banana"]
logits = np.array([3.0, 2.6, 1.5, 1.0, -2.0])
def probs(t):
z = logits / t
e = np.exp(z - z.max())
return e / e.sum()
for t in [0.2, 0.7, 1.0, 1.5]:
p = probs(t)
row = " ".join(f"{w}={x:.2f}" for w, x in zip(words, p))
print(f"T={t}: {row}")
At 0.2, approved takes 88% and the rest nearly vanish. At 1.5, rejected reaches 11%: in a system answering about a request's status, that means roughly one reply in nine could say "rejected" for an accepted request. And notice banana never quite disappears at high temperature. That is why systems answering from documents start with a low temperature.
The working rule: 0 to 0.3 for answers from documents, and higher only for creative writing.
4Structured output
Code does not read prose. Ask for JSON with a defined schema, check every field and its type, and if validation fails, ask again with the reason.
Look at the picture: three attempts, each rejection goes back to the model with its reason, and the third passes.
Three possible replies from a model asked for JSON. The code validates each, rejecting and asking again with the reason.
import json
replies = [
'Sure! Here is the data: {"name": "Sara", "days": 3}',
'{"name": "Sara", "days": "three"}',
'{"name": "Sara", "days": 3, "type": "sick"}',
]
schema = {"name": str, "days": int, "type": str}
def parse(reply):
try:
data = json.loads(reply)
except json.JSONDecodeError as e:
return None, f"not JSON: {e.msg}"
for key, kind in schema.items():
if key not in data:
return None, f"missing field {key!r}"
if not isinstance(data[key], kind):
return None, f"{key!r} should be {kind.__name__}"
return data, None
for attempt, reply in enumerate(replies, 1):
data, error = parse(reply)
if data:
print(f"attempt {attempt}: ok -> {data}")
break
print(f"attempt {attempt}: rejected ({error}), asking again with the error")
The first reply has a polite preamble before the JSON, the second wrote the number in words. Both look "fine" to a person and break the code. Providers now offer a "JSON mode" or "structured outputs" that constrains generation to the schema, so use it where available, and keep validation in code anyway: the schema guarantees the shape, not the meaning.
How It Works, Step by Step: how one request to a model goes
Build the message list: the fixed system message first, then the earlier conversation, then the user's new question.
Send it with a low temperature and max_tokens as a ceiling on the reply, with the key from an environment variable.
Check the reply: if you asked for JSON, parse it and check every field and its type.
If validation fails, retry with the reason at most two or three times, and log usage with every request.
Worked example
The model remembers nothing between requests. Every request carries the whole conversation from the start: the system message that sets the role, then user and assistant messages taking turns. Which is why every old message you keep, you pay for again in every new request.
Try it without code
Myth vs Reality
| Myth | Reality |
|---|---|
| The model remembers the conversation between requests. | The model has no memory, and every request carries the whole conversation from the start, so you pay for every old message in every new request. |
| Temperature zero guarantees a correct answer. | A low temperature only makes the answer more consistent. If the most likely option is wrong, you get the same mistake every time. |
| A provider's JSON mode makes validation in code unnecessary. | The schema guarantees the shape, not the meaning. A days field can arrive with the right type and the wrong value, so keep validation in your code. |
Try it yourself
A system extracts a date and amount from invoice text. Which temperature?
Extraction has one right answer, and variety here means mistakes. A low temperature makes the model pick the most likely option every time.
The model returned well-formed JSON, but days holds "three". What do you do?
Validation in code is the line of defence. Retrying with the reason fixes most errors on the second attempt, and the limit prevents a costly loop.
Quick review cards
system, user and assistant roles. The model has no memory, so every request carries the whole conversation.Check Your Understanding
Why does the twentieth request in a long conversation cost much more than the first, though the user's question is the same length?
Because it carries all nineteen earlier exchanges as input tokens. A conversation's cost grows roughly with the square of the number of turns if you keep everything. The fix is in the context engineering lesson: keep the last few turns and a summary of the rest.
Why put the fixed instructions in the system message at the start of the request rather than the end?
Because many providers cache the repeated start of a request and bill it at a lower price, provided it is byte-identical across requests. Fixed part first, changing part last.
Resources for this lesson
Grouped by type. Start with the official docs for precision, or an article or video for the explanation.
Official docs1
Done with this lesson?
Sign in to save your progress.