From 1c1a38217b45a109b2747822b9d17ff139bf7bd9 Mon Sep 17 00:00:00 2001 From: siddhantbhagat8 <103000056+siddhantbhagat8@users.noreply.github.com> Date: Tue, 2 Dec 2025 00:08:16 -0800 Subject: [PATCH 1/4] Insert Instructions at the end of System Prompt --- pydantic_ai_slim/pydantic_ai/models/anthropic.py | 2 +- pydantic_ai_slim/pydantic_ai/models/bedrock.py | 2 +- pydantic_ai_slim/pydantic_ai/models/cohere.py | 5 ++++- pydantic_ai_slim/pydantic_ai/models/gemini.py | 2 +- pydantic_ai_slim/pydantic_ai/models/google.py | 2 +- pydantic_ai_slim/pydantic_ai/models/groq.py | 7 ++++++- .../pydantic_ai/models/huggingface.py | 5 ++++- pydantic_ai_slim/pydantic_ai/models/mistral.py | 5 ++++- pydantic_ai_slim/pydantic_ai/models/openai.py | 15 +++++++++++++-- 9 files changed, 35 insertions(+), 10 deletions(-) diff --git a/pydantic_ai_slim/pydantic_ai/models/anthropic.py b/pydantic_ai_slim/pydantic_ai/models/anthropic.py index 9bc04f7619..2796027508 100644 --- a/pydantic_ai_slim/pydantic_ai/models/anthropic.py +++ b/pydantic_ai_slim/pydantic_ai/models/anthropic.py @@ -885,7 +885,7 @@ async def _map_message( # noqa: C901 else: assert_never(m) if instructions := self._get_instructions(messages, model_request_parameters): - system_prompt_parts.insert(0, instructions) + system_prompt_parts.append(instructions) system_prompt = '\n\n'.join(system_prompt_parts) # Add cache_control to the last message content if anthropic_cache_messages is enabled diff --git a/pydantic_ai_slim/pydantic_ai/models/bedrock.py b/pydantic_ai_slim/pydantic_ai/models/bedrock.py index 5589b57d3e..d92ef6741e 100644 --- a/pydantic_ai_slim/pydantic_ai/models/bedrock.py +++ b/pydantic_ai_slim/pydantic_ai/models/bedrock.py @@ -663,7 +663,7 @@ async def _map_messages( # noqa: C901 last_message = cast(dict[str, Any], current_message) if instructions := self._get_instructions(messages, model_request_parameters): - system_prompt.insert(0, {'text': instructions}) + system_prompt.append({'text': instructions}) if system_prompt and settings.get('bedrock_cache_instructions') and profile.bedrock_supports_prompt_caching: system_prompt.append({'cachePoint': {'type': 'default'}}) diff --git a/pydantic_ai_slim/pydantic_ai/models/cohere.py b/pydantic_ai_slim/pydantic_ai/models/cohere.py index 7d5bfeaa11..d2ec037b13 100644 --- a/pydantic_ai_slim/pydantic_ai/models/cohere.py +++ b/pydantic_ai_slim/pydantic_ai/models/cohere.py @@ -235,6 +235,9 @@ def _map_messages( ) -> list[ChatMessageV2]: """Just maps a `pydantic_ai.Message` to a `cohere.ChatMessageV2`.""" cohere_messages: list[ChatMessageV2] = [] + system_prompt_count = sum( + 1 for m in messages if isinstance(m, ModelRequest) for p in m.parts if isinstance(p, SystemPromptPart) + ) for message in messages: if isinstance(message, ModelRequest): cohere_messages.extend(self._map_user_message(message)) @@ -272,7 +275,7 @@ def _map_messages( else: assert_never(message) if instructions := self._get_instructions(messages, model_request_parameters): - cohere_messages.insert(0, SystemChatMessageV2(role='system', content=instructions)) + cohere_messages.insert(system_prompt_count, SystemChatMessageV2(role='system', content=instructions)) return cohere_messages def _get_tools(self, model_request_parameters: ModelRequestParameters) -> list[ToolV2]: diff --git a/pydantic_ai_slim/pydantic_ai/models/gemini.py b/pydantic_ai_slim/pydantic_ai/models/gemini.py index 1e71d16257..7ab6615a0e 100644 --- a/pydantic_ai_slim/pydantic_ai/models/gemini.py +++ b/pydantic_ai_slim/pydantic_ai/models/gemini.py @@ -365,7 +365,7 @@ async def _message_to_gemini_content( else: assert_never(m) if instructions := self._get_instructions(messages, model_request_parameters): - sys_prompt_parts.insert(0, _GeminiTextPart(text=instructions)) + sys_prompt_parts.append(_GeminiTextPart(text=instructions)) return sys_prompt_parts, contents async def _map_user_prompt(self, part: UserPromptPart) -> list[_GeminiPartUnion]: diff --git a/pydantic_ai_slim/pydantic_ai/models/google.py b/pydantic_ai_slim/pydantic_ai/models/google.py index c6f5459f08..2ae3fbbd4d 100644 --- a/pydantic_ai_slim/pydantic_ai/models/google.py +++ b/pydantic_ai_slim/pydantic_ai/models/google.py @@ -598,7 +598,7 @@ async def _map_messages( contents = [{'role': 'user', 'parts': [{'text': ''}]}] if instructions := self._get_instructions(messages, model_request_parameters): - system_parts.insert(0, {'text': instructions}) + system_parts.append({'text': instructions}) system_instruction = ContentDict(role='user', parts=system_parts) if system_parts else None return system_instruction, contents diff --git a/pydantic_ai_slim/pydantic_ai/models/groq.py b/pydantic_ai_slim/pydantic_ai/models/groq.py index d5f70fa451..bd57a5d844 100644 --- a/pydantic_ai_slim/pydantic_ai/models/groq.py +++ b/pydantic_ai_slim/pydantic_ai/models/groq.py @@ -398,6 +398,9 @@ def _map_messages( ) -> list[chat.ChatCompletionMessageParam]: """Just maps a `pydantic_ai.Message` to a `groq.types.ChatCompletionMessageParam`.""" groq_messages: list[chat.ChatCompletionMessageParam] = [] + system_prompt_count = sum( + 1 for m in messages if isinstance(m, ModelRequest) for p in m.parts if isinstance(p, SystemPromptPart) + ) for message in messages: if isinstance(message, ModelRequest): groq_messages.extend(self._map_user_message(message)) @@ -431,7 +434,9 @@ def _map_messages( else: assert_never(message) if instructions := self._get_instructions(messages, model_request_parameters): - groq_messages.insert(0, chat.ChatCompletionSystemMessageParam(role='system', content=instructions)) + groq_messages.insert( + system_prompt_count, chat.ChatCompletionSystemMessageParam(role='system', content=instructions) + ) return groq_messages @staticmethod diff --git a/pydantic_ai_slim/pydantic_ai/models/huggingface.py b/pydantic_ai_slim/pydantic_ai/models/huggingface.py index ab6652dbb4..4e6a6a90bb 100644 --- a/pydantic_ai_slim/pydantic_ai/models/huggingface.py +++ b/pydantic_ai_slim/pydantic_ai/models/huggingface.py @@ -334,6 +334,9 @@ async def _map_messages( ) -> list[ChatCompletionInputMessage | ChatCompletionOutputMessage]: """Just maps a `pydantic_ai.Message` to a `huggingface_hub.ChatCompletionInputMessage`.""" hf_messages: list[ChatCompletionInputMessage | ChatCompletionOutputMessage] = [] + system_prompt_count = sum( + 1 for m in messages if isinstance(m, ModelRequest) for p in m.parts if isinstance(p, SystemPromptPart) + ) for message in messages: if isinstance(message, ModelRequest): async for item in self._map_user_message(message): @@ -368,7 +371,7 @@ async def _map_messages( else: assert_never(message) if instructions := self._get_instructions(messages, model_request_parameters): - hf_messages.insert(0, ChatCompletionInputMessage(content=instructions, role='system')) # type: ignore + hf_messages.insert(system_prompt_count, ChatCompletionInputMessage(content=instructions, role='system')) # type: ignore return hf_messages @staticmethod diff --git a/pydantic_ai_slim/pydantic_ai/models/mistral.py b/pydantic_ai_slim/pydantic_ai/models/mistral.py index 01fee32a25..e29c44be22 100644 --- a/pydantic_ai_slim/pydantic_ai/models/mistral.py +++ b/pydantic_ai_slim/pydantic_ai/models/mistral.py @@ -530,6 +530,9 @@ def _map_messages( ) -> list[MistralMessages]: """Just maps a `pydantic_ai.Message` to a `MistralMessage`.""" mistral_messages: list[MistralMessages] = [] + system_prompt_count = sum( + 1 for m in messages if isinstance(m, ModelRequest) for p in m.parts if isinstance(p, SystemPromptPart) + ) for message in messages: if isinstance(message, ModelRequest): mistral_messages.extend(self._map_user_message(message)) @@ -559,7 +562,7 @@ def _map_messages( else: assert_never(message) if instructions := self._get_instructions(messages, model_request_parameters): - mistral_messages.insert(0, MistralSystemMessage(content=instructions)) + mistral_messages.insert(system_prompt_count, MistralSystemMessage(content=instructions)) # Post-process messages to insert fake assistant message after tool message if followed by user message # to work around `Unexpected role 'user' after role 'tool'` error. diff --git a/pydantic_ai_slim/pydantic_ai/models/openai.py b/pydantic_ai_slim/pydantic_ai/models/openai.py index efe9629c3a..a3a459e0e6 100644 --- a/pydantic_ai_slim/pydantic_ai/models/openai.py +++ b/pydantic_ai_slim/pydantic_ai/models/openai.py @@ -881,7 +881,11 @@ async def _map_messages( ) -> list[chat.ChatCompletionMessageParam]: """Just maps a `pydantic_ai.Message` to a `openai.types.ChatCompletionMessageParam`.""" openai_messages: list[chat.ChatCompletionMessageParam] = [] + system_prompt_count = 0 for message in messages: + for part in message.parts: + if isinstance(part, SystemPromptPart): + system_prompt_count += 1 if isinstance(message, ModelRequest): async for item in self._map_user_message(message): openai_messages.append(item) @@ -890,7 +894,9 @@ async def _map_messages( else: assert_never(message) if instructions := self._get_instructions(messages, model_request_parameters): - openai_messages.insert(0, chat.ChatCompletionSystemMessageParam(content=instructions, role='system')) + openai_messages.insert( + system_prompt_count, chat.ChatCompletionSystemMessageParam(content=instructions, role='system') + ) return openai_messages @staticmethod @@ -1369,7 +1375,12 @@ async def _responses_create( # noqa: C901 # > Response input messages must contain the word 'json' in some form to use 'text.format' of type 'json_object'. # Apparently they're only checking input messages for "JSON", not instructions. assert isinstance(instructions, str) - openai_messages.insert(0, responses.EasyInputMessageParam(role='system', content=instructions)) + system_prompt_count = sum( + 1 for m in messages if isinstance(m, ModelRequest) for p in m.parts if isinstance(p, SystemPromptPart) + ) + openai_messages.insert( + system_prompt_count, responses.EasyInputMessageParam(role='system', content=instructions) + ) instructions = OMIT if verbosity := model_settings.get('openai_text_verbosity'): From c0568ec9c19c76f9b2b6c18308a7392c456a2615 Mon Sep 17 00:00:00 2001 From: siddhantbhagat8 <103000056+siddhantbhagat8@users.noreply.github.com> Date: Wed, 3 Dec 2025 00:22:32 -0800 Subject: [PATCH 2/4] find the right index in the provider messages list and not pydantic ai ones --- pydantic_ai_slim/pydantic_ai/models/cohere.py | 4 +--- pydantic_ai_slim/pydantic_ai/models/groq.py | 4 +--- pydantic_ai_slim/pydantic_ai/models/huggingface.py | 4 +--- pydantic_ai_slim/pydantic_ai/models/mistral.py | 4 +--- pydantic_ai_slim/pydantic_ai/models/openai.py | 9 ++------- 5 files changed, 6 insertions(+), 19 deletions(-) diff --git a/pydantic_ai_slim/pydantic_ai/models/cohere.py b/pydantic_ai_slim/pydantic_ai/models/cohere.py index d2ec037b13..95e2ebba29 100644 --- a/pydantic_ai_slim/pydantic_ai/models/cohere.py +++ b/pydantic_ai_slim/pydantic_ai/models/cohere.py @@ -235,9 +235,6 @@ def _map_messages( ) -> list[ChatMessageV2]: """Just maps a `pydantic_ai.Message` to a `cohere.ChatMessageV2`.""" cohere_messages: list[ChatMessageV2] = [] - system_prompt_count = sum( - 1 for m in messages if isinstance(m, ModelRequest) for p in m.parts if isinstance(p, SystemPromptPart) - ) for message in messages: if isinstance(message, ModelRequest): cohere_messages.extend(self._map_user_message(message)) @@ -275,6 +272,7 @@ def _map_messages( else: assert_never(message) if instructions := self._get_instructions(messages, model_request_parameters): + system_prompt_count = sum(1 for m in cohere_messages if isinstance(m, SystemChatMessageV2)) cohere_messages.insert(system_prompt_count, SystemChatMessageV2(role='system', content=instructions)) return cohere_messages diff --git a/pydantic_ai_slim/pydantic_ai/models/groq.py b/pydantic_ai_slim/pydantic_ai/models/groq.py index bd57a5d844..422cdb40ce 100644 --- a/pydantic_ai_slim/pydantic_ai/models/groq.py +++ b/pydantic_ai_slim/pydantic_ai/models/groq.py @@ -398,9 +398,6 @@ def _map_messages( ) -> list[chat.ChatCompletionMessageParam]: """Just maps a `pydantic_ai.Message` to a `groq.types.ChatCompletionMessageParam`.""" groq_messages: list[chat.ChatCompletionMessageParam] = [] - system_prompt_count = sum( - 1 for m in messages if isinstance(m, ModelRequest) for p in m.parts if isinstance(p, SystemPromptPart) - ) for message in messages: if isinstance(message, ModelRequest): groq_messages.extend(self._map_user_message(message)) @@ -434,6 +431,7 @@ def _map_messages( else: assert_never(message) if instructions := self._get_instructions(messages, model_request_parameters): + system_prompt_count = sum(1 for m in groq_messages if m.get('role') == 'system') groq_messages.insert( system_prompt_count, chat.ChatCompletionSystemMessageParam(role='system', content=instructions) ) diff --git a/pydantic_ai_slim/pydantic_ai/models/huggingface.py b/pydantic_ai_slim/pydantic_ai/models/huggingface.py index 4e6a6a90bb..fc2c4859e6 100644 --- a/pydantic_ai_slim/pydantic_ai/models/huggingface.py +++ b/pydantic_ai_slim/pydantic_ai/models/huggingface.py @@ -334,9 +334,6 @@ async def _map_messages( ) -> list[ChatCompletionInputMessage | ChatCompletionOutputMessage]: """Just maps a `pydantic_ai.Message` to a `huggingface_hub.ChatCompletionInputMessage`.""" hf_messages: list[ChatCompletionInputMessage | ChatCompletionOutputMessage] = [] - system_prompt_count = sum( - 1 for m in messages if isinstance(m, ModelRequest) for p in m.parts if isinstance(p, SystemPromptPart) - ) for message in messages: if isinstance(message, ModelRequest): async for item in self._map_user_message(message): @@ -371,6 +368,7 @@ async def _map_messages( else: assert_never(message) if instructions := self._get_instructions(messages, model_request_parameters): + system_prompt_count = sum(1 for m in hf_messages if getattr(m, 'role', None) == 'system') hf_messages.insert(system_prompt_count, ChatCompletionInputMessage(content=instructions, role='system')) # type: ignore return hf_messages diff --git a/pydantic_ai_slim/pydantic_ai/models/mistral.py b/pydantic_ai_slim/pydantic_ai/models/mistral.py index e29c44be22..afda4854ab 100644 --- a/pydantic_ai_slim/pydantic_ai/models/mistral.py +++ b/pydantic_ai_slim/pydantic_ai/models/mistral.py @@ -530,9 +530,6 @@ def _map_messages( ) -> list[MistralMessages]: """Just maps a `pydantic_ai.Message` to a `MistralMessage`.""" mistral_messages: list[MistralMessages] = [] - system_prompt_count = sum( - 1 for m in messages if isinstance(m, ModelRequest) for p in m.parts if isinstance(p, SystemPromptPart) - ) for message in messages: if isinstance(message, ModelRequest): mistral_messages.extend(self._map_user_message(message)) @@ -562,6 +559,7 @@ def _map_messages( else: assert_never(message) if instructions := self._get_instructions(messages, model_request_parameters): + system_prompt_count = sum(1 for m in mistral_messages if isinstance(m, MistralSystemMessage)) mistral_messages.insert(system_prompt_count, MistralSystemMessage(content=instructions)) # Post-process messages to insert fake assistant message after tool message if followed by user message diff --git a/pydantic_ai_slim/pydantic_ai/models/openai.py b/pydantic_ai_slim/pydantic_ai/models/openai.py index a3a459e0e6..0d3ef00cce 100644 --- a/pydantic_ai_slim/pydantic_ai/models/openai.py +++ b/pydantic_ai_slim/pydantic_ai/models/openai.py @@ -881,11 +881,7 @@ async def _map_messages( ) -> list[chat.ChatCompletionMessageParam]: """Just maps a `pydantic_ai.Message` to a `openai.types.ChatCompletionMessageParam`.""" openai_messages: list[chat.ChatCompletionMessageParam] = [] - system_prompt_count = 0 for message in messages: - for part in message.parts: - if isinstance(part, SystemPromptPart): - system_prompt_count += 1 if isinstance(message, ModelRequest): async for item in self._map_user_message(message): openai_messages.append(item) @@ -894,6 +890,7 @@ async def _map_messages( else: assert_never(message) if instructions := self._get_instructions(messages, model_request_parameters): + system_prompt_count = sum(1 for m in openai_messages if m.get('role') == 'system') openai_messages.insert( system_prompt_count, chat.ChatCompletionSystemMessageParam(content=instructions, role='system') ) @@ -1375,9 +1372,7 @@ async def _responses_create( # noqa: C901 # > Response input messages must contain the word 'json' in some form to use 'text.format' of type 'json_object'. # Apparently they're only checking input messages for "JSON", not instructions. assert isinstance(instructions, str) - system_prompt_count = sum( - 1 for m in messages if isinstance(m, ModelRequest) for p in m.parts if isinstance(p, SystemPromptPart) - ) + system_prompt_count = sum(1 for m in openai_messages if m.get('role') == 'system') openai_messages.insert( system_prompt_count, responses.EasyInputMessageParam(role='system', content=instructions) ) From b6144bba91c49d81f0a34c2a09d8572c4bec833e Mon Sep 17 00:00:00 2001 From: siddhantbhagat8 <103000056+siddhantbhagat8@users.noreply.github.com> Date: Sun, 14 Dec 2025 23:49:55 -0800 Subject: [PATCH 3/4] add tests for the big 3 using _map_messages --- tests/models/test_anthropic.py | 28 ++++++++++++++++++ tests/models/test_google.py | 33 +++++++++++++++++++++ tests/models/test_openai.py | 25 ++++++++++++++++ tests/models/test_openai_responses.py | 42 +++++++++++++++++++++++++++ 4 files changed, 128 insertions(+) diff --git a/tests/models/test_anthropic.py b/tests/models/test_anthropic.py index 0e1230a7a5..81a256c4e5 100644 --- a/tests/models/test_anthropic.py +++ b/tests/models/test_anthropic.py @@ -8065,3 +8065,31 @@ async def test_anthropic_container_id_from_stream_response(allow_model_requests: assert model_response.provider_details is not None assert model_response.provider_details.get('container_id') == 'container_from_stream' assert model_response.provider_details.get('finish_reason') == 'end_turn' +async def test_anthropic_system_prompts_and_instructions_ordering(): + """Test that instructions are appended after all system prompts in the system prompt string.""" + m = AnthropicModel('claude-sonnet-4-5', provider=AnthropicProvider(api_key='test-key')) + + messages: list[ModelRequest | ModelResponse] = [ + ModelRequest( + parts=[ + SystemPromptPart(content='System prompt 1'), + SystemPromptPart(content='System prompt 2'), + UserPromptPart(content='Hello'), + ], + instructions='Instructions content', + ), + ] + + model_request_parameters = ModelRequestParameters( + function_tools=[], + builtin_tools=[], + output_tools=[], + ) + + system_prompt, anthropic_messages = await m._map_message(messages, model_request_parameters, {}) # pyright: ignore[reportPrivateUsage] + + # Verify system prompts and instructions are joined in order: system1, system2, instructions + assert system_prompt == 'System prompt 1\n\nSystem prompt 2\n\nInstructions content' + # Verify user message is in anthropic_messages + assert len(anthropic_messages) == 1 + assert anthropic_messages[0]['role'] == 'user' diff --git a/tests/models/test_google.py b/tests/models/test_google.py index be6d4bd68a..0eb048a5f4 100644 --- a/tests/models/test_google.py +++ b/tests/models/test_google.py @@ -29,6 +29,7 @@ FunctionToolCallEvent, FunctionToolResultEvent, ImageUrl, + ModelMessage, ModelRequest, ModelResponse, PartDeltaEvent, @@ -4610,3 +4611,35 @@ def get_country() -> str: ), ] ) +async def test_google_system_prompts_and_instructions_ordering(google_provider: GoogleProvider): + """Test that instructions are appended after all system prompts in the system instruction.""" + m = GoogleModel('gemini-2.0-flash', provider=google_provider) + + messages: list[ModelMessage] = [ + ModelRequest( + parts=[ + SystemPromptPart(content='System prompt 1'), + SystemPromptPart(content='System prompt 2'), + UserPromptPart(content='Hello'), + ], + instructions='Instructions content', + ), + ] + + model_request_parameters = ModelRequestParameters( + function_tools=[], + builtin_tools=[], + output_tools=[], + ) + + system_instruction, contents = await m._map_messages(messages, model_request_parameters) # pyright: ignore[reportPrivateUsage] + + # Verify system parts are in order: system1, system2, instructions + assert system_instruction + assert system_instruction == snapshot( + { + 'role': 'user', + 'parts': [{'text': 'System prompt 1'}, {'text': 'System prompt 2'}, {'text': 'Instructions content'}], + } + ) + assert contents == snapshot([{'role': 'user', 'parts': [{'text': 'Hello'}]}]) diff --git a/tests/models/test_openai.py b/tests/models/test_openai.py index 74cb3c1414..58a7a22791 100644 --- a/tests/models/test_openai.py +++ b/tests/models/test_openai.py @@ -3325,3 +3325,28 @@ async def test_openai_reasoning_in_thinking_tags(allow_model_requests: None): """, } ) +async def test_openai_chat_instructions_after_system_prompts(allow_model_requests: None): + """Test that instructions are inserted after all system prompts in mapped messages.""" + mock_client = MockOpenAI.create_mock(completion_message(ChatCompletionMessage(content='ok', role='assistant'))) + model = OpenAIChatModel('gpt-4o', provider=OpenAIProvider(openai_client=mock_client)) + + messages: list[ModelRequest | ModelResponse] = [ + ModelRequest( + parts=[ + SystemPromptPart(content='System prompt 1'), + SystemPromptPart(content='System prompt 2'), + UserPromptPart(content='Hello'), + ], + instructions='Instructions content', + ), + ] + + openai_messages = await model._map_messages(messages, ModelRequestParameters()) # pyright: ignore[reportPrivateUsage] + + # Verify order: system1, system2, instructions, user + assert len(openai_messages) == 4 + print(f'openai_messages: {openai_messages}') + assert openai_messages[0] == {'role': 'system', 'content': 'System prompt 1'} + assert openai_messages[1] == {'role': 'system', 'content': 'System prompt 2'} + assert openai_messages[2] == {'role': 'system', 'content': 'Instructions content'} + assert openai_messages[3]['role'] == 'user' diff --git a/tests/models/test_openai_responses.py b/tests/models/test_openai_responses.py index b03e99bb91..f2d9ab0e7c 100644 --- a/tests/models/test_openai_responses.py +++ b/tests/models/test_openai_responses.py @@ -24,6 +24,7 @@ PartEndEvent, PartStartEvent, RetryPromptPart, + SystemPromptPart, TextPart, TextPartDelta, ThinkingPart, @@ -8207,3 +8208,44 @@ async def test_web_search_call_action_find_in_page(allow_model_requests: None): 'type': 'web_search_call', } ) +async def test_openai_responses_system_prompts_ordering(allow_model_requests: None): + """Test that system prompts are correctly ordered in mapped messages.""" + c = response_message( + [ + ResponseOutputMessage( + id='msg_123', + content=cast(list[Content], [ResponseOutputText(text='ok', type='output_text', annotations=[])]), + role='assistant', + status='completed', + type='message', + ), + ], + ) + mock_client = MockOpenAIResponses.create_mock(c) + model = OpenAIResponsesModel('gpt-4o', provider=OpenAIProvider(openai_client=mock_client)) + + messages: list[ModelRequest | ModelResponse] = [ + ModelRequest( + parts=[ + SystemPromptPart(content='System prompt 1'), + SystemPromptPart(content='System prompt 2'), + UserPromptPart(content='Hello'), + ], + instructions='Instructions content', + ), + ] + + instructions, openai_messages = await model._map_messages( # type: ignore[reportPrivateUsage] + messages, + model_settings=cast(OpenAIResponsesModelSettings, {}), + model_request_parameters=ModelRequestParameters(), + ) + + # Verify instructions are returned separately + assert instructions == 'Instructions content' + + # Verify system prompts are in order, followed by user message + assert len(openai_messages) == 3 + assert openai_messages[0] == {'role': 'system', 'content': 'System prompt 1'} + assert openai_messages[1] == {'role': 'system', 'content': 'System prompt 2'} + assert openai_messages[2].get('role') == 'user' From 3ff17879989f072cdb25185d987390600b9bd55d Mon Sep 17 00:00:00 2001 From: siddhantbhagat8 <103000056+siddhantbhagat8@users.noreply.github.com> Date: Mon, 15 Dec 2025 00:01:51 -0800 Subject: [PATCH 4/4] format --- tests/models/test_anthropic.py | 2 ++ tests/models/test_google.py | 2 ++ tests/models/test_openai.py | 2 ++ tests/models/test_openai_responses.py | 2 ++ 4 files changed, 8 insertions(+) diff --git a/tests/models/test_anthropic.py b/tests/models/test_anthropic.py index 81a256c4e5..aba39a34cc 100644 --- a/tests/models/test_anthropic.py +++ b/tests/models/test_anthropic.py @@ -8065,6 +8065,8 @@ async def test_anthropic_container_id_from_stream_response(allow_model_requests: assert model_response.provider_details is not None assert model_response.provider_details.get('container_id') == 'container_from_stream' assert model_response.provider_details.get('finish_reason') == 'end_turn' + + async def test_anthropic_system_prompts_and_instructions_ordering(): """Test that instructions are appended after all system prompts in the system prompt string.""" m = AnthropicModel('claude-sonnet-4-5', provider=AnthropicProvider(api_key='test-key')) diff --git a/tests/models/test_google.py b/tests/models/test_google.py index 0eb048a5f4..dc5522d226 100644 --- a/tests/models/test_google.py +++ b/tests/models/test_google.py @@ -4611,6 +4611,8 @@ def get_country() -> str: ), ] ) + + async def test_google_system_prompts_and_instructions_ordering(google_provider: GoogleProvider): """Test that instructions are appended after all system prompts in the system instruction.""" m = GoogleModel('gemini-2.0-flash', provider=google_provider) diff --git a/tests/models/test_openai.py b/tests/models/test_openai.py index 58a7a22791..1d4858e884 100644 --- a/tests/models/test_openai.py +++ b/tests/models/test_openai.py @@ -3325,6 +3325,8 @@ async def test_openai_reasoning_in_thinking_tags(allow_model_requests: None): """, } ) + + async def test_openai_chat_instructions_after_system_prompts(allow_model_requests: None): """Test that instructions are inserted after all system prompts in mapped messages.""" mock_client = MockOpenAI.create_mock(completion_message(ChatCompletionMessage(content='ok', role='assistant'))) diff --git a/tests/models/test_openai_responses.py b/tests/models/test_openai_responses.py index f2d9ab0e7c..3a7c7e90a1 100644 --- a/tests/models/test_openai_responses.py +++ b/tests/models/test_openai_responses.py @@ -8208,6 +8208,8 @@ async def test_web_search_call_action_find_in_page(allow_model_requests: None): 'type': 'web_search_call', } ) + + async def test_openai_responses_system_prompts_ordering(allow_model_requests: None): """Test that system prompts are correctly ordered in mapped messages.""" c = response_message(