← Back to Technical Library

Tokens and Context Windows Explained

From BPE to 1M-Token Contexts -- How LLMs Read, Remember, and Bill You

Tokens and Context Windows Explained

The Fundamental Units of LLM Communication -- And the Memory That Binds Them

📄 Technical Guide ⏱️ 25 min read 📂 Core AI Concepts

Every interaction you have with a large language model begins and ends with tokens. A token is not a word, not a character, and not a byte -- it is a sub-word unit that sits between the human-readable text and the model's internal numerical representations. The context window is the maximum number of tokens a model can process in a single forward pass, and it determines how much text the model can see at once. This guide covers tokenization algorithms (BPE, WordPiece, SentencePiece, Tiktoken), per-token pricing economics, context window mechanics across model generations, KV cache memory implications, the "lost in the middle" quality degradation phenomenon, and practical strategies -- including CLI and Python tooling -- for working within context limits.

TokensTokenizationContext WindowBPEWordPieceSentencePieceTiktokenKV CacheLLMPer-Token Pricing
🎯 Bottom Line: Tokens are the atomic currency of LLMs -- they determine what the model can read, how much it costs, and how much memory it consumes. A typical English word is 1.3-1.5 tokens. Context windows have grown from 2K tokens (GPT-3, 2020) to 1M+ tokens (Gemini, 2024), but longer context does not mean better quality -- the "lost in the middle" phenomenon means models attend best to the beginning and end of context, with degraded retrieval in the center. Always count tokens before sending, budget for both input and output pricing, and use chunking or retrieval-augmented generation (RAG) rather than blindly stuffing everything into context.

What Are Tokens?

A token is a sub-word unit of text -- a chunk that a language model reads as a single piece. Tokens are not words, not characters, and not bytes. They are the result of running text through a tokenizer, which splits text according to a vocabulary learned during model training. The tokenizer maps each token to a unique integer ID, and the model operates exclusively on those IDs.

The key insight is that token boundaries do not align with word boundaries. Common words like "the" or "cat" might be single tokens, while rare or complex words like "tokenization" might split into multiple tokens: "token" + "ization". This means the number of tokens in a text is not the same as the number of words -- it is usually less for common English, but can be dramatically more for non-Latin scripts, code, or unusual text.

The Rough Rule of Thumb

For English text processed by modern tokenizers (GPT-4's cl100k_base, Llama's tokenizer, etc.), the approximate conversion ratios are:

  • 1 token ≈ 4 characters of English text
  • 1 token ≈ 0.75 words (so 100 words ≈ 133 tokens)
  • 1 word ≈ 1.3 tokens on average
  • 1 token ≈ 1 syllable in English (very rough)
  • 1 line of code ≈ 10-20 tokens depending on length
  • 1 emoji ≈ 1-4 tokens (varies by tokenizer)
These Ratios Are English-Centric: Non-English languages tokenize very differently. CJK (Chinese, Japanese, Korean) characters often consume 2-3 tokens each because tokenizers were trained predominantly on English. A Japanese sentence might cost 3-5x more tokens than its English translation. Russian, Arabic, and Hindi also consume more tokens per word than English. This has real cost implications when using per-token API pricing.

Tokens Are Not Characters

Consider the word "unbelievable". A character-based approach would see 11 characters. A word-based approach would see 1 word. A sub-word tokenizer might see: "un" + "believe" + "able" = 3 tokens. The advantage of sub-word tokenization is that it handles rare words by decomposing them into meaningful parts the model has seen before, while keeping common words as single tokens for efficiency.

Visualizing Token Splits

Here is how different texts tokenize with a typical BPE tokenizer:

# Token visualization (cl100k_base / GPT-4 tokenizer) # Each | separates a token boundary "Hello" → 1 token: |Hello| "Hello world" → 2 tokens: |Hello| |world| "tokenization" → 3 tokens: |token| |ization| (sometimes |token| |iz| |ation|) "uncharacteristically" → 4 tokens: |un| |character| |istic| |ally| "I love artificial intelligence!" → 5 tokens: |I| |love| |artificial| |intelligence| |!| "The quick brown fox" → 4 tokens: |The| |quick| |brown| |fox| "supercalifragilistic" → 6+ tokens (rare word, splits aggressively) print("hello") → 4 tokens: |print| |(| |"|hello| |")| "こんにちは" (Konnichiwa) → 5 tokens: each Hiragana character → 1+ tokens
Why Sub-Words Instead of Words? A purely word-based vocabulary would need to store every word in every language -- potentially millions of entries, with no way to handle new words, typos, or compounds. A purely character-based vocabulary would be tiny (~256 for ASCII) but produce extremely long sequences that models struggle to learn from. Sub-word tokenization is the compromise: common words get single tokens (efficiency), rare words decompose into known sub-words (coverage), and the vocabulary stays manageable (typically 30K-200K tokens).

The Vocabulary

Every model has a fixed vocabulary -- a table mapping token strings to integer IDs. The vocabulary size varies by model:

Model Family Tokenizer Vocab Size Notes
GPT-3 p50k_base ~50,257 Original GPT-3 and text-davinci models
GPT-4 / GPT-3.5-turbo cl100k_base ~100,277 Improved CJK and code tokenization
GPT-4o o200k_base ~200,000 Major multilingual improvements
Llama 2 / Llama 3 SentencePiece BPE 32,000 / 128,000 Llama 3 doubled vocab for better multilingual support
Mistral / Mixtral BPE 32,000 Standard SentencePiece-based
Gemini SentencePiece ~256,000 Large vocab for multilingual efficiency
Claude (Anthropic) BPE ~100,000 Custom BPE variant
BERT / RoBERTa WordPiece 30,522 / 50,265 Encoder-only models, smaller vocab
Tokenizer and Model Are Inseparable: The tokenizer is trained before the model and is frozen during model training. You cannot swap tokenizers on a trained model -- the model's embedding layer and output projection are sized to the vocabulary. If you change the tokenizer, you change the meaning of every token ID, and the model's learned representations become meaningless. This is why different model families (GPT, Llama, Claude, Gemini) have entirely different tokenizers and different token counts for the same text.

How Tokenization Works

Tokenization is the process of converting raw text into a sequence of token IDs. It happens in three stages:

  1. Normalization -- Text is cleaned (whitespace handling, Unicode normalization, lowercasing for some models).
  2. Pre-tokenization -- Text is split into rough chunks (words, punctuation, whitespace) using regex rules.
  3. Sub-word splitting -- Each pre-token is further split using the model's vocabulary and merge rules (BPE), or vocabulary lookup (WordPiece), until all pieces are in the vocabulary.

The four major sub-word tokenization algorithms used in modern LLMs are:

  • BPE (Byte-Pair Encoding) -- GPT-4, Llama 3, Mistral, Claude
  • WordPiece -- BERT, DistilBERT, Electra
  • SentencePiece -- Llama 2, T5, ALBERT, Gemini (wraps BPE or Unigram)
  • Tiktoken -- OpenAI's optimized BPE implementation (GPT-3.5, GPT-4, GPT-4o)

The Pre-tokenization Regex

Before sub-word splitting, text is divided into chunks using a regex pattern. GPT-4's cl100k_base uses this pattern (simplified):

# GPT-4 pre-tokenization regex (cl100k_base) pattern = r"""(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\r\n\p{L}\p{N}]?\p{L}+|\p{N}{1,3}| ?[^\s\p{L}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+ # This splits text into chunks like: # "Hello" → ["Hello"] # "don't" → ["don", "'t"] # " world" → [" world"] (leading space stays with word) # "12345" → ["123", "45"] (numbers split into groups of 3)
Leading Spaces Matter: Many tokenizers include the leading space as part of the token. So " world" (with space) is a different token than "world" (without space). This is why you sometimes see different token counts for seemingly identical text -- whitespace handling varies. GPT-style tokenizers use the '深' prefix (GPT-2) or 'Ġ' character internally to represent leading spaces.

Byte-Pair Encoding (BPE)

Byte-Pair Encoding (BPE) is the dominant tokenization algorithm for modern LLMs. It was originally developed in 1994 as a data compression technique and adapted for neural language models by Sennrich et al. in 2015. GPT-2 popularized it, and today it powers GPT-4, Llama 3, Mistral, Claude, and most other major models.

How BPE Works

BPE training builds a vocabulary by iteratively merging the most frequent adjacent byte/character pairs:

  1. Start with a vocabulary of all individual characters (or bytes).
  2. Count all adjacent pairs in the training corpus.
  3. Merge the most frequent pair into a new token, add it to the vocabulary.
  4. Repeat steps 2-3 until the vocabulary reaches the target size.

At inference time, the tokenizer applies the same merge rules in order. Text is first split into characters/bytes, then merge rules are applied greedily in the order they were learned:

# BPE encoding example (simplified) # Vocabulary includes merges: e+r→er, er+ing→ering, etc. Input: "lowering" Step 1: [l, o, w, e, r, i, n, g] # split into characters Step 2: [l, o, w, er, i, n, g] # merge "e+r"→"er" Step 3: [l, o, w, er, ing] # merge "i+n+g"→"ing" (if in vocab) Step 4: [low, er, ing] # merge "l+o+w"→"low" (if in vocab) Final: 3 tokens: [low] [er] [ing] # If "lowering" were in the vocab as a single token, it would be 1 token. # The merge order determines which splits happen first.

Byte-Level BPE

GPT-2 and later models use byte-level BPE, which operates on raw bytes (256 possible values) rather than Unicode characters. This means the base vocabulary is always 256 bytes, and every possible text input can be tokenized -- there are no "unknown" tokens. This solves the out-of-vocabulary problem that plagued earlier word-level and character-level tokenizers. The trade-off is that non-ASCII characters (which require multiple bytes in UTF-8) consume more tokens.

Byte-Level BPE and CJK Costs: A Chinese character in UTF-8 is 3 bytes. With byte-level BPE, if that character's byte sequence was not merged during training (because it was rare in the training data), it stays as 3 tokens. This is why Chinese, Japanese, and Korean text often costs 2-3x more tokens than equivalent English text. GPT-4o's o200k_base tokenizer was specifically designed to address this, with a 200K vocabulary that includes many more multilingual merges.

WordPiece and SentencePiece

WordPiece

WordPiece is the tokenization algorithm used by BERT, DistilBERT, and Electra. It is similar to BPE but uses a different merge criterion. Instead of merging the most frequent pair, WordPiece merges the pair that maximizes the likelihood of the training corpus when added to the vocabulary. The result is a vocabulary that tends to favor sub-words that appear in more diverse contexts.

WordPiece uses a "##" prefix to denote continuation sub-words. For example, "playing" might tokenize as ["play", "##ing"], where "##ing" indicates that "ing" is a continuation of the previous piece, not a standalone word. This helps the model distinguish word-internal sub-words from standalone words.

# WordPiece tokenization example Input: "unaffable" Tokens: ["un", "##aff", "##able"] Input: "playing" Tokens: ["play", "##ing"] Input: "hamburger" Tokens: ["ham", "##bur", "##ger"] # The "##" prefix means "this piece continues the previous word" # Standalone "ing" (a word) → token "ing" # Continuation "ing" (in "playing") → token "##ing"
WordPiece Has [UNK] Tokens: Unlike byte-level BPE, WordPiece cannot represent every possible character. If the input contains characters not in the vocabulary, it produces an [UNK] (unknown) token. This means BERT cannot properly handle text with characters outside its training vocabulary -- a limitation that byte-level BPE (GPT-2+) and SentencePiece solve.

SentencePiece

SentencePiece is not a single algorithm -- it is a tokenization library and framework developed by Google that can implement either BPE or Unigram language model tokenization. Its key innovation is that it treats the input as a raw stream of Unicode characters without assuming word boundaries (no whitespace pre-splitting). This makes it language-agnostic and particularly well-suited for languages that do not use spaces between words (Chinese, Japanese, Thai).

  • Llama 2 uses SentencePiece with BPE, 32K vocab
  • Llama 3 uses SentencePiece with BPE, 128K vocab (tiktoken-compatible)
  • T5 uses SentencePiece with Unigram model, 32K vocab
  • ALBERT uses SentencePiece with a 30K Unigram vocab
  • Gemini uses SentencePiece, ~256K vocab

SentencePiece uses a special unicode character (U+2581, "LIGHT SHADE") internally to represent spaces, so that whitespace is part of the tokenization rather than a delimiter. This means "Hello world" and "Helloworld" tokenize differently even though there are no word boundaries -- the space is encoded into the tokens themselves.

Unigram Language Model Tokenization

The Unigram approach (used by T5 via SentencePiece) works differently from BPE. Instead of building up by merging, it starts with a large vocabulary and prunes tokens that least reduce the likelihood of the corpus. The final vocabulary is the set of sub-words that best explains the training data. At inference time, multiple segmentations are possible and the tokenizer picks the one with the highest probability (using a Viterbi-like search). This makes Unigram tokenization probabilistic -- the same text always tokenizes the same way given the vocabulary, but the algorithm considers multiple possible segmentations.

Tiktoken

Tiktoken is OpenAI's open-source, high-performance BPE tokenizer library, written in Rust with Python bindings. It is the tokenizer used for all OpenAI API models (GPT-3.5-turbo, GPT-4, GPT-4o, o1, etc.). Tiktoken is significantly faster than pure-Python BPE implementations -- typically 3-6x faster than HuggingFace's tokenizers library for the same vocabulary.

Tiktoken Encodings

Tiktoken uses different encoding files (called "encodings") for different model generations:

Encoding Models Vocab Size Key Improvements
r50k_base GPT-3 (text-davinci-002 and earlier) ~50,257 Original GPT-3 tokenizer
p50k_base GPT-3 (text-davinci-003, davinci), Codex ~50,281 Added code-specific tokens
cl100k_base GPT-3.5-turbo, GPT-4, GPT-4-turbo ~100,277 2x vocab, better CJK and code
o200k_base GPT-4o, GPT-4o-mini, o1, o3 ~200,000 2x vocab again, major multilingual gains
o200k_base Multilingual Improvement: OpenAI reported that o200k_base (GPT-4o) reduces token count for non-English languages by 1.1x to 4.2x compared to cl100k_base (GPT-4). For example, a Hindi text that took 100 tokens on cl100k_base takes ~28 tokens on o200k_base. This directly reduces API costs for non-English users -- a significant fairness and accessibility improvement.

Tiktoken Is Just BPE

Despite the name, Tiktoken does not introduce a new tokenization algorithm -- it is a byte-level BPE implementation with a specific set of merge rules and regex pre-tokenization. The "tik" refers to its speed (tick-tock, fast). The real innovation is in the vocabulary design (which merges were learned) and the engineering (Rust core, regex-based pre-tokenization, cached merge lookups).

Token Costs and Pricing Implications

Most LLM APIs charge per token, not per request, per word, or per character. This means understanding token counts is essential for cost estimation. Pricing is typically split into input tokens (the prompt you send) and output tokens (the response the model generates). Output tokens are usually 3-5x more expensive than input tokens because generation requires autoregressive decoding (one forward pass per token) while input processing is a single forward pass.

Per-Token Pricing Model

API providers price per 1,000 or per 1,000,000 tokens. Here is a comparison of major API pricing (as of mid-2026, approximate):

Model Input ($/1M tokens) Output ($/1M tokens) Context Window Output/Input Ratio
GPT-4o $2.50 $10.00 128K 4.0x
GPT-4o-mini $0.15 $0.60 128K 4.0x
GPT-4-turbo $10.00 $30.00 128K 3.0x
Claude 3.5 Sonnet $3.00 $15.00 200K 5.0x
Claude 3.5 Haiku $0.80 $4.00 200K 5.0x
Gemini 1.5 Pro $1.25 $5.00 2M 4.0x
Gemini 1.5 Flash $0.075 $0.30 1M 4.0x
Llama 3.1 405B (via API) $3.00-$5.00 $15.00 128K 3-5x
DeepSeek V3 $0.27 $1.10 128K 4.1x
Pricing Changes Frequently: API pricing drops regularly as competition increases and efficiency improves. The prices above are approximate as of mid-2026. Always check the provider's official pricing page before budgeting. Some providers also offer batch API pricing (50% off for async, non-real-time requests), cached input pricing (Anthropic and OpenAI offer 50-90% off for cached prompt prefixes), and tiered pricing based on volume.

Calculating Real Costs

To estimate the cost of an API call, you need to count both input and output tokens:

# Cost calculation example # GPT-4o: $2.50/1M input, $10.00/1M output Input tokens: 5,000 # your prompt Output tokens: 1,500 # model's response Input cost: 5,000 / 1,000,000 * $2.50 = $0.0125 Output cost: 1,500 / 1,000,000 * $10.00 = $0.0150 Total cost: $0.0275 per call # For 1,000 calls/day: Daily cost: $27.50 Monthly cost: ~$825/month # For 100,000 calls/day (production scale): Daily cost: $2,750 Monthly cost: ~$82,500/month

Hidden Token Costs

Several factors inflate token costs beyond the obvious prompt + response:

  • System prompts -- Every call includes a system prompt (often 200-500 tokens). If you send it on every call, it adds up. Use prompt caching to reduce this.
  • Conversation history -- In a chat application, you must resend the entire conversation on each turn. A 10-turn conversation might have 3,000-10,000 input tokens by the last turn.
  • Few-shot examples -- Including 3-5 examples in your prompt adds 500-2,000 tokens. Cache these if possible.
  • Output token limits -- You pay for all tokens the model generates, including any it generates before the visible response (reasoning tokens in o1/o3 models).
  • Tool/function definitions -- JSON schemas for tools consume tokens. A complex function-calling setup might add 300-1,000 input tokens.
  • Structural overhead -- JSON, XML, or markdown formatting in your prompt adds tokens. Minimize unnecessary formatting.
🎯 Cost Optimization Checklist: 1. Use prompt caching for static prefixes (system prompts, examples, documents). 2. Choose the cheapest model that meets your quality needs (GPT-4o-mini over GPT-4o when possible). 3. Trim conversation history -- summarize old turns instead of replaying them. 4. Set max_tokens to avoid runaway generation costs. 5. Use batch APIs for non-real-time workloads (50% discount). 6. Count tokens before sending to catch unexpectedly large inputs. 7. Prefer English when possible -- non-English text costs more tokens.

Context Window Explained

The context window is the maximum number of tokens a model can process in a single inference call. This includes all tokens: the system prompt, the user's input, any conversation history, tool definitions, few-shot examples, retrieved documents (in RAG), and the space reserved for the model's response. If the total exceeds the context window, something must be cut.

What Fits in a Context Window?

Practical estimates of what different context window sizes can hold (assuming ~1.3 tokens per English word, ~500 words per page):

Context Window Approx. Words Approx. Pages What Fits
4K ~3,000 ~6 pages A short email chain, a few pages of a document, a brief conversation
8K ~6,000 ~12 pages A full academic paper abstract + body, a medium-length article
16K ~12,000 ~24 pages A full research paper, a short book chapter, a substantial code file
32K ~24,000 ~48 pages Multiple papers, a novella, a large codebase section
128K ~98,000 ~196 pages An entire book, a full code repository, hours of transcripts
200K ~150,000 ~300 pages A large novel, a full legal case file, a complete API documentation set
1M ~750,000 ~1,500 pages Multiple books, an entire codebase, days of video transcripts
2M ~1,500,000 ~3,000 pages An entire encyclopedia volume, a massive codebase, weeks of transcripts
Context Window Is Input + Output: The context window includes the space for the model's response. If a model has a 128K context window and your prompt is 120K tokens, the model has only 8K tokens left for its response. Most APIs let you set max_tokens to control output length, but you must ensure: prompt_tokens + max_tokens ≤ context_window. Some APIs (OpenAI, Anthropic) handle this automatically by reducing available output space, but others will error if you exceed the total.

What Does NOT Fit

Even with a 128K context window, you cannot fit:

  • An entire enterprise codebase (millions of lines, tens of millions of tokens)
  • A full legal database or case law collection
  • More than ~2-3 average-length books
  • Large CSV/JSON datasets (10K+ rows become millions of tokens)
  • Years of conversation logs or chat histories
  • Complete video/audio transcripts beyond a few hours

For these use cases, retrieval-augmented generation (RAG) is the standard approach: store the full corpus in a vector database, retrieve only the relevant chunks, and include just those in the context window. This is both more cost-effective and often higher quality than stuffing everything into context.

Context Length by Model

Context windows have grown dramatically. The original GPT-3 (2020) had a 2K context window. By 2024, Gemini 1.5 Pro reached 2M tokens. This 1,000x increase has transformed what LLMs can do -- from reading a single paragraph to reading an entire codebase. Here is a comparison of popular models and their context windows:

Model Provider Context Window Max Output Tokenizer Released
GPT-3 (davinci) OpenAI 2,048 ~2,048 r50k_base 2020
GPT-3.5-turbo OpenAI 16,384 4,096 cl100k_base 2023
GPT-4 OpenAI 8,192 4,096 cl100k_base 2023
GPT-4-turbo OpenAI 128,000 4,096 cl100k_base 2023
GPT-4o OpenAI 128,000 16,384 o200k_base 2024
o1 / o3 OpenAI 200,000 100,000 o200k_base 2024-25
Claude 2 Anthropic 100,000 8,192 Custom BPE 2023
Claude 3 Opus/Sonnet Anthropic 200,000 4,096 Custom BPE 2024
Claude 3.5 Sonnet Anthropic 200,000 8,192 Custom BPE 2024
Gemini 1.0 Pro Google 32,768 8,192 SentencePiece 2023
Gemini 1.5 Pro Google 2,097,152 (2M) 8,192 SentencePiece 2024
Gemini 1.5 Flash Google 1,048,576 (1M) 8,192 SentencePiece 2024
Llama 2 (7B-70B) Meta 4,096 4,096 SentencePiece BPE 2023
Llama 3 (8B-70B) Meta 8,192 8,192 BPE (128K vocab) 2024
Llama 3.1 (8B-405B) Meta 131,072 (128K) 8,192 BPE (128K vocab) 2024
Mistral 7B / Mixtral Mistral AI 32,768 varies BPE (32K vocab) 2023-24
DeepSeek V3 DeepSeek 131,072 (128K) 8,192 BPE 2024
Qwen 2.5 Alibaba 131,072 (128K) 8,192 BPE 2024
Phi-3 Microsoft 128,000 4,096 Custom 2024
The Long-Context Race: The jump from 4K to 128K (2020-2023) was enabled by Rotary Position Embeddings (RoPE) scaling, FlashAttention, and ring attention. The jump to 1M-2M (2024) required more aggressive architectural changes: Gemini 1.5 uses a mixture of local and global attention layers plus efficient memory management. These are not simple parameter increases -- they involve fundamental changes to how attention is computed to avoid the quadratic memory blowup of standard self-attention.

What Happens When You Exceed the Context Window

When the total token count (input + requested output) exceeds the model's context window, something must give. Different APIs and inference engines handle this differently:

Strategy 1: Hard Error (API Rejection)

Most commercial APIs (OpenAI, Anthropic, Google) return an HTTP 400 error if you exceed the context window. The request never reaches the model. You get a message like:

{ "error": { "message": "This model's maximum context length is 128000 tokens. However, your messages resulted in 135000 tokens. Please reduce the length of the messages.", "type": "invalid_request_error", "code": "context_length_exceeded" } }

Strategy 2: Automatic Truncation

Some inference engines (llama.cpp, Ollama, certain API wrappers) silently truncate the input. They drop tokens from the beginning of the conversation (the oldest messages) until the input fits within the context window. This is common in chat applications built on local models. The user sees a response, but the model has "forgotten" the earliest parts of the conversation.

Silent Truncation Is Dangerous: If your system prompt or important context is at the beginning, truncation removes it without warning. The model will respond as if those instructions never existed. Always check for truncation in production code -- many bugs in LLM applications are caused by the model "forgetting" instructions that were silently truncated.

Strategy 3: Sliding Window

A sliding window approach keeps only the most recent N tokens of context. As new messages arrive, old messages fall off the end. This is how many chat UIs maintain a continuous conversation with a fixed context model. The model always sees a window of recent context, with older context discarded:

# Sliding window conversation management Turn 1: [System] [User1] [Assistant1] Turn 2: [System] [User1] [Assistant1] [User2] [Assistant2] Turn 3: [System] [User1] [Assistant1] [User2] [Assistant2] [User3] [Assistant3] ... Turn N: Context exceeds window → drop oldest messages # After truncation (keep last ~4000 tokens): Turn N: [System] [User(N-3)] [Assistant(N-3)] [User(N-2)] ... [UserN] ^-- oldest messages dropped, system prompt usually preserved

Strategy 4: Summarization / Compression

Instead of dropping old context, you summarize it. When the conversation approaches the context limit, the system takes the oldest messages, asks the model to summarize them, and replaces the originals with the summary. This preserves the key information while reducing token count:

# Summarization-based context management # Before (8,000 tokens): [System: You are a helpful assistant] [User: I'm building a Python web app...] [Assistant: I can help with that. What framework...] [User: I'm using Flask and I need to...] [Assistant: For Flask, you should...] [User: Now I want to add authentication...] [Assistant: For auth in Flask...] ... # After summarization (2,500 tokens): [System: You are a helpful assistant] [System: Previous conversation summary: User is building a Flask web app and has discussed routing, templates, and is now adding authentication. Key decisions: using Flask-Login, SQLAlchemy for ORM...] [User: How do I handle password hashing?] [Assistant: ...]

Strategy 5: RAG (Retrieval-Augmented Generation)

For large knowledge bases, RAG is the standard solution. Instead of putting all documents in context, you store them in a vector database, retrieve only the most relevant chunks for the current query, and include just those in the prompt. This scales to unlimited knowledge while keeping context window usage bounded:

# RAG pipeline (simplified) 1. User asks: "What is our refund policy for digital products?" 2. System embeds the query → vector 3. Vector DB searches for similar chunks → top 5 results 4. Prompt constructed: [System: Answer based on the provided context.] [Context: - Chunk from refund-policy.md (similarity: 0.92) - Chunk from terms-of-service.md (similarity: 0.87) - Chunk from FAQ.md (similarity: 0.85) - Chunk from digital-products.md (similarity: 0.82) - Chunk from customer-support.md (similarity: 0.79) ] [User: What is our refund policy for digital products?] 5. Model answers using only the retrieved context Total context: ~2,000-5,000 tokens (not the entire 50,000-page knowledge base)

KV Cache and Memory Implications

The KV cache (Key-Value cache) is the primary memory mechanism that makes autoregressive LLM inference feasible. Without it, generating a 1,000-token response would require re-processing all preceding tokens for every single new token -- a quadratic operation. The KV cache stores the intermediate attention computations (keys and values) for all processed tokens so they do not need to be recomputed.

How the KV Cache Works

In a transformer's self-attention layer, each token produces a query, key, and value vector. During generation, the model needs the keys and values of all previous tokens to compute attention for the current token. The KV cache stores these keys and values so only the current token's Q, K, V need to be computed:

  • Without KV cache: Generating token N requires re-computing attention for all N-1 previous tokens. Cost per token: O(N). Total cost for N tokens: O(N²).
  • With KV cache: Generating token N requires computing Q, K, V for only token N, then attending against the cached K, V of all previous tokens. Cost per token: O(1) compute + O(N) memory. Total cost for N tokens: O(N) compute, O(N) memory.

KV Cache Memory Calculation

The KV cache size grows linearly with context length. The formula is:

# KV cache memory per layer: KV_cache = 2 * n_heads * head_dim * n_tokens * batch_size * bytes_per_element # Total KV cache (all layers): Total_KV = KV_cache_per_layer * n_layers # Example: Llama 3 70B # n_layers = 80 # n_kv_heads = 8 (GQA - grouped query attention) # head_dim = 128 # bytes_per_element = 2 (FP16) # batch_size = 1 # KV cache per token: 2 * 8 * 128 * 80 * 2 = 327,680 bytes = 320 KB per token # For 128K context: 327,680 * 131,072 = ~40 GB of KV cache alone # For 4K context: 327,680 * 4,096 = ~1.2 GB of KV cache
Long Context Is Memory-Expensive: The KV cache for a 128K context on Llama 3 70B requires ~40 GB of VRAM -- more than the model weights themselves (~40 GB at FP16). This is why running long-context models locally is challenging. The model weights and KV cache together may exceed a single GPU's VRAM. Solutions include: Grouped Query Attention (GQA) to reduce KV heads, KV cache quantization (INT8 or INT4), PagedAttention (vLLM), and offloading KV cache to CPU RAM.

Techniques to Reduce KV Cache Memory

Technique Reduction Quality Impact Used By
Grouped Query Attention (GQA) 4-8x fewer KV heads Negligible Llama 3, Mistral, Gemma
Multi-Query Attention (MQA) Single KV head Slight PaLM, Falcon
KV Cache Quantization (INT8) 2x reduction Minimal vLLM, llama.cpp
KV Cache Quantization (INT4) 4x reduction Moderate vLLM (experimental)
PagedAttention Reduces fragmentation None vLLM
Sliding Window Attention Fixed cache size Limits long-range deps Mistral, Gemma 2
Ring/Block Attention Distributes across GPUs None Internally by providers
KV Cache Offloading Uses CPU RAM Latency increase llama.cpp, Ollama
PagedAttention -- The vLLM Innovation: PagedAttention (introduced by vLLM in 2023) manages the KV cache like virtual memory in an OS. Instead of allocating a contiguous block for each request's KV cache, it uses fixed-size "pages" (blocks) that can be scattered across GPU memory. This eliminates fragmentation and allows much higher batch sizes -- vLLM can serve 2-4x more concurrent requests than naive batching on the same hardware. This is why vLLM is the most popular inference server for production LLM deployments.

How Context Length Affects Quality -- Lost in the Middle

A larger context window does not guarantee better retrieval or understanding. The "lost in the middle" phenomenon, documented by Liu et al. (2023), shows that models perform best at retrieving information from the beginning and end of the context, with significantly degraded performance for information placed in the middle.

The U-Shaped Performance Curve

In the original study, researchers placed a relevant fact at different positions within a long context and measured the model's ability to retrieve it. The results showed a U-shaped curve:

  • Position 0-10% (beginning): ~75-80% retrieval accuracy
  • Position 45-55% (middle): ~50-55% retrieval accuracy
  • Position 90-100% (end): ~75-80% retrieval accuracy

This means that even with a 128K context window, placing a critical document in the middle of the context can result in a 25-30% drop in retrieval accuracy compared to placing it at the beginning or end.

This Is Not Just a GPT-4 Problem: The lost-in-the-middle phenomenon has been observed across GPT-4, Claude, Gemini, Llama, Mistral, and open-source models. While newer models (especially those with 1M+ context) have improved, the effect persists. Google's own benchmarks for Gemini 1.5 Pro show that "needle in a haystack" retrieval is near-perfect (>99%) up to ~100K tokens but degrades at longer contexts, especially with multiple needles.

Why It Happens

Several factors contribute to lost-in-the-middle:

  • Attention dilution -- With more tokens, attention scores are distributed across more candidates. The softmax over thousands of tokens makes it harder for any single middle token to stand out.
  • Positional bias in training -- Training data often has important information at the beginning (instructions) or end (questions, conclusions). Models learn this distributional bias.
  • Recency and primacy effects -- Similar to human memory, models exhibit stronger attention to the first and last items in a sequence.
  • Attention sink -- The first few tokens (especially the BOS token) act as "attention sinks" that absorb disproportionate attention, creating a beginning bias.

