diff --git a/litellm/llms/cloudflare/chat/transformation.py b/litellm/llms/cloudflare/chat/transformation.py index 2bbfd2010cc..acc7609a36c 100644 --- a/litellm/llms/cloudflare/chat/transformation.py +++ b/litellm/llms/cloudflare/chat/transformation.py @@ -1,8 +1,13 @@ -from typing import Final +from collections.abc import Coroutine +from typing import Final, Literal, overload import httpx from litellm._logging import verbose_logger +from litellm.litellm_core_utils.prompt_templates.common_utils import ( + audio_or_image_in_message_content, + handle_messages_with_content_list_to_str_conversion, +) from litellm.litellm_core_utils.url_utils import join_api_base from litellm.llms.base_llm.chat.transformation import BaseLLMException from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig @@ -87,6 +92,35 @@ class CloudflareChatConfig(OpenAIGPTConfig): api_base=api_base, ) + @overload + def _transform_messages( + self, + messages: list[AllMessageValues], # mutable-ok: base signature + model: str, + is_async: Literal[True], + ) -> Coroutine[object, object, list[AllMessageValues]]: ... # mutable-ok: base signature + + @overload + def _transform_messages( + self, + messages: list[AllMessageValues], # mutable-ok: base signature + model: str, + is_async: Literal[False] = False, + ) -> list[AllMessageValues]: ... # mutable-ok: base signature + + def _transform_messages( + self, + messages: list[AllMessageValues], # mutable-ok: base signature + model: str, + is_async: bool = False, + ) -> list[AllMessageValues] | Coroutine[object, object, list[AllMessageValues]]: # mutable-ok: base signature + # Flattening keeps only the text, so skip it when any message carries an image or audio part + has_media: Final = any(audio_or_image_in_message_content(message) for message in messages) + flattened: Final = messages if has_media else handle_messages_with_content_list_to_str_conversion(messages) + if is_async: + return super()._transform_messages(messages=flattened, model=model, is_async=True) + return super()._transform_messages(messages=flattened, model=model, is_async=False) + def get_error_class(self, error_message: str, status_code: int, headers: dict | httpx.Headers) -> BaseLLMException: return CloudflareError( status_code=status_code, diff --git a/tests/unit/llms/cloudflare/test_cloudflare_transformation.py b/tests/unit/llms/cloudflare/test_cloudflare_transformation.py index ccbfc02f0e6..867074dfe90 100644 --- a/tests/unit/llms/cloudflare/test_cloudflare_transformation.py +++ b/tests/unit/llms/cloudflare/test_cloudflare_transformation.py @@ -205,6 +205,90 @@ def test_validate_environment_sets_bearer_and_content_type(): assert headers["Content-Type"] == "application/json" +def test_transform_request_flattens_content_part_arrays_to_string(): + config = CloudflareChatConfig() + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "Hello"}, + {"type": "text", "text": "More text"}, + ], + } + ] + + body = config.transform_request( + model="@cf/meta/llama-2-7b-chat-int8", + messages=messages, + optional_params={}, + litellm_params={}, + headers={}, + ) + + assert body["messages"][0]["content"] == "HelloMore text" + + +def test_transform_request_leaves_string_content_untouched(): + config = CloudflareChatConfig() + messages = [ + {"role": "system", "content": "you are terse"}, + {"role": "user", "content": "weather in nyc?"}, + ] + + body = config.transform_request( + model="@cf/meta/llama-2-7b-chat-int8", + messages=messages, + optional_params={}, + litellm_params={}, + headers={}, + ) + + assert [m["content"] for m in body["messages"]] == ["you are terse", "weather in nyc?"] + + +def test_transform_request_does_not_flatten_when_a_part_is_not_text(): + config = CloudflareChatConfig() + parts = [ + {"type": "text", "text": "What is in this picture?"}, + {"type": "image_url", "image_url": {"url": "https://example.com/cat.png"}}, + ] + + body = config.transform_request( + model="@cf/meta/llama-3.2-11b-vision-instruct", + messages=[{"role": "user", "content": parts}], + optional_params={}, + litellm_params={}, + headers={}, + ) + + assert body["messages"][0]["content"] == parts + + +def test_async_transform_request_flattens_content_part_arrays_to_string(): + config = CloudflareChatConfig() + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "Hello"}, + {"type": "text", "text": "More text"}, + ], + } + ] + + body = asyncio.run( + config.async_transform_request( + model="@cf/meta/llama-2-7b-chat-int8", + messages=messages, + optional_params={}, + litellm_params={}, + headers={}, + ) + ) + + assert body["messages"][0]["content"] == "HelloMore text" + + def _chat_response() -> dict[str, object]: return { "id": "chatcmpl-cf",