01
The one-paragraph version
A modern assistant does not do PDF → text → model. It does PDF → (text layer ∥ page images) → model, in parallel, and the model sees both.
The text carries exact strings and reading order. The pixels carry layout, diagrams, handwriting, stamps, table rules, and everything else a text extractor throws away. Retrieval is a scaling strategy applied on top when a document is too large to show whole — it is not the primary way a file gets read. And the heavy lifting that is genuinely deterministic — parsing, decoding, rendering — stays in ordinary software, because a parser is cheaper, faster and more reliable than a model at work that has a correct answer.
02
What happens when you upload a PDF
Anthropic documents its PDF pipeline in two sentences that settle the central question, so they are worth quoting exactly rather than paraphrasing:
“The system converts each page of the document into an image. The text from each page is extracted and provided alongside each page’s image.”
Google documents the same design for Gemini — every page is rasterised to a screenshot and OCR’d, which is precisely why scanned PDFs work there without any preparation PublishedGemini document processing. OpenAI’s Responses API accepts a PDF as a first-class input_file part carrying a render-quality detail knob, which only means something if pages are being rendered Publishedopenai-openapi, InputFileContentParam.
Three vendors, three independent specifications, one shape: dual channel. Text and pixels, per page, together. If you remember one thing from this page, that is the thing.
The cost of the second channel is documented too. The visual path runs roughly seven times the token cost of text alone — on the order of 1,500–3,000 text tokens per page plus a full image’s worth of tokens per page PublishedPDF support — “Estimate your costs”. Bedrock exposes both modes side by side so a caller can choose, which is the clearest public evidence that these are two genuinely different architectures rather than one pipeline with a flag.
The walkthrough
A 42-page PDF is uploaded, and the question is “what does page 37 say about the database architecture?”
| Stage | What happens | Who does it |
|---|---|---|
| Upload and store | Bytes to object storage. An id, a name, a size, a checksum. Nothing is read. | Deterministic |
| Type detection | Magic bytes — not the extension, and not the browser’s declared Content-Type. | Deterministic |
| Parse | Object graph, page tree, content streams. Yields page count, geometry, embedded fonts, text-show operators. | Deterministic |
| Text extraction | Text runs with x/y positions and font metrics. Reading order is reconstructed from geometry — it is not stored in the file. | Deterministic |
| Layout analysis | Runs into lines by baseline, lines into paragraphs by gap; columns, tables, headers and footers detected. | Deterministic, sometimes ML |
| Page rendering | Each page to an image at roughly 150 DPI. The second channel. | Deterministic |
| OCR | Only for pages with no text layer. A fallback, never the default. | Specialised ML |
| Structural extraction | Blocks — heading, paragraph, list item, table, code, caption, image — each with a page number and a bounding box. | Deterministic + ML |
| Indexing | Only when the document is too large to show whole, or when searching across documents. | ML |
| Context construction | For “page 37” this is a metadata filter, not a semantic search. Page 37’s blocks and page 37’s image go in the prompt. | Deterministic |
| Generation | The model reads text and pixels together and answers, citing the page. | Model |
So is it PDF → text → model?
No — and the failure of a system built that way is not subtle. A scanned page yields the empty string. A diagram yields its labels with the arrows removed. A two-column paper yields two columns interleaved line by line. Microsoft Copilot is the public cautionary case: a text-extraction pipeline with a character ceiling that fails outright on image-only PDFs PublishedCopilot Studio document handling.
03
What happens when you upload an image
An image takes a different route, and one step in it surprises almost everyone.
- 01Decode — Bytes to a pixel array. The format is sniffed from magic bytes, not the file name.
- 02Resize — Providers downscale before inference. Anthropic documents a maximum of 8000×8000 px and roughly 1,590 tokens for a 1.15 megapixel image; Gemini documents 258 tokens per tile. This is why a 4 % crop of a 4000 px screenshot is useless: it is 160 px before the provider shrinks it again.
- 03Patchify — The image is cut into fixed-size patches — typically 14×14 or 16×16 pixels — each linearly projected into an embedding.
- 04Vision encoder — A ViT-family transformer processes the patch sequence. Self-attention across patches is what encodes spatial relationship: this patch is left of that one, this arrow starts here and ends there.
- 05Projection — Visual embeddings are mapped into the language model’s embedding space, becoming tokens it attends over alongside text tokens.
- 06Joint reasoning — From the model’s point of view there is one sequence. It does not call a vision module; the picture is already in its context as vectors.
PublishedThe token arithmetic is documented — Anthropic vision and Gemini. InferredSteps 3 to 5 are the standard published vision-language architecture — CLIP, Flamingo, LLaVA, Qwen-VL — and the vendors’ token counts are consistent with fixed-size patching. Not publicWhether the frontier models implement exactly this is not public.
Why “what button is in the top right?” works
Because position survives. Patches enter the sequence with positional encodings, so “top right” is recoverable from the representation. OCR would have returned the button’s label and thrown away where it was.
Four things that get conflated
| What it does | What it cannot do | |
|---|---|---|
| OCR | Image to characters, with boxes | Say what anything means, or read a diagram’s arrows |
| Classical CV | Detect and segment known object classes | Generalise past its label set, or explain |
| Vision encoder | Encode an image into a shared semantic space | Reason at length on its own |
| Multimodal LLM | Reason jointly over pixels and text | Be as cheap or as exact as a parser |
Run OCR on an architecture diagram and you get Database API Frontend. The arrows — which are the architecture — are not characters, so they do not survive. That is the whole reason the dual-channel design exists.
04
Formats, and what flattening destroys
“Convert it to text” is not a neutral operation. Every format loses something specific, and knowing which thing is the difference between a pipeline that degrades and one that lies.
| Format | Carries | Destroyed by flattening |
|---|---|---|
| Pages, geometry, fonts, embedded images, annotations | Page boundaries, reading order in columns, diagrams, tables, anything scanned | |
| DOCX | Heading levels, lists, tables, links, footnotes, headers, tracked changes | Hierarchy — “Introduction” and a body sentence become indistinguishable strings. Cell relationships. Which footnote belongs where. |
| PPTX | Slide boundaries, titles, speaker notes, z-order, images | The slide as a unit; the speaker notes, which are often the actual argument; what was a title |
| XLSX | Sheets, cells, formulas, types, ranges, charts | Formulas — you keep the values and lose the logic. Which sheet a number came from. The header a value belongs to. |
| CSV | Rows, columns, header | Types (01234 becomes 1234), and which column is which once rows wrap |
| Markdown | Explicit structure, already text | Very little. Markdown is the happy case. |
| HTML | DOM, semantics, tables | Navigation and boilerplate pollute the content unless stripped — and it is an XSS vector, so it must never be served back inline |
| Source code | Line numbers, file path, symbols | Line numbers, without which a citation cannot be checked. In Python, indentation is syntax. |
Vendors are explicit about the limits here. Anthropic’s document block accepts only application/pdf and text/plain; .docx and .xlsx are rejected outright and must be converted first PublishedFiles API. Their own recommendation for a Word document containing images is to convert it to PDF and use the PDF path — precisely so the images survive.
Spreadsheets get a different path entirely. Published.xlsx and .csv go through code execution rather than document blocks. That is a deliberate architectural choice, and the right one: a spreadsheet question is usually a computation — “what is the Q3 total?” — and putting 40,000 cells into a context window to make a model add them up is both expensive and unreliable. Run pandas instead.
05
Reading a page versus looking at it
Take a page containing one diagram. What the parser gets and what the model needs are not the same object:
On the page
[database] → [API server] → [frontend]
After text extraction
Database API Frontend
Three nouns. The arrows are gone, so the direction of data flow — the entire content of the diagram — is gone with them. A model given only that text cannot answer “does the frontend talk to the database directly?”, and, worse, will usually answer anyway.
A strong system keeps both as parallel representations of the same page, joined by page number. A semantic layer — blocks with types, text, heading path and bounding boxes — is what exact quotation, search and citation run on. A visual layer — the rendered page — is what layout, arrows, charts, handwriting and stamps survive in. At answer time you send both, for the pages that matter.
The bounding boxes are what let you go further than “send the page”. If block seven on page fifteen is an image at a known rectangle, you can crop just that region and send it magnified — far cheaper than the whole page and far more legible than a downsampled one. In AlevrThat is what Alevr’s image inspection tool does: a region given in percentages, cropped and scaled up to at least 768 px on its short edge before the model ever sees it.
06
When the document is too big to send
A 500-page PDF cannot go in every prompt. At roughly 2,000 text tokens a page that is a million text tokens, and with the visual channel considerably more. There are five honest approaches and they are not interchangeable.
| Approach | Advantage | Disadvantage | Use when | |
|---|---|---|---|---|
| A | Everything in context | No infrastructure, no retrieval bugs, perfect recall inside the window | Cost scales with the document rather than the question; hits the window; attention degrades over very long contexts | The document fits comfortably — which today means most documents |
| B | Chunks, embeddings, vector store | Scales to any corpus; cheap per query | Misses exact strings; chunk boundaries sever context; a retrieval miss is indistinguishable from the document not containing the answer | Large corpus, semantic questions |
| C | Hybrid: BM25 + vectors + rerank | Catches both “about X” and “contains literally X”; the reranker fixes most ordering errors | Two indexes to keep in step; more moving parts | The production default for a corpus |
| D | Hierarchical summaries | Handles “summarise the whole thing”; navigable; cheap at the top level | Summarisation is lossy and must be recomputed on change | Very large single documents |
| E | Page-level multimodal embeddings | Layout and diagrams become retrievable; no parsing loss | Expensive to index; immature tooling; coarse — a page, not a sentence | Visually dense documents: slides, reports, scans |
The common mistake is jumping straight to B for a twelve-page PDF. That is infrastructure solving a problem you do not have — and it loses information relative to simply sending the document.
Threshold on size instead. If it fits the window, send it whole. If it does not, use hybrid retrieval over blocks with page and section filters, and rerank. If the question is “summarise this 500-page report”, retrieval cannot answer it at all — the answer is the whole document — so that is where hierarchical summaries earn their keep. Add page-image retrieval only when the documents are visually dense enough to need it.
07
What “context” actually means
“Context” sounds like one thing a model does. It is eleven things, and only four of them involve a model at all. Take “compare the authentication architecture described in sections 3 and 7.”
| Step | Deterministic or model? |
|---|---|
| Parse the question for explicit locators (“sections 3 and 7”) | Deterministic — regex and heuristics. Cheap and exact. |
| Query expansion (“authentication” → auth, login, OAuth, SSO) | Model, or a lexical thesaurus |
| Metadata filter — restrict to sections 3 and 7 | Deterministic — a WHERE clause |
| Lexical retrieval (BM25) | Deterministic |
| Vector retrieval | Model for the embedding, deterministic for the search |
| Fuse the two result sets | Deterministic (reciprocal rank fusion) |
| Rerank | Model — a cross-encoder |
| Choose which pages’ images to include | Deterministic policy over the reranked set |
| Assemble the prompt and enforce the token budget | Deterministic |
| Reason and answer | Model |
| Verify every citation resolves to a real block | Deterministic |
Two rows deserve emphasis. An explicit locator short-circuits everything — “page 37” is a filter, and running a semantic search for those words is strictly worse than applying it. And the last row is not optional: checking citations against what was actually retrieved, after generation, is how you stop hallucinated page numbers reaching a reader.
08
What we found wrong in our own pipeline
Generic lists of mistakes are cheap, so this one is grounded: every item below was a real defect in Alevr, found during the audit, with the symptom it produced in front of real people. All of them are fixed.
Architectural
- Analysing the file at upload, before a question exists. An extractor ran on arrival and its verdict became permanent, so a misjudged PDF was “couldn’t read this file” forever. Read when asked, not when stored.
- Treating extraction as a gate rather than an optimisation. If extraction fails the file should still be readable by other means — the bytes to the model, the pages as images, a code tool. It was the gate.
- One reader, no ladder. The ladder existed but was unreachable: the code returned on
failedbefore the rung that could have rescued the file. - Confusing retrieval with reading. Running retrieval over a single document the user is looking at is the wrong tool — there is no irrelevant part to filter out. Four retrieved passages stood in for a whole report.
Extraction
- An OCR fallback that could never run. Worse than not having one: pdf.js transfers its input buffer — measured, 16,978 bytes to 0 — so the OCR rung downstream received an empty file on every single document. Silent, and indistinguishable from “this scan has no text”.
- Trusting the declared MIME type. Browsers send
application/octet-streamconstantly. A PDF labelled that way skipped the raw-bytes path entirely. - The gate and the reader disagreeing. The upload gate judged MIME type while the router judged file extension, so a
.sqlfile was accepted from macOS and refused from Linux — the same bytes. - Refusing formats we could already read.
.docx,.xlsxand.pptxwere rejected at upload with working extractors sitting behind the wall. - Indexing markup as prose. RTF had no extractor, fell through the plain-text arm, and filled the index with
\fonttbland\pard. - Not sanitising for the datastore. A single NUL byte failed the insert for an entire 200-page document, because PostgreSQL’s
texttype cannot hold0x00. One bad byte, zero blocks stored.
Chunking and retrieval
- Chunks too large, so retrieval returns noise — or too small, so it returns a sentence with no context. Roughly 200–500 tokens with overlap is a sane default.
- Chunking across a table or a heading boundary, severing a row from its header.
- No metadata. No page, no section, no file. A chunk you cannot locate is a chunk you cannot cite.
- Confusing pages with chunks. A chunk is a retrieval unit; a page is a citation unit. They are not the same and you need both.
- Embeddings where exact search belongs. Identifiers, error codes, function names: that is BM25 territory. A stemmer will not find
NDA-4417. - No reranking. Vector top-k is a rough ordering, and a cross-encoder over the top fifty is the cheapest large quality win available.
- Sending everything retrieved. Irrelevant context measurably degrades answers, and costs money to do it.
Interface and honesty
- Passing judgement on a file before anyone asked. The composer said “couldn’t read this file” on the basis of a search index, while the actual answer depends on the model — which receives the PDF and reads a scan fine. Wrong in both directions.
- Telling the model a file is unreadable while handing it the file. The system prompt said “no readable text — do not invent contents” on turns where the adapter had inlined the PDF two blocks earlier. The best case reported as the worst.
- No size guard before base64 inlining. Base64 adds a third, so an oversized PDF failed the whole turn — the person lost their answer, not just their attachment.
- Expecting the model to reconstruct what you destroyed. If you flatten a table, no amount of prompting recovers the cell relationships.
09
The pipeline, stage by stage
What each stage is for, when it runs, how it fails, and what it does instead of failing. Note how many of them are conditional: a well-built pipeline does the minimum a question requires, not the maximum a file permits.
- 01
Upload and validate
Synchronous- What
- Accept the bytes, enforce size and type policy, store them. Nothing is read.
- Why
- The only stage that must be synchronous, because a person is watching it.
- Fails when
- Quota, oversize, a type the policy refuses.
- Recovers by
- Rejecting with the actual type and the actual limit, not “upload failed”.
- 02
Type detection
Synchronous- What
- Magic bytes first, extension second, the browser’s declared type last.
- Why
- All three disagree in practice, and the declared one is the least reliable of them.
- Fails when
- Container formats. A ZIP is a .docx, an .xlsx, an .odt, an .epub, and a folder of holiday photos.
- Recovers by
- Disambiguating on the archive’s inner manifest.
- 03
Parse and extract structure
Asynchronous- What
- A format-specific parser producing typed blocks that each know where they came from.
- Why
- This is the stage where structure is preserved or lost permanently.
- Fails when
- Encrypted files, corrupt files, exotic font encodings, XFA forms.
- Recovers by
- The ladder — native parser, then a second parser, then OCR, then vision. No rung’s failure ends the file’s life.
- 04
Page rendering
Conditional, async- What
- Each page to an image at roughly 150 DPI. This is the visual channel, and the thumbnail, and the OCR front end.
- Why
- Everything a text extractor throws away is still in the pixels.
- Fails when
- No rasteriser available on the platform.
- Recovers by
- Degrading to text only — and saying so, rather than quietly answering anyway.
- 05
OCR
Conditional, async- What
- Text for pages that have no text layer, with per-word confidence and boxes.
- Why
- Scans. Only scans. OCR is strictly worse than embedded text wherever embedded text exists.
- Fails when
- No OCR engine installed; genuinely bad scans.
- Recovers by
- Falling through to the visual channel. A model that can see the page does not need OCR to answer about it.
- 06
Indexing
Conditional, async- What
- Chunk, embed, write to the lexical and vector indexes.
- Why
- Only for documents too large to show whole, or for searching across a corpus. Never for a ten-page attachment.
- Fails when
- The embedding provider is down; the text contains bytes the datastore cannot hold.
- Recovers by
- Retry with backoff, and degrade to lexical search rather than to nothing — BM25 works without embeddings.
- 07
Retrieval and rerank
Synchronous- What
- Locator filters, then BM25 and vectors in parallel, fused, then a cross-encoder over the top of the list.
- Why
- It sits in the request path, so it has a latency budget of a few hundred milliseconds.
- Fails when
- Nothing relevant is found.
- Recovers by
- Saying so explicitly. “No passage matched” has to reach the model, or it will fill the silence.
- 08
Context assembly
Synchronous- What
- Fit the budget: instructions, question, selected blocks with their locators, selected page images, history.
- Why
- The token budget is finite and shared. No model is involved in spending it.
- Fails when
- Budget overrun.
- Recovers by
- Dropping the lowest-ranked material first — and telling the model what was dropped. Silent truncation manufactures confident wrong answers.
- 09
Generation and citation validation
Synchronous- What
- Call the model, then check that every locator it cited resolves to something that was actually retrieved.
- Why
- This is the step that makes a hallucinated page number impossible to display.
- Fails when
- The answer cites a page that was never in the context.
- Recovers by
- Stripping or flagging the unresolvable citation before anyone reads it.
10
A document representation worth having
The problem with reducing a document to a string is that every locator, every type and every relationship is destroyed in one step, and nothing downstream can recover them. This is roughly the minimum that keeps the options open.
document representation
{
"documentId": "doc_01H…",
"source": {
"fileName": "architecture-review.pdf",
"mimeType": "application/pdf",
"sha256": "…",
"sizeBytes": 4718592
},
"metadata": {
"title": "Platform Architecture Review",
"pageCount": 42,
"language": "en",
"hasTextLayer": true,
"parser": "pdfjs",
"status": "ok" // ok | degraded | failed
},
"sections": [
{ "id": "sec_3", "title": "Authentication", "level": 1,
"path": ["Architecture", "Authentication"],
"startPage": 12, "endPage": 19 }
],
"pages": [
{ "number": 15, "width": 612, "height": 792, "rotation": 0,
"render": { "uri": "s3://…/page-015.jpg", "dpi": 150 },
"hasTextLayer": true,
"blockIds": ["blk_15_1", "blk_15_7"] }
],
"blocks": [
{ "id": "blk_15_1",
"ordinal": 271, // reading order across the document
"page": 15,
"type": "heading", // heading | paragraph | list_item | table
// | code | caption | image | speaker_notes
"text": "3.2 Token exchange",
"headingPath": ["Architecture", "Authentication", "Token exchange"],
"bbox": [72, 648, 468, 24], // x, y, w, h in page units
"confidence": 1.0, // 1.0 embedded; below that, OCR, as measured
"source": "embedded" }, // embedded | pdfjs | ocr | vision
{ "id": "blk_22_3",
"ordinal": 402,
"page": 22,
"type": "table",
"text": "Region | Revenue\nOntario | 4,200,000", // for lexical search
"table": { // for computation
"columns": ["Region", "Revenue"],
"rows": [["Ontario", "4200000"], ["Quebec", "3100000"]]
},
"confidence": 1.0,
"source": "embedded" }
],
"chunks": [
{ "id": "chk_19",
"blockIds": ["blk_15_1", "blk_15_2", "blk_15_3"],
"text": "3.2 Token exchange\n\nThe service exchanges …",
"pageStart": 15, "pageEnd": 15,
"tokenCount": 380 }
]
}Every field earns its place. ordinal restores reading order after any query has scrambled it. bbox is what lets you crop a figure or highlight a citation in a viewer. confidence and source keep OCR guesses distinguishable from verified text — merging them silently is how a reconstruction turns into a quotation. headingPath gives a chunk its context without re-reading the document. chunks.blockIds is the join that makes citation possible at all: chunk, to blocks, to page. And table.rows beside table.text means the same table can be searched lexically and computed over.
11
Retrieval, and why vectors alone lose
Ask “what database technology is used, and why?” of a document that says “we chose PostgreSQL for its JSONB support”. The first half of that question is a lexical match and the second half has no lexical overlap with the answer at all. That is the whole case for hybrid retrieval in one sentence.
- Lexical / BM25 catches
PostgreSQL,pgvector,NDA-4417. Exact tokens, no embedding call, no semantic drift. Essential for identifiers, error codes, function names and part numbers. - Embeddings catch the “why”, where the answer shares no words with the question.
- Reciprocal rank fusion merges the two. Parameter-free and robust, and it needs no score normalisation between two incomparable scales.
- Metadata filters run first. “In section 3”, “on page 37”, “in the appendix” are
WHEREclauses. Applying them before the search is both cheaper and more correct. - Rerank the top fifty down to the top eight with a cross-encoder. Bi-encoders embed the query and the document separately; a cross-encoder sees both together and is markedly better at relevance. This is usually the single biggest quality gain per unit of effort in the whole system.
Vector search alone is insufficient for a structural reason, not a tuning reason. Embeddings compress meaning into a few hundred dimensions, which is exactly the wrong operation for exact strings: NDA-4417 and NDA-4418 are near-identical vectors and completely different contracts. Cosine similarity also has no notion of sufficiency — the top five come back whether or not any of them answers the question, so a retrieval miss is indistinguishable from the document not containing the answer. The model cannot tell which happened unless you tell it.
Visual retrieval
“What does the diagram on page 24 show?” needs no retrieval at all. Page 24 is a locator: filter, take the render, send it. Running a semantic search there is a strictly worse way to answer an exact question, and it is the most common over-engineering mistake in document question-answering.
“Show me the diagram of the auth flow”, with no page given, is the case that needs real visual retrieval. Four strategies work, cheapest first: index the caption and the paragraph that references it, since most figures are captioned and captions are written to be descriptive; store the OCR text of the image region separately from real text, so a hit is traceable to a guess; generate descriptions at index time with a vision model, which is genuinely effective at a cost of one call per figure; and finally embed the rendered page in a multimodal space, which is the strongest option for visually dense documents and the heaviest to index.
12
Citations you can actually check
A citation is only worth printing if the chain behind it is unbroken:
document → page 24 → block 7 → chunk 19 → retrieved result → model context → citationThree rules make that chain hold. One: every chunk carries its block ids and every block carries its page — locators are propagated, never re-derived, because the moment a component recomputes a page number it can be wrong. Two: the locator goes in the prompt, attached to the text, labelled inline; a model cannot cite what it was not told. Three: parse the citations back out of the answer and check each one against the set that was actually retrieved.
Rules one and two make correct citation possible. Rule three is what makes incorrect citation impossible to display — and it is the one people skip.
One extra guard is worth having: a citation whose page contains no retrieved block is a fabrication even when the page exists. Check the block, not just the number.
13
Naïve versus strong, concretely
| The question | Naïve pipeline | Strong pipeline |
|---|---|---|
| “What does page 37 say?” | Semantic search for “page 37”; returns whatever resembles those words | Filter page = 37; return its blocks and its image |
| A scanned contract | Empty string → “I cannot read this file” | No text layer → page images → the model reads it |
| “What does this diagram show?” | Database API Frontend | The page image. Arrows and all. |
| “Find clause NDA-4417” | Cosine similarity returns NDA-4418 | BM25 exact match |
| “Q3 total?” over a 50,000-row sheet | Truncated cells in context; the model does arithmetic badly | pandas in a sandbox; exact |
| A two-column paper | Columns interleaved line by line | Layout analysis keeps the column order |
| A 500-page report | Everything embedded; retrieval misses | Hierarchical summaries, hybrid retrieval, page filters |
| “Does it mention X?” — it does not | Five chunks come back regardless; the model infers something | “No passage matched” reaches the model |
The second system wins not because it has more components, but because it loses less information and it knows what it does not know.
14
Where a model earns its cost
| Task | Right tool | Why not a language model |
|---|---|---|
| Parse a PDF, DOCX or XLSX | A deterministic parser | There is a correct answer. A parser is orders of magnitude cheaper and does not invent a page number. |
| Decode, resize or crop an image | An image library | Same. |
| Detect a file type | Magic bytes | Same. |
| Chunk text | Deterministic, structure-aware code | Same. |
| OCR | A specialised OCR model | Purpose-built, cheap, and it returns per-word confidence. |
| Embeddings | An embedding model | Definitionally a model task. |
| Rerank | A cross-encoder | Around a hundred times cheaper than an LLM for the same ordering. |
| Describe a figure | A vision model | Requires understanding. |
| Reason, compare, synthesise | A language model | Requires understanding. |
“Use the LLM for everything” fails on four axes at once: cost, by orders of magnitude; latency, seconds against milliseconds; non-determinism where determinism was available, since parsing the same PDF twice ought to give the same answer; and debuggability. When a parser is wrong you get a stack trace. When a model is wrong you get a plausible paragraph.
The corollary is the direction Alevr has taken: rather than pre-digesting every upload, give the model a sandbox and let it choose the right deterministic tool per question — pypdf here, pandas there, Pillow for a crop — instead of guessing at upload time which one it will eventually need.
15
The stack, with reasons
| Role | Options | Choose when |
|---|---|---|
| PDF parsing | PyMuPDF (fast, layout-aware, AGPL), pdfplumber (tables, slow), pdf.js / unpdf (Node, no native dependency) | PyMuPDF if you are in Python and the licence permits; unpdf if you are Node-only |
| Page rendering | pdftoppm, PyMuPDF, pdfium, pdf.js with a canvas binding | Whatever avoids a system binary if you deploy serverless |
| DOCX / PPTX / XLSX | python-docx, python-pptx, openpyxl — or the raw OOXML | They are ZIP plus XML; direct parsing is very feasible and dependency-free |
| OCR | Tesseract, PaddleOCR, a cloud document-AI service | Tesseract until accuracy actually blocks you |
| Image manipulation | Pillow, sharp, @napi-rs/canvas | Language fit |
| Embeddings | Hosted embedding APIs, or BGE / E5 self-hosted | Hosted until volume or privacy says otherwise |
| Vector store | pgvector, Qdrant, Weaviate, LanceDB | pgvector if you already run PostgreSQL — one datastore beats two |
| Lexical search | PostgreSQL full-text, OpenSearch, Typesense | Postgres FTS is enough for far longer than people expect |
| Reranking | A hosted rerank API, or a cross-encoder you run | Hosted first. Small cost, large gain. |
| Multimodal model | Claude, GPT, Gemini — all take PDFs natively | Use the native document part. Do not pre-flatten. |
| Object storage | S3, R2, GCS | Always. Never the database. |
| Code sandbox | A remote isolated sandbox — E2B, Modal, Firecracker, gVisor | Never a subprocess on the application host |
16
Where Alevr stands today
What is good today. Dual-channel handling for every provider that supports a native document part, so the PDF itself reaches the model rather than our guess about it. Page rendering in-process, with no system binary to install. A block model with real locators — page, slide, sheet, cell range, line range, heading path, bounding box, confidence. Reading on demand rather than at upload. Tools that let the model navigate a file itself instead of being handed a summary. And a sandbox path that is correctly gated.
What is not there yet, in the order we think it is worth doing:
| Gap | Impact | Effort | |
|---|---|---|---|
| 1 | No remote sandbox provisioned, so the code tool is inert | High — no spreadsheet computation, no model-written parsing | Low |
| 2 | No reranker between retrieval and context assembly | High — retrieval quality across project corpora | Low |
| 3 | Page renders computed on demand but not persisted per page | Medium — blocks visual retrieval, and makes re-reads cost twice | Medium |
| 4 | No structured table field on blocks | Medium — tables are searchable but not computable | Medium |
| 5 | OCR needs binaries that are not installed | Medium — scans depend entirely on the visual channel | Medium |
| 6 | Legacy .doc / .xls / .ppt, .epub, .heic and .tiff unsupported | Low to medium | Medium |
| 7 | No hierarchical summaries for very large documents | Low, until people upload 500-page PDFs | High |
None of it is a rewrite. Each item is additive: provision a sandbox and set two environment variables; add one call between retrieval and assembly; add a page-asset row; add two columns that extractors fill when they can. The parts that work are not touched.
17
Fifteen things worth remembering
- 01Dual channel. Text and pixels, per page, together. The single most important design fact on this page.
- 02Never gate on text.length > 0. That check is what produces “couldn’t read this file” for documents that read perfectly.
- 03Read when asked, not when stored. At upload there is no question, no model and no reason. Every verdict made then is a guess that becomes permanent.
- 04Parsers for parsing, models for understanding. Using a model where a parser works costs a thousand times more and hallucinates.
- 05A ladder, never a gate. Native parser, better parser, OCR, vision. No single rung’s failure may end a file’s life.
- 06Locators are the product. Page, section, line. A chunk you cannot locate is a chunk you cannot cite.
- 07Retrieval is a scaling strategy, not a reading strategy. If it fits, send it whole.
- 08Hybrid beats vectors. Exact strings need BM25, meaning needs embeddings, and you need both, fused.
- 09Rerank. The cheapest large quality win in the whole pipeline.
- 10An explicit locator short-circuits retrieval. “Page 37” is a WHERE clause, not a search query.
- 11Structure once destroyed is never recovered. No prompt rebuilds a flattened table.
- 12Distinguish verified text from reconstructed text. OCR carries confidence; embedded text does not need it. Never merge them silently.
- 13Say what you dropped. Silent truncation manufactures confident wrong answers.
- 14Validate citations after generation. The only reliable defence against hallucinated page numbers.
- 15Sandbox anything the model writes, remotely. A prompt injection in a PDF must not become code execution on your host.
18
Sources
Everything tagged Published on this page comes from one of these. Where a vendor has not published something, the page says so rather than filling the gap.
Alevr puts every frontier model behind one subscription, metered by what answers actually cost. See how it works.