From bec56a451d8e1674d1e768eed8ece94c6bf80509 Mon Sep 17 00:00:00 2001 From: Jordan Dubrick Date: Tue, 21 Jul 2026 14:30:08 -0400 Subject: [PATCH 01/10] add vector store config classes and test Signed-off-by: Jordan Dubrick --- src/models/config.py | 195 +++++++++++++++++- .../models/config/test_dump_configuration.py | 10 + .../config/test_vector_store_providers.py | 190 +++++++++++++++++ tests/unit/utils/test_models_dumper.py | 4 + 4 files changed, 398 insertions(+), 1 deletion(-) create mode 100644 tests/unit/models/config/test_vector_store_providers.py diff --git a/src/models/config.py b/src/models/config.py index 28ff23264..d93e2897c 100644 --- a/src/models/config.py +++ b/src/models/config.py @@ -7,7 +7,7 @@ from functools import cached_property from pathlib import Path from re import Pattern -from typing import Any, Literal, Optional, Self +from typing import Annotated, Any, Literal, Optional, Self import jsonpath_ng import yaml @@ -2129,6 +2129,159 @@ def validate_rag_type_fields(self) -> Self: return self +class FaissVectorStoreProviderConfig(ConfigurationBase): + """Storage config for a FAISS dynamic vector-store provider.""" + + path: str = Field( + ..., + min_length=1, + title="DB path", + description="On-disk FAISS/SQLite path for this provider.", + ) + + +class PgvectorVectorStoreProviderConfig(ConfigurationBase): + """Storage config for a pgvector dynamic vector-store provider.""" + + host: Optional[str] = Field( + default=None, + title="PostgreSQL host", + description="PostgreSQL host. Defaults to ${env.POSTGRES_HOST}.", + ) + + port: Optional[str] = Field( + default=None, + title="PostgreSQL port", + description="PostgreSQL port. Defaults to ${env.POSTGRES_PORT}.", + ) + + db: Optional[str] = Field( + default=None, + title="PostgreSQL database", + description="PostgreSQL database name. Defaults to ${env.POSTGRES_DATABASE}.", + ) + + user: Optional[str] = Field( + default=None, + title="PostgreSQL user", + description="PostgreSQL user. Defaults to ${env.POSTGRES_USER}.", + ) + + password: Optional[SecretStr] = Field( + default=None, + title="PostgreSQL password", + description="PostgreSQL password. Defaults to ${env.POSTGRES_PASSWORD}.", + ) + + @model_validator(mode="after") + def apply_pgvector_env_defaults(self) -> Self: + """Fill unset connection fields with ${env.POSTGRES_*} references.""" + pgvector_defaults: dict[str, str | SecretStr] = { + "host": "${env.POSTGRES_HOST}", + "port": "${env.POSTGRES_PORT}", + "db": "${env.POSTGRES_DATABASE}", + "user": "${env.POSTGRES_USER}", + "password": SecretStr("${env.POSTGRES_PASSWORD}"), + } + for field_name, default_value in pgvector_defaults.items(): + if getattr(self, field_name) is None: + object.__setattr__(self, field_name, default_value) + return self + + +class VectorStoreProviderBase(ConfigurationBase): + """Shared fields for dynamic vector-store provider capacity entries.""" + + id: str = Field( + ..., + min_length=1, + title="Provider ID", + description="Llama Stack vector_io provider_id (emitted as-is).", + ) + embedding_model: str = Field( + ..., + min_length=1, + title="Embedding model", + description="Embedding model identification used for stores created " + "against this provider.", + ) + embedding_dimension: PositiveInt = Field( + ..., + title="Embedding dimension", + description="Dimensionality of embedding vectors for this provider.", + ) + default: bool = Field( + False, + title="Default provider", + description="When true, this entry drives vector_stores.default_* " + "in the synthesized Llama Stack config. Exactly one entry must set " + "this when vector_store_providers is non-empty.", + ) + + @field_validator("id") + @classmethod + def validate_id(cls, value: str) -> str: + """Validate and normalize the provider id. + + Parameters: + value: Raw provider id from configuration. + + Returns: + Stripped provider id. + + Raises: + ValueError: If the id is empty, uses the reserved ``byok_`` prefix, + or contains characters outside ``[a-z0-9_-]``. + """ + stripped = value.strip() + if not stripped: + raise ValueError("id must be non-empty after stripping whitespace") + if stripped.startswith("byok_"): + raise ValueError("id must not start with 'byok_' (reserved for BYOK RAG)") + if not re.fullmatch(r"[a-z0-9_-]+", stripped): + raise ValueError( + "id may contain only lowercase letters, digits, " + "underscores, and hyphens" + ) + return stripped + + +class FaissVectorStoreProvider(VectorStoreProviderBase): + """Dynamic FAISS vector-store provider (runtime create capacity).""" + + type: Literal["faiss"] = Field( + "faiss", + title="Provider type", + description="Product type for this dynamic vector-store provider.", + ) + config: FaissVectorStoreProviderConfig = Field( + ..., + title="Storage config", + description="FAISS storage settings for this provider.", + ) + + +class PgvectorVectorStoreProvider(VectorStoreProviderBase): + """Dynamic pgvector vector-store provider (runtime create capacity).""" + + type: Literal["pgvector"] = Field( + "pgvector", + title="Provider type", + description="Product type for this dynamic vector-store provider.", + ) + config: PgvectorVectorStoreProviderConfig = Field( + ..., + title="Storage config", + description="pgvector connection settings for this provider.", + ) + + +VectorStoreProvider = Annotated[ + FaissVectorStoreProvider | PgvectorVectorStoreProvider, + Field(discriminator="type"), +] + + class QuotaLimiterConfiguration(ConfigurationBase): """Configuration for one quota limiter. @@ -2708,6 +2861,16 @@ class Configuration(ConfigurationBase): "reconfigure Llama Stack through its run.yaml configuration file", ) + vector_store_providers: list[VectorStoreProvider] = Field( + default_factory=list, + title="Vector store providers", + description=( + "Dynamic vector-store provider capacity for runtime " + "POST /v1/vector-stores creates. " + "Not the same as byok_rag (static registered corpora)." + ), + ) + a2a_state: A2AStateConfiguration = Field( default_factory=A2AStateConfiguration, title="A2A state configuration", @@ -2775,6 +2938,36 @@ class Configuration(ConfigurationBase): "maximum prompts per user, display name length, and content length.", ) + @model_validator(mode="after") + def validate_vector_store_providers(self) -> Self: + """Validate vector_store_providers list constraints. + + When the list is non-empty, requires unique ids and exactly one + entry with ``default: true``. + + Returns: + Self: The validated configuration instance. + + Raises: + ValueError: If ids are duplicated or the default count is not + exactly one for a non-empty list. + """ + providers = self.vector_store_providers + if not providers: + return self + + ids = [provider.id for provider in providers] + if len(ids) != len(set(ids)): + raise ValueError(f"vector_store_providers ids must be unique; got {ids}") + + defaults = [provider.id for provider in providers if provider.default] + if len(defaults) != 1: + raise ValueError( + "vector_store_providers must set default: true on exactly one " + f"entry when the list is non-empty; found defaults on: {defaults}" + ) + return self + @model_validator(mode="after") def validate_mcp_auth_headers(self) -> Self: """ diff --git a/tests/unit/models/config/test_dump_configuration.py b/tests/unit/models/config/test_dump_configuration.py index e1cd67816..6cb852806 100644 --- a/tests/unit/models/config/test_dump_configuration.py +++ b/tests/unit/models/config/test_dump_configuration.py @@ -194,6 +194,7 @@ def test_dump_configuration_minimal_cfg(tmp_path: Path) -> None: }, "approvals": _DEFAULT_APPROVALS_DUMP, "byok_rag": [], + "vector_store_providers": [], "quota_handlers": { "sqlite": None, "postgres": None, @@ -417,6 +418,7 @@ def test_dump_configuration_valid_values(tmp_path: Path) -> None: }, "approvals": _DEFAULT_APPROVALS_DUMP, "byok_rag": [], + "vector_store_providers": [], "quota_handlers": { "sqlite": None, "postgres": None, @@ -776,6 +778,7 @@ def test_dump_configuration_with_quota_limiters(tmp_path: Path) -> None: }, "approvals": _DEFAULT_APPROVALS_DUMP, "byok_rag": [], + "vector_store_providers": [], "quota_handlers": { "sqlite": None, "postgres": None, @@ -1034,6 +1037,7 @@ def test_dump_configuration_with_quota_limiters_different_values( }, "approvals": _DEFAULT_APPROVALS_DUMP, "byok_rag": [], + "vector_store_providers": [], "quota_handlers": { "sqlite": None, "postgres": None, @@ -1287,6 +1291,7 @@ def test_dump_configuration_byok(tmp_path: Path) -> None: "password": None, }, ], + "vector_store_providers": [], "quota_handlers": { "sqlite": None, "postgres": None, @@ -1505,6 +1510,7 @@ def test_dump_configuration_pg_namespace(tmp_path: Path) -> None: }, "approvals": _DEFAULT_APPROVALS_DUMP, "byok_rag": [], + "vector_store_providers": [], "quota_handlers": { "sqlite": None, "postgres": None, @@ -1883,6 +1889,7 @@ def test_dump_configuration_allow_degraded_mode(tmp_path: Path) -> None: }, "approvals": _DEFAULT_APPROVALS_DUMP, "byok_rag": [], + "vector_store_providers": [], "quota_handlers": { "sqlite": None, "postgres": None, @@ -2107,6 +2114,7 @@ def test_dump_configuration_max_retries_settings(tmp_path: Path) -> None: }, "approvals": _DEFAULT_APPROVALS_DUMP, "byok_rag": [], + "vector_store_providers": [], "quota_handlers": { "sqlite": None, "postgres": None, @@ -2331,6 +2339,7 @@ def test_dump_configuration_retry_count_settings(tmp_path: Path) -> None: }, "approvals": _DEFAULT_APPROVALS_DUMP, "byok_rag": [], + "vector_store_providers": [], "quota_handlers": { "sqlite": None, "postgres": None, @@ -2562,6 +2571,7 @@ def test_dump_configuration_specific_compaction_values(tmp_path: Path) -> None: }, "approvals": _DEFAULT_APPROVALS_DUMP, "byok_rag": [], + "vector_store_providers": [], "quota_handlers": { "sqlite": None, "postgres": None, diff --git a/tests/unit/models/config/test_vector_store_providers.py b/tests/unit/models/config/test_vector_store_providers.py new file mode 100644 index 000000000..f040785d5 --- /dev/null +++ b/tests/unit/models/config/test_vector_store_providers.py @@ -0,0 +1,190 @@ +"""Unit tests for vector_store_providers config models.""" + +import copy +from typing import Any + +import pytest +import yaml +from pydantic import SecretStr, TypeAdapter, ValidationError + +from models.config import Configuration, VectorStoreProvider + +_PROVIDER_ADAPTER = TypeAdapter(VectorStoreProvider) + +_BASE_CONFIG_PATH = "tests/configuration/lightspeed-stack.yaml" + + +def _base_config_dict() -> dict[str, Any]: + """Load the base lightspeed-stack.yaml fixture as a fresh dict.""" + with open(_BASE_CONFIG_PATH, "r", encoding="utf-8") as file: + return copy.deepcopy(yaml.safe_load(file)) + + +def _faiss_provider( + *, + provider_id: str = "notebooks", + default: bool = False, + path: str = "/var/lib/notebooks.db", +) -> dict[str, Any]: + """Return a minimal valid faiss vector_store_providers entry.""" + return { + "id": provider_id, + "type": "faiss", + "default": default, + "embedding_model": "/rag-content/embeddings_model", + "embedding_dimension": 768, + "config": {"path": path}, + } + + +def test_faiss_provider_accepts_nested_config() -> None: + """Faiss provider accepts nested config and required embedding fields.""" + provider = _PROVIDER_ADAPTER.validate_python( + { + "id": "notebooks", + "type": "faiss", + "embedding_model": "/rag-content/embeddings_model", + "embedding_dimension": 768, + "default": True, + "config": {"path": "/var/lib/notebooks.db"}, + } + ) + assert provider.id == "notebooks" + assert provider.type == "faiss" + assert provider.config.path == "/var/lib/notebooks.db" + assert provider.embedding_dimension == 768 + assert provider.default is True + + +def test_faiss_requires_path() -> None: + """Faiss provider rejects missing path under config.""" + with pytest.raises(ValidationError): + _PROVIDER_ADAPTER.validate_python( + { + "id": "notebooks", + "type": "faiss", + "embedding_model": "/emb", + "embedding_dimension": 768, + "config": {}, + } + ) + + +def test_requires_embedding_model_and_dimension() -> None: + """Provider entry requires embedding_model and embedding_dimension.""" + with pytest.raises(ValidationError): + _PROVIDER_ADAPTER.validate_python( + { + "id": "notebooks", + "type": "faiss", + "config": {"path": "/tmp/x.db"}, + } + ) + + +def test_pgvector_applies_env_defaults() -> None: + """Pgvector provider fills unset connection fields with env placeholders.""" + provider = _PROVIDER_ADAPTER.validate_python( + { + "id": "nb-pg", + "type": "pgvector", + "embedding_model": "/emb", + "embedding_dimension": 768, + "default": True, + "config": {}, + } + ) + assert provider.config.host == "${env.POSTGRES_HOST}" + assert provider.config.password == SecretStr("${env.POSTGRES_PASSWORD}") + + +def test_rejects_byok_prefix_id() -> None: + """Provider id must not use the byok_ prefix reserved for BYOK RAG.""" + with pytest.raises(ValidationError, match="byok_"): + _PROVIDER_ADAPTER.validate_python( + { + "id": "byok_notebooks", + "type": "faiss", + "embedding_model": "/emb", + "embedding_dimension": 768, + "config": {"path": "/tmp/x.db"}, + } + ) + + +def test_rejects_invalid_id_chars() -> None: + """Provider id must match [a-z0-9_-]+.""" + with pytest.raises(ValidationError): + _PROVIDER_ADAPTER.validate_python( + { + "id": "NoteBooks", + "type": "faiss", + "embedding_model": "/emb", + "embedding_dimension": 768, + "config": {"path": "/tmp/x.db"}, + } + ) + + +def test_rejects_unknown_type() -> None: + """Unknown product type values are rejected.""" + with pytest.raises(ValidationError): + _PROVIDER_ADAPTER.validate_python( + { + "id": "x", + "type": "chroma", + "embedding_model": "/emb", + "embedding_dimension": 768, + "config": {"path": "/tmp/x.db"}, + } + ) + + +def test_rejects_non_empty_list_with_no_default() -> None: + """Non-empty list with no default: true is rejected.""" + config_dict = _base_config_dict() + config_dict["vector_store_providers"] = [_faiss_provider(default=False)] + with pytest.raises(ValidationError, match="default"): + Configuration(**config_dict) + + +def test_rejects_multiple_defaults() -> None: + """Non-empty list with more than one default: true is rejected.""" + config_dict = _base_config_dict() + config_dict["vector_store_providers"] = [ + _faiss_provider(provider_id="a", default=True, path="/tmp/a.db"), + _faiss_provider(provider_id="b", default=True, path="/tmp/b.db"), + ] + with pytest.raises(ValidationError, match="default"): + Configuration(**config_dict) + + +def test_duplicate_ids_rejected() -> None: + """Provider ids must be unique within vector_store_providers.""" + config_dict = _base_config_dict() + config_dict["vector_store_providers"] = [ + _faiss_provider(provider_id="notebooks", default=True, path="/tmp/a.db"), + _faiss_provider(provider_id="notebooks", default=False, path="/tmp/b.db"), + ] + with pytest.raises(ValidationError, match="unique|duplicate"): + Configuration(**config_dict) + + +def test_empty_list_ok_without_default() -> None: + """Empty vector_store_providers list is valid without a designated default.""" + config_dict = _base_config_dict() + config_dict["vector_store_providers"] = [] + cfg = Configuration(**config_dict) + assert cfg.vector_store_providers == [] + + +def test_single_default_provider_accepted() -> None: + """Non-empty list with exactly one default: true validates.""" + config_dict = _base_config_dict() + config_dict["vector_store_providers"] = [ + _faiss_provider(provider_id="notebooks", default=True), + _faiss_provider(provider_id="other", default=False, path="/tmp/other.db"), + ] + cfg = Configuration(**config_dict) + by_id = {provider.id: provider.default for provider in cfg.vector_store_providers} + assert by_id == {"notebooks": True, "other": False} diff --git a/tests/unit/utils/test_models_dumper.py b/tests/unit/utils/test_models_dumper.py index 2902fa25f..db6c55e13 100644 --- a/tests/unit/utils/test_models_dumper.py +++ b/tests/unit/utils/test_models_dumper.py @@ -9165,6 +9165,8 @@ def test_dump_models(tmpdir: Path) -> None: "FeedbackStatusUpdateResponse", "FileResponse", "FileTooLargeResponse", + "FaissVectorStoreProvider", + "FaissVectorStoreProviderConfig", "ForbiddenResponse", "HealthStatus", "InMemoryCacheConfig", @@ -9236,6 +9238,8 @@ def test_dump_models(tmpdir: Path) -> None: "OpenAIResponseUsageOutputTokensDetails", "OpenAITokenLogProb", "OpenAITopLogProb", + "PgvectorVectorStoreProvider", + "PgvectorVectorStoreProviderConfig", "PostgreSQLDatabaseConfiguration", "PromptCreateRequest", "PromptDeleteResponse", From 992f07915ff61a5ab4ee16fde8af1f5e54f043b4 Mon Sep 17 00:00:00 2001 From: Jordan Dubrick Date: Tue, 21 Jul 2026 15:00:47 -0400 Subject: [PATCH 02/10] add vector stores to synthesizer Signed-off-by: Jordan Dubrick --- src/llama_stack_configuration.py | 218 +++++++++++++++++++ tests/unit/test_llama_stack_configuration.py | 198 +++++++++++++++++ 2 files changed, 416 insertions(+) diff --git a/src/llama_stack_configuration.py b/src/llama_stack_configuration.py index 4e82bfbf8..ea4695478 100644 --- a/src/llama_stack_configuration.py +++ b/src/llama_stack_configuration.py @@ -81,6 +81,11 @@ }, } +VECTOR_STORE_PROVIDER_TYPE_MAP: dict[str, str] = { + "faiss": "inline::faiss", + "pgvector": "remote::pgvector", +} + class YamlDumper(yaml.Dumper): # pylint: disable=too-many-ancestors """Custom YAML dumper with proper indentation levels.""" @@ -501,6 +506,219 @@ def enrich_byok_rag(ls_config: dict[str, Any], byok_rag: list[dict[str, Any]]) - ) +# ============================================================================= +# Enrichment: vector_store_providers +# ============================================================================= + + +def _designated_vector_store_provider( + providers: list[dict[str, Any]], +) -> dict[str, Any] | None: + """Return the provider that should drive vector_stores.default_*. + + Parameters: + providers: High-level vector_store_providers entries. + + Returns: + The entry with ``default: true``, or None when none is marked. + """ + for provider in providers: + if provider.get("default"): + return provider + return None + + +def _upsert_vsprov_embedding_model( + ls_config: dict[str, Any], + provider_id: str, + embedding_model: str, + embedding_dimension: int | None, +) -> None: + """Register an embedding model if provider_model_id is not already present. + + Dedupes against BYOK/baseline rows by ``provider_model_id`` (after stripping + a leading ``sentence-transformers/`` prefix). + + Parameters: + ls_config: Llama Stack configuration modified in place. + provider_id: Dynamic provider id used to name the model row. + embedding_model: Configured embedding model path or id. + embedding_dimension: Embedding vector dimensionality. + """ + models = ls_config.setdefault("registered_resources", {}).setdefault("models", []) + provider_model_id = embedding_model.removeprefix("sentence-transformers/") + if any(model.get("provider_model_id") == provider_model_id for model in models): + return + models.append( + { + "model_id": f"vsprov_{provider_id}_embedding", + "model_type": "embedding", + "provider_id": "sentence-transformers", + "provider_model_id": provider_model_id, + "metadata": {"embedding_dimension": embedding_dimension}, + } + ) + + +def _vsprov_brag_fields_and_backend( + product_type: str, provider_id: str, cfg: dict[str, Any] +) -> tuple[dict[str, Any], str, dict[str, Any] | None]: + """Build BYOK-shaped fields and optional faiss storage backend. + + Parameters: + product_type: Product type (``faiss`` or ``pgvector``). + provider_id: Dynamic provider id. + cfg: Nested provider ``config`` dict. + + Returns: + Tuple of (brag_fields, backend_name, backend_entry_or_None). + """ + backend_name = f"vsprov_{provider_id}_storage" + if product_type == "faiss": + return ( + {"db_path": cfg["path"]}, + backend_name, + {"type": "kv_sqlite", "db_path": cfg["path"]}, + ) + if product_type == "pgvector": + return ( + { + "host": cfg.get("host"), + "port": cfg.get("port"), + "db": cfg.get("db"), + "user": cfg.get("user"), + "password": cfg.get("password"), + }, + backend_name, + None, + ) + raise ValueError( + f"Unsupported vector_store_providers type '{product_type}'. " + f"Supported types: {list(VECTOR_STORE_PROVIDER_TYPE_MAP)}" + ) + + +def _replace_or_append_vector_io( + vector_io: list[dict[str, Any]], + existing_ids: set[str], + provider_entry: dict[str, Any], +) -> None: + """Replace a vector_io entry with the same provider_id, else append. + + Parameters: + vector_io: Mutable providers.vector_io list. + existing_ids: Set of provider_ids already present (updated on append). + provider_entry: New provider entry to install. + """ + provider_id = provider_entry["provider_id"] + if provider_id not in existing_ids: + vector_io.append(provider_entry) + existing_ids.add(provider_id) + return + + for index, existing in enumerate(vector_io): + if isinstance(existing, dict) and existing.get("provider_id") == provider_id: + logger.info( + "Replacing existing vector_io provider with " + "provider_id=%r from vector_store_providers", + provider_id, + ) + vector_io[index] = provider_entry + return + + +def _apply_vector_stores_defaults( + ls_config: dict[str, Any], designated: dict[str, Any] +) -> None: + """Write vector_stores.default_* from the designated provider entry. + + Parameters: + ls_config: Llama Stack configuration modified in place. + designated: Provider entry with ``default: true``. + """ + vector_stores = ls_config.get("vector_stores") + if not isinstance(vector_stores, dict): + vector_stores = {} + ls_config["vector_stores"] = vector_stores + vector_stores["default_provider_id"] = designated["id"] + emb = designated.get("embedding_model") + if emb: + vector_stores["default_embedding_model"] = { + "provider_id": "sentence-transformers", + "model_id": emb, + } + + +def enrich_vector_store_providers( + ls_config: dict[str, Any], providers: list[dict[str, Any]] +) -> None: + """Enrich LS config with dynamic vector-store provider capacity. + + Appends or replaces ``providers.vector_io`` entries and faiss storage + backends, registers embedding models when needed, and writes + ``vector_stores.default_provider_id`` / ``default_embedding_model`` from + the entry marked ``default: true``. Does not register + ``registered_resources.vector_stores``. + + Parameters: + ls_config: Llama Stack configuration dictionary (modified in place). + providers: High-level ``vector_store_providers`` entries as dicts. + """ + if not providers: + logger.info("vector_store_providers not configured: skipping") + dedupe_providers_vector_io(ls_config) + return + + backends = ls_config.setdefault("storage", {}).setdefault("backends", {}) + providers_section = ls_config.setdefault("providers", {}) + vector_io = providers_section.get("vector_io") + if not isinstance(vector_io, list): + vector_io = [] + providers_section["vector_io"] = vector_io + ls_config.setdefault("registered_resources", {}).setdefault("models", []) + + existing_ids = { + str(entry.get("provider_id")).strip() + for entry in vector_io + if isinstance(entry, dict) and entry.get("provider_id") + } + + for entry in providers: + provider_id = str(entry["id"]).strip() + product_type = entry["type"] + ls_type = VECTOR_STORE_PROVIDER_TYPE_MAP[product_type] + brag_fields, backend_name, backend_entry = _vsprov_brag_fields_and_backend( + product_type, provider_id, entry.get("config") or {} + ) + if backend_entry is not None: + backends[backend_name] = backend_entry + + _replace_or_append_vector_io( + vector_io, + existing_ids, + { + "provider_id": provider_id, + "provider_type": ls_type, + "config": _build_vector_io_config(ls_type, backend_name, brag_fields), + }, + ) + + embedding_model = entry.get("embedding_model") + if embedding_model: + _upsert_vsprov_embedding_model( + ls_config, + provider_id=provider_id, + embedding_model=embedding_model, + embedding_dimension=entry.get("embedding_dimension"), + ) + + designated = _designated_vector_store_provider(providers) + if designated is not None: + _apply_vector_stores_defaults(ls_config, designated) + + dedupe_providers_vector_io(ls_config) + + # ============================================================================= # Enrichment: Solr # ============================================================================= diff --git a/tests/unit/test_llama_stack_configuration.py b/tests/unit/test_llama_stack_configuration.py index 9b23e8baa..6eba7fe8b 100644 --- a/tests/unit/test_llama_stack_configuration.py +++ b/tests/unit/test_llama_stack_configuration.py @@ -1,5 +1,7 @@ """Unit tests for src/llama_stack_configuration.py.""" +# pylint: disable=too-many-lines + from pathlib import Path from typing import Any @@ -16,6 +18,7 @@ enrich_azure_entra_id_inference, enrich_byok_rag, enrich_solr, + enrich_vector_store_providers, generate_configuration, ) from models.config import ( @@ -881,6 +884,201 @@ def test_enrich_solr_user_chunk_filter_query_is_conjoined() -> None: ) +# ============================================================================= +# Test enrich_vector_store_providers +# ============================================================================= + + +def test_enrich_vector_store_providers_faiss_appends() -> None: + """Faiss provider appends vector_io, backend, and default_* settings.""" + ls_config: dict[str, Any] = { + "providers": { + "vector_io": [ + { + "provider_id": "faiss", + "provider_type": "inline::faiss", + "config": { + "persistence": { + "namespace": "vector_io::faiss", + "backend": "kv_default", + } + }, + } + ] + }, + "storage": { + "backends": {"kv_default": {"type": "kv_sqlite", "db_path": "/tmp/kv.db"}} + }, + "registered_resources": {"models": [], "vector_stores": []}, + "vector_stores": { + "annotation_prompt_params": {"enable_annotations": False}, + "default_provider_id": "faiss", + }, + } + enrich_vector_store_providers( + ls_config, + [ + { + "id": "notebooks", + "type": "faiss", + "default": True, + "embedding_model": "/rag-content/embeddings_model", + "embedding_dimension": 768, + "config": {"path": "/var/lib/notebooks.db"}, + } + ], + ) + ids = {p["provider_id"] for p in ls_config["providers"]["vector_io"]} + assert ids == {"faiss", "notebooks"} + assert ( + ls_config["storage"]["backends"]["vsprov_notebooks_storage"]["db_path"] + == "/var/lib/notebooks.db" + ) + assert ls_config["vector_stores"]["default_provider_id"] == "notebooks" + assert ls_config["vector_stores"]["default_embedding_model"]["model_id"] == ( + "/rag-content/embeddings_model" + ) + assert ( + ls_config["vector_stores"]["annotation_prompt_params"]["enable_annotations"] + is False + ) + assert not ls_config["registered_resources"]["vector_stores"] + + +def test_enrich_vector_store_providers_replaces_same_provider_id() -> None: + """Same provider_id replaces the baseline entry and leaves orphan backends.""" + ls_config: dict[str, Any] = { + "providers": { + "vector_io": [ + { + "provider_id": "notebooks", + "provider_type": "inline::faiss", + "config": { + "persistence": { + "namespace": "vector_io::faiss", + "backend": "kv_notebooks", + } + }, + } + ] + }, + "storage": { + "backends": { + "kv_notebooks": { + "type": "kv_sqlite", + "db_path": "/old/notebooks.db", + } + } + }, + "registered_resources": {"models": []}, + "vector_stores": {}, + } + enrich_vector_store_providers( + ls_config, + [ + { + "id": "notebooks", + "type": "faiss", + "default": True, + "embedding_model": "/emb", + "embedding_dimension": 768, + "config": {"path": "/new/notebooks.db"}, + } + ], + ) + providers = ls_config["providers"]["vector_io"] + assert len(providers) == 1 + assert providers[0]["provider_id"] == "notebooks" + assert ( + providers[0]["config"]["persistence"]["backend"] == "vsprov_notebooks_storage" + ) + assert "kv_notebooks" in ls_config["storage"]["backends"] + assert ( + ls_config["storage"]["backends"]["vsprov_notebooks_storage"]["db_path"] + == "/new/notebooks.db" + ) + + +def test_enrich_vector_store_providers_pgvector_no_kv_backend() -> None: + """Pgvector provider does not create a kv_sqlite storage backend.""" + ls_config: dict[str, Any] = { + "providers": {}, + "storage": {"backends": {}}, + "registered_resources": {}, + "vector_stores": {}, + } + enrich_vector_store_providers( + ls_config, + [ + { + "id": "nb-pg", + "type": "pgvector", + "default": True, + "embedding_model": "/emb", + "embedding_dimension": 768, + "config": { + "host": "${env.POSTGRES_HOST}", + "port": "${env.POSTGRES_PORT}", + "db": "${env.POSTGRES_DATABASE}", + "user": "${env.POSTGRES_USER}", + "password": "${env.POSTGRES_PASSWORD}", + }, + } + ], + ) + assert ls_config["providers"]["vector_io"][0]["provider_type"] == "remote::pgvector" + assert "vsprov_nb-pg_storage" not in ls_config["storage"]["backends"] + + +def test_enrich_vector_store_providers_noop_without_entries() -> None: + """Empty list leaves baseline vector_stores defaults unchanged.""" + ls_config: dict[str, Any] = { + "providers": { + "vector_io": [ + {"provider_id": "faiss", "provider_type": "inline::faiss", "config": {}} + ] + }, + "vector_stores": {"default_provider_id": "faiss"}, + } + enrich_vector_store_providers(ls_config, []) + assert ls_config["vector_stores"]["default_provider_id"] == "faiss" + + +def test_enrich_vector_store_providers_dedupes_embedding_model() -> None: + """Same provider_model_id as an existing model does not add a second row.""" + ls_config: dict[str, Any] = { + "providers": {}, + "storage": {"backends": {}}, + "registered_resources": { + "models": [ + { + "model_id": "byok_rhdh-docs_embedding", + "model_type": "embedding", + "provider_id": "sentence-transformers", + "provider_model_id": "/rag-content/embeddings_model", + "metadata": {"embedding_dimension": 768}, + } + ], + "vector_stores": [], + }, + "vector_stores": {}, + } + enrich_vector_store_providers( + ls_config, + [ + { + "id": "notebooks", + "type": "faiss", + "default": True, + "embedding_model": "/rag-content/embeddings_model", + "embedding_dimension": 768, + "config": {"path": "/tmp/n.db"}, + } + ], + ) + assert len(ls_config["registered_resources"]["models"]) == 1 + + # ============================================================================= # Test _build_vector_io_config # ============================================================================= From a0204d969a9e121b4fe8fc4b58895b0f8ae71e70 Mon Sep 17 00:00:00 2001 From: Jordan Dubrick Date: Tue, 21 Jul 2026 15:07:37 -0400 Subject: [PATCH 03/10] add vector stores to enricher Signed-off-by: Jordan Dubrick --- src/llama_stack_configuration.py | 3 +++ tests/unit/test_llama_stack_synthesize.py | 27 +++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/llama_stack_configuration.py b/src/llama_stack_configuration.py index ea4695478..4c81d25c7 100644 --- a/src/llama_stack_configuration.py +++ b/src/llama_stack_configuration.py @@ -1115,6 +1115,9 @@ def synthesize_configuration( # unified output matches legacy output for equivalent inputs (R7). enrich_azure_entra_id_inference(ls_config, lcs_config.get("azure_entra_id")) enrich_byok_rag(ls_config, lcs_config.get("byok_rag", [])) + enrich_vector_store_providers( + ls_config, lcs_config.get("vector_store_providers", []) + ) enrich_solr(ls_config, lcs_config.get("rag", {}), lcs_config.get("okp", {})) # 5. High-level inference providers (Decision S5 — a root-level section). diff --git a/tests/unit/test_llama_stack_synthesize.py b/tests/unit/test_llama_stack_synthesize.py index eb2998e45..409c52c67 100644 --- a/tests/unit/test_llama_stack_synthesize.py +++ b/tests/unit/test_llama_stack_synthesize.py @@ -630,6 +630,33 @@ def test_synthesize_enriches_byok_rag_like_legacy() -> None: assert "vector_io" in result.get("providers", {}) +def test_synthesize_includes_vector_store_providers() -> None: + """vector_store_providers enrichment runs during unified synthesis.""" + lcs_config = { + "llama_stack": { + "use_as_library_client": True, + "config": {"baseline": "default"}, + }, + "vector_store_providers": [ + { + "id": "notebooks", + "type": "faiss", + "default": True, + "embedding_model": "/rag-content/embeddings_model", + "embedding_dimension": 768, + "config": {"path": "/tmp/notebooks.db"}, + } + ], + } + result = synthesize_configuration(lcs_config) + ids = {p["provider_id"] for p in result["providers"]["vector_io"]} + assert "notebooks" in ids + assert result["vector_stores"]["default_provider_id"] == "notebooks" + assert result["vector_stores"]["default_embedding_model"]["model_id"] == ( + "/rag-content/embeddings_model" + ) + + # --------------------------------------------------------------------------- # synthesize_to_file # --------------------------------------------------------------------------- From 1205be887528b97c467642ca172f701864e5e77f Mon Sep 17 00:00:00 2001 From: Jordan Dubrick Date: Tue, 21 Jul 2026 15:24:12 -0400 Subject: [PATCH 04/10] update documentation Signed-off-by: Jordan Dubrick --- docs/user_doc/byok_guide.md | 2 + docs/user_doc/config.md | 59 ++++++++++++++++++++++++ docs/user_doc/rag_guide.md | 92 +++++++++++++++++++++++++++++++++++-- 3 files changed, 150 insertions(+), 3 deletions(-) diff --git a/docs/user_doc/byok_guide.md b/docs/user_doc/byok_guide.md index dafdc9355..5299d72ff 100644 --- a/docs/user_doc/byok_guide.md +++ b/docs/user_doc/byok_guide.md @@ -416,6 +416,8 @@ The BYOK (Bring Your Own Knowledge) feature in Lightspeed Core provides powerful For additional support and advanced configurations, refer to: - [RAG Configuration Guide](rag_guide.md) +- [Dynamic vector store providers](rag_guide.md#configure-dynamic-vector-store-providers) (runtime creates) +- [Configuration schema](config.md) - [rag-content Tool Repository](https://github.com/lightspeed-core/rag-content) Remember to regularly update your knowledge sources and monitor system performance to maintain optimal BYOK functionality. diff --git a/docs/user_doc/config.md b/docs/user_doc/config.md index 752dcc0dc..e191fa012 100644 --- a/docs/user_doc/config.md +++ b/docs/user_doc/config.md @@ -247,6 +247,7 @@ Global service configuration. | compaction | | Controls when conversation history is summarized to keep the model's input below the context window limit. Disabled by default — when disabled, requests that exceed the window continue to surface as HTTP 413. | | approvals | | Settings for human-in-the-loop approval of MCP tool invocations | | byok_rag | array | BYOK RAG configuration. This configuration can be used to reconfigure Llama Stack through its run.yaml configuration file | +| vector_store_providers | array | Dynamic vector-store provider capacity for runtime POST /v1/vector-stores creates. Not the same as byok_rag (static registered corpora). When non-empty, exactly one entry must set default: true. Applied in unified synthesis only. | | a2a_state | | Configuration for A2A protocol persistent state storage. | | quota_handlers | | Quota handlers configuration | | azure_entra_id | | | @@ -315,6 +316,33 @@ Database configuration. | postgres | | PostgreSQL database configuration | +## FaissVectorStoreProvider + + +Dynamic FAISS vector-store provider (runtime create capacity). + + +| Field | Type | Description | +|---------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| id | string | Llama Stack vector_io provider_id (emitted as-is). Must match ``[a-z0-9_-]+`` and must not start with ``byok_``. | +| type | string | Product type for this dynamic vector-store provider. Must be ``faiss``. | +| embedding_model | string | Embedding model identification used for stores created against this provider. Required. | +| embedding_dimension | integer | Dimensionality of embedding vectors for this provider. Required. | +| default | boolean | When true, this entry drives vector_stores.default_* in the synthesized Llama Stack config. Exactly one entry must set this when vector_store_providers is non-empty. | +| config | | FAISS storage settings for this provider. | + + +## FaissVectorStoreProviderConfig + + +Storage config for a FAISS dynamic vector-store provider. + + +| Field | Type | Description | +|-------|--------|----------------------------------------------| +| path | string | On-disk FAISS/SQLite path for this provider. | + + ## InMemoryCacheConfig @@ -489,6 +517,37 @@ Only relevant when ``"okp"`` is listed in ``rag.inline`` or ``rag.tool``. | chunk_filter_query | string | Additional OKP filter query applied to every OKP search request. Use Solr boolean syntax, e.g. 'product:ansible AND product:*openshift*'. | +## PgvectorVectorStoreProvider + + +Dynamic pgvector vector-store provider (runtime create capacity). + + +| Field | Type | Description | +|---------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| id | string | Llama Stack vector_io provider_id (emitted as-is). Must match ``[a-z0-9_-]+`` and must not start with ``byok_``. | +| type | string | Product type for this dynamic vector-store provider. Must be ``pgvector``. | +| embedding_model | string | Embedding model identification used for stores created against this provider. Required. | +| embedding_dimension | integer | Dimensionality of embedding vectors for this provider. Required. | +| default | boolean | When true, this entry drives vector_stores.default_* in the synthesized Llama Stack config. Exactly one entry must set this when vector_store_providers is non-empty. | +| config | | pgvector connection settings for this provider. | + + +## PgvectorVectorStoreProviderConfig + + +Storage config for a pgvector dynamic vector-store provider. + + +| Field | Type | Description | +|----------|--------|-----------------------------------------------------------------| +| host | string | PostgreSQL host. Defaults to ${env.POSTGRES_HOST}. | +| port | string | PostgreSQL port. Defaults to ${env.POSTGRES_PORT}. | +| db | string | PostgreSQL database name. Defaults to ${env.POSTGRES_DATABASE}. | +| user | string | PostgreSQL user. Defaults to ${env.POSTGRES_USER}. | +| password | string | PostgreSQL password. Defaults to ${env.POSTGRES_PASSWORD}. | + + ## PostgreSQLDatabaseConfiguration diff --git a/docs/user_doc/rag_guide.md b/docs/user_doc/rag_guide.md index 2aa3a0c87..52e4d931a 100644 --- a/docs/user_doc/rag_guide.md +++ b/docs/user_doc/rag_guide.md @@ -16,6 +16,7 @@ This document explains how to configure and customize your RAG pipeline. You wil * [Set Up the Vector Database](#set-up-the-vector-database) * [Download an Embedding Model](#download-an-embedding-model) * [Configure BYOK Knowledge Sources](#configure-byok-knowledge-sources) +* [Configure Dynamic Vector Store Providers](#configure-dynamic-vector-store-providers) * [Add an Inference Model (LLM)](#add-an-inference-model-llm) * [Complete Configuration Reference](#complete-configuration-reference) * [System Prompt Guidance for RAG (as a tool)](#system-prompt-guidance-for-rag-as-a-tool) @@ -34,6 +35,11 @@ Lightspeed Core Stack (LCS) supports two complementary RAG strategies: Both strategies can be enabled independently via the `rag` section of `lightspeed-stack.yaml`. See [BYOK Feature Documentation](byok_guide.md) for configuration details. +For **runtime-created** vector stores (`POST /v1/vector-stores`), configure +[`vector_store_providers`](#configure-dynamic-vector-store-providers) instead of +`byok_rag`. BYOK registers static corpora with a fixed `vector_db_id`; dynamic +providers only declare capacity (provider id, storage, default embeddings). + The **Embedding Model** is used to convert queries and documents into vector representations for similarity matching. > [!NOTE] @@ -130,6 +136,64 @@ byok_rag: --- +## Configure Dynamic Vector Store Providers + +Use `vector_store_providers` when clients create vector stores at runtime +(for example `POST /v1/vector-stores` flows). This is +**not** BYOK: do not put a static corpus here, and do not use a `byok_` +provider id prefix. + +Requirements: + +- Supported types: `faiss`, `pgvector` +- `embedding_model` and `embedding_dimension` are **required** on every entry +- When the list is non-empty, exactly one entry must set `default: true` + (no sole-entry convenience) +- `id` must match `[a-z0-9_-]+` and must not start with `byok_` +- Applied in **unified** Llama Stack synthesis only + (`llama_stack.use_as_library_client: true` with `llama_stack.config`) + +The entry with `default: true` becomes +`vector_stores.default_provider_id` and `default_embedding_model` in the +synthesized Llama Stack config. FAISS entries also get a dedicated storage +backend named `vsprov__storage`. + +### FAISS example + +```yaml +vector_store_providers: + - id: example + type: faiss + default: true + embedding_model: /example/embeddings_model + embedding_dimension: 768 + config: + path: /example/abc/faiss_store.db +``` + +### pgvector example + +```yaml +vector_store_providers: + - id: example-pg + type: pgvector + default: true + embedding_model: sentence-transformers/all-mpnet-base-v2 + embedding_dimension: 768 + config: + # host/port/db/user/password optional; default to ${env.POSTGRES_*} + host: ${env.POSTGRES_HOST} + port: ${env.POSTGRES_PORT} + db: ${env.POSTGRES_DATABASE} + user: ${env.POSTGRES_USER} + password: ${env.POSTGRES_PASSWORD} +``` + +Field reference is in the [configuration schema](config.md) +(`FaissVectorStoreProvider`, `PgvectorVectorStoreProvider`). + +--- + ## Add an Inference Model (LLM) ### vLLM on RHEL AI (Llama 3.1) example @@ -330,12 +394,15 @@ the number of retrieved chunks, set the constants in `src/constants.py`: # Complete Configuration Reference -To enable RAG functionality, configure the `byok_rag` and `rag` sections in your `lightspeed-stack.yaml`. +To enable RAG functionality, configure the `byok_rag` and `rag` sections in +your `lightspeed-stack.yaml`. Add `vector_store_providers` when you also need +runtime `POST /v1/vector-stores` capacity. Below is an example of a working `lightspeed-stack.yaml` configuration with: * A local `all-mpnet-base-v2` embedding model -* A `FAISS`-based vector store +* A `FAISS`-based static BYOK corpus +* Optional FAISS capacity for runtime-created stores * Inline and Tool RAG enabled > [!TIP] @@ -356,6 +423,16 @@ byok_rag: vector_db_id: vs_3a7f9b2e-45dc-4e1a-b8f2-1c9d0e3f5a6b db_path: /home/USER/lightspeed-stack/vector_dbs/ocp_docs/faiss_store.db +# Optional: capacity for runtime POST /v1/vector-stores (not a static corpus) +vector_store_providers: + - id: example + type: faiss + default: true + embedding_model: sentence-transformers/all-mpnet-base-v2 + embedding_dimension: 768 + config: + path: /home/USER/lightspeed-stack/vector_dbs/example/faiss_store.db + rag: inline: - ocp-docs @@ -363,7 +440,11 @@ rag: - ocp-docs ``` -The BYOK vector store providers and registered resources are automatically generated at startup from the `byok_rag` entries above. Models and inference providers must be configured separately in your `run.yaml`. +BYOK providers and registered resources are generated at startup from +`byok_rag`. Dynamic providers and create defaults are generated from +`vector_store_providers` during unified synthesis. Models and inference +providers must still be configured separately (for example in your baseline +/ profile `run.yaml`). --- @@ -381,3 +462,8 @@ You are a helpful assistant with access to a 'knowledge_search' tool. When users The top-level `vector_stores` block in [`run.yaml`](../examples/run.yaml) may include `annotation_prompt_params` to control whether extra RAG annotation instructions are injected into the model prompt (for example, citation-style markers). The default configuration sets `enable_annotations: false` under that block to avoid unwanted annotations. +When `vector_store_providers` is configured, the entry with `default: true` +overwrites `vector_stores.default_provider_id` and `default_embedding_model` +during unified synthesis. Annotation settings are not managed by that enricher +— keep them in the Llama Stack baseline/profile or `native_override`. + From 18a58a15f9c6f4125423fa169d9953f040146d30 Mon Sep 17 00:00:00 2001 From: Jordan Dubrick Date: Tue, 21 Jul 2026 15:46:53 -0400 Subject: [PATCH 05/10] fix vector store types / refs Signed-off-by: Jordan Dubrick --- src/llama_stack_configuration.py | 29 +++++----- tests/unit/test_llama_stack_configuration.py | 61 +++++++++++++++++++- 2 files changed, 76 insertions(+), 14 deletions(-) diff --git a/src/llama_stack_configuration.py b/src/llama_stack_configuration.py index 4c81d25c7..ab4837256 100644 --- a/src/llama_stack_configuration.py +++ b/src/llama_stack_configuration.py @@ -360,14 +360,15 @@ def construct_models_section( def _build_vector_io_config( - rag_type: str, backend_name: str, brag: dict[str, Any] + rag_type: str, backend_name: str, extra_fields: dict[str, Any] ) -> dict[str, Any]: """Build the provider config dict from VECTOR_IO_TEMPLATES. Parameters: rag_type: Llama Stack provider type (e.g. 'inline::faiss', 'remote::pgvector'). backend_name: Storage backend name (used when template has '{backend_name}'). - brag: BYOK RAG entry dict — extra_fields are read from here. + extra_fields: Source values for template ``extra_fields`` (e.g. db_path, + host/port/db/user/password). Used by BYOK and vector_store_providers. Returns: dict[str, Any]: Provider config mapping. @@ -388,7 +389,7 @@ def _build_vector_io_config( } } for field, default in template.get("extra_fields", {}).items(): - value = brag.get(field) + value = extra_fields.get(field) if isinstance(value, SecretStr): value = value.get_secret_value() if value is None or (isinstance(value, str) and not value.strip()): @@ -532,7 +533,7 @@ def _upsert_vsprov_embedding_model( ls_config: dict[str, Any], provider_id: str, embedding_model: str, - embedding_dimension: int | None, + embedding_dimension: int, ) -> None: """Register an embedding model if provider_model_id is not already present. @@ -543,7 +544,8 @@ def _upsert_vsprov_embedding_model( ls_config: Llama Stack configuration modified in place. provider_id: Dynamic provider id used to name the model row. embedding_model: Configured embedding model path or id. - embedding_dimension: Embedding vector dimensionality. + embedding_dimension: Embedding vector dimensionality (required on + validated ``vector_store_providers`` entries). """ models = ls_config.setdefault("registered_resources", {}).setdefault("models", []) provider_model_id = embedding_model.removeprefix("sentence-transformers/") @@ -560,10 +562,10 @@ def _upsert_vsprov_embedding_model( ) -def _vsprov_brag_fields_and_backend( +def _vsprov_fields_and_backend( product_type: str, provider_id: str, cfg: dict[str, Any] ) -> tuple[dict[str, Any], str, dict[str, Any] | None]: - """Build BYOK-shaped fields and optional faiss storage backend. + """Build template extra fields and optional faiss storage backend. Parameters: product_type: Product type (``faiss`` or ``pgvector``). @@ -571,7 +573,7 @@ def _vsprov_brag_fields_and_backend( cfg: Nested provider ``config`` dict. Returns: - Tuple of (brag_fields, backend_name, backend_entry_or_None). + Tuple of (extra_fields, backend_name, backend_entry_or_None). """ backend_name = f"vsprov_{provider_id}_storage" if product_type == "faiss": @@ -665,7 +667,7 @@ def enrich_vector_store_providers( providers: High-level ``vector_store_providers`` entries as dicts. """ if not providers: - logger.info("vector_store_providers not configured: skipping") + logger.debug("vector_store_providers not configured: skipping") dedupe_providers_vector_io(ls_config) return @@ -687,7 +689,7 @@ def enrich_vector_store_providers( provider_id = str(entry["id"]).strip() product_type = entry["type"] ls_type = VECTOR_STORE_PROVIDER_TYPE_MAP[product_type] - brag_fields, backend_name, backend_entry = _vsprov_brag_fields_and_backend( + extra_fields, backend_name, backend_entry = _vsprov_fields_and_backend( product_type, provider_id, entry.get("config") or {} ) if backend_entry is not None: @@ -699,17 +701,18 @@ def enrich_vector_store_providers( { "provider_id": provider_id, "provider_type": ls_type, - "config": _build_vector_io_config(ls_type, backend_name, brag_fields), + "config": _build_vector_io_config(ls_type, backend_name, extra_fields), }, ) embedding_model = entry.get("embedding_model") - if embedding_model: + embedding_dimension = entry.get("embedding_dimension") + if embedding_model and embedding_dimension is not None: _upsert_vsprov_embedding_model( ls_config, provider_id=provider_id, embedding_model=embedding_model, - embedding_dimension=entry.get("embedding_dimension"), + embedding_dimension=embedding_dimension, ) designated = _designated_vector_store_provider(providers) diff --git a/tests/unit/test_llama_stack_configuration.py b/tests/unit/test_llama_stack_configuration.py index 6eba7fe8b..7ee0dca10 100644 --- a/tests/unit/test_llama_stack_configuration.py +++ b/tests/unit/test_llama_stack_configuration.py @@ -1026,10 +1026,69 @@ def test_enrich_vector_store_providers_pgvector_no_kv_backend() -> None: } ], ) - assert ls_config["providers"]["vector_io"][0]["provider_type"] == "remote::pgvector" + provider = ls_config["providers"]["vector_io"][0] + assert provider["provider_type"] == "remote::pgvector" + assert provider["config"]["persistence"]["backend"] == "kv_default" + assert provider["config"]["host"] == "${env.POSTGRES_HOST}" + assert provider["config"]["port"] == "${env.POSTGRES_PORT}" + assert provider["config"]["db"] == "${env.POSTGRES_DATABASE}" + assert provider["config"]["user"] == "${env.POSTGRES_USER}" + assert provider["config"]["password"] == "${env.POSTGRES_PASSWORD}" assert "vsprov_nb-pg_storage" not in ls_config["storage"]["backends"] +def test_enrich_vector_store_providers_multiple_entries() -> None: + """Multi-entry list: both providers, faiss-only backend, one default_* winner.""" + ls_config: dict[str, Any] = { + "providers": {}, + "storage": {"backends": {}}, + "registered_resources": {"models": [], "vector_stores": []}, + "vector_stores": { + "annotation_prompt_params": {"enable_annotations": False}, + }, + } + enrich_vector_store_providers( + ls_config, + [ + { + "id": "notebooks", + "type": "faiss", + "default": True, + "embedding_model": "/emb-faiss", + "embedding_dimension": 768, + "config": {"path": "/var/lib/notebooks.db"}, + }, + { + "id": "nb-pg", + "type": "pgvector", + "default": False, + "embedding_model": "/emb-pg", + "embedding_dimension": 768, + "config": { + "host": "${env.POSTGRES_HOST}", + "password": "${env.POSTGRES_PASSWORD}", + }, + }, + ], + ) + ids = {p["provider_id"] for p in ls_config["providers"]["vector_io"]} + assert ids == {"notebooks", "nb-pg"} + assert "vsprov_notebooks_storage" in ls_config["storage"]["backends"] + assert "vsprov_nb-pg_storage" not in ls_config["storage"]["backends"] + assert ls_config["vector_stores"]["default_provider_id"] == "notebooks" + assert ls_config["vector_stores"]["default_embedding_model"]["model_id"] == ( + "/emb-faiss" + ) + assert ( + ls_config["vector_stores"]["annotation_prompt_params"]["enable_annotations"] + is False + ) + model_ids = { + m["provider_model_id"] for m in ls_config["registered_resources"]["models"] + } + assert model_ids == {"/emb-faiss", "/emb-pg"} + + def test_enrich_vector_store_providers_noop_without_entries() -> None: """Empty list leaves baseline vector_stores defaults unchanged.""" ls_config: dict[str, Any] = { From ba03f7773ed238ef58d9401843f1dbf253bd5b2e Mon Sep 17 00:00:00 2001 From: Jordan Dubrick Date: Tue, 21 Jul 2026 16:40:12 -0400 Subject: [PATCH 06/10] RHIDP-14076: fix pylint locals and regenerate OpenAPI schema Split vector store provider enrichment to satisfy pylint, and refresh docs/devel_doc/openapi.json for the new config models. Signed-off-by: Jordan Dubrick Co-authored-by: Cursor --- docs/devel_doc/openapi.json | 209 +++++++++++++++++++++++++++++++ src/llama_stack_configuration.py | 75 +++++++---- 2 files changed, 257 insertions(+), 27 deletions(-) diff --git a/docs/devel_doc/openapi.json b/docs/devel_doc/openapi.json index ba4df6865..65d264373 100644 --- a/docs/devel_doc/openapi.json +++ b/docs/devel_doc/openapi.json @@ -12317,6 +12317,28 @@ "title": "BYOK RAG configuration", "description": "BYOK RAG configuration. This configuration can be used to reconfigure Llama Stack through its run.yaml configuration file" }, + "vector_store_providers": { + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/FaissVectorStoreProvider" + }, + { + "$ref": "#/components/schemas/PgvectorVectorStoreProvider" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "faiss": "#/components/schemas/FaissVectorStoreProvider", + "pgvector": "#/components/schemas/PgvectorVectorStoreProvider" + } + } + }, + "type": "array", + "title": "Vector store providers", + "description": "Dynamic vector-store provider capacity for runtime POST /v1/vector-stores creates. Not the same as byok_rag (static registered corpora)." + }, "a2a_state": { "$ref": "#/components/schemas/A2AStateConfiguration", "title": "A2A state configuration", @@ -13231,6 +13253,73 @@ "title": "DetailModel", "description": "Nested detail model for error responses." }, + "FaissVectorStoreProvider": { + "properties": { + "id": { + "type": "string", + "minLength": 1, + "title": "Provider ID", + "description": "Llama Stack vector_io provider_id (emitted as-is)." + }, + "embedding_model": { + "type": "string", + "minLength": 1, + "title": "Embedding model", + "description": "Embedding model identification used for stores created against this provider." + }, + "embedding_dimension": { + "type": "integer", + "exclusiveMinimum": 0.0, + "title": "Embedding dimension", + "description": "Dimensionality of embedding vectors for this provider." + }, + "default": { + "type": "boolean", + "title": "Default provider", + "description": "When true, this entry drives vector_stores.default_* in the synthesized Llama Stack config. Exactly one entry must set this when vector_store_providers is non-empty.", + "default": false + }, + "type": { + "type": "string", + "const": "faiss", + "title": "Provider type", + "description": "Product type for this dynamic vector-store provider.", + "default": "faiss" + }, + "config": { + "$ref": "#/components/schemas/FaissVectorStoreProviderConfig", + "title": "Storage config", + "description": "FAISS storage settings for this provider." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "id", + "embedding_model", + "embedding_dimension", + "config" + ], + "title": "FaissVectorStoreProvider", + "description": "Dynamic FAISS vector-store provider (runtime create capacity)." + }, + "FaissVectorStoreProviderConfig": { + "properties": { + "path": { + "type": "string", + "minLength": 1, + "title": "DB path", + "description": "On-disk FAISS/SQLite path for this provider." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "path" + ], + "title": "FaissVectorStoreProviderConfig", + "description": "Storage config for a FAISS dynamic vector-store provider." + }, "FeedbackCategory": { "type": "string", "enum": [ @@ -16680,6 +16769,126 @@ "title": "PasswordOAuthFlow", "description": "Defines configuration details for the OAuth 2.0 Resource Owner Password flow." }, + "PgvectorVectorStoreProvider": { + "properties": { + "id": { + "type": "string", + "minLength": 1, + "title": "Provider ID", + "description": "Llama Stack vector_io provider_id (emitted as-is)." + }, + "embedding_model": { + "type": "string", + "minLength": 1, + "title": "Embedding model", + "description": "Embedding model identification used for stores created against this provider." + }, + "embedding_dimension": { + "type": "integer", + "exclusiveMinimum": 0.0, + "title": "Embedding dimension", + "description": "Dimensionality of embedding vectors for this provider." + }, + "default": { + "type": "boolean", + "title": "Default provider", + "description": "When true, this entry drives vector_stores.default_* in the synthesized Llama Stack config. Exactly one entry must set this when vector_store_providers is non-empty.", + "default": false + }, + "type": { + "type": "string", + "const": "pgvector", + "title": "Provider type", + "description": "Product type for this dynamic vector-store provider.", + "default": "pgvector" + }, + "config": { + "$ref": "#/components/schemas/PgvectorVectorStoreProviderConfig", + "title": "Storage config", + "description": "pgvector connection settings for this provider." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "id", + "embedding_model", + "embedding_dimension", + "config" + ], + "title": "PgvectorVectorStoreProvider", + "description": "Dynamic pgvector vector-store provider (runtime create capacity)." + }, + "PgvectorVectorStoreProviderConfig": { + "properties": { + "host": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "PostgreSQL host", + "description": "PostgreSQL host. Defaults to ${env.POSTGRES_HOST}." + }, + "port": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "PostgreSQL port", + "description": "PostgreSQL port. Defaults to ${env.POSTGRES_PORT}." + }, + "db": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "PostgreSQL database", + "description": "PostgreSQL database name. Defaults to ${env.POSTGRES_DATABASE}." + }, + "user": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "PostgreSQL user", + "description": "PostgreSQL user. Defaults to ${env.POSTGRES_USER}." + }, + "password": { + "anyOf": [ + { + "type": "string", + "format": "password", + "writeOnly": true + }, + { + "type": "null" + } + ], + "title": "PostgreSQL password", + "description": "PostgreSQL password. Defaults to ${env.POSTGRES_PASSWORD}." + } + }, + "additionalProperties": false, + "type": "object", + "title": "PgvectorVectorStoreProviderConfig", + "description": "Storage config for a pgvector dynamic vector-store provider." + }, "PostgreSQLDatabaseConfiguration": { "properties": { "host": { diff --git a/src/llama_stack_configuration.py b/src/llama_stack_configuration.py index ab4837256..7e8e531b2 100644 --- a/src/llama_stack_configuration.py +++ b/src/llama_stack_configuration.py @@ -651,6 +651,52 @@ def _apply_vector_stores_defaults( } +def _enrich_one_vector_store_provider( + entry: dict[str, Any], + backends: dict[str, Any], + vector_io: list[Any], + existing_ids: set[str], + ls_config: dict[str, Any], +) -> None: + """Enrich LS config for a single ``vector_store_providers`` entry. + + Parameters: + entry: One high-level provider dict from Lightspeed config. + backends: ``storage.backends`` map (modified in place for faiss). + vector_io: ``providers.vector_io`` list (modified in place). + existing_ids: Known ``provider_id`` values already in ``vector_io``. + ls_config: Full Llama Stack config (for embedding model registration). + """ + provider_id = str(entry["id"]).strip() + product_type = entry["type"] + ls_type = VECTOR_STORE_PROVIDER_TYPE_MAP[product_type] + extra_fields, backend_name, backend_entry = _vsprov_fields_and_backend( + product_type, provider_id, entry.get("config") or {} + ) + if backend_entry is not None: + backends[backend_name] = backend_entry + + _replace_or_append_vector_io( + vector_io, + existing_ids, + { + "provider_id": provider_id, + "provider_type": ls_type, + "config": _build_vector_io_config(ls_type, backend_name, extra_fields), + }, + ) + + embedding_model = entry.get("embedding_model") + embedding_dimension = entry.get("embedding_dimension") + if embedding_model and embedding_dimension is not None: + _upsert_vsprov_embedding_model( + ls_config, + provider_id=provider_id, + embedding_model=embedding_model, + embedding_dimension=embedding_dimension, + ) + + def enrich_vector_store_providers( ls_config: dict[str, Any], providers: list[dict[str, Any]] ) -> None: @@ -686,34 +732,9 @@ def enrich_vector_store_providers( } for entry in providers: - provider_id = str(entry["id"]).strip() - product_type = entry["type"] - ls_type = VECTOR_STORE_PROVIDER_TYPE_MAP[product_type] - extra_fields, backend_name, backend_entry = _vsprov_fields_and_backend( - product_type, provider_id, entry.get("config") or {} + _enrich_one_vector_store_provider( + entry, backends, vector_io, existing_ids, ls_config ) - if backend_entry is not None: - backends[backend_name] = backend_entry - - _replace_or_append_vector_io( - vector_io, - existing_ids, - { - "provider_id": provider_id, - "provider_type": ls_type, - "config": _build_vector_io_config(ls_type, backend_name, extra_fields), - }, - ) - - embedding_model = entry.get("embedding_model") - embedding_dimension = entry.get("embedding_dimension") - if embedding_model and embedding_dimension is not None: - _upsert_vsprov_embedding_model( - ls_config, - provider_id=provider_id, - embedding_model=embedding_model, - embedding_dimension=embedding_dimension, - ) designated = _designated_vector_store_provider(providers) if designated is not None: From d12e99b8b27b735fcd451dc67f4d61fdb2306135 Mon Sep 17 00:00:00 2001 From: Jordan Dubrick Date: Tue, 21 Jul 2026 16:49:23 -0400 Subject: [PATCH 07/10] address coderabbit review Signed-off-by: Jordan Dubrick --- docs/devel_doc/openapi.json | 4 ++-- docs/user_doc/config.md | 4 ++-- docs/user_doc/rag_guide.md | 7 ++++--- src/llama_stack_configuration.py | 2 +- src/models/config.py | 5 ++++- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/devel_doc/openapi.json b/docs/devel_doc/openapi.json index 65d264373..598938993 100644 --- a/docs/devel_doc/openapi.json +++ b/docs/devel_doc/openapi.json @@ -13259,7 +13259,7 @@ "type": "string", "minLength": 1, "title": "Provider ID", - "description": "Llama Stack vector_io provider_id (emitted as-is)." + "description": "Llama Stack vector_io provider_id. Surrounding whitespace is stripped before validation and emission." }, "embedding_model": { "type": "string", @@ -16775,7 +16775,7 @@ "type": "string", "minLength": 1, "title": "Provider ID", - "description": "Llama Stack vector_io provider_id (emitted as-is)." + "description": "Llama Stack vector_io provider_id. Surrounding whitespace is stripped before validation and emission." }, "embedding_model": { "type": "string", diff --git a/docs/user_doc/config.md b/docs/user_doc/config.md index e191fa012..e5a082c89 100644 --- a/docs/user_doc/config.md +++ b/docs/user_doc/config.md @@ -324,7 +324,7 @@ Dynamic FAISS vector-store provider (runtime create capacity). | Field | Type | Description | |---------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| id | string | Llama Stack vector_io provider_id (emitted as-is). Must match ``[a-z0-9_-]+`` and must not start with ``byok_``. | +| id | string | Llama Stack vector_io provider_id. Surrounding whitespace is stripped before validation and emission. Must match ``[a-z0-9_-]+`` and must not start with ``byok_``. | | type | string | Product type for this dynamic vector-store provider. Must be ``faiss``. | | embedding_model | string | Embedding model identification used for stores created against this provider. Required. | | embedding_dimension | integer | Dimensionality of embedding vectors for this provider. Required. | @@ -525,7 +525,7 @@ Dynamic pgvector vector-store provider (runtime create capacity). | Field | Type | Description | |---------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| id | string | Llama Stack vector_io provider_id (emitted as-is). Must match ``[a-z0-9_-]+`` and must not start with ``byok_``. | +| id | string | Llama Stack vector_io provider_id. Surrounding whitespace is stripped before validation and emission. Must match ``[a-z0-9_-]+`` and must not start with ``byok_``. | | type | string | Product type for this dynamic vector-store provider. Must be ``pgvector``. | | embedding_model | string | Embedding model identification used for stores created against this provider. Required. | | embedding_dimension | integer | Dimensionality of embedding vectors for this provider. Required. | diff --git a/docs/user_doc/rag_guide.md b/docs/user_doc/rag_guide.md index 52e4d931a..2fc69a21e 100644 --- a/docs/user_doc/rag_guide.md +++ b/docs/user_doc/rag_guide.md @@ -442,9 +442,10 @@ rag: BYOK providers and registered resources are generated at startup from `byok_rag`. Dynamic providers and create defaults are generated from -`vector_store_providers` during unified synthesis. Models and inference -providers must still be configured separately (for example in your baseline -/ profile `run.yaml`). +`vector_store_providers` during unified synthesis. Embedding models for +those providers are registered automatically when needed. Inference models +and providers must still be configured separately (for example in your +baseline / profile `run.yaml`). --- diff --git a/src/llama_stack_configuration.py b/src/llama_stack_configuration.py index 7e8e531b2..6f7b3c590 100644 --- a/src/llama_stack_configuration.py +++ b/src/llama_stack_configuration.py @@ -642,7 +642,7 @@ def _apply_vector_stores_defaults( if not isinstance(vector_stores, dict): vector_stores = {} ls_config["vector_stores"] = vector_stores - vector_stores["default_provider_id"] = designated["id"] + vector_stores["default_provider_id"] = str(designated["id"]).strip() emb = designated.get("embedding_model") if emb: vector_stores["default_embedding_model"] = { diff --git a/src/models/config.py b/src/models/config.py index d93e2897c..b913670e2 100644 --- a/src/models/config.py +++ b/src/models/config.py @@ -2196,7 +2196,10 @@ class VectorStoreProviderBase(ConfigurationBase): ..., min_length=1, title="Provider ID", - description="Llama Stack vector_io provider_id (emitted as-is).", + description=( + "Llama Stack vector_io provider_id. Surrounding whitespace is " + "stripped before validation and emission." + ), ) embedding_model: str = Field( ..., From 06f85e2cafd418e1a820b669ed2a4ae41d0d504f Mon Sep 17 00:00:00 2001 From: Jordan Dubrick Date: Mon, 27 Jul 2026 11:48:01 -0400 Subject: [PATCH 08/10] use dedicated default field instead of boolean Signed-off-by: Jordan Dubrick --- docs/devel_doc/openapi.json | 79 ++++---- docs/user_doc/config.md | 19 +- docs/user_doc/rag_guide.md | 88 ++++----- src/llama_stack_configuration.py | 54 +++--- src/models/config.py | 127 ++++++++----- .../models/config/test_dump_configuration.py | 50 ++++- ...tore_providers.py => test_vector_store.py} | 99 ++++++---- tests/unit/test_llama_stack_configuration.py | 171 +++++++++--------- tests/unit/test_llama_stack_synthesize.py | 26 +-- tests/unit/utils/test_models_dumper.py | 1 + 10 files changed, 435 insertions(+), 279 deletions(-) rename tests/unit/models/config/{test_vector_store_providers.py => test_vector_store.py} (62%) diff --git a/docs/devel_doc/openapi.json b/docs/devel_doc/openapi.json index 598938993..724005153 100644 --- a/docs/devel_doc/openapi.json +++ b/docs/devel_doc/openapi.json @@ -12317,27 +12317,10 @@ "title": "BYOK RAG configuration", "description": "BYOK RAG configuration. This configuration can be used to reconfigure Llama Stack through its run.yaml configuration file" }, - "vector_store_providers": { - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/FaissVectorStoreProvider" - }, - { - "$ref": "#/components/schemas/PgvectorVectorStoreProvider" - } - ], - "discriminator": { - "propertyName": "type", - "mapping": { - "faiss": "#/components/schemas/FaissVectorStoreProvider", - "pgvector": "#/components/schemas/PgvectorVectorStoreProvider" - } - } - }, - "type": "array", - "title": "Vector store providers", - "description": "Dynamic vector-store provider capacity for runtime POST /v1/vector-stores creates. Not the same as byok_rag (static registered corpora)." + "vector_store": { + "$ref": "#/components/schemas/VectorStoreConfiguration", + "title": "Vector store configuration", + "description": "Dynamic vector-store provider capacity for runtime POST /v1/vector-stores creates. Not the same as byok_rag (static registered corpora). When providers is non-empty, default_provider is required and must match one of providers[].id. Applied in unified synthesis only." }, "a2a_state": { "$ref": "#/components/schemas/A2AStateConfiguration", @@ -13273,12 +13256,6 @@ "title": "Embedding dimension", "description": "Dimensionality of embedding vectors for this provider." }, - "default": { - "type": "boolean", - "title": "Default provider", - "description": "When true, this entry drives vector_stores.default_* in the synthesized Llama Stack config. Exactly one entry must set this when vector_store_providers is non-empty.", - "default": false - }, "type": { "type": "string", "const": "faiss", @@ -16789,12 +16766,6 @@ "title": "Embedding dimension", "description": "Dimensionality of embedding vectors for this provider." }, - "default": { - "type": "boolean", - "title": "Default provider", - "description": "When true, this entry drives vector_stores.default_* in the synthesized Llama Stack config. Exactly one entry must set this when vector_store_providers is non-empty.", - "default": false - }, "type": { "type": "string", "const": "pgvector", @@ -20781,6 +20752,48 @@ ], "title": "ValidationError" }, + "VectorStoreConfiguration": { + "properties": { + "default_provider": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Default provider", + "description": "Provider id used for vector_stores.default_* in the synthesized Llama Stack config. Required when providers is non-empty; must match one of providers[].id." + }, + "providers": { + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/FaissVectorStoreProvider" + }, + { + "$ref": "#/components/schemas/PgvectorVectorStoreProvider" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "faiss": "#/components/schemas/FaissVectorStoreProvider", + "pgvector": "#/components/schemas/PgvectorVectorStoreProvider" + } + } + }, + "type": "array", + "title": "Vector store providers", + "description": "Dynamic vector-store provider capacity for runtime POST /v1/vector-stores creates. Not the same as byok_rag (static registered corpora)." + } + }, + "additionalProperties": false, + "type": "object", + "title": "VectorStoreConfiguration", + "description": "Configuration for dynamic vector-store providers.\n\nMirrors ``InferenceConfiguration``: a providers list plus a sibling\n``default_provider`` pointer, rather than a per-entry default flag." + }, "VectorStoreCreateRequest": { "properties": { "name": { diff --git a/docs/user_doc/config.md b/docs/user_doc/config.md index e5a082c89..5b7479f64 100644 --- a/docs/user_doc/config.md +++ b/docs/user_doc/config.md @@ -247,7 +247,7 @@ Global service configuration. | compaction | | Controls when conversation history is summarized to keep the model's input below the context window limit. Disabled by default — when disabled, requests that exceed the window continue to surface as HTTP 413. | | approvals | | Settings for human-in-the-loop approval of MCP tool invocations | | byok_rag | array | BYOK RAG configuration. This configuration can be used to reconfigure Llama Stack through its run.yaml configuration file | -| vector_store_providers | array | Dynamic vector-store provider capacity for runtime POST /v1/vector-stores creates. Not the same as byok_rag (static registered corpora). When non-empty, exactly one entry must set default: true. Applied in unified synthesis only. | +| vector_store | | Dynamic vector-store provider capacity for runtime POST /v1/vector-stores creates. Not the same as byok_rag (static registered corpora). When providers is non-empty, default_provider is required and must match one of providers[].id. Applied in unified synthesis only. | | a2a_state | | Configuration for A2A protocol persistent state storage. | | quota_handlers | | Quota handlers configuration | | azure_entra_id | | | @@ -328,7 +328,6 @@ Dynamic FAISS vector-store provider (runtime create capacity). | type | string | Product type for this dynamic vector-store provider. Must be ``faiss``. | | embedding_model | string | Embedding model identification used for stores created against this provider. Required. | | embedding_dimension | integer | Dimensionality of embedding vectors for this provider. Required. | -| default | boolean | When true, this entry drives vector_stores.default_* in the synthesized Llama Stack config. Exactly one entry must set this when vector_store_providers is non-empty. | | config | | FAISS storage settings for this provider. | @@ -529,7 +528,6 @@ Dynamic pgvector vector-store provider (runtime create capacity). | type | string | Product type for this dynamic vector-store provider. Must be ``pgvector``. | | embedding_model | string | Embedding model identification used for stores created against this provider. Required. | | embedding_dimension | integer | Dimensionality of embedding vectors for this provider. Required. | -| default | boolean | When true, this entry drives vector_stores.default_* in the synthesized Llama Stack config. Exactly one entry must set this when vector_store_providers is non-empty. | | config | | pgvector connection settings for this provider. | @@ -925,3 +923,18 @@ User data collection configuration. | feedback_storage | string | Path to directory where feedback will be saved for further processing. | | transcripts_enabled | boolean | When set to true the conversation history is stored and later sent for analysis. | | transcripts_storage | string | Path to directory where conversation history will be saved for further processing. | + + +## VectorStoreConfiguration + + +Configuration for dynamic vector-store providers. + +Mirrors ``InferenceConfiguration``: a providers list plus a sibling +``default_provider`` pointer, rather than a per-entry default flag. + + +| Field | Type | Description | +|------------------|--------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| default_provider | string | Provider id used for vector_stores.default_* in the synthesized Llama Stack config. Required when providers is non-empty; must match one of providers[].id. | +| providers | array | Dynamic vector-store provider capacity for runtime POST /v1/vector-stores creates. Not the same as byok_rag (static registered corpora). | diff --git a/docs/user_doc/rag_guide.md b/docs/user_doc/rag_guide.md index 2fc69a21e..e3801ccde 100644 --- a/docs/user_doc/rag_guide.md +++ b/docs/user_doc/rag_guide.md @@ -36,7 +36,7 @@ Lightspeed Core Stack (LCS) supports two complementary RAG strategies: Both strategies can be enabled independently via the `rag` section of `lightspeed-stack.yaml`. See [BYOK Feature Documentation](byok_guide.md) for configuration details. For **runtime-created** vector stores (`POST /v1/vector-stores`), configure -[`vector_store_providers`](#configure-dynamic-vector-store-providers) instead of +[`vector_store`](#configure-dynamic-vector-store-providers) instead of `byok_rag`. BYOK registers static corpora with a fixed `vector_db_id`; dynamic providers only declare capacity (provider id, storage, default embeddings). @@ -138,7 +138,7 @@ byok_rag: ## Configure Dynamic Vector Store Providers -Use `vector_store_providers` when clients create vector stores at runtime +Use `vector_store` when clients create vector stores at runtime (for example `POST /v1/vector-stores` flows). This is **not** BYOK: do not put a static corpus here, and do not use a `byok_` provider id prefix. @@ -146,51 +146,54 @@ provider id prefix. Requirements: - Supported types: `faiss`, `pgvector` -- `embedding_model` and `embedding_dimension` are **required** on every entry -- When the list is non-empty, exactly one entry must set `default: true` - (no sole-entry convenience) -- `id` must match `[a-z0-9_-]+` and must not start with `byok_` +- `embedding_model` and `embedding_dimension` are **required** on every provider entry +- When `providers` is non-empty, `default_provider` is **required** and must + match one of `providers[].id` +- Provider `id` must match `[a-z0-9_-]+` and must not start with `byok_` - Applied in **unified** Llama Stack synthesis only (`llama_stack.use_as_library_client: true` with `llama_stack.config`) -The entry with `default: true` becomes -`vector_stores.default_provider_id` and `default_embedding_model` in the +`default_provider` becomes `vector_stores.default_provider_id` and that +provider's embedding model becomes `default_embedding_model` in the synthesized Llama Stack config. FAISS entries also get a dedicated storage backend named `vsprov__storage`. ### FAISS example ```yaml -vector_store_providers: - - id: example - type: faiss - default: true - embedding_model: /example/embeddings_model - embedding_dimension: 768 - config: - path: /example/abc/faiss_store.db +vector_store: + default_provider: example + providers: + - id: example + type: faiss + embedding_model: /example/embeddings_model + embedding_dimension: 768 + config: + path: /example/abc/faiss_store.db ``` ### pgvector example ```yaml -vector_store_providers: - - id: example-pg - type: pgvector - default: true - embedding_model: sentence-transformers/all-mpnet-base-v2 - embedding_dimension: 768 - config: - # host/port/db/user/password optional; default to ${env.POSTGRES_*} - host: ${env.POSTGRES_HOST} - port: ${env.POSTGRES_PORT} - db: ${env.POSTGRES_DATABASE} - user: ${env.POSTGRES_USER} - password: ${env.POSTGRES_PASSWORD} +vector_store: + default_provider: example-pg + providers: + - id: example-pg + type: pgvector + embedding_model: sentence-transformers/all-mpnet-base-v2 + embedding_dimension: 768 + config: + # host/port/db/user/password optional; default to ${env.POSTGRES_*} + host: ${env.POSTGRES_HOST} + port: ${env.POSTGRES_PORT} + db: ${env.POSTGRES_DATABASE} + user: ${env.POSTGRES_USER} + password: ${env.POSTGRES_PASSWORD} ``` Field reference is in the [configuration schema](config.md) -(`FaissVectorStoreProvider`, `PgvectorVectorStoreProvider`). +(`VectorStoreConfiguration`, `FaissVectorStoreProvider`, +`PgvectorVectorStoreProvider`). --- @@ -395,7 +398,7 @@ the number of retrieved chunks, set the constants in `src/constants.py`: # Complete Configuration Reference To enable RAG functionality, configure the `byok_rag` and `rag` sections in -your `lightspeed-stack.yaml`. Add `vector_store_providers` when you also need +your `lightspeed-stack.yaml`. Add `vector_store` when you also need runtime `POST /v1/vector-stores` capacity. Below is an example of a working `lightspeed-stack.yaml` configuration with: @@ -424,14 +427,15 @@ byok_rag: db_path: /home/USER/lightspeed-stack/vector_dbs/ocp_docs/faiss_store.db # Optional: capacity for runtime POST /v1/vector-stores (not a static corpus) -vector_store_providers: - - id: example - type: faiss - default: true - embedding_model: sentence-transformers/all-mpnet-base-v2 - embedding_dimension: 768 - config: - path: /home/USER/lightspeed-stack/vector_dbs/example/faiss_store.db +vector_store: + default_provider: example + providers: + - id: example + type: faiss + embedding_model: sentence-transformers/all-mpnet-base-v2 + embedding_dimension: 768 + config: + path: /home/USER/lightspeed-stack/vector_dbs/example/faiss_store.db rag: inline: @@ -442,7 +446,7 @@ rag: BYOK providers and registered resources are generated at startup from `byok_rag`. Dynamic providers and create defaults are generated from -`vector_store_providers` during unified synthesis. Embedding models for +`vector_store` during unified synthesis. Embedding models for those providers are registered automatically when needed. Inference models and providers must still be configured separately (for example in your baseline / profile `run.yaml`). @@ -463,8 +467,8 @@ You are a helpful assistant with access to a 'knowledge_search' tool. When users The top-level `vector_stores` block in [`run.yaml`](../examples/run.yaml) may include `annotation_prompt_params` to control whether extra RAG annotation instructions are injected into the model prompt (for example, citation-style markers). The default configuration sets `enable_annotations: false` under that block to avoid unwanted annotations. -When `vector_store_providers` is configured, the entry with `default: true` -overwrites `vector_stores.default_provider_id` and `default_embedding_model` +When `vector_store` is configured, `default_provider` overwrites +`vector_stores.default_provider_id` and `default_embedding_model` during unified synthesis. Annotation settings are not managed by that enricher — keep them in the Llama Stack baseline/profile or `native_override`. diff --git a/src/llama_stack_configuration.py b/src/llama_stack_configuration.py index 6f7b3c590..7790f2ef9 100644 --- a/src/llama_stack_configuration.py +++ b/src/llama_stack_configuration.py @@ -368,7 +368,7 @@ def _build_vector_io_config( rag_type: Llama Stack provider type (e.g. 'inline::faiss', 'remote::pgvector'). backend_name: Storage backend name (used when template has '{backend_name}'). extra_fields: Source values for template ``extra_fields`` (e.g. db_path, - host/port/db/user/password). Used by BYOK and vector_store_providers. + host/port/db/user/password). Used by BYOK and vector_store.providers. Returns: dict[str, Any]: Provider config mapping. @@ -508,23 +508,29 @@ def enrich_byok_rag(ls_config: dict[str, Any], byok_rag: list[dict[str, Any]]) - # ============================================================================= -# Enrichment: vector_store_providers +# Enrichment: vector_store # ============================================================================= -def _designated_vector_store_provider( - providers: list[dict[str, Any]], +def _vector_store_provider_by_id( + providers: list[dict[str, Any]], provider_id: str | None ) -> dict[str, Any] | None: - """Return the provider that should drive vector_stores.default_*. + """Return the provider entry matching ``provider_id``. Parameters: - providers: High-level vector_store_providers entries. + providers: High-level ``vector_store.providers`` entries. + provider_id: Id from ``vector_store.default_provider``. Returns: - The entry with ``default: true``, or None when none is marked. + Matching provider dict, or None when unset / not found. """ + if provider_id is None: + return None + cleaned = provider_id.strip() + if not cleaned: + return None for provider in providers: - if provider.get("default"): + if str(provider.get("id", "")).strip() == cleaned: return provider return None @@ -545,7 +551,7 @@ def _upsert_vsprov_embedding_model( provider_id: Dynamic provider id used to name the model row. embedding_model: Configured embedding model path or id. embedding_dimension: Embedding vector dimensionality (required on - validated ``vector_store_providers`` entries). + validated ``vector_store.providers`` entries). """ models = ls_config.setdefault("registered_resources", {}).setdefault("models", []) provider_model_id = embedding_model.removeprefix("sentence-transformers/") @@ -595,7 +601,7 @@ def _vsprov_fields_and_backend( None, ) raise ValueError( - f"Unsupported vector_store_providers type '{product_type}'. " + f"Unsupported vector_store.providers type '{product_type}'. " f"Supported types: {list(VECTOR_STORE_PROVIDER_TYPE_MAP)}" ) @@ -622,7 +628,7 @@ def _replace_or_append_vector_io( if isinstance(existing, dict) and existing.get("provider_id") == provider_id: logger.info( "Replacing existing vector_io provider with " - "provider_id=%r from vector_store_providers", + "provider_id=%r from vector_store.providers", provider_id, ) vector_io[index] = provider_entry @@ -636,7 +642,7 @@ def _apply_vector_stores_defaults( Parameters: ls_config: Llama Stack configuration modified in place. - designated: Provider entry with ``default: true``. + designated: Provider entry selected by ``vector_store.default_provider``. """ vector_stores = ls_config.get("vector_stores") if not isinstance(vector_stores, dict): @@ -658,7 +664,7 @@ def _enrich_one_vector_store_provider( existing_ids: set[str], ls_config: dict[str, Any], ) -> None: - """Enrich LS config for a single ``vector_store_providers`` entry. + """Enrich LS config for a single ``vector_store.providers`` entry. Parameters: entry: One high-level provider dict from Lightspeed config. @@ -697,23 +703,27 @@ def _enrich_one_vector_store_provider( ) -def enrich_vector_store_providers( - ls_config: dict[str, Any], providers: list[dict[str, Any]] +def enrich_vector_store( + ls_config: dict[str, Any], + vector_store: dict[str, Any] | None = None, ) -> None: """Enrich LS config with dynamic vector-store provider capacity. Appends or replaces ``providers.vector_io`` entries and faiss storage backends, registers embedding models when needed, and writes ``vector_stores.default_provider_id`` / ``default_embedding_model`` from - the entry marked ``default: true``. Does not register + ``vector_store.default_provider``. Does not register ``registered_resources.vector_stores``. Parameters: ls_config: Llama Stack configuration dictionary (modified in place). - providers: High-level ``vector_store_providers`` entries as dicts. + vector_store: High-level ``vector_store`` section + (``default_provider`` + ``providers``) as a dict. """ + vector_store = vector_store or {} + providers = vector_store.get("providers") or [] if not providers: - logger.debug("vector_store_providers not configured: skipping") + logger.debug("vector_store.providers not configured: skipping") dedupe_providers_vector_io(ls_config) return @@ -736,7 +746,9 @@ def enrich_vector_store_providers( entry, backends, vector_io, existing_ids, ls_config ) - designated = _designated_vector_store_provider(providers) + designated = _vector_store_provider_by_id( + providers, vector_store.get("default_provider") + ) if designated is not None: _apply_vector_stores_defaults(ls_config, designated) @@ -1139,9 +1151,7 @@ def synthesize_configuration( # unified output matches legacy output for equivalent inputs (R7). enrich_azure_entra_id_inference(ls_config, lcs_config.get("azure_entra_id")) enrich_byok_rag(ls_config, lcs_config.get("byok_rag", [])) - enrich_vector_store_providers( - ls_config, lcs_config.get("vector_store_providers", []) - ) + enrich_vector_store(ls_config, lcs_config.get("vector_store")) enrich_solr(ls_config, lcs_config.get("rag", {}), lcs_config.get("okp", {})) # 5. High-level inference providers (Decision S5 — a root-level section). diff --git a/src/models/config.py b/src/models/config.py index b913670e2..54f94f8cd 100644 --- a/src/models/config.py +++ b/src/models/config.py @@ -2213,13 +2213,6 @@ class VectorStoreProviderBase(ConfigurationBase): title="Embedding dimension", description="Dimensionality of embedding vectors for this provider.", ) - default: bool = Field( - False, - title="Default provider", - description="When true, this entry drives vector_stores.default_* " - "in the synthesized Llama Stack config. Exactly one entry must set " - "this when vector_store_providers is non-empty.", - ) @field_validator("id") @classmethod @@ -2285,6 +2278,83 @@ class PgvectorVectorStoreProvider(VectorStoreProviderBase): ] +class VectorStoreConfiguration(ConfigurationBase): + """Configuration for dynamic vector-store providers. + + Mirrors ``InferenceConfiguration``: a providers list plus a sibling + ``default_provider`` pointer, rather than a per-entry default flag. + """ + + default_provider: Optional[str] = Field( + None, + title="Default provider", + description=( + "Provider id used for vector_stores.default_* in the synthesized " + "Llama Stack config. Required when providers is non-empty; must " + "match one of providers[].id." + ), + ) + + providers: list[VectorStoreProvider] = Field( + default_factory=list, + title="Vector store providers", + description=( + "Dynamic vector-store provider capacity for runtime " + "POST /v1/vector-stores creates. " + "Not the same as byok_rag (static registered corpora)." + ), + ) + + @model_validator(mode="after") + def validate_providers_and_default(self) -> Self: + """Validate providers list and default_provider pointer. + + When providers is empty, default_provider must be unset. When + providers is non-empty, default_provider is required and must match + exactly one providers[].id. Provider ids must be unique. + + Returns: + Self: The validated configuration instance. + + Raises: + ValueError: If ids are duplicated, default_provider is missing + for a non-empty list, is set for an empty list, or does not + match a provider id. + """ + # pylint: disable=no-member + if not self.providers: + if self.default_provider is not None: + raise ValueError( + "vector_store.default_provider must not be set when " + "providers is empty" + ) + return self + + if self.default_provider is None: + raise ValueError( + "vector_store.default_provider is required when providers " + "is non-empty" + ) + + ids = [provider.id for provider in self.providers] + if len(ids) != len(set(ids)): + raise ValueError(f"vector_store.providers ids must be unique; got {ids}") + + default_provider = self.default_provider.strip() + if not default_provider: + raise ValueError( + "vector_store.default_provider must be non-empty after " + "stripping whitespace" + ) + if default_provider not in ids: + raise ValueError( + "vector_store.default_provider must match one of " + f"providers[].id; got {default_provider!r}, known ids: {ids}" + ) + object.__setattr__(self, "default_provider", default_provider) + return self + + class QuotaLimiterConfiguration(ConfigurationBase): """Configuration for one quota limiter. @@ -2864,13 +2934,18 @@ class Configuration(ConfigurationBase): "reconfigure Llama Stack through its run.yaml configuration file", ) - vector_store_providers: list[VectorStoreProvider] = Field( - default_factory=list, - title="Vector store providers", + vector_store: VectorStoreConfiguration = Field( + default_factory=lambda: VectorStoreConfiguration( + default_provider=None, providers=[] + ), + title="Vector store configuration", description=( "Dynamic vector-store provider capacity for runtime " "POST /v1/vector-stores creates. " - "Not the same as byok_rag (static registered corpora)." + "Not the same as byok_rag (static registered corpora). " + "When providers is non-empty, default_provider is required and " + "must match one of providers[].id. Applied in unified synthesis " + "only." ), ) @@ -2941,36 +3016,6 @@ class Configuration(ConfigurationBase): "maximum prompts per user, display name length, and content length.", ) - @model_validator(mode="after") - def validate_vector_store_providers(self) -> Self: - """Validate vector_store_providers list constraints. - - When the list is non-empty, requires unique ids and exactly one - entry with ``default: true``. - - Returns: - Self: The validated configuration instance. - - Raises: - ValueError: If ids are duplicated or the default count is not - exactly one for a non-empty list. - """ - providers = self.vector_store_providers - if not providers: - return self - - ids = [provider.id for provider in providers] - if len(ids) != len(set(ids)): - raise ValueError(f"vector_store_providers ids must be unique; got {ids}") - - defaults = [provider.id for provider in providers if provider.default] - if len(defaults) != 1: - raise ValueError( - "vector_store_providers must set default: true on exactly one " - f"entry when the list is non-empty; found defaults on: {defaults}" - ) - return self - @model_validator(mode="after") def validate_mcp_auth_headers(self) -> Self: """ diff --git a/tests/unit/models/config/test_dump_configuration.py b/tests/unit/models/config/test_dump_configuration.py index 6cb852806..2debc0291 100644 --- a/tests/unit/models/config/test_dump_configuration.py +++ b/tests/unit/models/config/test_dump_configuration.py @@ -194,7 +194,10 @@ def test_dump_configuration_minimal_cfg(tmp_path: Path) -> None: }, "approvals": _DEFAULT_APPROVALS_DUMP, "byok_rag": [], - "vector_store_providers": [], + "vector_store": { + "default_provider": None, + "providers": [], + }, "quota_handlers": { "sqlite": None, "postgres": None, @@ -418,7 +421,10 @@ def test_dump_configuration_valid_values(tmp_path: Path) -> None: }, "approvals": _DEFAULT_APPROVALS_DUMP, "byok_rag": [], - "vector_store_providers": [], + "vector_store": { + "default_provider": None, + "providers": [], + }, "quota_handlers": { "sqlite": None, "postgres": None, @@ -778,7 +784,10 @@ def test_dump_configuration_with_quota_limiters(tmp_path: Path) -> None: }, "approvals": _DEFAULT_APPROVALS_DUMP, "byok_rag": [], - "vector_store_providers": [], + "vector_store": { + "default_provider": None, + "providers": [], + }, "quota_handlers": { "sqlite": None, "postgres": None, @@ -1037,7 +1046,10 @@ def test_dump_configuration_with_quota_limiters_different_values( }, "approvals": _DEFAULT_APPROVALS_DUMP, "byok_rag": [], - "vector_store_providers": [], + "vector_store": { + "default_provider": None, + "providers": [], + }, "quota_handlers": { "sqlite": None, "postgres": None, @@ -1291,7 +1303,10 @@ def test_dump_configuration_byok(tmp_path: Path) -> None: "password": None, }, ], - "vector_store_providers": [], + "vector_store": { + "default_provider": None, + "providers": [], + }, "quota_handlers": { "sqlite": None, "postgres": None, @@ -1510,7 +1525,10 @@ def test_dump_configuration_pg_namespace(tmp_path: Path) -> None: }, "approvals": _DEFAULT_APPROVALS_DUMP, "byok_rag": [], - "vector_store_providers": [], + "vector_store": { + "default_provider": None, + "providers": [], + }, "quota_handlers": { "sqlite": None, "postgres": None, @@ -1889,7 +1907,10 @@ def test_dump_configuration_allow_degraded_mode(tmp_path: Path) -> None: }, "approvals": _DEFAULT_APPROVALS_DUMP, "byok_rag": [], - "vector_store_providers": [], + "vector_store": { + "default_provider": None, + "providers": [], + }, "quota_handlers": { "sqlite": None, "postgres": None, @@ -2114,7 +2135,10 @@ def test_dump_configuration_max_retries_settings(tmp_path: Path) -> None: }, "approvals": _DEFAULT_APPROVALS_DUMP, "byok_rag": [], - "vector_store_providers": [], + "vector_store": { + "default_provider": None, + "providers": [], + }, "quota_handlers": { "sqlite": None, "postgres": None, @@ -2339,7 +2363,10 @@ def test_dump_configuration_retry_count_settings(tmp_path: Path) -> None: }, "approvals": _DEFAULT_APPROVALS_DUMP, "byok_rag": [], - "vector_store_providers": [], + "vector_store": { + "default_provider": None, + "providers": [], + }, "quota_handlers": { "sqlite": None, "postgres": None, @@ -2571,7 +2598,10 @@ def test_dump_configuration_specific_compaction_values(tmp_path: Path) -> None: }, "approvals": _DEFAULT_APPROVALS_DUMP, "byok_rag": [], - "vector_store_providers": [], + "vector_store": { + "default_provider": None, + "providers": [], + }, "quota_handlers": { "sqlite": None, "postgres": None, diff --git a/tests/unit/models/config/test_vector_store_providers.py b/tests/unit/models/config/test_vector_store.py similarity index 62% rename from tests/unit/models/config/test_vector_store_providers.py rename to tests/unit/models/config/test_vector_store.py index f040785d5..ad3c13fba 100644 --- a/tests/unit/models/config/test_vector_store_providers.py +++ b/tests/unit/models/config/test_vector_store.py @@ -1,4 +1,4 @@ -"""Unit tests for vector_store_providers config models.""" +"""Unit tests for vector_store configuration models.""" import copy from typing import Any @@ -23,14 +23,12 @@ def _base_config_dict() -> dict[str, Any]: def _faiss_provider( *, provider_id: str = "notebooks", - default: bool = False, path: str = "/var/lib/notebooks.db", ) -> dict[str, Any]: - """Return a minimal valid faiss vector_store_providers entry.""" + """Return a minimal valid faiss vector_store.providers entry.""" return { "id": provider_id, "type": "faiss", - "default": default, "embedding_model": "/rag-content/embeddings_model", "embedding_dimension": 768, "config": {"path": path}, @@ -45,7 +43,6 @@ def test_faiss_provider_accepts_nested_config() -> None: "type": "faiss", "embedding_model": "/rag-content/embeddings_model", "embedding_dimension": 768, - "default": True, "config": {"path": "/var/lib/notebooks.db"}, } ) @@ -53,7 +50,6 @@ def test_faiss_provider_accepts_nested_config() -> None: assert provider.type == "faiss" assert provider.config.path == "/var/lib/notebooks.db" assert provider.embedding_dimension == 768 - assert provider.default is True def test_faiss_requires_path() -> None: @@ -90,7 +86,6 @@ def test_pgvector_applies_env_defaults() -> None: "type": "pgvector", "embedding_model": "/emb", "embedding_dimension": 768, - "default": True, "config": {}, } ) @@ -140,51 +135,85 @@ def test_rejects_unknown_type() -> None: ) -def test_rejects_non_empty_list_with_no_default() -> None: - """Non-empty list with no default: true is rejected.""" +def test_rejects_non_empty_providers_without_default_provider() -> None: + """Non-empty providers without default_provider is rejected.""" config_dict = _base_config_dict() - config_dict["vector_store_providers"] = [_faiss_provider(default=False)] - with pytest.raises(ValidationError, match="default"): + config_dict["vector_store"] = {"providers": [_faiss_provider()]} + with pytest.raises(ValidationError, match="default_provider"): Configuration(**config_dict) -def test_rejects_multiple_defaults() -> None: - """Non-empty list with more than one default: true is rejected.""" +def test_rejects_default_provider_not_in_providers() -> None: + """default_provider must match one of providers[].id.""" config_dict = _base_config_dict() - config_dict["vector_store_providers"] = [ - _faiss_provider(provider_id="a", default=True, path="/tmp/a.db"), - _faiss_provider(provider_id="b", default=True, path="/tmp/b.db"), - ] - with pytest.raises(ValidationError, match="default"): + config_dict["vector_store"] = { + "default_provider": "missing", + "providers": [_faiss_provider(provider_id="notebooks")], + } + with pytest.raises(ValidationError, match="default_provider"): + Configuration(**config_dict) + + +def test_rejects_default_provider_when_providers_empty() -> None: + """default_provider must not be set when providers is empty.""" + config_dict = _base_config_dict() + config_dict["vector_store"] = { + "default_provider": "notebooks", + "providers": [], + } + with pytest.raises(ValidationError, match="default_provider"): Configuration(**config_dict) def test_duplicate_ids_rejected() -> None: - """Provider ids must be unique within vector_store_providers.""" + """Provider ids must be unique within vector_store.providers.""" config_dict = _base_config_dict() - config_dict["vector_store_providers"] = [ - _faiss_provider(provider_id="notebooks", default=True, path="/tmp/a.db"), - _faiss_provider(provider_id="notebooks", default=False, path="/tmp/b.db"), - ] + config_dict["vector_store"] = { + "default_provider": "notebooks", + "providers": [ + _faiss_provider(provider_id="notebooks", path="/tmp/a.db"), + _faiss_provider(provider_id="notebooks", path="/tmp/b.db"), + ], + } with pytest.raises(ValidationError, match="unique|duplicate"): Configuration(**config_dict) -def test_empty_list_ok_without_default() -> None: - """Empty vector_store_providers list is valid without a designated default.""" +def test_rejects_whitespace_only_default_provider() -> None: + """Whitespace-only default_provider is rejected.""" config_dict = _base_config_dict() - config_dict["vector_store_providers"] = [] + config_dict["vector_store"] = { + "default_provider": " ", + "providers": [_faiss_provider(provider_id="notebooks")], + } + with pytest.raises(ValidationError, match="default_provider"): + Configuration(**config_dict) + + +def test_empty_providers_ok_without_default_provider() -> None: + """Empty providers list is valid without a designated default.""" + config_dict = _base_config_dict() + config_dict["vector_store"] = {"providers": []} cfg = Configuration(**config_dict) - assert cfg.vector_store_providers == [] + # pylint: disable=no-member + assert cfg.vector_store.providers == [] + assert cfg.vector_store.default_provider is None -def test_single_default_provider_accepted() -> None: - """Non-empty list with exactly one default: true validates.""" +def test_default_provider_accepted() -> None: + """Non-empty providers with matching default_provider validates.""" config_dict = _base_config_dict() - config_dict["vector_store_providers"] = [ - _faiss_provider(provider_id="notebooks", default=True), - _faiss_provider(provider_id="other", default=False, path="/tmp/other.db"), - ] + config_dict["vector_store"] = { + "default_provider": "notebooks", + "providers": [ + _faiss_provider(provider_id="notebooks"), + _faiss_provider(provider_id="other", path="/tmp/other.db"), + ], + } cfg = Configuration(**config_dict) - by_id = {provider.id: provider.default for provider in cfg.vector_store_providers} - assert by_id == {"notebooks": True, "other": False} + # pylint: disable=no-member + assert cfg.vector_store.default_provider == "notebooks" + assert [provider.id for provider in cfg.vector_store.providers] == [ + "notebooks", + "other", + ] diff --git a/tests/unit/test_llama_stack_configuration.py b/tests/unit/test_llama_stack_configuration.py index 7ee0dca10..a1311af0d 100644 --- a/tests/unit/test_llama_stack_configuration.py +++ b/tests/unit/test_llama_stack_configuration.py @@ -18,7 +18,7 @@ enrich_azure_entra_id_inference, enrich_byok_rag, enrich_solr, - enrich_vector_store_providers, + enrich_vector_store, generate_configuration, ) from models.config import ( @@ -885,11 +885,11 @@ def test_enrich_solr_user_chunk_filter_query_is_conjoined() -> None: # ============================================================================= -# Test enrich_vector_store_providers +# Test enrich_vector_store # ============================================================================= -def test_enrich_vector_store_providers_faiss_appends() -> None: +def test_enrich_vector_store_faiss_appends() -> None: """Faiss provider appends vector_io, backend, and default_* settings.""" ls_config: dict[str, Any] = { "providers": { @@ -915,18 +915,20 @@ def test_enrich_vector_store_providers_faiss_appends() -> None: "default_provider_id": "faiss", }, } - enrich_vector_store_providers( + enrich_vector_store( ls_config, - [ - { - "id": "notebooks", - "type": "faiss", - "default": True, - "embedding_model": "/rag-content/embeddings_model", - "embedding_dimension": 768, - "config": {"path": "/var/lib/notebooks.db"}, - } - ], + { + "default_provider": "notebooks", + "providers": [ + { + "id": "notebooks", + "type": "faiss", + "embedding_model": "/rag-content/embeddings_model", + "embedding_dimension": 768, + "config": {"path": "/var/lib/notebooks.db"}, + } + ], + }, ) ids = {p["provider_id"] for p in ls_config["providers"]["vector_io"]} assert ids == {"faiss", "notebooks"} @@ -945,7 +947,7 @@ def test_enrich_vector_store_providers_faiss_appends() -> None: assert not ls_config["registered_resources"]["vector_stores"] -def test_enrich_vector_store_providers_replaces_same_provider_id() -> None: +def test_enrich_vector_store_replaces_same_provider_id() -> None: """Same provider_id replaces the baseline entry and leaves orphan backends.""" ls_config: dict[str, Any] = { "providers": { @@ -973,18 +975,20 @@ def test_enrich_vector_store_providers_replaces_same_provider_id() -> None: "registered_resources": {"models": []}, "vector_stores": {}, } - enrich_vector_store_providers( + enrich_vector_store( ls_config, - [ - { - "id": "notebooks", - "type": "faiss", - "default": True, - "embedding_model": "/emb", - "embedding_dimension": 768, - "config": {"path": "/new/notebooks.db"}, - } - ], + { + "default_provider": "notebooks", + "providers": [ + { + "id": "notebooks", + "type": "faiss", + "embedding_model": "/emb", + "embedding_dimension": 768, + "config": {"path": "/new/notebooks.db"}, + } + ], + }, ) providers = ls_config["providers"]["vector_io"] assert len(providers) == 1 @@ -999,7 +1003,7 @@ def test_enrich_vector_store_providers_replaces_same_provider_id() -> None: ) -def test_enrich_vector_store_providers_pgvector_no_kv_backend() -> None: +def test_enrich_vector_store_pgvector_no_kv_backend() -> None: """Pgvector provider does not create a kv_sqlite storage backend.""" ls_config: dict[str, Any] = { "providers": {}, @@ -1007,24 +1011,26 @@ def test_enrich_vector_store_providers_pgvector_no_kv_backend() -> None: "registered_resources": {}, "vector_stores": {}, } - enrich_vector_store_providers( + enrich_vector_store( ls_config, - [ - { - "id": "nb-pg", - "type": "pgvector", - "default": True, - "embedding_model": "/emb", - "embedding_dimension": 768, - "config": { - "host": "${env.POSTGRES_HOST}", - "port": "${env.POSTGRES_PORT}", - "db": "${env.POSTGRES_DATABASE}", - "user": "${env.POSTGRES_USER}", - "password": "${env.POSTGRES_PASSWORD}", - }, - } - ], + { + "default_provider": "nb-pg", + "providers": [ + { + "id": "nb-pg", + "type": "pgvector", + "embedding_model": "/emb", + "embedding_dimension": 768, + "config": { + "host": "${env.POSTGRES_HOST}", + "port": "${env.POSTGRES_PORT}", + "db": "${env.POSTGRES_DATABASE}", + "user": "${env.POSTGRES_USER}", + "password": "${env.POSTGRES_PASSWORD}", + }, + } + ], + }, ) provider = ls_config["providers"]["vector_io"][0] assert provider["provider_type"] == "remote::pgvector" @@ -1037,8 +1043,8 @@ def test_enrich_vector_store_providers_pgvector_no_kv_backend() -> None: assert "vsprov_nb-pg_storage" not in ls_config["storage"]["backends"] -def test_enrich_vector_store_providers_multiple_entries() -> None: - """Multi-entry list: both providers, faiss-only backend, one default_* winner.""" +def test_enrich_vector_store_multiple_entries() -> None: + """Multi-entry list: both providers, faiss-only backend, default_provider winner.""" ls_config: dict[str, Any] = { "providers": {}, "storage": {"backends": {}}, @@ -1047,29 +1053,30 @@ def test_enrich_vector_store_providers_multiple_entries() -> None: "annotation_prompt_params": {"enable_annotations": False}, }, } - enrich_vector_store_providers( + enrich_vector_store( ls_config, - [ - { - "id": "notebooks", - "type": "faiss", - "default": True, - "embedding_model": "/emb-faiss", - "embedding_dimension": 768, - "config": {"path": "/var/lib/notebooks.db"}, - }, - { - "id": "nb-pg", - "type": "pgvector", - "default": False, - "embedding_model": "/emb-pg", - "embedding_dimension": 768, - "config": { - "host": "${env.POSTGRES_HOST}", - "password": "${env.POSTGRES_PASSWORD}", + { + "default_provider": "notebooks", + "providers": [ + { + "id": "notebooks", + "type": "faiss", + "embedding_model": "/emb-faiss", + "embedding_dimension": 768, + "config": {"path": "/var/lib/notebooks.db"}, }, - }, - ], + { + "id": "nb-pg", + "type": "pgvector", + "embedding_model": "/emb-pg", + "embedding_dimension": 768, + "config": { + "host": "${env.POSTGRES_HOST}", + "password": "${env.POSTGRES_PASSWORD}", + }, + }, + ], + }, ) ids = {p["provider_id"] for p in ls_config["providers"]["vector_io"]} assert ids == {"notebooks", "nb-pg"} @@ -1089,7 +1096,7 @@ def test_enrich_vector_store_providers_multiple_entries() -> None: assert model_ids == {"/emb-faiss", "/emb-pg"} -def test_enrich_vector_store_providers_noop_without_entries() -> None: +def test_enrich_vector_store_noop_without_entries() -> None: """Empty list leaves baseline vector_stores defaults unchanged.""" ls_config: dict[str, Any] = { "providers": { @@ -1099,11 +1106,11 @@ def test_enrich_vector_store_providers_noop_without_entries() -> None: }, "vector_stores": {"default_provider_id": "faiss"}, } - enrich_vector_store_providers(ls_config, []) + enrich_vector_store(ls_config, {"providers": []}) assert ls_config["vector_stores"]["default_provider_id"] == "faiss" -def test_enrich_vector_store_providers_dedupes_embedding_model() -> None: +def test_enrich_vector_store_dedupes_embedding_model() -> None: """Same provider_model_id as an existing model does not add a second row.""" ls_config: dict[str, Any] = { "providers": {}, @@ -1122,18 +1129,20 @@ def test_enrich_vector_store_providers_dedupes_embedding_model() -> None: }, "vector_stores": {}, } - enrich_vector_store_providers( + enrich_vector_store( ls_config, - [ - { - "id": "notebooks", - "type": "faiss", - "default": True, - "embedding_model": "/rag-content/embeddings_model", - "embedding_dimension": 768, - "config": {"path": "/tmp/n.db"}, - } - ], + { + "default_provider": "notebooks", + "providers": [ + { + "id": "notebooks", + "type": "faiss", + "embedding_model": "/rag-content/embeddings_model", + "embedding_dimension": 768, + "config": {"path": "/tmp/n.db"}, + } + ], + }, ) assert len(ls_config["registered_resources"]["models"]) == 1 diff --git a/tests/unit/test_llama_stack_synthesize.py b/tests/unit/test_llama_stack_synthesize.py index 409c52c67..dddb6f006 100644 --- a/tests/unit/test_llama_stack_synthesize.py +++ b/tests/unit/test_llama_stack_synthesize.py @@ -630,23 +630,25 @@ def test_synthesize_enriches_byok_rag_like_legacy() -> None: assert "vector_io" in result.get("providers", {}) -def test_synthesize_includes_vector_store_providers() -> None: - """vector_store_providers enrichment runs during unified synthesis.""" +def test_synthesize_includes_vector_store() -> None: + """vector_store enrichment runs during unified synthesis.""" lcs_config = { "llama_stack": { "use_as_library_client": True, "config": {"baseline": "default"}, }, - "vector_store_providers": [ - { - "id": "notebooks", - "type": "faiss", - "default": True, - "embedding_model": "/rag-content/embeddings_model", - "embedding_dimension": 768, - "config": {"path": "/tmp/notebooks.db"}, - } - ], + "vector_store": { + "default_provider": "notebooks", + "providers": [ + { + "id": "notebooks", + "type": "faiss", + "embedding_model": "/rag-content/embeddings_model", + "embedding_dimension": 768, + "config": {"path": "/tmp/notebooks.db"}, + } + ], + }, } result = synthesize_configuration(lcs_config) ids = {p["provider_id"] for p in result["providers"]["vector_io"]} diff --git a/tests/unit/utils/test_models_dumper.py b/tests/unit/utils/test_models_dumper.py index db6c55e13..3c0edf7df 100644 --- a/tests/unit/utils/test_models_dumper.py +++ b/tests/unit/utils/test_models_dumper.py @@ -9318,6 +9318,7 @@ def test_dump_models(tmpdir: Path) -> None: "UnifiedLlamaStackConfig", "UnprocessableEntityResponse", "UserDataCollection", + "VectorStoreConfiguration", "VectorStoreCreateRequest", "VectorStoreDeleteResponse", "VectorStoreFileCreateRequest", From ca95dd32a4014ff8b5ba620325b7b839ced1c9aa Mon Sep 17 00:00:00 2001 From: Jordan Dubrick Date: Mon, 27 Jul 2026 12:10:08 -0400 Subject: [PATCH 09/10] address coderabbit review Signed-off-by: Jordan Dubrick --- docs/user_doc/config.md | 4 +- docs/user_doc/rag_guide.md | 4 +- src/llama_stack_configuration.py | 7 +++ src/models/config.py | 49 +++++++++++----- .../models/config/test_dump_configuration.py | 57 +++++++++++++++++++ .../config/test_llama_stack_configuration.py | 47 +++++++++++++++ tests/unit/test_llama_stack_configuration.py | 57 +++++++++++++++++++ 7 files changed, 209 insertions(+), 16 deletions(-) diff --git a/docs/user_doc/config.md b/docs/user_doc/config.md index 5b7479f64..9556c301d 100644 --- a/docs/user_doc/config.md +++ b/docs/user_doc/config.md @@ -936,5 +936,5 @@ Mirrors ``InferenceConfiguration``: a providers list plus a sibling | Field | Type | Description | |------------------|--------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| default_provider | string | Provider id used for vector_stores.default_* in the synthesized Llama Stack config. Required when providers is non-empty; must match one of providers[].id. | -| providers | array | Dynamic vector-store provider capacity for runtime POST /v1/vector-stores creates. Not the same as byok_rag (static registered corpora). | +| default_provider | string | Provider id used for vector_stores.default_* in the synthesized Llama Stack config. Required when providers is non-empty; must match one of providers[].id. Must be omitted when providers is empty. | +| providers | array | Dynamic vector-store provider capacity for runtime POST /v1/vector-stores creates. Not the same as byok_rag (static registered corpora). | diff --git a/docs/user_doc/rag_guide.md b/docs/user_doc/rag_guide.md index e3801ccde..e888f1265 100644 --- a/docs/user_doc/rag_guide.md +++ b/docs/user_doc/rag_guide.md @@ -149,9 +149,11 @@ Requirements: - `embedding_model` and `embedding_dimension` are **required** on every provider entry - When `providers` is non-empty, `default_provider` is **required** and must match one of `providers[].id` +- When `providers` is empty, `default_provider` must be omitted - Provider `id` must match `[a-z0-9_-]+` and must not start with `byok_` - Applied in **unified** Llama Stack synthesis only - (`llama_stack.use_as_library_client: true` with `llama_stack.config`) + (`llama_stack.use_as_library_client: true` with a synthesis input such as + `llama_stack.config`, `inference.providers`, or `vector_store.providers`) `default_provider` becomes `vector_stores.default_provider_id` and that provider's embedding model becomes `default_embedding_model` in the diff --git a/src/llama_stack_configuration.py b/src/llama_stack_configuration.py index 7790f2ef9..68f344b1a 100644 --- a/src/llama_stack_configuration.py +++ b/src/llama_stack_configuration.py @@ -372,6 +372,9 @@ def _build_vector_io_config( Returns: dict[str, Any]: Provider config mapping. + + Raises: + ValueError: If ``rag_type`` is not present in VECTOR_IO_TEMPLATES. """ template = VECTOR_IO_TEMPLATES.get(rag_type) if template is None: @@ -580,6 +583,10 @@ def _vsprov_fields_and_backend( Returns: Tuple of (extra_fields, backend_name, backend_entry_or_None). + + Raises: + ValueError: If ``product_type`` is not a supported vector-store + provider type. """ backend_name = f"vsprov_{provider_id}_storage" if product_type == "faiss": diff --git a/src/models/config.py b/src/models/config.py index 54f94f8cd..c5f0aa639 100644 --- a/src/models/config.py +++ b/src/models/config.py @@ -2190,7 +2190,16 @@ def apply_pgvector_env_defaults(self) -> Self: class VectorStoreProviderBase(ConfigurationBase): - """Shared fields for dynamic vector-store provider capacity entries.""" + """Shared fields for dynamic vector-store provider capacity entries. + + Attributes: + id: Llama Stack vector_io provider_id. Surrounding whitespace is + stripped before validation and emission. + embedding_model: Embedding model identification used for stores + created against this provider. + embedding_dimension: Dimensionality of embedding vectors for this + provider. + """ id: str = Field( ..., @@ -2283,6 +2292,15 @@ class VectorStoreConfiguration(ConfigurationBase): Mirrors ``InferenceConfiguration``: a providers list plus a sibling ``default_provider`` pointer, rather than a per-entry default flag. + + Attributes: + default_provider: Provider id used for vector_stores.default_* in the + synthesized Llama Stack config. Required when providers is + non-empty; must match one of providers[].id. Must be omitted when + providers is empty. + providers: Dynamic vector-store provider capacity for runtime + POST /v1/vector-stores creates. Not the same as byok_rag (static + registered corpora). """ default_provider: Optional[str] = Field( @@ -3160,17 +3178,18 @@ def check_unified_vs_legacy(self) -> Self: """Reconcile unified synthesis inputs, legacy mode, and library-mode needs. Unified-mode *synthesis inputs* span the configuration root: a non-empty - top-level ``inference.providers`` (Decision S5) and/or a - ``llama_stack.config`` block. The legacy path is - ``llama_stack.library_client_config_path`` pointing at an external - run.yaml. Both checks live here on the root model rather than on - ``LlamaStackConfiguration`` (which cannot see ``inference.providers``): + top-level ``inference.providers`` (Decision S5), a non-empty + ``vector_store.providers``, and/or a ``llama_stack.config`` block. The + legacy path is ``llama_stack.library_client_config_path`` pointing at an + external run.yaml. Both checks live here on the root model rather than + on ``LlamaStackConfiguration`` (which cannot see root-level provider + lists): - A synthesis input and the legacy path are mutually exclusive — a single file must pick one shape. - Library mode needs *some* run source — a synthesis input or the - legacy path. ``inference.providers`` alone is sufficient; no - ``llama_stack.config`` block is required. + legacy path. ``inference.providers`` or ``vector_store.providers`` + alone is sufficient; no ``llama_stack.config`` block is required. Returns: Self: The validated configuration instance. @@ -3182,14 +3201,17 @@ def check_unified_vs_legacy(self) -> Self: """ # pylint: disable=no-member synthesis_input = ( - bool(self.inference.providers) or self.llama_stack.config is not None + bool(self.inference.providers) + or bool(self.vector_store.providers) + or self.llama_stack.config is not None ) legacy_input = self.llama_stack.library_client_config_path is not None if synthesis_input and legacy_input: raise ValueError( "Llama Stack configuration is ambiguous: unified synthesis " - "inputs (a non-empty inference.providers or a llama_stack.config " - "block) are mutually exclusive with the legacy " + "inputs (a non-empty inference.providers, a non-empty " + "vector_store.providers, or a llama_stack.config block) are " + "mutually exclusive with the legacy " "llama_stack.library_client_config_path. Use one or the other. " "To convert a legacy two-file setup to unified mode, run " "`lightspeed-stack --migrate-config`." @@ -3201,8 +3223,9 @@ def check_unified_vs_legacy(self) -> Self: ): raise ValueError( "Llama Stack library mode requires a run-configuration source: " - "set a non-empty inference.providers, a llama_stack.config " - "block, or library_client_config_path." + "set a non-empty inference.providers, a non-empty " + "vector_store.providers, a llama_stack.config block, or " + "library_client_config_path." ) return self diff --git a/tests/unit/models/config/test_dump_configuration.py b/tests/unit/models/config/test_dump_configuration.py index 2debc0291..5aae2db4b 100644 --- a/tests/unit/models/config/test_dump_configuration.py +++ b/tests/unit/models/config/test_dump_configuration.py @@ -16,6 +16,8 @@ Configuration, CORSConfiguration, DatabaseConfiguration, + FaissVectorStoreProvider, + FaissVectorStoreProviderConfig, InferenceConfiguration, LlamaStackConfiguration, ModelContextProtocolServer, @@ -26,7 +28,9 @@ ServiceConfiguration, SkillsConfiguration, TLSConfiguration, + UnifiedLlamaStackConfig, UserDataCollection, + VectorStoreConfiguration, ) _DEFAULT_APPROVALS_DUMP: dict[str, int] = { @@ -1105,6 +1109,59 @@ def test_dump_configuration_with_quota_limiters_different_values( } +def test_dump_configuration_with_vector_store(tmp_path: Path) -> None: + """Dump preserves a configured vector_store default_provider and providers.""" + cfg = Configuration( + name="test_name", + service=ServiceConfiguration( + tls_config=TLSConfiguration( + tls_certificate_path=Path("tests/configuration/server.crt"), + tls_key_path=Path("tests/configuration/server.key"), + tls_key_password=Path("tests/configuration/password"), + ), + cors=CORSConfiguration(), + ), + llama_stack=LlamaStackConfiguration( + use_as_library_client=True, + config=UnifiedLlamaStackConfig(baseline="default"), + api_key=SecretStr("whatever"), + ), + user_data_collection=UserDataCollection( + feedback_enabled=False, feedback_storage=None + ), + vector_store=VectorStoreConfiguration( + default_provider="notebooks", + providers=[ + FaissVectorStoreProvider( + id="notebooks", + type="faiss", + embedding_model="/rag-content/embeddings_model", + embedding_dimension=768, + config=FaissVectorStoreProviderConfig(path="/var/lib/notebooks.db"), + ) + ], + ), + ) + dump_file = tmp_path / "test.json" + cfg.dump(dump_file) + + with open(dump_file, "r", encoding="utf-8") as fin: + content = json.load(fin) + + assert content["vector_store"] == { + "default_provider": "notebooks", + "providers": [ + { + "id": "notebooks", + "type": "faiss", + "embedding_model": "/rag-content/embeddings_model", + "embedding_dimension": 768, + "config": {"path": "/var/lib/notebooks.db"}, + } + ], + } + + def test_dump_configuration_byok(tmp_path: Path) -> None: """ Test that the Configuration object can be serialized to a JSON file and diff --git a/tests/unit/models/config/test_llama_stack_configuration.py b/tests/unit/models/config/test_llama_stack_configuration.py index cdaed0059..239815d8f 100644 --- a/tests/unit/models/config/test_llama_stack_configuration.py +++ b/tests/unit/models/config/test_llama_stack_configuration.py @@ -259,6 +259,53 @@ def test_root_rejects_inference_providers_and_legacy_path_together() -> None: Configuration(**config_dict) +def test_root_rejects_vector_store_providers_and_legacy_path_together() -> None: + """Non-empty vector_store.providers plus a legacy path fail at load.""" + config_dict = _base_config_dict() + config_dict["llama_stack"] = { + "use_as_library_client": True, + "library_client_config_path": "tests/configuration/run.yaml", + } + config_dict["vector_store"] = { + "default_provider": "notebooks", + "providers": [ + { + "id": "notebooks", + "type": "faiss", + "embedding_model": "/rag-content/embeddings_model", + "embedding_dimension": 768, + "config": {"path": "/var/lib/notebooks.db"}, + } + ], + } + with pytest.raises(ValidationError, match="mutually exclusive"): + Configuration(**config_dict) + + +def test_root_accepts_vector_store_providers_only_no_config_block() -> None: + """Library mode driven by vector_store.providers alone is valid.""" + config_dict = _base_config_dict() + config_dict["llama_stack"] = {"use_as_library_client": True} + config_dict["inference"] = {"providers": []} + config_dict["vector_store"] = { + "default_provider": "notebooks", + "providers": [ + { + "id": "notebooks", + "type": "faiss", + "embedding_model": "/rag-content/embeddings_model", + "embedding_dimension": 768, + "config": {"path": "/var/lib/notebooks.db"}, + } + ], + } + cfg = Configuration(**config_dict) + # pylint: disable=no-member + assert cfg.llama_stack.config is None + assert cfg.vector_store.default_provider == "notebooks" + assert len(cfg.vector_store.providers) == 1 + + def test_root_accepts_unified_library_config() -> None: """A unified library-mode config (no legacy path) loads cleanly (R1).""" config_dict = _base_config_dict() diff --git a/tests/unit/test_llama_stack_configuration.py b/tests/unit/test_llama_stack_configuration.py index a1311af0d..74bd9d3fa 100644 --- a/tests/unit/test_llama_stack_configuration.py +++ b/tests/unit/test_llama_stack_configuration.py @@ -1147,6 +1147,63 @@ def test_enrich_vector_store_dedupes_embedding_model() -> None: assert len(ls_config["registered_resources"]["models"]) == 1 +def test_enrich_vector_store_skips_embedding_without_dimension() -> None: + """embedding_model without embedding_dimension does not register a model.""" + ls_config: dict[str, Any] = { + "providers": {}, + "storage": {"backends": {}}, + "registered_resources": {"models": []}, + "vector_stores": {"default_provider_id": "faiss"}, + } + enrich_vector_store( + ls_config, + { + "default_provider": "notebooks", + "providers": [ + { + "id": "notebooks", + "type": "faiss", + "embedding_model": "/emb", + "config": {"path": "/var/lib/notebooks.db"}, + } + ], + }, + ) + assert ls_config["providers"]["vector_io"][0]["provider_id"] == "notebooks" + assert not ls_config["registered_resources"]["models"] + assert ls_config["vector_stores"]["default_provider_id"] == "notebooks" + assert "default_embedding_model" in ls_config["vector_stores"] + + +def test_enrich_vector_store_unmatched_default_provider_skips_defaults() -> None: + """Unmatched default_provider still enriches providers but skips default_*.""" + ls_config: dict[str, Any] = { + "providers": {}, + "storage": {"backends": {}}, + "registered_resources": {"models": []}, + "vector_stores": {"default_provider_id": "faiss"}, + } + enrich_vector_store( + ls_config, + { + "default_provider": "missing", + "providers": [ + { + "id": "notebooks", + "type": "faiss", + "embedding_model": "/emb", + "embedding_dimension": 768, + "config": {"path": "/var/lib/notebooks.db"}, + } + ], + }, + ) + assert ls_config["providers"]["vector_io"][0]["provider_id"] == "notebooks" + assert ls_config["vector_stores"]["default_provider_id"] == "faiss" + assert "default_embedding_model" not in ls_config["vector_stores"] + assert len(ls_config["registered_resources"]["models"]) == 1 + + # ============================================================================= # Test _build_vector_io_config # ============================================================================= From fb409d70681a18f04b9f760f2b2f89b858f1bb67 Mon Sep 17 00:00:00 2001 From: Jordan Dubrick Date: Mon, 27 Jul 2026 12:18:00 -0400 Subject: [PATCH 10/10] regen openapi.json doc Signed-off-by: Jordan Dubrick --- docs/devel_doc/openapi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/devel_doc/openapi.json b/docs/devel_doc/openapi.json index 724005153..5c8161773 100644 --- a/docs/devel_doc/openapi.json +++ b/docs/devel_doc/openapi.json @@ -20792,7 +20792,7 @@ "additionalProperties": false, "type": "object", "title": "VectorStoreConfiguration", - "description": "Configuration for dynamic vector-store providers.\n\nMirrors ``InferenceConfiguration``: a providers list plus a sibling\n``default_provider`` pointer, rather than a per-entry default flag." + "description": "Configuration for dynamic vector-store providers.\n\nMirrors ``InferenceConfiguration``: a providers list plus a sibling\n``default_provider`` pointer, rather than a per-entry default flag.\n\nAttributes:\n default_provider: Provider id used for vector_stores.default_* in the\n synthesized Llama Stack config. Required when providers is\n non-empty; must match one of providers[].id. Must be omitted when\n providers is empty.\n providers: Dynamic vector-store provider capacity for runtime\n POST /v1/vector-stores creates. Not the same as byok_rag (static\n registered corpora)." }, "VectorStoreCreateRequest": { "properties": {