Repository navigation
The three limits the PDF vision path keeps on purpose #126
will-lamerton
announced in
Articles
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
The new opt-in PDF diagram reconstruction in get-md 1.7.0 is, by the numbers, a small feature. A handful of source files, an optional peer dependency pair, and a 10-page cap. The interesting part is not the feature itself. It is the three deliberate limits around it: remote providers only, a 10-page render cap, and a hard short-circuit on pure-scan PDFs. Each one of those limits looks, at first glance, like something a more complete implementation would lift. None of them should be lifted, and this post is the case for keeping them.
Built by the Nano Collective, a community collective building AI tooling not for profit, but for the community.
What the path actually does
With
useLLM: true, a remote vision-capable provider, andpdfjs-distplus@napi-rs/canvasinstalled, get-md renders the first 10 pages of a PDF to JPEG images at 2x scale and sends them to the model alongside the extracted text. The model's system prompt asks it to emit Mermaid fences inline where a diagram appeared in the page. That output rides the same Markdown pipeline as a text-only result, including the newvalidateMermaidstep.The mechanics are spread across four files worth naming, because the limits live in each of them and it is useful to know where:
src/extractors/pdf-renderer.tsrenders pages to JPEG and enforces the 10-page cap with aconsole.warnnaming it.src/extractors/pdf-extractor.tsextracts the text that the vision path is layered on top of, and short-circuits to an empty result when there is no text at all.src/index.tsholdssupportsVision,tryRenderPdfToImages, andemptyPdfResult, and is the only place where remote-provider detection happens.src/converters/remote-llm-converter.tsis the path that actually talks to a vision model, including the soft-fail fallback to text-only when the vision request itself errors.The headline release post covered the surface area. This one is the limits, and why they are not arbitrary.
Limit one: remote providers only
The vision gate in
supportsVisionis short enough to quote in full:Two conditions, both must hold.
useLLMhas to be on, and the configured provider has to be a non-local one. The!options.llmcheck matters: without an explicitllmconfig the path defaults tosdkProvider: "local-llama", and that branch never even asks the question. This is the part that surprises people who turn onuseLLMand expect the local path to magically handle diagrams.The reason is honest, and it is about what local-llama actually is. The local ReaderLM-v2 path in this codebase is text-only. It has no vision input. It does not accept images. If the renderer fed it JPEGs, they would be ignored, dropped, or counted as a malformed user message, depending on the model. Running the renderer at all on a text-only path would burn the 10-page render budget for nothing the model can see.
So the gate is not a design preference. It is the absence of a feature that does not exist in the underlying model. The 1.7.0 release notes state this plainly: "The local ReaderLM-v2 path is text-only, and so is
useLLMwith nollmconfig, since that defaults to local-llama." A user who wants diagram reconstruction and runs locally has only one honest path today: route through a remote provider that supports multimodal input. Wrapping the local path with a fake vision layer would be a worse user experience than telling them up front.The remote-only choice also keeps the default install footprint reasonable.
pdfjs-distand@napi-rs/canvasare loaded lazily, on demand, only whensupportsVisionreturns true. A user who never setsuseLLM, or who sets it without an explicit remote provider, never touches either dependency. The renderer is imported viaawait import("./extractors/pdf-renderer.js")and the canvas imports inside the renderer function itself, so the cost is paid only when the feature is used. Lifting the remote-only limit, in either direction, breaks one or the other of those properties.There is one case the limit makes genuinely awkward, and it is worth naming: a privacy-respecting user who wants to run everything on their own hardware and is fine paying a model-quality cost for it. The current answer for that user is no, the PDF vision path will not work for them. The future answer is a vision-capable local model behind the same gate, but that is a different feature, not a relaxation of this one.
Limit two: the 10-page render cap
The cap lives in one place:
10 pages, with a warning that names the cap, and a
Math.minso the loop never iterates past it. Text extraction still covers the whole document, so a long PDF loses only the diagram reconstruction, not the content.The cap is not arbitrary. It is a context-budget decision that the model side enforces whether we like it or not. Each rendered JPEG at 2x scale is hundreds of thousands of pixels. A modern vision-capable model accepts a handful of images before the multimodal message parts crowd the context window, push out the text, and start producing degraded output on both. Asking the same model to reconstruct diagrams from page 1 through page 400 of a handbook is not a stretch goal. It is a request that gets back lower-quality Markdown for every page, including the ones the user cared about.
Picking 10 is partly empirical. It is enough pages that a typical whitepaper, slide deck, or design doc has its diagrams covered. It is small enough that even a multi-image request stays well inside a normal context window. Lifting it would not improve the result on long documents; it would degrade the result on the same long documents. Lifting it without degrading the result would require a chunked, multi-request strategy with merge logic, which is the kind of feature that needs design, not just a higher constant.
There is also a second reason, which is operational. The renderer holds onto
pdfjs-distand@napi-rs/canvasfor the duration of a conversion. A 400-page render at 2x is a noticeable CPU and memory spike on the host process. The cap turns the worst case into a predictable one. A user who hits the cap gets a warning in stderr and the first 10 pages of diagrams; they do not get a multi-minute hang and an out-of-memory error.The honest workaround for long PDFs is structural. If you know your diagrams cluster early, the cap covers you. If they cluster late, render the relevant section as its own PDF first. If they are scattered, this is not the right tool for the job yet, and the path would tell you that with a warning rather than producing a result that quietly degraded partway through.
Limit three: pure-scan PDFs short-circuit early
This is the one the release notes call out as a known limitation:
The relevant code is in
src/index.ts, in both the buffer and the URL paths:The vision render call sits after the empty-text short-circuit, not before it. The text comes back empty,
emptyPdfResultreturns aMarkdownResultwithmarkdown: ""andinputLengthset to the buffer size, and the function exits.tryRenderPdfToImagesis never reached. The user gets an empty result that is the same shape a text-less PDF has produced since 1.6.0.Read in isolation that looks like an oversight. In context it is the deliberate consequence of two earlier decisions. First,
pdf-parseis the text extractor of record, and what it returns is the only signal the rest of the pipeline uses to decide the document is real. Second, the renderer is intentionally lazy: it importspdfjs-distand@napi-rs/canvas, hands pages to a vision model, and pays for all of that, only on a code path where text was extracted. Lifting the empty-text short-circuit to "still run the vision renderer and ask the model to read the whole thing" turns this from a cheap opt-in into the most expensive path in the library, on the input type where the user is most likely to be unhappy with the cost.There is also a correctness angle.
emptyPdfResultreturns a result withreadabilitySuccess: false, an empty body, andinputLengthset. A caller that already learned in 1.6.0 to detect "this PDF yielded no text, treat it as needing OCR" gets the same signal in 1.7.0. The 1.6.0 release notes are explicit about this contract: "Scanned/text-less PDFs return an empty result with a non-zeroinputLength(a signal OCR is needed)." Carrying the contract forward in 1.7.0, even with a vision path sitting one branch away, is the conservative move. A user who expects "give me back what you can, even if the text is empty" would get a worse tool, not a better one.The honest fix for the pure-scan case is upstream: run OCR, or use a tool whose job is OCR, then feed the OCR'd text back in. The vision path will not paper over that for you, and that is the right design call.
What "fails soft" actually means here
There are two fail-soft paths layered on top of the three limits, and they are part of the same opinion. The renderer fails soft when dependencies are missing or a render throws: a
console.warnand the conversion continues without images. The vision request fails soft when the model itself rejects the multimodal message:RemoteLlmConverter.convertcatches the error, swaps in the text-only system prompt, drops the image parts, and retries the same call. Either way the user gets Markdown. They get less Markdown than the full path promised, but they get Markdown.The retry is interesting because it is the only place in the path where the user-facing text could change shape. The vision request produces a richer system prompt ("you are provided with the text of the page, as well as images of the rendered page, if the page contains diagrams, flowcharts, or architecture diagrams, reconstruct them as Mermaid code block fences"). The fallback reverts to the plain text-to-Markdown system prompt. The text the model sees is the same; what it does not see is the images. A user who later turns off
useLLMand re-runs the same file should land on the same text-only path, and the fallback makes the failure mode of "vision did not work this run" behave the same as "vision was not asked this run".Pair this with
validateMermaid: trueand the soft-fail story is honest end-to-end. A user who runs the vision path gets whatever the model thinks the diagrams were. A user who pairs that with validation gets any reconstruction the model got wrong, flagged with a> [!WARNING]callout, kept in the output for repair. The two features are designed to be used together, and the limits in the vision path are written with the assumption that the validator will catch what the vision model misses.What the limits are not
Three things the limits are not, because each one comes up in user feedback:
What we kept, and what we did not
The PDF vision path in 1.7.0 is what survived the review pass: a deliberately narrow feature with three limits and two soft-fail paths, every limit encoding a tradeoff the user should be able to reason about. The narrowness is the point. A vision path that handled every PDF, ran locally, and never failed would either not work (it would silently produce wrong output), or be the most expensive thing in the library (it would run heavy multimodal inference on every scan, every long PDF, every input type).
The constraints below are the ones we kept:
If you want to dig into the renderer or the soft-fail paths,
src/extractors/pdf-renderer.ts,src/converters/remote-llm-converter.ts, and thesupportsVisionandemptyPdfResulthelpers insrc/index.tsare the entry points. The Diagrams and Mermaid guide and Remote LLM guide document the configuration shape and the failure modes end-to-end. Feedback on the limits, especially the ones that hurt your workflow, is welcome on GitHub or Discord.Repo: https://github.com/Nano-Collective/get-md
All reactions