From 54892d6a62f736aa4e30d8deeb859f5a870f80ee Mon Sep 17 00:00:00 2001 From: ishaan-j-berri Date: Sat, 31 Oct 2026 10:49:53 -0700 Subject: [PATCH] fix(oci): preserve all Gemini system instructions (#48813) (#49458) --- litellm/llms/oci/chat/transformation.py | 16 +++- .../oci/chat/test_oci_chat_transformation.py | 74 +++++++++++++++++++ 2 files changed, 86 insertions(+), 4 deletions(-) diff --git a/litellm/llms/oci/chat/transformation.py b/litellm/llms/oci/chat/transformation.py index 224674739f5..f97f8f4ca59 100644 --- a/litellm/llms/oci/chat/transformation.py +++ b/litellm/llms/oci/chat/transformation.py @@ -20,7 +20,11 @@ import litellm from litellm._logging import verbose_logger from litellm.constants import DEFAULT_OCI_CHAT_MAX_TOKENS from litellm.litellm_core_utils.logging_utils import track_llm_api_timing -from litellm.litellm_core_utils.prompt_templates.common_utils import messages_with_file_parts_as_image_urls +from litellm.litellm_core_utils.prompt_templates.common_utils import ( + merge_consecutive_system_messages, + messages_with_file_parts_as_image_urls, + system_messages_first, +) from litellm.litellm_core_utils.reasoning_effort_capability import declared_reasoning_efforts_for_model from litellm.litellm_core_utils.reasoning_effort_utils import warn_dropped_reasoning from litellm.litellm_core_utils.streaming_handler import CustomStreamWrapper @@ -693,14 +697,18 @@ class OCIChatConfig(BaseConfig): chatRequest=chat_request, ) else: + generic_messages: Final = list(messages_with_file_parts_as_image_urls(messages)) + normalized_messages: Final = ( + merge_consecutive_system_messages(system_messages_first(generic_messages)) + if model.lower().removeprefix("oci/").startswith("google.gemini") + else generic_messages + ) data = OCICompletionPayload( compartmentId=oci_compartment_id, servingMode=serving_mode, chatRequest=OCIChatRequestPayload( apiFormat=vendor.value, - messages=adapt_messages_to_generic_oci_standard( - list(messages_with_file_parts_as_image_urls(messages)) - ), + messages=adapt_messages_to_generic_oci_standard(normalized_messages), **self._get_optional_params(vendor, optional_params, model), ), ) diff --git a/tests/unit/llms/oci/chat/test_oci_chat_transformation.py b/tests/unit/llms/oci/chat/test_oci_chat_transformation.py index f22b5e32593..5093d01f153 100644 --- a/tests/unit/llms/oci/chat/test_oci_chat_transformation.py +++ b/tests/unit/llms/oci/chat/test_oci_chat_transformation.py @@ -1,4 +1,5 @@ import datetime +import copy import logging from typing import Final import httpx @@ -16,6 +17,8 @@ from litellm.llms.oci.chat.transformation import ( OCIRequestWrapper, version, ) +from litellm.types.llms.oci import OCIChatRequestPayload, OCICompletionPayload +from litellm.types.llms.openai import AllMessageValues TEST_MODEL_NAME = "xai.grok-4" TEST_MODEL = f"oci/{TEST_MODEL_NAME}" @@ -2205,3 +2208,74 @@ def test_transform_request_sends_a_pdf_file_part_as_an_image_data_url() -> None: ], } ] + + +@pytest.mark.parametrize("model", ["google.gemini-2.5-flash", "oci/google.gemini-2.5-flash"]) +@pytest.mark.parametrize("multipart", [False, True]) +@pytest.mark.parametrize("interleaved", [False, True]) +def test_gemini_preserves_all_system_instructions(model: str, multipart: bool, interleaved: bool) -> None: + first: Final = "End every answer with PINEAPPLE." + second: Final = "Start every answer with BANANA." + conversation: Final[list[AllMessageValues]] = [ + {"role": "user", "content": "What is 2+2?"}, + {"role": "assistant", "content": "Four."}, + ] + messages: Final[list[AllMessageValues]] = [ + {"role": "system", "content": first}, + *(conversation if interleaved else []), + {"role": "system", "content": [{"type": "text", "text": second}] if multipart else second}, + *(conversation if not interleaved else []), + {"role": "user", "content": "Repeat it."}, + ] + original: Final = copy.deepcopy(messages) + payload: Final = OCICompletionPayload.model_validate( + OCIChatConfig().transform_request( + model=model, + messages=messages, + optional_params={"oci_compartment_id": TEST_COMPARTMENT_ID}, + litellm_params={}, + headers={}, + ) + ) + chat: Final = payload.chatRequest + assert isinstance(chat, OCIChatRequestPayload) + assert [message.role for message in chat.messages] == ["SYSTEM", "USER", "ASSISTANT", "USER"] + assert chat.messages[0].model_dump(exclude_none=True) == { + "role": "SYSTEM", + "content": ( + [{"type": "TEXT", "text": first}, {"type": "TEXT", "text": second}] + if multipart + else [{"type": "TEXT", "text": f"{first}\n\n{second}"}] + ), + } + assert [message.model_dump(exclude_none=True) for message in chat.messages[1:]] == [ + {"role": "USER", "content": [{"type": "TEXT", "text": "What is 2+2?"}]}, + {"role": "ASSISTANT", "content": [{"type": "TEXT", "text": "Four."}]}, + {"role": "USER", "content": [{"type": "TEXT", "text": "Repeat it."}]}, + ] + assert messages == original + + +@pytest.mark.parametrize("model", ["xai.grok-4", "meta.llama-4-maverick"]) +def test_other_oci_models_preserve_system_message_positions(model: str) -> None: + messages: Final[list[AllMessageValues]] = [ + {"role": "system", "content": "Agent prompt."}, + {"role": "user", "content": "Hello."}, + {"role": "system", "content": "Conversation summary."}, + ] + payload: Final = OCICompletionPayload.model_validate( + OCIChatConfig().transform_request( + model=model, + messages=messages, + optional_params={"oci_compartment_id": TEST_COMPARTMENT_ID}, + litellm_params={}, + headers={}, + ) + ) + chat: Final = payload.chatRequest + assert isinstance(chat, OCIChatRequestPayload) + assert [message.model_dump(exclude_none=True) for message in chat.messages] == [ + {"role": "SYSTEM", "content": [{"type": "TEXT", "text": "Agent prompt."}]}, + {"role": "USER", "content": [{"type": "TEXT", "text": "Hello."}]}, + {"role": "SYSTEM", "content": [{"type": "TEXT", "text": "Conversation summary."}]}, + ]