From ebbf905dc5f1a95cda4ad3a8715bcfff5699dced Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Thu, 18 Jun 2026 00:48:10 +0200 Subject: [PATCH] fix(ingest): suppress misleading batch-fallback warn for in-cluster rung MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address claude-review round 3 on #922: - Important: _process_batch emitted "no gateway backend (provider=mistral or EMBEDDING_GATEWAY_URL unset)" for the gateway_only in-cluster rung, where the gateway IS configured — sending operators chasing a non-existent config problem. The real reason is "in-cluster GPU is synchronous-only; batch is the upstream path". Guard the warning with `if not self._gateway_only`. Extended test_gateway_only_processor_never_uses_batch_mode to drive _process_batch and assert _batch_fallback_warned stays False. - Nits: refresh stale ladder in escalation.py module docstring (fast->structured->ocr-incluster->ocr-upstream); fix "minimum='ocr'" -> "ocr-incluster" in a test docstring; rename stale tier="ocr" -> "ocr-upstream" in test_process_tier_oversize_fails_fast. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../document_processors/escalation.py | 2 +- .../document_processors/ocr.py | 13 +++++++++---- tests/unit/test_ocr_processor.py | 18 ++++++++++++++++++ tests/unit/test_registry_tiering.py | 6 ++++-- 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/nextcloud_mcp_server/document_processors/escalation.py b/nextcloud_mcp_server/document_processors/escalation.py index 7876cc53..e6c771b4 100644 --- a/nextcloud_mcp_server/document_processors/escalation.py +++ b/nextcloud_mcp_server/document_processors/escalation.py @@ -2,7 +2,7 @@ The escalation ladder is the cheapest-first ordering of extraction tiers: - fast -> structured -> ocr ( -> llm, reserved) + fast -> structured -> ocr-incluster -> ocr-upstream ( -> llm, reserved) It mirrors the ``tier`` vocabulary documented on :meth:`DocumentProcessor.tier <.base.DocumentProcessor.tier>` and the diff --git a/nextcloud_mcp_server/document_processors/ocr.py b/nextcloud_mcp_server/document_processors/ocr.py index 855516d0..d05b7aac 100644 --- a/nextcloud_mcp_server/document_processors/ocr.py +++ b/nextcloud_mcp_server/document_processors/ocr.py @@ -499,10 +499,15 @@ class OcrProcessor(DocumentProcessor): return None client = await self._get_batch_client() if client is None: - self._batch_fallback( - "no gateway backend (provider=mistral or EMBEDDING_GATEWAY_URL unset)", - filename, - ) + # The in-cluster (gateway_only) rung returns None here BY DESIGN — the + # GPU is synchronous-only, batch is the upstream Mistral path — so don't + # emit the "no gateway backend" warning (the gateway IS configured; that + # warning would send an operator chasing a non-existent config problem). + if not self._gateway_only: + self._batch_fallback( + "no gateway backend (provider=mistral or EMBEDDING_GATEWAY_URL unset)", + filename, + ) return None # Lazy import: keep the vector/DB stack off the document_processors load diff --git a/tests/unit/test_ocr_processor.py b/tests/unit/test_ocr_processor.py index cca2bdd7..469a9250 100644 --- a/tests/unit/test_ocr_processor.py +++ b/tests/unit/test_ocr_processor.py @@ -467,6 +467,24 @@ async def test_gateway_only_processor_never_uses_batch_mode(monkeypatch): assert await incluster._get_batch_client() is None assert called["n"] == 0 # short-circuited before building anything + # _process_batch returns None for the gateway-only rung WITHOUT emitting the + # misleading "no gateway backend" warning (the gateway IS configured; the rung + # is simply synchronous-only). _batch_fallback_warned stays False to prove it. + result = await incluster._process_batch( + b"%PDF-1.7", + "application/pdf", + "x.pdf", + dict(_IDENTITY), + _settings( + document_ocr_mode="batch", + document_ocr_provider="gateway", + embedding_gateway_url="https://gw", + document_ocr_incluster_model="surya/surya-ocr-2", + ), + ) + assert result is None + assert incluster._batch_fallback_warned is False + upstream = ocr.OcrProcessor() # gateway_only=False assert await upstream._get_batch_client() is not None assert called["n"] == 1 diff --git a/tests/unit/test_registry_tiering.py b/tests/unit/test_registry_tiering.py index 90a119e8..af7e28fb 100644 --- a/tests/unit/test_registry_tiering.py +++ b/tests/unit/test_registry_tiering.py @@ -479,7 +479,9 @@ async def test_process_tier_oversize_fails_fast(monkeypatch): reg_mod, "get_settings", lambda: _Settings(max_pdf_size_mb=0.001) ) r = _registry((_Fake("ocr-upstream", "ocr-upstream"), 5)) - res = await r.process_tier(b"x" * 4096, "application/pdf", "big.pdf", "ocr") + res = await r.process_tier( + b"x" * 4096, "application/pdf", "big.pdf", "ocr-upstream" + ) assert res.success is False assert res.metadata["parse_failed_reason"] == "oversize" @@ -724,7 +726,7 @@ def test_evaluate_escalation_terminal_when_ocr_unregistered_and_off(monkeypatch) def test_evaluate_escalation_empty_suppressed_even_when_structured_registered( monkeypatch, ): - """empty_text uses minimum='ocr', so it skips structured even when structured + """empty_text uses minimum='ocr-incluster', so it skips structured even when structured IS registered: with OCR off it suppresses to ocr, never hops to structured (a text extractor can't conjure text from a raster scan).""" monkeypatch.setattr(reg_mod, "record_document_classification", MagicMock())