diff --git a/litellm/litellm_core_utils/prompt_templates/factory.py b/litellm/litellm_core_utils/prompt_templates/factory.py index 93c5013fc6f..615df2cb894 100644 --- a/litellm/litellm_core_utils/prompt_templates/factory.py +++ b/litellm/litellm_core_utils/prompt_templates/factory.py @@ -680,12 +680,13 @@ def convert_generic_image_chunk_to_openai_image_obj( Return: "data:image/jpeg;base64,{base64_image}" """ - return "data:{};{},{}".format( - image_chunk["media_type"], image_chunk["type"], image_chunk["data"] - ) + media_type = image_chunk["media_type"] + return "data:{};{},{}".format(media_type, image_chunk["type"], image_chunk["data"]) -def convert_to_anthropic_image_obj(openai_image_url: str) -> GenericImageParsingChunk: +def convert_to_anthropic_image_obj( + openai_image_url: str, format: Optional[str] +) -> GenericImageParsingChunk: """ Input: "image_url": "data:image/jpeg;base64,{base64_image}", @@ -702,7 +703,11 @@ def convert_to_anthropic_image_obj(openai_image_url: str) -> GenericImageParsing openai_image_url = convert_url_to_base64(url=openai_image_url) # Extract the media type and base64 data media_type, base64_data = openai_image_url.split("data:")[1].split(";base64,") - media_type = media_type.replace("\\/", "/") + + if format: + media_type = format + else: + media_type = media_type.replace("\\/", "/") return GenericImageParsingChunk( type="base64", @@ -820,11 +825,12 @@ def anthropic_messages_pt_xml(messages: list): if isinstance(messages[msg_i]["content"], list): for m in messages[msg_i]["content"]: if m.get("type", "") == "image_url": + format = m["image_url"].get("format") user_content.append( { "type": "image", "source": convert_to_anthropic_image_obj( - m["image_url"]["url"] + m["image_url"]["url"], format=format ), } ) @@ -1156,10 +1162,13 @@ def convert_to_anthropic_tool_result( ) elif content["type"] == "image_url": if isinstance(content["image_url"], str): - image_chunk = convert_to_anthropic_image_obj(content["image_url"]) - else: image_chunk = convert_to_anthropic_image_obj( - content["image_url"]["url"] + content["image_url"], format=None + ) + else: + format = content["image_url"].get("format") + image_chunk = convert_to_anthropic_image_obj( + content["image_url"]["url"], format=format ) anthropic_content_list.append( AnthropicMessagesImageParam( @@ -1318,6 +1327,7 @@ def _anthropic_content_element_factory( data=image_chunk["data"], ), ) + return _anthropic_content_element @@ -1369,13 +1379,16 @@ def anthropic_messages_pt( # noqa: PLR0915 for m in user_message_types_block["content"]: if m.get("type", "") == "image_url": m = cast(ChatCompletionImageObject, m) + format: Optional[str] = None if isinstance(m["image_url"], str): image_chunk = convert_to_anthropic_image_obj( - openai_image_url=m["image_url"] + openai_image_url=m["image_url"], format=None ) else: + format = m["image_url"].get("format") image_chunk = convert_to_anthropic_image_obj( - openai_image_url=m["image_url"]["url"] + openai_image_url=m["image_url"]["url"], + format=format, ) _anthropic_content_element = ( @@ -2303,8 +2316,11 @@ class BedrockImageProcessor: ) @classmethod - def process_image_sync(cls, image_url: str) -> BedrockContentBlock: + def process_image_sync( + cls, image_url: str, format: Optional[str] = None + ) -> BedrockContentBlock: """Synchronous image processing.""" + if "base64" in image_url: img_bytes, mime_type, image_format = cls._parse_base64_image(image_url) elif "http://" in image_url or "https://" in image_url: @@ -2315,11 +2331,17 @@ class BedrockImageProcessor: "Unsupported image type. Expected either image url or base64 encoded string" ) + if format: + mime_type = format + image_format = mime_type.split("/")[1] + image_format = cls._validate_format(mime_type, image_format) return cls._create_bedrock_block(img_bytes, mime_type, image_format) @classmethod - async def process_image_async(cls, image_url: str) -> BedrockContentBlock: + async def process_image_async( + cls, image_url: str, format: Optional[str] + ) -> BedrockContentBlock: """Asynchronous image processing.""" if "base64" in image_url: @@ -2334,6 +2356,10 @@ class BedrockImageProcessor: "Unsupported image type. Expected either image url or base64 encoded string" ) + if format: # override with user-defined params + mime_type = format + image_format = mime_type.split("/")[1] + image_format = cls._validate_format(mime_type, image_format) return cls._create_bedrock_block(img_bytes, mime_type, image_format) @@ -2821,12 +2847,14 @@ class BedrockConverseMessagesProcessor: _part = BedrockContentBlock(text=element["text"]) _parts.append(_part) elif element["type"] == "image_url": + format: Optional[str] = None if isinstance(element["image_url"], dict): image_url = element["image_url"]["url"] + format = element["image_url"].get("format") else: image_url = element["image_url"] _part = await BedrockImageProcessor.process_image_async( # type: ignore - image_url=image_url + image_url=image_url, format=format ) _parts.append(_part) # type: ignore _cache_point_block = ( @@ -3059,12 +3087,15 @@ def _bedrock_converse_messages_pt( # noqa: PLR0915 _part = BedrockContentBlock(text=element["text"]) _parts.append(_part) elif element["type"] == "image_url": + format: Optional[str] = None if isinstance(element["image_url"], dict): image_url = element["image_url"]["url"] + format = element["image_url"].get("format") else: image_url = element["image_url"] _part = BedrockImageProcessor.process_image_sync( # type: ignore - image_url=image_url + image_url=image_url, + format=format, ) _parts.append(_part) # type: ignore _cache_point_block = ( diff --git a/litellm/llms/gemini/chat/transformation.py b/litellm/llms/gemini/chat/transformation.py index 6aa4cf5b52f..fbc1916dcce 100644 --- a/litellm/llms/gemini/chat/transformation.py +++ b/litellm/llms/gemini/chat/transformation.py @@ -114,12 +114,16 @@ class GoogleAIStudioGeminiConfig(VertexGeminiConfig): if element.get("type") == "image_url": img_element = element _image_url: Optional[str] = None + format: Optional[str] = None if isinstance(img_element.get("image_url"), dict): _image_url = img_element["image_url"].get("url") # type: ignore + format = img_element["image_url"].get("format") # type: ignore else: _image_url = img_element.get("image_url") # type: ignore if _image_url and "https://" in _image_url: - image_obj = convert_to_anthropic_image_obj(_image_url) + image_obj = convert_to_anthropic_image_obj( + _image_url, format=format + ) img_element["image_url"] = ( # type: ignore convert_generic_image_chunk_to_openai_image_obj( image_obj diff --git a/litellm/llms/openai/chat/gpt_transformation.py b/litellm/llms/openai/chat/gpt_transformation.py index 13b591e715d..1f34d63681c 100644 --- a/litellm/llms/openai/chat/gpt_transformation.py +++ b/litellm/llms/openai/chat/gpt_transformation.py @@ -20,7 +20,11 @@ from litellm.llms.base_llm.base_model_iterator import BaseModelResponseIterator from litellm.llms.base_llm.base_utils import BaseLLMModelInfo from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException from litellm.secret_managers.main import get_secret_str -from litellm.types.llms.openai import AllMessageValues, ChatCompletionImageObject +from litellm.types.llms.openai import ( + AllMessageValues, + ChatCompletionImageObject, + ChatCompletionImageUrlObject, +) from litellm.types.utils import ModelResponse, ModelResponseStream from litellm.utils import convert_to_model_response_object @@ -189,6 +193,16 @@ class OpenAIGPTConfig(BaseLLMModelInfo, BaseConfig): content_item["image_url"] = { "url": content_item["image_url"], } + elif isinstance(content_item["image_url"], dict): + litellm_specific_params = {"format"} + new_image_url_obj = ChatCompletionImageUrlObject( + **{ # type: ignore + k: v + for k, v in content_item["image_url"].items() + if k not in litellm_specific_params + } + ) + content_item["image_url"] = new_image_url_obj return messages def transform_request( diff --git a/litellm/llms/vertex_ai/gemini/transformation.py b/litellm/llms/vertex_ai/gemini/transformation.py index 8109c8bf612..d6bafc7c60f 100644 --- a/litellm/llms/vertex_ai/gemini/transformation.py +++ b/litellm/llms/vertex_ai/gemini/transformation.py @@ -55,10 +55,11 @@ else: LiteLLMLoggingObj = Any -def _process_gemini_image(image_url: str) -> PartType: +def _process_gemini_image(image_url: str, format: Optional[str] = None) -> PartType: """ Given an image URL, return the appropriate PartType for Gemini """ + try: # GCS URIs if "gs://" in image_url: @@ -66,25 +67,30 @@ def _process_gemini_image(image_url: str) -> PartType: extension_with_dot = os.path.splitext(image_url)[-1] # Ex: ".png" extension = extension_with_dot[1:] # Ex: "png" - file_type = get_file_type_from_extension(extension) + if not format: + file_type = get_file_type_from_extension(extension) - # Validate the file type is supported by Gemini - if not is_gemini_1_5_accepted_file_type(file_type): - raise Exception(f"File type not supported by gemini - {file_type}") + # Validate the file type is supported by Gemini + if not is_gemini_1_5_accepted_file_type(file_type): + raise Exception(f"File type not supported by gemini - {file_type}") - mime_type = get_file_mime_type_for_file_type(file_type) + mime_type = get_file_mime_type_for_file_type(file_type) + else: + mime_type = format file_data = FileDataType(mime_type=mime_type, file_uri=image_url) return PartType(file_data=file_data) elif ( "https://" in image_url - and (image_type := _get_image_mime_type_from_url(image_url)) is not None + and (image_type := format or _get_image_mime_type_from_url(image_url)) + is not None ): + file_data = FileDataType(file_uri=image_url, mime_type=image_type) return PartType(file_data=file_data) elif "http://" in image_url or "https://" in image_url or "base64" in image_url: # https links for unsupported mime types and base64 images - image = convert_to_anthropic_image_obj(image_url) + image = convert_to_anthropic_image_obj(image_url, format=format) _blob = BlobType(data=image["data"], mime_type=image["media_type"]) return PartType(inline_data=_blob) raise Exception("Invalid image received - {}".format(image_url)) @@ -159,11 +165,15 @@ def _gemini_convert_messages_with_history( # noqa: PLR0915 elif element["type"] == "image_url": element = cast(ChatCompletionImageObject, element) img_element = element + format: Optional[str] = None if isinstance(img_element["image_url"], dict): image_url = img_element["image_url"]["url"] + format = img_element["image_url"].get("format") else: image_url = img_element["image_url"] - _part = _process_gemini_image(image_url=image_url) + _part = _process_gemini_image( + image_url=image_url, format=format + ) _parts.append(_part) user_content.extend(_parts) elif ( diff --git a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py index a87b5f3a2af..20f9c9da479 100644 --- a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py +++ b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py @@ -1023,7 +1023,6 @@ class VertexLLM(VertexBase): gemini_api_key: Optional[str] = None, extra_headers: Optional[dict] = None, ) -> Union[ModelResponse, CustomStreamWrapper]: - should_use_v1beta1_features = self.is_using_v1beta1_features( optional_params=optional_params ) diff --git a/litellm/types/llms/openai.py b/litellm/types/llms/openai.py index 6dce7f33566..ee017744354 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -378,6 +378,7 @@ class ChatCompletionTextObject( class ChatCompletionImageUrlObject(TypedDict, total=False): url: Required[str] detail: str + format: str class ChatCompletionImageObject(TypedDict): diff --git a/tests/litellm/log.txt b/tests/litellm/log.txt new file mode 100644 index 00000000000..8d75e0c8009 --- /dev/null +++ b/tests/litellm/log.txt @@ -0,0 +1,28 @@ +============================= test session starts ============================== +platform darwin -- Python 3.11.4, pytest-7.4.1, pluggy-1.2.0 -- /Library/Frameworks/Python.framework/Versions/3.11/bin/python3 +cachedir: .pytest_cache +rootdir: /Users/krrishdholakia/Documents/litellm/tests/litellm +plugins: snapshot-0.9.0, cov-5.0.0, timeout-2.2.0, respx-0.21.1, asyncio-0.21.1, langsmith-0.3.4, anyio-4.8.0, mock-3.11.1, Faker-25.9.2 +asyncio: mode=Mode.STRICT +collecting ... collected 4 items + +test_main.py::test_url_with_format_param[True-gemini/gemini-1.5-flash] PASSED [ 25%] +test_main.py::test_url_with_format_param[True-bedrock/anthropic.claude-3-5-sonnet-20240620-v1:0] PASSED [ 50%] +test_main.py::test_url_with_format_param[False-gemini/gemini-1.5-flash] PASSED [ 75%] +test_main.py::test_url_with_format_param[False-bedrock/anthropic.claude-3-5-sonnet-20240620-v1:0] PASSED [100%] + +=============================== warnings summary =============================== +../../../../../../Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pydantic/_internal/_config.py:295 + /Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pydantic/_internal/_config.py:295: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.10/migration/ + warnings.warn(DEPRECATION_MESSAGE, DeprecationWarning) + +../../litellm/litellm_core_utils/get_model_cost_map.py:24 + /Users/krrishdholakia/Documents/litellm/litellm/litellm_core_utils/get_model_cost_map.py:24: DeprecationWarning: open_text is deprecated. Use files() instead. Refer to https://importlib-resources.readthedocs.io/en/latest/using.html#migrating-from-legacy for migration advice. + with importlib.resources.open_text( + +../../litellm/utils.py:168 + /Users/krrishdholakia/Documents/litellm/litellm/utils.py:168: DeprecationWarning: open_text is deprecated. Use files() instead. Refer to https://importlib-resources.readthedocs.io/en/latest/using.html#migrating-from-legacy for migration advice. + with resources.open_text( + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +======================== 4 passed, 3 warnings in 2.80s ========================= diff --git a/tests/litellm/test_main.py b/tests/litellm/test_main.py index b8384349156..4892300eb95 100644 --- a/tests/litellm/test_main.py +++ b/tests/litellm/test_main.py @@ -117,3 +117,115 @@ def test_completion_missing_role(openai_api_response): ) mock_create.assert_called_once() + + +@pytest.mark.parametrize( + "model", + [ + "gemini/gemini-1.5-flash", + "bedrock/anthropic.claude-3-5-sonnet-20240620-v1:0", + "bedrock/invoke/anthropic.claude-3-5-sonnet-20240620-v1:0", + "anthropic/claude-3-5-sonnet", + ], +) +@pytest.mark.parametrize("sync_mode", [True, False]) +@pytest.mark.asyncio +async def test_url_with_format_param(model, sync_mode): + from litellm import acompletion, completion + from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler + + if sync_mode: + client = HTTPHandler() + else: + client = AsyncHTTPHandler() + + args = { + "model": model, + "messages": [ + { + "role": "user", + "content": [ + { + "type": "image_url", + "image_url": { + "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", + "format": "image/png", + }, + }, + {"type": "text", "text": "Describe this image"}, + ], + } + ], + } + with patch.object(client, "post", new=MagicMock()) as mock_client: + try: + if sync_mode: + response = completion(**args, client=client) + else: + response = await acompletion(**args, client=client) + print(response) + except Exception as e: + print(e) + + mock_client.assert_called() + + print(mock_client.call_args.kwargs) + + if "data" in mock_client.call_args.kwargs: + json_str = mock_client.call_args.kwargs["data"] + else: + json_str = json.dumps(mock_client.call_args.kwargs["json"]) + assert "png" in json_str + assert "jpeg" not in json_str + + +@pytest.mark.parametrize("model", ["gpt-4o-mini"]) +@pytest.mark.parametrize("sync_mode", [True, False]) +@pytest.mark.asyncio +async def test_url_with_format_param_openai(model, sync_mode): + from openai import AsyncOpenAI, OpenAI + + from litellm import acompletion, completion + + if sync_mode: + client = OpenAI() + else: + client = AsyncOpenAI() + + args = { + "model": model, + "messages": [ + { + "role": "user", + "content": [ + { + "type": "image_url", + "image_url": { + "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", + "format": "image/png", + }, + }, + {"type": "text", "text": "Describe this image"}, + ], + } + ], + } + with patch.object( + client.chat.completions.with_raw_response, "create" + ) as mock_client: + try: + if sync_mode: + response = completion(**args, client=client) + else: + response = await acompletion(**args, client=client) + print(response) + except Exception as e: + print(e) + + mock_client.assert_called() + + print(mock_client.call_args.kwargs) + + json_str = json.dumps(mock_client.call_args.kwargs) + + assert "format" not in json_str diff --git a/tests/llm_translation/test_prompt_factory.py b/tests/llm_translation/test_prompt_factory.py index 36c119903e6..514b5f68f31 100644 --- a/tests/llm_translation/test_prompt_factory.py +++ b/tests/llm_translation/test_prompt_factory.py @@ -201,7 +201,7 @@ def test_convert_url_to_img(): ], ) def test_base64_image_input(url, expected_media_type): - response = convert_to_anthropic_image_obj(openai_image_url=url) + response = convert_to_anthropic_image_obj(openai_image_url=url, format=None) assert response["media_type"] == expected_media_type @@ -682,9 +682,9 @@ def test_convert_generic_image_chunk_to_openai_image_obj(): ) url = "https://i.pinimg.com/736x/b4/b1/be/b4b1becad04d03a9071db2817fc9fe77.jpg" - image_obj = convert_to_anthropic_image_obj(url) + image_obj = convert_to_anthropic_image_obj(url, format=None) url_str = convert_generic_image_chunk_to_openai_image_obj(image_obj) - image_obj = convert_to_anthropic_image_obj(url_str) + image_obj = convert_to_anthropic_image_obj(url_str, format=None) print(image_obj) diff --git a/tests/llm_translation/test_vertex.py b/tests/llm_translation/test_vertex.py index 08ea45cb20f..da6fd4e285b 100644 --- a/tests/llm_translation/test_vertex.py +++ b/tests/llm_translation/test_vertex.py @@ -1141,6 +1141,12 @@ def test_process_gemini_image(): mime_type="image/png", file_uri="gs://bucket/image.png" ) + # Test gs url with format specified + gcs_result = _process_gemini_image("gs://bucket/image", format="image/jpeg") + assert gcs_result["file_data"] == FileDataType( + mime_type="image/jpeg", file_uri="gs://bucket/image" + ) + # Test HTTPS JPG URL https_result = _process_gemini_image("https://example.com/image.jpg") print("https_result JPG", https_result) diff --git a/tests/local_testing/test_amazing_vertex_completion.py b/tests/local_testing/test_amazing_vertex_completion.py index 02e0c9b2f14..8595b54f709 100644 --- a/tests/local_testing/test_amazing_vertex_completion.py +++ b/tests/local_testing/test_amazing_vertex_completion.py @@ -3286,3 +3286,47 @@ def test_vertex_anthropic_completion(): assert response.choices[0].message.thinking_blocks is not None assert isinstance(response.choices[0].message.thinking_blocks, list) assert len(response.choices[0].message.thinking_blocks) > 0 + + +def test_signed_s3_url_with_format(): + from litellm import completion + from litellm.llms.custom_httpx.http_handler import HTTPHandler + + client = HTTPHandler() + + load_vertex_ai_credentials() + + args = { + "model": "vertex_ai/gemini-2.0-flash-001", + "messages": [ + { + "role": "user", + "content": [ + { + "type": "image_url", + "image_url": { + "url": "https://litellm-logo-aws-marketplace.s3.us-west-2.amazonaws.com/berriai-logo-github.png?response-content-disposition=inline&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Security-Token=IQoJb3JpZ2luX2VjENj%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLXdlc3QtMiJGMEQCIHlAy6QneghdEo4Dp4rw%2BHhdInKX4MU3T0hZT1qV3AD%2FAiBGY%2FtfxmBJkj%2BK6%2FxAgek6L3tpOcq6su1mBrj87El%2FCirLAwghEAEaDDg4ODYwMjIyMzQyOCIMzds7lsxAFHHCRHmkKqgDgnsJBaEmmwXBWqzyMMe3BUKsCqfvrYupFGxBREP%2BaEz%2ByLSKiTM3xWzaRz6vrP9T4HSJ97B9wQ3dhUBT22XzdOFsaq49wZapwy9hoPNrMyZ77DIa0MlEbg0uudGOaMAw4NbVEqoERQuZmIMMbNHCeoJsZxKCttRZlTDzU%2FeNNy96ltb%2FuIkX5b3OOYdUaKj%2FUjmPz%2FEufY%2Bn%2FFHawunSYXJwL4pYuBF1IKRtPjqamaYscH%2FrzD7fubGUMqk6hvyGEo%2BLqnVyruQEmVFqAnXyWlpHGqeWazEC7xcsC2lhLO%2FKUouyVML%2FxyYtL4CuKp52qtLWWauAFGnyBZnCHtSL58KLaMTSh7inhoFFIKDN2hymrJ4D9%2Bxv%2FMOzefH5X%2B0pcdJUwyxcwgL3myggRmIYq1L6IL4I%2F54BIU%2FMctJcRXQ8NhQNP2PsaCsXYHHVMXRZxps9v8t9Ciorb0PAaLr0DIGVgEqejSjwbzNTctQf59Rj0GhZ0A6A3nFaq3nL4UvO51aPP6aelN6RnLwHh8fF80iPWII7Oj9PWn9bkON%2F7%2B5k42oPFR0KDTD0yaO%2BBjrlAouRvkyHZnCuLuJdEeqc8%2Fwm4W8SbMiYDzIEPPe2wFR2sH4%2FDlnJRqia9Or00d4N%2BOefBkPv%2Bcdt68r%2FwjeWOrulczzLGjJE%2FGw1Lb9dtGtmupGm2XKOW3geJwXkk1qcr7u5zwy6DNamLJbitB026JFKorRnPajhe5axEDv%2BRu6l1f0eailIrCwZ2iytA94Ni8LTha2GbZvX7fFHcmtyNlgJPpMcELdkOEGTCNBldGck5MFHG27xrVrlR%2F7HZIkKYlImNmsOIjuK7acDiangvVdB6GlmVbzNUKtJ7YJhS2ivwvdDIf8XuaFAkhjRNpewDl0GzPvojK%2BDTizZydyJL%2B20pVkSXptyPwrrHEeiOFWwhszW2iTZij4rlRAoZW6NEdfkWsXrGMbxJTZa3E5URejJbg%2B4QgGtjLrgJhRC1pJGP02GX7VMxVWZzomfC2Hn7WaF44wgcuqjE4HGJfpA2ZLBxde52g%3D%3D&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ASIA45ZGR4NCKIUOODV3%2F20250305%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20250305T235823Z&X-Amz-Expires=43200&X-Amz-SignedHeaders=host&X-Amz-Signature=71a900a9467eaf3811553500aaf509a10a9e743a8133cfb6a78dcbcbc6da4a05", + "format": "image/jpeg", + }, + }, + {"type": "text", "text": "Describe this image"}, + ], + } + ], + } + with patch.object(client, "post", new=MagicMock()) as mock_client: + try: + response = completion(**args, client=client) + print(response) + except Exception as e: + print(e) + + print(mock_client.call_args.kwargs) + + mock_client.assert_called() + + print(mock_client.call_args.kwargs) + + json_str = json.dumps(mock_client.call_args.kwargs["json"]) + assert "image/jpeg" in json_str + assert "image/png" not in json_str