mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-12 08:23:23 +00:00
Merge pull request #26930 from stuxf/codex/vector-store-tenant-guard
chore(vector stores): tighten managed store access
This commit is contained in:
@@ -13,7 +13,16 @@ from pathlib import Path
|
||||
from typing import Dict, Optional, Set
|
||||
|
||||
SNAPSHOT_FILE = Path(__file__).parent / "_lazy_openapi_snapshot.json"
|
||||
HTTP_METHODS = {"delete", "get", "head", "options", "patch", "post", "put"}
|
||||
HTTP_METHOD_SUFFIXES = {
|
||||
"delete",
|
||||
"get",
|
||||
"head",
|
||||
"options",
|
||||
"patch",
|
||||
"post",
|
||||
"put",
|
||||
"trace",
|
||||
}
|
||||
|
||||
|
||||
def load_snapshot() -> Optional[Dict[str, Dict]]:
|
||||
@@ -90,9 +99,17 @@ def generate_snapshot() -> Dict[str, Dict]:
|
||||
paths = full.get("paths", {})
|
||||
_normalize_operation_ids(paths)
|
||||
# Group all of a feature's routes under one tag.
|
||||
for path_ops in paths.values():
|
||||
for op in path_ops.values():
|
||||
for path_ops in full.get("paths", {}).values():
|
||||
for method, op in path_ops.items():
|
||||
if isinstance(op, dict):
|
||||
operation_id = op.get("operationId")
|
||||
if isinstance(operation_id, str):
|
||||
for suffix in HTTP_METHOD_SUFFIXES:
|
||||
if operation_id.endswith(f"_{suffix}"):
|
||||
op["operationId"] = (
|
||||
operation_id[: -len(suffix)] + method
|
||||
)
|
||||
break
|
||||
op["tags"] = [feat.name]
|
||||
full = ensure_unique_openapi_operation_ids(full, used_operation_ids)
|
||||
fragments[feat.name] = {
|
||||
|
||||
@@ -97,6 +97,55 @@ def _serialize_http_exception_detail(
|
||||
return str(detail), None
|
||||
|
||||
|
||||
def _collect_response_file_search_vector_store_ids(data: Dict[str, Any]) -> set[str]:
|
||||
vector_store_ids: set[str] = set()
|
||||
tools = data.get("tools")
|
||||
if not isinstance(tools, list):
|
||||
return vector_store_ids
|
||||
|
||||
for tool in tools:
|
||||
if not isinstance(tool, dict) or tool.get("type") != "file_search":
|
||||
continue
|
||||
ids = tool.get("vector_store_ids") or []
|
||||
if not isinstance(ids, list):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": "file_search.vector_store_ids must be a list of strings"
|
||||
},
|
||||
)
|
||||
for vector_store_id in ids:
|
||||
if not isinstance(vector_store_id, str) or not vector_store_id:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": "file_search.vector_store_ids must be a list of strings"
|
||||
},
|
||||
)
|
||||
vector_store_ids.add(vector_store_id)
|
||||
|
||||
return vector_store_ids
|
||||
|
||||
|
||||
async def _authorize_response_file_search_vector_stores(
|
||||
data: Dict[str, Any],
|
||||
user_api_key_dict: UserAPIKeyAuth,
|
||||
) -> None:
|
||||
vector_store_ids = _collect_response_file_search_vector_store_ids(data)
|
||||
if not vector_store_ids:
|
||||
return
|
||||
|
||||
from litellm.proxy.vector_store_endpoints.utils import (
|
||||
assert_user_can_access_vector_store_id,
|
||||
)
|
||||
|
||||
for vector_store_id in sorted(vector_store_ids):
|
||||
await assert_user_can_access_vector_store_id(
|
||||
vector_store_id=vector_store_id,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
)
|
||||
|
||||
|
||||
async def _parse_event_data_for_error(event_line: Union[str, bytes]) -> Optional[int]:
|
||||
"""Parses an event line and returns an error code if present, else None."""
|
||||
event_line = (
|
||||
@@ -791,6 +840,11 @@ class ProxyBaseLLMRequestProcessing:
|
||||
version=version,
|
||||
proxy_config=proxy_config,
|
||||
)
|
||||
if route_type in {"aresponses", "_aresponses_websocket"}:
|
||||
await _authorize_response_file_search_vector_stores(
|
||||
data=self.data,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
)
|
||||
|
||||
# Calculate request queue time after add_litellm_data_to_request
|
||||
# which sets arrival_time in proxy_server_request
|
||||
|
||||
@@ -47,6 +47,8 @@ from litellm.types.passthrough_endpoints.pass_through_endpoints import (
|
||||
)
|
||||
from litellm.proxy.utils import is_known_model
|
||||
from litellm.proxy.vector_store_endpoints.utils import (
|
||||
assert_user_can_access_vector_store,
|
||||
get_litellm_managed_vector_store,
|
||||
is_allowed_to_call_vector_store_endpoint,
|
||||
)
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
@@ -533,6 +535,10 @@ async def milvus_proxy_route(
|
||||
)
|
||||
if vector_store is None:
|
||||
raise Exception(f"Vector store not found for {vector_store_name}")
|
||||
await assert_user_can_access_vector_store(
|
||||
vector_store=vector_store,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
)
|
||||
litellm_params = vector_store.get("litellm_params") or {}
|
||||
auth_credentials = provider_config.get_auth_credentials(
|
||||
litellm_params=litellm_params
|
||||
@@ -1438,6 +1444,10 @@ async def azure_proxy_route(
|
||||
)
|
||||
if vector_store is None:
|
||||
raise Exception(f"Vector store not found for {vector_store_name}")
|
||||
await assert_user_can_access_vector_store(
|
||||
vector_store=vector_store,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
)
|
||||
litellm_params = vector_store.get("litellm_params") or {}
|
||||
auth_credentials = provider_config.get_auth_credentials(
|
||||
litellm_params=litellm_params
|
||||
@@ -1777,6 +1787,11 @@ async def _base_vertex_proxy_route(
|
||||
request=request,
|
||||
api_key=api_key_to_use,
|
||||
)
|
||||
if router_credentials is not None:
|
||||
await assert_user_can_access_vector_store(
|
||||
vector_store=router_credentials,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
)
|
||||
|
||||
vertex_project: Optional[str] = get_vertex_project_id_from_url(endpoint)
|
||||
vertex_location: Optional[str] = get_vertex_location_from_url(endpoint)
|
||||
@@ -1913,11 +1928,11 @@ async def vertex_discovery_proxy_route(
|
||||
"Extracted vector store ID from endpoint: %s", vector_store_id
|
||||
)
|
||||
|
||||
# Retrieve vector store credentials from the registry
|
||||
vector_store_credentials = (
|
||||
passthrough_endpoint_router.get_vector_store_credentials(
|
||||
vector_store_id=vector_store_id
|
||||
)
|
||||
# Retrieve LiteLLM-managed vector store credentials if the datastore id
|
||||
# is registered with LiteLLM. Unknown datastore ids keep the existing
|
||||
# direct Vertex pass-through behavior.
|
||||
vector_store_credentials = await get_litellm_managed_vector_store(
|
||||
vector_store_id=vector_store_id
|
||||
)
|
||||
|
||||
if vector_store_credentials:
|
||||
@@ -1925,7 +1940,7 @@ async def vertex_discovery_proxy_route(
|
||||
"Found vector store credentials for ID: %s", vector_store_id
|
||||
)
|
||||
else:
|
||||
verbose_proxy_logger.warning(
|
||||
verbose_proxy_logger.debug(
|
||||
"Vector store ID %s found in endpoint but no credentials found in registry",
|
||||
vector_store_id,
|
||||
)
|
||||
|
||||
@@ -15,6 +15,7 @@ from fastapi.responses import ORJSONResponse
|
||||
|
||||
import litellm
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH
|
||||
from litellm.proxy._types import *
|
||||
from litellm.proxy.auth.user_api_key_auth import UserAPIKeyAuth, user_api_key_auth
|
||||
from litellm.proxy.common_utils.http_parsing_utils import (
|
||||
@@ -22,10 +23,88 @@ from litellm.proxy.common_utils.http_parsing_utils import (
|
||||
_safe_get_request_headers,
|
||||
get_form_data,
|
||||
)
|
||||
from litellm.proxy.vector_store_endpoints.utils import (
|
||||
assert_user_can_access_vector_store_id,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _raise_vector_store_scan_depth_exceeded() -> None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": f"Max depth of {DEFAULT_MAX_RECURSE_DEPTH} exceeded while scanning vector_store_id values"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _append_payload_to_scan_stack(
|
||||
payload_stack: list[tuple[Any, int]],
|
||||
value: Any,
|
||||
next_depth: int,
|
||||
) -> None:
|
||||
if isinstance(value, dict):
|
||||
if next_depth > DEFAULT_MAX_RECURSE_DEPTH:
|
||||
_raise_vector_store_scan_depth_exceeded()
|
||||
payload_stack.append((value, next_depth))
|
||||
elif isinstance(value, list):
|
||||
if next_depth > DEFAULT_MAX_RECURSE_DEPTH:
|
||||
if any(isinstance(item, (dict, list)) for item in value):
|
||||
_raise_vector_store_scan_depth_exceeded()
|
||||
return
|
||||
payload_stack.append((value, next_depth))
|
||||
|
||||
|
||||
def _collect_vector_store_ids_from_payload(payload: Any) -> set[str]:
|
||||
vector_store_ids: set[str] = set()
|
||||
payload_stack = [(payload, 0)]
|
||||
|
||||
while payload_stack:
|
||||
current_payload, depth = payload_stack.pop()
|
||||
if depth > DEFAULT_MAX_RECURSE_DEPTH:
|
||||
_raise_vector_store_scan_depth_exceeded()
|
||||
|
||||
if isinstance(current_payload, dict):
|
||||
for key, value in current_payload.items():
|
||||
if key == "vector_store_id":
|
||||
if not isinstance(value, str) or not value:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": "vector_store_id must be a non-empty string"
|
||||
},
|
||||
)
|
||||
vector_store_ids.add(value)
|
||||
continue
|
||||
if isinstance(value, (dict, list)):
|
||||
_append_payload_to_scan_stack(
|
||||
payload_stack=payload_stack,
|
||||
value=value,
|
||||
next_depth=depth + 1,
|
||||
)
|
||||
elif isinstance(current_payload, list):
|
||||
for item in current_payload:
|
||||
_append_payload_to_scan_stack(
|
||||
payload_stack=payload_stack,
|
||||
value=item,
|
||||
next_depth=depth + 1,
|
||||
)
|
||||
|
||||
return vector_store_ids
|
||||
|
||||
|
||||
async def _authorize_nested_vector_store_ids(
|
||||
payload: Any,
|
||||
user_api_key_dict: UserAPIKeyAuth,
|
||||
) -> None:
|
||||
for vector_store_id in sorted(_collect_vector_store_ids_from_payload(payload)):
|
||||
await assert_user_can_access_vector_store_id(
|
||||
vector_store_id=vector_store_id,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
)
|
||||
|
||||
|
||||
def _build_file_metadata_entry(
|
||||
response: Any,
|
||||
file_data: Optional[Tuple[str, bytes, str]] = None,
|
||||
@@ -385,6 +464,11 @@ async def rag_ingest(
|
||||
},
|
||||
)
|
||||
|
||||
await _authorize_nested_vector_store_ids(
|
||||
payload=ingest_options,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
)
|
||||
|
||||
# Add litellm data
|
||||
request_data: Dict[str, Any] = {}
|
||||
request_data = await add_litellm_data_to_request(
|
||||
@@ -537,11 +621,20 @@ async def rag_query(
|
||||
status_code=400,
|
||||
detail={"error": "retrieval_config is required"},
|
||||
)
|
||||
if not isinstance(retrieval_config, dict):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={"error": "retrieval_config must be an object"},
|
||||
)
|
||||
if "vector_store_id" not in retrieval_config:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={"error": "retrieval_config must contain 'vector_store_id'"},
|
||||
)
|
||||
await _authorize_nested_vector_store_ids(
|
||||
payload=retrieval_config,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
)
|
||||
|
||||
# Add litellm data
|
||||
request_data: Dict[str, Any] = {}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
||||
|
||||
import litellm
|
||||
from litellm.integrations.vector_store_integrations.vector_store_pre_call_hook import (
|
||||
LiteLLM_ManagedVectorStore,
|
||||
)
|
||||
@@ -10,7 +8,10 @@ from litellm.proxy._types import CommonProxyErrors, UserAPIKeyAuth
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing
|
||||
from litellm.proxy.utils import jsonify_object
|
||||
from litellm.proxy.vector_store_endpoints.utils import can_user_access_vector_store
|
||||
from litellm.proxy.vector_store_endpoints.utils import (
|
||||
assert_user_can_access_vector_store,
|
||||
get_litellm_managed_vector_store,
|
||||
)
|
||||
from litellm.types.vector_stores import IndexCreateRequest
|
||||
|
||||
router = APIRouter()
|
||||
@@ -19,24 +20,6 @@ router = APIRouter()
|
||||
########################################################
|
||||
|
||||
|
||||
async def _check_vector_store_access(
|
||||
vector_store: LiteLLM_ManagedVectorStore,
|
||||
user_api_key_dict: UserAPIKeyAuth,
|
||||
) -> bool:
|
||||
"""
|
||||
Check if the user has access to the vector store.
|
||||
|
||||
Delegates to :func:`can_user_access_vector_store`, which honors:
|
||||
- PROXY_ADMIN bypass
|
||||
- legacy vector stores with no team_id
|
||||
- key-level and team-level ``object_permission.vector_stores`` allowlists
|
||||
- team_id match between key and store
|
||||
"""
|
||||
return await can_user_access_vector_store(
|
||||
vector_store=vector_store, user_api_key_dict=user_api_key_dict
|
||||
)
|
||||
|
||||
|
||||
async def _update_request_data_with_litellm_managed_vector_store_registry(
|
||||
data: Dict,
|
||||
vector_store_id: str,
|
||||
@@ -53,35 +36,27 @@ async def _update_request_data_with_litellm_managed_vector_store_registry(
|
||||
Raises:
|
||||
HTTPException: If user doesn't have access to the vector store
|
||||
"""
|
||||
if litellm.vector_store_registry is not None:
|
||||
vector_store_to_run: Optional[LiteLLM_ManagedVectorStore] = (
|
||||
litellm.vector_store_registry.get_litellm_managed_vector_store_from_registry(
|
||||
vector_store_id=vector_store_id
|
||||
vector_store_to_run: Optional[LiteLLM_ManagedVectorStore] = (
|
||||
await get_litellm_managed_vector_store(vector_store_id=vector_store_id)
|
||||
)
|
||||
if vector_store_to_run is not None:
|
||||
if user_api_key_dict is not None:
|
||||
await assert_user_can_access_vector_store(
|
||||
vector_store=vector_store_to_run,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
)
|
||||
)
|
||||
if vector_store_to_run is not None:
|
||||
if user_api_key_dict is not None:
|
||||
if not await _check_vector_store_access(
|
||||
vector_store_to_run, user_api_key_dict
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Access denied: You do not have permission to access this vector store",
|
||||
)
|
||||
|
||||
if "custom_llm_provider" in vector_store_to_run:
|
||||
data["custom_llm_provider"] = vector_store_to_run.get(
|
||||
"custom_llm_provider"
|
||||
)
|
||||
if "custom_llm_provider" in vector_store_to_run:
|
||||
data["custom_llm_provider"] = vector_store_to_run.get("custom_llm_provider")
|
||||
|
||||
if "litellm_credential_name" in vector_store_to_run:
|
||||
data["litellm_credential_name"] = vector_store_to_run.get(
|
||||
"litellm_credential_name"
|
||||
)
|
||||
if "litellm_credential_name" in vector_store_to_run:
|
||||
data["litellm_credential_name"] = vector_store_to_run.get(
|
||||
"litellm_credential_name"
|
||||
)
|
||||
|
||||
if "litellm_params" in vector_store_to_run:
|
||||
litellm_params = vector_store_to_run.get("litellm_params", {}) or {}
|
||||
data.update(litellm_params)
|
||||
if "litellm_params" in vector_store_to_run:
|
||||
litellm_params = vector_store_to_run.get("litellm_params", {}) or {}
|
||||
data.update(litellm_params)
|
||||
return data
|
||||
|
||||
|
||||
@@ -121,8 +96,7 @@ async def vector_store_search(
|
||||
)
|
||||
|
||||
data = await _read_request_body(request=request)
|
||||
if "vector_store_id" not in data:
|
||||
data["vector_store_id"] = vector_store_id
|
||||
data["vector_store_id"] = vector_store_id
|
||||
|
||||
# Check for legacy vector store registry (non-managed vector stores)
|
||||
data = await _update_request_data_with_litellm_managed_vector_store_registry(
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import json
|
||||
from typing import Any, Dict, Literal, Optional
|
||||
|
||||
from fastapi import HTTPException, Request
|
||||
|
||||
import litellm
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.proxy._types import (
|
||||
LiteLLM_ObjectPermissionTable,
|
||||
@@ -13,6 +15,21 @@ from litellm.types.vector_stores import LiteLLM_ManagedVectorStore
|
||||
from litellm.utils import ProviderConfigManager
|
||||
|
||||
|
||||
def _normalize_litellm_params(
|
||||
vector_store: LiteLLM_ManagedVectorStore,
|
||||
) -> LiteLLM_ManagedVectorStore:
|
||||
litellm_params = vector_store.get("litellm_params")
|
||||
if isinstance(litellm_params, str):
|
||||
normalized = LiteLLM_ManagedVectorStore(**dict(vector_store))
|
||||
try:
|
||||
parsed = json.loads(litellm_params)
|
||||
normalized["litellm_params"] = parsed if isinstance(parsed, dict) else {}
|
||||
except (TypeError, ValueError):
|
||||
normalized["litellm_params"] = {}
|
||||
return normalized
|
||||
return vector_store
|
||||
|
||||
|
||||
def _is_proxy_admin(user_api_key_dict: UserAPIKeyAuth) -> bool:
|
||||
return (
|
||||
user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN
|
||||
@@ -120,6 +137,104 @@ async def can_user_access_vector_store(
|
||||
return False
|
||||
|
||||
|
||||
async def get_litellm_managed_vector_store(
|
||||
vector_store_id: str,
|
||||
) -> Optional[LiteLLM_ManagedVectorStore]:
|
||||
"""
|
||||
Resolve a LiteLLM-managed vector store from the registry or shared cache.
|
||||
|
||||
Provider-native vector store IDs will not be present in either location and
|
||||
return None, preserving direct provider behavior while still protecting
|
||||
LiteLLM-managed multi-tenant stores.
|
||||
"""
|
||||
if not vector_store_id:
|
||||
return None
|
||||
|
||||
if litellm.vector_store_registry is not None:
|
||||
try:
|
||||
vector_store = litellm.vector_store_registry.get_litellm_managed_vector_store_from_registry(
|
||||
vector_store_id=vector_store_id
|
||||
)
|
||||
if vector_store is not None:
|
||||
return _normalize_litellm_params(vector_store)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.warning(
|
||||
"Failed to resolve vector store id=%s from registry: %s",
|
||||
vector_store_id,
|
||||
e,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Unable to validate vector store access",
|
||||
) from e
|
||||
|
||||
try:
|
||||
from litellm.proxy.auth.auth_checks import (
|
||||
get_managed_vector_store_rows_by_uuids,
|
||||
)
|
||||
from litellm.proxy.proxy_server import (
|
||||
prisma_client,
|
||||
proxy_logging_obj,
|
||||
user_api_key_cache,
|
||||
)
|
||||
|
||||
if prisma_client is None:
|
||||
return None
|
||||
rows = await get_managed_vector_store_rows_by_uuids(
|
||||
uuids=[vector_store_id],
|
||||
prisma_client=prisma_client,
|
||||
user_api_key_cache=user_api_key_cache,
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
)
|
||||
if not rows:
|
||||
return None
|
||||
return _normalize_litellm_params(
|
||||
LiteLLM_ManagedVectorStore(**rows[0].model_dump())
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.warning(
|
||||
"Failed to resolve vector store id=%s from shared cache: %s",
|
||||
vector_store_id,
|
||||
e,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Unable to validate vector store access",
|
||||
) from e
|
||||
|
||||
|
||||
async def assert_user_can_access_vector_store(
|
||||
vector_store: LiteLLM_ManagedVectorStore,
|
||||
user_api_key_dict: UserAPIKeyAuth,
|
||||
detail: str = "Access denied: You do not have permission to access this vector store",
|
||||
) -> None:
|
||||
"""Raise 403 unless the caller can access the resolved vector store."""
|
||||
if not await can_user_access_vector_store(vector_store, user_api_key_dict):
|
||||
raise HTTPException(status_code=403, detail=detail)
|
||||
|
||||
|
||||
async def assert_user_can_access_vector_store_id(
|
||||
vector_store_id: str,
|
||||
user_api_key_dict: UserAPIKeyAuth,
|
||||
detail: str = "Access denied: You do not have permission to access this vector store",
|
||||
) -> Optional[LiteLLM_ManagedVectorStore]:
|
||||
"""
|
||||
Resolve a managed vector store id and enforce ownership if it exists.
|
||||
|
||||
Unknown ids are treated as provider-native ids and are not rejected here.
|
||||
"""
|
||||
vector_store = await get_litellm_managed_vector_store(
|
||||
vector_store_id=vector_store_id
|
||||
)
|
||||
if vector_store is not None:
|
||||
await assert_user_can_access_vector_store(
|
||||
vector_store=vector_store,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
detail=detail,
|
||||
)
|
||||
return vector_store
|
||||
|
||||
|
||||
def _does_endpoint_match(endpoint_path: str, request_path: str) -> bool:
|
||||
if endpoint_path in request_path:
|
||||
return True
|
||||
|
||||
@@ -17,9 +17,11 @@ from litellm.proxy.openai_files_endpoints.common_utils import (
|
||||
prepare_data_with_credentials,
|
||||
)
|
||||
from litellm.proxy.vector_store_endpoints.utils import (
|
||||
assert_user_can_access_vector_store_id,
|
||||
is_allowed_to_call_vector_store_files_endpoint,
|
||||
)
|
||||
from litellm.types.utils import LlmProviders
|
||||
from litellm.types.vector_stores import LiteLLM_ManagedVectorStore
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.router import Router
|
||||
@@ -193,6 +195,8 @@ def _update_request_data_with_litellm_managed_vector_store_registry(
|
||||
data: Dict,
|
||||
vector_store_id: str,
|
||||
llm_router: Optional["Router"] = None,
|
||||
managed_vector_store: Optional[LiteLLM_ManagedVectorStore] = None,
|
||||
should_lookup_registry: bool = True,
|
||||
) -> Dict:
|
||||
"""
|
||||
Update request data with model routing information from managed vector store.
|
||||
@@ -262,23 +266,27 @@ def _update_request_data_with_litellm_managed_vector_store_registry(
|
||||
|
||||
return data
|
||||
|
||||
# Legacy path: Check vector store registry for non-managed vector stores
|
||||
if litellm.vector_store_registry is not None:
|
||||
# Legacy path: Check vector store registry for non-managed vector stores.
|
||||
vector_store_to_run = managed_vector_store
|
||||
if (
|
||||
vector_store_to_run is None
|
||||
and should_lookup_registry
|
||||
and litellm.vector_store_registry is not None
|
||||
):
|
||||
vector_store_to_run = litellm.vector_store_registry.get_litellm_managed_vector_store_from_registry(
|
||||
vector_store_id=vector_store_id
|
||||
)
|
||||
if vector_store_to_run is not None:
|
||||
if "custom_llm_provider" in vector_store_to_run:
|
||||
data["custom_llm_provider"] = vector_store_to_run.get(
|
||||
"custom_llm_provider"
|
||||
)
|
||||
if "litellm_credential_name" in vector_store_to_run:
|
||||
data["litellm_credential_name"] = vector_store_to_run.get(
|
||||
"litellm_credential_name"
|
||||
)
|
||||
if "litellm_params" in vector_store_to_run:
|
||||
litellm_params = vector_store_to_run.get("litellm_params", {}) or {}
|
||||
data.update(litellm_params)
|
||||
|
||||
if vector_store_to_run is not None:
|
||||
if "custom_llm_provider" in vector_store_to_run:
|
||||
data["custom_llm_provider"] = vector_store_to_run.get("custom_llm_provider")
|
||||
if "litellm_credential_name" in vector_store_to_run:
|
||||
data["litellm_credential_name"] = vector_store_to_run.get(
|
||||
"litellm_credential_name"
|
||||
)
|
||||
if "litellm_params" in vector_store_to_run:
|
||||
litellm_params = vector_store_to_run.get("litellm_params", {}) or {}
|
||||
data.update(litellm_params)
|
||||
|
||||
return data
|
||||
|
||||
@@ -363,8 +371,11 @@ async def vector_store_file_create(
|
||||
)
|
||||
|
||||
data = await _read_request_body(request=request)
|
||||
if "vector_store_id" not in data:
|
||||
data["vector_store_id"] = vector_store_id
|
||||
data["vector_store_id"] = vector_store_id
|
||||
managed_vector_store = await assert_user_can_access_vector_store_id(
|
||||
vector_store_id=vector_store_id,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
)
|
||||
|
||||
# Handle managed file IDs if present in request body
|
||||
original_managed_file_id = None
|
||||
@@ -375,7 +386,11 @@ async def vector_store_file_create(
|
||||
|
||||
# Then handle managed vector store IDs
|
||||
data = _update_request_data_with_litellm_managed_vector_store_registry(
|
||||
data=data, vector_store_id=vector_store_id, llm_router=llm_router
|
||||
data=data,
|
||||
vector_store_id=vector_store_id,
|
||||
llm_router=llm_router,
|
||||
managed_vector_store=managed_vector_store,
|
||||
should_lookup_registry=False,
|
||||
)
|
||||
|
||||
provider_enum = await _resolve_provider(data=data, request=request)
|
||||
@@ -459,9 +474,18 @@ async def vector_store_file_list(
|
||||
query_params = dict(request.query_params)
|
||||
data: Dict[str, Optional[str]] = {"vector_store_id": vector_store_id}
|
||||
data.update(query_params)
|
||||
data["vector_store_id"] = vector_store_id
|
||||
managed_vector_store = await assert_user_can_access_vector_store_id(
|
||||
vector_store_id=vector_store_id,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
)
|
||||
|
||||
data = _update_request_data_with_litellm_managed_vector_store_registry(
|
||||
data=data, vector_store_id=vector_store_id, llm_router=llm_router
|
||||
data=data,
|
||||
vector_store_id=vector_store_id,
|
||||
llm_router=llm_router,
|
||||
managed_vector_store=managed_vector_store,
|
||||
should_lookup_registry=False,
|
||||
)
|
||||
|
||||
provider_enum = await _resolve_provider(data=data, request=request)
|
||||
@@ -541,6 +565,10 @@ async def vector_store_file_retrieve(
|
||||
"vector_store_id": vector_store_id,
|
||||
"file_id": file_id,
|
||||
}
|
||||
managed_vector_store = await assert_user_can_access_vector_store_id(
|
||||
vector_store_id=vector_store_id,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
)
|
||||
|
||||
# Handle managed file IDs first
|
||||
data, original_managed_file_id = _update_request_data_with_managed_file_id(
|
||||
@@ -549,7 +577,11 @@ async def vector_store_file_retrieve(
|
||||
|
||||
# Then handle managed vector store IDs
|
||||
data = _update_request_data_with_litellm_managed_vector_store_registry(
|
||||
data=data, vector_store_id=vector_store_id, llm_router=llm_router
|
||||
data=data,
|
||||
vector_store_id=vector_store_id,
|
||||
llm_router=llm_router,
|
||||
managed_vector_store=managed_vector_store,
|
||||
should_lookup_registry=False,
|
||||
)
|
||||
|
||||
provider_enum = await _resolve_provider(data=data, request=request)
|
||||
@@ -635,6 +667,10 @@ async def vector_store_file_content(
|
||||
"vector_store_id": vector_store_id,
|
||||
"file_id": file_id,
|
||||
}
|
||||
managed_vector_store = await assert_user_can_access_vector_store_id(
|
||||
vector_store_id=vector_store_id,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
)
|
||||
|
||||
# Handle managed file IDs first
|
||||
data, original_managed_file_id = _update_request_data_with_managed_file_id(
|
||||
@@ -643,7 +679,11 @@ async def vector_store_file_content(
|
||||
|
||||
# Then handle managed vector store IDs
|
||||
data = _update_request_data_with_litellm_managed_vector_store_registry(
|
||||
data=data, vector_store_id=vector_store_id, llm_router=llm_router
|
||||
data=data,
|
||||
vector_store_id=vector_store_id,
|
||||
llm_router=llm_router,
|
||||
managed_vector_store=managed_vector_store,
|
||||
should_lookup_registry=False,
|
||||
)
|
||||
|
||||
provider_enum = await _resolve_provider(data=data, request=request)
|
||||
@@ -729,6 +769,10 @@ async def vector_store_file_update(
|
||||
data = await _read_request_body(request=request)
|
||||
data["vector_store_id"] = vector_store_id
|
||||
data["file_id"] = file_id
|
||||
managed_vector_store = await assert_user_can_access_vector_store_id(
|
||||
vector_store_id=vector_store_id,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
)
|
||||
|
||||
# Handle managed file IDs first
|
||||
data, original_managed_file_id = _update_request_data_with_managed_file_id(
|
||||
@@ -737,7 +781,11 @@ async def vector_store_file_update(
|
||||
|
||||
# Then handle managed vector store IDs
|
||||
data = _update_request_data_with_litellm_managed_vector_store_registry(
|
||||
data=data, vector_store_id=vector_store_id, llm_router=llm_router
|
||||
data=data,
|
||||
vector_store_id=vector_store_id,
|
||||
llm_router=llm_router,
|
||||
managed_vector_store=managed_vector_store,
|
||||
should_lookup_registry=False,
|
||||
)
|
||||
|
||||
provider_enum = await _resolve_provider(data=data, request=request)
|
||||
@@ -823,6 +871,10 @@ async def vector_store_file_delete(
|
||||
"vector_store_id": vector_store_id,
|
||||
"file_id": file_id,
|
||||
}
|
||||
managed_vector_store = await assert_user_can_access_vector_store_id(
|
||||
vector_store_id=vector_store_id,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
)
|
||||
|
||||
# Handle managed file IDs first
|
||||
data, original_managed_file_id = _update_request_data_with_managed_file_id(
|
||||
@@ -831,7 +883,11 @@ async def vector_store_file_delete(
|
||||
|
||||
# Then handle managed vector store IDs
|
||||
data = _update_request_data_with_litellm_managed_vector_store_registry(
|
||||
data=data, vector_store_id=vector_store_id, llm_router=llm_router
|
||||
data=data,
|
||||
vector_store_id=vector_store_id,
|
||||
llm_router=llm_router,
|
||||
managed_vector_store=managed_vector_store,
|
||||
should_lookup_registry=False,
|
||||
)
|
||||
|
||||
provider_enum = await _resolve_provider(data=data, request=request)
|
||||
|
||||
@@ -377,15 +377,11 @@ def search(
|
||||
_is_async = kwargs.pop("asearch", False) is True
|
||||
|
||||
# pull credentials from registry if available
|
||||
vector_store_id_for_credentials = kwargs.get("vector_store_id", vector_store_id)
|
||||
if (
|
||||
litellm.vector_store_registry is not None
|
||||
and vector_store_id_for_credentials is not None
|
||||
):
|
||||
if litellm.vector_store_registry is not None and vector_store_id is not None:
|
||||
try:
|
||||
registry_credentials = (
|
||||
litellm.vector_store_registry.get_credentials_for_vector_store(
|
||||
vector_store_id_for_credentials
|
||||
vector_store_id
|
||||
)
|
||||
)
|
||||
kwargs.update(registry_credentials)
|
||||
|
||||
@@ -156,8 +156,11 @@ async def test_update_request_data_with_litellm_managed_vector_store_registry():
|
||||
vector_store_id="test_store_id"
|
||||
)
|
||||
|
||||
# Test with no vector store registry
|
||||
with patch.object(litellm, "vector_store_registry", None):
|
||||
# Test with no vector store registry or DB fallback
|
||||
with (
|
||||
patch.object(litellm, "vector_store_registry", None),
|
||||
patch("litellm.proxy.proxy_server.prisma_client", None),
|
||||
):
|
||||
original_data = {"existing_key": "existing_value"}
|
||||
result = await _update_request_data_with_litellm_managed_vector_store_registry(
|
||||
data=original_data, vector_store_id=vector_store_id
|
||||
|
||||
@@ -0,0 +1,540 @@
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException, Request, Response
|
||||
|
||||
import litellm
|
||||
from litellm.proxy._types import LiteLLM_ManagedVectorStoresTable, UserAPIKeyAuth
|
||||
|
||||
|
||||
def _mock_request() -> MagicMock:
|
||||
request = MagicMock(spec=Request)
|
||||
request.headers = {}
|
||||
request.method = "POST"
|
||||
request.query_params = {}
|
||||
request.url.path = "/v1/vector_stores/vs_path/search"
|
||||
return request
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_vector_store_search_forces_path_id_over_body_id():
|
||||
from litellm.proxy.vector_store_endpoints.endpoints import vector_store_search
|
||||
|
||||
captured_data = {}
|
||||
|
||||
async def fake_base_process(self, **kwargs):
|
||||
captured_data.update(self.data)
|
||||
return {"ok": True}
|
||||
|
||||
request = _mock_request()
|
||||
with (
|
||||
patch(
|
||||
"litellm.proxy.proxy_server._read_request_body",
|
||||
new=AsyncMock(
|
||||
return_value={
|
||||
"vector_store_id": "vs_body_victim",
|
||||
"query": "test",
|
||||
}
|
||||
),
|
||||
),
|
||||
patch.object(litellm, "vector_store_registry", None),
|
||||
patch("litellm.proxy.proxy_server.prisma_client", None),
|
||||
patch(
|
||||
"litellm.proxy.vector_store_endpoints.endpoints.ProxyBaseLLMRequestProcessing.base_process_llm_request",
|
||||
new=fake_base_process,
|
||||
),
|
||||
):
|
||||
response = await vector_store_search(
|
||||
request=request,
|
||||
vector_store_id="vs_path_allowed",
|
||||
fastapi_response=Response(),
|
||||
user_api_key_dict=UserAPIKeyAuth(team_id="team-a"),
|
||||
)
|
||||
|
||||
assert response == {"ok": True}
|
||||
assert captured_data["vector_store_id"] == "vs_path_allowed"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_vector_store_file_create_forces_path_id_over_body_id():
|
||||
from litellm.proxy.vector_store_files_endpoints.endpoints import (
|
||||
vector_store_file_create,
|
||||
)
|
||||
|
||||
captured_data = {}
|
||||
|
||||
async def fake_base_process(self, **kwargs):
|
||||
captured_data.update(self.data)
|
||||
return {"ok": True}
|
||||
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get_litellm_managed_vector_store_from_registry.return_value = {
|
||||
"vector_store_id": "vs_path_allowed",
|
||||
"custom_llm_provider": "openai",
|
||||
"team_id": "team-a",
|
||||
}
|
||||
|
||||
request = _mock_request()
|
||||
with (
|
||||
patch(
|
||||
"litellm.proxy.proxy_server._read_request_body",
|
||||
new=AsyncMock(
|
||||
return_value={
|
||||
"vector_store_id": "vs_body_victim",
|
||||
"file_id": "file_123",
|
||||
}
|
||||
),
|
||||
),
|
||||
patch.object(litellm, "vector_store_registry", mock_registry),
|
||||
patch(
|
||||
"litellm.proxy.vector_store_files_endpoints.endpoints.ProxyBaseLLMRequestProcessing.base_process_llm_request",
|
||||
new=fake_base_process,
|
||||
),
|
||||
):
|
||||
response = await vector_store_file_create(
|
||||
vector_store_id="vs_path_allowed",
|
||||
request=request,
|
||||
fastapi_response=Response(),
|
||||
user_api_key_dict=UserAPIKeyAuth(team_id="team-a"),
|
||||
)
|
||||
|
||||
assert response == {"ok": True}
|
||||
assert captured_data["vector_store_id"] == "vs_path_allowed"
|
||||
assert captured_data["custom_llm_provider"] == "openai"
|
||||
mock_registry.get_litellm_managed_vector_store_from_registry.assert_called_once_with(
|
||||
vector_store_id="vs_path_allowed"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_vector_store_file_create_denies_other_team_path_store():
|
||||
from litellm.proxy.vector_store_files_endpoints.endpoints import (
|
||||
vector_store_file_create,
|
||||
)
|
||||
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get_litellm_managed_vector_store_from_registry.return_value = {
|
||||
"vector_store_id": "vs_other_team",
|
||||
"custom_llm_provider": "openai",
|
||||
"team_id": "team-b",
|
||||
}
|
||||
|
||||
request = _mock_request()
|
||||
with (
|
||||
patch(
|
||||
"litellm.proxy.proxy_server._read_request_body",
|
||||
new=AsyncMock(return_value={"file_id": "file_123"}),
|
||||
),
|
||||
patch.object(litellm, "vector_store_registry", mock_registry),
|
||||
patch(
|
||||
"litellm.proxy.vector_store_files_endpoints.endpoints.ProxyBaseLLMRequestProcessing.base_process_llm_request",
|
||||
new=AsyncMock(),
|
||||
) as mock_base_process,
|
||||
):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await vector_store_file_create(
|
||||
vector_store_id="vs_other_team",
|
||||
request=request,
|
||||
fastapi_response=Response(),
|
||||
user_api_key_dict=UserAPIKeyAuth(team_id="team-a"),
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 403
|
||||
mock_base_process.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rag_query_denies_nested_other_team_vector_store():
|
||||
from litellm.proxy.rag_endpoints.endpoints import rag_query
|
||||
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get_litellm_managed_vector_store_from_registry.return_value = {
|
||||
"vector_store_id": "vs_other_team",
|
||||
"custom_llm_provider": "openai",
|
||||
"team_id": "team-b",
|
||||
}
|
||||
|
||||
request = _mock_request()
|
||||
with (
|
||||
patch(
|
||||
"litellm.proxy.rag_endpoints.endpoints._read_request_body",
|
||||
new=AsyncMock(
|
||||
return_value={
|
||||
"model": "gpt-4o-mini",
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
"retrieval_config": {"vector_store_id": "vs_other_team"},
|
||||
}
|
||||
),
|
||||
),
|
||||
patch.object(litellm, "vector_store_registry", mock_registry),
|
||||
patch(
|
||||
"litellm.proxy.rag_endpoints.endpoints.litellm.aquery",
|
||||
new=AsyncMock(),
|
||||
) as mock_aquery,
|
||||
):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await rag_query(
|
||||
request=request,
|
||||
fastapi_response=Response(),
|
||||
user_api_key_dict=UserAPIKeyAuth(team_id="team-a"),
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 403
|
||||
mock_aquery.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rag_ingest_denies_nested_other_team_vector_store():
|
||||
from litellm.proxy.rag_endpoints.endpoints import rag_ingest
|
||||
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get_litellm_managed_vector_store_from_registry.return_value = {
|
||||
"vector_store_id": "vs_other_team",
|
||||
"custom_llm_provider": "openai",
|
||||
"team_id": "team-b",
|
||||
}
|
||||
|
||||
request = _mock_request()
|
||||
with (
|
||||
patch(
|
||||
"litellm.proxy.rag_endpoints.endpoints.parse_rag_ingest_request",
|
||||
new=AsyncMock(
|
||||
return_value=(
|
||||
{
|
||||
"vector_store": {
|
||||
"custom_llm_provider": "openai",
|
||||
"vector_store_id": "vs_other_team",
|
||||
}
|
||||
},
|
||||
None,
|
||||
"https://example.com/file.txt",
|
||||
None,
|
||||
)
|
||||
),
|
||||
),
|
||||
patch.object(litellm, "vector_store_registry", mock_registry),
|
||||
patch(
|
||||
"litellm.proxy.rag_endpoints.endpoints.litellm.aingest",
|
||||
new=AsyncMock(),
|
||||
) as mock_aingest,
|
||||
):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await rag_ingest(
|
||||
request=request,
|
||||
fastapi_response=Response(),
|
||||
user_api_key_dict=UserAPIKeyAuth(team_id="team-a"),
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 403
|
||||
mock_aingest.assert_not_called()
|
||||
|
||||
|
||||
def test_rag_payload_scan_rejects_excessive_nesting():
|
||||
from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH
|
||||
from litellm.proxy.rag_endpoints.endpoints import (
|
||||
_collect_vector_store_ids_from_payload,
|
||||
)
|
||||
|
||||
payload = {}
|
||||
current = payload
|
||||
for _ in range(DEFAULT_MAX_RECURSE_DEPTH + 1):
|
||||
current["nested"] = {}
|
||||
current = current["nested"]
|
||||
current["vector_store_id"] = "vs_too_deep"
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
_collect_vector_store_ids_from_payload(payload)
|
||||
|
||||
assert exc_info.value.status_code == 400
|
||||
|
||||
|
||||
def test_rag_payload_scan_accepts_vector_store_id_at_depth_limit():
|
||||
from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH
|
||||
from litellm.proxy.rag_endpoints.endpoints import (
|
||||
_collect_vector_store_ids_from_payload,
|
||||
)
|
||||
|
||||
payload = {}
|
||||
current = payload
|
||||
for _ in range(DEFAULT_MAX_RECURSE_DEPTH):
|
||||
current["nested"] = {}
|
||||
current = current["nested"]
|
||||
current["vector_store_id"] = "vs_at_limit"
|
||||
|
||||
assert _collect_vector_store_ids_from_payload(payload) == {"vs_at_limit"}
|
||||
|
||||
|
||||
def test_rag_payload_scan_ignores_primitive_list_beyond_depth_limit():
|
||||
from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH
|
||||
from litellm.proxy.rag_endpoints.endpoints import (
|
||||
_collect_vector_store_ids_from_payload,
|
||||
)
|
||||
|
||||
payload = {}
|
||||
current = payload
|
||||
for _ in range(DEFAULT_MAX_RECURSE_DEPTH):
|
||||
current["nested"] = {}
|
||||
current = current["nested"]
|
||||
current["labels"] = ["alpha", "beta"]
|
||||
|
||||
assert _collect_vector_store_ids_from_payload(payload) == set()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_responses_file_search_denies_other_team_vector_store():
|
||||
from litellm.proxy.common_request_processing import (
|
||||
_authorize_response_file_search_vector_stores,
|
||||
)
|
||||
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get_litellm_managed_vector_store_from_registry.return_value = {
|
||||
"vector_store_id": "vs_other_team",
|
||||
"custom_llm_provider": "openai",
|
||||
"team_id": "team-b",
|
||||
}
|
||||
|
||||
with patch.object(litellm, "vector_store_registry", mock_registry):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await _authorize_response_file_search_vector_stores(
|
||||
data={
|
||||
"tools": [
|
||||
{
|
||||
"type": "file_search",
|
||||
"vector_store_ids": ["vs_other_team"],
|
||||
}
|
||||
]
|
||||
},
|
||||
user_api_key_dict=UserAPIKeyAuth(team_id="team-a"),
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_vertex_discovery_denies_other_team_vector_store_credentials():
|
||||
from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
|
||||
_base_vertex_proxy_route,
|
||||
)
|
||||
|
||||
request = _mock_request()
|
||||
request.method = "GET"
|
||||
vector_store_credentials = {
|
||||
"vector_store_id": "vs_other_team",
|
||||
"custom_llm_provider": "vertex_ai",
|
||||
"team_id": "team-b",
|
||||
}
|
||||
|
||||
with patch(
|
||||
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.user_api_key_auth",
|
||||
new=AsyncMock(return_value=UserAPIKeyAuth(team_id="team-a")),
|
||||
):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await _base_vertex_proxy_route(
|
||||
endpoint="projects/p/locations/us-central1/dataStores/vs_other_team",
|
||||
request=request,
|
||||
fastapi_response=Response(),
|
||||
get_vertex_pass_through_handler=MagicMock(),
|
||||
router_credentials=vector_store_credentials,
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_managed_vector_store_uses_shared_cache_helper_for_db_fallback():
|
||||
from litellm.proxy.vector_store_endpoints.utils import (
|
||||
get_litellm_managed_vector_store,
|
||||
)
|
||||
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get_litellm_managed_vector_store_from_registry.return_value = None
|
||||
cache_helper = AsyncMock(
|
||||
return_value=[
|
||||
LiteLLM_ManagedVectorStoresTable(
|
||||
vector_store_id="vs_cached",
|
||||
custom_llm_provider="openai",
|
||||
vector_store_name=None,
|
||||
vector_store_description=None,
|
||||
vector_store_metadata=None,
|
||||
created_at=None,
|
||||
updated_at=None,
|
||||
litellm_credential_name=None,
|
||||
litellm_params={"api_base": "https://example.com"},
|
||||
team_id="team-a",
|
||||
user_id=None,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(litellm, "vector_store_registry", mock_registry),
|
||||
patch("litellm.proxy.proxy_server.prisma_client", MagicMock()),
|
||||
patch("litellm.proxy.proxy_server.user_api_key_cache", MagicMock()),
|
||||
patch("litellm.proxy.proxy_server.proxy_logging_obj", MagicMock()),
|
||||
patch(
|
||||
"litellm.proxy.auth.auth_checks.get_managed_vector_store_rows_by_uuids",
|
||||
new=cache_helper,
|
||||
),
|
||||
):
|
||||
vector_store = await get_litellm_managed_vector_store(
|
||||
vector_store_id="vs_cached"
|
||||
)
|
||||
|
||||
assert vector_store is not None
|
||||
assert vector_store["vector_store_id"] == "vs_cached"
|
||||
assert vector_store["team_id"] == "team-a"
|
||||
cache_helper.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_managed_vector_store_fails_closed_on_lookup_error():
|
||||
from litellm.proxy.vector_store_endpoints.utils import (
|
||||
get_litellm_managed_vector_store,
|
||||
)
|
||||
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get_litellm_managed_vector_store_from_registry.side_effect = (
|
||||
RuntimeError("registry unavailable")
|
||||
)
|
||||
|
||||
with patch.object(litellm, "vector_store_registry", mock_registry):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await get_litellm_managed_vector_store(vector_store_id="vs_registry_only")
|
||||
|
||||
assert exc_info.value.status_code == 500
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_vertex_discovery_allows_unregistered_provider_native_datastore_id():
|
||||
from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
|
||||
vertex_discovery_proxy_route,
|
||||
)
|
||||
|
||||
request = _mock_request()
|
||||
request.method = "GET"
|
||||
|
||||
with (
|
||||
patch(
|
||||
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.get_litellm_managed_vector_store",
|
||||
new=AsyncMock(return_value=None),
|
||||
) as mock_lookup,
|
||||
patch(
|
||||
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints._base_vertex_proxy_route",
|
||||
new=AsyncMock(return_value={"ok": True}),
|
||||
) as mock_base_route,
|
||||
):
|
||||
response = await vertex_discovery_proxy_route(
|
||||
endpoint="projects/p/locations/us-central1/dataStores/vs_unknown",
|
||||
request=request,
|
||||
fastapi_response=Response(),
|
||||
)
|
||||
|
||||
assert response == {"ok": True}
|
||||
mock_lookup.assert_awaited_once_with(vector_store_id="vs_unknown")
|
||||
assert mock_base_route.call_args.kwargs["router_credentials"] is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_milvus_passthrough_denies_other_team_vector_store_index():
|
||||
from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
|
||||
milvus_proxy_route,
|
||||
)
|
||||
|
||||
request = _mock_request()
|
||||
request.url.path = "/milvus/v2/vectordb/entities/search"
|
||||
|
||||
index_object = MagicMock()
|
||||
index_object.litellm_params.vector_store_name = "tenant-b-store"
|
||||
index_object.litellm_params.vector_store_index = "tenant_b_collection"
|
||||
|
||||
mock_index_registry = MagicMock()
|
||||
mock_index_registry.is_vector_store_index.return_value = True
|
||||
mock_index_registry.get_vector_store_index_by_name.return_value = index_object
|
||||
|
||||
mock_vector_registry = MagicMock()
|
||||
mock_vector_registry.get_litellm_managed_vector_store_from_registry_by_name.return_value = {
|
||||
"vector_store_id": "vs_other_team",
|
||||
"custom_llm_provider": "milvus",
|
||||
"team_id": "team-b",
|
||||
"litellm_params": {"api_base": "https://milvus.example.com"},
|
||||
}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.ProviderConfigManager.get_provider_vector_stores_config",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
patch(
|
||||
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.get_request_body",
|
||||
new=AsyncMock(return_value={"collectionName": "managed_index"}),
|
||||
),
|
||||
patch(
|
||||
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.is_allowed_to_call_vector_store_endpoint",
|
||||
return_value=True,
|
||||
),
|
||||
patch.object(litellm, "vector_store_index_registry", mock_index_registry),
|
||||
patch.object(litellm, "vector_store_registry", mock_vector_registry),
|
||||
):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await milvus_proxy_route(
|
||||
endpoint="v2/vectordb/entities/search",
|
||||
request=request,
|
||||
fastapi_response=Response(),
|
||||
user_api_key_dict=UserAPIKeyAuth(team_id="team-a"),
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_azure_passthrough_denies_other_team_vector_store_index():
|
||||
from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
|
||||
azure_proxy_route,
|
||||
)
|
||||
|
||||
request = _mock_request()
|
||||
request.url.path = "/azure/indexes/managed_index/docs/search"
|
||||
|
||||
index_object = MagicMock()
|
||||
index_object.litellm_params.vector_store_name = "tenant-b-store"
|
||||
|
||||
mock_index_registry = MagicMock()
|
||||
mock_index_registry.is_vector_store_index.side_effect = (
|
||||
lambda vector_store_index_name: vector_store_index_name == "managed_index"
|
||||
)
|
||||
mock_index_registry.get_vector_store_index_by_name.return_value = index_object
|
||||
|
||||
mock_vector_registry = MagicMock()
|
||||
mock_vector_registry.get_litellm_managed_vector_store_from_registry_by_name.return_value = {
|
||||
"vector_store_id": "vs_other_team",
|
||||
"custom_llm_provider": "azure_ai",
|
||||
"team_id": "team-b",
|
||||
"litellm_params": {"api_base": "https://azure.example.com"},
|
||||
}
|
||||
|
||||
with (
|
||||
patch("litellm.proxy.proxy_server.llm_router", MagicMock()),
|
||||
patch(
|
||||
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.is_passthrough_request_using_router_model",
|
||||
return_value=False,
|
||||
),
|
||||
patch(
|
||||
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.ProviderConfigManager.get_provider_vector_stores_config",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
patch(
|
||||
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.is_allowed_to_call_vector_store_endpoint",
|
||||
return_value=True,
|
||||
),
|
||||
patch.object(litellm, "vector_store_index_registry", mock_index_registry),
|
||||
patch.object(litellm, "vector_store_registry", mock_vector_registry),
|
||||
):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await azure_proxy_route(
|
||||
endpoint="indexes/managed_index/docs/search",
|
||||
request=request,
|
||||
fastapi_response=Response(),
|
||||
user_api_key_dict=UserAPIKeyAuth(team_id="team-a"),
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 403
|
||||
Reference in New Issue
Block a user