mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-26 11:14:04 +00:00
Merge 0a1bebd5c8
into f5996b2f6b
This commit is contained in:
commit
a4c3f7cca5
4 changed files with 302 additions and 2 deletions
|
@ -844,6 +844,21 @@ def exception_type( # type: ignore # noqa: PLR0915
|
||||||
llm_provider="bedrock",
|
llm_provider="bedrock",
|
||||||
response=getattr(original_exception, "response", None),
|
response=getattr(original_exception, "response", None),
|
||||||
)
|
)
|
||||||
|
elif (
|
||||||
|
"Client error '400 Bad Request'" in error_str
|
||||||
|
and (
|
||||||
|
"files are not supported" in error_str
|
||||||
|
or "Unsupported image format" in error_str
|
||||||
|
or "Supported formats are:" in error_str
|
||||||
|
)
|
||||||
|
):
|
||||||
|
exception_mapping_worked = True
|
||||||
|
raise BadRequestError(
|
||||||
|
message=f"BedrockException - {error_str}",
|
||||||
|
model=model,
|
||||||
|
llm_provider="bedrock",
|
||||||
|
response=getattr(original_exception, "response", None),
|
||||||
|
)
|
||||||
elif (
|
elif (
|
||||||
"Unable to locate credentials" in error_str
|
"Unable to locate credentials" in error_str
|
||||||
or "The security token included in the request is invalid"
|
or "The security token included in the request is invalid"
|
||||||
|
|
|
@ -2352,8 +2352,9 @@ class BedrockImageProcessor:
|
||||||
]
|
]
|
||||||
|
|
||||||
if not valid_extensions:
|
if not valid_extensions:
|
||||||
|
document_type = mime_type.split('/')[-1].upper() # e.g., "PDF" from "application/pdf"
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"No supported extensions for MIME type: {mime_type}. Supported formats: {supported_doc_formats}"
|
f"Client error '400 Bad Request': {document_type} files are not supported. Supported formats are: {supported_doc_formats}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Use first valid extension instead of provided image_format
|
# Use first valid extension instead of provided image_format
|
||||||
|
@ -2361,7 +2362,7 @@ class BedrockImageProcessor:
|
||||||
else:
|
else:
|
||||||
if image_format not in supported_image_formats:
|
if image_format not in supported_image_formats:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Unsupported image format: {image_format}. Supported formats: {supported_image_formats}"
|
f"Client error '400 Bad Request': Unsupported image format: {image_format}. Supported formats are: {supported_image_formats}"
|
||||||
)
|
)
|
||||||
return image_format
|
return image_format
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,194 @@
|
||||||
|
import pytest
|
||||||
|
import requests
|
||||||
|
import base64
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Set up logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PROXY_URL = "http://0.0.0.0:4000/chat/completions"
|
||||||
|
|
||||||
|
def create_base64_pdf():
|
||||||
|
"""Create a minimal valid PDF in base64 format"""
|
||||||
|
minimal_pdf = b"%PDF-1.0\n1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj\n2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj\n3 0 obj<</Type/Page/MediaBox[0 0 3 3]>>endobj\nxref\n0 4\n0000000000 65535 f\n0000000010 00000 n\n0000000053 00000 n\n0000000102 00000 n\ntrailer<</Size 4/Root 1 0 R>>\nstartxref\n149\n%EOF"
|
||||||
|
return base64.b64encode(minimal_pdf).decode()
|
||||||
|
|
||||||
|
def create_base64_png():
|
||||||
|
"""Create a simple PNG in base64 format (red dot)"""
|
||||||
|
return "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="
|
||||||
|
|
||||||
|
def create_base64_jpeg():
|
||||||
|
"""Create a simple JPEG in base64 format (white pixel)"""
|
||||||
|
return "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////2wBDAf//////////////////////////////////////////////////////////////////////////////////////wAARCAABAAEDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3+iiigD//2Q=="
|
||||||
|
|
||||||
|
def test_pdf_content_raises_400():
|
||||||
|
"""Test that sending PDF content raises a 400 error with correct message"""
|
||||||
|
pdf_base64 = create_base64_pdf()
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"model": "claude-3",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "image_url",
|
||||||
|
"image_url": {
|
||||||
|
"url": f"data:application/pdf;base64,{pdf_base64}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "What's in this document?"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
PROXY_URL,
|
||||||
|
json=payload,
|
||||||
|
headers={"Content-Type": "application/json"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
response_data = response.json()
|
||||||
|
|
||||||
|
logger.info(f"Error response for PDF test: {response_data}")
|
||||||
|
|
||||||
|
error_message = response_data["error"]["message"]
|
||||||
|
assert "litellm.BadRequestError: AnthropicException" in error_message
|
||||||
|
assert "claude-3-haiku-20240307" in error_message
|
||||||
|
assert "does not support PDF input" in error_message
|
||||||
|
|
||||||
|
def test_png_content_succeeds():
|
||||||
|
"""Test that sending PNG content works successfully"""
|
||||||
|
png_base64 = create_base64_png()
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"model": "claude-3",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "image_url",
|
||||||
|
"image_url": {
|
||||||
|
"url": f"data:image/png;base64,{png_base64}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "What's in this image?"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
PROXY_URL,
|
||||||
|
json=payload,
|
||||||
|
headers={"Content-Type": "application/json"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
response_data = response.json()
|
||||||
|
logger.info(f"Successful PNG response status: {response.status_code}")
|
||||||
|
|
||||||
|
# Check response has expected structure
|
||||||
|
assert "choices" in response_data
|
||||||
|
assert len(response_data["choices"]) > 0
|
||||||
|
assert "message" in response_data["choices"][0]
|
||||||
|
assert "content" in response_data["choices"][0]["message"]
|
||||||
|
|
||||||
|
def test_jpeg_content_succeeds():
|
||||||
|
"""Test that sending JPEG content works successfully"""
|
||||||
|
jpeg_base64 = create_base64_jpeg()
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"model": "claude-3",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "image_url",
|
||||||
|
"image_url": {
|
||||||
|
"url": f"data:image/jpeg;base64,{jpeg_base64}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "What's in this image?"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
PROXY_URL,
|
||||||
|
json=payload,
|
||||||
|
headers={"Content-Type": "application/json"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
response_data = response.json()
|
||||||
|
logger.info(f"Successful JPEG response status: {response.status_code}")
|
||||||
|
|
||||||
|
# Check response has expected structure
|
||||||
|
assert "choices" in response_data
|
||||||
|
assert len(response_data["choices"]) > 0
|
||||||
|
assert "message" in response_data["choices"][0]
|
||||||
|
assert "content" in response_data["choices"][0]["message"]
|
||||||
|
|
||||||
|
def test_multiple_images_succeeds():
|
||||||
|
"""Test that sending multiple supported images in one request works"""
|
||||||
|
png_base64 = create_base64_png()
|
||||||
|
jpeg_base64 = create_base64_jpeg()
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"model": "claude-3",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "image_url",
|
||||||
|
"image_url": {
|
||||||
|
"url": f"data:image/png;base64,{png_base64}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "image_url",
|
||||||
|
"image_url": {
|
||||||
|
"url": f"data:image/jpeg;base64,{jpeg_base64}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "What's in these images?"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
PROXY_URL,
|
||||||
|
json=payload,
|
||||||
|
headers={"Content-Type": "application/json"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
response_data = response.json()
|
||||||
|
logger.info(f"Successful multiple images response status: {response.status_code}")
|
||||||
|
|
||||||
|
# Check response has expected structure
|
||||||
|
assert "choices" in response_data
|
||||||
|
assert len(response_data["choices"]) > 0
|
||||||
|
assert "message" in response_data["choices"][0]
|
||||||
|
assert "content" in response_data["choices"][0]["message"]
|
|
@ -0,0 +1,90 @@
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
import mimetypes
|
||||||
|
from litellm.exceptions import BadRequestError
|
||||||
|
from litellm.litellm_core_utils.exception_mapping_utils import exception_type
|
||||||
|
from litellm.litellm_core_utils.prompt_templates.factory import BedrockImageProcessor
|
||||||
|
|
||||||
|
# Mock the configurations and classes we need
|
||||||
|
class MockAmazonConfig:
|
||||||
|
def get_supported_image_types(self):
|
||||||
|
return ['png', 'jpeg', 'gif', 'webp']
|
||||||
|
|
||||||
|
def get_supported_document_types(self):
|
||||||
|
return ['pdf', 'docx']
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_mapping_ValueError_to_BadRequest():
|
||||||
|
"""Test that ValueError gets mapped to BadRequestError with correct message"""
|
||||||
|
print("\n--- Running test_error_mapping_ValueError_to_BadRequest ---")
|
||||||
|
try:
|
||||||
|
# Simulate the ValueError being raised
|
||||||
|
error_message = "Client error '400 Bad Request': PDF files are not supported. Supported formats are: ['docx']"
|
||||||
|
print(f"Raising ValueError with message: {error_message}")
|
||||||
|
|
||||||
|
raise ValueError(error_message)
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"Caught ValueError: {e}")
|
||||||
|
|
||||||
|
with pytest.raises(BadRequestError) as exc_info:
|
||||||
|
print("Calling exception_type...")
|
||||||
|
exception_type(
|
||||||
|
model="model-name",
|
||||||
|
original_exception=e,
|
||||||
|
custom_llm_provider="bedrock"
|
||||||
|
)
|
||||||
|
|
||||||
|
error = exc_info.value
|
||||||
|
print(f"Caught BadRequestError: {error}")
|
||||||
|
print(f"Error status code: {error.status_code}")
|
||||||
|
print(f"Error message: {str(error)}")
|
||||||
|
|
||||||
|
assert error.status_code == 400
|
||||||
|
assert "PDF files are not supported" in str(error)
|
||||||
|
print("Assertions passed successfully!")
|
||||||
|
|
||||||
|
def test_error_mapping_413_error():
|
||||||
|
"""Test that 413 status code gets mapped correctly"""
|
||||||
|
print("\n--- Running test_error_mapping_413_error ---")
|
||||||
|
mock_exception = Mock()
|
||||||
|
mock_exception.status_code = 413
|
||||||
|
mock_exception.message = "File too large"
|
||||||
|
|
||||||
|
print(f"Mock exception status code: {mock_exception.status_code}")
|
||||||
|
print(f"Mock exception message: {mock_exception.message}")
|
||||||
|
|
||||||
|
with pytest.raises(BadRequestError) as exc_info:
|
||||||
|
exception_type(
|
||||||
|
model="model-name",
|
||||||
|
original_exception=mock_exception,
|
||||||
|
custom_llm_provider="replicate"
|
||||||
|
)
|
||||||
|
|
||||||
|
error = exc_info.value
|
||||||
|
print(f"Caught BadRequestError: {error}")
|
||||||
|
print(f"Error status code: {error.status_code}")
|
||||||
|
print(f"Error message: {str(error)}")
|
||||||
|
|
||||||
|
assert error.status_code == 400
|
||||||
|
assert "ReplicateException" in str(error)
|
||||||
|
assert "File too large" in str(error)
|
||||||
|
print("Assertions passed successfully!")
|
||||||
|
|
||||||
|
def test_validation_edge_cases():
|
||||||
|
"""Test edge cases in format validation"""
|
||||||
|
print("\n--- Running test_validation_edge_cases ---")
|
||||||
|
with patch('litellm.AmazonConverseConfig', return_value=MockAmazonConfig()):
|
||||||
|
# Test empty mime type
|
||||||
|
print("Testing empty mime type")
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
BedrockImageProcessor._validate_format("", "")
|
||||||
|
print(f"Caught ValueError: {exc_info.value}")
|
||||||
|
assert "Client error '400 Bad Request'" in str(exc_info.value)
|
||||||
|
|
||||||
|
# Test invalid mime type
|
||||||
|
print("Testing invalid mime type")
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
BedrockImageProcessor._validate_format("invalid/type", "format")
|
||||||
|
print(f"Caught ValueError: {exc_info.value}")
|
||||||
|
assert "Client error '400 Bad Request'" in str(exc_info.value)
|
||||||
|
print("Assertions passed successfully!")
|
Loading…
Add table
Add a link
Reference in a new issue