mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
fix(bedrock): report a bad vector store filter member without calling its group empty
An and/or group's non-empty check ran as min_length on the partly validated tuple after fail_fast stopped at a bad member, so every enclosing group also reported that it had no members. It now runs as an after-validator, which only sees a group whose members all validated, so one bad member gives one error at any depth. A reason over 2000 characters now keeps its first and last 1000 characters, so a deeply nested path still ends on the bad field and its message.
This commit is contained in:
parent
2b32463e76
commit
c595f7a47f
3 changed files with 35 additions and 6 deletions
|
|
@ -63,6 +63,7 @@ _AWS_FILTER_KEYS: Final = frozenset(
|
|||
)
|
||||
_OPENAI_FILTER: Final[TypeAdapter[OpenAIComparisonFilter | OpenAICompoundFilter]] = TypeAdapter(OpenAIVectorStoreFilter)
|
||||
_MAX_REASON_CHARS: Final = 2000
|
||||
_REASON_END_CHARS: Final = _MAX_REASON_CHARS // 2
|
||||
|
||||
|
||||
def _aws_retrieval_filter(openai_filter: OpenAIComparisonFilter | OpenAICompoundFilter) -> Mapping[str, object]:
|
||||
|
|
@ -81,7 +82,9 @@ def _validation_reason(error: ValidationError) -> str:
|
|||
".".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]}..."
|
||||
return (
|
||||
reason if len(reason) <= _MAX_REASON_CHARS else f"{reason[:_REASON_END_CHARS]}...{reason[-_REASON_END_CHARS:]}"
|
||||
)
|
||||
|
||||
|
||||
class BedrockVectorStoreConfig(BaseVectorStoreConfig, BaseAWSLLM):
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from datetime import datetime
|
|||
from enum import Enum
|
||||
from typing import Annotated, Any, Literal, TypeAlias
|
||||
|
||||
from pydantic import ConfigDict, FailFast, Field, StrictFloat, StrictInt, StrictStr
|
||||
from pydantic import AfterValidator, ConfigDict, FailFast, Field, StrictFloat, StrictInt, StrictStr
|
||||
from typing_extensions import ReadOnly, TypedDict
|
||||
|
||||
from litellm.types.llms.base import LiteLLMBaseModel
|
||||
|
|
@ -332,11 +332,17 @@ class OpenAIComparisonFilter(LiteLLMBaseModel):
|
|||
value: str | bool | int | float | Annotated[tuple[StrictStr | StrictInt | StrictFloat, ...], FailFast()]
|
||||
|
||||
|
||||
def _require_members(members: tuple["OpenAIVectorStoreFilter", ...]) -> tuple["OpenAIVectorStoreFilter", ...]:
|
||||
if not members:
|
||||
raise ValueError("an and/or filter needs at least one member")
|
||||
return members
|
||||
|
||||
|
||||
class OpenAICompoundFilter(LiteLLMBaseModel):
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
type: Literal["and", "or"]
|
||||
filters: tuple["OpenAIVectorStoreFilter", ...] = Field(min_length=1, fail_fast=True)
|
||||
filters: Annotated[tuple["OpenAIVectorStoreFilter", ...], FailFast(), AfterValidator(_require_members)]
|
||||
|
||||
|
||||
OpenAIVectorStoreFilter: TypeAlias = Annotated[
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import json
|
||||
import traceback
|
||||
from collections.abc import Mapping
|
||||
from typing import Final
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
|
@ -649,7 +650,7 @@ async def _rejection(respx_mock: respx.MockRouter, filters: object) -> BaseExcep
|
|||
return error.value
|
||||
|
||||
|
||||
def _nested_compound(depth: int) -> dict[str, object]:
|
||||
def _nested_compound(depth: int) -> Mapping[str, object]:
|
||||
if depth == 0:
|
||||
return {"type": "eq", "key": "year"}
|
||||
return {"type": "and", "filters": [_nested_compound(depth - 1)]}
|
||||
|
|
@ -694,8 +695,27 @@ async def test_search_rejection_and_its_traceback_do_not_grow_with_nesting_depth
|
|||
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))
|
||||
shallow: Final = await _rejection(respx_mock, _nested_compound(160))
|
||||
deep: Final = await _rejection(respx_mock, _nested_compound(240))
|
||||
|
||||
assert len(str(deep)) == len(str(shallow))
|
||||
assert "eq.value" in str(deep)
|
||||
assert len("".join(traceback.format_exception(deep))) == len("".join(traceback.format_exception(shallow)))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_rejection_names_a_bad_member_the_same_wherever_it_sits(
|
||||
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
import litellm
|
||||
|
||||
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
|
||||
monkeypatch.setattr(litellm, "drop_params", False)
|
||||
good: Final = {"type": "eq", "key": "department", "value": "hr"}
|
||||
bad: Final = {"key": "year"}
|
||||
|
||||
bad_first: Final = str(await _rejection(respx_mock, {"type": "and", "filters": [bad, good]}))
|
||||
bad_last: Final = str(await _rejection(respx_mock, {"type": "and", "filters": [good, bad]}))
|
||||
|
||||
assert bad_first.replace("filters.and.filters.0", "filters.and.filters.1") == bad_last
|
||||
assert "..." not in bad_last
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue