From d6d584f2d799bd4ccd00eb73b35542035d10ae84 Mon Sep 17 00:00:00 2001 From: Vineeth Sai Date: Mon, 26 Oct 2026 15:08:27 -0400 Subject: [PATCH] fix(cloudflare): flatten content-part message arrays to a string Cloudflare Workers AI rejects message content that is an array, so requests using OpenAI content parts failed with a type mismatch. Flatten them in _transform_messages, the way the other string-only providers do. --- .../llms/cloudflare/chat/transformation.py | 33 +++++++++- .../test_cloudflare_transformation.py | 66 +++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/litellm/llms/cloudflare/chat/transformation.py b/litellm/llms/cloudflare/chat/transformation.py index 2bbfd2010cc..b32215320c9 100644 --- a/litellm/llms/cloudflare/chat/transformation.py +++ b/litellm/llms/cloudflare/chat/transformation.py @@ -1,8 +1,12 @@ -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 ( + 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 +91,33 @@ 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 + flattened: Final = 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 3a45c83556c..1b52e6a717d 100644 --- a/tests/unit/llms/cloudflare/test_cloudflare_transformation.py +++ b/tests/unit/llms/cloudflare/test_cloudflare_transformation.py @@ -205,6 +205,72 @@ 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_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",