diff --git a/litellm/completion_extras/litellm_responses_transformation/transformation.py b/litellm/completion_extras/litellm_responses_transformation/transformation.py index a108d367cc7..9ba42ffff58 100644 --- a/litellm/completion_extras/litellm_responses_transformation/transformation.py +++ b/litellm/completion_extras/litellm_responses_transformation/transformation.py @@ -11,6 +11,7 @@ from typing import ( Iterable, Iterator, List, + Literal, Optional, Tuple, Union, @@ -25,6 +26,7 @@ from litellm.llms.base_llm.bridges.completion_transformation import ( ) if TYPE_CHECKING: + from openai.types.responses import ResponseInputImageParam from pydantic import BaseModel from litellm import LiteLLMLoggingObj, ModelResponse @@ -32,6 +34,7 @@ if TYPE_CHECKING: from litellm.types.llms.openai import ( ALL_RESPONSES_API_TOOL_PARAMS, AllMessageValues, + ChatCompletionImageObject, ChatCompletionThinkingBlock, OpenAIMessageContentListBlock, ) @@ -141,10 +144,10 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge): responses_api_request["max_output_tokens"] = value elif key == "tools" and value is not None: # Convert chat completion tools to responses API tools format - responses_api_request[ - "tools" - ] = self._convert_tools_to_responses_format( - cast(List[Dict[str, Any]], value) + responses_api_request["tools"] = ( + self._convert_tools_to_responses_format( + cast(List[Dict[str, Any]], value) + ) ) elif key in ResponsesAPIOptionalRequestParams.__annotations__.keys(): responses_api_request[key] = value # type: ignore @@ -320,6 +323,36 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge): else: return {"type": "output_text", "text": content} + def _convert_content_to_responses_format_image( + self, content: "ChatCompletionImageObject", role: str + ) -> "ResponseInputImageParam": + from openai.types.responses import ResponseInputImageParam + + content_image_url = content.get("image_url") + actual_image_url: Optional[str] = None + detail: Optional[Literal["low", "high", "auto"]] = None + + if isinstance(content_image_url, str): + actual_image_url = content_image_url + elif isinstance(content_image_url, dict): + actual_image_url = content_image_url.get("url") + detail = cast( + Optional[Literal["low", "high", "auto"]], + content_image_url.get("detail"), + ) + + if actual_image_url is None: + raise ValueError(f"Invalid image URL: {content_image_url}") + + image_param = ResponseInputImageParam( + image_url=actual_image_url, detail="auto", type="input_image" + ) + + if detail: + image_param["detail"] = detail + + return image_param + def _convert_content_to_responses_format( self, content: Union[ @@ -331,6 +364,8 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge): role: str, ) -> List[Dict[str, Any]]: """Convert chat completion content to responses API format""" + from litellm.types.llms.openai import ChatCompletionImageObject + verbose_logger.debug( f"Chat provider: Converting content to responses format - input type: {type(content)}" ) @@ -360,10 +395,12 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge): verbose_logger.debug(f"Chat provider: text -> {converted}") elif original_type == "image_url": # Map to responses API image format - converted = { - "type": "input_image", - "image_url": item.get("image_url", {}), - } + converted = cast( + dict, + self._convert_content_to_responses_format_image( + cast(ChatCompletionImageObject, item), role + ), + ) result.append(converted) verbose_logger.debug( f"Chat provider: image_url -> {converted}" diff --git a/litellm/llms/bedrock/base_aws_llm.py b/litellm/llms/bedrock/base_aws_llm.py index 337794f1625..1275ea1595a 100644 --- a/litellm/llms/bedrock/base_aws_llm.py +++ b/litellm/llms/bedrock/base_aws_llm.py @@ -330,9 +330,18 @@ class BaseAWSLLM: and isinstance(standard_aws_region_name, str) ): aws_region_name = standard_aws_region_name - if aws_region_name is None: - aws_region_name = "us-west-2" + try: + import boto3 + + session = boto3.Session() + configured_region = session.region_name + if configured_region: + aws_region_name = configured_region + else: + aws_region_name = "us-west-2" + except Exception: + aws_region_name = "us-west-2" return aws_region_name diff --git a/tests/test_litellm/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py b/tests/test_litellm/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py new file mode 100644 index 00000000000..4d3654ca473 --- /dev/null +++ b/tests/test_litellm/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py @@ -0,0 +1,52 @@ +import datetime +import json +import os +import sys +import unittest +from typing import List, Optional, Tuple +from unittest.mock import ANY, MagicMock, Mock, patch + +import httpx +import pytest + +sys.path.insert( + 0, os.path.abspath("../../..") +) # Adds the parent directory to the system-path +import litellm + + +def test_convert_chat_completion_messages_to_responses_api_image_input(): + from litellm.completion_extras.litellm_responses_transformation.transformation import ( + LiteLLMResponsesTransformationHandler, + ) + + handler = LiteLLMResponsesTransformationHandler() + + user_content = "What's in this image?" + user_image = "https://w7.pngwing.com/pngs/666/274/png-transparent-image-pictures-icon-photo-thumbnail.png" + + messages = [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": user_content, + }, + { + "type": "image_url", + "image_url": {"url": user_image}, + }, + ], + }, + ] + + response, _ = handler.convert_chat_completion_messages_to_responses_api(messages) + + response_str = json.dumps(response) + + assert user_content in response_str + assert user_image in response_str + + print("response: ", response) + assert response[0]["content"][1]["image_url"] == user_image diff --git a/tests/test_litellm/llms/bedrock/test_base_aws_llm.py b/tests/test_litellm/llms/bedrock/test_base_aws_llm.py index 3a2f691c1d6..01282ad8c17 100644 --- a/tests/test_litellm/llms/bedrock/test_base_aws_llm.py +++ b/tests/test_litellm/llms/bedrock/test_base_aws_llm.py @@ -98,3 +98,81 @@ def test_auth_functions_tracer_wrapping(): assert ( has_tracer_wrap ), f"Auth function on line {line_number} is not wrapped with @tracer.wrap: {line.strip()}" + + +def test_get_aws_region_name_boto3_fallback(): + """ + Test the boto3 session fallback logic in _get_aws_region_name method. + + This tests the specific code block that tries to get the region from boto3.Session() + when aws_region_name is None and not found in environment variables. + """ + base_aws_llm = BaseAWSLLM() + + # Test case 1: boto3.Session() returns a configured region + with patch("litellm.llms.bedrock.base_aws_llm.get_secret") as mock_get_secret: + mock_get_secret.return_value = None # No region in env vars + + with patch("boto3.Session") as mock_boto3_session: + mock_session = MagicMock() + mock_session.region_name = "us-east-1" + mock_boto3_session.return_value = mock_session + + optional_params = {} + result = base_aws_llm._get_aws_region_name(optional_params) + + assert result == "us-east-1" + mock_boto3_session.assert_called_once() + + # Test case 2: boto3.Session() returns None for region (should default to us-west-2) + with patch("litellm.llms.bedrock.base_aws_llm.get_secret") as mock_get_secret: + mock_get_secret.return_value = None # No region in env vars + + with patch("boto3.Session") as mock_boto3_session: + mock_session = MagicMock() + mock_session.region_name = None + mock_boto3_session.return_value = mock_session + + optional_params = {} + result = base_aws_llm._get_aws_region_name(optional_params) + + assert result == "us-west-2" + mock_boto3_session.assert_called_once() + + # Test case 3: boto3 import/session creation raises exception (should default to us-west-2) + with patch("litellm.llms.bedrock.base_aws_llm.get_secret") as mock_get_secret: + mock_get_secret.return_value = None # No region in env vars + + with patch("boto3.Session") as mock_boto3_session: + mock_boto3_session.side_effect = Exception("boto3 not available") + + optional_params = {} + result = base_aws_llm._get_aws_region_name(optional_params) + + assert result == "us-west-2" + mock_boto3_session.assert_called_once() + + # Test case 4: aws_region_name is provided in optional_params (should not use boto3) + with patch("boto3.Session") as mock_boto3_session: + optional_params = {"aws_region_name": "eu-west-1"} + result = base_aws_llm._get_aws_region_name(optional_params) + + assert result == "eu-west-1" + mock_boto3_session.assert_not_called() + + # Test case 5: aws_region_name found in environment variables (should not use boto3) + with patch("litellm.llms.bedrock.base_aws_llm.get_secret") as mock_get_secret: + + def side_effect(key, default=None): + if key == "AWS_REGION_NAME": + return "ap-southeast-1" + return default + + mock_get_secret.side_effect = side_effect + + with patch("boto3.Session") as mock_boto3_session: + optional_params = {} + result = base_aws_llm._get_aws_region_name(optional_params) + + assert result == "ap-southeast-1" + mock_boto3_session.assert_not_called()