From 517bd79d65673a08e05a9f4b5eb1726635a1da89 Mon Sep 17 00:00:00 2001 From: Priyansh Nandwana Date: Sun, 30 Aug 2026 10:47:56 +0530 Subject: [PATCH 1/2] fix(vector_stores): stop sharing one list across default-constructed registries VectorStoreIndexRegistry and VectorStoreRegistry took a mutable list as their default argument. Python evaluates that once at class definition, so every registry built without arguments shared one list. The proxy builds one that way during startup, so an upsert into it leaked into every registry created after, and into the class default itself. Default to None and build a fresh list per instance. A caller-supplied list is still used as given, including an empty one, so nothing that passes its own list changes behaviour. Fixes #38874 --- .../vector_stores/vector_store_registry.py | 10 +++-- .../test_vector_store_registry.py | 40 ++++++++++++++++++- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/litellm/vector_stores/vector_store_registry.py b/litellm/vector_stores/vector_store_registry.py index e78d2aa5f6a..6a6668119b5 100644 --- a/litellm/vector_stores/vector_store_registry.py +++ b/litellm/vector_stores/vector_store_registry.py @@ -32,8 +32,10 @@ else: class VectorStoreIndexRegistry: - def __init__(self, vector_store_indexes: list[LiteLLM_ManagedVectorStoreIndex] = []): - self.vector_store_indexes: list[LiteLLM_ManagedVectorStoreIndex] = vector_store_indexes + def __init__(self, vector_store_indexes: list[LiteLLM_ManagedVectorStoreIndex] | None = None): + self.vector_store_indexes: list[LiteLLM_ManagedVectorStoreIndex] = ( + vector_store_indexes if vector_store_indexes is not None else [] + ) def get_vector_store_indexes(self) -> list[LiteLLM_ManagedVectorStoreIndex]: """ @@ -101,8 +103,8 @@ 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): + self.vector_stores: list[LiteLLM_ManagedVectorStore] = vector_stores if vector_stores is not None else [] self.vector_store_ids_to_vector_store_map: dict[str, LiteLLM_ManagedVectorStore] = {} def _extract_tool_params(self, tool: dict) -> VectorStoreToolParams: diff --git a/tests/unit/vector_stores/test_vector_store_registry.py b/tests/unit/vector_stores/test_vector_store_registry.py index 762176d6a81..d3008741a18 100644 --- a/tests/unit/vector_stores/test_vector_store_registry.py +++ b/tests/unit/vector_stores/test_vector_store_registry.py @@ -13,7 +13,10 @@ from unittest.mock import AsyncMock, MagicMock import litellm from litellm.types.vector_stores import LiteLLM_ManagedVectorStore from litellm.vector_stores.main import search -from litellm.vector_stores.vector_store_registry import VectorStoreRegistry +from litellm.vector_stores.vector_store_registry import ( + VectorStoreIndexRegistry, + VectorStoreRegistry, +) @pytest.fixture(autouse=True) @@ -249,3 +252,38 @@ async def test_config_owned_store_survives_db_liveness_check_while_missing_db_st prisma_client.db.litellm_managedvectorstorestable.find_unique.assert_awaited_once_with( where={"vector_store_id": "vs_from_db"} ) + + +class TestRegistriesDoNotShareDefaultState: + """#38874: a mutable default argument made every default-constructed registry share + one list, so upserting into one polluted every other instance and the class default.""" + + def test_vector_store_registries_do_not_share_a_list(self): + first = VectorStoreRegistry() + second = VectorStoreRegistry() + assert first.vector_stores is not second.vector_stores + + def test_index_registries_do_not_share_a_list(self): + first = VectorStoreIndexRegistry() + second = VectorStoreIndexRegistry() + assert first.vector_store_indexes is not second.vector_store_indexes + + def test_mutating_one_registry_leaves_the_next_one_empty(self): + first = VectorStoreRegistry() + first.vector_stores.append( + LiteLLM_ManagedVectorStore(vector_store_id="vs-leak", custom_llm_provider="bedrock") + ) + assert VectorStoreRegistry().vector_stores == [] + + def test_mutating_one_index_registry_leaves_the_next_one_empty(self): + first = VectorStoreIndexRegistry() + first.vector_store_indexes.append({"index_name": "leak"}) + assert VectorStoreIndexRegistry().get_vector_store_indexes() == [] + + def test_a_supplied_list_is_still_used_as_given(self): + supplied = [LiteLLM_ManagedVectorStore(vector_store_id="vs-1", custom_llm_provider="bedrock")] + assert VectorStoreRegistry(vector_stores=supplied).vector_stores is supplied + + def test_a_supplied_empty_list_is_still_used_as_given(self): + supplied: list = [] + assert VectorStoreRegistry(vector_stores=supplied).vector_stores is supplied From 620f586dbbd15dd42e9df6a285b4c6f3d7bb3270 Mon Sep 17 00:00:00 2001 From: Priyansh Nandwana Date: Mon, 14 Sep 2026 11:27:33 +0530 Subject: [PATCH 2/2] test(vector_stores): annotate locals Final and mutate registries through their public API The leak tests now go through add_vector_store_to_registry and upsert_vector_store_index rather than appending to the registry's list directly, which is closer to how the proxy hits the bug. --- .../test_vector_store_registry.py | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/tests/unit/vector_stores/test_vector_store_registry.py b/tests/unit/vector_stores/test_vector_store_registry.py index d3008741a18..f435b1e7630 100644 --- a/tests/unit/vector_stores/test_vector_store_registry.py +++ b/tests/unit/vector_stores/test_vector_store_registry.py @@ -8,10 +8,15 @@ from fastapi.testclient import TestClient from datetime import datetime, timezone +from typing import Final from unittest.mock import AsyncMock, MagicMock import litellm -from litellm.types.vector_stores import LiteLLM_ManagedVectorStore +from litellm.types.vector_stores import ( + IndexCreateLiteLLMParams, + LiteLLM_ManagedVectorStore, + LiteLLM_ManagedVectorStoreIndex, +) from litellm.vector_stores.main import search from litellm.vector_stores.vector_store_registry import ( VectorStoreIndexRegistry, @@ -255,35 +260,38 @@ async def test_config_owned_store_survives_db_liveness_check_while_missing_db_st class TestRegistriesDoNotShareDefaultState: - """#38874: a mutable default argument made every default-constructed registry share - one list, so upserting into one polluted every other instance and the class default.""" + """#38874: default-constructed registries must not share one list.""" def test_vector_store_registries_do_not_share_a_list(self): - first = VectorStoreRegistry() - second = VectorStoreRegistry() + first: Final = VectorStoreRegistry() + second: Final = VectorStoreRegistry() assert first.vector_stores is not second.vector_stores def test_index_registries_do_not_share_a_list(self): - first = VectorStoreIndexRegistry() - second = VectorStoreIndexRegistry() + first: Final = VectorStoreIndexRegistry() + second: Final = VectorStoreIndexRegistry() assert first.vector_store_indexes is not second.vector_store_indexes - def test_mutating_one_registry_leaves_the_next_one_empty(self): - first = VectorStoreRegistry() - first.vector_stores.append( + def test_adding_to_one_registry_leaves_the_next_one_empty(self): + VectorStoreRegistry().add_vector_store_to_registry( LiteLLM_ManagedVectorStore(vector_store_id="vs-leak", custom_llm_provider="bedrock") ) assert VectorStoreRegistry().vector_stores == [] - def test_mutating_one_index_registry_leaves_the_next_one_empty(self): - first = VectorStoreIndexRegistry() - first.vector_store_indexes.append({"index_name": "leak"}) + def test_upserting_into_one_index_registry_leaves_the_next_one_empty(self): + VectorStoreIndexRegistry().upsert_vector_store_index( + LiteLLM_ManagedVectorStoreIndex( + id="idx-leak", + index_name="leak", + litellm_params=IndexCreateLiteLLMParams(vector_store_index="vs-leak", vector_store_name="leak"), + ) + ) assert VectorStoreIndexRegistry().get_vector_store_indexes() == [] def test_a_supplied_list_is_still_used_as_given(self): - supplied = [LiteLLM_ManagedVectorStore(vector_store_id="vs-1", custom_llm_provider="bedrock")] + supplied: Final = [LiteLLM_ManagedVectorStore(vector_store_id="vs-1", custom_llm_provider="bedrock")] assert VectorStoreRegistry(vector_stores=supplied).vector_stores is supplied def test_a_supplied_empty_list_is_still_used_as_given(self): - supplied: list = [] + supplied: Final[list[LiteLLM_ManagedVectorStore]] = [] assert VectorStoreRegistry(vector_stores=supplied).vector_stores is supplied