refuse a document that is too long, in milliseconds (v0.4.4)

New `ocr.max_pages`. A document with more pages than the ceiling is refused
with a 413 naming its page count and the limit, instead of being read.

Checked off the page tree, before anything is rendered, split or sent -- so
an over-long document costs milliseconds rather than an hour of time,
memory, tokens and (where pages are read on rented cards) money, only to
die in a client's timeout anyway. Length is the one cost knowable before
any work, and the one that scales every other cost at once.

Shipped OFF (0 = no ceiling). Page count is not a fault: an install that
batch-reads book scans is as legitimate as one that reads two-page letters,
and this product does not know which it is talking to. Settable from the
OCR settings page.

Refused LOUDLY and with both numbers. This subsystem already carries the
trap that a degraded page answers 200; a skipped document that looked like
an empty read would be the same mistake with a bigger blast radius. The
message names the setting so an operator can find it, and says the document
was NOT read so a caller can route it elsewhere rather than file an empty
result.

`max_pages` at exactly the ceiling passes -- "more than N" means N is fine.
A nonsense value fails OPEN: refusing every document would be worse than
the problem.
Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnfkxBpcZ1v6ypRFFyhrVM
parent 12b262d9
......@@ -16,7 +16,7 @@
# Canonical product version for CoderAI — single source of truth. Both the API
# metadata and the admin web UI read from here.
__version__ = "0.4.3"
__version__ = "0.4.4"
__tagline__ = "Complete orchestration, distribution and escalation of remotizable advanced inference."
# Configure the CUDA caching allocator BEFORE torch is imported anywhere.
......
......@@ -4135,6 +4135,7 @@ def build_settings_dict(c, gpu_cards):
"dpi": c.ocr.dpi,
"surya_dpi": c.ocr.surya_dpi,
"max_concurrency": c.ocr.max_concurrency,
"max_pages": c.ocr.max_pages,
"max_resident_mb": c.ocr.max_resident_mb,
"page_window": c.ocr.page_window,
"raster_workers": c.ocr.raster_workers,
......@@ -4985,6 +4986,7 @@ async def api_save_settings(request: Request, username: str = Depends(require_ad
if "surya_dpi" in d: o.surya_dpi = _i("surya_dpi", o.surya_dpi, 0)
if "max_concurrency" in d: o.max_concurrency = _i("max_concurrency", o.max_concurrency, 1)
# 0 = derive from the cgroup limit or host RAM; see codai/ocr/budget.py
if "max_pages" in d: o.max_pages = _i("max_pages", o.max_pages, 0)
if "max_resident_mb" in d: o.max_resident_mb = _i("max_resident_mb", o.max_resident_mb, 0)
if "page_window" in d: o.page_window = _i("page_window", o.page_window, 0)
if "raster_workers" in d: o.raster_workers = _i("raster_workers", o.raster_workers, 0)
......
......@@ -1123,6 +1123,14 @@
<span class="form-hint">A ceiling on concurrent page WORK, not on memory. It does
not bound how much a document holds &mdash; use the two settings below for that.</span>
</div>
<div class="form-group" style="flex:1;min-width:130px">
<label class="form-label">Max pages per document</label>
<input type="number" id="s-ocr-max-pages" class="form-input" min="0" placeholder="0">
<span class="form-hint">Refuse a document longer than this instead of reading it.
<b>0 = no ceiling.</b> Checked off the page tree before any work, so an
over-long document is refused in milliseconds with a 413 naming its page
count &mdash; never a silent or empty read.</span>
</div>
<div class="form-group" style="flex:1;min-width:140px">
<label class="form-label">Max resident pages (MB)</label>
<input type="number" id="s-ocr-max-resident-mb" class="form-input" min="0" placeholder="0">
......@@ -2246,6 +2254,7 @@ async function loadSettings(){
document.getElementById('s-ocr-dpi').value = ocr.dpi ?? 200;
document.getElementById('s-ocr-surya-dpi').value = ocr.surya_dpi ?? 250;
document.getElementById('s-ocr-max-concurrency').value = ocr.max_concurrency ?? 64;
document.getElementById('s-ocr-max-pages').value = ocr.max_pages ?? 0;
document.getElementById('s-ocr-max-resident-mb').value = ocr.max_resident_mb ?? 0;
document.getElementById('s-ocr-page-window').value = ocr.page_window ?? 0;
document.getElementById('s-ocr-raster-workers').value = ocr.raster_workers ?? 0;
......@@ -2550,6 +2559,7 @@ async function saveSettings(silent){
dpi: parseInt(document.getElementById('s-ocr-dpi').value) || 200,
surya_dpi: parseInt(document.getElementById('s-ocr-surya-dpi').value) || 0,
max_concurrency: parseInt(document.getElementById('s-ocr-max-concurrency').value) || 64,
max_pages: parseInt(document.getElementById('s-ocr-max-pages').value) || 0,
max_resident_mb: parseInt(document.getElementById('s-ocr-max-resident-mb').value) || 0,
page_window: parseInt(document.getElementById('s-ocr-page-window').value) || 0,
raster_workers: parseInt(document.getElementById('s-ocr-raster-workers').value) || 0,
......
......@@ -1030,6 +1030,24 @@ class OcrConfig:
# that will never set one. A document bigger than the whole budget still runs,
# alone -- a gate that can refuse forever is just a deadlock.
max_resident_mb: int = 0
# Refuse a document longer than this many pages instead of reading it.
# 0 = no ceiling, which is the shipped default: page count is not a fault,
# and an install that batch-reads book scans is as legitimate as one that
# reads two-page letters.
#
# It exists because length is the one property that is knowable BEFORE any
# work is done (page_estimate reads the page tree) and the one that scales
# every cost at once -- time, memory, tokens and, where pages are read on
# rented cards, money. An operator who knows their corpus has a tail of
# 1800-page outliers that will never finish inside a client's timeout is
# better served refusing them in milliseconds, with a reason, than having
# them time out expensively an hour later.
#
# Refusing LOUDLY is the whole point: a skipped document answers 413 with
# its page count and this limit, so a caller can route it somewhere else.
# It must never look like an empty or failed read -- see the "a degraded
# page still answers 200" trap this subsystem already carries.
max_pages: int = 0
# How many pages of one document are rasterised at a time. The document is read
# in windows, so residency is O(window) instead of O(document): a 400-page
# filing costs the same as an 8-page one. 0 = follow `max_concurrency` (capped
......@@ -2034,6 +2052,7 @@ class ConfigManager:
"dpi": self.config.ocr.dpi,
"max_concurrency": self.config.ocr.max_concurrency,
"max_resident_mb": self.config.ocr.max_resident_mb,
"max_pages": self.config.ocr.max_pages,
"page_window": self.config.ocr.page_window,
"raster_workers": self.config.ocr.raster_workers,
"lang": self.config.ocr.lang,
......
......@@ -1318,6 +1318,25 @@ class OcrManager:
# already spent, so admission control has to work from the page tree.
n_pages, est_bytes = page_estimate(data, filename=filename,
content_type=content_type, dpi=use_dpi)
# Length is the one cost knowable before any work, and it scales every
# other one at once. An install that sets a ceiling gets it enforced
# HERE -- in milliseconds, off the page tree -- rather than an hour
# later as a timeout that already cost the time, the memory and, on
# rented cards, the money. Refused out loud, with both numbers, so a
# caller can route the document somewhere else; a skip that looked
# like an empty read would be the worst of both.
limit = 0
try:
limit = int(getattr(cfg, "max_pages", 0) or 0)
except Exception:
limit = 0
if limit > 0 and n_pages > limit:
raise OcrError(
f"{filename or 'this document'} has {n_pages} pages, over the "
f"{limit}-page ceiling this install sets for one document "
f"(ocr.max_pages) — it was NOT read. Raise the ceiling, or "
f"split the document and send the parts.",
status=413)
window = self._page_window(cfg)
# Charge for the WINDOW, not the document -- windowing is what makes that
# honest, and charging whole documents would queue work that never coexists.
......
# CoderAI - OpenAI-compatible API server
# Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
"""`ocr.max_pages`: refuse an over-long document instead of reading it.
Length is the one cost knowable BEFORE any work (page_estimate reads the page
tree) and the one that scales every other cost at once. An operator whose
corpus has a tail of outliers that will never finish inside a client's timeout
is better served refusing them in milliseconds than paying for them twice.
Two properties matter and both are easy to get wrong:
* it is OFF by default -- page count is not a fault, and an install reading
book scans is as legitimate as one reading two-page letters;
* a refusal is LOUD. This subsystem already carries the trap that a degraded
page answers 200; a skipped document that looked like an empty read would be
the same mistake with a bigger blast radius.
"""
import types
import pytest
import codai.ocr.manager as om
from codai.ocr.base import OcrError
def _cfg(**kw):
"""The real config object: the ceiling must hold against the shipped one,
not a stub that happens to carry only the fields this test thought of."""
from codai.config import OcrConfig
cfg = OcrConfig()
cfg.enabled = True
cfg.default_engine = "surya"
cfg.surya_enabled = True
cfg.surya_accept_license = True
for k, v in kw.items():
setattr(cfg, k, v)
return cfg
def _mgr(cfg, pages: int):
mgr = om.OcrManager()
mgr._cfg = cfg
return mgr
@pytest.fixture
def anyio_backend():
return "asyncio"
@pytest.fixture(autouse=True)
def _no_real_work(monkeypatch):
"""Nothing past the ceiling check should run in these tests."""
monkeypatch.setattr(om, "_wait_thermal_safe", lambda: None)
def _never(*_a, **_k):
raise AssertionError("the document was read despite the ceiling")
monkeypatch.setattr(om, "pod_pipeline_model", lambda *_a, **_k: "")
return _never
async def _read(mgr, pages_claimed, monkeypatch, _never):
monkeypatch.setattr(om, "page_estimate",
lambda *_a, **_k: (pages_claimed, pages_claimed * 1000))
monkeypatch.setattr(om.OcrManager, "_read_windowed", _never)
return await mgr.ocr_document(b"%PDF-1.4 x", filename="big.pdf",
content_type="application/pdf")
@pytest.mark.anyio
async def test_a_document_over_the_ceiling_is_refused(monkeypatch, _no_real_work):
mgr = _mgr(_cfg(max_pages=200), 201)
with pytest.raises(OcrError) as e:
await _read(mgr, 201, monkeypatch, _no_real_work)
assert e.value.status == 413
@pytest.mark.anyio
async def test_the_refusal_names_both_numbers(monkeypatch, _no_real_work):
"""A caller has to be able to act on it: how long, and how long is allowed."""
mgr = _mgr(_cfg(max_pages=200), 1847)
with pytest.raises(OcrError) as e:
await _read(mgr, 1847, monkeypatch, _no_real_work)
msg = str(e.value)
assert "1847" in msg and "200" in msg
assert "NOT read" in msg
assert "ocr.max_pages" in msg # the setting, by name
@pytest.mark.anyio
async def test_exactly_the_ceiling_is_allowed(monkeypatch, _no_real_work):
"""`more than 200` means 200 passes. Off-by-one here refuses real work."""
mgr = _mgr(_cfg(max_pages=200), 200)
monkeypatch.setattr(om, "page_estimate", lambda *_a, **_k: (200, 200000))
reached = {}
async def _ok(self, *a, **k):
reached["yes"] = True
return ("surya", [], [], [], [])
monkeypatch.setattr(om.OcrManager, "_read_windowed", _ok)
monkeypatch.setattr(om.OcrManager, "_retry_degraded",
lambda self, *a, **k: _done(""))
await mgr.ocr_document(b"%PDF-1.4 x", filename="ok.pdf",
content_type="application/pdf")
assert reached.get("yes")
async def _done(v):
return v
@pytest.mark.anyio
async def test_zero_means_no_ceiling(monkeypatch, _no_real_work):
"""The shipped default must not refuse anything."""
mgr = _mgr(_cfg(max_pages=0), 100000)
monkeypatch.setattr(om, "page_estimate", lambda *_a, **_k: (100000, 10**9))
reached = {}
async def _ok(self, *a, **k):
reached["yes"] = True
return ("surya", [], [], [], [])
monkeypatch.setattr(om.OcrManager, "_read_windowed", _ok)
monkeypatch.setattr(om.OcrManager, "_retry_degraded",
lambda self, *a, **k: _done(""))
await mgr.ocr_document(b"%PDF-1.4 x", filename="huge.pdf",
content_type="application/pdf")
assert reached.get("yes")
@pytest.mark.anyio
async def test_a_nonsense_setting_does_not_refuse_everything(monkeypatch,
_no_real_work):
"""A bad value must fail OPEN: refusing every document is worse."""
mgr = _mgr(_cfg(max_pages="not a number"), 10)
monkeypatch.setattr(om, "page_estimate", lambda *_a, **_k: (10, 10000))
reached = {}
async def _ok(self, *a, **k):
reached["yes"] = True
return ("surya", [], [], [], [])
monkeypatch.setattr(om.OcrManager, "_read_windowed", _ok)
monkeypatch.setattr(om.OcrManager, "_retry_degraded",
lambda self, *a, **k: _done(""))
await mgr.ocr_document(b"%PDF-1.4 x", filename="x.pdf",
content_type="application/pdf")
assert reached.get("yes")
def test_the_default_is_off():
from codai.config import OcrConfig
assert OcrConfig().max_pages == 0
def test_the_setting_survives_a_save_and_the_admin_api():
"""A ceiling that is not persisted is a ceiling that lasts one restart."""
import pathlib
cfg_src = pathlib.Path("codai/config.py").read_text()
assert '"max_pages": self.config.ocr.max_pages,' in cfg_src
routes = pathlib.Path("codai/admin/routes.py").read_text()
assert '"max_pages": c.ocr.max_pages,' in routes
assert 'if "max_pages" in d:' in routes
html = pathlib.Path("codai/admin/templates/settings.html").read_text()
assert "s-ocr-max-pages" in html
assert "max_pages: parseInt" in html
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment