Merge branch 'fix/v147-pdf-unrenderable' into release/v1.47.0

# Conflicts:
#	.env.production.example
This commit is contained in:
Marc-André Bombeck committed 2026-11-03 22:10:14 +01:00
commit b1cb387062
45 files changed
+1789 -192

No files matched your search

+28 -24
View File
@@ -19823,26 +19823,29 @@ paths:
capability envelope when it is closed), then rate / budget gated. With no provider configured this returns 422
(`documents.inbound.providerUnsupported`, with `meta.capability` and `meta.reason = \"no_provider\"`) — the
stored document is untouched, only the enhancement fails. Three modes: VISION (empty body) decrypts and scans
the stored original (a PDF is rendered and read a page at a time; one that cannot be rendered needs a provider
that reads PDFs natively, else 422 `documents.inbound.pdfNeedsAnthropic`); TEXT (`application/json`, opt-in
local OCR) `{ mode: \"text\", text }` structures browser-OCR'd text; STORED (`application/json`) `{ mode:
\"stored\" }` structures the document's own stored extracted text (its content index) — the manual recovery for
a skipped/failed automatic staging run, 422 `documents.inbound.notIndexed` when no stored text exists. A long
document is read a page or a part at a time under the same per-read limits as the lab scan (`LABS_OCR_EXTRACT`),
each read after the first reserved against the day's budget on its own. Without `Prefer: respond-async` the
extraction runs inside the request and makes at most three reads (six model calls): a longer document has its
pages or text shared out between them, and what does not fit is left out and reported as `extractionIncomplete:
true` on the answered document; the background run reads it a page at a time within its own deadline; an
extraction in which no read produced a complete fact fails with 422 `documents.inbound.outputLimit` when the
reads stopped at the provider's output limit, 429 `documents.inbound.budgetExceeded` when the budget refused the
reads, and 422 `documents.inbound.extractFailed` only when the provider's answers were not the schema.
Extraction reproduces what the document states — it never interprets. Nothing reaches the structured stores
here; the confirm route is the only write path. With `Prefer: respond-async` (v1.40) the extraction runs in the
background: after the same quick refusals the route answers 202 with a run id, a second request while an
extraction of this document from the same input is queued or running answers with that run, and `GET
/api/ai-runs/{id}` serves `DocumentExtractRunResult` (`{ documentId, factsStaged, status }`, plus `incomplete`
when part of the document may be missing from the staged facts; not the document detail) as the run's `result`.
Staging re-checks the document under a row lock, so a review finished while the read ran fails the run with
the stored original (a PDF is rendered and read a page at a time; one that will not render answers 422
`documents.inbound.extractFailed`; on a server that cannot render PDF pages at all, a provider that reads PDFs
natively gets the PDF whole, any other provider reads its text layer a page group at a time with the pages
without one reported through `extractionIncomplete`, and a scanned PDF with no text layer answers 422
`documents.inbound.pdfRendererUnavailable`); TEXT (`application/json`, opt-in local OCR) `{ mode: \"text\", text
}` structures browser-OCR'd text; STORED (`application/json`) `{ mode: \"stored\" }` structures the document's
own stored extracted text (its content index) — the manual recovery for a skipped/failed automatic staging run,
422 `documents.inbound.notIndexed` when no stored text exists. A long document is read a page or a part at a
time under the same per-read limits as the lab scan (`LABS_OCR_EXTRACT`), each read after the first reserved
against the day's budget on its own. Without `Prefer: respond-async` the extraction runs inside the request and
makes at most three reads (six model calls): a longer document has its pages or text shared out between them,
and what does not fit is left out and reported as `extractionIncomplete: true` on the answered document; the
background run reads it a page at a time within its own deadline; an extraction in which no read produced a
complete fact fails with 422 `documents.inbound.outputLimit` when the reads stopped at the provider's output
limit, 429 `documents.inbound.budgetExceeded` when the budget refused the reads, and 422
`documents.inbound.extractFailed` only when the provider's answers were not the schema. Extraction reproduces
what the document states — it never interprets. Nothing reaches the structured stores here; the confirm route is
the only write path. With `Prefer: respond-async` (v1.40) the extraction runs in the background: after the same
quick refusals the route answers 202 with a run id, a second request while an extraction of this document from
the same input is queued or running answers with that run, and `GET /api/ai-runs/{id}` serves
`DocumentExtractRunResult` (`{ documentId, factsStaged, status }`, plus `incomplete` when part of the document
may be missing from the staged facts; not the document detail) as the run's `result`. Staging re-checks the
document under a row lock, so a review finished while the read ran fails the run with
`documents.inbound.alreadyPartlyConfirmed` (409) or `documents.inbound.alreadyConfirmed` (422) and leaves the
review untouched; the synchronous form answers the same in that race."
parameters:
@@ -38771,7 +38774,7 @@ components:
documents.inbound.fileType, documents.inbound.invalidMetadata, documents.inbound.invalidQuery,
documents.inbound.invalidUpdate, documents.inbound.localOcrDisabled, documents.inbound.notDeleted,
documents.inbound.notFound, documents.inbound.notIndexed, documents.inbound.outputLimit,
documents.inbound.pdfNeedsAnthropic, documents.inbound.providerUnsupported,
documents.inbound.pdfRendererUnavailable, documents.inbound.providerUnsupported,
documents.inbound.quotaExceeded, documents.inbound.rateLimited, documents.inbound.restoreDuplicate,
documents.inbound.restoreGone, documents.inbound.sourceAliasLimit, documents.inbound.uploadBusy,
documents.inbound.uploadTimeout, documents.inbound.vaccinationNotFound, documents.sources.authRefused,
@@ -38801,7 +38804,7 @@ components:
labs.biomarker.merge.invalid, labs.biomarker.merge.unitMismatch, labs.biomarker.notFound,
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.fileType, labs.ocr.localOcrDisabled, labs.ocr.outputLimit, labs.ocr.pdfRendererUnavailable,
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,
@@ -76185,8 +76188,9 @@ components:
additionalProperties: false
unreadPages:
description: "Present only when part of a multi-part read produced nothing: the provider failed partway through, or a
part's reply could not be read. The rows of the other parts are kept; these pages are not in `rows`. Empty
when the parts were not pages (OCR'd text)."
part's reply could not be read; or, on a server that cannot render PDF pages, a PDF read by its text layer
had pages without one (scanned pages). The rows of the other parts are kept; these pages are not in `rows`.
Empty when the parts were not pages (OCR'd text)."
type: array
items:
type: integer