Skip to content
AI Full-Stack Wiki

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.

Difficulty: Beginner18 min readUpdated
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.

count-tokens.ts
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.ts

The 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:

1/8

Prompt text

The capital of France is

Next-token probabilities

  • Paris
  • a
  • located
  • the

Illustrative probabilities; real tokenizers split text differently.

Step 1. Your prompt arrives as plain text.

How an LLM generates text: the prompt is split into tokens, the model predicts a probability for every possible next token, one token is appended, and the loop repeats until an end-of-turn token.
Show text description

The animation steps through generating an answer to “The capital of France is”.

  1. The prompt arrives as text.
  2. A tokenizer splits it into tokens: The, capital, of, France, is.
  3. The model outputs probabilities for the next token: Paris 91%, a 3%, located 2%, the 2%.
  4. “Paris” is appended to the sequence.
  5. The model predicts again with the longer sequence: a period 78%, comma 12%.
  6. The period is appended.
  7. 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_reason tells you which: end_turn (finished), max_tokens (your limit cut it off), tool_use (it wants a tool), or refusal. 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)
Anatomy of the context window: every request carries the system prompt, tool definitions, conversation history, retrieved documents, and the new message, plus room reserved for the output. Typical token counts for three kinds of requests.
Show 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.

Tokens per context part
RequestSystem promptTool definitionsConversation historyRetrieved documentsNew user messageOutput budget (max_tokens)Total
Simple chat, turn 380001,40001202,0004,320
Support agent with tools2,4003,2006,50001804,09616,376
RAG answer with citations1,80002,20012,000904,09620,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.

Each request resends the growing conversationThe user sends three messages. For each one, the app sends the model every earlier message plus the new one, and saves the reply; request size grows with every turn.UserYour appLLM API“Hi, I'm Ada.”messages:[user₁] (1message)“Hi Ada!”“What's myname?”messages:[user₁,assistant₁,user₂] (3)“Your name isAda.”“Summarize ourchat.”messages: [5messages] (5)summary

Step 1 of 9

“Hi, I'm Ada.”

The first message of a new conversation.

Stateless chat: the model remembers nothing between requests, so your app stores the conversation and resends the growing history on every turn.
Show 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.

  1. User → Your app: “Hi, I'm Ada.” · The first message of a new conversation.
  2. Your app → LLM API: messages: [user₁] (1 message) · The request contains only the first message.
  3. LLM API → Your app: “Hi Ada!” · The app saves the reply as assistant₁ in its database. The model keeps nothing.
  4. User → Your app: “What's my name?” · A follow-up that depends on the first turn.
  5. Your app → LLM API: messages: [user₁, assistant₁, user₂] (3) · The app sends the full history. Without it, the model could not know the name.
  6. LLM API → Your app: “Your name is Ada.” · Correct only because the history was included.
  7. User → Your app: “Summarize our chat.” · A third turn.
  8. 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.
  9. 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:

stateless.ts
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:

  1. 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.
  2. 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).
  3. 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.

thinking.ts
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 tokens

Reasoning 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 at max_tokens looks 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

  1. Run stateless.ts and note how input_tokens grows between the first and third request.
  2. Extend it into a loop of ten turns and print input_tokens for each request. Plot or tabulate the growth.
  3. 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

Your second request to the model asks a follow-up question but does not include earlier messages. What happens?
A response's stop_reason is max_tokens. What does that mean?
How are reasoning (thinking) tokens billed?

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_reason you 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 effort and measure where it pays off.

Next steps#

Finished this article? Track it to see your progress on the roadmap.