diff --git a/haiku_rag_slim/haiku/rag/ingester/api/routes/providers.py b/haiku_rag_slim/haiku/rag/ingester/api/routes/providers.py index b2b9dc73..e984ff75 100644 --- a/haiku_rag_slim/haiku/rag/ingester/api/routes/providers.py +++ b/haiku_rag_slim/haiku/rag/ingester/api/routes/providers.py @@ -29,9 +29,16 @@ async def _probe(client: httpx.AsyncClient, base_url: str) -> ProviderEndpoint: @router.get("/providers", response_model=ProvidersResponse) async def providers(state: APIState = Depends(get_state)) -> ProvidersResponse: - """Probe configured external providers (currently docling-serve) and - return their reachability. Dashboard polls this to surface a downstream - outage that the ingester itself can only see via worker job failures.""" + """Probe external providers actually in use by the current converter / + chunker and return their reachability. Dashboard polls this to surface a + downstream outage that the ingester itself can only see via worker job + failures.""" + processing = state.config.processing + uses_docling_serve = ( + processing.converter == "docling-serve" or processing.chunker == "docling-serve" + ) + if not uses_docling_serve: + return ProvidersResponse(docling_serve=[]) base_urls = state.config.providers.docling_serve.base_urls async with httpx.AsyncClient(timeout=_PROBE_TIMEOUT_S) as client: results = await asyncio.gather(*(_probe(client, u) for u in base_urls)) diff --git a/tests/ingester/test_api.py b/tests/ingester/test_api.py index 4e5143d9..9146f73b 100644 --- a/tests/ingester/test_api.py +++ b/tests/ingester/test_api.py @@ -538,6 +538,8 @@ async def test_providers_probes_each_docling_serve_url(state, monkeypatch): return ProviderEndpoint(base_url=base_url, reachable=True, status_code=200) monkeypatch.setattr(providers_mod, "_probe", _fake_probe) + state.config.processing.converter = "docling-serve" + state.config.processing.chunker = "docling-serve" state.config.providers.docling_serve.base_url = [ "http://docling-serve-up:5001", "http://docling-serve-down:5001", @@ -556,6 +558,49 @@ async def test_providers_probes_each_docling_serve_url(state, monkeypatch): assert "Name or service not known" in body["docling_serve"][1]["error"] +@pytest.mark.asyncio +async def test_providers_skips_docling_serve_when_not_in_use(state, monkeypatch): + """With docling-local for both converter and chunker, /providers + returns an empty docling_serve list — and crucially does not probe.""" + from haiku.rag.ingester.api.routes import providers as providers_mod + + probed: list[str] = [] + + async def _spy_probe(client, base_url): # pragma: no cover - asserted not called + probed.append(base_url) + raise AssertionError("probe should not run when docling-serve is not in use") + + monkeypatch.setattr(providers_mod, "_probe", _spy_probe) + state.config.processing.converter = "docling-local" + state.config.processing.chunker = "docling-local" + async with _client(state) as client: + resp = await client.get("/providers") + assert resp.status_code == 200 + assert resp.json() == {"docling_serve": []} + assert probed == [] + + +@pytest.mark.asyncio +async def test_providers_probes_when_only_chunker_uses_docling_serve( + state, monkeypatch +): + """A mixed config (local converter + docling-serve chunker, or vice versa) + still counts as 'in use' and triggers the probe.""" + from haiku.rag.ingester.api.routes import providers as providers_mod + from haiku.rag.ingester.api.schemas import ProviderEndpoint + + async def _fake_probe(client, base_url): + return ProviderEndpoint(base_url=base_url, reachable=True, status_code=200) + + monkeypatch.setattr(providers_mod, "_probe", _fake_probe) + state.config.processing.converter = "docling-local" + state.config.processing.chunker = "docling-serve" + async with _client(state) as client: + resp = await client.get("/providers") + assert resp.status_code == 200 + assert len(resp.json()["docling_serve"]) == 1 + + @pytest.mark.asyncio async def test_providers_probe_with_real_httpx_transport(): """End-to-end through the actual _probe — MockTransport drives the