mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-03 04:22:22 +00:00
fix(moonshot): preserve image_url blocks in multimodal messages
Moonshot's _transform_messages unconditionally flattened content arrays to plain text, dropping image_url blocks. Vision models like kimi-k2.5 accept the standard OpenAI content array format. Now checks for image_url blocks before flattening — if any message contains images the content array is preserved intact. Fixes #20862
This commit is contained in:
@@ -219,6 +219,37 @@ curl http://localhost:4000/v1/chat/completions \
|
||||
|
||||
For more detailed information on using the LiteLLM Proxy, see the [LiteLLM Proxy documentation](../providers/litellm_proxy).
|
||||
|
||||
## Image / Vision Support
|
||||
|
||||
Moonshot vision models (`kimi-k2.5`, `kimi-latest`, `moonshot-v1-*-vision-preview`, etc.) accept the standard OpenAI content array with `image_url` blocks.
|
||||
|
||||
LiteLLM automatically detects when your messages contain images and preserves the content array so the image payload reaches the Moonshot API. For text-only requests the content is flattened to a plain string, as required by Moonshot text models.
|
||||
|
||||
```python showLineNumbers title="Moonshot Vision Example"
|
||||
import os
|
||||
import litellm
|
||||
|
||||
os.environ["MOONSHOT_API_KEY"] = ""
|
||||
|
||||
response = litellm.completion(
|
||||
model="moonshot/kimi-k2.5",
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "What is in this image?"},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": "https://example.com/image.png"},
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
print(response.choices[0].message.content)
|
||||
```
|
||||
|
||||
## Moonshot AI Limitations & LiteLLM Handling
|
||||
|
||||
LiteLLM automatically handles the following [Moonshot AI limitations](https://platform.moonshot.ai/docs/guide/migrating-from-openai-to-kimi#about-api-compatibility) to provide seamless OpenAI compatibility:
|
||||
|
||||
@@ -33,9 +33,25 @@ class MoonshotChatConfig(OpenAIGPTConfig):
|
||||
self, messages: List[AllMessageValues], model: str, is_async: bool = False
|
||||
) -> Union[List[AllMessageValues], Coroutine[Any, Any, List[AllMessageValues]]]:
|
||||
"""
|
||||
Moonshot AI does not support content in list format.
|
||||
Moonshot text-only models don't support content in list format.
|
||||
Multimodal models (kimi-k2.5, kimi-latest, etc.) accept the
|
||||
standard OpenAI content array with non-text blocks (image_url,
|
||||
input_audio, video_url, file, etc.).
|
||||
|
||||
If any message contains a non-text content part, skip flattening
|
||||
so the multimodal payload is preserved.
|
||||
"""
|
||||
messages = handle_messages_with_content_list_to_str_conversion(messages)
|
||||
has_non_text = False
|
||||
for m in messages:
|
||||
_content = m.get("content")
|
||||
if _content and isinstance(_content, list):
|
||||
if any(c.get("type") != "text" for c in _content):
|
||||
has_non_text = True
|
||||
break
|
||||
|
||||
if not has_non_text:
|
||||
messages = handle_messages_with_content_list_to_str_conversion(messages)
|
||||
|
||||
if is_async:
|
||||
return super()._transform_messages(
|
||||
messages=messages, model=model, is_async=True
|
||||
|
||||
@@ -309,4 +309,99 @@ class TestMoonshotConfig:
|
||||
|
||||
# Check that no extra message was added
|
||||
assert len(result["messages"]) == 1
|
||||
assert result["messages"][0]["content"] == "What's the weather?"
|
||||
assert result["messages"][0]["content"] == "What's the weather?"
|
||||
|
||||
def test_transform_messages_preserves_image_url_content(self):
|
||||
"""Test that messages with image_url blocks are NOT flattened to strings.
|
||||
|
||||
Multimodal models like kimi-k2.5 accept the standard OpenAI content
|
||||
array with non-text blocks. When any message contains a non-text part,
|
||||
the content array must be preserved so the payload reaches the API.
|
||||
"""
|
||||
config = MoonshotChatConfig()
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "What is in this image?"},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": "https://example.com/image.png"},
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
result = config.transform_request(
|
||||
model="kimi-k2.5",
|
||||
messages=messages,
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
# Content must remain a list (not flattened to a string)
|
||||
assert isinstance(result["messages"][0]["content"], list)
|
||||
assert len(result["messages"][0]["content"]) == 2
|
||||
assert result["messages"][0]["content"][0]["type"] == "text"
|
||||
assert result["messages"][0]["content"][1]["type"] == "image_url"
|
||||
|
||||
def test_transform_messages_preserves_non_text_content(self):
|
||||
"""Test that any non-text content type (input_audio, video_url, file,
|
||||
etc.) also prevents flattening, matching the OpenAI content spec."""
|
||||
config = MoonshotChatConfig()
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "Transcribe this audio"},
|
||||
{
|
||||
"type": "input_audio",
|
||||
"input_audio": {"data": "base64data", "format": "wav"},
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
result = config.transform_request(
|
||||
model="kimi-k2.5",
|
||||
messages=messages,
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert isinstance(result["messages"][0]["content"], list)
|
||||
assert len(result["messages"][0]["content"]) == 2
|
||||
assert result["messages"][0]["content"][1]["type"] == "input_audio"
|
||||
|
||||
def test_transform_messages_flattens_text_only_content(self):
|
||||
"""Test that text-only content arrays ARE flattened to strings.
|
||||
|
||||
For text-only requests, Moonshot expects plain string content.
|
||||
The content list should be converted to a single string.
|
||||
"""
|
||||
config = MoonshotChatConfig()
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "Hello, how are you?"},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
result = config.transform_request(
|
||||
model="moonshot-v1-8k",
|
||||
messages=messages,
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
# Content should be flattened to a plain string
|
||||
assert isinstance(result["messages"][0]["content"], str)
|
||||
assert result["messages"][0]["content"] == "Hello, how are you?"
|
||||
Reference in New Issue
Block a user