fix(ingest): suppress misleading batch-fallback warn for in-cluster rung

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) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-06-18 00:48:10 +02:00
co-authored by Claude Opus 4.8
parent 6e32bd9561
commit ebbf905dc5
4 changed files with 32 additions and 7 deletions
@@ -2,7 +2,7 @@
The escalation ladder is the cheapest-first ordering of extraction tiers: 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 It mirrors the ``tier`` vocabulary documented on
:meth:`DocumentProcessor.tier <.base.DocumentProcessor.tier>` and the :meth:`DocumentProcessor.tier <.base.DocumentProcessor.tier>` and the
@@ -499,6 +499,11 @@ class OcrProcessor(DocumentProcessor):
return None return None
client = await self._get_batch_client() client = await self._get_batch_client()
if client is None: if client is None:
# 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( self._batch_fallback(
"no gateway backend (provider=mistral or EMBEDDING_GATEWAY_URL unset)", "no gateway backend (provider=mistral or EMBEDDING_GATEWAY_URL unset)",
filename, filename,
+18
View File
@@ -467,6 +467,24 @@ async def test_gateway_only_processor_never_uses_batch_mode(monkeypatch):
assert await incluster._get_batch_client() is None assert await incluster._get_batch_client() is None
assert called["n"] == 0 # short-circuited before building anything 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 upstream = ocr.OcrProcessor() # gateway_only=False
assert await upstream._get_batch_client() is not None assert await upstream._get_batch_client() is not None
assert called["n"] == 1 assert called["n"] == 1
+4 -2
View File
@@ -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) reg_mod, "get_settings", lambda: _Settings(max_pdf_size_mb=0.001)
) )
r = _registry((_Fake("ocr-upstream", "ocr-upstream"), 5)) 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.success is False
assert res.metadata["parse_failed_reason"] == "oversize" 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( def test_evaluate_escalation_empty_suppressed_even_when_structured_registered(
monkeypatch, 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 IS registered: with OCR off it suppresses to ocr, never hops to structured
(a text extractor can't conjure text from a raster scan).""" (a text extractor can't conjure text from a raster scan)."""
monkeypatch.setattr(reg_mod, "record_document_classification", MagicMock()) monkeypatch.setattr(reg_mod, "record_document_classification", MagicMock())