- Pull the text out of a PDF with
/augment/text-parser - Describe the record we want as a JSON schema
- Extract it, with the schema enforced rather than requested
- Handle the file that has no text in it at all
- Compare what the two routes produce from the same page
Setup
You need Python 3.9 or newer, therequests package, and a Venice API key. See Generating an API Key if you do not have one.
extract.py:
AUTH and JSON_HEADERS are separate. The parser takes a multipart upload, and setting Content-Type yourself on a multipart request stops requests from adding the boundary, which fails in a way that is annoying to diagnose.
1. Get the text out
/augment/text-parser takes a PDF, DOCX, XLSX, or plain text file up to 25 MB and returns the text with a token count. Documents are processed in memory and the content is not retained.
tokens count is the useful part of that response. It tells you what the document will cost you in the next request before you make it, which matters because a long PDF can easily outgrow what you meant to spend.
2. Describe the record you want
Asking a model for JSON gets you JSON shaped roughly the way you asked. Passing a schema gets you JSON that matches, because the schema constrains generation rather than advising it.additionalProperties: False is worth setting at every level. Without it a model that finds something interesting can add a key you never planned for, and the code reading the result will not expect it.
3. Extract
One call, withresponse_format carrying the schema and strict turned on:
200:
affiliation is required, so the model returned an empty string rather than leaving it out. That is the schema doing exactly what you told it to.
An empty string and a missing value are different facts, and
required collapses them. If you need to tell “the document does not say” apart from “the document says nothing here”, type the field as {"type": ["string", "null"]} and ask for null in the system prompt. Strict mode accepts the union, and you get null instead of "".Turn the thinking off
disable_thinking is the line in that request worth arguing about, so here is the argument. The default text model reasons before it answers, and reasoning is drawn from the same completion budget as the JSON. Run the same extraction four times and watch what the model spends:
Raising the budget does not fix the first problem, it just raises the ceiling the model is allowed to hit. The run that spent 4003 tokens came back with
finish_reason of length and an empty string.
Turning thinking off made this extraction five times cheaper and, more usefully, made it the same every time. The schema is already doing the work that reasoning would do, which is deciding what shape the answer takes.
4. When there is no text to get
A PDF produced by a scanner holds pictures of pages, not text. Nothing in the filename says so, and nothing in the file size gives it away either. You do not have to detect it, because the parser does:400, and it is a routing signal rather than a failure. The text route is unavailable for this file, so take the other one: render the page and let a model look at it.
5. What the two routes disagree about
Run both against the same first page and the records come back almost identical. Almost is the interesting part:
The text route preserved the Ł. The vision route returned an ASCII L, because it is reading letterforms rather than character codes, and a diacritic is a small visual detail that survives poorly. If you are matching extracted names against a database, that difference decides whether the row is found.
The eighth author matters more. The page states no affiliation for Illia Polosukhin, and the text route reports that faithfully as an empty string every time. The vision route has, on some runs, filled the field in with a plausible neighbor from the same page. Reading pixels leaves more room to infer than reading characters does, and a required field is an invitation to fill it. When you cannot check the output by hand, that is a reason to prefer parsed text wherever the document offers it.
The cost is closer than it looks. With thinking off on both sides, the two routes ran about the same prompt size on this page:
The image was 923,732 characters of base64, and none of that is what you pay for. Images are tokenized by size, not by the length of their encoding, so a large PNG does not cost what it looks like it should.
Prefer parsed text when the document has text. It keeps the exact characters, it costs nothing extra to reach beyond page one, and it does not care how the page was laid out. Reach for vision when the parser says there is nothing to read, or when the meaning is in the layout, as it is in a chart, a stamp, or a signature.
Extracting something else
Nothing above is specific to papers. Swap the schema and the system prompt, and the pipeline extracts invoices:description fields are doing real work. A date is only unambiguous once you have said which format you want, and 03/04/2026 means two different days depending on who wrote it.
Next steps
- Validate the result against the schema with
pydanticorjsonschema, so a malformed record fails at the boundary rather than three functions later. - Store the extracted text with Embeddings to search across documents instead of re-extracting them.
- Attach documents straight to a chat completion with File Inputs when you want answers rather than records.
- Give the extractor to an agent as a tool, using Building a Tool-Using Agent with Function Calling.
Document Processing
Reference for the text-parser endpoint.
Structured Responses
How json_schema constrains a completion.
Vision
Sending images to a chat model.
File Inputs
Attach a document without parsing it yourself.