mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-09 22:31:41 +00:00
fix(bedrock): reject Marengo 2.7-only and misplaced media params on 3.0 unless drop_params
Marengo 3.0 requests now get a 400 naming any textTruncate, lengthSec, useFixedLengthSec, or minClipSec parameter, and any video or audio option sent with a text, image, text_image, or multi_input request, instead of silently dropping them. drop_params (global, per deployment, or per request) drops them instead. Pydantic validation errors name the field and the reason, and the 3.0 marker is the exact "marengo-embed-3-" model id segment.
This commit is contained in:
parent
86790a7723
commit
80fea089b6
4 changed files with 158 additions and 25 deletions
|
|
@ -35,7 +35,7 @@ from .amazon_titan_multimodal_transformation import (
|
|||
)
|
||||
from .amazon_titan_v2_transformation import AmazonTitanV2Config
|
||||
from .cohere_transformation import BedrockCohereEmbeddingConfig
|
||||
from .twelvelabs_marengo_transformation import TwelveLabsMarengoEmbeddingConfig
|
||||
from .twelvelabs_marengo_transformation import TwelveLabsMarengoEmbeddingConfig, drop_params_enabled
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
|
|
@ -480,6 +480,7 @@ class BedrockEmbedding(BaseAWSLLM):
|
|||
async_invoke_route=has_async_invoke,
|
||||
model_id=modelId,
|
||||
output_s3_uri=inference_params.get("output_s3_uri"),
|
||||
drop_params=drop_params_enabled(litellm_params),
|
||||
)
|
||||
batch_data.append(twelvelabs_request)
|
||||
elif provider == "nova":
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ from litellm.types.llms.bedrock import (
|
|||
)
|
||||
from litellm.utils import get_base64_str
|
||||
|
||||
MARENGO_3_MODEL_MARKER: Final = "marengo-embed-3"
|
||||
MARENGO_3_MODEL_MARKER: Final = "marengo-embed-3-"
|
||||
S3_URI_PREFIX: Final = "s3://"
|
||||
TIMED_MEDIA_OPTION_FIELDS: Final = MappingProxyType(
|
||||
{
|
||||
|
|
@ -48,6 +48,9 @@ TIMED_MEDIA_OPTION_FIELDS: Final = MappingProxyType(
|
|||
}
|
||||
)
|
||||
TIMED_MEDIA_OPTIONS: Final = TypeAdapter(TwelveLabsMarengo3TimedMediaOptions)
|
||||
TIMED_INPUT_TYPES: Final = frozenset({"video", "audio"})
|
||||
MARENGO_2_7_ONLY_PARAMS: Final = ("textTruncate", "lengthSec", "useFixedLengthSec", "minClipSec")
|
||||
MARENGO_2_7_ONLY_FIELDS: Final = MappingProxyType({name: True for name in MARENGO_2_7_ONLY_PARAMS})
|
||||
|
||||
|
||||
def is_marengo_3_model(model: str | None) -> bool:
|
||||
|
|
@ -69,15 +72,23 @@ class Marengo3Params(BaseModel):
|
|||
embeddingType: tuple[TWELVELABS_MARENGO_3_EMBEDDING_TYPES, ...] | None = None
|
||||
embeddingScope: tuple[TWELVELABS_MARENGO_3_EMBEDDING_SCOPES, ...] | None = None
|
||||
inferenceId: str | None = None
|
||||
textTruncate: object = None
|
||||
lengthSec: object = None
|
||||
useFixedLengthSec: object = None
|
||||
minClipSec: object = None
|
||||
|
||||
@property
|
||||
def resolved_input_type(self) -> TWELVELABS_MARENGO_3_INPUT_TYPES:
|
||||
return self.inputType or self.input_type or "text"
|
||||
|
||||
def timed_media_options(self) -> TwelveLabsMarengo3TimedMediaOptions:
|
||||
return TIMED_MEDIA_OPTIONS.validate_python(
|
||||
self.model_dump(include=TIMED_MEDIA_OPTION_FIELDS, exclude_none=True)
|
||||
)
|
||||
return TIMED_MEDIA_OPTIONS.validate_python(self.given_timed_media_options())
|
||||
|
||||
def given_timed_media_options(self) -> dict[str, object]:
|
||||
return self.model_dump(include=TIMED_MEDIA_OPTION_FIELDS, exclude_none=True)
|
||||
|
||||
def given_2_7_only_params(self) -> dict[str, object]:
|
||||
return self.model_dump(include=MARENGO_2_7_ONLY_FIELDS, exclude_none=True)
|
||||
|
||||
|
||||
def _s3_location(uri: str, bucket_owner: str | None) -> TwelveLabsS3Location:
|
||||
|
|
@ -113,11 +124,23 @@ def _timed_media_input(media: str, params: Marengo3Params) -> TwelveLabsMarengo3
|
|||
return timed
|
||||
|
||||
|
||||
def _describe(error: ValidationError) -> str:
|
||||
return "; ".join(
|
||||
f"{'.'.join(str(part) for part in problem['loc'])}: {problem['msg']}" for problem in error.errors()
|
||||
)
|
||||
|
||||
|
||||
def _validated_params(inference_params: Mapping[str, object]) -> Marengo3Params:
|
||||
try:
|
||||
return Marengo3Params.model_validate(inference_params)
|
||||
except ValidationError as error:
|
||||
raise BedrockError(status_code=400, message=f"Invalid Marengo 3.0 parameters: {error}") from error
|
||||
raise BedrockError(status_code=400, message=f"Invalid Marengo 3.0 parameters: {_describe(error)}") from error
|
||||
|
||||
|
||||
def _reject_unless_dropped(given: Mapping[str, object], drop_params: bool, reason: str) -> None:
|
||||
if not given or drop_params:
|
||||
return
|
||||
raise BedrockError(status_code=400, message=f"{reason} {', '.join(given)}; set drop_params to drop them")
|
||||
|
||||
|
||||
def _require(value: str | None, input_type: str, param_name: str) -> str:
|
||||
|
|
@ -143,10 +166,19 @@ def _request_base(inference_id: str | None) -> TwelveLabsMarengo3RequestBase:
|
|||
return identified
|
||||
|
||||
|
||||
def build_marengo_3_request(input: str, inference_params: Mapping[str, object]) -> TwelveLabsMarengo3EmbeddingRequest:
|
||||
def build_marengo_3_request(
|
||||
input: str, inference_params: Mapping[str, object], drop_params: bool = False
|
||||
) -> TwelveLabsMarengo3EmbeddingRequest:
|
||||
params: Final = _validated_params(inference_params)
|
||||
base: Final = _request_base(params.inferenceId)
|
||||
input_type: Final = params.resolved_input_type
|
||||
_reject_unless_dropped(
|
||||
params.given_2_7_only_params(), drop_params, "Marengo 3.0 does not accept the Marengo 2.7 parameters"
|
||||
)
|
||||
if input_type not in TIMED_INPUT_TYPES:
|
||||
_reject_unless_dropped(
|
||||
params.given_timed_media_options(), drop_params, f"Input type '{input_type}' does not accept"
|
||||
)
|
||||
match input_type:
|
||||
case "text":
|
||||
text_request: Final[TwelveLabsMarengo3TextRequest] = {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ from typing import Final, cast
|
|||
from pydantic import BaseModel, ConfigDict, TypeAdapter
|
||||
from typing_extensions import assert_never
|
||||
|
||||
import litellm
|
||||
from litellm.llms.bedrock.embed.twelvelabs_marengo_3_transformation import (
|
||||
MARENGO_2_7_ONLY_PARAMS,
|
||||
build_marengo_3_request,
|
||||
is_marengo_3_model,
|
||||
)
|
||||
|
|
@ -99,6 +101,25 @@ def _billed_usage(batch_data: list[dict] | None) -> Usage:
|
|||
return Usage(prompt_tokens=0, completion_tokens=0, total_tokens=0, prompt_tokens_details=details)
|
||||
|
||||
|
||||
MARENGO_SHARED_PARAMS: Final = (
|
||||
"encoding_format",
|
||||
"embeddingOption",
|
||||
"startSec",
|
||||
"input_type",
|
||||
"endSec",
|
||||
"segmentation",
|
||||
"embeddingType",
|
||||
"embeddingScope",
|
||||
"inferenceId",
|
||||
"media_source",
|
||||
"media_sources",
|
||||
)
|
||||
|
||||
|
||||
def drop_params_enabled(litellm_params: Mapping[str, object]) -> bool:
|
||||
return litellm.drop_params is True or litellm_params.get("drop_params") is True
|
||||
|
||||
|
||||
class TwelveLabsMarengoEmbeddingConfig:
|
||||
"""
|
||||
Reference - https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-marengo.html
|
||||
|
|
@ -115,23 +136,9 @@ class TwelveLabsMarengoEmbeddingConfig:
|
|||
self.is_marengo_3: Final = is_marengo_3_model(model)
|
||||
|
||||
def get_supported_openai_params(self) -> list[str]:
|
||||
return [
|
||||
"encoding_format",
|
||||
"textTruncate",
|
||||
"embeddingOption",
|
||||
"startSec",
|
||||
"lengthSec",
|
||||
"useFixedLengthSec",
|
||||
"minClipSec",
|
||||
"input_type",
|
||||
"endSec",
|
||||
"segmentation",
|
||||
"embeddingType",
|
||||
"embeddingScope",
|
||||
"inferenceId",
|
||||
"media_source",
|
||||
"media_sources",
|
||||
]
|
||||
if self.is_marengo_3:
|
||||
return list(MARENGO_SHARED_PARAMS)
|
||||
return [*MARENGO_SHARED_PARAMS, *MARENGO_2_7_ONLY_PARAMS]
|
||||
|
||||
def map_openai_params(self, non_default_params: dict, optional_params: dict) -> dict:
|
||||
for k, v in non_default_params.items():
|
||||
|
|
@ -179,6 +186,7 @@ class TwelveLabsMarengoEmbeddingConfig:
|
|||
async_invoke_route: bool = False,
|
||||
model_id: str | None = None,
|
||||
output_s3_uri: str | None = None,
|
||||
drop_params: bool = False,
|
||||
) -> TwelveLabsMarengoEmbeddingRequest | TwelveLabsMarengo3EmbeddingRequest | TwelveLabsAsyncInvokeRequest:
|
||||
"""
|
||||
Transform OpenAI-style input to TwelveLabs Marengo format/async-invoke format.
|
||||
|
|
@ -203,7 +211,9 @@ class TwelveLabsMarengoEmbeddingConfig:
|
|||
)
|
||||
|
||||
if self.is_marengo_3:
|
||||
marengo_3_request: Final = build_marengo_3_request(input=input, inference_params=inference_params)
|
||||
marengo_3_request: Final = build_marengo_3_request(
|
||||
input=input, inference_params=inference_params, drop_params=drop_params
|
||||
)
|
||||
if async_invoke_route and model_id:
|
||||
return self._wrap_async_invoke_request(
|
||||
model_input=marengo_3_request, model_id=model_id, output_s3_uri=output_s3_uri
|
||||
|
|
|
|||
|
|
@ -2,13 +2,16 @@ import json
|
|||
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm.llms.bedrock.common_utils import BedrockError
|
||||
from litellm.llms.bedrock.embed.twelvelabs_marengo_3_transformation import (
|
||||
MARENGO_2_7_ONLY_PARAMS,
|
||||
build_marengo_3_request,
|
||||
is_marengo_3_model,
|
||||
)
|
||||
from litellm.llms.bedrock.embed.twelvelabs_marengo_transformation import (
|
||||
TwelveLabsMarengoEmbeddingConfig,
|
||||
drop_params_enabled,
|
||||
)
|
||||
|
||||
MARENGO_3_BASE = "twelvelabs.marengo-embed-3-0-v1:0"
|
||||
|
|
@ -27,6 +30,7 @@ OUTPUT_S3_URI = "s3://out-bucket/marengo/"
|
|||
("async_invoke/twelvelabs.marengo-embed-3-0-v1:0", True),
|
||||
(MARENGO_27_US, False),
|
||||
("twelvelabs.marengo-embed-2-7-v1:0", False),
|
||||
("twelvelabs.marengo-embed-30-v1:0", False),
|
||||
(None, False),
|
||||
],
|
||||
)
|
||||
|
|
@ -266,3 +270,89 @@ def test_marengo_3_only_params_are_forwarded_by_map_openai_params():
|
|||
"embeddingScope": ["clip"],
|
||||
"inferenceId": "req-1",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params,problem",
|
||||
[
|
||||
(
|
||||
{"input_type": "clip"},
|
||||
"input_type: Input should be 'text', 'image', 'video', 'audio', 'text_image' or 'multi_input'",
|
||||
),
|
||||
({"input_type": "video", "embeddingOption": "visual"}, "embeddingOption: Input should be a valid tuple"),
|
||||
(
|
||||
{"input_type": "multi_input", "media_sources": ["not", "a", "mapping"]},
|
||||
"media_sources: Input should be a valid dictionary",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_invalid_marengo_3_params_name_the_field_and_the_reason(params, problem):
|
||||
with pytest.raises(BedrockError) as excinfo:
|
||||
build_marengo_3_request("s3://media/clip.mp4", params)
|
||||
assert excinfo.value.message == f"Invalid Marengo 3.0 parameters: {problem}"
|
||||
|
||||
|
||||
MARENGO_2_7_ONLY_VALUES = {"textTruncate": "end", "lengthSec": 5, "useFixedLengthSec": True, "minClipSec": 2}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", MARENGO_2_7_ONLY_PARAMS)
|
||||
def test_marengo_2_7_only_params_are_rejected_on_3_0_unless_dropped(name):
|
||||
params = {"input_type": "text", name: MARENGO_2_7_ONLY_VALUES[name]}
|
||||
with pytest.raises(BedrockError) as excinfo:
|
||||
build_marengo_3_request("hello", params)
|
||||
assert excinfo.value.status_code == 400
|
||||
assert excinfo.value.message == (
|
||||
f"Marengo 3.0 does not accept the Marengo 2.7 parameters {name}; set drop_params to drop them"
|
||||
)
|
||||
assert build_marengo_3_request("hello", params, drop_params=True) == {
|
||||
"inputType": "text",
|
||||
"text": {"inputText": "hello"},
|
||||
}
|
||||
|
||||
|
||||
def test_marengo_2_7_only_params_are_advertised_only_for_2_7():
|
||||
marengo_3 = TwelveLabsMarengoEmbeddingConfig(model=MARENGO_3_US).get_supported_openai_params()
|
||||
marengo_27 = TwelveLabsMarengoEmbeddingConfig(model=MARENGO_27_US).get_supported_openai_params()
|
||||
assert set(MARENGO_2_7_ONLY_PARAMS).isdisjoint(marengo_3)
|
||||
assert set(MARENGO_2_7_ONLY_PARAMS) <= set(marengo_27)
|
||||
assert set(marengo_3) <= set(marengo_27)
|
||||
|
||||
|
||||
def test_drop_params_comes_from_the_call_or_the_global(monkeypatch):
|
||||
monkeypatch.setattr(litellm, "drop_params", False)
|
||||
assert drop_params_enabled({}) is False
|
||||
assert drop_params_enabled({"drop_params": True}) is True
|
||||
monkeypatch.setattr(litellm, "drop_params", True)
|
||||
assert drop_params_enabled({}) is True
|
||||
|
||||
|
||||
def test_config_drops_marengo_2_7_only_params_only_when_asked():
|
||||
config = TwelveLabsMarengoEmbeddingConfig(model=MARENGO_3_US)
|
||||
with pytest.raises(BedrockError, match=r"Marengo 2\.7 parameters textTruncate"):
|
||||
config._transform_request("hello", {"textTruncate": "end"})
|
||||
assert config._transform_request("hello", {"textTruncate": "end"}, drop_params=True) == {
|
||||
"inputType": "text",
|
||||
"text": {"inputText": "hello"},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
{"input_type": "text"},
|
||||
{"input_type": "image"},
|
||||
{"input_type": "text_image", "media_source": DUCK_DATA_URL},
|
||||
{"input_type": "multi_input", "media_sources": {"bird": DUCK_DATA_URL}},
|
||||
],
|
||||
)
|
||||
def test_timed_media_options_are_rejected_on_untimed_input_types_unless_dropped(params):
|
||||
timed = {**params, "startSec": 0, "embeddingOption": ["visual"]}
|
||||
with pytest.raises(BedrockError) as excinfo:
|
||||
build_marengo_3_request(DUCK_DATA_URL, timed)
|
||||
assert excinfo.value.status_code == 400
|
||||
assert excinfo.value.message == (
|
||||
f"Input type '{params['input_type']}' does not accept startSec, embeddingOption; set drop_params to drop them"
|
||||
)
|
||||
assert build_marengo_3_request(DUCK_DATA_URL, timed, drop_params=True) == build_marengo_3_request(
|
||||
DUCK_DATA_URL, params
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue