diff --git a/litellm/llms/base_llm/base_utils.py b/litellm/llms/base_llm/base_utils.py index 9e5f12c9b05..d365c942dec 100644 --- a/litellm/llms/base_llm/base_utils.py +++ b/litellm/llms/base_llm/base_utils.py @@ -218,28 +218,44 @@ def map_developer_role_to_system_role( if not any(m["role"] == "developer" for m in messages): return messages - system_message: Optional[Dict[str, Any]] = None - system_contents: List[Any] = [] - non_system_messages: List[AllMessageValues] = [] - for m in messages: - if m["role"] in {"developer", "system"}: - if system_message is None: - system_message = dict(m) - system_message["role"] = "system" - system_contents.append(m["content"]) - else: - non_system_messages.append(m) + new_messages: List[AllMessageValues] = [] + leading_system_message: Optional[Dict[str, Any]] = None + leading_system_contents: List[Any] = [] + idx = 0 + while idx < len(messages) and messages[idx]["role"] in {"developer", "system"}: + m = messages[idx] + if leading_system_message is None: + leading_system_message = dict(m) + leading_system_message["role"] = "system" + leading_system_contents.append(m["content"]) if m["role"] == "developer": - verbose_logger.debug( - "Translating developer role to system role for non-OpenAI providers." - ) # ensure user knows what's happening with their input. + _log_developer_role_translation() + idx += 1 - if system_message is None: - return non_system_messages + if leading_system_message is not None: + leading_system_message["content"] = _merge_system_message_contents( + leading_system_contents + ) + new_messages.append(cast(AllMessageValues, leading_system_message)) - system_message["content"] = _merge_system_message_contents(system_contents) - return [cast(AllMessageValues, system_message), *non_system_messages] + for m in messages[idx:]: + if m["role"] in {"developer", "system"}: + if m["role"] == "developer": + _log_developer_role_translation() + new_messages.append(cast(AllMessageValues, {**m, "role": "system"})) + else: + new_messages.append(m) + continue + + new_messages.append(m) + return new_messages + + +def _log_developer_role_translation() -> None: + verbose_logger.debug( + "Translating developer role to system role for non-OpenAI providers." + ) # ensure user knows what's happening with their input. def _merge_system_message_contents(contents: List[Any]) -> Union[str, List[Any]]: diff --git a/tests/llm_translation/test_base_llm_base_utils.py b/tests/test_litellm/litellm_core_utils/test_base_llm_base_utils.py similarity index 65% rename from tests/llm_translation/test_base_llm_base_utils.py rename to tests/test_litellm/litellm_core_utils/test_base_llm_base_utils.py index a7fa0c353b8..00aa2c95936 100644 --- a/tests/llm_translation/test_base_llm_base_utils.py +++ b/tests/test_litellm/litellm_core_utils/test_base_llm_base_utils.py @@ -13,12 +13,12 @@ def test_map_developer_role_leaves_messages_without_developer_role_unchanged(): assert map_developer_role_to_system_role(messages=messages) is messages -def test_map_developer_role_merges_system_equivalent_messages(): +def test_map_developer_role_merges_leading_system_equivalent_messages(): messages = [ {"role": "system", "content": "Follow the product policy."}, {"role": "developer", "content": "Prefer concise answers."}, - {"role": "user", "content": "Hello!"}, {"role": "system", "content": "Use markdown only when helpful."}, + {"role": "user", "content": "Hello!"}, ] result = map_developer_role_to_system_role(messages=messages) @@ -36,9 +36,11 @@ def test_map_developer_role_merges_system_equivalent_messages(): ] -def test_map_developer_role_preserves_structured_system_content(): +def test_map_developer_role_preserves_structured_leading_system_content(): messages = [ + {"role": "developer", "content": ""}, {"role": "system", "content": [{"type": "text", "text": "System rules."}]}, + {"role": "developer", "content": None}, {"role": "developer", "content": "Developer rules."}, {"role": "user", "content": "Hello!"}, ] @@ -58,6 +60,38 @@ def test_map_developer_role_preserves_structured_system_content(): ] +def test_map_developer_role_converts_later_developer_messages_in_place(): + messages = [ + {"role": "system", "content": "Follow the product policy."}, + {"role": "user", "content": "Hello!"}, + {"role": "developer", "content": "Prefer concise answers."}, + {"role": "assistant", "content": "Hi."}, + ] + + result = map_developer_role_to_system_role(messages=messages) + + assert result == [ + {"role": "system", "content": "Follow the product policy."}, + {"role": "user", "content": "Hello!"}, + {"role": "system", "content": "Prefer concise answers."}, + {"role": "assistant", "content": "Hi."}, + ] + + +def test_map_developer_role_converts_later_developer_without_leading_system(): + messages = [ + {"role": "user", "content": "Hello!"}, + {"role": "developer", "content": "Prefer concise answers."}, + ] + + result = map_developer_role_to_system_role(messages=messages) + + assert result == [ + {"role": "user", "content": "Hello!"}, + {"role": "system", "content": "Prefer concise answers."}, + ] + + def test_responses_instructions_and_developer_input_become_single_system_message(): request = LiteLLMCompletionResponsesConfig.transform_responses_api_request_to_chat_completion_request( model="anthropic/claude-sonnet-4-5",