Multimodal fallback for scanned PDFs: Gemini and Claude get inline PDF vision, OpenAI gets an explicit stub
Scanned legal documents - the kind with no text layer - were silently returning empty cells in tabular reviews. ecarjat fixed that by detecting the failure and routing to native PDF vision on Gemini and Claude, with a deliberate "not supported" cell for OpenAI rather than a silent zero.
The change touches four files. In routes/tabular.ts, loadSourceTexts now checks whether pdfjs extracted any text; if it didn't, it encodes the raw PDF bytes as base64 and attaches them to a new rawPdfBase64 field on the source object. The calling function queryGeminiAllColumns then dispatches on provider: Gemini gets streamGeminiMultimodal, which passes an inlineData part on the user turn; Claude gets streamClaudeMultimodal, which wraps the bytes in an Anthropic document content block. OpenAI, which has no equivalent, gets a grey "OpenAI does not support PDF vision" cell. All three new exports are wired through backend/src/lib/llm/index.ts, and a GeminiUserPart type is exposed for callers.
The two multimodal stream functions are additive - existing text-path behaviour is unchanged. The dispatch in queryGeminiAllColumns is the only integration point that runs on live traffic.
A few things to think through before importing. Raw PDF bytes sent to an LLM are substantially larger than extracted text: a 50-page scanned document is going to hit your token budget hard. If you're pulling this in, pairing it with a per-user page cap is worth doing upfront. Also, the function name queryGeminiAllColumns is now misleading - it dispatches across three providers. Worth renaming on import to avoid confusion later.
Spotted something wrong? Or know the PR text has fresher detail than the writeup above?