From 4dbf362261941c361d9811d2a433d8bd4c31a11b Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Fri, 5 Jun 2026 01:49:52 +0200 Subject: [PATCH] fix: OCR escalation falls back to tier-1 result when OCR can't run OCR is an enhancement, not a gate. Previously, escalating a scanned doc to the OCR tier returned the OCR result unconditionally -- so with DOCUMENT_OCR_ENABLED =true but no backend configured (no gateway URL / no MISTRAL_API_KEY) the OCR processor returned success=False and the whole document was marked failed and skipped: strictly worse than leaving OCR off (where it would at least index the tier-1 text). Now the registry keeps the tier-1 fast result when the OCR escalation doesn't succeed (no backend, API down, empty output), logging a warning. A misconfiguration degrades gracefully instead of dropping scanned docs. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../document_processors/registry.py | 15 ++++++++++++++- tests/unit/test_registry_tiering.py | 14 ++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/nextcloud_mcp_server/document_processors/registry.py b/nextcloud_mcp_server/document_processors/registry.py index 2aff91c5..53b296c2 100644 --- a/nextcloud_mcp_server/document_processors/registry.py +++ b/nextcloud_mcp_server/document_processors/registry.py @@ -264,7 +264,7 @@ class ProcessorRegistry: filename or "", reason, ) - return await self._run_processor( + ocr_result = await self._run_processor( ocr, content, content_type, @@ -273,6 +273,19 @@ class ProcessorRegistry: progress_callback, escalated=True, ) + # OCR is an enhancement, not a gate: if it can't run (no backend + # configured / API down) or returns nothing, keep the tier-1 + # result rather than failing the document. Otherwise an operator + # who sets DOCUMENT_OCR_ENABLED=true without credentials would + # make scanned docs fail entirely -- strictly worse than off. + if ocr_result.success: + return ocr_result + logger.warning( + "OCR escalation did not succeed for %s (%s); keeping the " + "tier-1 result", + filename or "", + ocr_result.metadata.get("parse_failed_reason", "error"), + ) return result diff --git a/tests/unit/test_registry_tiering.py b/tests/unit/test_registry_tiering.py index 1124a69f..37dd6177 100644 --- a/tests/unit/test_registry_tiering.py +++ b/tests/unit/test_registry_tiering.py @@ -117,6 +117,20 @@ async def test_ocr_escalation_on_empty_text(monkeypatch): esc.assert_called_once() +async def test_ocr_failure_falls_back_to_fast(monkeypatch): + # OCR enabled but the backend can't run (no creds / API down) -> keep the + # tier-1 result instead of failing the document. + monkeypatch.setattr(reg_mod, "get_settings", lambda: _Settings(ocr=True)) + monkeypatch.setattr(reg_mod, "record_document_escalation", MagicMock()) + r = _registry( + (_Fake("fast", "fast", text=""), 20), + (_Fake("ocr", "ocr", text="", success=False), 5), + ) + res = await r.process(b"%PDF-1.7", "application/pdf") + assert res.processor == "fast" + assert res.success is True + + async def test_no_ocr_escalation_when_disabled(monkeypatch): monkeypatch.setattr(reg_mod, "get_settings", lambda: _Settings(ocr=False)) r = _registry(