Files
litellm/tests/llm_translation/test_bedrock_embedding.py
T
Ishaan Jaff 061b7b2713 [Fix] Bedrock Embeddings - Ensure correct aws_region is used when provided dynamically (#16547)
* test_bedrock_embedding_uses_correct_region_when_specified

* fix aws_region_name in bedrock embeddings
2025-11-12 12:38:58 -08:00

451 lines
18 KiB
Python

import json
import os
import sys
from datetime import datetime
from unittest.mock import AsyncMock, Mock, patch
import pytest
import base64
import httpx
sys.path.insert(
0, os.path.abspath("../..")
) # Adds the parent directory to the system path
import litellm
from litellm.llms.custom_httpx.http_handler import HTTPHandler, AsyncHTTPHandler
titan_embedding_response = {"embedding": [0.1, 0.2, 0.3], "inputTextTokenCount": 10}
cohere_embedding_response = {"embeddings": [[0.1, 0.2, 0.3]], "inputTextTokenCount": 10}
img_base_64 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkBAMAAACCzIhnAAAAG1BMVEURAAD///+ln5/h39/Dv79qX18uHx+If39MPz9oMSdmAAAACXBIWXMAAA7EAAAOxAGVKw4bAAABB0lEQVRYhe2SzWrEIBCAh2A0jxEs4j6GLDS9hqWmV5Flt0cJS+lRwv742DXpEjY1kOZW6HwHFZnPmVEBEARBEARB/jd0KYA/bcUYbPrRLh6amXHJ/K+ypMoyUaGthILzw0l+xI0jsO7ZcmCcm4ILd+QuVYgpHOmDmz6jBeJImdcUCmeBqQpuqRIbVmQsLCrAalrGpfoEqEogqbLTWuXCPCo+Ki1XGqgQ+jVVuhB8bOaHkvmYuzm/b0KYLWwoK58oFqi6XfxQ4Uz7d6WeKpna6ytUs5e8betMcqAv5YPC5EZB2Lm9FIn0/VP6R58+/GEY1X1egVoZ/3bt/EqF6malgSAIgiDIH+QL41409QMY0LMAAAAASUVORK5CYII="
@pytest.mark.parametrize(
"model,input_type,embed_response",
[
(
"bedrock/amazon.titan-embed-text-v1",
"text",
titan_embedding_response,
), # V1 text model
(
"bedrock/amazon.titan-embed-text-v2:0",
"text",
titan_embedding_response,
), # V2 text model
(
"bedrock/amazon.titan-embed-image-v1",
"image",
titan_embedding_response,
), # Image model
(
"bedrock/cohere.embed-english-v3",
"text",
cohere_embedding_response,
), # Cohere English
(
"bedrock/cohere.embed-multilingual-v3",
"text",
cohere_embedding_response,
), # Cohere Multilingual
],
)
def test_bedrock_embedding_models(model, input_type, embed_response):
"""Test embedding functionality for all Bedrock models with different input types"""
litellm.set_verbose = True
client = HTTPHandler()
with patch.object(client, "post") as mock_post:
mock_response = Mock()
mock_response.status_code = 200
mock_response.text = json.dumps(embed_response)
mock_response.json = lambda: json.loads(mock_response.text)
mock_post.return_value = mock_response
# Prepare input based on type
input_data = (
img_base_64 if input_type == "image" else "Hello world from litellm"
)
try:
response = litellm.embedding(
model=model,
input=input_data,
client=client,
aws_region_name="us-west-2",
aws_bedrock_runtime_endpoint="https://bedrock-runtime.us-west-2.amazonaws.com",
)
# Verify response structure
assert isinstance(response, litellm.EmbeddingResponse)
print(response.data)
assert isinstance(response.data[0]["embedding"], list)
assert len(response.data[0]["embedding"]) == 3 # Based on mock response
# Fetch request body
request_data = json.loads(mock_post.call_args.kwargs["data"])
# Verify AWS params are not in request body
aws_params = ["aws_region_name", "aws_bedrock_runtime_endpoint"]
for param in aws_params:
assert (
param not in request_data
), f"AWS param {param} should not be in request body"
except Exception as e:
pytest.fail(f"Error occurred: {e}")
def test_e2e_bedrock_embedding():
"""
Test text embedding with TwelveLabs Marengo.
Validates that the transformation properly extracts embedding data from TwelveLabs response format.
"""
print("Testing text embedding...")
original_region_name = os.environ.get("AWS_REGION_NAME")
os.environ["AWS_REGION_NAME"] = "us-east-1"
litellm._turn_on_debug()
response = litellm.embedding(
model="bedrock/us.twelvelabs.marengo-embed-2-7-v1:0",
input=["Hello world from LiteLLM with TwelveLabs Marengo!"],
)
# Validate response structure
assert isinstance(
response, litellm.EmbeddingResponse
), "Response should be EmbeddingResponse type"
assert hasattr(response, "data"), "Response should have 'data' attribute"
assert len(response.data) > 0, "Response data should not be empty"
# Validate first embedding
embedding_obj = response.data[0]
assert hasattr(
embedding_obj, "embedding"
), "Embedding object should have 'embedding' attribute"
assert isinstance(
embedding_obj.embedding, list
), "Embedding should be a list of floats"
assert len(embedding_obj.embedding) > 0, "Embedding vector should not be empty"
assert all(
isinstance(x, (int, float)) for x in embedding_obj.embedding
), "All embedding values should be numeric"
# Validate embedding properties
assert embedding_obj.index == 0, "First embedding should have index 0"
assert (
embedding_obj.object == "embedding"
), "Embedding object type should be 'embedding'"
# Validate usage information
assert hasattr(response, "usage"), "Response should have usage information"
assert response.usage is not None, "Usage should not be None"
assert response.usage.total_tokens >= 0, "Total tokens should be non-negative"
print(
f"Text embedding successful! Vector size: {len(embedding_obj.embedding)}, Response: {response}"
)
# Restore original region name
if original_region_name:
os.environ["AWS_REGION_NAME"] = original_region_name
def test_e2e_bedrock_embedding_image_twelvelabs_marengo():
"""
Test image embedding with TwelveLabs Marengo.
Validates that the transformation properly extracts embedding data from TwelveLabs response format for images.
"""
print("Testing image embedding...")
original_region_name = os.environ.get("AWS_REGION_NAME")
os.environ["AWS_REGION_NAME"] = "us-east-1"
litellm._turn_on_debug()
# Load duck.png and convert to base64
duck_img_path = os.path.join(os.path.dirname(__file__), "duck.png")
with open(duck_img_path, "rb") as img_file:
duck_img_data = base64.b64encode(img_file.read()).decode("utf-8")
duck_img_base64 = f"data:image/png;base64,{duck_img_data}"
response = litellm.embedding(
model="bedrock/us.twelvelabs.marengo-embed-2-7-v1:0",
input=[duck_img_base64],
aws_region_name="us-east-1",
input_type="image",
)
# Validate response structure
assert isinstance(
response, litellm.EmbeddingResponse
), "Response should be EmbeddingResponse type"
assert hasattr(response, "data"), "Response should have 'data' attribute"
assert len(response.data) > 0, "Response data should not be empty"
# Validate first embedding
embedding_obj = response.data[0]
assert hasattr(
embedding_obj, "embedding"
), "Embedding object should have 'embedding' attribute"
assert isinstance(
embedding_obj.embedding, list
), "Embedding should be a list of floats"
assert len(embedding_obj.embedding) > 0, "Embedding vector should not be empty"
assert all(
isinstance(x, (int, float)) for x in embedding_obj.embedding
), "All embedding values should be numeric"
# Validate embedding properties
assert embedding_obj.index == 0, "First embedding should have index 0"
assert (
embedding_obj.object == "embedding"
), "Embedding object type should be 'embedding'"
# Validate usage information
assert hasattr(response, "usage"), "Response should have usage information"
assert response.usage is not None, "Usage should not be None"
assert response.usage.total_tokens >= 0, "Total tokens should be non-negative"
# TwelveLabs Marengo should return 1024-dimensional embeddings
expected_dimension = 1024
assert (
len(embedding_obj.embedding) == expected_dimension
), f"TwelveLabs Marengo should return {expected_dimension}-dimensional embeddings, got {len(embedding_obj.embedding)}"
print(
f"Image embedding successful! Vector size: {len(embedding_obj.embedding)}, Response: {response}"
)
# Restore original region name
if original_region_name:
os.environ["AWS_REGION_NAME"] = original_region_name
def test_e2e_bedrock_async_invoke_embedding_twelvelabs_marengo():
"""
Test async invoke embedding with TwelveLabs Marengo.
Validates that async invoke responses include job ID in hidden parameters.
"""
print("Testing async invoke embedding...")
original_region_name = os.environ.get("AWS_REGION_NAME")
os.environ["AWS_REGION_NAME"] = "us-east-1"
litellm._turn_on_debug()
# Mock the HTTP call to return async invoke response
with patch(
"litellm.llms.bedrock.embed.embedding.BedrockEmbedding._make_sync_call"
) as mock_call:
mock_call.return_value = {
"invocationArn": "arn:aws:bedrock:us-east-1:123456789012:async-invoke/test-job-123"
}
response = litellm.embedding(
model="bedrock/async_invoke/us.twelvelabs.marengo-embed-2-7-v1:0",
input=["Hello world from LiteLLM async invoke!"],
aws_region_name="us-east-1",
inputType="text",
output_s3_uri="s3://test-bucket/async-invoke-output/",
)
# Validate response structure
assert isinstance(
response, litellm.EmbeddingResponse
), "Response should be EmbeddingResponse type"
assert hasattr(
response, "_hidden_params"
), "Response should have _hidden_params"
assert response._hidden_params is not None, "Hidden params should not be None"
# Validate hidden params contain invocation ARN
assert hasattr(
response._hidden_params, "_invocation_arn"
), "Hidden params should have _invocation_arn"
assert (
response._hidden_params._invocation_arn
== "arn:aws:bedrock:us-east-1:123456789012:async-invoke/test-job-123"
), "Invocation ARN should be preserved"
# Validate embedding structure
assert len(response.data) == 1, "Should have one embedding"
assert (
response.data[0].object == "embedding"
), "Embedding object should be 'embedding'"
assert (
response.data[0].embedding == []
), "Embedding should be empty for async jobs"
print(
f"Async invoke embedding successful! Invocation ARN: {response._hidden_params._invocation_arn}"
)
# Restore original region name
if original_region_name:
os.environ["AWS_REGION_NAME"] = original_region_name
@pytest.mark.asyncio
async def test_e2e_bedrock_async_invoke_embedding_async_twelvelabs_marengo():
"""
Test async invoke embedding with async calls.
Validates that async invoke responses work with aembedding.
"""
print("Testing async invoke embedding with async calls...")
original_region_name = os.environ.get("AWS_REGION_NAME")
os.environ["AWS_REGION_NAME"] = "us-east-1"
litellm._turn_on_debug()
# Mock the async HTTP call to return async invoke response
with patch(
"litellm.llms.bedrock.embed.embedding.BedrockEmbedding._make_async_call"
) as mock_call:
mock_call.return_value = {
"invocationArn": "arn:aws:bedrock:us-east-1:123456789012:async-invoke/test-async-job-456"
}
response = await litellm.aembedding(
model="bedrock/async_invoke/us.twelvelabs.marengo-embed-2-7-v1:0",
input=["Hello world from LiteLLM async invoke async!"],
aws_region_name="us-east-1",
inputType="text",
output_s3_uri="s3://test-bucket/async-invoke-output/",
)
# Validate response structure
assert isinstance(
response, litellm.EmbeddingResponse
), "Response should be EmbeddingResponse type"
assert hasattr(
response, "_hidden_params"
), "Response should have _hidden_params"
assert response._hidden_params is not None, "Hidden params should not be None"
# Validate hidden params contain invocation ARN
assert hasattr(
response._hidden_params, "_invocation_arn"
), "Hidden params should have _invocation_arn"
assert (
response._hidden_params._invocation_arn
== "arn:aws:bedrock:us-east-1:123456789012:async-invoke/test-async-job-456"
), "Invocation ARN should be preserved"
print(
f"Async invoke embedding successful! Invocation ARN: {response._hidden_params._invocation_arn}"
)
# Restore original region name
if original_region_name:
os.environ["AWS_REGION_NAME"] = original_region_name
titan_embedding_response = {"embedding": [0.1, 0.2, 0.3], "inputTextTokenCount": 10}
def test_bedrock_embedding_uses_correct_region_when_specified():
"""
Test that when aws_region_name is explicitly passed, it's used correctly
even if AWS_REGION_NAME env var is set to a different region.
relevant issue: https://github.com/BerriAI/litellm/issues/16517
"""
# Save original env var
original_region_name = os.environ.get("AWS_REGION_NAME")
# Set env var to a different region (this should NOT be used)
os.environ["AWS_REGION_NAME"] = "ap-northeast-1"
try:
client = HTTPHandler()
with patch.object(client, "post") as mock_post:
mock_response = Mock()
mock_response.status_code = 200
mock_response.text = json.dumps(titan_embedding_response)
mock_response.json = lambda: json.loads(mock_response.text)
mock_post.return_value = mock_response
# Call with explicit region
response = litellm.embedding(
model="bedrock/amazon.titan-embed-image-v1",
input=["test input"],
client=client,
aws_region_name="us-east-1", # Explicitly set to us-east-1
)
# Verify the request was made to the correct region
assert mock_post.called, "HTTP post should have been called"
# Get the URL from the call
call_args = mock_post.call_args
url = call_args.kwargs.get("url", "")
# The URL should contain us-east-1, NOT ap-northeast-1
assert "us-east-1" in url, f"URL should contain us-east-1, but got: {url}"
assert "ap-northeast-1" not in url, f"URL should NOT contain ap-northeast-1, but got: {url}"
print(f"✓ Test passed: URL contains correct region: {url}")
finally:
# Restore original env var
if original_region_name:
os.environ["AWS_REGION_NAME"] = original_region_name
else:
os.environ.pop("AWS_REGION_NAME", None)
def test_bedrock_embedding_region_bug_reproduction():
"""
Reproduces the bug where aws_region_name is ignored when passed explicitly.
relevant issue: https://github.com/BerriAI/litellm/issues/16517
"""
# Save original env var
original_region_name = os.environ.get("AWS_REGION_NAME")
# Set env var to ap-northeast-1 (this is what the bug report shows)
os.environ["AWS_REGION_NAME"] = "ap-northeast-1"
try:
client = HTTPHandler()
with patch.object(client, "post") as mock_post:
mock_response = Mock()
mock_response.status_code = 200
mock_response.text = json.dumps(titan_embedding_response)
mock_response.json = lambda: json.loads(mock_response.text)
mock_post.return_value = mock_response
# Call with explicit region (as in the bug report)
response = litellm.embedding(
model="bedrock/amazon.titan-embed-image-v1",
input=["test input"],
client=client,
aws_region_name="us-east-1", # Explicitly set to us-east-1
)
# Verify the request was made
assert mock_post.called, "HTTP post should have been called"
# Get the URL from the call
call_args = mock_post.call_args
url = call_args.kwargs.get("url", "")
print(f"Request URL: {url}")
print(f"Expected region in URL: us-east-1")
print(f"Environment AWS_REGION_NAME: {os.environ.get('AWS_REGION_NAME')}")
# This assertion will FAIL if the bug exists (it will use ap-northeast-1)
# This assertion will PASS if the bug is fixed (it will use us-east-1)
if "ap-northeast-1" in url:
print("❌ BUG REPRODUCED: Using wrong region from env var instead of explicit parameter")
assert False, f"Bug reproduced: URL contains ap-northeast-1 instead of us-east-1. URL: {url}"
else:
print("✓ Bug NOT reproduced: Using correct region from explicit parameter")
assert "us-east-1" in url, f"URL should contain us-east-1, but got: {url}"
finally:
# Restore original env var
if original_region_name:
os.environ["AWS_REGION_NAME"] = original_region_name
else:
os.environ.pop("AWS_REGION_NAME", None)