Mitigating Lost in the Middle

  1. Put critical information first or last -- Place the most important context at the beginning (after system prompt) or at the end (right before the user's question).
  2. Use RAG instead of stuffing -- Retrieve only relevant chunks rather than dumping 100K tokens. Fewer, more relevant tokens mean better attention.
  3. Reorder retrieved documents -- If using RAG, place the most relevant documents at the beginning and end of the context, less relevant ones in the middle.
  4. Repeat key instructions -- If a critical instruction must be followed, state it at the beginning and end of the context.
  5. Use structured prompts -- XML/JSON tags, headers, and delimiters help the model locate information regardless of position.
  6. Chunk and map-reduce -- Process long documents in chunks and combine results, rather than sending the entire document at once.
🎯 The Reordering Strategy: If you must include many documents, order them by relevance in a "butterfly" pattern: most relevant first, second most relevant last, third most relevant second, fourth most relevant second-to-last, and so on. This places the highest-relevance content at the positions where the model attends best (beginning and end) and lower-relevance content in the middle where attention is weakest.

Practical Strategies for Working Within Context Limits

1. Know Your Token Budget

Before sending any request, calculate: system_prompt_tokens + user_input_tokens + conversation_history_tokens + max_output_tokens. This must be ≤ the model's context window. Use a token counter (see the CLI and Python sections below) to measure each component.

2. Use Prompt Caching

Both OpenAI and Anthropic now offer prompt caching. If you send the same prompt prefix (system prompt, documents, examples) across multiple calls, the provider caches the KV cache for that prefix. Subsequent calls with the same prefix are charged at 50-90% less and are significantly faster. This is essential for applications with large static contexts.

# Anthropic prompt caching example import anthropic client = anthropic.Anthropic() response = client.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=1024, system=[ { "type": "text", "text": "You are an expert legal analyst. Here is the full case file: " + large_legal_document, # ~50,000 tokens "cache_control": {"type": "ephemeral"} # cache this! } ], messages=[ {"role": "user", "content": "What is the statute of limitations?"} ] ) # First call: full price for 50K input tokens # Subsequent calls (within 5 min): 90% discount on cached tokens # $3.00/1M → $0.30/1M for cached input tokens

3. Implement Conversation Memory Management

For chat applications, implement a memory manager that:

  • Always preserves the system prompt (never truncate it)
  • Keeps the most recent N turns verbatim
  • Summarizes older turns into a compact summary block
  • Drops the oldest summarized content if still over budget
  • Uses function calling to offload facts to a database (long-term memory)

4. Use RAG for Knowledge Bases

For any knowledge base larger than the context window, use RAG. The standard pipeline:

  1. Chunk documents into 500-1000 token pieces with overlap
  2. Embed each chunk using an embedding model (text-embedding-3-small, etc.)
  3. Store embeddings in a vector database (Pinecone, Weaviate, Qdrant, Chroma)
  4. Retrieve top-K chunks for each query (K = 3-10 typically)
  5. Construct a prompt with only the retrieved chunks
  6. Generate the answer grounded in the retrieved context

5. Choose the Right Context Length

Do not always use the maximum context. Longer context costs more (both compute and money), is slower, and may suffer quality degradation. Match context to the task:

Task Recommended Context Strategy
Simple Q&A, chat 4K-8K Use a smaller/cheaper model. Minimal context needed.
Document summarization (1 doc) 16K-32K Fit the full document. One-pass summary.
Code analysis (single file) 8K-32K Most files fit in 32K. Large files may need chunking.
Multi-document RAG 16K-32K Retrieve 5-10 chunks, include in context. Not all docs.
Full codebase analysis 128K+ Or use agentic RAG with multiple retrieval steps.
Long document Q&A (book-length) 128K-200K Fit the full document. Beware lost-in-the-middle.
Entire codebase / massive corpus 1M-2M (Gemini) or agentic Use Gemini 1.5 Pro for in-context, or multi-step RAG.

6. Minimize Token Waste

  • Avoid redundant instructions -- Do not repeat the same guidance in the system prompt and the user message.
  • Use concise formatting -- Markdown adds fewer tokens than verbose JSON. Avoid unnecessary whitespace.
  • Trim documents -- Remove boilerplate, headers, footers, and navigation text from documents before including them.
  • Pre-process code -- Remove comments and blank lines if the task does not need them. Minify where appropriate.
  • Use references -- Instead of including full documents, include section references and retrieve on demand.
  • Batch similar queries -- If you have multiple questions about the same document, ask them in one call rather than re-sending the document each time.

CLI Tools for Counting Tokens

Tiktoken CLI

The most common tool for counting OpenAI-model tokens is tiktoken. Install it and use it from the command line:

# Install tiktoken $ pip install tiktoken # Count tokens for a string (cl100k_base = GPT-4/GPT-3.5-turbo) $ python -c "import tiktoken; enc=tiktoken.get_encoding('cl100k_base'); print(len(enc.encode('Hello, world!')))" 6 # Count tokens from a file $ python -c " import tikencoder enc = tiktoken.get_encoding('cl100k_base') with open('document.txt') as f: text = f.read() tokens = enc.encode(text) print(f'Token count: {len(tokens)}') print(f'Char count: {len(text)}') print(f'Ratio: {len(text)/len(tokens):.2f} chars/token') " # For GPT-4o, use o200k_base: $ python -c "import tiktoken; enc=tiktoken.get_encoding('o200k_base'); print(len(enc.encode('Hello, world!')))" # For a specific model (auto-selects encoding): $ python -c "import tiktoken; enc=tiktoken.encoding_for_model('gpt-4o'); print(len(enc.encode('Hello, world!')))"

HuggingFace Transformers Tokenizer

For non-OpenAI models (Llama, Mistral, BERT, etc.), use the HuggingFace transformers library:

# Install transformers $ pip install transformers # Count tokens for Llama 3 $ python -c " from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained('meta-llama/Meta-Llama-3-70B') text = 'Hello, world! This is a test.' tokens = tokenizer.encode(text) print(f'Token count: {len(tokens)}') print(f'Tokens: {tokens}') print(f'Decoded: {tokenizer.convert_ids_to_tokens(tokens)}') " # Count tokens for Mistral $ python -c " from transformers import AutoTokenizer tok = AutoTokenizer.from_pretrained('mistralai/Mistral-7B-v0.3') print(len(tok.encode('Hello, world!'))) " # Count tokens for BERT (WordPiece) $ python -c " from transformers import AutoTokenizer tok = AutoTokenizer.from_pretrained('bert-base-uncased') print(tok.tokenize('unbelievable tokenization')) # Output: ['un', '##bel', '##ievable', 'token', '##ization'] "

Ollama Token Counting

If you run models locally with Ollama, you can count tokens using the Ollama API:

# Count tokens using Ollama's API $ curl -s http://localhost:11434/api/tokenize -d '{ "model": "llama3.1", "prompt": "Hello, world! This is a test." }' | python -c "import sys,json; d=json.load(sys.stdin); print(f'Tokens: {len(d[\"tokens\"])}')" # Or using the Ollama Python library $ python -c " import ollama response = ollama.tokenize(model='llama3.1', prompt='Hello, world!') print(f'Tokens: {len(response[\"tokens\"])}') "

Quick Shell One-Liners

# Approximate token count (rough: chars / 4) $ wc -c document.txt | awk '{print int($1/4) " approximate tokens"}' # Word count to token estimate (words * 1.3) $ wc -w document.txt | awk '{print int($1*1.3) " approximate tokens"}' # Exact count with tiktoken for a file $ python -c "import tiktoken; print(len(tiktoken.get_encoding('cl100k_base').encode(open('doc.txt').read())))" # Count tokens for all .txt files in a directory $ for f in *.txt; do python -c "import tiktoken; print('$f:', len(tiktoken.get_encoding('cl100k_base').encode(open('$f').read())))" done

Code Examples for Token Counting in Python

Basic Tiktoken Usage (OpenAI Models)

import tiktoken # Get the encoder for a specific model # This auto-selects the correct encoding enc = tiktoken.encoding_for_model("gpt-4o") # enc = tiktoken.encoding_for_model("gpt-4") # cl100k_base # enc = tiktoken.encoding_for_model("gpt-3.5-turbo") # cl100k_base text = "The quick brown fox jumps over the lazy dog." # Encode text to token IDs token_ids = enc.encode(text) print(f"Token IDs: {token_ids}") # [791, 4062, 11739, 18719, 16268, 683, 264, 41128, 465, 13] print(f"Token count: {len(token_ids)}") # Token count: 10 # Decode back to text decoded = enc.decode(token_ids) print(f"Decoded: {decoded}") # "The quick brown fox jumps over the lazy dog." # See the actual token strings (with special chars for spaces) for tid in token_ids: print(f" {tid}: {repr(enc.decode([tid]))}")

Counting Tokens for API Calls

import tiktoken def count_message_tokens(messages, model="gpt-4o"): """Count tokens in a chat messages list (OpenAI format).""" enc = tiktoken.encoding_for_model(model) # OpenAI adds ~3 tokens of overhead per message (role, content keys) # Plus 3 tokens for the assistant's reply priming num_tokens = 3 # priming tokens_per_message = 3 # overhead per message for message in messages: num_tokens += tokens_per_message for key, value in message.items(): num_tokens += len(enc.encode(value)) if key == "role": num_tokens += 1 # role token overhead return num_tokens # Example usage messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "What is the capital of France?"}, {"role": "assistant", "content": "The capital of France is Paris."}, {"role": "user", "content": "What about Germany?"}, ] total = count_message_tokens(messages, "gpt-4o") print(f"Total input tokens: {total}") # Check if it fits in context window context_window = 128000 max_output = 4096 if total + max_output <= context_window: print(f"Fits! ({total} + {max_output} = {total + max_output} <= {context_window})") else: print(f"Too large! Need to trim {(total + max_output) - context_window} tokens")

HuggingFace Tokenizer (Non-OpenAI Models)

from transformers import AutoTokenizer # Load tokenizer for any HuggingFace model tokenizer = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3.1-8B") text = "The quick brown fox jumps over the lazy dog." # Method 1: encode + count token_ids = tokenizer.encode(text) print(f"Token count: {len(token_ids)}") # Method 2: direct call (returns BatchEncoding) result = tokenizer(text) print(f"Token count: {len(result['input_ids'])}") # See token strings tokens = tokenizer.convert_ids_to_tokens(token_ids) print(f"Tokens: {tokens}") # Decode back decoded = tokenizer.decode(token_ids) print(f"Decoded: {decoded}") # Count tokens for a chat-formatted prompt # (Llama 3 uses a specific chat template) messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "What is 2+2?"}, ] formatted = tokenizer.apply_chat_template(messages, tokenize=False) token_count = len(tokenizer.encode(formatted)) print(f"Chat template token count: {token_count}")

Context Budget Manager

import tiktoken from typing import List, Dict class ContextBudgetManager: """Manage token budget for LLM API calls.""" def __init__(self, model="gpt-4o", context_window=128000, max_output=4096, reserve_system_prompt=200): self.encoder = tiktoken.encoding_for_model(model) self.context_window = context_window self.max_output = max_output self.reserve_system_prompt = reserve_system_prompt def count_tokens(self, text: str) -> int: return len(self.encoder.encode(text)) def count_messages_tokens(self, messages: List[Dict]) -> int: total = 3 # priming tokens for msg in messages: total += 3 # message overhead total += len(self.encoder.encode(msg.get("role", ""))) total += len(self.encoder.encode(msg.get("content", ""))) return total def available_for_context(self, system_prompt: str) -> int: """How many tokens available for user content.""" sys_tokens = self.count_tokens(system_prompt) return self.context_window - self.max_output - sys_tokens - 10 def trim_conversation(self, messages: List[Dict], system_prompt: str) -> List[Dict]: """Trim conversation to fit context window.""" system_tokens = self.count_tokens(system_prompt) budget = self.context_window - self.max_output - system_tokens - 20 # Keep messages from most recent, working backwards kept = [] used = 0 for msg in reversed(messages): msg_tokens = self.count_messages_tokens([msg]) if used + msg_tokens > budget: break kept.insert(0, msg) used += msg_tokens trimmed = len(messages) - len(kept) if trimmed > 0: print(f"Trimmed {trimmed} oldest messages ({used} tokens used)") return kept def estimate_cost(self, input_tokens: int, output_tokens: int, input_price: float, output_price: float) -> float: """Estimate API cost in USD. Prices per 1M tokens.""" return (input_tokens / 1_000_000 * input_price + output_tokens / 1_000_000 * output_price) # Usage manager = ContextBudgetManager( model="gpt-4o", context_window=128000, max_output=4096 ) system_prompt = "You are a helpful coding assistant." print(f"Available for context: {manager.available_for_context(system_prompt)} tokens") conversation = [ {"role": "user", "content": "How do I read a file in Python?"}, {"role": "assistant", "content": "Use open() or pathlib..."}, {"role": "user", "content": "What about async file reading?"}, {"role": "assistant", "content": "Use aiofiles for async..."}, ] trimmed = manager.trim_conversation(conversation, system_prompt) total_tokens = manager.count_messages_tokens(trimmed) + manager.count_tokens(system_prompt) cost = manager.estimate_cost(total_tokens, 500, 2.50, 10.00) print(f"Estimated cost: ${cost:.4f}")

Comparing Tokenizers Across Models

import tiktoken from transformers import AutoTokenizer text = "The quick brown fox jumps over the lazy dog. Tokenization is fascinating!" # OpenAI tokenizers for enc_name in ["r50k_base", "cl100k_base", "o200k_base"]: enc = tiktoken.get_encoding(enc_name) count = len(enc.encode(text)) print(f"{enc_name:15s}: {count:4d} tokens") # HuggingFace tokenizers for model_name in ["bert-base-uncased", "meta-llama/Meta-Llama-3.1-8B", "mistralai/Mistral-7B-v0.3"]: try: tok = AutoTokenizer.from_pretrained(model_name) count = len(tok.encode(text)) print(f"{model_name:40s}: {count:4d} tokens") except Exception as e: print(f"{model_name:40s}: ERROR - {e}") # Expected output (varies slightly): # r50k_base : 13 tokens # cl100k_base : 13 tokens # o200k_base : 13 tokens # bert-base-uncased : 14 tokens # meta-llama/Meta-Llama-3.1-8B : 13 tokens # mistralai/Mistral-7B-v0.3 : 13 tokens
Token Counts Differ Across Models: The same text produces different token counts on different models because each model has its own vocabulary and merge rules. A text that is 1,000 tokens on GPT-4 (cl100k_base) might be 1,050 tokens on Llama 3, 980 tokens on GPT-4o (o200k_base), or 1,100 tokens on BERT. Always count with the tokenizer matching your target model -- never assume counts transfer between models.

References & Citation

Source: Avondale.AI Technical Documentation Library
Category: Core AI Concepts
Type: Technical Reference Guide
License: CC BY 4.0

Further Reading

  • Tiktoken: github.com/openai/tiktoken -- OpenAI's fast BPE tokenizer
  • SentencePiece: github.com/google/sentencepiece -- Google's language-agnostic tokenizer
  • HuggingFace Tokenizers: huggingface.co/docs/transformers -- Tokenizer documentation
  • Sennrich et al. (2015). "Neural Machine Translation of Rare Words with Subword Units." arXiv:1508.07909 -- Original BPE for NMT paper
  • Kudo & Richardson (2018). "SentencePiece: A simple and language independent subword tokenizer and detokenizer for Neural Text Processing." arXiv:1808.06226
  • Schuster & Nakajima (2012). "Japanese and Korean Voice Search." -- Original WordPiece paper
  • Liu et al. (2023). "Lost in the Middle: How Language Models Use Long Contexts." arXiv:2307.03172 -- The key lost-in-the-middle study
  • Kwon et al. (2023). "Efficient Memory Management for Large Language Model Serving with PagedAttention." arXiv:2309.06180 -- vLLM/PagedAttention paper
  • Ainslie et al. (2023). "GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints." arXiv:2305.13245 -- GQA paper
  • OpenAI Tokenizer: platform.openai.com/tokenizer -- Interactive tokenizer tool
  • OpenAI Pricing: openai.com/api/pricing -- Current API pricing
  • Anthropic Prompt Caching: docs.anthropic.com -- Prompt caching
  • Google Gemini Context: ai.google.dev -- Long context documentation
  • Radford et al. (2019). "Language Models are Unsupervised Multitask Learners." -- GPT-2 paper (introduced byte-level BPE for LLMs)
  • Devlin et al. (2018). "BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding." arXiv:1810.04805 -- WordPiece tokenization

Suggested Citation:
Tokens and Context Windows Explained. Avondale.AI Technical Documentation Library. Accessed June 2026. https://avondale.ai/technical/tokens-context-windows.html