mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-09 22:31:41 +00:00
feat(bedrock): add TwelveLabs Marengo Embed 3.0 embeddings
This commit is contained in:
parent
9d0c9b9382
commit
dc09d9e7cf
12 changed files with 961 additions and 13 deletions
|
|
@ -1370,6 +1370,7 @@ bedrock_embedding_models: Final[set] = set(
|
|||
"cohere.embed-multilingual-v3",
|
||||
"cohere.embed-v4:0",
|
||||
"twelvelabs.marengo-embed-2-7-v1:0",
|
||||
"twelvelabs.marengo-embed-3-0-v1:0",
|
||||
]
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -474,7 +474,7 @@ class BedrockEmbedding(BaseAWSLLM):
|
|||
elif provider == "twelvelabs":
|
||||
batch_data = []
|
||||
for i in input:
|
||||
twelvelabs_request = TwelveLabsMarengoEmbeddingConfig()._transform_request(
|
||||
twelvelabs_request = TwelveLabsMarengoEmbeddingConfig(model=model)._transform_request(
|
||||
input=i,
|
||||
inference_params=inference_params,
|
||||
async_invoke_route=has_async_invoke,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,204 @@
|
|||
"""
|
||||
Request builder for Bedrock TwelveLabs Marengo Embed 3.0, whose payload nests the input under a key named after
|
||||
``inputType`` instead of the flat 2.7 layout.
|
||||
|
||||
Docs - https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-marengo-3.html
|
||||
"""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from types import MappingProxyType
|
||||
from typing import Final, assert_never
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, TypeAdapter, ValidationError
|
||||
|
||||
from litellm.llms.bedrock.common_utils import BedrockError
|
||||
from litellm.types.llms.bedrock import (
|
||||
TWELVELABS_MARENGO_3_EMBEDDING_OPTIONS,
|
||||
TWELVELABS_MARENGO_3_EMBEDDING_SCOPES,
|
||||
TWELVELABS_MARENGO_3_EMBEDDING_TYPES,
|
||||
TWELVELABS_MARENGO_3_INPUT_TYPES,
|
||||
TwelveLabsMarengo3AudioRequest,
|
||||
TwelveLabsMarengo3EmbeddingRequest,
|
||||
TwelveLabsMarengo3ImageRequest,
|
||||
TwelveLabsMarengo3MultiInputRequest,
|
||||
TwelveLabsMarengo3NamedMediaSource,
|
||||
TwelveLabsMarengo3RequestBase,
|
||||
TwelveLabsMarengo3Segmentation,
|
||||
TwelveLabsMarengo3TextImageRequest,
|
||||
TwelveLabsMarengo3TextRequest,
|
||||
TwelveLabsMarengo3TimedMediaInput,
|
||||
TwelveLabsMarengo3TimedMediaOptions,
|
||||
TwelveLabsMarengo3VideoRequest,
|
||||
TwelveLabsMediaSource,
|
||||
TwelveLabsS3Location,
|
||||
)
|
||||
from litellm.utils import get_base64_str
|
||||
|
||||
MARENGO_3_MODEL_MARKER: Final = "marengo-embed-3"
|
||||
S3_URI_PREFIX: Final = "s3://"
|
||||
TIMED_MEDIA_OPTION_FIELDS: Final = MappingProxyType(
|
||||
{
|
||||
"startSec": True,
|
||||
"endSec": True,
|
||||
"segmentation": True,
|
||||
"embeddingOption": True,
|
||||
"embeddingType": True,
|
||||
"embeddingScope": True,
|
||||
}
|
||||
)
|
||||
TIMED_MEDIA_OPTIONS: Final = TypeAdapter(TwelveLabsMarengo3TimedMediaOptions)
|
||||
|
||||
|
||||
def is_marengo_3_model(model: str | None) -> bool:
|
||||
return MARENGO_3_MODEL_MARKER in (model or "")
|
||||
|
||||
|
||||
class Marengo3Params(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", frozen=True)
|
||||
|
||||
inputType: TWELVELABS_MARENGO_3_INPUT_TYPES | None = None
|
||||
input_type: TWELVELABS_MARENGO_3_INPUT_TYPES | None = None
|
||||
media_source: str | None = None
|
||||
media_sources: Mapping[str, str] | None = None
|
||||
bucketOwner: str | None = None
|
||||
startSec: float | None = None
|
||||
endSec: float | None = None
|
||||
segmentation: TwelveLabsMarengo3Segmentation | None = None
|
||||
embeddingOption: tuple[TWELVELABS_MARENGO_3_EMBEDDING_OPTIONS, ...] | None = None
|
||||
embeddingType: tuple[TWELVELABS_MARENGO_3_EMBEDDING_TYPES, ...] | None = None
|
||||
embeddingScope: tuple[TWELVELABS_MARENGO_3_EMBEDDING_SCOPES, ...] | None = None
|
||||
inferenceId: str | None = 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)
|
||||
)
|
||||
|
||||
|
||||
def _s3_location(uri: str, bucket_owner: str | None) -> TwelveLabsS3Location:
|
||||
if bucket_owner is None:
|
||||
unowned: Final[TwelveLabsS3Location] = {"uri": uri}
|
||||
return unowned
|
||||
owned: Final[TwelveLabsS3Location] = {"uri": uri, "bucketOwner": bucket_owner}
|
||||
return owned
|
||||
|
||||
|
||||
def _media_source(media: str, bucket_owner: str | None) -> TwelveLabsMediaSource:
|
||||
if not media.startswith(S3_URI_PREFIX):
|
||||
inline: Final[TwelveLabsMediaSource] = {"base64String": get_base64_str(media)}
|
||||
return inline
|
||||
remote: Final[TwelveLabsMediaSource] = {"s3Location": _s3_location(media, bucket_owner)}
|
||||
return remote
|
||||
|
||||
|
||||
def _named_media_source(name: str, media: str, bucket_owner: str | None) -> TwelveLabsMarengo3NamedMediaSource:
|
||||
named: Final[TwelveLabsMarengo3NamedMediaSource] = {
|
||||
"name": name,
|
||||
"mediaType": "image",
|
||||
**_media_source(media, bucket_owner),
|
||||
}
|
||||
return named
|
||||
|
||||
|
||||
def _timed_media_input(media: str, params: Marengo3Params) -> TwelveLabsMarengo3TimedMediaInput:
|
||||
timed: Final[TwelveLabsMarengo3TimedMediaInput] = {
|
||||
"mediaSource": _media_source(media, params.bucketOwner),
|
||||
**params.timed_media_options(),
|
||||
}
|
||||
return timed
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def _require(value: str | None, input_type: str, param_name: str) -> str:
|
||||
if value is None:
|
||||
raise BedrockError(status_code=400, message=f"Input type '{input_type}' requires the '{param_name}' parameter")
|
||||
return value
|
||||
|
||||
|
||||
def _require_media_sources(value: Mapping[str, str] | None) -> Mapping[str, str]:
|
||||
if not value:
|
||||
raise BedrockError(
|
||||
status_code=400,
|
||||
message="Input type 'multi_input' requires a non-empty 'media_sources' mapping of name to media",
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def _request_base(inference_id: str | None) -> TwelveLabsMarengo3RequestBase:
|
||||
if inference_id is None:
|
||||
anonymous: Final[TwelveLabsMarengo3RequestBase] = {}
|
||||
return anonymous
|
||||
identified: Final[TwelveLabsMarengo3RequestBase] = {"inferenceId": inference_id}
|
||||
return identified
|
||||
|
||||
|
||||
def build_marengo_3_request(input: str, inference_params: Mapping[str, object]) -> TwelveLabsMarengo3EmbeddingRequest:
|
||||
params: Final = _validated_params(inference_params)
|
||||
base: Final = _request_base(params.inferenceId)
|
||||
input_type: Final = params.resolved_input_type
|
||||
match input_type:
|
||||
case "text":
|
||||
text_request: Final[TwelveLabsMarengo3TextRequest] = {
|
||||
**base,
|
||||
"inputType": "text",
|
||||
"text": {"inputText": input},
|
||||
}
|
||||
return text_request
|
||||
case "image":
|
||||
image_request: Final[TwelveLabsMarengo3ImageRequest] = {
|
||||
**base,
|
||||
"inputType": "image",
|
||||
"image": {"mediaSource": _media_source(input, params.bucketOwner)},
|
||||
}
|
||||
return image_request
|
||||
case "video":
|
||||
video_request: Final[TwelveLabsMarengo3VideoRequest] = {
|
||||
**base,
|
||||
"inputType": "video",
|
||||
"video": _timed_media_input(input, params),
|
||||
}
|
||||
return video_request
|
||||
case "audio":
|
||||
audio_request: Final[TwelveLabsMarengo3AudioRequest] = {
|
||||
**base,
|
||||
"inputType": "audio",
|
||||
"audio": _timed_media_input(input, params),
|
||||
}
|
||||
return audio_request
|
||||
case "text_image":
|
||||
text_image_request: Final[TwelveLabsMarengo3TextImageRequest] = {
|
||||
**base,
|
||||
"inputType": "text_image",
|
||||
"text_image": {
|
||||
"inputText": input,
|
||||
"mediaSource": _media_source(
|
||||
_require(params.media_source, input_type, "media_source"), params.bucketOwner
|
||||
),
|
||||
},
|
||||
}
|
||||
return text_image_request
|
||||
case "multi_input":
|
||||
media_sources: Final = tuple(
|
||||
_named_media_source(name, media, params.bucketOwner)
|
||||
for name, media in _require_media_sources(params.media_sources).items()
|
||||
)
|
||||
multi_input_request: Final[TwelveLabsMarengo3MultiInputRequest] = {
|
||||
**base,
|
||||
"inputType": "multi_input",
|
||||
"multi_input": {"inputText": input, "mediaSources": media_sources}
|
||||
if input
|
||||
else {"mediaSources": media_sources},
|
||||
}
|
||||
return multi_input_request
|
||||
case _:
|
||||
assert_never(input_type)
|
||||
|
|
@ -4,13 +4,19 @@ Transformation logic from OpenAI /v1/embeddings format to Bedrock TwelveLabs Mar
|
|||
Why separate file? Make it easy to see how transformation works
|
||||
|
||||
Docs - https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-marengo.html
|
||||
Marengo 3.0 docs - https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-marengo-3.html
|
||||
"""
|
||||
|
||||
from typing import Final, cast
|
||||
|
||||
from litellm.llms.bedrock.embed.twelvelabs_marengo_3_transformation import (
|
||||
build_marengo_3_request,
|
||||
is_marengo_3_model,
|
||||
)
|
||||
from litellm.types.llms.bedrock import (
|
||||
TWELVELABS_EMBEDDING_INPUT_TYPES,
|
||||
TwelveLabsAsyncInvokeRequest,
|
||||
TwelveLabsMarengo3EmbeddingRequest,
|
||||
TwelveLabsMarengoEmbeddingRequest,
|
||||
TwelveLabsOutputDataConfig,
|
||||
TwelveLabsS3Location,
|
||||
|
|
@ -26,10 +32,13 @@ class TwelveLabsMarengoEmbeddingConfig:
|
|||
Supports text, image, video, and audio inputs.
|
||||
- InvokeModel: text and image inputs
|
||||
- StartAsyncInvoke: video, audio, image, and text inputs
|
||||
|
||||
Marengo 3.0 (model ids containing "marengo-embed-3") nests the input under a key named after inputType and
|
||||
adds the text_image and multi_input input types; that payload is built by build_marengo_3_request.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
def __init__(self, model: str | None = None) -> None:
|
||||
self.is_marengo_3: Final = is_marengo_3_model(model)
|
||||
|
||||
def get_supported_openai_params(self) -> list[str]:
|
||||
return [
|
||||
|
|
@ -41,13 +50,20 @@ class TwelveLabsMarengoEmbeddingConfig:
|
|||
"useFixedLengthSec",
|
||||
"minClipSec",
|
||||
"input_type",
|
||||
"endSec",
|
||||
"segmentation",
|
||||
"embeddingType",
|
||||
"embeddingScope",
|
||||
"inferenceId",
|
||||
"media_source",
|
||||
"media_sources",
|
||||
]
|
||||
|
||||
def map_openai_params(self, non_default_params: dict, optional_params: dict) -> dict:
|
||||
for k, v in non_default_params.items():
|
||||
if k == "encoding_format":
|
||||
# TwelveLabs doesn't have encoding_format, but we can map it to embeddingOption
|
||||
if v == "float":
|
||||
if v == "float" and not self.is_marengo_3:
|
||||
optional_params["embeddingOption"] = ["visual-text", "visual-image"]
|
||||
elif k == "textTruncate":
|
||||
optional_params["textTruncate"] = v
|
||||
|
|
@ -56,7 +72,19 @@ class TwelveLabsMarengoEmbeddingConfig:
|
|||
elif k == "input_type":
|
||||
# Map input_type to inputType for Bedrock
|
||||
optional_params["inputType"] = v
|
||||
elif k in ["startSec", "lengthSec", "useFixedLengthSec", "minClipSec"]:
|
||||
elif k in (
|
||||
"startSec",
|
||||
"lengthSec",
|
||||
"useFixedLengthSec",
|
||||
"minClipSec",
|
||||
"endSec",
|
||||
"segmentation",
|
||||
"embeddingType",
|
||||
"embeddingScope",
|
||||
"inferenceId",
|
||||
"media_source",
|
||||
"media_sources",
|
||||
):
|
||||
optional_params[k] = v
|
||||
return optional_params
|
||||
|
||||
|
|
@ -77,7 +105,7 @@ class TwelveLabsMarengoEmbeddingConfig:
|
|||
async_invoke_route: bool = False,
|
||||
model_id: str | None = None,
|
||||
output_s3_uri: str | None = None,
|
||||
) -> TwelveLabsMarengoEmbeddingRequest | TwelveLabsAsyncInvokeRequest:
|
||||
) -> TwelveLabsMarengoEmbeddingRequest | TwelveLabsMarengo3EmbeddingRequest | TwelveLabsAsyncInvokeRequest:
|
||||
"""
|
||||
Transform OpenAI-style input to TwelveLabs Marengo format/async-invoke format.
|
||||
|
||||
|
|
@ -87,20 +115,27 @@ class TwelveLabsMarengoEmbeddingConfig:
|
|||
- Video inputs (async-invoke only)
|
||||
- Audio inputs (async-invoke only)
|
||||
- S3 URLs for all media types (async-invoke only)
|
||||
- Marengo 3.0 only: text_image and multi_input inputs (nested payload)
|
||||
"""
|
||||
# Get input_type or default to "text"
|
||||
input_type: Final = cast(
|
||||
TWELVELABS_EMBEDDING_INPUT_TYPES,
|
||||
inference_params.get("inputType") or inference_params.get("input_type") or "text",
|
||||
)
|
||||
|
||||
# Validate that async-invoke is used for video/audio
|
||||
if input_type in ["video", "audio"] and not async_invoke_route:
|
||||
raise ValueError(
|
||||
f"Input type '{input_type}' requires async_invoke route. "
|
||||
f"Use model format: 'bedrock/async_invoke/model_id'"
|
||||
)
|
||||
|
||||
if self.is_marengo_3:
|
||||
marengo_3_request: Final = build_marengo_3_request(input=input, inference_params=inference_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
|
||||
)
|
||||
return marengo_3_request
|
||||
|
||||
transformed_request: Final[TwelveLabsMarengoEmbeddingRequest] = {"inputType": input_type}
|
||||
|
||||
if input_type == "text":
|
||||
|
|
@ -154,7 +189,7 @@ class TwelveLabsMarengoEmbeddingConfig:
|
|||
|
||||
def _wrap_async_invoke_request(
|
||||
self,
|
||||
model_input: TwelveLabsMarengoEmbeddingRequest,
|
||||
model_input: TwelveLabsMarengoEmbeddingRequest | TwelveLabsMarengo3EmbeddingRequest,
|
||||
model_id: str,
|
||||
output_s3_uri: str | None = None,
|
||||
) -> TwelveLabsAsyncInvokeRequest:
|
||||
|
|
|
|||
|
|
@ -690,6 +690,45 @@
|
|||
"supports_embedding_image_input": true,
|
||||
"supports_image_input": true
|
||||
},
|
||||
"twelvelabs.marengo-embed-3-0-v1:0": {
|
||||
"input_cost_per_token": 7e-05,
|
||||
"litellm_provider": "bedrock",
|
||||
"max_input_tokens": 500,
|
||||
"max_tokens": 500,
|
||||
"mode": "embedding",
|
||||
"output_cost_per_token": 0.0,
|
||||
"output_vector_size": 512,
|
||||
"supports_embedding_image_input": true,
|
||||
"supports_image_input": true
|
||||
},
|
||||
"us.twelvelabs.marengo-embed-3-0-v1:0": {
|
||||
"input_cost_per_token": 7e-05,
|
||||
"input_cost_per_video_per_second": 0.0007,
|
||||
"input_cost_per_audio_per_second": 0.00014,
|
||||
"input_cost_per_image": 0.0001,
|
||||
"litellm_provider": "bedrock",
|
||||
"max_input_tokens": 500,
|
||||
"max_tokens": 500,
|
||||
"mode": "embedding",
|
||||
"output_cost_per_token": 0.0,
|
||||
"output_vector_size": 512,
|
||||
"supports_embedding_image_input": true,
|
||||
"supports_image_input": true
|
||||
},
|
||||
"eu.twelvelabs.marengo-embed-3-0-v1:0": {
|
||||
"input_cost_per_token": 7e-05,
|
||||
"input_cost_per_video_per_second": 0.0007,
|
||||
"input_cost_per_audio_per_second": 0.00014,
|
||||
"input_cost_per_image": 0.0001,
|
||||
"litellm_provider": "bedrock",
|
||||
"max_input_tokens": 500,
|
||||
"max_tokens": 500,
|
||||
"mode": "embedding",
|
||||
"output_cost_per_token": 0.0,
|
||||
"output_vector_size": 512,
|
||||
"supports_embedding_image_input": true,
|
||||
"supports_image_input": true
|
||||
},
|
||||
"twelvelabs.pegasus-1-2-v1:0": {
|
||||
"input_cost_per_video_per_second": 0.00049,
|
||||
"output_cost_per_token": 7.5e-06,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import json
|
||||
from collections.abc import Sequence
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal, TypeAlias
|
||||
|
||||
from typing_extensions import ReadOnly, Required, TypedDict, override
|
||||
|
||||
|
|
@ -557,7 +557,7 @@ class AmazonTitanMultimodalEmbeddingResponse(TypedDict):
|
|||
message: str # Specifies any errors that occur during generation.
|
||||
|
||||
|
||||
# TwelveLabs Marengo Embed 2.7 types
|
||||
# TwelveLabs Marengo Embed types
|
||||
TWELVELABS_EMBEDDING_INPUT_TYPES = Literal["text", "image", "video", "audio"]
|
||||
TWELVELABS_EMBEDDING_OPTIONS = Literal["visual-text", "visual-image", "audio"]
|
||||
|
||||
|
|
@ -591,6 +591,113 @@ class TwelveLabsMarengoEmbeddingResponse(TypedDict):
|
|||
endSec: float
|
||||
|
||||
|
||||
TWELVELABS_MARENGO_3_INPUT_TYPES: TypeAlias = Literal["text", "image", "video", "audio", "text_image", "multi_input"]
|
||||
TWELVELABS_MARENGO_3_EMBEDDING_OPTIONS: TypeAlias = Literal["visual", "audio", "transcription"]
|
||||
TWELVELABS_MARENGO_3_EMBEDDING_TYPES: TypeAlias = Literal["separate_embedding", "fused_embedding"]
|
||||
TWELVELABS_MARENGO_3_EMBEDDING_SCOPES: TypeAlias = Literal["clip", "asset"]
|
||||
|
||||
|
||||
class TwelveLabsMarengo3FixedSegmentationConfig(TypedDict):
|
||||
durationSec: ReadOnly[int]
|
||||
|
||||
|
||||
class TwelveLabsMarengo3FixedSegmentation(TypedDict):
|
||||
method: ReadOnly[Literal["fixed"]]
|
||||
fixed: ReadOnly[TwelveLabsMarengo3FixedSegmentationConfig]
|
||||
|
||||
|
||||
class TwelveLabsMarengo3DynamicSegmentationConfig(TypedDict):
|
||||
minDurationSec: ReadOnly[int]
|
||||
|
||||
|
||||
class TwelveLabsMarengo3DynamicSegmentation(TypedDict):
|
||||
method: ReadOnly[Literal["dynamic"]]
|
||||
dynamic: ReadOnly[TwelveLabsMarengo3DynamicSegmentationConfig]
|
||||
|
||||
|
||||
TwelveLabsMarengo3Segmentation: TypeAlias = TwelveLabsMarengo3FixedSegmentation | TwelveLabsMarengo3DynamicSegmentation
|
||||
|
||||
|
||||
class TwelveLabsMarengo3TextInput(TypedDict):
|
||||
inputText: ReadOnly[str]
|
||||
|
||||
|
||||
class TwelveLabsMarengo3ImageInput(TypedDict):
|
||||
mediaSource: ReadOnly[TwelveLabsMediaSource]
|
||||
|
||||
|
||||
class TwelveLabsMarengo3TimedMediaOptions(TypedDict, total=False):
|
||||
startSec: ReadOnly[float]
|
||||
endSec: ReadOnly[float]
|
||||
segmentation: ReadOnly[TwelveLabsMarengo3Segmentation]
|
||||
embeddingOption: ReadOnly[Sequence[TWELVELABS_MARENGO_3_EMBEDDING_OPTIONS]]
|
||||
embeddingType: ReadOnly[Sequence[TWELVELABS_MARENGO_3_EMBEDDING_TYPES]]
|
||||
embeddingScope: ReadOnly[Sequence[TWELVELABS_MARENGO_3_EMBEDDING_SCOPES]]
|
||||
|
||||
|
||||
class TwelveLabsMarengo3TimedMediaInput(TwelveLabsMarengo3TimedMediaOptions):
|
||||
mediaSource: Required[ReadOnly[TwelveLabsMediaSource]]
|
||||
|
||||
|
||||
class TwelveLabsMarengo3TextImageInput(TypedDict):
|
||||
inputText: ReadOnly[str]
|
||||
mediaSource: ReadOnly[TwelveLabsMediaSource]
|
||||
|
||||
|
||||
class TwelveLabsMarengo3NamedMediaSource(TwelveLabsMediaSource):
|
||||
name: Required[ReadOnly[str]]
|
||||
mediaType: Required[ReadOnly[Literal["image"]]]
|
||||
|
||||
|
||||
class TwelveLabsMarengo3MultiInput(TypedDict, total=False):
|
||||
inputText: ReadOnly[str]
|
||||
mediaSources: Required[ReadOnly[Sequence[TwelveLabsMarengo3NamedMediaSource]]]
|
||||
|
||||
|
||||
class TwelveLabsMarengo3RequestBase(TypedDict, total=False):
|
||||
inferenceId: ReadOnly[str]
|
||||
|
||||
|
||||
class TwelveLabsMarengo3TextRequest(TwelveLabsMarengo3RequestBase):
|
||||
inputType: ReadOnly[Literal["text"]]
|
||||
text: ReadOnly[TwelveLabsMarengo3TextInput]
|
||||
|
||||
|
||||
class TwelveLabsMarengo3ImageRequest(TwelveLabsMarengo3RequestBase):
|
||||
inputType: ReadOnly[Literal["image"]]
|
||||
image: ReadOnly[TwelveLabsMarengo3ImageInput]
|
||||
|
||||
|
||||
class TwelveLabsMarengo3VideoRequest(TwelveLabsMarengo3RequestBase):
|
||||
inputType: ReadOnly[Literal["video"]]
|
||||
video: ReadOnly[TwelveLabsMarengo3TimedMediaInput]
|
||||
|
||||
|
||||
class TwelveLabsMarengo3AudioRequest(TwelveLabsMarengo3RequestBase):
|
||||
inputType: ReadOnly[Literal["audio"]]
|
||||
audio: ReadOnly[TwelveLabsMarengo3TimedMediaInput]
|
||||
|
||||
|
||||
class TwelveLabsMarengo3TextImageRequest(TwelveLabsMarengo3RequestBase):
|
||||
inputType: ReadOnly[Literal["text_image"]]
|
||||
text_image: ReadOnly[TwelveLabsMarengo3TextImageInput]
|
||||
|
||||
|
||||
class TwelveLabsMarengo3MultiInputRequest(TwelveLabsMarengo3RequestBase):
|
||||
inputType: ReadOnly[Literal["multi_input"]]
|
||||
multi_input: ReadOnly[TwelveLabsMarengo3MultiInput]
|
||||
|
||||
|
||||
TwelveLabsMarengo3EmbeddingRequest: TypeAlias = (
|
||||
TwelveLabsMarengo3TextRequest
|
||||
| TwelveLabsMarengo3ImageRequest
|
||||
| TwelveLabsMarengo3VideoRequest
|
||||
| TwelveLabsMarengo3AudioRequest
|
||||
| TwelveLabsMarengo3TextImageRequest
|
||||
| TwelveLabsMarengo3MultiInputRequest
|
||||
)
|
||||
|
||||
|
||||
class TwelveLabsS3OutputDataConfig(TypedDict):
|
||||
s3Uri: str
|
||||
|
||||
|
|
@ -601,7 +708,7 @@ class TwelveLabsOutputDataConfig(TypedDict):
|
|||
|
||||
class TwelveLabsAsyncInvokeRequest(TypedDict):
|
||||
modelId: str
|
||||
modelInput: TwelveLabsMarengoEmbeddingRequest
|
||||
modelInput: ReadOnly[TwelveLabsMarengoEmbeddingRequest | TwelveLabsMarengo3EmbeddingRequest]
|
||||
outputDataConfig: TwelveLabsOutputDataConfig
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3623,7 +3623,7 @@ def get_optional_params_embeddings(
|
|||
elif "cohere.embed" in model:
|
||||
object = litellm.BedrockCohereEmbeddingConfig()
|
||||
elif "twelvelabs" in model or "marengo" in model:
|
||||
object = litellm.TwelveLabsMarengoEmbeddingConfig()
|
||||
object = litellm.TwelveLabsMarengoEmbeddingConfig(model=model)
|
||||
elif "nova" in model.lower():
|
||||
object = litellm.AmazonNovaEmbeddingConfig()
|
||||
else: # unmapped model
|
||||
|
|
|
|||
|
|
@ -690,6 +690,45 @@
|
|||
"supports_embedding_image_input": true,
|
||||
"supports_image_input": true
|
||||
},
|
||||
"twelvelabs.marengo-embed-3-0-v1:0": {
|
||||
"input_cost_per_token": 7e-05,
|
||||
"litellm_provider": "bedrock",
|
||||
"max_input_tokens": 500,
|
||||
"max_tokens": 500,
|
||||
"mode": "embedding",
|
||||
"output_cost_per_token": 0.0,
|
||||
"output_vector_size": 512,
|
||||
"supports_embedding_image_input": true,
|
||||
"supports_image_input": true
|
||||
},
|
||||
"us.twelvelabs.marengo-embed-3-0-v1:0": {
|
||||
"input_cost_per_token": 7e-05,
|
||||
"input_cost_per_video_per_second": 0.0007,
|
||||
"input_cost_per_audio_per_second": 0.00014,
|
||||
"input_cost_per_image": 0.0001,
|
||||
"litellm_provider": "bedrock",
|
||||
"max_input_tokens": 500,
|
||||
"max_tokens": 500,
|
||||
"mode": "embedding",
|
||||
"output_cost_per_token": 0.0,
|
||||
"output_vector_size": 512,
|
||||
"supports_embedding_image_input": true,
|
||||
"supports_image_input": true
|
||||
},
|
||||
"eu.twelvelabs.marengo-embed-3-0-v1:0": {
|
||||
"input_cost_per_token": 7e-05,
|
||||
"input_cost_per_video_per_second": 0.0007,
|
||||
"input_cost_per_audio_per_second": 0.00014,
|
||||
"input_cost_per_image": 0.0001,
|
||||
"litellm_provider": "bedrock",
|
||||
"max_input_tokens": 500,
|
||||
"max_tokens": 500,
|
||||
"mode": "embedding",
|
||||
"output_cost_per_token": 0.0,
|
||||
"output_vector_size": 512,
|
||||
"supports_embedding_image_input": true,
|
||||
"supports_image_input": true
|
||||
},
|
||||
"twelvelabs.pegasus-1-2-v1:0": {
|
||||
"input_cost_per_video_per_second": 0.00049,
|
||||
"output_cost_per_token": 7.5e-06,
|
||||
|
|
|
|||
|
|
@ -184,6 +184,44 @@ class TestBedrockAsyncInvokeEmbedding:
|
|||
request_url = mock_post.call_args.kwargs.get("url", "")
|
||||
assert "/async-invoke" in request_url
|
||||
|
||||
def test_async_invoke_marengo_3_wraps_the_nested_payload_with_the_base_model_id(self):
|
||||
client = HTTPHandler()
|
||||
|
||||
with patch.object(client, "post") as mock_post:
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.text = json.dumps(async_invoke_response)
|
||||
mock_response.json = lambda: json.loads(mock_response.text)
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
response = litellm.embedding(
|
||||
model="bedrock/async_invoke/twelvelabs.marengo-embed-3-0-v1:0",
|
||||
input="s3://test-bucket/clip.mp4",
|
||||
client=client,
|
||||
aws_region_name="us-east-1",
|
||||
aws_bedrock_runtime_endpoint="https://bedrock-runtime.us-east-1.amazonaws.com",
|
||||
api_key="test-bearer-token-12345",
|
||||
input_type="video",
|
||||
embeddingOption=["visual", "audio"],
|
||||
segmentation={"method": "fixed", "fixed": {"durationSec": 6}},
|
||||
output_s3_uri="s3://test-bucket/async-invoke-output/",
|
||||
)
|
||||
|
||||
assert response._hidden_params._invocation_arn == async_invoke_response["invocationArn"]
|
||||
assert mock_post.call_args.kwargs["url"].endswith("/async-invoke")
|
||||
assert json.loads(mock_post.call_args.kwargs["data"]) == {
|
||||
"modelId": "twelvelabs.marengo-embed-3-0-v1:0",
|
||||
"modelInput": {
|
||||
"inputType": "video",
|
||||
"video": {
|
||||
"mediaSource": {"s3Location": {"uri": "s3://test-bucket/clip.mp4"}},
|
||||
"segmentation": {"method": "fixed", "fixed": {"durationSec": 6}},
|
||||
"embeddingOption": ["visual", "audio"],
|
||||
},
|
||||
},
|
||||
"outputDataConfig": {"s3OutputDataConfig": {"s3Uri": "s3://test-bucket/async-invoke-output/"}},
|
||||
}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_invoke_twelvelabs_embedding_async_with_mock(self):
|
||||
"""Test async invoke embedding with async calls."""
|
||||
|
|
|
|||
|
|
@ -1059,3 +1059,132 @@ def test_bedrock_embedding_bearer_token_never_runs_the_sigv4_credential_chain(mo
|
|||
|
||||
assert response.data[0]["embedding"] == titan_embedding_response["embedding"]
|
||||
assert mock_post.call_args.kwargs["headers"]["Authorization"] == "Bearer env-bearer-token-12345"
|
||||
|
||||
|
||||
marengo_3_embedding_response = {"data": [{"embedding": [0.01 * i for i in range(512)]}]}
|
||||
MARENGO_3_DUCK = "data:image/png;base64,ZHVjaw=="
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model,kwargs,expected_body",
|
||||
[
|
||||
(
|
||||
"bedrock/us.twelvelabs.marengo-embed-3-0-v1:0",
|
||||
{"input_type": "text"},
|
||||
{"inputType": "text", "text": {"inputText": "a duck on water"}},
|
||||
),
|
||||
(
|
||||
"bedrock/twelvelabs.marengo-embed-3-0-v1:0",
|
||||
{"input_type": "text"},
|
||||
{"inputType": "text", "text": {"inputText": "a duck on water"}},
|
||||
),
|
||||
(
|
||||
"bedrock/us.twelvelabs.marengo-embed-3-0-v1:0",
|
||||
{"input_type": "text_image", "media_source": MARENGO_3_DUCK},
|
||||
{
|
||||
"inputType": "text_image",
|
||||
"text_image": {"inputText": "a duck on water", "mediaSource": {"base64String": "ZHVjaw=="}},
|
||||
},
|
||||
),
|
||||
(
|
||||
"bedrock/us.twelvelabs.marengo-embed-3-0-v1:0",
|
||||
{"input_type": "multi_input", "media_sources": {"bird": MARENGO_3_DUCK}},
|
||||
{
|
||||
"inputType": "multi_input",
|
||||
"multi_input": {
|
||||
"inputText": "a duck on water",
|
||||
"mediaSources": [{"name": "bird", "mediaType": "image", "base64String": "ZHVjaw=="}],
|
||||
},
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_marengo_3_embedding_sends_the_nested_payload_and_parses_512_dims(model, kwargs, expected_body):
|
||||
client = HTTPHandler()
|
||||
|
||||
with patch.object(client, "post") as mock_post:
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.text = json.dumps(marengo_3_embedding_response)
|
||||
mock_response.json = lambda: json.loads(mock_response.text)
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
response = litellm.embedding(
|
||||
model=model,
|
||||
input="a duck on water",
|
||||
client=client,
|
||||
aws_region_name="us-east-1",
|
||||
api_key="test-bearer-token-12345",
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
assert json.loads(mock_post.call_args.kwargs["data"]) == expected_body
|
||||
assert mock_post.call_args.kwargs["url"].endswith(f"/model/{model.removeprefix('bedrock/').replace(':', '%3A')}/invoke")
|
||||
assert len(response.data[0]["embedding"]) == 512
|
||||
assert response.data[0]["embedding"][:2] == [0.0, 0.01]
|
||||
assert response.usage.prompt_tokens == 128
|
||||
|
||||
|
||||
def test_marengo_3_image_embedding_sends_the_media_under_the_image_key():
|
||||
client = HTTPHandler()
|
||||
|
||||
with patch.object(client, "post") as mock_post:
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.text = json.dumps(marengo_3_embedding_response)
|
||||
mock_response.json = lambda: json.loads(mock_response.text)
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
response = litellm.embedding(
|
||||
model="bedrock/us.twelvelabs.marengo-embed-3-0-v1:0",
|
||||
input=MARENGO_3_DUCK,
|
||||
client=client,
|
||||
aws_region_name="us-east-1",
|
||||
api_key="test-bearer-token-12345",
|
||||
input_type="image",
|
||||
)
|
||||
|
||||
assert json.loads(mock_post.call_args.kwargs["data"]) == {
|
||||
"inputType": "image",
|
||||
"image": {"mediaSource": {"base64String": "ZHVjaw=="}},
|
||||
}
|
||||
assert len(response.data[0]["embedding"]) == 512
|
||||
assert response.data[0]["embedding"][:2] == [0.0, 0.01]
|
||||
|
||||
|
||||
def test_marengo_2_7_embedding_keeps_the_flat_payload():
|
||||
client = HTTPHandler()
|
||||
|
||||
with patch.object(client, "post") as mock_post:
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.text = json.dumps(twelvelabs_embedding_response)
|
||||
mock_response.json = lambda: json.loads(mock_response.text)
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
response = litellm.embedding(
|
||||
model="bedrock/us.twelvelabs.marengo-embed-2-7-v1:0",
|
||||
input="a duck on water",
|
||||
client=client,
|
||||
aws_region_name="us-east-1",
|
||||
api_key="test-bearer-token-12345",
|
||||
input_type="text",
|
||||
)
|
||||
|
||||
assert json.loads(mock_post.call_args.kwargs["data"]) == {
|
||||
"inputType": "text",
|
||||
"inputText": "a duck on water",
|
||||
"textTruncate": "end",
|
||||
}
|
||||
assert response.data[0]["embedding"] == [0.1, 0.2, 0.3]
|
||||
|
||||
|
||||
def test_marengo_3_text_image_without_media_source_is_a_bad_request():
|
||||
with pytest.raises(litellm.BadRequestError, match=r"text_image.*media_source"):
|
||||
litellm.embedding(
|
||||
model="bedrock/us.twelvelabs.marengo-embed-3-0-v1:0",
|
||||
input="a duck on water",
|
||||
aws_region_name="us-east-1",
|
||||
api_key="test-bearer-token-12345",
|
||||
input_type="text_image",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,268 @@
|
|||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from litellm.llms.bedrock.common_utils import BedrockError
|
||||
from litellm.llms.bedrock.embed.twelvelabs_marengo_3_transformation import (
|
||||
build_marengo_3_request,
|
||||
is_marengo_3_model,
|
||||
)
|
||||
from litellm.llms.bedrock.embed.twelvelabs_marengo_transformation import (
|
||||
TwelveLabsMarengoEmbeddingConfig,
|
||||
)
|
||||
|
||||
MARENGO_3_BASE = "twelvelabs.marengo-embed-3-0-v1:0"
|
||||
MARENGO_3_US = "us.twelvelabs.marengo-embed-3-0-v1:0"
|
||||
MARENGO_27_US = "us.twelvelabs.marengo-embed-2-7-v1:0"
|
||||
DUCK_DATA_URL = "data:image/png;base64,ZHVjaw=="
|
||||
OUTPUT_S3_URI = "s3://out-bucket/marengo/"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model,expected",
|
||||
[
|
||||
(MARENGO_3_BASE, True),
|
||||
(MARENGO_3_US, True),
|
||||
("eu.twelvelabs.marengo-embed-3-0-v1:0", True),
|
||||
("async_invoke/twelvelabs.marengo-embed-3-0-v1:0", True),
|
||||
(MARENGO_27_US, False),
|
||||
("twelvelabs.marengo-embed-2-7-v1:0", False),
|
||||
(None, False),
|
||||
],
|
||||
)
|
||||
def test_is_marengo_3_model(model, expected):
|
||||
assert is_marengo_3_model(model) is expected
|
||||
|
||||
|
||||
def wire(request: object) -> object:
|
||||
return json.loads(json.dumps(request))
|
||||
|
||||
|
||||
def test_text_request_nests_input_text_under_text():
|
||||
assert build_marengo_3_request("a dog on the beach", {"input_type": "text"}) == {
|
||||
"inputType": "text",
|
||||
"text": {"inputText": "a dog on the beach"},
|
||||
}
|
||||
|
||||
|
||||
def test_missing_input_type_defaults_to_text():
|
||||
assert build_marengo_3_request("hello", {})["inputType"] == "text"
|
||||
|
||||
|
||||
def test_camel_case_input_type_wins_over_snake_case():
|
||||
request = build_marengo_3_request(DUCK_DATA_URL, {"inputType": "image", "input_type": "text"})
|
||||
assert request["inputType"] == "image"
|
||||
|
||||
|
||||
def test_image_request_strips_data_url_prefix():
|
||||
assert build_marengo_3_request(DUCK_DATA_URL, {"input_type": "image"}) == {
|
||||
"inputType": "image",
|
||||
"image": {"mediaSource": {"base64String": "ZHVjaw=="}},
|
||||
}
|
||||
|
||||
|
||||
def test_image_request_from_s3_carries_bucket_owner():
|
||||
request = build_marengo_3_request("s3://media/duck.png", {"input_type": "image", "bucketOwner": "123456789012"})
|
||||
assert request == {
|
||||
"inputType": "image",
|
||||
"image": {"mediaSource": {"s3Location": {"uri": "s3://media/duck.png", "bucketOwner": "123456789012"}}},
|
||||
}
|
||||
|
||||
|
||||
def test_s3_media_without_bucket_owner_omits_the_key():
|
||||
request = build_marengo_3_request("s3://media/duck.png", {"input_type": "image"})
|
||||
assert request["image"]["mediaSource"] == {"s3Location": {"uri": "s3://media/duck.png"}}
|
||||
|
||||
|
||||
def test_text_image_request_pairs_text_with_media_source():
|
||||
request = build_marengo_3_request(
|
||||
"a duck", {"input_type": "text_image", "media_source": DUCK_DATA_URL, "output_s3_uri": OUTPUT_S3_URI}
|
||||
)
|
||||
assert request == {
|
||||
"inputType": "text_image",
|
||||
"text_image": {"inputText": "a duck", "mediaSource": {"base64String": "ZHVjaw=="}},
|
||||
}
|
||||
|
||||
|
||||
def test_text_image_request_requires_media_source():
|
||||
with pytest.raises(BedrockError, match=r"text_image.*media_source") as excinfo:
|
||||
build_marengo_3_request("a duck", {"input_type": "text_image"})
|
||||
assert excinfo.value.status_code == 400
|
||||
|
||||
|
||||
def test_multi_input_request_names_each_media_source():
|
||||
request = build_marengo_3_request(
|
||||
"a photo of <@bird> next to <@dog>",
|
||||
{
|
||||
"input_type": "multi_input",
|
||||
"media_sources": {"bird": DUCK_DATA_URL, "dog": "s3://media/dog.png"},
|
||||
"bucketOwner": "123456789012",
|
||||
},
|
||||
)
|
||||
assert wire(request) == {
|
||||
"inputType": "multi_input",
|
||||
"multi_input": {
|
||||
"inputText": "a photo of <@bird> next to <@dog>",
|
||||
"mediaSources": [
|
||||
{"name": "bird", "mediaType": "image", "base64String": "ZHVjaw=="},
|
||||
{
|
||||
"name": "dog",
|
||||
"mediaType": "image",
|
||||
"s3Location": {"uri": "s3://media/dog.png", "bucketOwner": "123456789012"},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_multi_input_without_text_omits_input_text():
|
||||
request = build_marengo_3_request("", {"input_type": "multi_input", "media_sources": {"bird": DUCK_DATA_URL}})
|
||||
assert "inputText" not in request["multi_input"]
|
||||
assert request["multi_input"]["mediaSources"][0]["name"] == "bird"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("params", [{"input_type": "multi_input"}, {"input_type": "multi_input", "media_sources": {}}])
|
||||
def test_multi_input_request_requires_media_sources(params):
|
||||
with pytest.raises(BedrockError, match=r"multi_input.*media_sources") as excinfo:
|
||||
build_marengo_3_request("<@bird>", params)
|
||||
assert excinfo.value.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.parametrize("input_type", ["video", "audio"])
|
||||
def test_timed_media_request_nests_every_option_under_the_media_key(input_type):
|
||||
request = build_marengo_3_request(
|
||||
"s3://media/clip.mp4",
|
||||
{
|
||||
"input_type": input_type,
|
||||
"startSec": 2,
|
||||
"endSec": 12.5,
|
||||
"segmentation": {"method": "dynamic", "dynamic": {"minDurationSec": 4}},
|
||||
"embeddingOption": ["visual", "audio"],
|
||||
"embeddingType": ["fused_embedding"],
|
||||
"embeddingScope": ["clip", "asset"],
|
||||
"inferenceId": "req-42",
|
||||
},
|
||||
)
|
||||
assert wire(request) == {
|
||||
"inputType": input_type,
|
||||
input_type: {
|
||||
"mediaSource": {"s3Location": {"uri": "s3://media/clip.mp4"}},
|
||||
"startSec": 2.0,
|
||||
"endSec": 12.5,
|
||||
"segmentation": {"method": "dynamic", "dynamic": {"minDurationSec": 4}},
|
||||
"embeddingOption": ["visual", "audio"],
|
||||
"embeddingType": ["fused_embedding"],
|
||||
"embeddingScope": ["clip", "asset"],
|
||||
},
|
||||
"inferenceId": "req-42",
|
||||
}
|
||||
|
||||
|
||||
def test_timed_media_request_without_options_carries_only_the_media_source():
|
||||
request = build_marengo_3_request("s3://media/clip.mp4", {"input_type": "video"})
|
||||
assert request["video"] == {"mediaSource": {"s3Location": {"uri": "s3://media/clip.mp4"}}}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
{"input_type": "clip"},
|
||||
{"input_type": "video", "embeddingOption": ["visual-text"]},
|
||||
{"input_type": "video", "segmentation": {"method": "fixed", "dynamic": {"minDurationSec": 4}}},
|
||||
{"input_type": "multi_input", "media_sources": ["not", "a", "mapping"]},
|
||||
],
|
||||
)
|
||||
def test_invalid_marengo_3_params_are_rejected_before_the_request_is_sent(params):
|
||||
with pytest.raises(BedrockError, match=r"Invalid Marengo 3\.0 parameters") as excinfo:
|
||||
build_marengo_3_request("s3://media/clip.mp4", params)
|
||||
assert excinfo.value.status_code == 400
|
||||
|
||||
|
||||
def test_config_sends_the_nested_payload_for_marengo_3_and_the_flat_one_for_2_7():
|
||||
nested = TwelveLabsMarengoEmbeddingConfig(model=MARENGO_3_US)._transform_request(
|
||||
input="hello", inference_params={"input_type": "text"}
|
||||
)
|
||||
flat = TwelveLabsMarengoEmbeddingConfig(model=MARENGO_27_US)._transform_request(
|
||||
input="hello", inference_params={"input_type": "text"}
|
||||
)
|
||||
assert nested == {"inputType": "text", "text": {"inputText": "hello"}}
|
||||
assert flat == {"inputType": "text", "inputText": "hello", "textTruncate": "end"}
|
||||
|
||||
|
||||
def test_config_without_a_model_keeps_the_2_7_payload():
|
||||
request = TwelveLabsMarengoEmbeddingConfig()._transform_request(input="hello", inference_params={})
|
||||
assert request == {"inputType": "text", "inputText": "hello", "textTruncate": "end"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("input_type", ["video", "audio"])
|
||||
def test_marengo_3_video_and_audio_still_require_the_async_route(input_type):
|
||||
with pytest.raises(ValueError, match=f"Input type '{input_type}' requires async_invoke route"):
|
||||
TwelveLabsMarengoEmbeddingConfig(model=MARENGO_3_BASE)._transform_request(
|
||||
input="s3://media/clip.mp4", inference_params={"input_type": input_type}
|
||||
)
|
||||
|
||||
|
||||
def test_marengo_3_async_invoke_wraps_the_nested_payload_with_the_base_model_id():
|
||||
request = TwelveLabsMarengoEmbeddingConfig(model=MARENGO_3_BASE)._transform_request(
|
||||
input="s3://media/clip.mp4",
|
||||
inference_params={"input_type": "video", "embeddingOption": ["visual"], "output_s3_uri": OUTPUT_S3_URI},
|
||||
async_invoke_route=True,
|
||||
model_id="async_invoke%2Ftwelvelabs.marengo-embed-3-0-v1%3A0",
|
||||
output_s3_uri=OUTPUT_S3_URI,
|
||||
)
|
||||
assert wire(request) == {
|
||||
"modelId": MARENGO_3_BASE,
|
||||
"modelInput": {
|
||||
"inputType": "video",
|
||||
"video": {"mediaSource": {"s3Location": {"uri": "s3://media/clip.mp4"}}, "embeddingOption": ["visual"]},
|
||||
},
|
||||
"outputDataConfig": {"s3OutputDataConfig": {"s3Uri": OUTPUT_S3_URI}},
|
||||
}
|
||||
|
||||
|
||||
def test_marengo_3_async_invoke_requires_an_output_s3_uri():
|
||||
with pytest.raises(ValueError, match="output_s3_uri cannot be empty"):
|
||||
TwelveLabsMarengoEmbeddingConfig(model=MARENGO_3_BASE)._transform_request(
|
||||
input="hello",
|
||||
inference_params={"input_type": "text"},
|
||||
async_invoke_route=True,
|
||||
model_id=MARENGO_3_BASE,
|
||||
output_s3_uri="",
|
||||
)
|
||||
|
||||
|
||||
def test_encoding_format_float_no_longer_injects_2_7_embedding_options_for_marengo_3():
|
||||
marengo_3 = TwelveLabsMarengoEmbeddingConfig(model=MARENGO_3_US).map_openai_params(
|
||||
non_default_params={"encoding_format": "float"}, optional_params={}
|
||||
)
|
||||
marengo_27 = TwelveLabsMarengoEmbeddingConfig(model=MARENGO_27_US).map_openai_params(
|
||||
non_default_params={"encoding_format": "float"}, optional_params={}
|
||||
)
|
||||
assert marengo_3 == {}
|
||||
assert marengo_27 == {"embeddingOption": ["visual-text", "visual-image"]}
|
||||
|
||||
|
||||
def test_marengo_3_only_params_are_forwarded_by_map_openai_params():
|
||||
mapped = TwelveLabsMarengoEmbeddingConfig(model=MARENGO_3_US).map_openai_params(
|
||||
non_default_params={
|
||||
"input_type": "text_image",
|
||||
"media_source": DUCK_DATA_URL,
|
||||
"media_sources": {"bird": DUCK_DATA_URL},
|
||||
"endSec": 5,
|
||||
"segmentation": {"method": "fixed", "fixed": {"durationSec": 6}},
|
||||
"embeddingType": ["separate_embedding"],
|
||||
"embeddingScope": ["clip"],
|
||||
"inferenceId": "req-1",
|
||||
},
|
||||
optional_params={},
|
||||
)
|
||||
assert mapped == {
|
||||
"inputType": "text_image",
|
||||
"media_source": DUCK_DATA_URL,
|
||||
"media_sources": {"bird": DUCK_DATA_URL},
|
||||
"endSec": 5,
|
||||
"segmentation": {"method": "fixed", "fixed": {"durationSec": 6}},
|
||||
"embeddingType": ["separate_embedding"],
|
||||
"embeddingScope": ["clip"],
|
||||
"inferenceId": "req-1",
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm.constants import bedrock_embedding_models
|
||||
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
|
||||
from litellm.types.utils import Usage
|
||||
|
||||
REPO_ROOT = Path(__file__).parents[2]
|
||||
MAIN_PATH = REPO_ROOT / "model_prices_and_context_window.json"
|
||||
BACKUP_PATH = REPO_ROOT / "litellm" / "model_prices_and_context_window_backup.json"
|
||||
|
||||
BASE_MODEL = "twelvelabs.marengo-embed-3-0-v1:0"
|
||||
PROFILE_MODELS = ("us.twelvelabs.marengo-embed-3-0-v1:0", "eu.twelvelabs.marengo-embed-3-0-v1:0")
|
||||
ALL_MODELS = (BASE_MODEL, *PROFILE_MODELS)
|
||||
|
||||
TEXT_REQUEST_COST = 7e-05
|
||||
IMAGE_REQUEST_COST = 0.0001
|
||||
VIDEO_COST_PER_SECOND = 0.0007
|
||||
AUDIO_COST_PER_SECOND = 0.00014
|
||||
|
||||
|
||||
def _load(path):
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", ALL_MODELS)
|
||||
def test_marengo_embed_3_specs(model):
|
||||
info = _load(MAIN_PATH).get(model)
|
||||
assert info is not None, f"{model} missing from model_prices_and_context_window.json"
|
||||
|
||||
assert info["litellm_provider"] == "bedrock"
|
||||
assert info["mode"] == "embedding"
|
||||
assert info["input_cost_per_token"] == TEXT_REQUEST_COST
|
||||
assert info["output_cost_per_token"] == 0.0
|
||||
assert info["max_input_tokens"] == 500
|
||||
assert info["max_tokens"] == 500
|
||||
assert info["output_vector_size"] == 512
|
||||
assert info["supports_embedding_image_input"] is True
|
||||
assert info["supports_image_input"] is True
|
||||
assert "deprecation_date" not in info
|
||||
|
||||
routed_model, provider, _, _ = get_llm_provider(model=f"bedrock/{model}")
|
||||
assert routed_model == model
|
||||
assert provider == "bedrock"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", PROFILE_MODELS)
|
||||
def test_marengo_embed_3_inference_profiles_price_image_video_and_audio(model):
|
||||
info = _load(MAIN_PATH)[model]
|
||||
assert info["input_cost_per_image"] == IMAGE_REQUEST_COST
|
||||
assert info["input_cost_per_video_per_second"] == VIDEO_COST_PER_SECOND
|
||||
assert info["input_cost_per_audio_per_second"] == AUDIO_COST_PER_SECOND
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", ALL_MODELS)
|
||||
def test_marengo_embed_3_is_visible_to_callers(model, local_model_cost_map):
|
||||
info = litellm.get_model_info(model=model, custom_llm_provider="bedrock")
|
||||
assert info["mode"] == "embedding"
|
||||
assert info["output_vector_size"] == 512
|
||||
assert info["max_input_tokens"] == 500
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", ALL_MODELS)
|
||||
def test_marengo_embed_3_text_request_is_billed(model, local_model_cost_map):
|
||||
usage = Usage(prompt_tokens=128, completion_tokens=0, total_tokens=128)
|
||||
prompt_cost, completion_cost = litellm.cost_per_token(
|
||||
model=model, usage_object=usage, custom_llm_provider="bedrock"
|
||||
)
|
||||
assert prompt_cost == pytest.approx(128 * TEXT_REQUEST_COST)
|
||||
assert completion_cost == 0.0
|
||||
|
||||
|
||||
def test_marengo_embed_3_is_a_known_bedrock_embedding_model():
|
||||
assert BASE_MODEL in bedrock_embedding_models
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", ALL_MODELS)
|
||||
def test_backup_matches_main(model):
|
||||
main_cost = _load(MAIN_PATH)
|
||||
backup_cost = _load(BACKUP_PATH)
|
||||
|
||||
assert model in main_cost, f"{model} missing from model_prices_and_context_window.json"
|
||||
assert model in backup_cost, f"{model} missing from model_prices_and_context_window_backup.json"
|
||||
assert backup_cost[model] == main_cost[model], f"{model} differs between main and backup model cost maps"
|
||||
Loading…
Add table
Reference in a new issue