mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
Merge effc55c8e9 into 587f227b9d
This commit is contained in:
commit
f9330ca5ff
2 changed files with 122 additions and 5 deletions
|
|
@ -14,6 +14,14 @@ from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig
|
|||
from litellm.types.llms.openai import AllMessageValues
|
||||
|
||||
|
||||
def _has_text_only_content(message: AllMessageValues) -> bool:
|
||||
"""Whether the message content is a string, or a list holding text parts only."""
|
||||
content = message.get("content")
|
||||
if not isinstance(content, list):
|
||||
return True
|
||||
return all(isinstance(part, str) or (isinstance(part, dict) and part.get("type") == "text") for part in content)
|
||||
|
||||
|
||||
class SambanovaConfig(OpenAIGPTConfig):
|
||||
"""
|
||||
Reference: https://docs.sambanova.ai/cloud/api-reference/
|
||||
|
|
@ -117,14 +125,22 @@ class SambanovaConfig(OpenAIGPTConfig):
|
|||
"""
|
||||
Transform messages to handle content list conversion.
|
||||
|
||||
SambaNova API doesn't support content as a list - only string content.
|
||||
This converts content lists like [{"type": "text", "text": "..."}] to strings.
|
||||
SambaNova's API takes a string for `content`, and its vision models also take
|
||||
the OpenAI content-list form with `image_url` parts. Flatten only the messages
|
||||
whose list is entirely text: flattening a list that carries an `image_url` (or
|
||||
any other non-text part) drops the attachment, and the model then answers as if
|
||||
nothing was sent - HTTP 200 and no error, so the loss is silent.
|
||||
"""
|
||||
|
||||
def _transform():
|
||||
# handle_messages_with_content_list_to_str_conversion mutates the messages it
|
||||
# is given, so passing the text-only subset converts exactly those.
|
||||
handle_messages_with_content_list_to_str_conversion([m for m in messages if _has_text_only_content(m)])
|
||||
return messages
|
||||
|
||||
async def _async_transform():
|
||||
return handle_messages_with_content_list_to_str_conversion(messages)
|
||||
return _transform()
|
||||
|
||||
if is_async:
|
||||
return _async_transform()
|
||||
messages = handle_messages_with_content_list_to_str_conversion(messages)
|
||||
return messages
|
||||
return _transform()
|
||||
|
|
|
|||
101
tests/test_litellm/llms/sambanova/test_chat.py
Normal file
101
tests/test_litellm/llms/sambanova/test_chat.py
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
"""
|
||||
Unit tests for SambaNova chat message transformation
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from litellm.llms.sambanova.chat import SambanovaConfig
|
||||
|
||||
|
||||
class TestSambanovaNonTextContentParts:
|
||||
"""
|
||||
Content lists that carry a non-text part must keep their list form.
|
||||
"""
|
||||
|
||||
def test_content_list_with_image_is_preserved(self):
|
||||
"""
|
||||
A content list carrying an `image_url` part must NOT be flattened.
|
||||
|
||||
SambaNova's API accepts the OpenAI content-list form with `image_url`, and its
|
||||
vision models read it. Flattening dropped the image while still returning HTTP
|
||||
200, so the model answered as if nothing had been attached.
|
||||
"""
|
||||
config = SambanovaConfig()
|
||||
|
||||
image_part = {
|
||||
"type": "image_url",
|
||||
"image_url": {"url": "data:image/png;base64,iVBORw0KGgo="},
|
||||
}
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{"type": "text", "text": "What colour is this?"}, image_part],
|
||||
}
|
||||
]
|
||||
|
||||
transformed_messages = config._transform_messages(
|
||||
messages=messages, model="sambanova/gemma-4-31B-it", is_async=False
|
||||
)
|
||||
|
||||
content = transformed_messages[0]["content"]
|
||||
assert isinstance(content, list)
|
||||
assert content[0] == {"type": "text", "text": "What colour is this?"}
|
||||
assert content[1] == image_part
|
||||
|
||||
def test_string_content_is_left_alone(self):
|
||||
"""A message whose content is already a string passes through untouched."""
|
||||
config = SambanovaConfig()
|
||||
|
||||
messages = [{"role": "user", "content": "Hello"}]
|
||||
|
||||
transformed_messages = config._transform_messages(
|
||||
messages=messages, model="sambanova/gemma-4-31B-it", is_async=False
|
||||
)
|
||||
|
||||
assert transformed_messages[0]["content"] == "Hello"
|
||||
|
||||
def test_text_only_messages_are_still_flattened_alongside_image_messages(self):
|
||||
"""
|
||||
Mixed conversation: text-only lists are flattened as before, and only the
|
||||
message that carries the image keeps its list form.
|
||||
"""
|
||||
config = SambanovaConfig()
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": [{"type": "text", "text": "Hello"}]},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "And this?"},
|
||||
{"type": "image_url", "image_url": {"url": "https://example.com/a.png"}},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
transformed_messages = config._transform_messages(
|
||||
messages=messages, model="sambanova/gemma-4-31B-it", is_async=False
|
||||
)
|
||||
|
||||
assert transformed_messages[0]["content"] == "Hello"
|
||||
assert isinstance(transformed_messages[1]["content"], list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_transform_preserves_image_content(self):
|
||||
"""The async path must behave like the sync one."""
|
||||
config = SambanovaConfig()
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "What colour is this?"},
|
||||
{"type": "image_url", "image_url": {"url": "https://example.com/a.png"}},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
transformed_messages = await config._transform_messages(
|
||||
messages=messages, model="sambanova/gemma-4-31B-it", is_async=True
|
||||
)
|
||||
|
||||
assert isinstance(transformed_messages[0]["content"], list)
|
||||
Loading…
Add table
Reference in a new issue