mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-09 22:31:41 +00:00
fix: preserve MongoDB deadlines and secure remote sidecar transport
This commit is contained in:
parent
5c037299f4
commit
0c519b162f
4 changed files with 97 additions and 42 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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", {})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue