Relax embedding compat check to vector_dim only

This commit is contained in:
Yiorgis Gozadinos 2026-05-13 13:58:52 +03:00
parent d11b8a4e6e
commit 701e9d3632
No known key found for this signature in database
4 changed files with 94 additions and 45 deletions

View file

@ -13,6 +13,7 @@
### Changed
- **Chat TUI streams markdown incrementally.** Assistant messages now use Textual's `MarkdownStream` (`Markdown.get_stream`) and write per-token deltas instead of re-parsing the entire accumulated message on every token. Removes the O(n²) re-parse that visibly stuttered long responses. Bumps `textual` floor to `>=8.2.4` so `Markdown.get_stream` is reachable via the public API.
- **Embedding compatibility check only raises on `vector_dim` mismatch.** `provider` and `name` drift (legitimate when the same model is served by a different stack, e.g. Ollama → vLLM-via-openai) now logs a one-time warning and updates the stored settings to match the current config. Subsequent opens are silent. Run `rebuild --embed-only` if you also want to re-embed under the new stack.
### Fixed

View file

@ -3,7 +3,9 @@
Configuration is done through YAML configuration files.
!!! note
If you create a db with certain settings and later change them, `haiku.rag` will detect incompatibilities (for example, if you change embedding provider) and will exit. You can **rebuild** the database to apply the new settings, see [Rebuild Database](../cli.md#rebuild-database).
haiku.rag enforces one hard rule on existing databases: the embedding `vector_dim` in your config must match the value stored in the db. A mismatch exits with `ConfigMismatchError` and you must **rebuild** to apply the change (see [Rebuild Database](../cli.md#rebuild-database)).
Changing only `provider` or `name` (e.g. switching from Ollama to vLLM serving the same model) is treated as soft drift: haiku.rag logs a one-time warning and updates the stored settings to match your config. Subsequent opens are silent. If the change was unintentional, revert your config to match the previously stored settings before running anything that depends on retrieval quality.
## Getting Started

View file

@ -1,7 +1,10 @@
import json
import logging
from haiku.rag.store.engine import SettingsRecord, Store, query_to_pydantic
logger = logging.getLogger(__name__)
class ConfigMismatchError(Exception):
"""Raised when stored config doesn't match current config."""
@ -99,7 +102,15 @@ class SettingsRepository:
await self.store.settings_table.add([settings_record])
async def validate_config_compatibility(self) -> None:
"""Validate that the current configuration is compatible with stored settings."""
"""Validate that the current configuration is compatible with stored settings.
``vector_dim`` mismatches raise corpus and query vectors must live in
the same dimensional space. ``provider`` and ``name`` mismatches are
treated as soft drift: legitimate when the same model is served by a
different stack (Ollama vs vLLM-via-openai, etc.). Surface the change
once via a warning and overwrite stored settings so the warning does
not fire on every subsequent open.
"""
stored_settings = await self.get_current_settings()
# If no stored settings, this is a new database - save current config and return
@ -109,13 +120,9 @@ class SettingsRepository:
current_config = self.store._config.model_dump(mode="json")
# Check if embedding provider or model has changed
# Both stored and current use nested structure: embeddings.model.{provider,name,vector_dim}
stored_embeddings = stored_settings.get("embeddings", {})
current_embeddings = current_config.get("embeddings", {})
stored_model_obj = stored_embeddings.get("model", {})
current_model_obj = current_embeddings.get("model", {})
stored_model_obj = stored_settings.get("embeddings", {}).get("model", {})
current_model_obj = current_config.get("embeddings", {}).get("model", {})
stored_provider = stored_model_obj.get("provider")
current_provider = current_model_obj.get("provider")
@ -126,28 +133,31 @@ class SettingsRepository:
stored_vector_dim = stored_model_obj.get("vector_dim")
current_vector_dim = current_model_obj.get("vector_dim")
# Check for incompatible changes
incompatible_changes = []
if stored_provider and stored_provider != current_provider:
incompatible_changes.append(
f"Stored (db) embedding provider: '{stored_provider}' -> Environment (current) embedding provider: '{current_provider}'"
)
if stored_model and stored_model != current_model:
incompatible_changes.append(
f"Stored (db) embedding model '{stored_model}' -> Environment (current) embedding model '{current_model}'"
)
if stored_vector_dim and stored_vector_dim != current_vector_dim:
incompatible_changes.append(
f"Stored (db) embedding vector dimension {stored_vector_dim} -> Environment (current) embedding vector dimension {current_vector_dim}"
)
if incompatible_changes:
error_msg = (
if (
stored_vector_dim
and current_vector_dim
and stored_vector_dim != current_vector_dim
):
raise ConfigMismatchError(
"Database configuration is incompatible with current settings:\n"
+ "\n".join(f" - {change}" for change in incompatible_changes)
f" - Stored (db) embedding vector dimension {stored_vector_dim} -> "
f"Environment (current) embedding vector dimension {current_vector_dim}\n"
"\nPlease rebuild the database using: haiku-rag rebuild"
)
error_msg += "\n\nPlease rebuild the database using: haiku-rag rebuild"
raise ConfigMismatchError(error_msg)
soft_changes: list[str] = []
if stored_provider and stored_provider != current_provider:
soft_changes.append(
f"provider: '{stored_provider}' -> '{current_provider}'"
)
if stored_model and stored_model != current_model:
soft_changes.append(f"model: '{stored_model}' -> '{current_model}'")
if soft_changes:
logger.warning(
"Embedding identity changed (vector_dim matches, stored settings "
"will be updated to match current config): %s. If this is not "
"intentional, revert your config to match the stored settings.",
"; ".join(soft_changes),
)
await self.save_current_settings()

View file

@ -1,3 +1,5 @@
import logging
import pytest
from haiku.rag.config import AppConfig, Config
@ -90,16 +92,27 @@ class TestValidateConfigCompatibility:
await settings_repo.validate_config_compatibility()
@pytest.mark.asyncio
async def test_provider_mismatch_raises_error(self, temp_db_path):
"""Different embedding provider raises ConfigMismatchError."""
async def test_provider_mismatch_warns_and_syncs(
self, temp_db_path, caplog, monkeypatch
):
"""Provider mismatch with matching vector_dim warns and overwrites stored.
Same model served by a different stack (Ollama vs vLLM via openai-compat)
legitimately differs in `provider`. Validation should surface the change
once, then trust the user's config as the new source of truth so the
warning doesn't fire on every subsequent open.
"""
from haiku.rag.store.engine import Store
from haiku.rag.store.repositories.settings import SettingsRepository
# Create store with default config (ollama)
# haiku.rag.logging.get_logger() sets propagate=False on the
# `haiku.rag` logger. caplog's handler attaches to root by default,
# so without restoring propagation the records never reach it.
monkeypatch.setattr(logging.getLogger("haiku.rag"), "propagate", True)
async with Store(temp_db_path, create=True):
pass
# Create new config with different provider
new_config = AppConfig()
new_config.embeddings.model.provider = "openai"
@ -108,24 +121,41 @@ class TestValidateConfigCompatibility:
) as store2:
settings_repo = SettingsRepository(store2)
with pytest.raises(ConfigMismatchError) as exc_info:
with caplog.at_level(logging.WARNING):
await settings_repo.validate_config_compatibility()
assert "embedding provider" in str(exc_info.value)
assert "ollama" in str(exc_info.value)
assert "openai" in str(exc_info.value)
# Warning surfaced the change
assert any(
"provider" in r.getMessage()
and "ollama" in r.getMessage()
and "openai" in r.getMessage()
for r in caplog.records
)
# Stored settings now match current
saved = await settings_repo.get_current_settings()
assert saved["embeddings"]["model"]["provider"] == "openai"
# Second open is silent — stored matches current.
caplog.clear()
with caplog.at_level(logging.WARNING):
await settings_repo.validate_config_compatibility()
assert not any("provider" in r.getMessage() for r in caplog.records)
@pytest.mark.asyncio
async def test_model_mismatch_raises_error(self, temp_db_path):
"""Different embedding model raises ConfigMismatchError."""
async def test_model_mismatch_warns_and_syncs(
self, temp_db_path, caplog, monkeypatch
):
"""Model name mismatch with matching vector_dim warns and overwrites stored."""
from haiku.rag.store.engine import Store
from haiku.rag.store.repositories.settings import SettingsRepository
# Create store with default config
# See test_provider_mismatch_warns_and_syncs for the propagate=True rationale.
monkeypatch.setattr(logging.getLogger("haiku.rag"), "propagate", True)
async with Store(temp_db_path, create=True):
pass
# Create new config with different model
new_config = AppConfig()
new_config.embeddings.model.name = "different-model"
@ -134,10 +164,16 @@ class TestValidateConfigCompatibility:
) as store2:
settings_repo = SettingsRepository(store2)
with pytest.raises(ConfigMismatchError) as exc_info:
with caplog.at_level(logging.WARNING):
await settings_repo.validate_config_compatibility()
assert "embedding model" in str(exc_info.value)
assert any(
"model" in r.getMessage() and "different-model" in r.getMessage()
for r in caplog.records
)
saved = await settings_repo.get_current_settings()
assert saved["embeddings"]["model"]["name"] == "different-model"
@pytest.mark.asyncio
async def test_vector_dim_mismatch_raises_error(self, temp_db_path):