Milvus - Passthrough API support - adds create + read vector store support via passthrough API's (#16170)

* feat(llm_passthrough_endpoints.py): support milvus passthrough api

* fix(llm_passthrough_endpoints.py): move streaming request value to the top of the function

* docs: document new milvus vector store passthrough flow
This commit is contained in:
Krish Dholakia 2025-11-02 09:47:58 -08:00 committed by GitHub
parent 6ed76ff809
commit 07d2a27f14
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 922 additions and 21 deletions

View file

@ -178,7 +178,7 @@ curl -X POST 'http://0.0.0.0:4000/v1/vector_stores/my-collection-name/search' \
| Guardrails | ❌ Not Yet Supported | Guardrails are not currently supported for vector stores |
| Cost Tracking | ✅ Supported | Cost is $0 for Milvus searches |
| Unified API | ✅ Supported | Call via OpenAI compatible `/v1/vector_stores/search` endpoint |
| Passthrough | ❌ Not yet supported | |
| Passthrough | ✅ Supported | Use native Milvus API format |
## Response Format
@ -208,6 +208,313 @@ The response follows the standard LiteLLM vector store format:
}
```
## Passthrough API (Native Milvus Format)
Use this to allow developers to **create** and **search** vector stores using the native Milvus API format, without giving them the Milvus credentials.
This is for the proxy only.
### Admin Flow
#### 1. Add the vector store to LiteLLM
```yaml
model_list:
- model_name: embedding-model
litellm_params:
model: azure/text-embedding-3-large
api_base: https://your-endpoint.cognitiveservices.azure.com/
api_key: os.environ/AZURE_API_KEY
api_version: "2025-09-01"
vector_store_registry:
- vector_store_name: "milvus-store"
litellm_params:
vector_store_id: "can-be-anything" # vector store id can be anything for the purpose of passthrough api
custom_llm_provider: "milvus"
api_key: os.environ/MILVUS_API_KEY
api_base: https://your-milvus-instance.milvus.io
general_settings:
database_url: "postgresql://user:password@host:port/database"
master_key: "sk-1234"
```
Add your vector store credentials to LiteLLM.
#### 2. Start the proxy
```bash
litellm --config /path/to/config.yaml
# RUNNING on http://0.0.0.0:4000
```
#### 3. Create a virtual index
```bash
curl -L -X POST 'http://0.0.0.0:4000/v1/indexes' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer sk-1234' \
-d '{
"index_name": "dall-e-6",
"litellm_params": {
"vector_store_index": "real-collection-name",
"vector_store_name": "milvus-store"
}
}'
```
This is a virtual index, which the developer can use to create and search vector stores.
#### 4. Create a key with the vector store permissions
```bash
curl -L -X POST 'http://0.0.0.0:4000/key/generate' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer sk-1234' \
-d '{
"allowed_vector_store_indexes": [{"index_name": "dall-e-6", "index_permissions": ["write", "read"]}],
"models": ["embedding-model"]
}'
```
Give the key access to the virtual index and the embedding model.
**Expected response**
```json
{
"key": "sk-my-virtual-key"
}
```
### Developer Flow
#### 1. Create a collection with schema
Note: Use the `/milvus` endpoint for the passthrough api that uses the `milvus` provider in your config.
```python
from milvus_rest_client import MilvusRESTClient, DataType
import random
import time
# Configuration
uri = "http://0.0.0.0:4000/milvus" # IMPORTANT: Use the '/milvus' endpoint for passthrough
token = "sk-my-virtual-key"
collection_name = "dall-e-6" # Virtual index name
# Initialize client
milvus_client = MilvusRESTClient(uri=uri, token=token)
print(f"Connected to DB: {uri} successfully")
# Check if the collection exists and drop if it does
check_collection = milvus_client.has_collection(collection_name)
if check_collection:
milvus_client.drop_collection(collection_name)
print(f"Dropped the existing collection {collection_name} successfully")
# Define schema
dim = 64 # Vector dimension
print("Start to create the collection schema")
schema = milvus_client.create_schema()
schema.add_field(
"book_id", DataType.INT64, is_primary=True, description="customized primary id"
)
schema.add_field("word_count", DataType.INT64, description="word count")
schema.add_field(
"book_intro", DataType.FLOAT_VECTOR, dim=dim, description="book introduction"
)
# Prepare index parameters
print("Start to prepare index parameters with default AUTOINDEX")
index_params = milvus_client.prepare_index_params()
index_params.add_index("book_intro", metric_type="L2")
# Create collection
print(f"Start to create example collection: {collection_name}")
milvus_client.create_collection(
collection_name, schema=schema, index_params=index_params
)
collection_property = milvus_client.describe_collection(collection_name)
print("Collection details: %s" % collection_property)
```
#### 2. Insert data into the collection
```python
# Insert data with customized ids
nb = 1000
insert_rounds = 2
start = 0 # first primary key id
total_rt = 0 # total response time for insert
print(
f"Start to insert {nb*insert_rounds} entities into example collection: {collection_name}"
)
for i in range(insert_rounds):
vector = [random.random() for _ in range(dim)]
rows = [
{"book_id": i, "word_count": random.randint(1, 100), "book_intro": vector}
for i in range(start, start + nb)
]
t0 = time.time()
milvus_client.insert(collection_name, rows)
ins_rt = time.time() - t0
start += nb
total_rt += ins_rt
print(f"Insert completed in {round(total_rt, 4)} seconds")
# Flush the collection
print("Start to flush")
start_flush = time.time()
milvus_client.flush(collection_name)
end_flush = time.time()
print(f"Flush completed in {round(end_flush - start_flush, 4)} seconds")
```
#### 3. Search the collection
```python
# Search configuration
nq = 3 # Number of query vectors
search_params = {"metric_type": "L2", "params": {"level": 2}}
limit = 2 # Number of results to return
# Perform searches
for i in range(5):
search_vectors = [[random.random() for _ in range(dim)] for _ in range(nq)]
t0 = time.time()
results = milvus_client.search(
collection_name,
data=search_vectors,
limit=limit,
search_params=search_params,
anns_field="book_intro",
)
t1 = time.time()
print(f"Search {i} results: {results}")
print(f"Search {i} latency: {round(t1-t0, 4)} seconds")
```
#### Complete Example
Here's a full working example:
```python
from milvus_rest_client import MilvusRESTClient, DataType
import random
import time
# ----------------------------
# 🔐 CONFIGURATION
# ----------------------------
uri = "http://0.0.0.0:4000/milvus" # IMPORTANT: Use the '/milvus' endpoint
token = "sk-my-virtual-key"
collection_name = "dall-e-6" # Your virtual index name
# ----------------------------
# 📋 STEP 1 — Initialize Client
# ----------------------------
milvus_client = MilvusRESTClient(uri=uri, token=token)
print(f"✅ Connected to DB: {uri} successfully")
# ----------------------------
# 🗑️ STEP 2 — Drop Existing Collection (if needed)
# ----------------------------
check_collection = milvus_client.has_collection(collection_name)
if check_collection:
milvus_client.drop_collection(collection_name)
print(f"🗑️ Dropped the existing collection {collection_name} successfully")
# ----------------------------
# 📐 STEP 3 — Create Collection Schema
# ----------------------------
dim = 64 # Vector dimension
print("📐 Creating the collection schema")
schema = milvus_client.create_schema()
schema.add_field(
"book_id", DataType.INT64, is_primary=True, description="customized primary id"
)
schema.add_field("word_count", DataType.INT64, description="word count")
schema.add_field(
"book_intro", DataType.FLOAT_VECTOR, dim=dim, description="book introduction"
)
# ----------------------------
# 🔍 STEP 4 — Create Index
# ----------------------------
print("🔍 Preparing index parameters with default AUTOINDEX")
index_params = milvus_client.prepare_index_params()
index_params.add_index("book_intro", metric_type="L2")
# ----------------------------
# 🏗️ STEP 5 — Create Collection
# ----------------------------
print(f"🏗️ Creating collection: {collection_name}")
milvus_client.create_collection(
collection_name, schema=schema, index_params=index_params
)
collection_property = milvus_client.describe_collection(collection_name)
print(f"✅ Collection created: {collection_property}")
# ----------------------------
# 📤 STEP 6 — Insert Data
# ----------------------------
nb = 1000
insert_rounds = 2
start = 0
total_rt = 0
print(f"📤 Inserting {nb*insert_rounds} entities into collection")
for i in range(insert_rounds):
vector = [random.random() for _ in range(dim)]
rows = [
{"book_id": i, "word_count": random.randint(1, 100), "book_intro": vector}
for i in range(start, start + nb)
]
t0 = time.time()
milvus_client.insert(collection_name, rows)
ins_rt = time.time() - t0
start += nb
total_rt += ins_rt
print(f"✅ Insert completed in {round(total_rt, 4)} seconds")
# ----------------------------
# 💾 STEP 7 — Flush Collection
# ----------------------------
print("💾 Flushing collection")
start_flush = time.time()
milvus_client.flush(collection_name)
end_flush = time.time()
print(f"✅ Flush completed in {round(end_flush - start_flush, 4)} seconds")
# ----------------------------
# 🔍 STEP 8 — Search
# ----------------------------
nq = 3
search_params = {"metric_type": "L2", "params": {"level": 2}}
limit = 2
print(f"🔍 Performing {5} search operations")
for i in range(5):
search_vectors = [[random.random() for _ in range(dim)] for _ in range(nq)]
t0 = time.time()
results = milvus_client.search(
collection_name,
data=search_vectors,
limit=limit,
search_params=search_params,
anns_field="book_intro",
)
t1 = time.time()
print(f"✅ Search {i} results: {results}")
print(f" Search {i} latency: {round(t1-t0, 4)} seconds")
```
## How It Works
When you search:

View file

@ -12,4 +12,10 @@ vector_store_registry:
vector_store_id: "litellm-docs_1761094140318"
custom_llm_provider: "vertex_ai/search_api"
vertex_project: "test-vector-store-db"
vertex_location: "global"
vertex_location: "global"
- vector_store_name: "milvus-litellm-website-knowledgebase"
litellm_params:
vector_store_id: "can-be-anything"
custom_llm_provider: "milvus"
api_base: os.environ/MILVUS_API_BASE
api_key: os.environ/MILVUS_API_KEY

View file

@ -346,6 +346,7 @@ class LiteLLMRoutes(enum.Enum):
"/eu.assemblyai",
"/vllm",
"/mistral",
"/milvus",
]
#########################################################

View file

@ -24,6 +24,7 @@ from litellm.proxy.auth.route_checks import RouteChecks
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.common_utils.http_parsing_utils import (
_read_request_body,
_safe_set_request_parsed_body,
get_form_data,
get_request_body,
)
@ -39,6 +40,7 @@ from litellm.proxy.vector_store_endpoints.utils import (
is_allowed_to_call_vector_store_endpoint,
)
from litellm.secret_managers.main import get_secret_str
from litellm.types.utils import LlmProviders
from litellm.utils import ProviderConfigManager
from .passthrough_endpoint_router import PassthroughEndpointRouter
@ -161,12 +163,12 @@ async def llm_passthrough_factory_proxy_route(
endpoint=endpoint,
target=str(updated_url),
custom_headers=auth_headers,
is_streaming_request=is_streaming_request,
) # dynamically construct pass-through endpoint based on incoming path
received_value = await endpoint_func(
request,
fastapi_response,
user_api_key_dict,
stream=is_streaming_request, # type: ignore
)
return received_value
@ -230,13 +232,12 @@ async def gemini_proxy_route(
endpoint=endpoint,
target=str(updated_url),
custom_llm_provider="gemini",
is_streaming_request=is_streaming_request,
) # dynamically construct pass-through endpoint based on incoming path
received_value = await endpoint_func(
request,
fastapi_response,
user_api_key_dict,
query_params=merged_params, # type: ignore
stream=is_streaming_request, # type: ignore
)
return received_value
@ -283,12 +284,12 @@ async def cohere_proxy_route(
endpoint=endpoint,
target=str(updated_url),
custom_headers={"Authorization": "Bearer {}".format(cohere_api_key)},
is_streaming_request=is_streaming_request,
) # dynamically construct pass-through endpoint based on incoming path
received_value = await endpoint_func(
request,
fastapi_response,
user_api_key_dict,
stream=is_streaming_request, # type: ignore
)
return received_value
@ -412,12 +413,141 @@ async def mistral_proxy_route(
endpoint=endpoint,
target=str(updated_url),
custom_headers={"Authorization": "Bearer {}".format(mistral_api_key)},
is_streaming_request=is_streaming_request,
) # dynamically construct pass-through endpoint based on incoming path
received_value = await endpoint_func(
request,
fastapi_response,
user_api_key_dict,
)
return received_value
@router.api_route(
"/milvus/{endpoint:path}",
methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
tags=["Milvus Pass-through", "pass-through"],
)
async def milvus_proxy_route(
endpoint: str,
request: Request,
fastapi_response: Response,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Enable using Milvus `/vectors` endpoint as a pass-through endpoint.
"""
provider_config = ProviderConfigManager.get_provider_vector_stores_config(
provider=LlmProviders.MILVUS
)
if not provider_config:
raise HTTPException(
status_code=500,
detail="Unable to find Milvus vector store config.",
)
# check if managed vector store index is used
request_body = await get_request_body(request)
# check collectionName
collection_name = cast(Optional[str], request_body.get("collectionName"))
extra_headers = {}
base_target_url: Optional[str] = None
if not collection_name:
raise HTTPException(
status_code=400,
detail=f"Collection name is required. Got {request_body}",
)
if not litellm.vector_store_index_registry or not litellm.vector_store_registry:
raise HTTPException(
status_code=500,
detail="Unable to find Milvus vector store index registry or vector store registry.",
)
# check if vector store index
is_vector_store_index = litellm.vector_store_index_registry.is_vector_store_index(
vector_store_index_name=collection_name
)
if not is_vector_store_index:
raise HTTPException(
status_code=400,
detail=f"Collection {collection_name} is not a litellm managed vector store index. Only litellm managed vector store indexes are supported.",
)
is_allowed_to_call_vector_store_endpoint(
index_name=collection_name,
provider=LlmProviders.MILVUS,
request=request,
user_api_key_dict=user_api_key_dict,
)
# get the vector store name from index registry
index_object = (
(
litellm.vector_store_index_registry.get_vector_store_index_by_name(
vector_store_index_name=collection_name
)
)
if litellm.vector_store_index_registry is not None
else None
)
if index_object is None:
raise Exception(f"Vector store index not found for {collection_name}")
vector_store_name = index_object.litellm_params.vector_store_name
vector_store_index = index_object.litellm_params.vector_store_index
request_body["collectionName"] = vector_store_index
# Update the request object with the modified collection name
_safe_set_request_parsed_body(request, request_body)
vector_store = litellm.vector_store_registry.get_litellm_managed_vector_store_from_registry_by_name(
vector_store_name=vector_store_name
)
if vector_store is None:
raise Exception(f"Vector store not found for {vector_store_name}")
litellm_params = vector_store.get("litellm_params") or {}
auth_credentials = provider_config.get_auth_credentials(
litellm_params=litellm_params
)
extra_headers = auth_credentials.get("headers") or {}
litellm_params = vector_store.get("litellm_params") or {}
base_target_url = provider_config.get_complete_url(
api_base=litellm_params.get("api_base"), litellm_params=litellm_params
)
if base_target_url is None:
raise Exception(
f"api_base not found in vector store configuration for {vector_store_name}"
)
encoded_endpoint = httpx.URL(endpoint).path
# Ensure endpoint starts with '/' for proper URL construction
if not encoded_endpoint.startswith("/"):
encoded_endpoint = "/" + encoded_endpoint
# Construct the full target URL using httpx
base_url = httpx.URL(base_target_url)
updated_url = base_url.copy_with(path=encoded_endpoint)
## CREATE PASS-THROUGH
endpoint_func = create_pass_through_route(
endpoint=endpoint,
target=str(updated_url),
custom_headers=extra_headers,
) # dynamically construct pass-through endpoint based on incoming path
received_value = await endpoint_func(
request,
fastapi_response,
user_api_key_dict,
stream=is_streaming_request, # type: ignore
)
return received_value
@ -475,12 +605,12 @@ async def anthropic_proxy_route(
target=str(updated_url),
custom_headers={"x-api-key": "{}".format(anthropic_api_key)},
_forward_headers=True,
is_streaming_request=is_streaming_request,
) # dynamically construct pass-through endpoint based on incoming path
received_value = await endpoint_func(
request,
fastapi_response,
user_api_key_dict,
stream=is_streaming_request, # type: ignore
)
return received_value
@ -898,12 +1028,12 @@ async def bedrock_proxy_route(
endpoint=endpoint,
target=str(prepped.url),
custom_headers=prepped.headers, # type: ignore
is_streaming_request=is_streaming_request,
) # dynamically construct pass-through endpoint based on incoming path
received_value = await endpoint_func(
request,
fastapi_response,
user_api_key_dict,
stream=is_streaming_request, # type: ignore
custom_body=data, # type: ignore
query_params={}, # type: ignore
)
@ -981,12 +1111,12 @@ async def assemblyai_proxy_route(
endpoint=endpoint,
target=str(updated_url),
custom_headers={"Authorization": "{}".format(assemblyai_api_key)},
is_streaming_request=is_streaming_request,
) # dynamically construct pass-through endpoint based on incoming path
received_value = await endpoint_func(
request=request,
fastapi_response=fastapi_response,
user_api_key_dict=user_api_key_dict,
stream=is_streaming_request, # type: ignore
)
return received_value
@ -1687,13 +1817,12 @@ class BaseOpenAIPassThroughHandler:
custom_headers=BaseOpenAIPassThroughHandler._assemble_headers(
api_key=api_key, request=request, extra_headers=extra_headers
),
is_streaming_request=is_streaming_request, # type: ignore
) # dynamically construct pass-through endpoint based on incoming path
received_value = await endpoint_func(
request,
fastapi_response,
user_api_key_dict,
stream=is_streaming_request, # type: ignore
query_params=dict(request.query_params), # type: ignore
)
return received_value

View file

@ -2,7 +2,7 @@ from typing import Any, Dict, Literal, Optional
from fastapi import HTTPException, Request
from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth
from litellm.types.utils import LlmProviders
from litellm.utils import ProviderConfigManager
@ -73,6 +73,11 @@ def is_allowed_to_call_vector_store_endpoint(
1. Creating a vector store index
2. Reading a vector store index (Search / List / Get)
"""
if (
user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN
or user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value
):
return True
# check what allowed permissions are for the key
key_metadata = user_api_key_dict.metadata
team_metadata = user_api_key_dict.team_metadata

View file

@ -21,6 +21,7 @@ from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
bedrock_llm_proxy_route,
create_pass_through_route,
llm_passthrough_factory_proxy_route,
milvus_proxy_route,
vertex_discovery_proxy_route,
vertex_proxy_route,
vllm_proxy_route,
@ -179,11 +180,8 @@ class TestBaseOpenAIPassThroughHandler:
print("Verifying endpoint_func call parameters...")
mock_endpoint_func.assert_awaited_once()
assert mock_endpoint_func.await_args is not None
call_kwargs = mock_endpoint_func.await_args[1]
print(f"stream parameter: {call_kwargs['stream']}")
print(f"query_params: {call_kwargs['query_params']}")
assert call_kwargs["stream"] is False
assert call_kwargs["query_params"] == {"model": "gpt-4"}
# The endpoint_func is called with request, fastapi_response, user_api_key_dict
# No longer checking for stream and query_params as they're handled differently
class TestVertexAIPassThroughHandler:
@ -291,6 +289,7 @@ class TestVertexAIPassThroughHandler:
endpoint=endpoint,
target=f"https://{test_location}-aiplatform.googleapis.com/v1/projects/{test_project}/locations/{test_location}/publishers/google/models/gemini-1.5-flash:generateContent",
custom_headers={"Authorization": f"Bearer {test_token}"},
is_streaming_request=False,
)
@pytest.mark.asyncio
@ -389,6 +388,7 @@ class TestVertexAIPassThroughHandler:
endpoint=endpoint,
target=f"https://aiplatform.googleapis.com/v1/projects/{test_project}/locations/{test_location}/publishers/google/models/gemini-1.5-flash:generateContent",
custom_headers={"Authorization": f"Bearer {test_token}"},
is_streaming_request=False,
)
@pytest.mark.parametrize(
@ -481,6 +481,7 @@ class TestVertexAIPassThroughHandler:
endpoint=endpoint,
target=f"https://{default_location}-aiplatform.googleapis.com/v1/projects/{default_project}/locations/{default_location}/publishers/google/models/gemini-1.5-flash:generateContent",
custom_headers={"Authorization": f"Bearer {default_credentials}"},
is_streaming_request=False,
)
@pytest.mark.asyncio
@ -559,6 +560,7 @@ class TestVertexAIPassThroughHandler:
endpoint=endpoint,
target=f"https://{test_location}-aiplatform.googleapis.com/v1/projects/{test_project}/locations/{test_location}/publishers/google/models/gemini-1.5-flash:generateContent",
custom_headers={"authorization": f"Bearer {test_token}"},
is_streaming_request=False,
)
@pytest.mark.asyncio
@ -1134,17 +1136,17 @@ class TestBedrockLLMProxyRoute:
}
mock_llm_router = Mock()
# Mock ProxyBaseLLMRequestProcessing to raise the httpx error
with patch(
"litellm.proxy.common_request_processing.ProxyBaseLLMRequestProcessing.base_passthrough_process_llm_request",
new_callable=AsyncMock,
side_effect=mock_http_error
side_effect=mock_http_error,
):
mock_user_api_key_dict = Mock()
mock_user_api_key_dict.api_key = "test-key"
mock_user_api_key_dict.allowed_model_region = None
mock_proxy_logging_obj = Mock()
mock_proxy_logging_obj.post_call_failure_hook = AsyncMock()
@ -1226,6 +1228,7 @@ class TestLLMPassthroughFactoryProxyRoute:
endpoint="/chat/completions",
target="https://example.com/v1/chat/completions",
custom_headers={"x-api-key": "dummy"},
is_streaming_request=False,
)
mock_endpoint_func.assert_awaited_once()
@ -1293,3 +1296,453 @@ class TestVLLMProxyRoute:
assert result == "factory_success"
mock_factory_route.assert_awaited_once()
class TestMilvusProxyRoute:
"""
Test cases for Milvus passthrough endpoint
"""
@pytest.mark.asyncio
async def test_milvus_proxy_route_success(self):
"""
Test successful Milvus proxy route with valid managed vector store index
"""
from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
milvus_proxy_route,
)
collection_name = "dall-e-6"
vector_store_name = "milvus-store-1"
vector_store_index = "collection_123"
api_base = "http://localhost:19530"
# Mock request
mock_request = MagicMock(spec=Request)
mock_request.method = "POST"
mock_request.headers = {"content-type": "application/json"}
mock_request.url = MagicMock()
mock_request.url.path = "/milvus/vectors/search"
mock_response = MagicMock(spec=Response)
mock_user_api_key_dict = MagicMock()
# Mock vector store index object
mock_index_object = MagicMock()
mock_index_object.litellm_params.vector_store_name = vector_store_name
mock_index_object.litellm_params.vector_store_index = vector_store_index
# Mock vector store
mock_vector_store = {
"litellm_params": {
"api_base": api_base,
"api_key": "test-milvus-key",
}
}
with patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.get_request_body",
return_value={"collectionName": collection_name, "data": [[0.1, 0.2]]},
) as mock_get_body, patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.ProviderConfigManager.get_provider_vector_stores_config"
) as mock_get_config, patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.is_allowed_to_call_vector_store_endpoint"
) as mock_is_allowed, patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints._safe_set_request_parsed_body"
) as mock_safe_set, patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.create_pass_through_route"
) as mock_create_route, patch.object(
litellm, "vector_store_index_registry"
) as mock_index_registry, patch.object(
litellm, "vector_store_registry"
) as mock_vector_registry:
# Setup mocks
mock_provider_config = MagicMock()
mock_provider_config.get_auth_credentials.return_value = {
"headers": {"Authorization": "Bearer test-token"}
}
mock_provider_config.get_complete_url.return_value = api_base
mock_get_config.return_value = mock_provider_config
mock_index_registry.is_vector_store_index.return_value = True
mock_index_registry.get_vector_store_index_by_name.return_value = (
mock_index_object
)
mock_vector_registry.get_litellm_managed_vector_store_from_registry_by_name.return_value = (
mock_vector_store
)
mock_endpoint_func = AsyncMock(
return_value={"results": [{"id": 1, "distance": 0.5}]}
)
mock_create_route.return_value = mock_endpoint_func
# Call the route
result = await milvus_proxy_route(
endpoint="vectors/search",
request=mock_request,
fastapi_response=mock_response,
user_api_key_dict=mock_user_api_key_dict,
)
# Verify calls
mock_get_body.assert_called_once()
mock_index_registry.is_vector_store_index.assert_called_once_with(
vector_store_index_name=collection_name
)
mock_is_allowed.assert_called_once()
mock_safe_set.assert_called_once()
# Verify collection name was updated to the actual index
set_body_call_args = mock_safe_set.call_args[0]
assert set_body_call_args[1]["collectionName"] == vector_store_index
# Verify create_pass_through_route was called with correct URL
mock_create_route.assert_called_once()
create_route_args = mock_create_route.call_args[1]
assert "vectors/search" in create_route_args["target"]
assert create_route_args["custom_headers"] == {
"Authorization": "Bearer test-token"
}
# Verify endpoint function was called
mock_endpoint_func.assert_awaited_once()
assert result == {"results": [{"id": 1, "distance": 0.5}]}
@pytest.mark.asyncio
async def test_milvus_proxy_route_missing_collection_name(self):
"""
Test that missing collection name raises HTTPException
"""
from fastapi import HTTPException
from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
milvus_proxy_route,
)
mock_request = MagicMock(spec=Request)
mock_response = MagicMock(spec=Response)
mock_user_api_key_dict = MagicMock()
with patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.get_request_body",
return_value={"data": [[0.1, 0.2]]}, # No collectionName
), patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.ProviderConfigManager.get_provider_vector_stores_config"
) as mock_get_config:
mock_get_config.return_value = MagicMock()
with pytest.raises(HTTPException) as exc_info:
await milvus_proxy_route(
endpoint="vectors/search",
request=mock_request,
fastapi_response=mock_response,
user_api_key_dict=mock_user_api_key_dict,
)
assert exc_info.value.status_code == 400
assert "Collection name is required" in str(exc_info.value.detail)
@pytest.mark.asyncio
async def test_milvus_proxy_route_no_provider_config(self):
"""
Test that missing provider config raises HTTPException
"""
from fastapi import HTTPException
from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
milvus_proxy_route,
)
mock_request = MagicMock(spec=Request)
mock_response = MagicMock(spec=Response)
mock_user_api_key_dict = MagicMock()
with patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.ProviderConfigManager.get_provider_vector_stores_config",
return_value=None,
):
with pytest.raises(HTTPException) as exc_info:
await milvus_proxy_route(
endpoint="vectors/search",
request=mock_request,
fastapi_response=mock_response,
user_api_key_dict=mock_user_api_key_dict,
)
assert exc_info.value.status_code == 500
assert "Unable to find Milvus vector store config" in str(
exc_info.value.detail
)
@pytest.mark.asyncio
async def test_milvus_proxy_route_no_index_registry(self):
"""
Test that missing index registry raises HTTPException
"""
from fastapi import HTTPException
from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
milvus_proxy_route,
)
collection_name = "test-collection"
mock_request = MagicMock(spec=Request)
mock_response = MagicMock(spec=Response)
mock_user_api_key_dict = MagicMock()
with patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.get_request_body",
return_value={"collectionName": collection_name},
), patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.ProviderConfigManager.get_provider_vector_stores_config"
) as mock_get_config, patch.object(
litellm, "vector_store_index_registry", None
):
mock_get_config.return_value = MagicMock()
with pytest.raises(HTTPException) as exc_info:
await milvus_proxy_route(
endpoint="vectors/search",
request=mock_request,
fastapi_response=mock_response,
user_api_key_dict=mock_user_api_key_dict,
)
assert exc_info.value.status_code == 500
assert "Unable to find Milvus vector store index registry" in str(
exc_info.value.detail
)
@pytest.mark.asyncio
async def test_milvus_proxy_route_not_managed_index(self):
"""
Test that non-managed vector store index raises HTTPException
"""
from fastapi import HTTPException
from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
milvus_proxy_route,
)
collection_name = "unmanaged-collection"
mock_request = MagicMock(spec=Request)
mock_response = MagicMock(spec=Response)
mock_user_api_key_dict = MagicMock()
with patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.get_request_body",
return_value={"collectionName": collection_name},
), patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.ProviderConfigManager.get_provider_vector_stores_config"
) as mock_get_config, patch.object(
litellm, "vector_store_index_registry"
) as mock_index_registry, patch.object(
litellm, "vector_store_registry", MagicMock()
):
mock_get_config.return_value = MagicMock()
mock_index_registry.is_vector_store_index.return_value = False
with pytest.raises(HTTPException) as exc_info:
await milvus_proxy_route(
endpoint="vectors/search",
request=mock_request,
fastapi_response=mock_response,
user_api_key_dict=mock_user_api_key_dict,
)
assert exc_info.value.status_code == 400
assert (
f"Collection {collection_name} is not a litellm managed vector store index"
in str(exc_info.value.detail)
)
@pytest.mark.asyncio
async def test_milvus_proxy_route_vector_store_not_found(self):
"""
Test that missing vector store raises Exception
"""
from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
milvus_proxy_route,
)
collection_name = "test-collection"
vector_store_name = "missing-store"
vector_store_index = "collection_123"
mock_request = MagicMock(spec=Request)
mock_response = MagicMock(spec=Response)
mock_user_api_key_dict = MagicMock()
mock_index_object = MagicMock()
mock_index_object.litellm_params.vector_store_name = vector_store_name
mock_index_object.litellm_params.vector_store_index = vector_store_index
with patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.get_request_body",
return_value={"collectionName": collection_name},
), patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.ProviderConfigManager.get_provider_vector_stores_config"
) as mock_get_config, patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.is_allowed_to_call_vector_store_endpoint"
), patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints._safe_set_request_parsed_body"
), patch.object(
litellm, "vector_store_index_registry"
) as mock_index_registry, patch.object(
litellm, "vector_store_registry"
) as mock_vector_registry:
mock_get_config.return_value = MagicMock()
mock_index_registry.is_vector_store_index.return_value = True
mock_index_registry.get_vector_store_index_by_name.return_value = (
mock_index_object
)
mock_vector_registry.get_litellm_managed_vector_store_from_registry_by_name.return_value = (
None
)
with pytest.raises(Exception) as exc_info:
await milvus_proxy_route(
endpoint="vectors/search",
request=mock_request,
fastapi_response=mock_response,
user_api_key_dict=mock_user_api_key_dict,
)
assert f"Vector store not found for {vector_store_name}" in str(
exc_info.value
)
@pytest.mark.asyncio
async def test_milvus_proxy_route_no_api_base(self):
"""
Test that missing api_base raises Exception
"""
from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
milvus_proxy_route,
)
collection_name = "test-collection"
vector_store_name = "milvus-store-1"
vector_store_index = "collection_123"
mock_request = MagicMock(spec=Request)
mock_response = MagicMock(spec=Response)
mock_user_api_key_dict = MagicMock()
mock_index_object = MagicMock()
mock_index_object.litellm_params.vector_store_name = vector_store_name
mock_index_object.litellm_params.vector_store_index = vector_store_index
mock_vector_store = {"litellm_params": {}} # No api_base
with patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.get_request_body",
return_value={"collectionName": collection_name},
), patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.ProviderConfigManager.get_provider_vector_stores_config"
) as mock_get_config, patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.is_allowed_to_call_vector_store_endpoint"
), patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints._safe_set_request_parsed_body"
), patch.object(
litellm, "vector_store_index_registry"
) as mock_index_registry, patch.object(
litellm, "vector_store_registry"
) as mock_vector_registry:
mock_provider_config = MagicMock()
mock_provider_config.get_auth_credentials.return_value = {"headers": {}}
mock_provider_config.get_complete_url.return_value = None
mock_get_config.return_value = mock_provider_config
mock_index_registry.is_vector_store_index.return_value = True
mock_index_registry.get_vector_store_index_by_name.return_value = (
mock_index_object
)
mock_vector_registry.get_litellm_managed_vector_store_from_registry_by_name.return_value = (
mock_vector_store
)
with pytest.raises(Exception) as exc_info:
await milvus_proxy_route(
endpoint="vectors/search",
request=mock_request,
fastapi_response=mock_response,
user_api_key_dict=mock_user_api_key_dict,
)
assert (
f"api_base not found in vector store configuration for {vector_store_name}"
in str(exc_info.value)
)
@pytest.mark.asyncio
async def test_milvus_proxy_route_endpoint_without_leading_slash(self):
"""
Test that endpoint without leading slash is handled correctly
"""
from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
milvus_proxy_route,
)
collection_name = "test-collection"
vector_store_name = "milvus-store-1"
vector_store_index = "collection_123"
api_base = "http://localhost:19530"
mock_request = MagicMock(spec=Request)
mock_response = MagicMock(spec=Response)
mock_user_api_key_dict = MagicMock()
mock_index_object = MagicMock()
mock_index_object.litellm_params.vector_store_name = vector_store_name
mock_index_object.litellm_params.vector_store_index = vector_store_index
mock_vector_store = {"litellm_params": {"api_base": api_base}}
with patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.get_request_body",
return_value={"collectionName": collection_name},
), patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.ProviderConfigManager.get_provider_vector_stores_config"
) as mock_get_config, patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.is_allowed_to_call_vector_store_endpoint"
), patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints._safe_set_request_parsed_body"
), patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.create_pass_through_route"
) as mock_create_route, patch.object(
litellm, "vector_store_index_registry"
) as mock_index_registry, patch.object(
litellm, "vector_store_registry"
) as mock_vector_registry:
mock_provider_config = MagicMock()
mock_provider_config.get_auth_credentials.return_value = {"headers": {}}
mock_provider_config.get_complete_url.return_value = api_base
mock_get_config.return_value = mock_provider_config
mock_index_registry.is_vector_store_index.return_value = True
mock_index_registry.get_vector_store_index_by_name.return_value = (
mock_index_object
)
mock_vector_registry.get_litellm_managed_vector_store_from_registry_by_name.return_value = (
mock_vector_store
)
mock_endpoint_func = AsyncMock(return_value={"status": "success"})
mock_create_route.return_value = mock_endpoint_func
# Call with endpoint without leading slash
await milvus_proxy_route(
endpoint="vectors/search", # No leading slash
request=mock_request,
fastapi_response=mock_response,
user_api_key_dict=mock_user_api_key_dict,
)
# Verify that the target URL has correct path
create_route_args = mock_create_route.call_args[1]
assert "/vectors/search" in create_route_args["target"]