I Showed Cloudflare's Clef an Image of a Document (in English) and Had It Find Errors in the OCR Results
This page has been translated by machine translation. View original
Hello, I'm Keima.
For OCR processing of documents on AWS, one approach is to host an OCR model on Amazon SageMaker to extract text, then use a downstream LLM to structure keys and values while making use of the character recognition confidence scores.
However, hosting a model on SageMaker around the clock tends to get expensive, and because multiple services are combined, the architecture also tends to become heavy.
Recently, the character recognition accuracy of VLMs themselves has improved, so a setup where a VLM alone handles everything through extraction has become practical.
However, the confidence scores an LLM outputs tend to be self-reported, and it can give high scores even to misread values, so they are a weak basis for judgment that cannot be fully trusted.
On the other hand, calling yet another LLM just for verification would add further cost.
Clef, released on Cloudflare Workers AI on October 1, 2026, is a decision model that supports image input and returns probabilities directly in response to questions.
Because it runs lightweight and at low cost, I thought it might be suitable for independently checking extraction results.
So in this article, I pass a document image and its extraction results to Clef, have it judge "Is this value exactly as in the image?", and test whether it can find errors in OCR results.
1. Test conditions
First, I had Claude Haiku 4.5 extract fields from English quotes, invoices, and receipts (3 documents, 91 fields in total), trying to detect naturally occurring errors.
However, Haiku turned out to be better than expected and extracted every field correctly, so for this test I intentionally injected 3 patterns of errors (12 each, 36 in total).
The breakdown of the injected errors is as follows.
-
Digit change (12): change just one digit of an amount or date (
$1,150.00→$1,160.00,09/18/2026→09/19/2026, etc.) -
Change to a similar character (12): replace with a character easily confused in OCR (
0→O,1→l,m→rn, etc.) -
Replacement with a different value (12): replace with another word that is not unnatural in context (
SAMPLE MART→FRESH MARKET,Service→Hardware, etc.)
The judgment and verification conditions are as follows.
-
Judgment method: pass one document image and the extraction results, and for each field ask with
noul(a question that returns the probability of yes) "Is this value exactly as printed in the image?" A probability below 0.5 is judged as an "error" -
Models:
clef(27B) andclef-flash(9B)
{
"model": "clef",
"images": ["data:image/png;base64,..."],
// state: premise for the judgment (an OCR system read the invoice image and extracted keys and values)
"state": {
"task": "An OCR system read the attached invoice image ...",
"extracted": { "invoice_no": "INV-2026-0042" }
},
"questions": {
"field.invoice_no": {
"type": "noul", // a yes/no question. Returns the probability of yes
// instructions: judgment instructions (Does the extracted invoice_no value match the string printed in the document image exactly, character for character?)
"instructions": "Is the extracted value of \"invoice_no\" (\"INV-2026-0042\") exactly what is printed in the document image, character for character?"
}
}
}

The invoice used in the test
2. Results
2.1 Could it find the errors?
Clef returns, for each field, the "probability that the value matches the image."
Fields with a probability below 0.5 were judged as "errors," and I counted the results from the following two perspectives.
-
Error detection: Of the 36 injected errors, the number correctly judged as "errors"
-
Correct values judged as errors: The number of values that were actually correct but were judged as "errors" (false positives).
| Condition | Error detection (out of 36) | Correct values judged as errors (out of 237) |
|---|---|---|
| clef | 36 | 0 |
| clef-flash | 36 | 3 |
clef detected all of the injected errors and had 0 false positives.
On the other hand, clef-flash had 3 false positives, where fields that should have been correct were wrongly judged as "errors (probability below 0.5)." The fields involved were as follows, and all of them were amount-related fields such as the total amount.
| Document | Field | Value actually entered (correct, as in the image) | Probability judged by clef-flash |
|---|---|---|---|
| Quote | Total amount (Total) | $5,820.12 |
0.17 to 0.19 |
| Receipt | Price of line item 1 | 4.29 |
0.34 |
- The quote's Total was falsely flagged in each of two trials, so the count is 3 in total, but the fields themselves are the two kinds above.
The match probabilities for all 36 injected errors are as follows.
These values represent "the probability that the extracted value is correct according to the image," so the lower the number, the more strongly the model suspects that it "does not match the image (is an error)."
Match probabilities for the 36 injected errors (click to expand)
| Document | Set | Field | Correct value | Injected value | Pattern | clef | clef-flash |
|---|---|---|---|---|---|---|---|
| Quote | A | item1_unit_price |
$1,150.00 |
$1,160.00 |
Digit | 0.02 | 0.02 |
| Quote | A | tax |
$431.12 |
$431.72 |
Digit | 0.23 | 0.05 |
| Quote | A | quote_no |
Q-2026-0187 |
Q-2026-O187 |
Similar character | 0.06 | 0.10 |
| Quote | A | item3_amount |
$435.00 |
$435.0O |
Similar character | 0.02 | 0.16 |
| Quote | A | item4_name |
Office suite license |
Project management tool |
Replacement | 0.02 | 0.02 |
| Quote | A | item6_category |
Service |
Hardware |
Replacement | 0.01 | 0.03 |
| Quote | B | date |
09/18/2026 |
09/19/2026 |
Digit | 0.02 | 0.03 |
| Quote | B | item3_qty |
3 |
5 |
Digit | 0.03 | 0.02 |
| Quote | B | total |
$5,820.12 |
$5,82O.12 |
Similar character | 0.07 | 0.10 |
| Quote | B | item1_name |
Laptop computer |
Laptop cornputer |
Similar character | 0.03 | 0.08 |
| Quote | B | item2_amount |
$840.00 |
$1,240.00 |
Replacement | 0.03 | 0.11 |
| Quote | B | note |
Prices are valid for 30 days from the quote date. |
Prices include free shipping. |
Replacement | 0.02 | 0.02 |
| Invoice | A | invoice_no |
INV-2026-0042 |
INV-2026-0045 |
Digit | 0.02 | 0.02 |
| Invoice | A | item2_qty |
5 |
6 |
Digit | 0.06 | 0.03 |
| Invoice | A | vendor_address_line2 |
Columbus, OH 43004 |
Columbus, OH 43OO4 |
Similar character | 0.05 | 0.04 |
| Invoice | A | bill_to_address_line1 |
88 Innovation Way |
B8 Innovation Way |
Similar character | 0.13 | 0.04 |
| Invoice | A | bill_to_name |
Example Robotics Inc. |
Example Logistics LLC |
Replacement | 0.03 | 0.02 |
| Invoice | A | payment_terms |
Net 30 |
Due on receipt |
Replacement | 0.02 | 0.02 |
| Invoice | B | due_date |
10/15/2026 |
10/16/2026 |
Digit | 0.08 | 0.02 |
| Invoice | B | amount_due |
$1,234.50 |
$1,284.50 |
Digit | 0.07 | 0.04 |
| Invoice | B | po_number |
PO-7781 |
PO-778l |
Similar character | 0.07 | 0.08 |
| Invoice | B | vendor_email |
billing@sample-supply.example |
billing@sarnple-supply.example |
Similar character | 0.03 | 0.03 |
| Invoice | B | item3_description |
Pallet wrap film |
Bubble wrap roll |
Replacement | 0.03 | 0.02 |
| Invoice | B | shipping |
$14.37 |
$0.00 |
Replacement | 0.02 | 0.02 |
| Receipt | A | item2_price |
3.49 |
3.48 |
Digit | 0.11 | 0.02 |
| Receipt | A | time |
14:32 |
14:52 |
Digit | 0.04 | 0.01 |
| Receipt | A | receipt_no |
#0815 |
#O815 |
Similar character | 0.07 | 0.13 |
| Receipt | A | total |
28.94 |
2B.94 |
Similar character | 0.04 | 0.01 |
| Receipt | A | item4_name |
APPLES 2LB |
BANANAS 3LB |
Replacement | 0.03 | 0.01 |
| Receipt | A | store_name |
SAMPLE MART |
FRESH MARKET |
Replacement | 0.02 | 0.01 |
| Receipt | B | date |
09/21/2026 |
09/27/2026 |
Digit | 0.03 | 0.01 |
| Receipt | B | item5_price |
11.99 |
17.99 |
Digit | 0.03 | 0.01 |
| Receipt | B | store_address_line2 |
Portland, OR 97201 |
Portland, OR 972O1 |
Similar character | 0.06 | 0.05 |
| Receipt | B | payment_card |
VISA ****0000 |
VISA ****OOOO |
Similar character | 0.09 | 0.12 |
| Receipt | B | item1_name |
MILK 1GAL |
ORANGE JUICE |
Replacement | 0.02 | 0.01 |
| Receipt | B | subtotal |
28.94 |
31.50 |
Replacement | 0.03 | 0.01 |
2.2 How the probabilities separate
To see where the threshold should be set, I summarized the spread of the "probability that the value matches the image" assigned to the error fields (36) and the correct fields (237).
The bottom 5% and top 5% are the values at the 5% and 95% positions from the bottom when the probabilities are sorted from lowest to highest.
| Condition | Target | Min | Bottom 5% | Median | Top 5% | Max |
|---|---|---|---|---|---|---|
| clef | Errors | 0.012 | 0.016 | 0.030 | 0.112 | 0.227 |
| clef | Correct values | 0.622 | 0.769 | 0.914 | 0.974 | 0.983 |
| clef-flash | Errors | 0.009 | 0.012 | 0.023 | 0.122 | 0.162 |
| clef-flash | Correct values | 0.166 | 0.645 | 0.940 | 0.973 | 0.981 |
For the correct fields, both models gave high probabilities with medians in the 0.9s (clef: 0.914, clef-flash: 0.940).
Looking at the bottom 5%, clef was at 0.769 and clef-flash at 0.645, so some fields came out somewhat low.
The results when changing the threshold are as follows.
| Threshold | clef: error detection | clef: false positives | clef-flash: error detection | clef-flash: false positives |
|---|---|---|---|---|
| 0.5 | 36/36 | 0/237 | 36/36 | 3/237 |
| 0.6 | 36/36 | 0/237 | 36/36 | 8/237 |
| 0.7 | 36/36 | 5/237 | 36/36 | 18/237 |
| 0.8 | 36/36 | 23/237 | 36/36 | 50/237 |
At every threshold, both models caught all 36 injected errors.
The difference was in the number of false positives: the higher the threshold, the more correct values get sent to human review.
With clef, there were 0 false positives at 0.5 to 0.6, but at 0.8 about 10% of the correct values (23) would be sent to human review.
clef-flash had 3 false positives even at 0.5, and at 0.8 it reaches about 20% (50), so clef is better suited if you want to keep the review effort down.
However, since the error data is only 36 cases, for actual operation I recommend collecting similar statistics on the documents you handle and deciding the threshold from them.
2.3 Time and cost
The requests to Clef were made once per document.
In each request, I sent one image together with the questions for all fields of that document (21 to 38 questions).
| Condition | Time per request (median) | Total time for 9 requests | Total cost for 9 requests |
|---|---|---|---|
| clef | 1.737 s (1,737 ms) | 14.46 s | $0.0103 (about 1.55 yen) |
| clef-flash | 0.953 s (953 ms) | 8.69 s | $0.0039 (about 0.58 yen) |
Times are measured values when calling the REST API from within Japan, and costs are converted at 150 yen per dollar.
Extraction by Haiku takes about 10 to 13 seconds per document, so adding this check barely changes the overall processing time.
3. Summary
As far as I tested with three English documents, clef, given the images, detected all the injected errors and had no false positives.
However, the errors this time were injected artificially, so I have not confirmed performance against the kinds of misreadings Haiku itself makes.
If you incorporate this into a document extraction check, rather than having the VLM self-report the correctness of its own extraction results, an effective setup is to pass the results together with the image to a lightweight decision model like Clef and have it double-check.
