From bec56a451d8e1674d1e768eed8ece94c6bf80509 Mon Sep 17 00:00:00 2001 From: Jordan Dubrick Date: Tue, 21 Jul 2026 14:30:08 -0400 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 4/7] 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 5/7] 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 6/7] 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 7/7] 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( ...,