fix(github_copilot): flatten list-form content for system messages (fixes #48568)

Signed-off-by: gauravatwork117 <gauravatwork117@gmail.com>
This commit is contained in:
gauravatwork117 2026-10-28 23:05:37 +05:30
parent 3d7b94b34f
commit 0bae2ef755
2 changed files with 126 additions and 12 deletions

View file

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

View file

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