mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
fix(bedrock): reject booleans in OpenAI in/nin filter lists and map older filter shapes first
A list value now holds only strings and numbers, as in OpenAI's schema, so pydantic no longer turns true/false into 1/0 before the filter reaches AWS. The older operator and and/or shapes are checked before the type key, so an older filter that also carries a type key maps as it did before.
This commit is contained in:
parent
ea0153a416
commit
0bc2f59d62
3 changed files with 27 additions and 18 deletions
|
|
@ -181,15 +181,15 @@ class BedrockVectorStoreConfig(BaseVectorStoreConfig, BaseAWSLLM):
|
|||
def _aws_filters(self, filters: object, drop_params: bool) -> Mapping[str, object] | None:
|
||||
if not isinstance(filters, dict):
|
||||
return self._invalid_filters(f"filters must be an object, got {type(filters).__name__}", drop_params)
|
||||
if "operator" in filters:
|
||||
return self._map_operator_filter(filters)
|
||||
if "and" in filters or "or" in filters:
|
||||
return self._map_and_or_filters(filters)
|
||||
if "type" in filters:
|
||||
try:
|
||||
return _aws_retrieval_filter(_OPENAI_FILTER.validate_python(filters))
|
||||
except ValidationError as error:
|
||||
return self._invalid_filters(_validation_reason(error), drop_params)
|
||||
if "operator" in filters:
|
||||
return self._map_operator_filter(filters)
|
||||
if "and" in filters or "or" in filters:
|
||||
return self._map_and_or_filters(filters)
|
||||
return filters
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from datetime import datetime
|
|||
from enum import Enum
|
||||
from typing import Annotated, Any, Literal, TypeAlias
|
||||
|
||||
from pydantic import ConfigDict, Field
|
||||
from pydantic import ConfigDict, Field, StrictFloat, StrictInt, StrictStr
|
||||
from typing_extensions import ReadOnly, TypedDict
|
||||
|
||||
from litellm.types.llms.base import LiteLLMBaseModel
|
||||
|
|
@ -329,7 +329,7 @@ class OpenAIComparisonFilter(LiteLLMBaseModel):
|
|||
|
||||
type: Literal["eq", "ne", "gt", "gte", "lt", "lte", "in", "nin"]
|
||||
key: str
|
||||
value: str | bool | int | float | tuple[str | int | float, ...]
|
||||
value: str | bool | int | float | tuple[StrictStr | StrictInt | StrictFloat, ...]
|
||||
|
||||
|
||||
class OpenAICompoundFilter(LiteLLMBaseModel):
|
||||
|
|
|
|||
|
|
@ -502,6 +502,11 @@ def _sent_filter(retrieve_route: respx.Route) -> object:
|
|||
{"notEquals": {"key": "department", "value": "hr"}},
|
||||
id="operator-shape-still-mapped",
|
||||
),
|
||||
pytest.param(
|
||||
{"key": "department", "value": "hr", "operator": "eq", "type": "string"},
|
||||
{"equals": {"key": "department", "value": "hr"}},
|
||||
id="operator-shape-with-type-key-still-mapped",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"and": [
|
||||
|
|
@ -535,22 +540,26 @@ async def test_search_sends_openai_filters_as_aws_retrieval_filters(
|
|||
assert _sent_filter(retrieve_route) == aws_filter
|
||||
|
||||
|
||||
INVALID_FILTERS: Final = [
|
||||
pytest.param({"type": "regex", "key": "department", "value": "h.*"}, id="unknown-type"),
|
||||
pytest.param({"type": "eq", "key": "department"}, id="missing-value"),
|
||||
pytest.param({"type": "and", "filters": []}, id="empty-compound"),
|
||||
pytest.param(
|
||||
INVALID_FILTERS: Final = (
|
||||
("unknown-type", {"type": "regex", "key": "department", "value": "h.*"}, "Input tag 'regex'"),
|
||||
("missing-value", {"type": "eq", "key": "department"}, "filters.eq.value: Field required"),
|
||||
("empty-compound", {"type": "and", "filters": []}, "filters.and.filters: Tuple should have at least 1 item"),
|
||||
(
|
||||
"untyped-member",
|
||||
{"type": "and", "filters": [{"type": "eq", "key": "department", "value": "hr"}, {"key": "year"}]},
|
||||
id="untyped-member",
|
||||
"filters.and.filters.1: Unable to extract tag",
|
||||
),
|
||||
pytest.param([{"type": "eq", "key": "department", "value": "hr"}], id="not-an-object"),
|
||||
]
|
||||
("bools-in-list", {"type": "in", "key": "remote", "value": [True, False]}, "filters.in.value"),
|
||||
("not-an-object", [{"type": "eq", "key": "department", "value": "hr"}], "filters must be an object, got list"),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("filters", INVALID_FILTERS)
|
||||
@pytest.mark.parametrize(
|
||||
("filters", "reason"), [pytest.param(filters, reason, id=case) for case, filters, reason in INVALID_FILTERS]
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_rejects_invalid_filters_with_400_without_calling_aws(
|
||||
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch, filters: object
|
||||
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch, filters: object, reason: str
|
||||
):
|
||||
import litellm
|
||||
|
||||
|
|
@ -572,11 +581,11 @@ async def test_search_rejects_invalid_filters_with_400_without_calling_aws(
|
|||
)
|
||||
|
||||
assert error.value.status_code == 400
|
||||
assert "filters" in error.value.message
|
||||
assert reason in error.value.message
|
||||
assert retrieve_route.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("filters", INVALID_FILTERS)
|
||||
@pytest.mark.parametrize("filters", [pytest.param(filters, id=case) for case, filters, _ in INVALID_FILTERS])
|
||||
@pytest.mark.parametrize("drop_params_source", ["request", "global"])
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_drops_invalid_filters_under_drop_params(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue