mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-06 07:48:06 +00:00
fix(github_copilot): flatten list-form content for system messages (fixes #48568)
Signed-off-by: gauravatwork117 <gauravatwork117@gmail.com>
This commit is contained in:
parent
3d7b94b34f
commit
0bae2ef755
2 changed files with 126 additions and 12 deletions
|
|
@ -73,6 +73,38 @@ class GithubCopilotConfig(OpenAIConfig):
|
|||
) -> tuple[str | None, str | None, str]:
|
||||
return self._get_openai_compatible_provider_info(model, api_base, api_key, custom_llm_provider)
|
||||
|
||||
@staticmethod
|
||||
def _flatten_text_content_blocks(
|
||||
content: object,
|
||||
) -> tuple[str, object | None] | None:
|
||||
"""
|
||||
If content is a list of text-only blocks, returns (flattened_text, cache_control).
|
||||
Leaves multimodal or non-text content (e.g. image_url) untouched by returning None.
|
||||
"""
|
||||
if not isinstance(content, list):
|
||||
return None
|
||||
|
||||
extracted_texts: list[str] = []
|
||||
cache_control: object | None = None
|
||||
|
||||
for item in content:
|
||||
if isinstance(item, str):
|
||||
extracted_texts.append(item)
|
||||
elif isinstance(item, dict):
|
||||
item_type = item.get("type")
|
||||
if item_type == "text" or (item_type is None and "text" in item):
|
||||
text_val = item.get("text")
|
||||
if isinstance(text_val, str):
|
||||
extracted_texts.append(text_val)
|
||||
if cache_control is None and "cache_control" in item:
|
||||
cache_control = item.get("cache_control")
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
return "".join(extracted_texts), cache_control
|
||||
|
||||
def _transform_messages(
|
||||
self,
|
||||
messages,
|
||||
|
|
@ -80,20 +112,34 @@ class GithubCopilotConfig(OpenAIConfig):
|
|||
):
|
||||
import litellm
|
||||
|
||||
# Check if system-to-assistant conversion is disabled
|
||||
if litellm.disable_copilot_system_to_assistant:
|
||||
# GitHub Copilot API now supports system prompts for all models (Claude, GPT, etc.)
|
||||
# No conversion needed - just return messages as-is
|
||||
return messages
|
||||
|
||||
# Default behavior: convert system messages to assistant for compatibility
|
||||
disable_conversion: Final = bool(litellm.disable_copilot_system_to_assistant)
|
||||
transformed_messages: Final = []
|
||||
for message in messages:
|
||||
if message.get("role") == "system":
|
||||
# Convert system message to assistant message
|
||||
transformed_messages.append(
|
||||
cast(AllMessageValues, {**message, "role": "assistant"}) # cast-ok: dict matches the union
|
||||
)
|
||||
role = message.get("role")
|
||||
if role == "system":
|
||||
target_role = "system" if disable_conversion else "assistant"
|
||||
flattened = self._flatten_text_content_blocks(message.get("content"))
|
||||
if flattened is not None:
|
||||
flattened_content, cache_control = flattened
|
||||
transformed_message = cast(
|
||||
AllMessageValues,
|
||||
{
|
||||
**message,
|
||||
"role": target_role,
|
||||
"content": flattened_content,
|
||||
**(
|
||||
{"cache_control": cache_control}
|
||||
if cache_control is not None and "cache_control" not in message
|
||||
else {}
|
||||
),
|
||||
},
|
||||
)
|
||||
else:
|
||||
transformed_message = cast(
|
||||
AllMessageValues,
|
||||
{**message, "role": target_role},
|
||||
)
|
||||
transformed_messages.append(transformed_message)
|
||||
else:
|
||||
transformed_messages.append(message)
|
||||
|
||||
|
|
|
|||
|
|
@ -178,6 +178,74 @@ def test_transform_messages_disable_copilot_system_to_assistant(monkeypatch):
|
|||
litellm.disable_copilot_system_to_assistant = original_flag
|
||||
|
||||
|
||||
def test_transform_messages_flattens_text_only_system_content_list():
|
||||
"""Test that text-only content lists on system messages are flattened to a single string (fixes #48568)."""
|
||||
import litellm
|
||||
from litellm.llms.github_copilot.chat.transformation import GithubCopilotConfig
|
||||
|
||||
config = GithubCopilotConfig()
|
||||
original_flag = litellm.disable_copilot_system_to_assistant
|
||||
try:
|
||||
# Case 1: disable_copilot_system_to_assistant = True (role remains system)
|
||||
litellm.disable_copilot_system_to_assistant = True
|
||||
messages = [
|
||||
{"role": "system", "content": [{"type": "text", "text": "You are a helpful assistant."}]},
|
||||
{"role": "user", "content": "What is 2+2?"},
|
||||
]
|
||||
out = config.transform_messages(messages, model="github_copilot/claude-sonnet-5")
|
||||
assert out[0]["role"] == "system"
|
||||
assert out[0]["content"] == "You are a helpful assistant."
|
||||
assert out[1]["role"] == "user"
|
||||
|
||||
# Case 2: disable_copilot_system_to_assistant = False (role converted to assistant)
|
||||
litellm.disable_copilot_system_to_assistant = False
|
||||
out = config.transform_messages(messages, model="github_copilot/claude-sonnet-5")
|
||||
assert out[0]["role"] == "assistant"
|
||||
assert out[0]["content"] == "You are a helpful assistant."
|
||||
assert out[1]["role"] == "user"
|
||||
finally:
|
||||
litellm.disable_copilot_system_to_assistant = original_flag
|
||||
|
||||
|
||||
def test_transform_messages_flattens_multiple_text_blocks_and_preserves_cache_control():
|
||||
"""Test multiple text blocks are joined and explicit cache_control is promoted to message level."""
|
||||
from litellm.llms.github_copilot.chat.transformation import GithubCopilotConfig
|
||||
|
||||
config = GithubCopilotConfig()
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": [
|
||||
{"type": "text", "text": "Part 1. "},
|
||||
{"type": "text", "text": "Part 2.", "cache_control": {"type": "ephemeral"}},
|
||||
],
|
||||
},
|
||||
{"role": "user", "content": "Hello"},
|
||||
]
|
||||
out = config.transform_messages(messages, model="github_copilot/claude-sonnet-5")
|
||||
assert out[0]["content"] == "Part 1. Part 2."
|
||||
assert out[0]["cache_control"] == {"type": "ephemeral"}
|
||||
|
||||
|
||||
def test_transform_messages_leaves_non_text_content_blocks_intact():
|
||||
"""Test non-text blocks (such as image_url) are left untouched as a list."""
|
||||
from litellm.llms.github_copilot.chat.transformation import GithubCopilotConfig
|
||||
|
||||
config = GithubCopilotConfig()
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": [
|
||||
{"type": "text", "text": "Describe image:"},
|
||||
{"type": "image_url", "image_url": {"url": "https://example.com/test.png"}},
|
||||
],
|
||||
},
|
||||
]
|
||||
out = config.transform_messages(messages, model="github_copilot/claude-sonnet-5")
|
||||
assert isinstance(out[0]["content"], list)
|
||||
assert len(out[0]["content"]) == 2
|
||||
|
||||
|
||||
def test_x_initiator_header_user_request():
|
||||
"""Test that user-only messages result in X-Initiator: user header"""
|
||||
config = GithubCopilotConfig()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue