mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
Completion-To-Responses Bridge: Support passing image url's (#11833)
* fix(completion_to_responses_bridge_transformation.py): support passing image urls' to responses api models Fixes https://github.com/BerriAI/litellm/issues/11820 * fix(base_aws_llm.py): if boto3 present, try and get the configured region name Closes https://github.com/BerriAI/litellm/issues/8847 * fix: fix imports * fix: fix linting error s
This commit is contained in:
parent
0319adbf5d
commit
e5fd313a48
4 changed files with 186 additions and 10 deletions
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue