fix: preserve MongoDB deadlines and secure remote sidecar transport

This commit is contained in:
Yuneng Jiang 2026-09-07 23:43:21 -07:00
parent 5c037299f4
commit 0c519b162f
No known key found for this signature in database
4 changed files with 97 additions and 42 deletions

View file

@ -1,4 +1,5 @@
from collections.abc import Mapping, Sequence
from ipaddress import ip_address
from math import isfinite
from types import MappingProxyType
from typing import TYPE_CHECKING, Final, Literal, NoReturn
@ -196,7 +197,7 @@ class MongoDBVectorStoreConfig(BaseQueryEmbeddingVectorStoreConfig):
def get_complete_url(self, api_base: str | None, litellm_params: dict[str, object]) -> str:
if not api_base:
raise config_error("MongoDB sidecar api_base is required, for example http://mongodb-sidecar:8080.")
raise config_error("MongoDB sidecar api_base is required, for example http://127.0.0.1:8080.")
try:
parsed: Final = urlsplit(api_base)
valid: Final = parsed.scheme in ("http", "https") and bool(parsed.hostname) and parsed.port != 0
@ -206,6 +207,17 @@ class MongoDBVectorStoreConfig(BaseQueryEmbeddingVectorStoreConfig):
raise config_error(
"MongoDB sidecar api_base must be an HTTP or HTTPS URL without credentials, query, or fragment."
)
if parsed.scheme == "http":
try:
loopback: Final = ip_address(parsed.hostname or "").is_loopback
except ValueError:
raise config_error(
"MongoDB sidecar requires HTTPS. HTTP is supported only for a loopback IP such as 127.0.0.1."
) from None
if not loopback:
raise config_error(
"MongoDB sidecar requires HTTPS. HTTP is supported only for a loopback IP such as 127.0.0.1."
)
return api_base.rstrip("/")
@staticmethod
@ -215,7 +227,10 @@ class MongoDBVectorStoreConfig(BaseQueryEmbeddingVectorStoreConfig):
return 30_000
if not isinstance(seconds, (int, float)) or not isfinite(seconds) or seconds <= 0:
raise config_error("MongoDB search timeout must be a positive finite number.")
return max(1, min(int(seconds * 1000), 30_000))
try:
return max(1, int(seconds * 1000))
except (ValueError, OverflowError):
raise config_error("MongoDB search timeout must be a positive finite number.") from None
@classmethod
def _params(

View file

@ -7,10 +7,10 @@ import httpx
import pytest
import litellm
from litellm.llms.custom_httpx.http_handler import HTTPHandler
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
from litellm.llms.mongodb.vector_stores.transformation import MongoDBVectorStoreConfig
from litellm.types.utils import EmbeddingResponse
from litellm.types.vector_stores import VectorStoreSearchOptionalRequestParams
from litellm.types.vector_stores import VectorStoreSearchOptionalRequestParams, VectorStoreSearchResponse
BASE_PARAMS: Final = {
"api_base": "https://sidecar.example/prefix",
@ -131,52 +131,92 @@ def test_invalid_search_is_rejected_before_embedding(
(200, {**RESULT, "data": [{"score": "wrong"}]}, litellm.ServiceUnavailableError),
(0, {}, litellm.Timeout),
(-1, {}, litellm.BadRequestError),
(-2, {"api_base": "http://sidecar.example"}, litellm.BadRequestError),
(-2, {"api_base": "http://10.0.0.10:8080"}, litellm.BadRequestError),
(-2, {"api_base": "http://localhost:8080"}, litellm.BadRequestError),
(200, RESULT, None),
],
)
def test_public_sdk_preserves_http_errors_response_and_timeout(
status: int, body: Mapping[str, object], error_type: type[Exception] | None
@pytest.mark.parametrize("asynchronous", [False, True])
@pytest.mark.parametrize("timeout", [0.75, 120.0])
@pytest.mark.parametrize("api_base", ["https://sidecar.example/prefix", "http://127.0.0.1:8080", "http://[::1]:8080"])
@pytest.mark.asyncio
async def test_public_sdk_preserves_http_errors_response_and_timeout(
status: int,
body: Mapping[str, object],
error_type: type[Exception] | None,
asynchronous: bool,
timeout: float,
api_base: str,
) -> None:
executor: Final = RecordingEmbeddingExecutor()
if status == -1:
with pytest.raises(litellm.BadRequestError, match="search-only"):
litellm.vector_stores.create(custom_llm_provider="mongodb")
executor.call.assert_not_called()
if asynchronous:
with pytest.raises(litellm.BadRequestError, match="search-only"):
await litellm.vector_stores.acreate(custom_llm_provider="mongodb")
else:
with pytest.raises(litellm.BadRequestError, match="search-only"):
litellm.vector_stores.create(custom_llm_provider="mongodb")
return
def respond(request: httpx.Request) -> httpx.Response:
assert request.url == "https://sidecar.example/prefix/v1/vector_stores/policy_index/search"
assert request.headers["authorization"] == "Bearer test-sidecar-key"
assert request.extensions["timeout"]["read"] == 0.75
payload: Final = json.loads(request.content)
assert payload["timeout_ms"] == 750
assert payload["query_vector"] == [0.1, 0.2, 0.3]
if status == 0:
raise httpx.ReadTimeout("timed out", request=request)
return httpx.Response(status, json=body)
with httpx.Client(transport=httpx.MockTransport(respond)) as transport:
client: Final = HTTPHandler(client=transport)
if error_type is not None:
with pytest.raises(error_type):
if status == -2:
rejected_params: Final = {**BASE_PARAMS, "api_base": str(body["api_base"])}
if asynchronous:
with pytest.raises(litellm.BadRequestError, match="requires HTTPS"):
await litellm.vector_stores.asearch(
vector_store_id="policy_index",
query="travel policy",
custom_llm_provider="mongodb",
_direct_vector_store_embedding_executor=executor,
**rejected_params,
)
else:
with pytest.raises(litellm.BadRequestError, match="requires HTTPS"):
litellm.vector_stores.search(
vector_store_id="policy_index",
query="travel policy",
custom_llm_provider="mongodb",
_direct_vector_store_embedding_executor=executor,
client=client,
timeout=0.75,
**BASE_PARAMS,
**rejected_params,
)
else:
result: Final = litellm.vector_stores.search(
vector_store_id="policy_index",
query="travel policy",
custom_llm_provider="mongodb",
_direct_vector_store_embedding_executor=executor,
client=client,
timeout=0.75,
**BASE_PARAMS,
)
assert result == RESULT
executor.call.assert_not_called()
return
def respond(request: httpx.Request) -> httpx.Response:
assert request.url == f"{api_base}/v1/vector_stores/policy_index/search"
assert request.headers["authorization"] == "Bearer test-sidecar-key"
assert request.extensions["timeout"]["read"] == timeout
payload: Final = json.loads(request.content)
assert payload["timeout_ms"] == int(timeout * 1000)
assert payload["query_vector"] == [0.1, 0.2, 0.3]
if status == 0:
raise httpx.ReadTimeout("timed out", request=request)
return httpx.Response(status, json=body)
async with httpx.AsyncClient(transport=httpx.MockTransport(respond)) as async_transport:
with httpx.Client(transport=httpx.MockTransport(respond)) as transport:
client: Final = AsyncHTTPHandler() if asynchronous else HTTPHandler(client=transport)
if isinstance(client, AsyncHTTPHandler):
await client.client.aclose()
client.client = async_transport
async def search() -> VectorStoreSearchResponse:
kwargs: Final = {
**BASE_PARAMS,
"api_base": api_base,
"vector_store_id": "policy_index",
"query": "travel policy",
"custom_llm_provider": "mongodb",
"_direct_vector_store_embedding_executor": executor,
"client": client,
"timeout": timeout,
}
if asynchronous:
return await litellm.vector_stores.asearch(**kwargs)
return litellm.vector_stores.search(**kwargs)
if error_type is not None:
with pytest.raises(error_type):
await search()
else:
assert await search() == RESULT
executor.call.assert_called_once_with("embedding-alias", "travel policy", {})

View file

@ -69,7 +69,7 @@ describe("VectorStoreForm", () => {
});
});
const MONGODB_SIDECAR_URL = "http://mongodb-sidecar:8080";
const MONGODB_SIDECAR_URL = "http://127.0.0.1:8080";
const MONGODB_REQUIRED_FORM_VALUES = {
api_base: MONGODB_SIDECAR_URL,

View file

@ -177,8 +177,8 @@ export const vectorStoreProviderFields: Record<string, VectorStoreFieldConfig[]>
{
name: "api_base",
label: "Sidecar URL",
tooltip: "The URL of your separately deployed MongoDB sidecar. Configure MongoDB credentials in the sidecar",
placeholder: "http://mongodb-sidecar:8080",
tooltip: "Use HTTPS for a remote sidecar, or HTTP with a loopback IP for a sidecar on the same host or Pod",
placeholder: "http://127.0.0.1:8080",
required: true,
type: "text",
},