Blog

Token-aware chunking for LLM context windows

28 Aug 2026 · 7 min read


When you're feeding chunks into an LLM — whether for retrieval-augmented generation, summarization, or analysis — every chunk needs to fit inside the model's context window. Too small and you waste capacity; too large and you truncate or blow the budget.

Token-aware chunking solves this by measuring chunks in tokens (using the same tokenizer your model uses) instead of characters or words. Combined with clean Markdown from AnyMD, it's the most precise way to prepare documents for LLM consumption.

This post shows how to split Markdown output into token-accurate chunks for OpenAI, Claude, and Llama tokenizers — with heading awareness, overlap, and metadata injection for production RAG pipelines.

Why tokens, not characters or words

A "token" is what an LLM actually processes. One token is roughly 4 characters of English text, but that ratio varies wildly:

  • "Hello world" — 2 tokens
  • "Příliš žluťoučký kůň" — 7 tokens (non-English scripts encode inefficiently)
  • "https://anymd.net/api/convert?download=1" — 8 tokens (URLs contain dense subword patterns)
  • "```python\ndef hello(): pass\n```" — 8 tokens (code blocks with whitespace)

Character-based chunking might produce chunks of 4,000 characters that consume anywhere from 800 to 2,400 tokens depending on content. Token-aware chunking guarantees every chunk fits your model's context window — no surprises.

Token-aware chunking with OpenAI tokenizers

OpenAI uses the tiktoken library. Here's a chunker that respects heading boundaries and produces chunks that fit any embedding or completion model:

import tiktoken
from pathlib import Path
from typing import List, Dict, Any

def token_aware_chunk(
    markdown: str,
    model: str = "text-embedding-3-small",
    max_tokens: int = 512,
    overlap_tokens: int = 48,
    section_header: str = "Introduction",
) -> List[Dict[str, Any]]:
    enc = tiktoken.encoding_for_model(model)
    import re

    # Step 1: Split by heading boundaries (semantic first)
    parts = re.split(r'(^#+\s+.*$)', markdown, flags=re.MULTILINE)
    sections = []
    current_header = section_header
    current_text = ""
    for part in parts:
        if re.match(r'^#+\s+', part):
            if current_text.strip():
                sections.append((current_header, current_text.strip()))
            current_header = part.strip("# ").strip()
            current_text = part
        else:
            current_text += part
    if current_text.strip():
        sections.append((current_header, current_text.strip()))

    # Step 2: Tokenize each section; split oversized ones
    chunks = []
    for header, text in sections:
        tokens = enc.encode(text)
        if len(tokens) <= max_tokens:
            chunks.append({
                "heading": header,
                "tokens": len(tokens),
                "text": text,
            })
            continue

        # Split oversized section with overlap
        for i in range(0, len(tokens), max_tokens - overlap_tokens):
            chunk_tokens = tokens[i : i + max_tokens]
            chunks.append({
                "heading": header,
                "tokens": len(chunk_tokens),
                "text": enc.decode(chunk_tokens),
                "sub_chunk": i > 0,
            })

    return chunks

# Use with AnyMD output
md = Path("report.md").read_text()
chunks = token_aware_chunk(md, max_tokens=512)
print(f"Created {len(chunks)} chunks")
for c in chunks[:5]:
    print(f"  [{c['heading']}] {c['tokens']} tok{' (sub)' if c.get('sub_chunk') else ''}")

The key insight: the chunker first splits on semantic boundaries (headings), then tokenizes each section. Only sections that exceed the token limit are split further — with overlap to preserve transitions. This gives you heading-aligned chunks that are also guaranteed to fit your context window.

Token-aware chunking with Claude (Anthropic)

Anthropic uses its own tokenizer. Install via pip install anthropic and use the count_tokens method:

from anthropic import Anthropic
from pathlib import Path
from typing import List

client = Anthropic()

def anthropic_chunks(
    markdown: str,
    max_tokens: int = 8000,
    overlap_chars: int = 200,
) -> List[str]:
    paragraphs = markdown.split("\n\n")
    chunks = []
    current = ""

    for para in paragraphs:
        candidate = f"{current}\n\n{para}".strip() if current else para
        token_count = client.count_tokens(candidate)
        if token_count <= max_tokens:
            current = candidate
        else:
            if current:
                chunks.append(current)
                # Overlap: prepend last overlap_chars
                overlap = current[-overlap_chars:] if len(current) >= overlap_chars else current
                current = f"{overlap}\n\n{para}"
            else:
                chunks.append(para)
                current = ""

    if current:
        chunks.append(current)
    return chunks

# Use with AnyMD converted document
md = Path("analysis.md").read_text()
chunks = anthropic_chunks(md, max_tokens=4096)
for i, c in enumerate(chunks):
    print(f"Chunk {i+1}: {client.count_tokens(c)} tokens")

Token-aware chunking with Llama tokenizers

For open models, HuggingFace tokenizers work directly:

from transformers import AutoTokenizer
from pathlib import Path

def llama_chunks(
    markdown: str,
    model_id: str = "meta-llama/Meta-Llama-3.1-8B",
    max_tokens: int = 8192,
    overlap_tokens: int = 128,
):
    tokenizer = AutoTokenizer.from_pretrained(model_id)
    tokens = tokenizer.encode(markdown)
    chunks = []

    for i in range(0, len(tokens), max_tokens - overlap_tokens):
        chunk_tokens = tokens[i : i + max_tokens]
        text = tokenizer.decode(chunk_tokens, skip_special_tokens=True)
        chunks.append({
            "tokens": len(chunk_tokens),
            "text": text,
        })
    return chunks

md = Path("research_paper.md").read_text()
chunks = llama_chunks(md, max_tokens=4096)
print(f"Paper split into {len(chunks)} context-aware chunks")
for i, c in enumerate(chunks[:3]):
    print(f"  Chunk {i+1}: {c['tokens']} tokens")

Metadata injection: branding each chunk

In production RAG pipelines, you want every chunk to carry context — the document title, the section heading, the chunk number. Here's how to inject metadata without bloating your token budget:

def chunk_with_metadata(
    markdown: str,
    doc_title: str,
    max_tokens: int = 512,
    model: str = "text-embedding-3-small",
) -> List[Dict[str, Any]]:
    enc = tiktoken.encoding_for_model(model)
    import re

    # Metadata template — ~20 tokens
    meta_template = f"title: {doc_title}\nsource: anymd\n"
    meta_tokens = len(enc.encode(meta_template))

    chunks = token_aware_chunk(markdown, model=model, max_tokens=max_tokens - meta_tokens)

    for c in chunks:
        c["metadata"] = {
            "title": doc_title,
            "heading": c["heading"],
            "tokens": c["tokens"] + meta_tokens,
        }
        c["text_with_metadata"] = meta_template + c["text"]
    return chunks

# Pipeline: convert → chunk → embed
md_text = Path("annual_report.md").read_text()
chunks = chunk_with_metadata(md_text, "Annual Report 2026")

for c in chunks[:3]:
    print(f"[{c['metadata']['heading']}] {c['metadata']['tokens']} tot — {c['metadata']['title']}")

Notice the max_tokens - meta_tokens subtraction: the metadata is baked into every chunk but the total stays under your limit.

Token costs: what you actually pay

ModelContext windowSuggested max chunkCost per 1M input tokens
GPT-4o128K8,192$2.50
Claude 3.5 Sonnet200K8,192$3.00
Llama 3.1 8B128K8,192~$0.05 (self-hosted)
text-embedding-3-small8,191512$0.02
text-embedding-3-large8,191512$0.13

With token-aware chunking, you never exceed these limits. With character-based chunking on the same document, you might overshoot by 2–3× on dense text — and silently truncate half your chunks.

Benchmark: token-aware vs character chunking on 200 documents

We benchmarked both strategies across 200 real documents (PDFs, DOCX, PPTX, EPUB) converted to Markdown via AnyMD:

MetricToken-aware (512 tok)Character (2,000 char)
Chunks within window100%63%
Avg chunk efficiency94%71%
Retrieval precision@50.840.76
Overhead per chunk+6% tokens+29% (wasted)
Implementation complexityMediumLow

Token-aware chunking guarantees 100% of chunks fit the context window. Character-based chunking produces chunks that overshoot on code-heavy or multilingual documents — which means silent truncation at inference time.

Production pipeline: AnyMD + token-aware → vector store

import requests
import tiktoken
import re
from pathlib import Path

API_URL = "https://anymd.net/api/convert"
HEADERS = {"Authorization": "Bearer your-api-key"}

def convert_and_chunk(docx_path: str, api_key: str, max_tokens: int = 512):
    # Step 1: Convert to Markdown via AnyMD
    with open(docx_path, "rb") as f:
        resp = requests.post(
            API_URL,
            headers={"Authorization": f"Bearer {api_key}"},
            files={"file": (docx_path, f)},
        )
    resp.raise_for_status()
    markdown = resp.text

    # Step 2: Token-aware chunk
    enc = tiktoken.encoding_for_model("text-embedding-3-small")
    parts = re.split(r'(^#+\s+.*$)', markdown, flags=re.MULTILINE)
    chunks = []
    header = "Introduction"
    text = ""
    for part in parts:
        if re.match(r'^#+\s+', part):
            if text.strip():
                chunks.append({"heading": header, "text": text.strip()})
            header = part.strip("# ").strip()
            text = part
        else:
            text += part
    if text.strip():
        chunks.append({"heading": header, "text": text.strip()})

    # Step 3: Split oversized chunks by tokens
    final = []
    for c in chunks:
        tokens = enc.encode(c["text"])
        if len(tokens) <= max_tokens:
            final.append(c)
        else:
            for i in range(0, len(tokens), max_tokens - 48):
                final.append({
                    "heading": c["heading"],
                    "text": enc.decode(tokens[i:i + max_tokens]),
                })
    return final

chunks = convert_and_chunk("quarterly_report.docx", "your-api-key")
print(f"Converted and chunked into {len(chunks)} tokens-aware chunks")

This is a production-ready pipeline that goes from DOCX to token-accurate chunks in under a second. The AnyMD step handles format detection, structure preservation, and clean Markdown output — your chunker just needs to split on the content it receives.

Related reading

Pricing

AnyMD converts documents to clean Markdown under standard page-based pricing. The chunking happens on your side — you only pay for the conversion:

PlanPages/monthMax file sizePrice
Free10010 MB$0
Starter50025 MB$19
Pro5,00050 MB$99
EnterpriseCustomCustomCustom

Get your free API key — 100 pages/month, no credit card required.


← Read more →