mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
fix(bedrock): stop validating a vector store filter at its first bad item
A malformed OpenAI filter listed every pydantic error in its 400, so one 30 KB filter built a 1.4 MB message that stalled the worker while it was redacted. Validation now stops at the first bad list item or group member, the reason is cut at 2000 characters, and the 400 no longer chains the pydantic error into the logged traceback.
This commit is contained in:
parent
39f4360e12
commit
2b32463e76
3 changed files with 68 additions and 5 deletions
|
|
@ -62,6 +62,7 @@ _AWS_FILTER_KEYS: Final = frozenset(
|
|||
)
|
||||
)
|
||||
_OPENAI_FILTER: Final[TypeAdapter[OpenAIComparisonFilter | OpenAICompoundFilter]] = TypeAdapter(OpenAIVectorStoreFilter)
|
||||
_MAX_REASON_CHARS: Final = 2000
|
||||
|
||||
|
||||
def _aws_retrieval_filter(openai_filter: OpenAIComparisonFilter | OpenAICompoundFilter) -> Mapping[str, object]:
|
||||
|
|
@ -76,10 +77,11 @@ def _aws_retrieval_filter(openai_filter: OpenAIComparisonFilter | OpenAICompound
|
|||
|
||||
|
||||
def _validation_reason(error: ValidationError) -> str:
|
||||
return "; ".join(
|
||||
reason: Final = "; ".join(
|
||||
".".join(("filters", *(str(part) for part in detail["loc"]))) + f": {detail['msg']}"
|
||||
for detail in error.errors()
|
||||
)
|
||||
return reason if len(reason) <= _MAX_REASON_CHARS else f"{reason[:_MAX_REASON_CHARS]}..."
|
||||
|
||||
|
||||
class BedrockVectorStoreConfig(BaseVectorStoreConfig, BaseAWSLLM):
|
||||
|
|
@ -209,7 +211,7 @@ class BedrockVectorStoreConfig(BaseVectorStoreConfig, BaseAWSLLM):
|
|||
"Fix the filter, or set drop_params to drop it.",
|
||||
model=None,
|
||||
llm_provider="bedrock",
|
||||
)
|
||||
) from None
|
||||
verbose_logger.debug("Dropping invalid Bedrock vector store filters under drop_params: %s", reason)
|
||||
|
||||
def validate_environment(self, headers: dict, litellm_params: GenericLiteLLMParams | None) -> dict:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from datetime import datetime
|
|||
from enum import Enum
|
||||
from typing import Annotated, Any, Literal, TypeAlias
|
||||
|
||||
from pydantic import ConfigDict, Field, StrictFloat, StrictInt, StrictStr
|
||||
from pydantic import ConfigDict, FailFast, Field, StrictFloat, StrictInt, StrictStr
|
||||
from typing_extensions import ReadOnly, TypedDict
|
||||
|
||||
from litellm.types.llms.base import LiteLLMBaseModel
|
||||
|
|
@ -329,14 +329,14 @@ class OpenAIComparisonFilter(LiteLLMBaseModel):
|
|||
|
||||
type: Literal["eq", "ne", "gt", "gte", "lt", "lte", "in", "nin"]
|
||||
key: str
|
||||
value: str | bool | int | float | tuple[StrictStr | StrictInt | StrictFloat, ...]
|
||||
value: str | bool | int | float | Annotated[tuple[StrictStr | StrictInt | StrictFloat, ...], FailFast()]
|
||||
|
||||
|
||||
class OpenAICompoundFilter(LiteLLMBaseModel):
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
type: Literal["and", "or"]
|
||||
filters: tuple["OpenAIVectorStoreFilter", ...] = Field(min_length=1)
|
||||
filters: tuple["OpenAIVectorStoreFilter", ...] = Field(min_length=1, fail_fast=True)
|
||||
|
||||
|
||||
OpenAIVectorStoreFilter: TypeAlias = Annotated[
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import json
|
||||
import traceback
|
||||
from typing import Final
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
|
@ -638,3 +639,63 @@ async def test_search_drops_invalid_filters_under_drop_params(
|
|||
|
||||
assert retrieve_route.call_count == 1
|
||||
assert _sent_filter(retrieve_route) is None
|
||||
|
||||
|
||||
async def _rejection(respx_mock: respx.MockRouter, filters: object) -> BaseException:
|
||||
import litellm
|
||||
|
||||
with pytest.raises(litellm.BadRequestError) as error:
|
||||
await _search_with_filters(respx_mock, filters)
|
||||
return error.value
|
||||
|
||||
|
||||
def _nested_compound(depth: int) -> dict[str, object]:
|
||||
if depth == 0:
|
||||
return {"type": "eq", "key": "year"}
|
||||
return {"type": "and", "filters": [_nested_compound(depth - 1)]}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("one_bad_item", "many_bad_items"),
|
||||
[
|
||||
pytest.param(
|
||||
{"type": "in", "key": "remote", "value": [True]},
|
||||
{"type": "in", "key": "remote", "value": [True] * 1000},
|
||||
id="bools-in-list",
|
||||
),
|
||||
pytest.param(
|
||||
{"type": "and", "filters": [{"key": "year"}]},
|
||||
{"type": "and", "filters": [{"key": "year"}] * 1000},
|
||||
id="untyped-members",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_rejection_names_the_first_bad_item_however_many_follow(
|
||||
respx_mock: respx.MockRouter,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
one_bad_item: dict[str, object],
|
||||
many_bad_items: dict[str, object],
|
||||
):
|
||||
import litellm
|
||||
|
||||
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
|
||||
monkeypatch.setattr(litellm, "drop_params", False)
|
||||
|
||||
assert str(await _rejection(respx_mock, many_bad_items)) == str(await _rejection(respx_mock, one_bad_item))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_rejection_and_its_traceback_do_not_grow_with_nesting_depth(
|
||||
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
import litellm
|
||||
|
||||
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
|
||||
monkeypatch.setattr(litellm, "drop_params", False)
|
||||
|
||||
shallow: Final = await _rejection(respx_mock, _nested_compound(100))
|
||||
deep: Final = await _rejection(respx_mock, _nested_compound(200))
|
||||
|
||||
assert len(str(deep)) == len(str(shallow))
|
||||
assert len("".join(traceback.format_exception(deep))) == len("".join(traceback.format_exception(shallow)))
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue