Chat PDF: asking questions about a PDF through an AI API
Two ways to question a PDF with a model: put the document in context and cache it, or retrieve the relevant parts. Python code and the price of twenty questions.
6 min · · автоматизация · API keys
A chat PDF AI does a simple thing: you upload a document and ask questions in plain words. Ready-made services of this kind exist, but if you have many documents, they are confidential or you need custom logic, it is easier to build it yourself via the API. There are two fundamentally different approaches: put the whole document into the model's context and cache it, or search for the relevant fragments and send only those. We go through both in Python and calculate the cost of a series of questions.
Chat PDF with AI: step 0, extract text from the PDF
If the PDF has a text layer, extract the text with page numbers so the model can cite them:
from pypdf import PdfReader
def pdf_pages(path):
reader = PdfReader(path)
return [(i, (p.extract_text() or "").strip())
for i, p in enumerate(reader.pages, 1)]
pages = pdf_pages("report.pdf")
empty = [n for n, t in pages if len(t) < 20]
print("pages without text:", len(empty))If there are many empty pages, you have a scan. Then you need either recognition or sending pages as images, covered in detail in how to recognize a PDF with AI. Word documents and Excel spreadsheets reduce to the same scheme: python-docx extracts text from .docx, and openpyxl or pandas reads sheets from .xlsx. After that everything is the same: you get text with labels (page, sheet) and pass it to the model.
Approach A. The whole document in context plus cache
Zukko models have a 1M-token context (GLM 5.3 has 128K, Grok has 256K), so a document of a hundred pages fits whole. Measure the size in advance with count_tokens (see the reference):
import os
import anthropic
client = anthropic.Anthropic(base_url="https://api.zukko.pro",
api_key=os.environ["ZUKKO_API_KEY"])
DOC = "\n\n".join(f"[p. {n}]\n{t}" for n, t in pages)
MODEL = "claude-sonnet-5-5"
n = client.messages.count_tokens(
model=MODEL, messages=[{"role": "user", "content": DOC}]).input_tokens
print("tokens in document:", n)Now the chat itself. The document goes into system with a cache marker, and questions are appended to the history. The whole repeating prefix (document plus earlier turns) must stay byte for byte identical:
SYSTEM = [
{"type": "text",
"text": "Answer only from the document below. After each statement "
"give the page as [p. N]. If the document has no answer, "
"write: not found in the document."},
{"type": "text", "text": DOC, "cache_control": {"type": "ephemeral"}},
]
history = []
def ask(question):
history.append({"role": "user", "content": question})
msg = client.messages.create(model=MODEL, max_tokens=1000,
system=SYSTEM, messages=history)
answer = msg.content[0].text
history.append({"role": "assistant", "content": answer})
u = msg.usage
print("fresh input:", u.input_tokens,
"from cache:", getattr(u, "cache_read_input_tokens", 0),
"written:", getattr(u, "cache_creation_input_tokens", 0))
return answerOn the first question you will see a cache write, on the second and later a cache read. The cache lives for a limited time and is extended on access; see Anthropic's documentation for exact conditions (including the minimum prefix length). If the pause between questions is long, the cache is written again. More on billing: prompt caching: reads and writes in Zukko. The Claude cache discount comes specifically from the Anthropic format; in the OpenAI format all Claude input is billed at the input price.
Pros of this approach: the model sees the whole document, so you can ask questions like "what are the contradictions between sections". Cons: the entire document goes into every request, and the longer it is, the more tokens you pay for on each question.
Approach B. Retrieval by parts
Split the document into fragments, find those relevant to the question and send only them. Without a vector database, search can be done with TF-IDF:
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from openai import OpenAI
chunks = [(n, t) for n, t in pages if t]
vec = TfidfVectorizer(analyzer="char_wb", ngram_range=(3, 5))
matrix = vec.fit_transform([t for _, t in chunks])
oa = OpenAI(base_url="https://api.zukko.pro/v1",
api_key=os.environ["ZUKKO_API_KEY"])
def ask_rag(question, k=5):
scores = cosine_similarity(vec.transform([question]), matrix)[0]
top = scores.argsort()[::-1][:k]Character n-grams are chosen because word-level TF-IDF copes poorly with Russian morphology. This is the simplest variant; for complex documents vector search is used, as in the RAG pipeline. The weak point of the approach: questions whose answer is spread across the whole document ("summarize all sections") may be missed by the search.
What to choose
| A: document in context | B: retrieval by parts | |
|---|---|---|
| Questions about the whole document | Good | Weak |
| Pinpoint questions | Good | Good |
| Document size | Limited by context | Any |
| Price of a series of questions | Grows with document size | Almost independent of size |
| Complexity | Minimal | Search needed |
A practical order: start with approach A for documents up to a hundred pages, and move to B for large archives and dozens of files.
What a series of questions costs
Formula: (fresh input × input price + cache read × read price + cache write × write price + output × output price) / 1,000,000. Assumptions (for simplicity we ignore the growth of the dialogue history): a 60,000-token document (measure yours with count_tokens), 20 questions of 300 tokens, answers of 400 tokens (8,000 output tokens). Zukko prices at October 18, 2026.
claude-sonnet-5-5, input 0.4, cache read 0.05, cache write 0.5, output 2 ($ per 1M):
- Without cache: 20 × 60,300 = 1,206,000 tokens × 0.4 / 1,000,000 = 0.4824, plus output 8,000 × 2 / 1,000,000 = 0.016. Total about $0.50.
- With cache (Anthropic format): write 60,000 × 0.5 / 1,000,000 = 0.03; reads 19 × 60,000 = 1,140,000 × 0.05 / 1,000,000 = 0.057; questions 20 × 300 = 6,000 × 0.4 / 1,000,000 = 0.0024; output 0.016. Total about $0.105.
- Approach B (5 fragments, about 2,800 tokens per question): 56,000 × 0.4 / 1,000,000 + 0.016 = about $0.038.
gpt-6.1-sol (input 0.27, cache read 0.013, output 1.32): with no cache hits, 1,206,000 × 0.27 / 1,000,000 + 8,000 × 1.32 / 1,000,000 = 0.3256 + 0.0106, about $0.34. If the cache worked from the second question: first question 60,300 × 0.27 / 1,000,000 = 0.0163, reads 1,140,000 × 0.013 / 1,000,000 = 0.0148, remaining input 5,700 × 0.27 / 1,000,000 = 0.0015, output 0.0106; total about $0.043. For GPT, cache reads are visible in both formats, but a write is not charged separately.
deepseek-v4.1-flash (input 0.013, output 0.038, cache read 0.013): 1,206,000 × 0.013 / 1,000,000 + 8,000 × 0.038 / 1,000,000 = 0.0157 + 0.0003, about $0.016. There is no cache discount here, but the price is low anyway.
The conclusion is simple: with a long document and a series of questions the gap between "no cache" and "with cache" is a multiple, so do not forget it. But from the numbers and the strength index (Sonnet 5.5 has 48, GPT-6.1 Sol 52, DeepSeek V4.1 Flash 39) you cannot tell who answers better on your PDF; test on ten of your own questions. Current prices: GET https://api.zukko.pro/v1/models.
Connecting
Create a key in the dashboard and use the format you need: the Anthropic SDK with base URL https://api.zukko.pro or the OpenAI SDK with https://api.zukko.pro/v1. Top-ups start at $1: SBP (the payment page adds 8.5% on top), cryptocurrency or a MasterCard.
FAQ
Can I load confidential documents into such a chat?
The text goes to the model provider. Decide in advance what may be sent, anonymize data and read the providers' terms; we make no claims about them.
Why does the model answer "not found" when the answer is in the PDF?
Either the text was not extracted (a scan, complex layout, tables), or with retrieval the right fragment did not make it into the selection. Check the extracted text and increase the number of fragments.
How do I avoid invented answers?
Require a page reference and check quotes with code; treat an answer without a reference as invalid.
Which AI should analyze a PDF file?
For the test, take three models from different price groups and ten real questions about your documents. Decide by result and price.
zukko
Pay 25×
less
for Claude Code & ChatGPT
One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.
Related articles
- DeepSeek API OCR: extracting text from images with V4 ProDeepSeek V4 Pro accepts images, so it can read text from screenshots and scans. Python code, a JSON mode, accuracy checks and a cost calculation for 1,000 pages.
- Knowledge base in Open WebUI: RAG setup on your own documentsOpen WebUI lets you upload documents and ask questions about them. We show how to create a knowledge base, which settings affect answer accuracy, and what a single question costs.
- Website analysis with AI: auditing pages, texts and structure via APICollect page data with a script, send the model compact JSON and get a report on headings, meta tags and text structure. With a cost estimate and honest limits.
- OpenAI API Keys in the Open: Why You Should Not Use ThemWhere free keys on GitHub come from, why using them is dangerous for you and for the owner, and how to protect your own key from the same leak.