From 8040927673d1dcee7f31fd663911afad81acef30 Mon Sep 17 00:00:00 2001 From: Sarveswaran MG Date: Tue, 21 Jul 2026 13:08:10 +0530 Subject: [PATCH 1/5] fix(vector_stores): stop sharing mutable default state across registry instances VectorStoreRegistry and VectorStoreIndexRegistry both declared their constructor argument with a mutable `= []` default. That list is created once at def-time and shared by every instance built without an explicit argument, and several methods append into it: add_vector_store_to_registry, update_vector_store_in_registry, load_vector_stores_from_config and upsert_vector_store_index. A brand-new registry therefore inherited a previous instance's vector stores, including their credential references consumed by get_credentials_for_vector_store. The proxy reaches the no-arg path in ProxyConfig.load_config, where VectorStoreRegistry() is constructed and then immediately fed load_vector_stores_from_config. delete_vector_store_from_registry made it worse by rebinding self.vector_stores to a fresh list rather than mutating, so a delete never removed the entry from the shared default and any later no-arg registry resurrected it. Both constructors now take Optional[...] = None and copy into their own list, which also stops a caller-supplied list from being mutated behind the caller's back. Also fixes delete_vector_store_index, which was an unconditional no-op: it compared each stored LiteLLM_ManagedVectorStoreIndex against the index-name string, and a model is never equal to a str, so the comprehension always returned the list unchanged. It now filters on index_name, matching get_vector_store_index_by_name and is_vector_store_index. --- .../vector_stores/vector_store_registry.py | 12 +-- .../test_vector_store_registry.py | 96 ++++++++++++++++++- 2 files changed, 100 insertions(+), 8 deletions(-) diff --git a/litellm/vector_stores/vector_store_registry.py b/litellm/vector_stores/vector_store_registry.py index 5070db1c89e..2bac8921d5f 100644 --- a/litellm/vector_stores/vector_store_registry.py +++ b/litellm/vector_stores/vector_store_registry.py @@ -25,8 +25,8 @@ 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: Optional[List[LiteLLM_ManagedVectorStoreIndex]] = None): + self.vector_store_indexes: List[LiteLLM_ManagedVectorStoreIndex] = list(vector_store_indexes or ()) def get_vector_store_indexes(self) -> List[LiteLLM_ManagedVectorStoreIndex]: """ @@ -55,11 +55,11 @@ class VectorStoreIndexRegistry: return self.vector_store_indexes.append(vector_store_index) - def delete_vector_store_index(self, vector_store_index: str): + def delete_vector_store_index(self, index_name: str): """ Deletes a vector store index from the registry """ - self.vector_store_indexes = [index for index in self.vector_store_indexes if index != vector_store_index] + self.vector_store_indexes = [index for index in self.vector_store_indexes if index.index_name != index_name] def is_vector_store_index(self, vector_store_index_name: str) -> bool: """ @@ -94,8 +94,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: Optional[List[LiteLLM_ManagedVectorStore]] = None): + self.vector_stores: List[LiteLLM_ManagedVectorStore] = list(vector_stores or ()) self.vector_store_ids_to_vector_store_map: Dict[str, LiteLLM_ManagedVectorStore] = {} def _extract_tool_params(self, tool: Dict) -> VectorStoreToolParams: diff --git a/tests/test_litellm/vector_stores/test_vector_store_registry.py b/tests/test_litellm/vector_stores/test_vector_store_registry.py index 9f4c5a905b3..ea3b9e6d42d 100644 --- a/tests/test_litellm/vector_stores/test_vector_store_registry.py +++ b/tests/test_litellm/vector_stores/test_vector_store_registry.py @@ -16,9 +16,16 @@ from datetime import datetime, timezone from unittest.mock import MagicMock, patch 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 VectorStoreRegistry +from litellm.vector_stores.vector_store_registry import ( + VectorStoreIndexRegistry, + VectorStoreRegistry, +) @pytest.fixture(autouse=True) @@ -187,3 +194,88 @@ def test_search_uses_registry_credentials(): assert getattr(called_params, "aws_region_name") == "us-east-1" finally: litellm.vector_store_registry = original_registry + + +def _managed_vector_store(vector_store_id: str) -> LiteLLM_ManagedVectorStore: + return LiteLLM_ManagedVectorStore( + vector_store_id=vector_store_id, + custom_llm_provider="openai", + vector_store_name=f"store_{vector_store_id}", + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ) + + +def _managed_vector_store_index(index_name: str) -> LiteLLM_ManagedVectorStoreIndex: + return LiteLLM_ManagedVectorStoreIndex( + id=f"id_{index_name}", + index_name=index_name, + litellm_params=IndexCreateLiteLLMParams( + vector_store_index=index_name, + vector_store_name=f"store_{index_name}", + ), + ) + + +def test_registries_do_not_share_default_storage(): + """ + A registry built without arguments must own its own storage. + + Regression test: both registries used a mutable `= []` default, so the list was created once at + def-time and shared by every no-arg instance. Mutating one registry leaked into the next one, + which on the proxy meant a fresh registry inherited another's vector stores (and their + credential references). + """ + first_registry = VectorStoreRegistry() + first_registry.add_vector_store_to_registry(_managed_vector_store("vs_leak")) + assert first_registry.vector_stores[0]["vector_store_id"] == "vs_leak" + + assert VectorStoreRegistry().vector_stores == [] + + first_index_registry = VectorStoreIndexRegistry() + first_index_registry.upsert_vector_store_index(_managed_vector_store_index("idx_leak")) + assert first_index_registry.vector_store_indexes[0].index_name == "idx_leak" + + assert VectorStoreIndexRegistry().vector_store_indexes == [] + + +def test_registries_do_not_alias_caller_list(): + """ + The constructor must copy the caller's list rather than alias it. + + Otherwise registry mutations write back into a list the caller still owns; the proxy passes + lists freshly read from the DB, and `load_vector_stores_from_config` appends into whatever it + was handed. + """ + caller_vector_stores = [_managed_vector_store("vs_1")] + registry = VectorStoreRegistry(caller_vector_stores) + registry.add_vector_store_to_registry(_managed_vector_store("vs_2")) + + assert len(registry.vector_stores) == 2 + assert len(caller_vector_stores) == 1 + + caller_indexes = [_managed_vector_store_index("idx_1")] + index_registry = VectorStoreIndexRegistry(caller_indexes) + index_registry.upsert_vector_store_index(_managed_vector_store_index("idx_2")) + + assert len(index_registry.vector_store_indexes) == 2 + assert len(caller_indexes) == 1 + + +def test_delete_vector_store_index_removes_by_name(): + """ + Regression test: delete_vector_store_index compared each stored index object against the + index-name string, which is never equal, so the delete was an unconditional no-op. + """ + registry = VectorStoreIndexRegistry( + [_managed_vector_store_index("idx_a"), _managed_vector_store_index("idx_b")] + ) + + registry.delete_vector_store_index("idx_a") + + assert [index.index_name for index in registry.vector_store_indexes] == ["idx_b"] + assert registry.get_vector_store_index_by_name("idx_a") is None + assert registry.is_vector_store_index("idx_b") is True + + registry.delete_vector_store_index("idx_does_not_exist") + assert [index.index_name for index in registry.vector_store_indexes] == ["idx_b"] From 22417073dec42e133f7eaaad91af0421e25c987f Mon Sep 17 00:00:00 2001 From: Sarveswaran MG Date: Wed, 22 Jul 2026 18:33:17 +0530 Subject: [PATCH 2/5] fix(vector_stores): stop _get_vector_store_ids_from_tool_calls sharing and mutating its default list Same defect class as the registry constructors, one method further down the file. _get_vector_store_ids_from_tool_calls took `vector_store_ids: List[str] = []` and extended it in place, so the def-time list would accumulate ids across every call that omitted the argument and then return them as if they came from the request. The single caller passes the argument explicitly, so nothing leaks today; this is a latent landmine rather than an active bug. Fixing it alongside the constructors keeps the file free of the pattern instead of leaving one instance for the next caller to trip over. Rebuilt as a single comprehension so it neither seeds a mutable default nor writes back into a caller-owned list. Order is unchanged: seeded ids first, then ids discovered in tool calls. --- .../vector_stores/vector_store_registry.py | 11 +++++----- .../test_vector_store_registry.py | 22 +++++++++++++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/litellm/vector_stores/vector_store_registry.py b/litellm/vector_stores/vector_store_registry.py index 2bac8921d5f..520edc06548 100644 --- a/litellm/vector_stores/vector_store_registry.py +++ b/litellm/vector_stores/vector_store_registry.py @@ -376,16 +376,15 @@ class VectorStoreRegistry: return vector_stores_to_run def _get_vector_store_ids_from_tool_calls( - self, tools: Optional[List[Dict]] = None, vector_store_ids: List[str] = [] + self, tools: Optional[List[Dict]] = None, vector_store_ids: Optional[List[str]] = None ) -> List[str]: """ Returns the vector store ids from the tool calls """ - if tools: - for tool in tools: - if "vector_store_ids" in tool: - vector_store_ids.extend(tool["vector_store_ids"]) - return vector_store_ids + return [ + *(vector_store_ids or ()), + *(vs_id for tool in (tools or ()) if "vector_store_ids" in tool for vs_id in tool["vector_store_ids"]), + ] def load_vector_stores_from_config(self, vector_stores_config: List[Dict]): """ diff --git a/tests/test_litellm/vector_stores/test_vector_store_registry.py b/tests/test_litellm/vector_stores/test_vector_store_registry.py index ea3b9e6d42d..d9337d32c13 100644 --- a/tests/test_litellm/vector_stores/test_vector_store_registry.py +++ b/tests/test_litellm/vector_stores/test_vector_store_registry.py @@ -279,3 +279,25 @@ def test_delete_vector_store_index_removes_by_name(): registry.delete_vector_store_index("idx_does_not_exist") assert [index.index_name for index in registry.vector_store_indexes] == ["idx_b"] + + +def test_get_vector_store_ids_from_tool_calls_does_not_share_or_mutate(): + """ + Regression test: _get_vector_store_ids_from_tool_calls took `vector_store_ids: List[str] = []` + and extended it in place, so the def-time default accumulated ids across every call that + omitted the argument, and a caller-supplied list was mutated behind the caller's back. + """ + registry = VectorStoreRegistry() + tools = [{"vector_store_ids": ["vs_a"]}] + + first = registry._get_vector_store_ids_from_tool_calls(tools=tools) + second = registry._get_vector_store_ids_from_tool_calls(tools=tools) + + assert first == ["vs_a"] + assert second == ["vs_a"] + + caller_ids = ["vs_seed"] + result = registry._get_vector_store_ids_from_tool_calls(tools=tools, vector_store_ids=caller_ids) + + assert result == ["vs_seed", "vs_a"] + assert caller_ids == ["vs_seed"] From 00a478fb4d5b42977354fc1ea598e3b15fb4c611 Mon Sep 17 00:00:00 2001 From: Sarveswaran MG Date: Wed, 22 Jul 2026 20:51:30 +0530 Subject: [PATCH 3/5] fix(vector_stores): keep param name for backward compat, correct index deletion --- litellm/vector_stores/vector_store_registry.py | 6 ++++-- .../vector_stores/test_vector_store_registry.py | 4 +--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/litellm/vector_stores/vector_store_registry.py b/litellm/vector_stores/vector_store_registry.py index 520edc06548..3a435af59ad 100644 --- a/litellm/vector_stores/vector_store_registry.py +++ b/litellm/vector_stores/vector_store_registry.py @@ -55,11 +55,13 @@ class VectorStoreIndexRegistry: return self.vector_store_indexes.append(vector_store_index) - def delete_vector_store_index(self, index_name: str): + def delete_vector_store_index(self, vector_store_index: str): """ Deletes a vector store index from the registry """ - self.vector_store_indexes = [index for index in self.vector_store_indexes if index.index_name != index_name] + self.vector_store_indexes = [ + index for index in self.vector_store_indexes if index.index_name != vector_store_index + ] def is_vector_store_index(self, vector_store_index_name: str) -> bool: """ diff --git a/tests/test_litellm/vector_stores/test_vector_store_registry.py b/tests/test_litellm/vector_stores/test_vector_store_registry.py index d9337d32c13..70a38de9317 100644 --- a/tests/test_litellm/vector_stores/test_vector_store_registry.py +++ b/tests/test_litellm/vector_stores/test_vector_store_registry.py @@ -267,9 +267,7 @@ def test_delete_vector_store_index_removes_by_name(): Regression test: delete_vector_store_index compared each stored index object against the index-name string, which is never equal, so the delete was an unconditional no-op. """ - registry = VectorStoreIndexRegistry( - [_managed_vector_store_index("idx_a"), _managed_vector_store_index("idx_b")] - ) + registry = VectorStoreIndexRegistry([_managed_vector_store_index("idx_a"), _managed_vector_store_index("idx_b")]) registry.delete_vector_store_index("idx_a") From 96be6a52628b59cb713093e867311f464d0f4a35 Mon Sep 17 00:00:00 2001 From: Sarveswaran MG Date: Wed, 22 Jul 2026 22:34:51 +0530 Subject: [PATCH 4/5] fix(vector_stores): use PEP 604 union syntax to satisfy UP045 strict gate --- litellm/vector_stores/vector_store_registry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/litellm/vector_stores/vector_store_registry.py b/litellm/vector_stores/vector_store_registry.py index 3a435af59ad..290b5b04aae 100644 --- a/litellm/vector_stores/vector_store_registry.py +++ b/litellm/vector_stores/vector_store_registry.py @@ -25,7 +25,7 @@ else: class VectorStoreIndexRegistry: - def __init__(self, vector_store_indexes: Optional[List[LiteLLM_ManagedVectorStoreIndex]] = None): + def __init__(self, vector_store_indexes: List[LiteLLM_ManagedVectorStoreIndex] | None = None): self.vector_store_indexes: List[LiteLLM_ManagedVectorStoreIndex] = list(vector_store_indexes or ()) def get_vector_store_indexes(self) -> List[LiteLLM_ManagedVectorStoreIndex]: @@ -96,7 +96,7 @@ class VectorStoreIndexRegistry: class VectorStoreRegistry: - def __init__(self, vector_stores: Optional[List[LiteLLM_ManagedVectorStore]] = None): + def __init__(self, vector_stores: List[LiteLLM_ManagedVectorStore] | None = None): self.vector_stores: List[LiteLLM_ManagedVectorStore] = list(vector_stores or ()) self.vector_store_ids_to_vector_store_map: Dict[str, LiteLLM_ManagedVectorStore] = {} @@ -378,7 +378,7 @@ class VectorStoreRegistry: return vector_stores_to_run def _get_vector_store_ids_from_tool_calls( - self, tools: Optional[List[Dict]] = None, vector_store_ids: Optional[List[str]] = None + self, tools: List[Dict] | None = None, vector_store_ids: List[str] | None = None ) -> List[str]: """ Returns the vector store ids from the tool calls From bd2236831bb1e23f45bb7680ae558e18b088375f Mon Sep 17 00:00:00 2001 From: Sarveswaran MG Date: Thu, 6 Aug 2026 22:37:22 +0530 Subject: [PATCH 5/5] fix(vector_stores): restore builtin generics so the registry module imports The merge with litellm_internal_staging kept List/Dict annotations from this branch while upstream had already switched the file to builtin generics and dropped those names from its typing import, so importing litellm.vector_stores.vector_store_registry raised NameError and every CI job that imports litellm failed. --- litellm/vector_stores/vector_store_registry.py | 14 +++++++------- .../vector_stores/test_vector_store_registry.py | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/litellm/vector_stores/vector_store_registry.py b/litellm/vector_stores/vector_store_registry.py index 71e7829ba16..61cf5923b02 100644 --- a/litellm/vector_stores/vector_store_registry.py +++ b/litellm/vector_stores/vector_store_registry.py @@ -25,8 +25,8 @@ else: class VectorStoreIndexRegistry: - def __init__(self, vector_store_indexes: List[LiteLLM_ManagedVectorStoreIndex] | None = None): - self.vector_store_indexes: List[LiteLLM_ManagedVectorStoreIndex] = list(vector_store_indexes or ()) + def __init__(self, vector_store_indexes: list[LiteLLM_ManagedVectorStoreIndex] | None = None): + self.vector_store_indexes: list[LiteLLM_ManagedVectorStoreIndex] = list(vector_store_indexes or ()) def get_vector_store_indexes(self) -> list[LiteLLM_ManagedVectorStoreIndex]: """ @@ -96,9 +96,9 @@ class VectorStoreIndexRegistry: class VectorStoreRegistry: - def __init__(self, vector_stores: List[LiteLLM_ManagedVectorStore] | None = None): - self.vector_stores: List[LiteLLM_ManagedVectorStore] = list(vector_stores or ()) - self.vector_store_ids_to_vector_store_map: Dict[str, LiteLLM_ManagedVectorStore] = {} + def __init__(self, vector_stores: list[LiteLLM_ManagedVectorStore] | None = None): + self.vector_stores: list[LiteLLM_ManagedVectorStore] = list(vector_stores or ()) + self.vector_store_ids_to_vector_store_map: dict[str, LiteLLM_ManagedVectorStore] = {} def _extract_tool_params(self, tool: dict) -> VectorStoreToolParams: """ @@ -377,8 +377,8 @@ class VectorStoreRegistry: return vector_stores_to_run def _get_vector_store_ids_from_tool_calls( - self, tools: List[Dict] | None = None, vector_store_ids: List[str] | None = None - ) -> List[str]: + self, tools: list[dict] | None = None, vector_store_ids: list[str] | None = None + ) -> list[str]: """ Returns the vector store ids from the tool calls """ diff --git a/tests/test_litellm/vector_stores/test_vector_store_registry.py b/tests/test_litellm/vector_stores/test_vector_store_registry.py index 70a38de9317..049f2dca340 100644 --- a/tests/test_litellm/vector_stores/test_vector_store_registry.py +++ b/tests/test_litellm/vector_stores/test_vector_store_registry.py @@ -281,7 +281,7 @@ def test_delete_vector_store_index_removes_by_name(): def test_get_vector_store_ids_from_tool_calls_does_not_share_or_mutate(): """ - Regression test: _get_vector_store_ids_from_tool_calls took `vector_store_ids: List[str] = []` + Regression test: _get_vector_store_ids_from_tool_calls took `vector_store_ids: list[str] = []` and extended it in place, so the def-time default accumulated ids across every call that omitted the argument, and a caller-supplied list was mutated behind the caller's back. """