feat(labs): make the lab-scan limit operator-tunable

A self-hoster scanning a stack of lab reports ran into a fixed six scans
an hour with no way to raise it. LABS_OCR_LIMIT_PER_HOUR now sets the
per-user ceiling (default 6, clamped to 1-1000, falling back to the
default when unset or not a number), the same shape as
DOCUMENT_AI_LIMIT_PER_HOUR. It is on the compose whitelist, in the
production example, the env manifest and the AI provider guide.

The route also charged the slot before it had looked at the request, so
a malformed body, an unsupported or oversized file, a PDF that could not
be rendered or a scan the daily budget refused still used up the hour.
Those failures hand the slot back now; a scan that reached the provider
keeps it. The 429 carries meta.retryAt. Refs #985
This commit is contained in:
Marc-André Bombeck committed 2026-09-15 23:01:10 +02:00
1 parent 8457e4ffcb
commit ed8a93afc2
10 files changed
+304 -37

No files matched your search

+4 -2
View File
@@ -13489,8 +13489,10 @@ paths:
provider; ≤ 12 MiB) is run through the user's vision-capable provider; the upload lives in memory only and is
never persisted or logged. TEXT (`application/json`, opt-in local OCR): the browser OCR's the image
(tesseract.js) and POSTs `{ mode: \"text\", text }` — only the extracted text reaches the server, so a text-only
provider (e.g. ChatGPT-OAuth) reaches the same review/commit flow. Both modes are gated by AI consent, a 6/hour
rate bucket, and the per-day token budget, and return proposed rows for the mandatory human review screen —
provider (e.g. ChatGPT-OAuth) reaches the same review/commit flow. Both modes are gated by AI consent, a
per-user hourly rate bucket (default 6, operator-tunable via `LABS_OCR_LIMIT_PER_HOUR`; a slot is only consumed
when the scan reaches the provider, and the 429 carries `Retry-After`, the `X-RateLimit-*` triple and
`meta.retryAt`), and the per-day token budget, and return proposed rows for the mandatory human review screen —
nothing is written. Extracted content is treated as untrusted (prompt-injection); the review step is the safety
boundary."
requestBody:
+9 -1
View File
@@ -412,7 +412,15 @@ many documents against a generous provider budget; lower it on a
tight one. The 429 names the actual wait (the window is an hour) and
carries the reset instant in `X-RateLimit-Reset`.
All three variables must also be listed in the compose
`LABS_OCR_LIMIT_PER_HOUR` (default `6`, clamped to 1–1000) does the
same for lab-report scans. The photo or PDF scan and the browser-OCR
text scan share one bucket per user, and a scan that fails before the
provider is called (malformed body, file too large or not an image or
PDF, a PDF that cannot be rendered, exhausted daily budget) hands its
slot back. Raise it when users scan a stack of reports at once; the
daily AI budget still bounds the cost.
All four variables must also be listed in the compose
`environment:` whitelist to reach the container; the shipped
`docker-compose.yml` lists them.