This commit is contained in:
Miyar 2026-08-27 11:18:30 +09:00 committed by GitHub
commit f9330ca5ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 122 additions and 5 deletions

View file

@ -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()

View 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)