fix(labs): reserve and re-check each page of a scan, and read digits from the text layer

The route reserved one read's budget and checked the wire once, and a
20-page report then ran up to 40 calls for many minutes on that single
admission. Every read after the first now reserves its own share of the
day's budget and asks the wire again (aiEgressRefusal) before it is sent;
when either refuses, the scan stops there, keeps the pages already read
and names the rest with stoppedBy budget or unavailable. The run settles
against everything it reserved, with the provider's reported usage net of
its cached share, and a scan that fails after several calls is charged
for them instead of refunded in full.

A rendered PDF page now goes out with its own text layer, read by pdfjs
beside the image, so a digitally produced report's digits, decimal
separators and units come from its characters rather than from a 1600 px
picture. Splitting the PDF into single-page documents would need a PDF
writer the project does not ship; the text layer needs none and helps
every provider. A scanned page has no text layer and reads as before.

Codex's cut reply keeps the text it streamed (on a non-enumerable
property, so no log or event serialises it), and its closed rows are
salvaged like every other provider's. The merge folds a reading only into
one an earlier page listed, never two rows of the same page, and the
printed range is part of the key. OCR'd text the deadline cannot split
small enough for one read is refused up front as labs.ocr.tooLarge.
This commit is contained in:
Marc-André Bombeck committed 2026-10-21 11:10:02 +02:00
1 parent 6376710254
commit 79d905f089
26 files changed
+715 -126

No files matched your search

+17 -7
View File
@@ -36232,9 +36232,9 @@ components:
labs.biomarker.update.invalid, labs.biomarker.update.referenceRangeInvalid, labs.create.invalid,
labs.link.invalid, labs.ocr.budgetExceeded, labs.ocr.extractFailed, labs.ocr.fileTooLarge,
labs.ocr.fileType, labs.ocr.localOcrDisabled, labs.ocr.outputLimit, labs.ocr.pdfNeedsAnthropic,
labs.ocr.providerUnsupported, labs.ocr.rateLimited, labs.restore.invalid, labs.result.notFound,
labs.unit.mismatch, labs.update.invalid, labs.update.linkedFieldsImmutable, labs.update.numericExpected,
labs.update.qualitativeExpected, labs.update.referenceRangeInvalid,
labs.ocr.providerUnsupported, labs.ocr.rateLimited, labs.ocr.tooLarge, labs.restore.invalid,
labs.result.notFound, labs.unit.mismatch, labs.update.invalid, labs.update.linkedFieldsImmutable,
labs.update.numericExpected, labs.update.qualitativeExpected, labs.update.referenceRangeInvalid,
labs.update.sourceReferenceRangeInvalid; life_event — life_event.invalid, life_event.not_found;
managed_profile — managed_profile.guardian.duplicate, managed_profile.guardian.managed_invitee,
managed_profile.guardian.required, managed_profile.handover.no_pending,
@@ -71319,6 +71319,15 @@ components:
type: integer
exclusiveMinimum: 0
maximum: 9007199254740991
stoppedBy:
description: "Present only when the scan stopped before its last read for a reason other than the provider: `budget`,
the day's AI budget ran out (every read after the first is reserved on its own); `unavailable`, AI was
turned off for the record while it ran (the wire is re-checked before every read). The pages it did not
reach are in `unreadPages`."
type: string
enum:
- budget
- unavailable
rowsCapped:
description: "Present only when the report held more readings than one scan carries (400): `rows` holds the first `kept`
of the `found` readings."
@@ -71342,10 +71351,11 @@ components:
- rows
additionalProperties: false
description: The proposed rows for the human review screen, served by `GET /api/ai-runs/{id}` for a `LABS_OCR_EXTRACT`
run. A PDF of more than one page is read a page at a time and the pages merged in order, a reading printed on
two pages listed once. Never written to the person's record; the commit route is the only write. A run in which
no read produced a single complete row fails with 422 `labs.ocr.outputLimit` when the reads stopped at the
provider's output limit, else `labs.ocr.extractFailed`.
run. A PDF of more than one page is read a page at a time, each rendered page sent with its text layer, and the
pages merged in order, a reading an earlier page already listed listed once. Never written to the person's
record; the commit route is the only write. A run in which no read produced a single complete row fails with 422
`labs.ocr.outputLimit` when the reads stopped at the provider's output limit, else `labs.ocr.extractFailed`;
OCR'd text too long for the reads the deadline allows is refused up front with 422 `labs.ocr.tooLarge`.
OcrExtractedRow:
type: object
properties: