From aa3e9406cf03ce06fa15824f80ee56d644877d2c Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Mon, 4 May 2026 13:12:19 +0300 Subject: [PATCH] auto-append /v1 to per-model Ollama base_url, fix flaky tests --- .../haiku/rag/embeddings/__init__.py | 4 +- haiku_rag_slim/haiku/rag/utils.py | 7 ++- .../test_rechunk_preserves_picture_data.yaml | 42 +++++++++++++++++ tests/store/test_document_items.py | 9 ++-- tests/test_config.py | 46 +++++++++++++++---- tests/test_embedder_config.py | 36 +++++++++++++++ tests/test_picture_in_context.py | 16 +++++-- tests/test_utils.py | 21 +++++++++ 8 files changed, 161 insertions(+), 20 deletions(-) create mode 100644 tests/cassettes/test_picture_in_context/test_rechunk_preserves_picture_data.yaml diff --git a/haiku_rag_slim/haiku/rag/embeddings/__init__.py b/haiku_rag_slim/haiku/rag/embeddings/__init__.py index 3032d4e9..e28f100e 100644 --- a/haiku_rag_slim/haiku/rag/embeddings/__init__.py +++ b/haiku_rag_slim/haiku/rag/embeddings/__init__.py @@ -167,7 +167,9 @@ def get_embedder(config: AppConfig = Config) -> EmbedderWrapper: if provider == "ollama": # Use model-level base_url if set, otherwise fall back to providers config - base_url = embedding_model.base_url or f"{config.providers.ollama.base_url}/v1" + base_url = embedding_model.base_url or config.providers.ollama.base_url + if not base_url.rstrip("/").endswith("/v1"): + base_url = base_url.rstrip("/") + "/v1" model = OpenAIEmbeddingModel( model_name, provider=OllamaProvider(base_url=base_url), diff --git a/haiku_rag_slim/haiku/rag/utils.py b/haiku_rag_slim/haiku/rag/utils.py index 90c67c14..ef7a41e2 100644 --- a/haiku_rag_slim/haiku/rag/utils.py +++ b/haiku_rag_slim/haiku/rag/utils.py @@ -158,8 +158,11 @@ def get_model( model_settings, OpenAIChatModelSettings, model_config ) - # Use model-level base_url if set, otherwise fall back to providers config - base_url = model_config.base_url or f"{app_config.providers.ollama.base_url}/v1" + # Ollama's OpenAI-compatible API lives under /v1. Append it if the + # configured base_url doesn't already include it. + base_url = model_config.base_url or app_config.providers.ollama.base_url + if not base_url.rstrip("/").endswith("/v1"): + base_url = base_url.rstrip("/") + "/v1" return OpenAIChatModel( model_name=model, diff --git a/tests/cassettes/test_picture_in_context/test_rechunk_preserves_picture_data.yaml b/tests/cassettes/test_picture_in_context/test_rechunk_preserves_picture_data.yaml new file mode 100644 index 00000000..f0943250 --- /dev/null +++ b/tests/cassettes/test_picture_in_context/test_rechunk_preserves_picture_data.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '81' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - Hello world + model: qwen3-embedding:4b + uri: http://localhost:11434/v1/embeddings + response: + headers: + content-type: + - application/json + transfer-encoding: + - chunked + parsed_body: + data: + - embedding: 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 + index: 0 + object: embedding + model: qwen3-embedding:4b + object: list + usage: + prompt_tokens: 3 + total_tokens: 3 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/store/test_document_items.py b/tests/store/test_document_items.py index 24ead8b7..ab7636b6 100644 --- a/tests/store/test_document_items.py +++ b/tests/store/test_document_items.py @@ -5,6 +5,7 @@ from haiku.rag.client.documents import ( _store_document_with_chunks, _update_document_with_chunks, ) +from haiku.rag.config import AppConfig from haiku.rag.store.engine import Store from haiku.rag.store.models.document_item import ( DocumentItem, @@ -646,9 +647,11 @@ class TestPictureDataPreservedThroughRoundTrip: docling_doc = _docling_doc_with_picture() - async with HaikuRAG(temp_db_path, create=True) as rag: - # Preservation only kicks in under modes that retain picture bytes. - rag._config.processing.pictures = "image" + config = AppConfig() + # Preservation only kicks in under modes that retain picture bytes. + config.processing.pictures = "image" + + async with HaikuRAG(temp_db_path, config=config, create=True) as rag: document = Document(content="Hello world", uri="test://doc") document.set_docling(docling_doc) created = await _store_document_with_chunks(rag, document, [], docling_doc) diff --git a/tests/test_config.py b/tests/test_config.py index 87a50dcf..ccae77fa 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,3 +1,5 @@ +import logging + import pytest import yaml @@ -7,6 +9,16 @@ from haiku.rag.config.loader import ( generate_default_config, load_yaml_config, ) +from haiku.rag.config.loader import logger as loader_logger + + +class _ListHandler(logging.Handler): + def __init__(self) -> None: + super().__init__(level=logging.WARNING) + self.records: list[logging.LogRecord] = [] + + def emit(self, record: logging.LogRecord) -> None: + self.records.append(record) def test_load_yaml_config(tmp_path): @@ -237,7 +249,7 @@ def _write(tmp_path, body: str): return p -def test_load_yaml_legacy_picture_description_maps_to_description(tmp_path, caplog): +def test_load_yaml_legacy_picture_description_maps_to_description(tmp_path): """`picture_description.enabled=true` (with or without the image flag) maps to `processing.pictures: description`.""" config_file = _write( @@ -251,15 +263,21 @@ processing: timeout: 120 """, ) - with caplog.at_level("WARNING", logger="haiku.rag.config.loader"): + handler = _ListHandler() + loader_logger.addHandler(handler) + try: data = load_yaml_config(config_file) + finally: + loader_logger.removeHandler(handler) cfg = AppConfig.model_validate(data) assert cfg.processing.pictures == "description" assert cfg.processing.conversion_options.picture_description.timeout == 120 - assert any("picture_description.enabled=true" in m.message for m in caplog.records) + assert any( + "picture_description.enabled=true" in r.getMessage() for r in handler.records + ) -def test_load_yaml_legacy_generate_picture_images_maps_to_image(tmp_path, caplog): +def test_load_yaml_legacy_generate_picture_images_maps_to_image(tmp_path): """`generate_picture_images=true` alone maps to `pictures: image`.""" config_file = _write( tmp_path, @@ -269,14 +287,20 @@ processing: generate_picture_images: true """, ) - with caplog.at_level("WARNING", logger="haiku.rag.config.loader"): + handler = _ListHandler() + loader_logger.addHandler(handler) + try: data = load_yaml_config(config_file) + finally: + loader_logger.removeHandler(handler) cfg = AppConfig.model_validate(data) assert cfg.processing.pictures == "image" - assert any("generate_picture_images=true" in m.message for m in caplog.records) + assert any( + "generate_picture_images=true" in r.getMessage() for r in handler.records + ) -def test_load_yaml_no_legacy_fields_keeps_default_none(tmp_path, caplog): +def test_load_yaml_no_legacy_fields_keeps_default_none(tmp_path): """Empty processing block leaves the default `none` mode untouched and does not warn.""" config_file = _write( @@ -286,11 +310,15 @@ processing: chunk_size: 256 """, ) - with caplog.at_level("WARNING", logger="haiku.rag.config.loader"): + handler = _ListHandler() + loader_logger.addHandler(handler) + try: data = load_yaml_config(config_file) + finally: + loader_logger.removeHandler(handler) cfg = AppConfig.model_validate(data) assert cfg.processing.pictures == "none" - assert not caplog.records + assert not handler.records def test_load_yaml_explicit_pictures_wins_over_legacy(tmp_path): diff --git a/tests/test_embedder_config.py b/tests/test_embedder_config.py index f65f629b..ee620dfd 100644 --- a/tests/test_embedder_config.py +++ b/tests/test_embedder_config.py @@ -75,3 +75,39 @@ def test_unsupported_provider_raises(): with pytest.raises(ValueError, match="Unsupported embedding provider"): get_embedder(custom_config) + + +def test_ollama_embedder_appends_v1_when_missing(): + """Per-model base_url without /v1 should get it appended for Ollama.""" + config = AppConfig( + embeddings=EmbeddingsConfig( + model=EmbeddingModelConfig( + provider="ollama", + name="qwen3-embedding:4b", + vector_dim=2560, + base_url="http://my-ollama:11434", + ), + ), + ) + embedder = get_embedder(config) + pa_model = embedder._embedder._model # type: ignore[union-attr] # ty: ignore[unresolved-attribute] + assert str(pa_model.base_url).rstrip("/").endswith("/v1") # type: ignore[union-attr] # ty: ignore[unresolved-attribute] + + +def test_ollama_embedder_does_not_double_append_v1(): + """If the user already includes /v1 we leave it alone.""" + config = AppConfig( + embeddings=EmbeddingsConfig( + model=EmbeddingModelConfig( + provider="ollama", + name="qwen3-embedding:4b", + vector_dim=2560, + base_url="http://my-ollama:11434/v1", + ), + ), + ) + embedder = get_embedder(config) + pa_model = embedder._embedder._model # type: ignore[union-attr] # ty: ignore[unresolved-attribute] + url = str(pa_model.base_url).rstrip("/") # type: ignore[union-attr] # ty: ignore[unresolved-attribute] + assert url.endswith("/v1") + assert not url.endswith("/v1/v1") diff --git a/tests/test_picture_in_context.py b/tests/test_picture_in_context.py index 8ccf9229..6fdd7ddb 100644 --- a/tests/test_picture_in_context.py +++ b/tests/test_picture_in_context.py @@ -12,7 +12,7 @@ from pydantic_ai.usage import RunUsage from haiku.rag.client import HaikuRAG from haiku.rag.client.search import _populate_image_data -from haiku.rag.config import Config +from haiku.rag.config import AppConfig, Config from haiku.rag.context import expand_with_items from haiku.rag.store.models.chunk import SearchResult from haiku.rag.store.models.document_item import DocumentItem @@ -167,6 +167,7 @@ async def test_expand_context_preserves_picture_refs_with_empty_text(temp_db_pat assert "picture" in out.labels +@pytest.mark.vcr() @pytest.mark.asyncio async def test_rechunk_preserves_picture_data(temp_db_path): """``rebuild --rechunk`` keeps ``picture_data`` for every picture row.""" @@ -177,8 +178,10 @@ async def test_rechunk_preserves_picture_data(temp_db_path): docling_doc = _docling_doc_with_picture() - async with HaikuRAG(temp_db_path, create=True) as rag: - rag._config.processing.pictures = "image" + config = AppConfig() + config.processing.pictures = "image" + + async with HaikuRAG(temp_db_path, config=config, create=True) as rag: document = Document(content="x", uri="test://doc") document.set_docling(docling_doc) created = await _store_document_with_chunks(rag, document, [], docling_doc) @@ -193,6 +196,7 @@ async def test_rechunk_preserves_picture_data(temp_db_path): assert after.get("#/pictures/0") == before.get("#/pictures/0") +@pytest.mark.vcr() @pytest.mark.asyncio async def test_update_clears_picture_data_when_mode_none(temp_db_path): """Switching to ``pictures="none"`` and re-running update_document @@ -207,9 +211,11 @@ async def test_update_clears_picture_data_when_mode_none(temp_db_path): docling_doc = _docling_doc_with_picture() - async with HaikuRAG(temp_db_path, create=True) as rag: + config = AppConfig() + config.processing.pictures = "image" + + async with HaikuRAG(temp_db_path, config=config, create=True) as rag: # Ingest under "image" so picture bytes land in document_items. - rag._config.processing.pictures = "image" document = Document(content="x", uri="test://doc") document.set_docling(docling_doc) created = await _store_document_with_chunks(rag, document, [], docling_doc) diff --git a/tests/test_utils.py b/tests/test_utils.py index f598163a..6aa35244 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -162,6 +162,27 @@ def test_get_model_ollama_with_settings(): assert isinstance(result, OpenAIChatModel) +def test_get_model_ollama_appends_v1_to_per_model_base_url(): + """Per-model base_url without /v1 should get it appended.""" + model_config = ModelConfig( + provider="ollama", name="qwen3.6", base_url="http://my-ollama:11434" + ) + result = get_model(model_config) + assert isinstance(result, OpenAIChatModel) + assert str(result.client.base_url).rstrip("/").endswith("/v1") + + +def test_get_model_ollama_does_not_double_append_v1(): + """If the per-model base_url already ends with /v1, leave it alone.""" + model_config = ModelConfig( + provider="ollama", name="qwen3.6", base_url="http://my-ollama:11434/v1" + ) + result = get_model(model_config) + url = str(result.client.base_url).rstrip("/") + assert url.endswith("/v1") + assert not url.endswith("/v1/v1") + + def test_get_model_openai(): """Test get_model returns OpenAIChatModel for OpenAI.""" model_config = ModelConfig(provider="openai", name="gpt-4o")