How LLMs Work: Tokens, Context Windows, Statelessness, and Reasoning
A developer's mental model of LLMs: tokens, next-token prediction, the context window, why the API is stateless, and how reasoning works, with runnable code.
On this page· Tokens: the unit of everything
Why this matters
You do not need to know how to train a model to build great AI features, but you do need an accurate mental model of what happens on each request. Almost every production surprise traces back to one of five facts: models read and write tokens, they generate one token at a time, everything they know about your task must fit in the context window, the API is stateless, and reasoning costs tokens too. This article builds that model with code you can run.
Tokens: the unit of everything#
A large language model does not see characters or words. It sees tokens: chunks of text from a fixed vocabulary, often a common word, part of a longer word, a space plus a word, or a punctuation mark. In English a token averages roughly three to four characters; code, non-English text, and unusual strings use more tokens per character.
Tokens matter because everything is measured in them: price (per million input and output tokens), the context window, output limits, rate limits, and latency (each output token takes time to generate). Never estimate with another provider's tokenizer; count with the API you will call.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const samples = [
"Hello, world!",
"Tokenization splits text into subword units.",
"const total = items.reduce((sum, item) => sum + item.price, 0);",
"Bonjour, comment ça va aujourd'hui ?",
];
for (const text of samples) {
const { input_tokens } = await client.messages.countTokens({
model: "claude-opus-5-5",
messages: [{ role: "user", content: text }],
});
console.log(`${String(input_tokens).padStart(3)} tokens · ${String(text.length).padStart(3)} chars · ${text}`);
}node --env-file=.env.local count-tokens.tsThe count includes a few tokens of message framing, so tiny strings look expensive. Run it on a real prompt from your app and you have the most important number for estimating cost.
Generation is a loop of next-token predictions#
Given all the tokens so far, the model computes a probability for every token in its vocabulary. One token is selected and appended, and the whole thing repeats. Step through it:
Prompt text
Next-token probabilities
- Paris
- a
- located
- the
Illustrative probabilities; real tokenizers split text differently.
Step 1. Your prompt arrives as plain text.
Show text descriptionHide text description
The animation steps through generating an answer to “The capital of France is”.
- The prompt arrives as text.
- A tokenizer splits it into tokens: The, capital, of, France, is.
- The model outputs probabilities for the next token: Paris 91%, a 3%, located 2%, the 2%.
- “Paris” is appended to the sequence.
- The model predicts again with the longer sequence: a period 78%, comma 12%.
- The period is appended.
- The next most likely token is the end-of-turn token (94%), so generation stops.
The probabilities are illustrative. Each appended token is one streamed chunk of output.
Four practical consequences follow:
- Output is streamed naturally. Each appended token can be sent to the user immediately, which is why streaming is the default for chat interfaces.
- Output length drives latency. A 1,000-token answer takes much longer than a 100-token one. Asking for concise answers is a performance optimization.
- Generation stops for a reason. The response's
stop_reasontells you which:end_turn(finished),max_tokens(your limit cut it off),tool_use(it wants a tool), orrefusal. Always check it. - Answers vary between runs. Selection is not always the single most likely token, so the same prompt can produce different wording. Design for this: validate structured output and evaluate on many samples, not one.
The context window#
The context window is the maximum number of tokens one request can involve: everything you send plus everything the model writes back. Current Claude models accept up to 1 million input tokens (Claude Haiku 4.5: 200,000) and can write up to 128,000 output tokens per response. Those limits are generous; the real constraint is that every input token costs money and time on every request.
- System prompt
- Tool definitions
- Conversation history
- Retrieved documents
- New user message
- Output budget (max_tokens)
Show text descriptionHide text description
Three representative requests, with token counts per part of the context. The counts are typical for well-built features, not limits; current models accept far larger contexts, but every input token costs money and latency.
| Request | System prompt | Tool definitions | Conversation history | Retrieved documents | New user message | Output budget (max_tokens) | Total |
|---|---|---|---|---|---|---|---|
| Simple chat, turn 3 | 800 | 0 | 1,400 | 0 | 120 | 2,000 | 4,320 |
| Support agent with tools | 2,400 | 3,200 | 6,500 | 0 | 180 | 4,096 | 16,376 |
| RAG answer with citations | 1,800 | 0 | 2,200 | 12,000 | 90 | 4,096 | 20,186 |
The model knows nothing about your product, your user, or the conversation except what is in this window. Deciding what goes in, and in what order, is context engineering, and it is most of the work in the LLM App Engineering section.
The API is stateless#
The model does not remember previous requests. A chat "remembers" because your app stores the conversation and sends the whole history again on every turn.
Step 1 of 9
“Hi, I'm Ada.”
The first message of a new conversation.
Show text descriptionHide text description
Three turns of a conversation. Each request to the model contains the entire conversation so far. Participants, left to right: User, Your app, LLM API.
- User → Your app: “Hi, I'm Ada.” · The first message of a new conversation.
- Your app → LLM API: messages: [user₁] (1 message) · The request contains only the first message.
- LLM API → Your app: “Hi Ada!” · The app saves the reply as assistant₁ in its database. The model keeps nothing.
- User → Your app: “What's my name?” · A follow-up that depends on the first turn.
- Your app → LLM API: messages: [user₁, assistant₁, user₂] (3) · The app sends the full history. Without it, the model could not know the name.
- LLM API → Your app: “Your name is Ada.” · Correct only because the history was included.
- User → Your app: “Summarize our chat.” · A third turn.
- Your app → LLM API: messages: [5 messages] (5) · The request grows every turn. Long chats eventually need trimming or summarizing, and prompt caching to keep cost down.
- LLM API → Your app: summary · Each answer costs input tokens for the whole history, not just the new message.
You can prove it in a few lines:
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
async function ask(messages: Anthropic.MessageParam[]) {
return client.messages.create({
model: "claude-opus-5-5",
max_tokens: 1024,
output_config: { effort: "low" },
messages,
});
}
const text = (message: Anthropic.Message) =>
message.content.flatMap((block) => (block.type === "text" ? [block.text] : [])).join("");
const intro: Anthropic.MessageParam = { role: "user", content: "Hi, my name is Ada." };
const first = await ask([intro]);
console.log("1:", text(first));
// A new request without history: the model has never heard of Ada.
const forgetful = await ask([{ role: "user", content: "What is my name?" }]);
console.log("2 (no history):", text(forgetful));
// The same question with the history resent. Append the assistant turn exactly as received.
const remembered = await ask([intro, { role: "assistant", content: first.content }, { role: "user", content: "What is my name?" }]);
console.log("3 (with history):", text(remembered));
console.log("input tokens grew from", first.usage.input_tokens, "to", remembered.usage.input_tokens);Statelessness has three consequences you will design around:
- Cost grows with conversation length, because each turn re-sends all earlier turns. Long chats need trimming or summarizing, and prompt caching makes the repeated prefix cheap.
- You own memory. What to keep, where to store it, and whose data it is are application decisions (covered in multi-turn chat and agent memory).
- Replay content blocks unchanged. Responses can contain reasoning and tool blocks that must be sent back exactly as received. Store and resend
response.content, not just the text.
Reasoning: thinking before answering#
Current models can think before answering: they generate internal reasoning tokens, then the final answer. With adaptive thinking the model decides how much reasoning a request needs; you steer the overall depth with effort (low, medium, high, xhigh, max). Higher effort usually improves hard tasks (multi-step problems, code, agentic tool use) and costs more output tokens and time; easy tasks such as classification or short chat replies do well at low.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const response = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 16000,
thinking: { type: "adaptive", display: "summarized" }, // show a readable summary of the reasoning
output_config: { effort: "high" },
messages: [
{
role: "user",
content:
"A bat and a ball cost $1.10 in total. The bat costs $1.00 more than the ball. " +
"How much does the ball cost? Answer in one sentence.",
},
],
});
for (const block of response.content) {
if (block.type === "thinking") console.log("[reasoning summary]", block.thinking);
if (block.type === "text") console.log("[answer]", block.text);
}
console.log(response.usage); // reasoning is billed as output tokensReasoning tokens count as output tokens. By default the raw reasoning is not shown (the thinking field comes back empty); display: "summarized" returns a readable summary, which is useful while developing and for showing progress in a UI.
What models know, and what they make up#
A model's built-in knowledge stops at its training cutoff and never includes your private data. When it lacks information it can still produce fluent, confident text, a hallucination. The engineering answer is not a better prompt alone: put the facts in the context (retrieval, tool results), ask for citations, allow "I don't know", and measure with evals.
Common mistakes#
- Estimating tokens by word count or another vendor's tokenizer. Use the token counting endpoint for the model you call.
- Assuming the model remembers. Every request must contain everything the model needs, including prior turns.
- Saving only the text of responses. Reasoning and tool blocks must be replayed unchanged; store the content blocks.
- Ignoring
stop_reason. A response cut off atmax_tokenslooks like a complete answer to code that only reads the text. - Maximum effort everywhere. It multiplies cost and latency on tasks that do not need it. Start low for simple tasks and raise effort only where evals show a gain.
Try it
Measure the cost of memory
- Run
stateless.tsand note howinput_tokensgrows between the first and third request. - Extend it into a loop of ten turns and print
input_tokensfor each request. Plot or tabulate the growth. - Estimate the input cost of turn 10 at $4 per million input tokens, then of a 100-turn conversation. Write down when you would start trimming history.
Check your understanding
Key takeaways
- Models read and write tokens; price, limits, and latency are all measured in tokens. Count them with the API.
- Generation is a next-token loop: it streams naturally, takes longer for longer output, and ends with a
stop_reasonyou must check. - The context window holds everything the model knows about your task; every input token costs money on every request.
- The API is stateless: your app stores history and resends it, replaying content blocks unchanged.
- Reasoning is extra output; steer it with
effortand measure where it pays off.
Next steps#
Finished this article? Track it to see your progress on the roadmap.
Related articles
- Choosing LLMs and Providers: Capability vs Cost vs LatencyHow to pick an LLM per feature by capability, cost, and latency: the current Claude lineup, effort levels, routing, and a harness to benchmark your own tasks.
- Your First LLM API Call in Node.js: Request and Response AnatomyMake your first Claude API call from Node.js and TypeScript, and learn every part of the request and response: messages, system, max_tokens, stop_reason, usage.