From 97b9d9a0f9b585784be22eadbc16ab422d40ad4a Mon Sep 17 00:00:00 2001 From: KK-MCP Date: Tue, 25 Aug 2026 14:05:55 -0700 Subject: [PATCH] fix: mutable-default in VectorStoreRegistry, ruff format, endpoint test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Root-cause the shard failures on the previous commit: `__init__`'s `vector_stores: list = []` default was a shared mutable, so every `VectorStoreRegistry()` without an explicit list appended to the same underlying list — the four new tests leaked state into one another and passed locally only because they were the only registrants. Switch the default to `None` and materialize a per-instance list. - Apply `ruff format` to vector_store_registry.py (fixes `lint` job). - Add an endpoint-level regression test to raise codecov/patch: `test_list_vector_stores_returns_config_sourced_and_leaves_registry_intact` exercises the DB-empty + config-loaded branch of `list_vector_stores` and asserts the registry is NOT mutated after the call. --- .../vector_stores/vector_store_registry.py | 11 ++-- .../test_vector_store_endpoints.py | 62 +++++++++++++++++++ 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/litellm/vector_stores/vector_store_registry.py b/litellm/vector_stores/vector_store_registry.py index 2a7a0b7edd4..2c875eb5e46 100644 --- a/litellm/vector_stores/vector_store_registry.py +++ b/litellm/vector_stores/vector_store_registry.py @@ -94,8 +94,12 @@ class VectorStoreIndexRegistry: class VectorStoreRegistry: - def __init__(self, vector_stores: list[LiteLLM_ManagedVectorStore] = []): - self.vector_stores: list[LiteLLM_ManagedVectorStore] = vector_stores + def __init__(self, vector_stores: list[LiteLLM_ManagedVectorStore] | None = None): + # Never bind a mutable default: the previous `= []` default caused every + # VectorStoreRegistry() with no argument to share the same list, so a + # mutation in one place (test, request handler, etc.) leaked into every + # other caller. + self.vector_stores: list[LiteLLM_ManagedVectorStore] = list(vector_stores) if vector_stores is not None else [] self.vector_store_ids_to_vector_store_map: dict[str, LiteLLM_ManagedVectorStore] = {} # IDs of vector stores that were loaded from config.yaml / kustomization. # These have no DB row, so reconciliation code paths that treat "in memory @@ -400,8 +404,7 @@ class VectorStoreRegistry: list-endpoint reconciliation on the very next reload. """ new_config_ids: set[str] = { - (cfg.get("litellm_params") or {}).get("vector_store_id") - for cfg in vector_stores_config + (cfg.get("litellm_params") or {}).get("vector_store_id") for cfg in vector_stores_config } new_config_ids.discard(None) # ids missing here will re-raise below with a clearer error diff --git a/tests/test_litellm/proxy/vector_store_endpoints/test_vector_store_endpoints.py b/tests/test_litellm/proxy/vector_store_endpoints/test_vector_store_endpoints.py index 905928428b7..49ca2db5059 100644 --- a/tests/test_litellm/proxy/vector_store_endpoints/test_vector_store_endpoints.py +++ b/tests/test_litellm/proxy/vector_store_endpoints/test_vector_store_endpoints.py @@ -3107,3 +3107,65 @@ class TestAzureAIAnalyzeNamedIndexClassification: user_api_key_dict=self._team_member("analyze", ["read"]), ) assert result is True + + +# --------------------------------------------------------------------------- +# /vector_store/list — config-sourced entries must survive DB reconciliation. +# Regression test for: config.yaml-loaded stores were pruned from the +# in-memory registry on every list call because they had no DB row. +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_list_vector_stores_returns_config_sourced_and_leaves_registry_intact(): + from litellm.proxy.vector_store_endpoints.management_endpoints import list_vector_stores + from litellm.vector_stores.vector_store_registry import VectorStoreRegistry + + registry = VectorStoreRegistry() + registry.load_vector_stores_from_config( + [ + { + "vector_store_name": "docs-kb", + "litellm_params": { + "vector_store_id": "vs_from_config", + "custom_llm_provider": "bedrock", + }, + } + ] + ) + + original_registry = litellm.vector_store_registry + try: + litellm.vector_store_registry = registry + + with ( + # Empty DB — the store exists only in config/memory. + patch( + "litellm.vector_stores.vector_store_registry.VectorStoreRegistry._get_vector_stores_from_db", + new=AsyncMock(return_value=[]), + ), + patch("litellm.proxy.proxy_server.prisma_client", MagicMock()), + patch( + "litellm.proxy.vector_store_endpoints.management_endpoints._check_vector_store_access", + new=AsyncMock(return_value=True), + ), + patch( + "litellm.proxy.vector_store_endpoints.management_endpoints.check_feature_access_for_user", + new=AsyncMock(return_value=None), + ), + ): + response = await list_vector_stores( + user_api_key_dict=UserAPIKeyAuth( + token="sk-test", + key_name="sk-...test", + user_role=LitellmUserRoles.PROXY_ADMIN, + ), + ) + + returned_ids = [vs["vector_store_id"] for vs in response.data] + assert "vs_from_config" in returned_ids + # And, critically, the registry must NOT have been mutated — + # config-loaded stores stay resident for downstream routing. + assert any(vs.get("vector_store_id") == "vs_from_config" for vs in registry.vector_stores) + finally: + litellm.vector_store_registry = original_registry